字符串全排列和组合算法

 

一、全排列的递归实现

为方便起见,用123来示例下。123的全排列有123、132、213、231、312、321这六种。首先考虑213和321这二个数是如何得出的。显然这二个都是123中的1与后面两数交换得到的。然后可以将123的第二个数和每三个数交换得到132。同理可以根据213和321来得231和312。因此可以知道——全排列就是从第一个数字起每个数分别与它后面的数字交换。找到这个规律后,递归的代码就很容易写出来了:

#include <iostream>

using namespace std;

void perm(char *pstr,char *pbegin)
{
    if(*pbegin == '\0')
    {
        static int count = 1;
        cout << "第" << count++ <<"个排列是:"
        << pstr << endl;
    }
    else
    {
        for(char * pch = pbegin;*pch != '\0';pch++)
        {
            swap(*pbegin,*pch);
            perm(pstr,pbegin+1);
            swap(*pbegin,*pch);
        }
    }
}
int main()
{
    char str[] = {"abcde"};
    perm(str,str);
    return 0;
}

 

另一种写法:

#include <iostream>
#include<stdio.h>
#include<cstring>
using namespace std;
void perm(char *str,int start,int end)
{
    if(start == end)
    {
        static int count = 1;
        printf("第%d个排列:%s\n",count++,str);
    }
    else
    {
        for(int i=start;i<=end;i++)
        {
            swap(*(str+start),*(str+i));
            perm(str,start+1,end);
            swap(*(str+start),*(str+i));
        }
    }
}
int main()
{
    char str[] = {"abcde"};
    perm(str,0,strlen(str)-1);
    return 0;
}

 

二、去掉重复的全排列的递归实现
由于全排列就是从第一个数字起每个数分别与它后面的数字交换。我们先尝试加个这样的判断——如果一个数与后面的数字相同那么这二个数就不交换了。如122,第一个数与后面交换得212、221。然后122中第二数就不用与第三个数交换了,但对212,它第二个数与第三个数是不相同的,交换之后得到221。与由122中第一个数与第三个数交换所得的221重复了。所以这个方法不行。

换种思维,对122,第一个数1与第二个数2交换得到212,然后考虑第一个数1与第三个数2交换,此时由于第三个数等于第二个数,所以第一个数不再与第三个数交换。再考虑212,它的第二个数与第三个数交换可以得到解决221。此时全排列生成完毕。
这样我们也得到了在全排列中去掉重复的规则——去重的全排列就是从第一个数字起每个数分别与它后面非重复出现的数字交换。下面给出完整代码:

#include <iostream>
#include<stdio.h>
#include<cstring>
using namespace std;
bool isswap(char *str,int start,int end)
{
    for(int i=start;i<end;i++)
    {
        if(*(str+i) - *(str+end) == 0)
           return false;
    }
    return true;
}
void perm(char *str,int start,int end)
{
    if(start == end)
    {
        static int count = 1;
        printf("第%d个排列:%s\n",count++,str);
    }
    else
    {
        for(int i=start;i<=end;i++)
        {
            if(isswap(str,start,i))
            {
                swap(*(str+start),*(str+i));
                perm(str,start+1,end);
                swap(*(str+start),*(str+i));
            }
        }
    }
}
int main()
{
    char str[] = {"11000"};
    perm(str,0,strlen(str)-1);
    return 0;
}

三、全排列的非递归实现
要考虑全排列的非递归实现,先来考虑如何计算字符串的下一个排列。如"1234"的下一个排列就是"1243"。只要对字符串反复求出下一个排列,全排列的也就迎刃而解了。
如何计算字符串的下一个排列了?来考虑"926520"这个字符串,我们从后向前找第一双相邻的递增数字,"20"、"52"都是非递增的,"26 "即满足要求,称前一个数字2为替换数,替换数的下标称为替换点,再从后面找一个比替换数大的最小数(这个数必然存在),0、2都不行,5可以,将5和2交换得到"956220",然后再将替换点后的字符串"6220"颠倒即得到"950226"。
对于像“4321”这种已经是最“大”的排列,采用STL中的处理方法,将字符串整个颠倒得到最“小”的排列"1234"并返回false。

这样,只要一个循环再加上计算字符串下一个排列的函数就可以轻松的实现非递归的全排列算法。按上面思路并参考STL中的实现源码,不难写成一份质量较高的代码。值得注意的是在循环前要对字符串排序下,可以自己写快速排序的代码(请参阅《白话经典算法之六 快速排序 快速搞定》),也可以直接使用VC库中的快速排序函数(请参阅《使用VC库函数中的快速排序函数》)。下面列出完整代码:

#include <iostream>
#include<stdio.h>
#include<cstring>
#include<algorithm>
using namespace std;
void Reverse(char *pbegin,char *pend)
{
    while(pbegin < pend)
    {
        swap(*pbegin++,*pend--);
    }
}
bool next_perm(char str[])
{
     int len = strlen(str);
     char * pend = str+len-1;
     if(pend == str)
     return false;
     char *pre,*last;
     last = pend;
     while(last != str)
     {
         pre = last-1;
         if(*pre < *last)
         {
             char *pfind = pend;
             while(*pfind < *pre)
             pfind --;
             swap(*pre,*pfind);
             Reverse(last,pend);
             return true;
         }
         last--;
     }
     Reverse(str,pend);
     return false;

}
int main()
{
    char str[] = {"abdce"};
    sort(str,str+strlen(str));
    cout << str << endl;
    do
    {
        static int count=1;
        printf("第%d个排列是:%s\n",count++,str);
    }while(next_perm(str));
    return 0;
}


至此我们已经运用了递归与非递归的方法解决了全排列问题,总结一下就是:
1、全排列就是从第一个数字起每个数分别与它后面的数字交换。
2、去重的全排列就是从第一个数字起每个数分别与它后面非重复出现的数字交换。
3、全排列的非递归就是由后向前找替换数和替换点,然后由后向前找第一个比替换数大的数与替换数交换,最后颠倒替换点后的所有数据。



 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值