字符串全排列

字符串的全排列是字符串类的算法题的一个考察点,属于普通问题,它有两种实现方法,递归算法和非递归算法,非递归的方法要稍微难一点,以下会依次进行介绍。

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
复制代码

传送门——有重复数字的全排列数 (LeetCode)

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;
}
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值