重构字符串 图解题解
这道题到底在问什么
- 输入
- s="aab"
- 输出
- "aba" (a 隔开放,b 夹中间)
- 输入
- s="aaab"
- 输出
- "" (a 有 3 个,太多,挤不开)
最优解:一步一步想明白
- 3记住「先判 max ≤ ⌊(n+1)/2⌋,再按频次降序先填偶位后填奇位」,下面每帧都在套它。limit = ⌊(6+1)/2⌋ = 3。
- 4第一步先数清每个字符各出现几次,结果记在右边的频次表里。
- 5扫到第 0 个字符 'a',它的计数变成 1。右边频次表实时更新。
- 6扫到第 1 个字符 'a',它的计数变成 2。右边频次表实时更新。
- 7扫到第 2 个字符 'a',它的计数变成 3。右边频次表实时更新。
- 8扫到第 3 个字符 'b',它的计数变成 1。右边频次表实时更新。
- 9扫到第 4 个字符 'b',它的计数变成 2。右边频次表实时更新。
- 10扫到第 5 个字符 'c',它的计数变成 1。右边频次表实时更新。
- 11数完后看最多的 'a'(3 个)和 limit=3 比:3 ≤ 3,说明排得开,继续填。
- 12下面这条 6 格的带子就是答案的槽位('·' 表示空)。按频次从多到少:'a' → 'b' → 'c',先往偶数位塞。
- 13轮到 'a',按当前 idx 把它放到下标 0(紫色框)。
- 14'a' 落在下标 0(绿色),idx 每次 +2 跳着走,保证同一个字符之间至少隔一格。
- 15轮到 'a',按当前 idx 把它放到下标 2(紫色框)。
- 16'a' 落在下标 2(绿色),idx 每次 +2 跳着走,保证同一个字符之间至少隔一格。
- 17轮到 'a',按当前 idx 把它放到下标 4(紫色框)。
- 18'a' 落在下标 4(绿色),idx 每次 +2 跳着走,保证同一个字符之间至少隔一格。
- 19偶数位 0,2,4 都填满了,下标超出末尾,于是跳回下标 1 开始填奇数位。紫色框就是 'b' 要落的新位置。
- 20'b' 落在下标 1(绿色),idx 每次 +2 跳着走,保证同一个字符之间至少隔一格。
- 21轮到 'b',按当前 idx 把它放到下标 3(紫色框)。
- 22'b' 落在下标 3(绿色),idx 每次 +2 跳着走,保证同一个字符之间至少隔一格。
- 23轮到 'c',按当前 idx 把它放到下标 5(紫色框)。
- 24'c' 落在下标 5(绿色),idx 每次 +2 跳着走,保证同一个字符之间至少隔一格。
- 25全部填完,得到 "ababac"。从头检查相邻:'a'≠'b','b'≠'a','a'≠'b','b'≠'a','a'≠'c',全部不同,合法。
- 26反例 s="aaab":'a' 有 3 个,超过 limit=2,无论怎么排都有两个 'a' 挨着,所以一开始就返回空串。
⚠️ 容易写错的地方
✗ 错:可行性判成 max < (n+1)/2
✓ 对:应是 max ≤ ⌊(n+1)/2⌋
恰好等于上限时仍可排(如 "aab" 里 a=2,limit=2)
✗ 错:只填偶数位、越界后停下
✓ 对:偶位填满要回到下标 1 接着填奇位
不回填奇位会漏掉一半字符,答案残缺
✗ 错:不按频次排序就随便填
✓ 对:必须频次降序填
不先安排最多的字符,它可能被挤到相邻位置导致冲突
完整代码(Python / C++ / Java)
Python
class Solution:
def reorganizeString(self, s: str) -> str:
cnt = [0] * 26
for ch in s:
cnt[ord(ch) - 97] += 1
n = len(s)
if max(cnt) > (n + 1) // 2:
return ''
chars = sorted(range(26), key=lambda i: (-cnt[i], i))
ans = [''] * n
idx = 0
for c in chars:
for _ in range(cnt[c]):
if idx >= n:
idx = 1
ans[idx] = chr(c + 97)
idx += 2
return ''.join(ans)C++
#include <algorithm>
#include <numeric>
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
string reorganizeString(string s) {
vector<int> cnt(26);
for (char c : s) cnt[c - 'a']++;
int n = s.size();
if (*max_element(cnt.begin(), cnt.end()) > (n + 1) / 2) return "";
vector<int> order(26);
iota(order.begin(), order.end(), 0);
sort(order.begin(), order.end(), [&](int a, int b){ return cnt[a] == cnt[b] ? a < b : cnt[a] > cnt[b]; });
string ans(n, ' ');
int idx = 0;
for (int c : order) while (cnt[c]--) {
if (idx >= n) idx = 1;
ans[idx] = char('a' + c);
idx += 2;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public String reorganizeString(String s) {
int[] cnt = new int[26];
for (char c : s.toCharArray()) cnt[c - 'a']++;
int n = s.length(), max = 0;
for (int x : cnt) max = Math.max(max, x);
if (max > (n + 1) / 2) return "";
Integer[] order = new Integer[26];
for (int i = 0; i < 26; i++) order[i] = i;
Arrays.sort(order, (a,b) -> cnt[a] == cnt[b] ? a - b : cnt[b] - cnt[a]);
char[] ans = new char[n];
int idx = 0;
for (int c : order) while (cnt[c]-- > 0) {
if (idx >= n) idx = 1;
ans[idx] = (char) ('a' + c);
idx += 2;
}
return new String(ans);
}
}复杂度
时间
O(n + 26 log 26)
扫一遍数频次 O(n),对 26 个字母排序是常数级,写答案 O(n)
空间
O(1)
计数数组与排序数组都只有 26,与 n 无关(答案串不计入)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 重构字符串 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么这里用计数数组 + 排序,而不用大顶堆?+
两种都对。大顶堆每次取频次最高的字符放一个、用完再放回,是经典写法;但本题字符只有 26 种,直接用长度 26 的计数数组排序一次再「偶位优先」填,常数更小、更好写,复杂度同为 O(n)。堆写法在「k 种类很大」时才更划算。
如果要求相邻字符间隔至少 k(LC358 任务调度类),还能这么填吗?+
思路相通但更复杂:仍是频次最高者优先,但要保证两次使用同一字符间隔 ≥ k,通常用大顶堆 + 一个「冷却队列」记录每个字符下次可用的时间。按 LC358 把 k 定义为「相同字符的下标距离至少 k」,本题「相邻不同」就是 k=2 的特例(中间至少隔 1 个字符),才简化成「偶位优先」直接填。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 重构字符串 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。