题目:
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
思路:
一个很直观的思路,对于任意一个字符串,例如"abcde",我首先枚举选定首字符,例如先选定了首字符是"a",那剩下部分的怎么permutation的事情就交给函数permutation("bcde")好了。
一旦我得到了permutation("bcde")的结果,我只要把所有的"bcde"的排列前面都拼接上一个字符"a"即可。
然后,选定首字符是"b",剩下的工作交给permutation("acde")。
再然后,选定首字符是"c",剩下的工作交给permutation("abde")。
以上一个主干思路。
然后需要注意的一个小的问题是,permutation结果不能出现重复,上面的思路在遇到"aacbb"这种字符串里有重复字符的时候会导致permutation结果重复。
显然,我们只需要选定首字符时做一下筛选,首字符选"a"只选一次,选"b"的时候也只选一次,即可。
这个的话,考虑字符串不一定只有小写字母,因此可以先对"aacbb"做一次排序,得到"aabbc",然后遍历选取首字母的时候跳过重复的即可。
Code:
class Solution { public: vector<string> permutation(string s) { vector<string> res = {}; if (s.empty()) return res; sort(s.begin(), s.end()); return perm(s); } vector<string> perm(const string &s) { vector<string> res = {}; if (s.length() == 1) { res.push_back(s); return res; } char previous = s[0] + 1; for (int i = 0; i < s.length(); i++) { if (s[i] == previous) continue; string other = s.substr(0, i) + s.substr(i + 1); vector<string> temp_result = perm(other); for (int j = 0; j < temp_result.size(); j++) res.push_back(s[i] + temp_result[j]); temp_result.clear(); previous = s[i]; } return res; } };
时间复杂度:
在最开始需要做一个$O(n log n)$的排序。
后面的递归,记permutation长度为n的字符串的时间复杂度为$f(n)$,
有 $f(n) = n cdot f(n - 1)$,根据 $f(1) = 1$,易得 $f(n) = n!$。
所以时间复杂度 $O(n log n + n!) = O(n!)$。
思路2:
参考官方的题解,通过一个传引用的string& perm来实时维护目前的“填字符”进度。
最开始是空的字符串,表示还未开始填字符,然后遍历尝试往其中填入第一个字符,被选中填入的字符要打上标记,
当换到下一个填入时记得把之前那个pop掉,并且取消标记。
当然,避免重复还是上面的老办法,用一个previous变量记录一下。
Code:
class Solution { public: int n; // 输入字符串的长度 vector<string> res; // 存储permutation结果 vector<bool> vis; // 标记数组 vector<string> permutation(string s) { res.clear(); if (s.empty()) return res; n = s.length(); vis.resize(n); vis.clear(); sort(s.begin(), s.end()); string start = ""; dfs(s, start); return res; } void dfs(const string &s, string &perm) { if (perm.length() == n) { // 此时的perm已经是一个可行的排列 res.push_back(perm); return; } // perm.length() < n // 还需要继续往perm后添加字符 char previous = s[n - 1] + 1; // 使其避免和第一个能被用的s[i]一样即可 for (int i = 0; i < n; i++) { if (vis[i]) // 已经用过的显然要直接跳过 continue; if (previous == s[i]) // 之前已经出现过则不能再用 continue; else previous = s[i]; vis[i] = true; perm.push_back(s[i]); dfs(s, perm); perm.pop_back(); vis[i] = false; } return; } };
时间复杂度:
每次产生一个可行的排列需要 $O(n)$,可行的排列个数 $O(n!)$,时间复杂度 $O(n cdot n!)$。
讲道理,这个做法和我自己写那个的时间复杂度是一样的,但是我那个耗时和内存消耗都挺大。
应该是函数直接返回vector,以及常数大的缘故。
因此,我尝试了把返回vector<string>改成传引用:
class Solution { public: vector<string> permutation(string s) { vector<string> res = {}; if (s.empty()) return res; sort(s.begin(), s.end()); perm(s, res); return res; } void perm(const string &s, vector<string> &res) { if (s.length() == 1) { res.push_back(s); return; } char previous = s[0] + 1; for (int i = 0; i < s.length(); i++) { if (s[i] == previous) continue; string other = s.substr(0, i) + s.substr(i + 1); int pre_size = res.size(); perm(other, res); for (int j = pre_size; j < res.size(); j++) res[j] = s[i] + res[j]; previous = s[i]; } return; } };
可以看到,内存消耗已经降下来了,但是耗时还是很大,应该是都花在遍历vector上面了。
然后我只好给vector<string>预分配一定的空间来提升速度:
class Solution { public: vector<string> permutation(string s) { vector<string> res = {}; if (s.empty()) return res; int sz_for_rsz = s.length(); for (int i = 1; i <= s.length(); i++) sz_for_rsz *= i; res.reserve(sz_for_rsz); sort(s.begin(), s.end()); perm(s, res); return res; } void perm(const string &s, vector<string> &res) { if (s.length() == 1) { res.push_back(s); return; } char previous = s[0] + 1; for (int i = 0; i < s.length(); i++) { if (s[i] == previous) continue; string other = s.substr(0, i) + s.substr(i + 1); int pre_size = res.size(); perm(other, res); for (int j = pre_size; j < res.size(); j++) res[j] = s[i] + res[j]; previous = s[i]; } return; } };
运行结果:
看来我这个预分配的空间卡的还算可以,没有涨特别多的内存消耗,耗时也是进一步缩短了。
那最后导致常数大的原因,应该就只剩下字符串的拼接和取子串的操作了。
因此,我又简单改了一下,把 res[j] = s[i] + res[j]; 变成 res[j] += s[i]; ,因为实际上对于permutation来说,反转一下字符串没有任何区别:
class Solution { public: vector<string> permutation(string s) { vector<string> res = {}; if (s.empty()) return res; int sz_for_rsz = s.length(); for (int i = 1; i <= s.length(); i++) sz_for_rsz *= i; res.reserve(sz_for_rsz); sort(s.begin(), s.end()); perm(s, res); return res; } void perm(const string &s, vector<string> &res) { if (s.length() == 1) { res.push_back(s); return; } char previous = s[0] + 1; for (int i = 0; i < s.length(); i++) { if (s[i] == previous) continue; string other = s.substr(0, i) + s.substr(i + 1); int pre_size = res.size(); perm(other, res); for (int j = pre_size; j < res.size(); j++) res[j] += s[i]; previous = s[i]; } return; } };
算是差不多了,不改准备再改了,再改的话就要对substr() + substr()那一句动刀子了,这样的话要改动的代码量就很大了,而且用vis数组去标记的话,改完其实已经和官方给的题解在思路上基本如出一辙了,没什么大的区别,因此就不改了罢。