[230516 剑指38] 字符串的排列
一 题目
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
限制:
1 <= s 的长度 <= 8
二 整体思路
求一个字符串的全排列,本质上就是穷举。我们可以把这个问题看作“有 n 个空位,我们使用给定的 n 个字符从左往右填满这 n 个空位,并且每个字符不能重复使用”。我们可以使用回溯来模拟这个过程。
递归函数的每一次调用,代表着选定了空位;在回溯函数内部,遍历 n 个字符,尝试将某一字符放入该空位,然后进行下一次递归调用,对下一个空位进行相同的操作。
在写代码的过程中描述每一个变量、每一个参数的意思。
三 关键点/重点/难点
关键点:
- 因为是求全排列,所以递归函数内部中的遍历,每次都要从下标为 0 的字符开始
难点:
- 如果原字符串中有重复字符,要对结果集中重复的排列进行去重
- 如果求出所有排列以后再进行去重,时间复杂度会很高,应该会超时
- 所以我们借助一个元素类型为 bool 的 vector,它标记的是有哪些元素已经被放入了空位。同时,在函数内部的遍历中,我们保证:相同的字符不能第二次填入同一个空位。
if(isUsed[i] || (i > 0 && s[i] == s[i - 1] && !isUsed[i - 1])) {
continue;
}
- 举个例子:给定字符串 “aab”,开始时选定第一个空位,第一个 a 可以填入第一个空位,但进行回溯后,第二个 a 不允许再填入第一个空位。
四 代码分析
class Solution {
private:
vector<string> result;
string path;
void traceback(const string& s, vector<bool>& isUsed) {
if(path.size() == s.size()) {
result.push_back(path);
return;
}
for(int i = 0; i < s.size(); ++i) {
if(isUsed[i] || (i > 0 && s[i] == s[i - 1] && !isUsed[i - 1])) {
continue;
}
path.push_back(s[i]);
isUsed[i] = true;
traceback(s, isUsed);
path.pop_back();
isUsed[i] = false;
}
}
public:
vector<string> permutation(string s) {
result.clear();
path.clear();
sort(s.begin(), s.end());
vector<bool> isUsed(s.size(), false);
traceback(s, isUsed);
return result;
}
};
(五)一题多解
下一个排列:已知当前的一个排列,快速得到字典序中下一个更大的排列。
见 31. 下一个排列的官方题解 。
(六) 知识扩展
C++ 中的 sort 函数
函数实现:
- C++ 排序函数使用 introsort,这是一种混合算法。不同的实现使用不同的算法。例如,GNU 标准 C++ 库使用三部分混合排序算法:首先执行 introsort,然后对结果进行插入排序。
- Introsort 是一种混合排序算法,使用三种排序算法来最大限度地减少运行时间,三种排序算法 分别是 Quicksort、Heapsort 和 Insertion Sort。
Introsort 排序算法:
- Introsort 从快速排序开始,如果递归深度超过特定限制,它会切换到堆排序,以避免快速排序的更坏情况O(N2)时间复杂性。当要排序的元素数量非常少时,它还使用插入排序。
- 所以执行 Introsort 时首先会将根据数据量的大小进行讨论:
- 如果分区大小有可能超过最大深度限制,则Introsort切换到Heapsort。我们将最大深度限制定义为2*log(N)。
- 如果分区大小太小,那么Quicksort会衰减到Insertion Sort。我们将此截止值定义为16(由于研究)。因此,如果分区大小小于16,那么我们将进行插入排序。
- 如果分区大小低于限制并且不太小(即在16和2*log(N)之间),则它执行简单的快速排序。