
我刷力扣有个习惯碰到题目先不看题解自己硬啃实在卡住再翻讨论区。这种方式经常让我在 Easy 题上翻车1417. Reformat The String重新格式化字符串就是最典型的一例。这道题的标签是 Easy我却在上面折腾了快 100 分钟不是题目本身多难而是我一开始就走错了方向试图在原字符串上通过交换相邻字符来“修正”序列结果把简单的计数题变成了一堆 if-else 的泥潭。最后 AC 的代码不到二十行核心思路用一句话就能说清把字母和数字分别统计数量只要两者数量差的绝对值不超过 1就一定能重新格式化否则直接返回空字符串。这篇文章把我踩坑、重写、补边界测试的完整过程整理下来给同样在做 LeetCode 热门题、周赛前热身或者想彻底搞懂这道题的朋友一个参考。1. 我为什么会在 Easy 题上翻车原地交换的直觉陷阱1.1 第一版代码想用双指针救场越救越乱刚看到题目时我的直觉是原字符串里如果出现相邻两个同类那我就拿一个异类跟它们交换。比如字符串aabb1后面有数字可以把第二个b和1交换变成aab1b结果还是存在相邻的aa于是还要继续交换。这种思路注定会产生连锁反应因为交换一个字符后它两侧的关系可能又被破坏需要反复扫描整个字符串。为了处理“交换后可能又出现新的相邻同类”的问题我加了一个 while 循环反复扫描又把“已经交换过”的位置记下来写了一堆标记变量。本地测试示例能过但提交后要么效率难看要么逻辑根本说不清。我当时 debug 了大概 40 多分钟越改越乱代码长度从十几行膨胀到五十多行还是不敢保证所有输入都能正确输出。停下来复盘才发现这个方向从根上就是错的。字符串只有两类字符相邻冲突只可能出现在“字母-字母”或“数字-数字”之间但交换一个字符会同时影响它前面和后面的两对关系。用一个局部交换去修复全局约束本质上没有利用“只需要统计数量”这个全局信息。等我把这个想法写进草稿纸才发现真正的问题模型根本不需要双指针。1.2 换成一个直观模型两类球穿插排队我把自己从代码里拉出来重新用数学的角度想。既然只关心“类型是否不同”可以把所有字母看成红色球所有数字看成蓝色球。字符串重排的问题就变成了把若干红球和蓝球排成一列要求相邻球的颜色不同。这个模型一出来整个问题的核心就变了。我不需要关心原字符串的顺序不需要关心具体字符是谁只需要知道红球有几个、蓝球有几个。如果红球有 3 个、蓝球有 3 个那可以排成红蓝红蓝红蓝或者蓝红蓝红蓝红如果红球有 3 个、蓝球有 2 个只能让红球在开头和结尾排成红蓝红蓝红。我在草稿纸上画了 10 个小球立刻发现核心规律两类球数量差超过 1 时无论如何都无法做到完全交替因为数量多的那一类在“两端”也只能占两个位置中间一旦有空隙必然会有两个同类相邻。这个认知让我意识到前面 40 多分钟全部浪费在了一个错误的方向上。整道题的本质不是“怎么交换”而是“能不能排”以及“谁开头”。2. 正确思路拆解分类统计 交替摆放2.1 第一步一遍扫描把字符分成两桶解法其实非常朴素遍历字符串一次用isdigit()判断当前字符是数字还是字母分别放入digits和letters两个容器。这里有个容易忽略的点题目只要求“相邻字符类型不同”并不要求保持原字符串中字符的相对顺序。所以digits和letters内部的具体顺序都不重要我们想怎么放就怎么放。很多新手会下意识认为要基于原序遍历来重排其实完全不是。只要构造出的结果满足相邻类型不同任何顺序都算正确答案。用 C 写大概是string digits, letters; for (char c : s) { if (isdigit(c)) digits.push_back(c); else letters.push_back(c); }这一步的时间复杂度是 O(n)空间复杂度也是 O(n)。对于这道题字符串长度最大只有 500 的约束来说已经非常宽裕。2.2 充要条件两种球数量差最多为 1现在进入最关键的判断环节。什么时候能排什么时候不能排用一个生活化的场景来解释男生和女生排队要求必须一男一女相间排列。如果男生有 5 个、女生有 3 个无论怎么排至少会有两个男生相邻。因为队伍最多只有 4 个“间隔位置”能靠女生隔开他们5 个男生占了 5 个坑必然有 2 个坑之间没有女生。反过来只要男生最多比女生多 1 个就一定能让他们完美交错。因为当男生比女生多 1 个时可以排成男 女 男 女 ... 男以男生开头、男生结尾中间女生恰好把男生两两隔开。所以结论就是abs(数字数量 - 字母数量) 1如果这个条件不满足直接返回空字符串。这个条件不只是充分条件也是必要条件。严格一点说假设字母数量 a 大于等于数字数量 b。如果排成合法序列那么序列中字母要么出现在偶数位要么出现在奇数位。由于相邻位置不能同类型序列必须完全交替因此字母数量最多比数字数量多 1。如果 a - b 1那么即使把字母全部放在偶数位偶数位的数量也不足以容纳 a 个字母必然有字母落到奇数位形成相邻。所以abs(d - l) 1时一定无解。2.3 构造阶段为什么“多的类型”要放偶数位确定了有解之后下一步就是构造结果。这里有一个细节值得单独拎出来说数量多的那一类要放在偶数位下标 0, 2, 4 …。为什么因为当总量为奇数时偶数位的数量恰好比奇数位的数量多 1。比如总长度为 5偶数位有 3 个奇数位有 2 个。如果字母比数字多 1 个字母就必须占据那 3 个偶数位数字占据 2 个奇数位这样刚好排满。反过来如果字母被放在奇数位只有 2 个位置多余的 1 个字母会无处可放或者被迫与另一个字母相邻。即使字母和数字数量相同谁放偶数位都无所谓都能构造出合法结果。实现上不需要真的处理“偶数位”这个概念只需要保证先取数量多的集合再取数量少的集合交替拼接即可。因为交替拼接天然就是偶数位放第一类、奇数位放第二类。伪代码逻辑如下如果数字数量 字母数量first指向数字集合second指向字母集合否则反过来循环里先 push 一个first的字符再 push 一个second的字符当second取完只剩first有剩余时再 push 一个first就结束。这样构造出来的字符串一定满足相邻字符类型不同。3. 两版可运行代码C 与 Python 的写法差异3.1 C 实现引用别名让代码更干净这里给出我最终提交的 C 版本带注释#include cctype #include string using namespace std; class Solution { public: string reformat(string s) { string digits, letters; for (char c : s) { if (isdigit(c)) digits.push_back(c); else letters.push_back(c); } int d digits.size(); int l letters.size(); if (abs(d - l) 1) return ; // first 指向数量多的集合second 指向数量少的集合 string first (d l) ? digits : letters; string second (d l) ? letters : digits; string ans; int i 0, j 0; while (i first.size() || j second.size()) { if (i first.size()) ans.push_back(first[i]); if (j second.size()) ans.push_back(second[j]); } return ans; } };几个值得注意的细节isdigit()需要包含cctype头文件。力扣的编译环境通常会默认包含一些常用头文件但本地编译时最好显式加上避免意外报错。abs(d - l)中d和l都是int所以不会有问题。如果直接用digits.size() - letters.size()这个表达式的结果是无符号数当letters更长时会变成一个巨大的正数导致判断出错。这是 C 里最常见的坑之一后面我会专门展开。我用string引用别名来统一处理“谁先谁后”的逻辑这样就不需要写两个几乎一样的分支。如果不用引用就得写成if (d l) { // 数字在前 } else { // 字母在前 }代码会明显冗余。3.2 Python 实现列表收集是标准答案Python 版本更简洁class Solution: def reformat(self, s: str) - str: digits [c for c in s if c.isdigit()] letters [c for c in s if c.isalpha()] if abs(len(digits) - len(letters)) 1: return # 让 digits 成为数量多的集合 if len(digits) len(letters): digits, letters letters, digits res [] for i in range(len(digits)): res.append(digits[i]) if i len(letters): res.append(letters[i]) return .join(res)Python 的优势在于字符串不可变所以用列表收集字符再join是最自然的写法。交换两个列表直接digits, letters letters, digits一行搞定比 C 的操作看起来直观很多。需要注意的是isdigit()和isalpha()的语义在 Python 中更宽泛isalpha()对中文字符也返回True但 LeetCode 的输入明确只包含小写字母和数字所以这里完全安全。如果是在通用场景下处理任意字符串可能需要额外限定 ASCII 范围但这道题不需要担心。3.3 两种语言的易错点对比我把自己在两种语言里踩过的坑整理成了表格语言易错点处理方式Cdigits.size() - letters.size()因为 size_t 无符号导致溢出先转int再计算或直接用int变量接收size()返回值C忘了包含cctype本地编译时显式添加头文件C用while (i first.size() j second.size())漏掉最后一个字符改成 Pythonisalpha()语义宽泛题目只含小写字母无影响通用场景下按需限定Python字符串不可变直接拼接效率低使用列表收集后join这些细节看起来琐碎但每一个都可能导致一次 WAWrong Answer。我在 100 分钟里至少踩中了其中三个。4. 边界用例与错误提交复盘把“耗时100”拆开看4.1 我用表格整理的边界测试集这道题通过示例后我本来以为万事大吉结果提交上去连续吃了几发 WA。痛定思痛我拉了一张表把所有能想到的特殊输入都列出来逐个手推预期输出。输入预期输出说明空串没有字符自然返回空aa单个字符没有相邻关系直接返回自身11同上12全是数字没有字母可以穿插无法满足条件ab全是字母同理a1a1或1a字母和数字各一个怎么排都合法a1ba1b等合法序列字母比数字多一个字母必须在两端covid2019c2o0v1i9d等合法序列字母 5 个、数字 4 个字母放偶数位这张表帮我确认了一件事这道题的判断条件会自动处理所有边界情况。空串、单字符、全数字、全字母这些情况都能被abs(d - l) 1和交替构造逻辑覆盖不需要额外写特殊分支。4.2 三次典型 WA 的完整复盘讲几个我实际犯过的错误给正在刷题的朋友提个醒。第一个错误判断条件写反。我第一次写完代码后条件写成了if (abs(d - l) 1) return ;也就是把“有解”当成了“无解”把所有合法输入都拦腰截断了。输入a1直接返回空串而正确输出是a1。这种错误很低级但恰恰是紧张状态下最容易犯的。第二个错误循环边界漏字符。我最初写的构造逻辑是while (i first.size() j second.size()) { ans.push_back(first[i]); ans.push_back(second[j]); }这个版本在处理数量相等时没问题但一旦first比second多一个循环结束后还剩一个first字符没有放入结果。比如输入a1b会输出a1丢掉了末尾的b。这就是经典的一个字符导致全部错误后来改成||条件并分别判断索引边界才彻底解决。第三个错误C 无符号数溢出。我一开始直接写了if (digits.size() - letters.size() 1) return ;看起来没毛病但digits.size() - letters.size()是size_t类型运算当digits.size()小于letters.size()时结果是巨大的无符号正数远大于 1直接误判为无解。比如输入a1时digits.size()为 1letters.size()为 1相减为 0没问题但输入ab1时letters.size()为 2digits.size()为 11 - 2在size_t下是一个天文数字条件成立结果错误返回空串。修复方式是先转换成int或者用abs((int)digits.size() - (int)letters.size()) 1判断。这三个错误加起来让我在提交—失败—再提交的循环里折腾了大半个小时。事后想想如果一开始就在草稿纸上把边界表画出来至少能省一半时间。5. 扩展从 1417 到 Reorganize String 的通用解法5.1 1417 的本质是“两类物品穿插”问题把这道题吃透之后我发现它属于一个更大的题型家族相邻元素不能相同的重排问题。它的特殊之处在于所有数字都被视为同一类所有字母也被视为同一类整个系统只有两个类别。类别少了问题就退化成简单的计数比较。数字和字母内部本身有不同字符但它们之间的冲突并不区分具体字符所以不需要对每个字符单独计数。这也是为什么一个abs(d - l) 1就能完成可行性判断的原因。5.2 LeetCode 767 Reorganize String 为什么不能照搬LeetCode 767 是这道题最有名的变体给定一个字符串要求重排使得任意相邻字符都不相同能排则返回任意合法串否则返回空串。题目表面上和 1417 很像但有一个关键区别767 中每个具体字符都是独立类别比如a和b之间不算冲突只有a和a才算冲突。这意味着我们不能简单地分成“字母”和“数字”两个桶而是要对 26 个小写字母分别计数。可行性判断也要升级某一种字符如果出现次数超过(n 1) / 2就一定无解否则一定可以构造。这个条件的本质和 1417 完全一致都是“数量最多的那一类元素不能超过位置容量”。在 1417 中位置容量由另一类元素决定在 767 中位置容量由所有其他字符的总数决定。构造阶段767 通常需要用最大堆优先队列来贪心选择字符每次取出当前剩余次数最多的字符放入结果如果它和上一个字符相同就选择次大的字符。另一种更直观的实现是每次从堆顶取两个不同的字符交替放置相当于把 1417 的“两桶互拼”扩展到多桶场景。从实现角度看1417 是 767 的退化特例。理解了 1417 的计数比较再看 767 的堆贪心会有一种很清晰的递进感。5.3 遇到“相邻冲突”类题目时的通用思路刷多了这类题之后我总结了一套普适的判断流程可以快速定位解法先问自己冲突发生在什么粒度如果只有两个大类如字母和数字直接分类统计数量差判断可行性然后交替构造。如果每个字符都是独立类别需要统计每个字符的频率并检查最高频次是否超过(n 1) / 2。其次问构造方式是“交替”还是“插空”两类的场景用交替拼接就足够。多类的场景用最大堆每次取剩余次数最多的字符或者每次取两个不同字符交替放置。最后想边界情况有哪些空串、单字符、全是同一类字符、长度为 2 的字符串这些一眼能看清的例子里往往藏着最容易踩的坑。按照这个流程1417 我可以在 5 分钟内写完767 也能很快锁定堆解法。从 1417 入手再去看 767 的题解会比直接啃堆的代码轻松很多。现在回头看这道题对我最大的价值不是那十几行代码而是纠正了我“拿到题就开写”的毛病。Easy 题里也有值得想清楚再动手的约束条件我在草稿纸上画球的时间只花了不到五分钟却省下了一个多小时的无效循环。如果你也卡在 1417 上我的建议是照这个流程来先统计再判断最后构造。别像我一样先写代码后想逻辑最后只能靠测试用例反向修正大脑里的模型。