767. 重构字符串
给定一个字符串S,检查是否能重新排布其中的字母,使得两相邻的字符不同。
若可行,输出任意可行的结果。若不可行,返回空字符串。
示例 1:
输入: S = "aab"
输出: "aba"
示例 2:
输入: S = "aaab"
输出: ""
注意:
- S 只包含小写字母并且长度在[1, 500]区间内。
解题思路:
先思考不能重构的情况,当有一个字母的次数大于整个字符串的一半,即 maxCount > (S.length() + 1) / 2。反之,字符串可以重构。
可以重构的前提下,暴力一点的思路:
- 因为只有26个小写字母,用 int[26] 存储每个字母出现的次数。
- S 长度为 n,定义 char[n]存字符,每次取「不等于上一个且出现次数最多」 的字母放入char[],对应次数减一。
- 一共取 n 次,每次需要遍历26个字母取值,所以总的时间复杂度为 O(n * 26)。
基于上面的思路,思考优化:每次都遍历 26 个字母取值似乎有点傻~。
- 使用优先队列辅助维护每个字符的次数,次数从到大小排列。
- 因为需要不同的字母,每次从优先队列中弹出次数最多的两个字母。如果没用完,再放回优先队列。
- 基于优先队列,可以将时间复杂度缩短为 O(n * log26)。
方法一:优先队列
public String reorganizeString(String S) {
// 统计每个字母出现的次数,顺便维护最大出现次数
int n = S.length();
int[] counts = new int[26];
int maxCount = 0;
for (int i = 0; i < n; i++) {
char c = S.charAt(i);
counts[c - 'a']++;
maxCount = Math.max(maxCount, counts[c - 'a']);
}
// 如果最大次数大于数组的一半,则一定不能重构
if (maxCount > (n + 1) / 2) {
return "";
}
// 根据出现次数从大到小 构建优先队列
PriorityQueue<Character> queue = new PriorityQueue<>(Comparator.comparingInt(c -> -counts[c - 'a']));
for (int i = 0; i < 26; i++) {
if (counts[i] > 0) {
queue.offer((char) (i + 'a'));
}
}
// 重构字符串,每次取出最多的2个字符放到数组,如果没用完再放回去
int index = 0;
char[] cs = new char[n];
while (queue.size() > 1) {
char c1 = queue.poll();
char c2 = queue.poll();
cs[index++] = c1;
cs[index++] = c2;
if (--counts[c1 - 'a'] > 0) {
queue.offer(c1);
}
if (--counts[c2 - 'a'] > 0) {
queue.offer(c2);
}
}
// 最后可能还剩一个字母
if (queue.size() > 0) {
cs[index] = queue.poll();
}
return new String(cs);
}
方法二:基于计数的贪心算法
此方法参考官方解答,自己没想出来~
可以重构的前提下:
- 用 int[26] 存储每个字母出现的次数
- S 长度为 n,定义 char[n]存字符,并将位置分为 偶数角标 和 奇数角标
- 在不考虑特殊情况时,遍历 int[26] 将字母先放偶数角标,偶数角标放满后再放奇数角标。基于此,隔开相同的字母。
示例:
假设 S 为:abzabz。偶数角标:0,2,4;奇数角标:1,3,5。int[26] 如下图:
遍历 int[26] 步骤如下:
- 两个 a 放到偶数角标 0 和 2
- 两个 b 先放一个到 4,此时偶数角标放满了,另外一个放到奇数角标 1
- 两个 z 放到奇数角标 3 和 5
- 最后返回 abazbz
特殊情况: S = aaccc,3个 c 必须放到偶数角标0,2,4才能保证重构成功。若按照上面的流程,a 先放了偶数角标,重构字符串为acacc,重构失败。
为了满足特殊情况,在遍历时如果当前字母的出现次数小于或等于 n / 2,我们应该先放奇数位置,把偶数位置留给特殊情况。
public String reorganizeString(String S) {
// 统计每个字母出现的次数,顺便维护最大出现次数
int n = S.length();
int[] counts = new int[26];
int maxCount = 0;
for (int i = 0; i < n; i++) {
char c = S.charAt(i);
counts[c - 'a']++;
maxCount = Math.max(maxCount, counts[c - 'a']);
}
// 如果最大次数大于数组的一半,则一定不能重构
if (maxCount > (n + 1) / 2) {
return "";
}
// 重构字符串
char[] cs = new char[n];
int evenIdx = 0, oddIdx = 1;
int half = n / 2;
for (int i = 0; i < 26; i++) {
char c = (char) ('a' + i);
// 不为特殊情况时,优先使用奇数位置
while (counts[i] > 0 && counts[i] <= half && oddIdx < n) {
cs[oddIdx] = c;
counts[i]--;
oddIdx += 2;
}
while (counts[i] > 0) {
cs[evenIdx] = c;
counts[i]--;
evenIdx += 2;
}
}
return new String(cs);
}