字符串的全排列是字符串类的算法题的一个考察点,属于普通问题,它有两种实现方法,递归算法和非递归算法,非递归的方法要稍微难一点,以下会依次进行介绍。
1.递归算法
算法思想:求 n 位的字符串的全排列,先确定第 0 位,然后对后面 n - 1 位进行全排列,在对 n - 1 为进行全排列时,先确定第 1 位,然后对后面的 n - 2 位进行全排列...由此得到递归函数和递归的结束条件。全排列也就是交换位置,到 n - 2 位时,就是将 n - 2 和 n - 1 交换位置。
例子:
- abc,第一位是 a 固定,对后面的 bc 交换位置得 abc,acb。
- 当 a 和 b 交换位置之后,得到 bac,对 ac 进行全排列 bac,bca。
- 当 a 和 c 交换位置之后,得到 cba,对 ba 进行全排列得 cba,cab。
代码如下:
void permute (string str, int i, int n) {
if (i == n) {
cout<<str<<endl;
} else {
for (int j = i; j < n; j++) {
swap(str[i], str[j]);
permute(str, i+1, n);
swap(str[i], str[j]);
}
}
}
复制代码
但是以上算法会出现一个问题,比如字符串为 abb,结果会出错:
Finished in 2 ms
abb
abb
bab
bba
bba
bab
复制代码
所以这种方法只适用于无重复的字符串,还需要进行改进。传送门——无重复数字的全排列数 (LeetCode)
2.递归算法的改进
出现以上问题的原因,主要是因为相同的字符进行了多次交换。举个例子 abb,a 固定时,后面的字符位置不变,得到 abb,当第2个 b 和第3个 b 交换时,又得到了 abb,解决这个问题的思路在于,在交换时进行判断,如果后面的字符有重复就不交换。当第 i 个字符和第 j 个字符交换位置时,判断范围是 [i, j) 是否有和 j 重复的数,代码如下:
判断是否交换的函数:
bool isSwap (string str, int start, int end) {
for (int i = start; i < end;i++) {
if (str[i] == str[end])
return false;
}
return true;
}
复制代码
改进 permute 函数:
void permute (string str, int i, int n) {
if (i == n) {
cout<<str<<endl;
} else {
for (int j = i; j < n; j++) {
if (isSwap(str,i,j)) {
swap(str[i], str[j]);
permute(str, i+1, n);
swap(str[i], str[j]);
}
}
}
}
复制代码
结果为:
Finished in 2 ms
abb
bab
bba
复制代码
3.非递归算法
首先介绍一个知识点,替换点和替换数。假设有字符串 "13421",我们从字符串的最后一位开始扫描,找到第一对递减的字符,在这个例子中,12 不是,24 不是,43 就是了,其中 3 就是替换数,替换数的位置就是替换点。
算法思想:先找到替换点,然后从最后一位开始扫描,找到比替换数大的最小的数【注意必须是最小的】,交换两个的位置,然后将替换点后面的数进行逆序,如 "321" 就逆序成 "123"。如此循环,得到全排列。
注意:在实现时我们可以先对原来的字符串进行排序,这里使用快排函数 qsort(),先对字符串进行排序的好处在于,找比替换数大的最小的数时处理很方便,因为升序排序之后,比替换点大的最小的数就是距离替换点最近的数。而且,升序之后,再根据我们的算法,我们可以发现,原理就是得到所有排列中的最小值,然后寻找下一个排列,下一个排列比当前值大,而且是所有排列中比当前值大的最小排列。
例子:
"13421" --> "11234" 快排
然后根据我们的算法依次输出:
第一次:"11234" 找替换数 3 --> 交换("11243") --> 后面的数逆序("11243")
第二次:"11243" 找替换数 2 --> 交换("11342") --> 后面的数逆序("11324")
第三次:"11324" 找替换数 2 --> 交换("11342") --> 后面的数逆序("11342")
复制代码
根据结果,我们可以判断,综合下来就是输出最小值,然后依次增大。传送门——算法证明
代码如下:
void Reverse(char list[], int a, int b) {
while (a < b)
swap(list[a++], list[b--]);
}
bool nextPermutation(char list[]){
//从字符串的最后一位开始向前扫描
int pEnd = strlen(list) - 1;
int p, q, pFind;//p指向字符串的前一位,q指向字符串的后一位
p = pEnd;
while (p != 0) {
q = p;
--p;
if (list[p] < list[q]){//找降序的两个数,即后一个大于前一个,前一个是替换点
//从后面找比替换数p大的最小的数
pFind = pEnd;
while (list[pFind] <= list[p])//替换点后的数,从后往前一定是递增的
--pFind;
//替换
swap(list[pFind], list[p]);
//替换点之后的数反转
Reverse(list, q, pEnd);
return true;
}
}
// reverse(p, pEnd);//如果没有下一个排列,全部反转
return false;
}
int qSortCmp(const void *pa, const void *pb) {
return *(char*)pa - *(char*)pb;
}
int main(void) {
char list[] = "13421";
//先对字符串进行快速排序
qsort(list, strlen(list), sizeof(list[0]),qSortCmp);
do{
printf("%s\n",list);
} while (nextPermutation(list));
return 0;
}
复制代码