zoukankan      html  css  js  c++  java
  • 全排列与next_permutation

    全排列是面试笔试过程中经常遇到的一个问题。对于练习过的同学来说,这个问题其实
    不算一个难题,但是对于没有练习过的同学,或者说只是知道大致思路的同学来说,
    要在短时间内写出正确的全排列代码还是有点难度的。

    本文是作者在学习全排列时的一个总结笔记,主要包括:
    [1]. 全排列的递归实现
    [2]. 全排列的非递归实现
    [3]. STL中的next_permutation
    全排列的递归实现
    递归方法的全排列思想挺简单的,就是从第一个数字起,将它与其后面的每个数字
    进行交换。

    这是大部分教程或博客告诉我们的。我在读完这句话后也觉得挺简单的,就是一个
    不断交换的过程嘛,例如“123”的全排列就是将1与后面的每个数字交换得到“213”,
    “321”,再将第二个数字与之后的每个数字交换得到“132”,“231”,“312”,这样就
    得到了“123”的全排列:123,213,321,132,231,312.

    然而,当让我在纸上把代码写出来时,就懵了,不知道要从何入手了。
    我对递归的理解也不是很透彻,相信看到这篇文章的你也不会很透彻,
    (透彻的话谁还来搜“全排列的递归实现”啊)。

    所以,接下来,我们不能用人的思路来考虑问题了,要从计算机的角度出发,从
    递归的角度出发来看问题。

    那么,问题转化为:输入数据是“123”,期望得到的结果是顺序输出“123”,“132”,
    “213”,“231”,“312”,“321”. 程序是一个递归程序,其中涉及到的操作包括
    交换每个数字与该数字之后的所有数字。

    递归程序需要至少一个变量来控制递归执行的深度,在这里,最外层的递归需要交换
    “123”中的第一位与之后的每一位,这样可以获得“123”,“213”,“321”,因为全排列
    包括它本身,所以需要与自己交换一次。这之后要控制程序进入第二层递归,也就是
    说,要获取第二位之后的所有数字的全排列 …

    所以,这里我们用一个变量来控制递归的层次,感觉有点像剥洋葱,一层一层下去,
    直到达到洋葱中心(终止条件),再一步一步退出来。
    这里,我用idx来表示这个变量,表示当前递归时元素的索引(index)。

    至于终止条件,当然是i指向最后一个元素时为止,假如元素有n个,那么i=n就是
    递归的终止条件,此时就得到了一组排列,将该组排列输入即可。

    //全排列的递归实现1
    #include <iostream>
    using namespace std;
    void get_permutation(char *a, int idx, int length)
    {
        if (idx == length-1){
            for (int i=0; i<length; ++i)
                cout << a[i];
            cout << endl;
        }
        else {
            for (int i=idx; i<length; ++i){
                swap(a[i],a[idx]);
                get_permutation(a,idx+1,length);
                swap(a[i],a[idx]);
            }
        }
    }
    int main()
    {
        char array[] = "123";
        get_permutation(array,0,3);
        return 0;
    }
    //程序输出结果为:
    123
    132
    213
    231
    321
    312

    可以看出该程序可以正确输出“123”的全排列。但是,细心的读者可能也发现了,
    最后两个顺序反了,虽然也算不上什么大问题,但这个小瑕疵真的很膈应人。

    //全排列的递归实现2
    #include <iostream>
    #include <string>
    using namespace std;
    void get_permutation(string a, int idx, int length)
    {
        if (idx == length-1){
            for (int i=0; i<length; ++i)
                cout << a[i];
            cout << endl;
        }
        else {
            for (int i=idx; i<length; ++i){
                swap(a[i],a[idx]);
                get_permutation(a,idx+1,length);
                swap(a[i],a[idx]);      // 注释
            }
        }
    }
    int main()
    {
        string a = "123";
        get_permutation(a,0,3);
        return 0;
    }
    //程序输出结果为:
    123
    132
    213
    231
    321
    312

    一下子完美了…
    那么,为什么用string来存储数据就可以得到完全不同的结果呢?
    这就是C++中string和字符数组在作为函数参数进行传递时的不同之处。
    (这里理解不是很透彻,需要结合递归进行更深入的分析…)

    当一个string对象作为参数传递给函数时,传递的是引用的一个copy,原来的引用没有改变。
    当一个字符数组作为参数传递给函数时,传递的是这个数组的引用,对它进行函数处理,就是
    对原数组进行处理。

    全排列的非递归实现
    全排列的非递归实现思想是:每次找出当前排列的下一个排列。因此,
    问题就转换为,给一个排列,如何找出它的下一个排列。

    例如:“95382”的下一个排列是“95823”;
    “95832”的下一个排列是“98235”;
    “98532”是全排列的最后一个,没有下一个全排列。

    从右向左找到第一个逆序的数字,例如“95382”的第一个逆序的数字是‘3’;
    从右向左找到第一个大于该逆序数字的数字,“95382”中从右向左大于‘3’
    的第一个数字是‘8’;
    交换这两个数字的位置,得到“95832”;
    再将第一个逆序数字所在位置之后的数字翻转,即得到该排列的下一个排列,
    即“95823”.

    这样,给定一个排列就可以得到其下一个排列,我们就可以安装这种方法依次打印出
    所有排列,就得到了所需的全排列。

    代码如下:

    #include<iostream>
    #include<string>
    #include<algorithm>
    
    template<typename Iterator>
    bool my_next_permutation(Iterator first, Iterator last)
    {
        if (first == last) // s="", s.begin()==s.end()
            return false;
        Iterator i = last;
        if (first == --i)  // s="1", s.begin()==--s.end()
            return false;
        //迭代器string.end()指向string最后一个元素的后一个位置
        //要获取最后一个元素需要回退一步,即--s.end()指向最后一个元素
        //程序运行到此处,i指向最后一个元素,因为i在第二个if语句处
        //执行了‘--i’操作
    
        while (true) {
            Iterator i1 = i, i2;
            if (*--i < *i1) {
                i2 = last;
                while (!(*i < *--i2))
                    ;
                std::iter_swap(i, i2);
                std::reverse(i1, last);
                return true;
            }
            if (i == first) {
                std::reverse(first, last);
                return false;
            }
        }
    }
    
    int main()
    {
        std::string s = "1232";       //baa
        std::sort(s.begin(), s.end());
        do{
            std::cout << s << '
    ';
        }while(my_next_permutation(s.begin(), s.end()));
    
        return 0;
    }

    输出结果为:123,132,213,231,312,321
    正是我们期望的按顺序的输出。

    让我们对该代码中while循环部分进行一个刨根问底的分析。
    从头捋一遍,给定一个排列,获得它的下一个排列的方法为:

    • 首先,从右向左找到第一个非递增的数字的位置i,以953882为例,
    • 第一个非递增的数字为3,并记该数字的前一个数字的位置为i1;

    • 从最后一个数字开始往左找到第一个大于3(位置i)的数字8,
      记其位置为i2(这里的倒数第2位);

    • 交换i与i2位置的数字,得到958832;

    • 将i1到结尾的数字逆序,得到958238,此即为953882的下一个排列958238

    STL中的next_permutation
    话说,要成为世界第一,就得向世界第一学习。

    STL中提供了计算下一个排列的函数next_permutation,利用该函数我们可以方便
    地获得全排列。

    代码如下:

    #include<iostream>
    #include<string>
    #include<algorithm>
    
    using namespace std;
    
    int main()
    {
        string s = "1232";
        sort(s.begin(), s.end());
        do{
            cout << s << '
    ';
        }while(next_permutation(s.begin(), s.end()));
    
        return 0;
    }

    这里的next_permutation的实现思想跟第二部分中全排列的非递归实现的思想一样.
    下面给出cppreference.com
    提供的一种next_permutation的实现方法:

    template<class BidirIt>
    bool next_permutation(BidirIt first, BidirIt last)
    {
        if (first == last) return false;
        BidirIt i = last;
        if (first == --i) return false;
    
        while (true) {
            BidirIt i1, i2;
    
            i1 = i;
            if (*--i < *i1) {
                i2 = last;
                while (!(*i < *--i2))
                    ;
                std::iter_swap(i, i2);
                std::reverse(i1, last);
                return true;
            }
            if (i == first) {
                std::reverse(first, last);
                return false;
            }
        }
    }

    这里的next_permutation的实现方法同第二部分,相信读者也看出来了,
    在第二部分全排列的非递归实现中所使用的next_permutation的方法就是
    cppreference.com
    网站所提供的实现方法。所以,该实现方法的具体分析见第二部分内容,
    这里就不再赘述了。
    总结

    • [1]全排列的递归方法思路比较简单,代码实现起来也更容易。
      但是,它不能解决有重复数字的全排列问题。
      另外需要注意的是,对于不同的数据结构(字符数组和字符串),
      在代码实现上会有所不同。

    • [2].全排列的非递归实现方法既可以解决无重复字符的全排列问题,
      也可以解决有重复数字的全排列问题。但是,相对于递归实现方法来说,
      代码实现稍显复杂,需要对获取下一个全排列的算法思路有透彻的理解。
      不过,幸运的是,STL算法库中已经封装了实现好的next_permutation()
      算法,在不做要求的情况下,我们可以直接拿来使用。

    转处:https://blog.csdn.net/yingyujianmo/article/details/52046398

  • 相关阅读:
    jdbc-------JDBCUtil类 工具类
    jdbc --- javabean
    MapReduce 找出共同好友
    mapReducer 去重副的单词
    用户定义的java计数器
    mapReducer第一个例子WordCount
    win10 Java环境变量,hadoop 环境变量
    Writable序列化
    io 流操作hdfs
    [常用命令]OSX命令
  • 原文地址:https://www.cnblogs.com/laohaozi/p/12538071.html
Copyright © 2011-2022 走看看