• 递归讨论(三)


    递归对于编程者来说,是比较难理解的,但当你完全清晰程序思路时,它会变得容易理解了。

    递归思想成为解决一些编程难题所常用的,所以多多练习,多多理解它,会让我们对之爱不释手,作为初学者的我,每当理解如何递归的解决问题时,就会非常开心,这也许就是挑战所带来的成就感,所学即所得的满足感吧。

    下面以如下一道例题为例吧:

    在数据结构(c语言版)一书中,页码p8,有如下一题

    问题1:<全排列>对给定的n个元素的集合,输出该集合所有可能的排列;

    我们对比着看下面一道问题,可以先思考思考:

    问题2:<全排列>对给定的n个元素的序列(可能含重复元素),输出该序列所有可能的排列。(后面来解析该题。)

    我们来先说说问题1吧:

    即然是n个元素的集合,那么由众所周知的集合定义,我们可以明确得出:这n个元素是互不相同的。

    那么问题转换为求n个不重复元素的全排列,同问题2有着显著区别哦。

    对于该问题:

    首先对于全排列:

    首字符是这n个元素中的其中一个,如果首字符不同,我们得到的排列也必是不同的。

    其次:(核心思想)

    我们可以先预定某个元素为首字符,再对剩余字符进行全排列(嘿嘿,此处是不是回归到我们开始要求的问题了呀,只不过此处变成了对n-1的全排列了啊,这这。。。显然明确是递归的结构啊)

    最后:

    我们循环的将首字符换成与之前不同的字符(这句话要好好理解,问题2的小核心),再对剩余进行全排列,直至首字符遍历完n个元素。

    文字太枯燥,来个例子,比如:对{a,b,c,d}进行全排列,

    以a为头,对{b,c,d}进行全排列

    以b为头,对{a,c,d}进行全排列

    以c为头,对{a,b,d}进行全排列

    以d为头,对{a,b,c}进行全排列

    问题的规模,在递归的结构下,由规模4变成了规模3了,然后规模3又在递归的结构下变成了规模2.。。。。。最后递归到只有1个规模了,那么此时,说明当前递归已到递归基上了,说明已进行了一次全排列。此时我们就可以输出当前的排列了。

    运行到此处,我们还要注意些程序实现的细节,不然很难实现最终的结果哦。

    先上个代码,代码里有相关说明,会让你更明白些。

    #include<iostream>
    
    void perm(char*,int,int);
    int main()
    {
        using namespace std;
        char* p=new char[4];
        for(int i=0;i<4;i++)
            cin >> *(p+i);
        perm(p,0,3);
        cout << num;
        for(int i=0;i<4;i++)
            cout << p[i] << "--";
        delete[] p;
        return 0;
    }
    
                                                //递归产生的全排列
    void perm(char* p,int a,int b)              //a表示待全排的数组下标开始,b表示下标尾
    {
        if(a == b)                              //递归基,说明只有一个元素了
        {
            for(int i=0;i<=b;i++)               //此时按下标顺序输出当前数组中的元素,因为数组通过交换元素的方式实现了排列。
            {
                std::cout << p[i];
            }
            std::cout << std::endl;
        }
        else
        {
            for(int j=a;j<=b;j++)               //该处的循环作用就是,让首字符取遍所有可能字符,每次循环,都会将首字符为当前循环
                                                       //字符的所有可能排列全部输出
            {
                std::swap(p[a],p[j]);           //此处就是通过交换元素的方式让当前首字符更改,不同于之前循环,也惟有这样处理,、
                                                       //首字符才可能取遍所有可能字符
                  perm(p,a+1,b);                  //此处递归调用,因为首字符我们已经确定了,我们只需对剩余元素进行全排列,所以只需作a+1处理即可
                  std::swap(p[a],p[j]);           //将数组还原到初始状态,对于此处,开始我比较难理解,下面画个调用图,看看它是如何还原的
            }
        }
    }

    为了简化时序分析,我们这里分析的是3个元素的全排序,其调用如下图所示:

    我们可以在这里清晰的看到调用的过程,以及相关变量的值变化。我们如果由3个元素推广更多的元素,我们会发现其中很多自相似的调用图谱。另外我们也注意到,在一个完整的for循环中,每个循环开始前,p数组的序列都是一样的。

    主要由于两个swap的作用,也算是此处的小核心了。

    也惟有保证每次循环前,p序列回归原始状态,我们才能保证下次循环开始,将下一个不同的字符放在首字符。

    下面这个图画的略草,但还可以凑和着看了。

    调用时序图

    我们总结问题1的解决方案:

    首先要保证首字符能取遍所有可能字符,再对余下字符进行全排列

    其次在排列完后,我们要把字符序列回复到原始状态,

    这个过程类似走路一样,我们怎么走去的,我们原路返回,我们就能准确的回归到原点。(这个有点意思呵,仔细想想就是哦)。

    下面我们来分析分析问题2:

    首先说下排列数,对于aabbbccdd,它的排列数算法如下:

    clip_image001

    (具体算法自己查书去,或者自己回高中学学排列组合)

    问题2与1的主要区别在于,2中的序列可能含重复的,所以算法过程会有所区别,但两者的算法思想在大致都差不多,

    但问题2多了的是判断,要判断的是我们是否产生了重复排列。

    若按照问题1的思路,我们确定首字符,然后排列剩余的,

    因为存在相同字符,我们可能会把某个字符两次或多次放在首字符,但这种情况,两者要排序的内容完全,

    首字符都一样,剩余的字符肯定也是完全一样的啊,那么排列的内容就重复了啊。

    所以我们要在此处添加一个判断,我们遍历时,将当前字符与其之前的字符(即已经遍历过的字符)相比较,若在遍历过的字符中发现了同样的字符,那么我们就立即结束当前循环,进入下次循环。呵呵,这样就跳过重复了。也是比较好理解的了。

    代码如下:

    #include<iostream>
    void perm(char*,int,int);
    int main()
    {
        using namespace std;
        bool flag=true;
        while(flag)
        {
            cout << "input the number:";
            int n;
            cin >> n;
            cout << "enter " << n << " character:";
            char* p=new char[n];
            for(int i=0;i<n;i++)
                cin >> *(p+i);
            perm(p,0,n-1);
            cout << "continue?(y/n)";
            char ch;
            cin >> ch;
            if(ch!='y')
            {
                flag=false;
            }
            delete[] p;
        }
    
        return 0;
    }
    
    void perm(char* p,int a,int b)
    {
        if(a == b)
        {
            for(int i=0;i<=b;i++)
            {
                std::cout << p[i];
            }
            std::cout << std::endl;
        }
        else
        {
            for(int j=a;j<=b;j++)
            {
                bool flag = false;
                for(int i=a;i<j;i++)
                {
                    if(p[i]==p[j])
                    {
                        flag = true;
                    }
                }
                if(flag)
                {
                    continue;
                }
                std::swap(p[a],p[j]);
                perm(p,a+1,b);
                std::swap(p[a],p[j]);
            }
        }
    }

    运行结果如下:

    image

    由于这个全排列成阶乘特征,比较大,下面的程序就只显示排列数了:

    #include<iostream>
    void perm(char*,int,int,int&);
    int main()
    {
        using namespace std;
        bool flag=true;
        while(flag)
        {
            int num=0;
            cout << "input the number:";
            int n;
            cin >> n;
            cout << "enter " << n << " character:";
            char* p=new char[n];
            for(int i=0;i<n;i++)
                cin >> *(p+i);
            perm(p,0,n-1,num);
            cout << "the number of permutation:" << num << endl;
            cout << "continue?(y/n)";
            char ch;
            cin >> ch;
            if(ch!='y')
            {
                flag=false;
            }
            delete[] p;
        }
        return 0;
    }
    
    void perm(char* p,int a,int b,int& num)
    {
        if(a == b)
        {
            num++;
        }
        else
        {
            for(int j=a;j<=b;j++)
            {
                bool flag = false;
                for(int i=a;i<j;i++)
                {
                    if(p[i]==p[j])
                    {
                        flag = true;
                    }
                }
                if(flag)
                {
                    continue;
                }
                std::swap(p[a],p[j]);
                perm(p,a+1,b,num);
                std::swap(p[a],p[j]);
            }
        }
    }

    运行结果如下:

    image

  • 相关阅读:
    python开发初识函数:函数定义,返回值,参数
    py基础2--列表,元祖,字典,集合,文件
    python中的urlencode与urldecode
    使用pymysql进行mysql数据库操作
    docker 命令
    docker镜象
    docker的安装
    JS中的prototype(原文地址:http://www.cnblogs.com/yjf512/archive/2011/06/03/2071914.html)
    linux远程复制和压缩文件的命令
    rosbag 初尝试
  • 原文地址:https://www.cnblogs.com/qinghua0310/p/4087333.html
Copyright © 2020-2023  润新知