题目链接:LeetCode 剑指Offer38 字符串的排列
题目大意:
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
题解:
回溯
通过搜索和回溯枚举所有的排列情况,但会有重复的情况。
只要在递归函数中设定一个规则,保证在填每一个空位的时候重复字符只会被填入一次。具体地,我们首先对原字符串排序,保证相同的字符都相邻,在递归函数中,我们限制每次填入的字符一定是这个字符所在重复字符集合中“从左往右第一个未被填入的字符”,即如下的判断条件:
if (vis[j] || (j > 0 && !vis[j - 1] && s[j - 1] == s[j])) {
continue;
}
这个限制条件保证了对于重复的字符,我们一定是从左往右依次填入的空位中的。
class Solution {
private:
vector<string> ans;
vector<int> vis;
public:
void backtrack(const string& s, int i, int n, string& now) {
if (i == n) {
ans.push_back(now);
return;
}
for (int j = 0; j < n; j++) {
if (vis[j] || (j > 0 && !vis[j - 1] && s[j - 1] == s[j])) {
continue;
}
vis[j] = true;
now.push_back(s[j]);
backtrack(s, i + 1, n, now);
now.pop_back();
vis[j] = false;
}
}
vector<string> permutation(string s) {
int n = s.size();
vis.resize(n);
sort(s.begin(), s.end());
string now;
backtrack(s, 0, n, now);
return ans;
}
};
尺取法
按字典序从小到大寻找字符排列,注意到下一个排列总是比当前排列要大,除非该排列已经是最大的排列。我们希望找到一种方法,能够找到一个大于当前排列的新排列,且变大的幅度尽可能小。具体地:
- 我们需要将一个左边的“较小字符”与一个右边的“较大字符”交换,以能够让当前排列变大,从而得到下一个排列。
- 同时我们要让这个“较小字符”尽量靠右,而“较大字符”尽可能小。当交换完成后,“较大字符”右边的字符需要按照升序重新排列。这样可以在保证新排列大于原来排列的情况下,使变大的幅度尽可能小。
具体地,我们这样描述该算法,对于长度为\(n\)的字符串\(s\):
- 首先从后向前查找第一个顺序对\((i,i+1)\),满足\(s[i]<s[i+1]\)。这样“较小字符”即为\(s[i]\)。此时\([i+1,n)\)必然是下降序列。
- 如果找到了顺序对,那么在区间\([i+1,n)\)中从后向前查找第一个元素\(j\)满足\(s[i]<s[j]\)。这样“较大字符”即为\(s[j]\)。
- 交换\(s[i]\)与\(s[j]\),此时可以证明区间\([i+1,n)\)必为降序。我们可以直接使用双指针反转区间\([i+1,n)\)使其变为升序,而无需对该区间进行排序。
class Solution {
public:
bool nextPermutation(string& s) {
int i = s.length() - 2;
while (i >= 0 && s[i] >= s[i + 1]) {
i--;
}
if (i < 0) {
return false;
}
int j = s.length() - 1;
while (s[i] >= s[j]) {
j--;
}
swap(s[i], s[j]);
reverse(s.begin() + i + 1, s.end());
return true;
}
vector<string> permutation(string s) {
vector<string> ans;
sort(s.begin(), s.end());
do {
ans.push_back(s);
} while (nextPermutation(s));
return ans;
}
};