1.题目
题目描述
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c
所能排列出来的所有字符串abc,acb,bac,bca,cab,cba
。
输入描述:
输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。
2.我的题解
本题需要实现以下几个功能:
- 全排列:若干字符的全排列;
- 去重:有重复字符时,需要去掉重复的结果;
- 有序:结果需要按照字典序输出;
这里使用递归法(交换)实现,具体思路是:
- 给定初始状态,确定字符个数
len
; - 以交换的方式确定当前位置的字符,并保证了不遗漏。然后将后续部分整体看做一个子问题进行递归,直到最后一个位置。
- 假设当前位置为
pos
,考察位置i(pos<i<len)
以确定i
和pos
位置是否可以交换(直接交换的话可能会导致排列重复):如果[pos,i)
位置上没有与i位置上相同的值,那么就可以交换。 - 该方法并不保证有序性,但通过判断可以去重复。
举个例子:求1,2,3
的全排列
初始状态 | 递归一层(交换) | 递归两层(交换) |
---|---|---|
1,2,3 | 1,2,3 1,3,2 | |
1,2,3 | 2,1,3 | 2,1,3 2,3,1 |
3,2,1 | 3,2,1 3,1,2 |
举例说明是否可交换的判断:1,2,3,4,2,5,6
- 假设当前位置为第一个
2
。当第一个2
想和第二个2
交换时,可以看到二者之间的位置(前开后闭)存在一个2
,那么就不应该交换; - 一个直观的理解。假设在
5
这个位置上,令一个不等于5
的数与之交换,即可得到一个新的排列,显然2
与5
交换可以得到一个新的排列。但是这里有两个2
,两个2
与5
交换将得到相同的排列,为了避免重复我们仅想让一个2
与5
交换,于是上述判断的依据实质上就是保证一个位置仅被重复的数字交换一次,说白了也就是设置“管辖区域”,在本例中,1,3,4
由第一个2
交换,5,6
由第二个2
交换; - 判断的依据不仅可以是上述说的
[pos,i)
,还可以是[i,len)
,这样就是让第一个2
管5,6
,第二个2
管1,3,4
;
2.1递归法(交换)
class Solution {
vector<string> res_;
void swap(string &s, int i, int j) {
char tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
void MyPermutation(string &s, int n, int pos) {
if (pos == n)res_.push_back(s);
for (int i = pos; i < n; i++) {
bool flag = true;
for (int j = i+1; j < n; j++)//for (int j = pos; j < i; j++)
if (s[j] == s[i])flag = false;
if (flag) {
swap(s, i, pos);
MyPermutation(s, n, pos + 1);
swap(s, i, pos);
}
}
}
public:
vector<string> Permutation(string str) {
res_.clear();
if(str.size()==0)return res_;
MyPermutation(str,str.size(),0);
sort(res_.begin(),res_.end());
return res_;
}
};
3.别人的题解
3.1 字典序法
字典序之间可有关系?如找到一个排列的下一个字典序?例如我们都知道123
的下一个字典序是132
,那么有什么普适的寻找方法呢?
寻找下一个字典序排列的方法:
- 举例:1532(下标从0起)
- 从后向前找到第一个相邻的正序对的位置
i
:即array[i]<array[i+1]
,这里i=0
; - 从
i
开始向右搜索,找到比array[i]
大的当中最小的那个位置j
:这里j=3
; - 交换
i,j
位置上的数:得到2531
; - 将
i
位置后的字符串反转2135
;
优点: 该方法可去重读、有序、高效。
class Solution {
vector<string> res_;
void swap(char *ch1, char *ch2) {
char tmp = *ch1;
*ch1 = *ch2;
*ch2 = tmp;
}
string nextPermutation(string s) {
if (s.size()<2)return "";
int i = s.size() - 2, j = 0, k = 0;
//正序对
while (i >= 0 && s[i]>=s[i + 1])i--;
if (i<0)return "";
//大于s[i]的最小的数
j = i + 1;
for (int k = i + 2; k<s.size(); k++)
if (s[k]>s[i] && s[k]<s[j])j = k;
//swap
swap(&s[i], &s[j]);
//reverse
j = i + 1, k = s.size() - 1;
while (j<k) {
swap(&s[j++], &s[k--]);
}
return s;
}
public:
vector<string> Permutation(string str) {
if (str.size() == 0)return res_;
res_.clear();
sort(str.begin(), str.end());
do {
//cout << str << endl;
res_.push_back(str);
} while ((str = nextPermutation(str)) != "");
return res_;
}
};
3.2 递归回溯法(填空)
上面的递归法(交换)是在给定初始状态(即某个排列)的基础上,通过交换确定某个位置上的值,从而获取全部排列的方法。本方法中换一个思路,不再进行交换,而是进行填空,即在某个位置上填写值,这需要维护一个剩余字符集合,具体如下:
- 初始化长度为
len
的空排列,剩余字符集合包含所有字符; - 向当前位置填值,值可以是剩余字符结合中的任意字符,同时剩余字符结合要去掉该字符。后续部分通过递归继续处理。
- 该方法不能去除重复、不能保证有序性,需要借助
set
中转。
记录一下实现时的坑:
- 这个思路在评论区看的,
java
版本实现,使用ArrayList
作为递归时的参数维护剩余字符集合,该数据结构实现了通过下标的读取和删除(get(i),remove(i))
。我用C++
实现,该用什么呢?list
? - 入坑了:
list
似乎不支持通过下标的读取和删除,我用的较少,没搞出来。循环时放弃使用下标,用迭代器吧!读是可以读了,删除呢? remove
:它接收值作为参数,返回值为空,删除容器中所有等于该值的节点。erase
:它接收迭代器为参数,返回值为下一个迭代器,删除当前迭代器指向的节点。erase
删除后会丢失迭代器,要用it = list.erase(it);
或list.erase(it++);
- 坑1:直接使用
erase
删除当前参数中的某个位置,并当做参数传入进行下一次递归。我是傻的吗?递归回来进行下一次循环的时候,我又要删除一个,那么下一次循环时传入的参数被我删了两个,这不合适吧? - 坑2:咱新建一个
list
,把新建的当做参数传递,这样就不会影响当前层的参数。可是没有下标,新建的list
我不知道该删哪个地方啊!没有迭代器也不能通过下标。 - 坑3:
remove
更不行,有重复字符的话删除必多删,必出错。 - 坑4:不新建
list
,删除当前层参数的某个位置,并当作参数传入下一层递归,递归回来后,利用insert
还原当前层的参数。。。坑,兜了一圈还不如用vector
呢。 - 使用
vector
实现,主要vector
可以方便的地实现下标操作。本题中合法的字符是字母,那么0
作为删除的标记,遇到就跳过该位置即可。
//vector实现
class Solution {
vector<string> res_;
set<string> set_;
string array_;
vector<char> left_;
int len = 0;
void solve(int pos) {
if (pos == len) {
if (set_.find(array_) == set_.end()){
set_.insert(array_);
//cout << array_ << endl;
}
return;
}
for (int i = 0; i < left_.size();i++) {
if (left_[i] == '0')continue;
array_[pos] = left_[i];
left_[i] = '0';
solve(pos + 1);
left_[i] = array_[pos];
}
}
public:
vector<string> Permutation(string str) {
//init
len = str.size();
if (len == 0)return res_;
res_.clear();
set_.clear();
left_.clear();
array_ = str;
for (int i = 0; i < len; i++)left_.insert(left_.end(), str[i]);
//solve
solve(0);
//res
res_.assign(set_.begin(), set_.end());
return res_;
}
};
//list实现
class Solution {
vector<string> res_;
set<string> set_;
string array_;
list<char> left_;
int len = 0;
void solve(int pos,list<char> left) {
if (pos == len) {
if (set_.find(array_) == set_.end()){
set_.insert(array_);
//cout << array_ << endl;
}
return;
}
for (auto it = left.begin(); it != left.end();) {
array_[pos] = *it;
it = left.erase(it);
solve(pos + 1,left);
left.insert(it, array_[pos]);
}
}
public:
vector<string> Permutation(string str) {
//init
len = str.size();
if (len == 0)return res_;
res_.clear();
set_.clear();
left_.clear();
array_ = str;
for (int i = 0; i < len; i++)left_.insert(left_.end(), str[i]);
//solve
solve(0,left_);
//res
res_.assign(set_.begin(), set_.end());
return res_;
}
};
4.总结与反思
(1)递归一看就会,一写就废,仍要多温习。
(2)全排列、去重、有序。
(3)STL list
的使用,remove(),erase()
。