STL全排列函数详解

一、前言

    STL中有两个关于全排列的函数,分别为next_permutation(下一个排列)和prev_permutation(上一个排列),这两个算法都是以“字典序”为准则进行全排列的。

二、字典序

    字典序是一种规定好的排序准则,用于比较字符(串)或整数之间的大小,一般是用于比较两个序列的大小,其比较准则如下:

    将两个序列中的元素一一比较,知道一下情况发生:

        (1)如果比较到两个元素时,这两个元素不相等,则这两个元素的比较结果就作为两个序列的比较结果;

        (2)如果情况(1)没有发生,且有一个序列的元素已经比较完,即该序列比另一个序列短,则比较短的这个序列小于另一个序列;

        (3)如果情况(1)(2)都没有发生,说明两个序列的每个元素都比较完成,即两个序列大小一样,且每个对应元素都对应相等,则这两个序列相等。

    如:“abc” < “acc”,”abc” < “ac”;情况(1)

            “abc” < “abca”,”a” < “ac”;情况(2)

            “abd” = “abd”;情况(3)

三、某一序列的全排列

    所谓某一序列的全排列,指的是:“序列上每一位都取完该序列所包含的所有元素”,如“123”的全排列为:“123”、“132”、“213”、“231”、“312”、“321”,共6种情况。

    而所谓的某一个排列的下一个排列,指的是在该序列的全排列中,紧接着本排列的下一个排列,而一般全排列都是按照升序进行排列的,即按照“字典序”的升序,第一个排列小于下一个排列,如“123” < “132”,也就是说按照“字典序”,“123”是其全排列中最小的,比较刚刚6种情况,确实是这样的。也就是说下一个排列指的是在全排列中大于本排列的最小排列。

    而某一排列的上一排列就是在全排列中小于本排列的最大排列。

    但是,上面关于下一排列和上一排列的解释遗漏了一种特殊情况:对于下一排列,如果当前排列是全排列中的最后一个排列,则其下一排列就是全排列中的第一个排列;对于上一排列,如果当前排列是全排列中的第一个排列,则其上一个排列就是全排列中的最后一个排列。

四、实现STL全排列算法

    STL中已经为我们提供了下一排列和上一排列的接口算法,分别为:next_permutation(下一个排列)和prev_permutation(上一个排列),这是很方便的,如果有需要可以直接调用,但是为了理解全排列我们还是有需要自己实现上一排列和下一排列函数。

    1、下一排列

        如果给定一个序列的任一排列,如排列:“6 5 4 8 7 5 1”,怎么求其下一排列?首先,想一下下一排列的定义:全排列中大于本排列的最小排列。怎么找到这个最小排列呢?根据“字典序”的规则,首先,每个排列的长度一样,所以不可能出现情况(2),同样,也不会出现情况(3),因为每个排列都不一样,所以,只能出现情况(1)的情况。这里需要再详细的分析一下情况(1),全排列是按照“字典序”的升序排列,情况(1)说明一一比较下一排列和当前排列的每一个元素,到某一元素时,当前排列中该元素小于下一排列中该位置元素,所以为了找到“全排列中大于本排列的最小排列”,需要从本排列中最后一个元素开始寻找下一排列,尝试交换“6 5 4 8 7 5 1”中的1和5,发现并不是变大了,而是变小了,同样,1和5、7、8以及前面的都无法交换,在尝试交换5和7,也不能,直到5和4交换后,当前排列变成“6 5 5 8 7 4 1”,但该排列并不是全排列中大于本排列的最小排列,比如“6 5 5 4 7 8 1”就大于本排列,但是也小于“6 5 5 8 7 4 1”排列,下面总结下一排列算法流程

(1)找到当前排列中最后一个升序二元数对,比如“6 5 4 8 7 5 1”中是“4 8”,找到元素4后面的最后一个大于4的元素,这里是5(注意,这里有一个隐含的意义是最后一个升序数对后面必定是一个递减序列,因为我们找的是最后一个升序数对),然后交换4和5,所以数列就变成了“6 5 5 8 7 4 1”;

        (2)然后再找到大于本排列的最小排列,很简单,只需要递增的对4后面的元素进行排序,就得到了最后的序列“6 5 5 1 4 7 8”;

        (3)求下一排列是需要注意特殊情况就是如果当前排列是全排列中的最后一个排列,则只需要逆序输出当前排列,就得到了下一排列。

    具体代码如下:

void nextPermutation(vector<int>& nums) 
    {
        int pos = -1;
        auto len = nums.size();
        //找到最后一个升序位置(注意是最后一个升序)
        for(int j=len-1; j>0; --j)
        {
            if(nums[j]>nums[j-1])
            {
                pos = j-1;
                break;
            }
        }
       
        //此时,pos指向第一个升序位置的前一个元素
        if(pos < 0)
        {
            reverse(nums.begin(),nums.end());
            return;
        }
        else
        {
            for(int i=len-1; i>pos; i--)
            {
                if(nums[i]>nums[pos])
                {
                    swap(nums[pos], nums[i]);    //交换
                    break;
                }
            }
        }
        reverse(nums.begin()+pos+1, nums.end());
        copy(nums.begin(), nums.end(), ostream_iterator(cout " "));      //输出下一排列
    }

    2、上一排列

        有了对求下一排列的解释基础,求上一排列就很简单了,就是一个逆向思维,其具体流程如下:

        (1)首先,找到当前序列(“6 5 5 1 4 7 8”)中的最后一个降序二元数对,如果找到了,则这个数对后面的所有元素都是升序的,这里最后一个降序二元数对是“5 1”;

        (2)然后,找到5后面元素中最后一个小于5的元素(注意,第一步中5后面是一个递增数列,所以很好找到最后一个小于5的元素),然后交换5和这个元素,这里是4,交换后得到数列“6 5 4 1 5 7 8”,最后,对4后面的元素进行递减排序,得到上一个数列“6 5 4 8 7 5 1”;

        (2)同样,这里也需要注意特殊情况,如果当前数列是全排列的第一个排列,则其上一排列就是直接翻转当前排列即得到上一排列。

点赞