目录
题目陈述
大意:给定一个字符串,求出它的字符的所有排序(元素可能有重复),并且答案按从小到大给出
方法一: O ( n n ) O(n^n) O(nn)做法
算法思路
- 递归搜索进行搜素,对于第h层,枚举第h个位置,应该填入什么下标的元素
- 注意:此处因为字符串可能有重复的元素,故我们记录的是下标,因为下标不可能重复(相当于一种没有哈希冲突的哈希函数)
- 然后我们也需要开一个bool数组来记录哪些下标没有被使用过,确保同一下标只被使用过一次
- 递归边界条件h==s.size(),因为第h层代表正在枚举第h个位置(即前面h-1个位置已经枚举完毕),所以第h=s.size()层代表前面s.size()-1层都已经枚举完毕
- 回缩的时候,记得将原来状态恢复即可
- 时间复杂度 O ( n n ) O(n^n) O(nn),每一层都需要枚举所有数是否能使用,总共需要枚举n层
代码实现
class Solution {
public:
void dfs(int h,string &s,string cur,set &ans,vector &f){
if(h==s.size()){//递归边界
ans.insert(cur);//找到一个答案并且插入
return ;
}
for(int i=0;i<s.size();i++){
if(!f[i]){//当前位置的字符未被使用
f[i]=1;//则使用
cur+=s[i];//更新当前字符排序
dfs(h+1,s,cur,ans,f);//递归下一层
cur.pop_back();//还原当前字符排序
f[i]=0;//还原f
}
}
}
vector Permutation(string str) {
if(str.empty())return {};
set ans;//因为题目说str中可能有重复元素,所以需要集合来去重,并且起到从小到大排序作用
vector f(str.size(),0);//用来标记第i个位置是否已被使用
string cur;//当前的搜索出来的字符串
dfs(0,str,cur,ans,f);//进行递归搜索字符串排序
return vector({ans.begin(),ans.end()});//vector迭代器拷贝初始化,只需要元素的类型一样即可,跟容器无关
}
};
思考:是否一定需要用set
- 肯定有同学在这边会有疑问,set内嵌红黑树,每次维护需要花费 log 2 n \log{2n} log2n的代价,但是如果我们一开始先将str排序一下,是不是就不需要set了呢?或者可以将set换成vector?
- 答案是显然的,不可以。为什么?因为题目里面的元素可能是重复的,所以最后总的排序个数可能小于(N!)
- 就算我们先排序,最后求出的答案,在vector中依旧会面临一个去重的问题,依旧需要借助set来执行,所以此处我们set还是不能被替换为vector的
算法二: O ( n ! ) O(n!) O(n!)做法
算法思路
- 我们可以发现,全排列的个数也就刚好 A n n = n ! A_n^n=n! Ann=n!个,我们这个算法的时间复杂度也刚好是 O ( n ! ) O(n!) O(n!),可以说,已经是最优的了
- 此处我们先说算法的复杂度,随后再证明正确性
- 对于字符串s,递归搜索,对于第h层,依次和下标在[h,s.size()-1]中的元素互换
- 递归边界为h==s.size()-1,(自己跟自己换此时没有意义)
- 我们先来看递归代码,顺带证明正确性
证明:算法正确性
void dfs(int h,string s,set &ans){
if(h==s.size()-1){//递归边界
ans.insert(s);//找到一个排序,并且插入
return ;
}
for(int i=h;i<s.size();i++){//假设原来的元素映射到他的下标,那么我们搜索的是下标的全排序
swap(s[i],s[h]);
dfs(h+1,s,ans);//进入下一层递归
swap(s[i],s[h]);//还原s
}
}
复杂度分析
- 对于第i层循环,设第i层的时间复杂度为f(i),不难发现,对于第二层for循环有, f ( i ) = ( s i z e − i ) ∗ f ( i + 1 ) f(i)=(size-i)*f(i+1) f(i)=(size−i)∗f(i+1),即循环执行n-i次,因为有这么多个位置允许被互换,内层又执行递归第i+1层
- 不难得出f(size-1)=1,易得 T ( n ) = f ( 0 ) = n ! T(n)=f(0)=n! T(n)=f(0)=n!,故该算***生成 n ! n! n!个排序
互异性证明
- 接下来只需要证明,递归搜索生成的序列,具有互异性,即两两不同,则该算法正确
- 反证法
- 假设可以通过不同的互换,得到两个相同的序列,设前面[0,i-1]个位置的互换都相同,在第i层,互换不同,即i和j换,和i和k换,且 ( j ! = k ) (j!=k) (j!=k),
- 又,最后的序列相同,后续的交换无法改变前面的位置,则推出 j = = k j==k j==k
- 故矛盾,则不存在不同的交换方式,使得两个序列的一样的,证毕
- 注意:此处下标所生成的排序不是字典序递增!
动画演示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cHtuvoSW-1624100679373)(https://img-blog.csdnimg.cn/20210619175656586.png?x-oss-process=image/watermark],type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0doMHN0X0x4,size_16,color_FFFFFF,t_70)
代码实现
C++
class Solution {
public:
void dfs(int h,string s,set &ans){
if(h==s.size()-1){//递归边界
ans.insert(s);//找到一个排序,并且插入
return ;
}
for(int i=h;i<s.size();i++){//假设原来的元素映射到他的下标,那么我们搜索的是下标的全排序
swap(s[i],s[h]);
dfs(h+1,s,ans);//进入下一层递归
swap(s[i],s[h]);//还原s
}
}
vector Permutation(string str) {
if(str.empty())return {};
set ans;//因为题目说str中可能有重复元素,所以需要集合来去重,并且起到从小到大排序作用
dfs(0,str,ans);//开始搜索字符串
return vector({ans.begin(),ans.end()});//vector迭代器拷贝初始化,只需要元素的类型一样即可,跟容器无关
}
};
python
递归枚举不要s[i]的permutation,然后再拼接上去,递归边界,空串或者一个字符
class Solution(object):
def Permutation(self, ss):
n=len(ss)
if n<=1:#递归边界,空串或者一个字符
return ss
lst=[]
for i in range(n):
s1=ss[i]
for s2 in self.Permutation(ss[:i]+ss[i+1:]):#递归搜索,不取ss[i]
new_str=s1+s2# s1和s2拼接成新的排序
if new_str not in lst :#如果当前没有这个排序
lst.append(new_str)
lst=sorted(lst)#字典序,从小到大排序
return lst