LeetCode 面试题 08.08. 有重复字符串的排列组合
题目描述

题意分析
返回英文字母字符串的所有不同排列,每个结果都要使用全部字符,并保留重复字符的原有数量。不同下标上的相同字符不能被当成不同答案,去重也不能把这些字符的副本删掉。
解法:排序并限制重复字符的选择顺序
核心思路
[!blue]
仍然逐位置选择尚未使用的字符:
dfs(i)填写输出位置i,t[0..i)是已填前缀,vis[j]记录输入下标j是否已被前缀使用。但相同字符的下标互换会生成相同字符串,需要为这些副本规定唯一的选择顺序。先排序,使相同字符相邻。枚举
j时,如果它与前一个字符相同,并且前一个下标还没有使用,就跳过j。因为当前这一位选择较早的那份相同字符即可,选择较晚的副本会形成等价分支。若前一个相同字符已经在前缀中,当前副本就可以使用,不能一概跳过重复字符。代码中的条件
j == 0 || s[j] != s[j - 1] || vis[j - 1],正是要求每种字符优先使用当前最靠前的未用副本。这样相同字符沿一条完整路径总按下标顺序进入排列。任意合法的字符排列,都可以把同字符的第几次出现分配给排序后第几份副本,恰好得到一条保留的路径;其他仅交换相同副本身份的路径被剪掉。因此既保留全部不同排列,又不产生重复答案。
选择后标记并填写当前位,递归返回后撤销标记。所有位置填满时把缓冲转换成独立字符串保存;下一分支会覆盖字符位置,不需要把缓冲逐项清空。
解题步骤
- 将输入转为字符数组并排序,准备等长缓冲和全为未使用的标记。
- 从第 0 个输出位置开始,每层枚举全部输入下标。
- 跳过已经使用的下标,以及“与前一字符相同、前一副本尚未使用”的下标。
- 对允许的候选,标记使用、填入当前位置、递归下一位,返回后撤销标记。
- 填满全部位置时保存字符串,直到所有允许的分支完成。
代码实现
// 相同字符必须按下标顺序进入当前前缀,剪掉同层等价分支。
class Solution {
private char[] s;
private char[] t;
private boolean[] vis;
private List<String> answer = new ArrayList<>();
public String[] permutation(String S) {
int n = S.length();
s = S.toCharArray();
Arrays.sort(s);
t = new char[n];
vis = new boolean[n];
dfs(0);
return answer.toArray(new String[0]);
}
private void dfs(int i) {
if (i >= s.length) {
answer.add(new String(t));
return;
}
for (int j = 0; j < s.length; ++j) {
if (!vis[j] && (j == 0 || s[j] != s[j - 1] || vis[j - 1])) {
vis[j] = true;
t[i] = s[j];
dfs(i + 1);
vis[j] = false;
}
}
}
}
import (
"slices"
"sort"
)
// 相同字符必须按下标顺序进入当前前缀,剪掉同层等价分支。
func permutation(S string) (answer []string) {
s := []byte(S)
sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
t := slices.Clone(s)
vis := make([]bool, len(s))
var dfs func(int)
dfs = func(i int) {
if i >= len(s) {
answer = append(answer, string(t))
return
}
for j := range s {
if !vis[j] && (j == 0 || s[j] != s[j-1] || vis[j-1]) {
vis[j] = true
t[i] = s[j]
dfs(i + 1)
vis[j] = false
}
}
}
dfs(0)
return
}
复杂度分析
- 时间复杂度:设长度为 $n$,不同排列数为 $U$。排序为 $O(n\log n)$;每层不同前缀数不超过 $U$,总计最多 $O(nU)$ 个搜索节点,而当前实现每个非叶节点都扫描全部 $n$ 个下标,故可给出 $O(n\log n+n^2U)$ 的上界。不能直接写成 $O(nU)$:字符全相同时虽然只有一个结果,仍需在 $n$ 层中各扫描 $n$ 个候选。
- 空间复杂度:不计返回结果为 $O(n)$,用于字符缓冲、标记和递归栈;输出占 $O(nU)$。
关键点总结
[!green]
- 排序将相同副本放在一起,使用顺序限制才真正完成去重。
- 前一相同副本未使用时跳过当前副本;前一副本已进入路径时允许继续使用。
- 每个不同字符排列保留一种规范的副本下标安排,去重不损失字符重数。
易错点总结
[!yellow]
- 用集合去掉重复字符,会改变输入重数,生成的就不是原字符串排列。
- 把相同字符一律跳过,会使需要多份相同字符的排列无法填满。
- 将“前一副本未使用”的条件写反,会改变这套按下标顺序选择的去重规则。
- 递归返回后不撤销
vis,会把本分支的选择错误地限制到后续分支。- 仅仅排序而不检查副本使用状态,仍会枚举出相同的字符串。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 面试题 08.07. 无重复字符串的排列组合 | 中等 | 基础回溯相同,本题新增相同字符副本的选择顺序限制。 |
| 90. 子集 II | 中等 | 同样按层剪掉重复选择,但子集题每层只枚举后续下标,排列题枚举所有未使用下标。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!