全排列问题全面解析

一、简介

  包含全排列问题、包含重复元素的全排列问题、以及它们的递归和非递归实现、还有如何寻找字典顺序的下一个排列、以及寻找第k个排列数。

二、内容

1、全排列问题(递归解法)

描述

给定一个数组nums,要求给出所有排列情况。

例子:nums = {1,2,3},返回结果为

[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

思路

对于以上nums = {1,2,3}的例子,我们对全排列的情况做一个分类:

  ①以1开头,后面跟着{2,3}的全排列

  ②以2开头,后面跟着{1,3}的全排列

  ③以3开头,后面跟着{1,2}的全排列

而对于{2,3}、{1,3}、{1,2},我们又可以重复使用以上的思路,下面以{2,3}为例分析:

  ①以2开头,后面跟着{3}的全排列

  ②以3开头,后面跟着{2}的全排列

而对于{2}、{3}这些只有一个数的集合,它们的全排列就是它们本身。这也就是递归的终止条件。

以上的思路,用代码按如下方式实现:

对于一个有n个元素的数组a,假设其n个元素分别为a[n-1]。

第一步:

将其分成两个部分,第一个元素a[0]和后面其他元素,全排列表示以a[0]为首的所有排列,等于a[0]加上后面部分的全排列

第二步:

使用a[0]和后面的a[1]交换,同样,得到了以a[1]为首的所有全排列。

第三步:

交换回原来的序列,然后再交换a[0]和a[2]。这样能得到以a[2]为首的所有排列,然后再交换回初始状态

 

 

 第四步:

按上面的交换--求排列--再交换回来的流程分别求出以a[3]-a[n-1]为首的所有排列。

这样就得到了所有的排列情况。

 注意:对后面部分的排列情况调用递归实现,返回条件是当后面部分只有一个元素的时候。

实现:

void get_permute(vector<int>& nums, int pos, vector<vector<int>>& result) {
    if (nums.size() == pos) {
        result.push_back(nums);
        return;
    }
    for (int i = pos; i < nums.size(); i++) {
        swap(nums[pos], nums[i]);
        get_permute(nums,pos+1,result);
        swap(nums[i], nums[pos]);
    }
}
vector
<vector<int>> permute(vector<int>& nums) { vector<vector<int>> result; get_permute(nums,0,result); return result; } //这是测试用例 int main() { vector<int> v = {1,1,3}; auto result = permute(v); for (auto i : result) { for (auto j : i) cout << j; cout << endl; } system("pause"); return 0; }

 

2、含有重复元素的全排列问题(递归解法)

描述:

假如给定的数组有重复元素,比如nums={1,1,2},要求求出全排列,不能有重复。

思路:

考虑其中任意一个递归时的情况,假设其第一部分为a[i],第二部分为a[i+1]到a[n-1]。在这种情况,按照上面的描述将数组分为两个部分求解全排列。(我们假设有元素a[j],其中i+1 ≤ j ≤ n-1)

重复的情况分为以下两种:

  ①a[i] = a[j]时,a[i]和a[j]互换得到的序列与之前一样,再求一遍全排列会有重复

  ②a[j] = a[j2](j2和j取同样范围,但j != j2)。当a[i]与a[j]互换后得到的序列与a[i]与a[j2]互换得到的序列相等,分别对他们求全排列也会出现重复

于是,在上述解法的代码中,get_permute函数的for循环中加入限制条件,让他们跳过这两种情况。

实现:

void get_permuteUnique(vector<int>& nums, int pos, vector<vector<int>>& result) {
    if (nums.size() == pos) {
        result.push_back(nums);
        return;
    }
    for (int i = pos; i < nums.size(); i++) {
        //第一种重复,位置pos和位置i的数重复
        if (i != pos && nums[i] == nums[pos]) continue;
        //第二种重复,位置pos和位置i的数不重复
        //但pos之后有两个位置i和j,nums[i]和nums[j]有重复,导致pos和同一个数交换了两次
        bool find = false;        //用find来指示是否找到了这样的i和j
        if(i != pos){
            int temp = i;
            while (temp < nums.size()) {
                if (temp != i && nums[temp] == nums[i]) {
                    find = true;
                    break;
                }
                temp++;
            }
        }
        if (find == true)
            continue;
        //除去重复结束

        swap(nums[pos], nums[i]);
        get_permuteUnique(nums, pos + 1, result);
        swap(nums[i], nums[pos]);
    }
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
    vector<vector<int>> result;
    get_permuteUnique(nums, 0, result);
    return result;
}

//这是测试用例
int main()
{
    vector<int> v = {1,1,3};
    auto result = permuteUnique(v);
    for (auto i : result) {
        for (auto j : i)
            cout << j;
        cout << endl;
    }

    system("pause");
    return 0;
}

 

3、下一个排列(在讲解非递归实现之前的问题)

描述:

给定一个数,按字典顺序找出下一个排列。(如果给出是最大的,则返回最小)

例子:123,下一个比123大的排列是132

   321,这是最大的数,返回最小的数123

   115,下一个比112大的数是152

思路:

  第一步:我们从后往前遍历,寻找第一个位置i,其中i满足nums[i]<nums[i+1],即前项数比后项数小。因为下一个排列数一定比当前数大,当数组从后往前依次递增的时候表示数是最大的,在i处数突然变小了,说明使用后面的一位比i更大的数来替换i能获得更大的排列数。

  第二步:从i处向后寻找,找到一个比i处数更大,但是和i处的数之差最小的位置j。要得到下一个排列数,一定要使换到u处的数尽可能小,但是要大于i处的数。

  第三步:调换i和j处的数。这样确保了i处的数是最合适的。因为此时数组后半部分中,j处的数是所有大于i处的数中最小的

  第四部:对i+1到末尾的数据进行从小到大排序。最高位替换成了更大的数,故后面的数要尽可能的小,大的数要往后排。

实现:

void nextPermutation(vector<int>& nums) {
    int last = INT_MIN;
    int i;
    for (i = nums.size() - 1; i >= 0; i--) {
        if (nums.at(i) < last) 
            break;
        last = nums.at(i);
    }
    //判断数据是否是最大排列数,如果是,返回最小排列数
    if (i == -1) {
        sort(nums.begin(), nums.end());
        return;
    }
    int min_pos = i;
    int min = INT_MAX;
    for (int j = i; j < nums.size(); j++) {
        if (nums.at(j) - nums.at(i)>0&& nums.at(j) - nums.at(i)< min)
            min_pos = j;
    }
    swap(nums.at(i), nums.at(min_pos));
    sort(nums.begin()+i+1,nums.end());
}

//这是测试用例
int main()
{
    vector<int> v = {2,3,1};
    nextPermutation(v);
    for (auto k : v) {
        cout << k << endl;
    }

    system("pause");
    return 0;
}

4、全排列的非递归实现(不含重复元素)

思路:

上面我们描述了如何寻找下一个排列,嗯,那是非递归实现的,我们只需要对先数进行排序,然后依次调用这个函数就可以了。看实现。

实现:

void nextPermutation(vector<int>& nums,bool& end) {
    int last = INT_MIN;
    int i;
    for (i = nums.size() - 1; i >= 0; i--) {
        if (nums.at(i) < last) 
            break;
        last = nums.at(i);
    }
    if (i == -1) {
        end = true;
        return;
    }
    int min_pos = i;
    int min = INT_MAX;
    for (int j = i; j < nums.size(); j++) {
        if (nums.at(j) - nums.at(i)>0&& nums.at(j) - nums.at(i)< min)
            min_pos = j;
    }
    swap(nums.at(i), nums.at(min_pos));
    sort(nums.begin()+i+1,nums.end());
}

vector<vector<int>> permuteNoRecursion(vector<int>& nums) {
    vector<vector<int>> result;
    bool end = false;
    sort(nums.begin(),nums.end());
    while (end == false) {
        vector<int> thistime = nums;
        result.push_back(thistime);
        nextPermutation(nums,end);
    }
    return result;
}

//这是测试用例
int main()
{
    vector<int> v = {1,2,3};
    auto result = permuteNoRecursion(v);
    for (auto i : result) {
        for (auto j : i)
            cout << j;
        cout << endl;
    }

    system("pause");
    return 0;
}

5、全排列的非递归实现(含重复元素)

思路:

和上面一样利用nextPermutation函数,只是在得到排列之后判断一下是否与之前的排列一样。为此实现了一个简单判断vector<int>是否一致的函数check。应该很好懂了。直接上代码

实现:

void nextPermutation(vector<int>& nums,bool& end) {
    int last = INT_MIN;
    int i;
    for (i = nums.size() - 1; i >= 0; i--) {
        if (nums.at(i) < last) 
            break;
        last = nums.at(i);
    }
    if (i == -1) {
        end = true;
        return;
    }
    int min_pos = i;
    int min = INT_MAX;
    for (int j = i; j < nums.size(); j++) {
        if (nums.at(j) - nums.at(i)>0&& nums.at(j) - nums.at(i)< min)
            min_pos = j;
    }
    swap(nums.at(i), nums.at(min_pos));
    sort(nums.begin()+i+1,nums.end());
}

//判断两个vector<int>是否相等
bool check(vector<int>& nums1, vector<int>& nums2) {
    if (nums1.size() != nums2.size()) return false;
    for (int i = 0; i < nums1.size(); i++)
        if (nums2[i] != nums1[i])
            return false;
    return true;
}

//主逻辑函数
vector<vector<int>> permuteNoRecursion(vector<int>& nums) {
    vector<vector<int>> result;
    vector<int> thistime;
    bool end = false;
    sort(nums.begin(),nums.end());
    while (end == false) {
        if (check(thistime, nums) == false) {
            thistime = nums;
            result.push_back(thistime);
        }
        nextPermutation(nums,end);
    }
    return result;
}


//这是测试用例
int main()
{
    vector<int> v = {1,1,3};
    auto result = permuteNoRecursion(v);
    for (auto i : result) {
        for (auto j : i)
            cout << j;
        cout << endl;
    }

    system("pause");
    return 0;
}

 6、寻找第k个排列(不含重复元素)

第一种:使用前面提到的寻找下一个排列,循环k次,得到第k个排列

第二种:使用排列顺序的规则来直接得到数字

假设有n=4个数,nums={1,2,3,4}

我们可以得到它的有序排列为,共有n!个排列

以1开头 1234
1243
1324
1342
1423
1432
以2开头 2134
2143
2314
2341
2413
2431
以3开头 3124
3142
3214
3241
3412
3421
以4开头

4123
4132
4213
4231
4312
4321

我们首先看第一个位置的数,假设我们要得到第10个排列,以每一个数开头的排列都有(n-1)!种,使用10除(n-1)!,得10/(n-1)! = 1,即第一个数为1,由于10%(n-1)!=4,所以我们要求的排列数在以2开头的排列中位于第四。

接着,n减一,我们使用上一步的余数4/2!得到2,即开头第一个数应该是第二个(由于2在第一个数已经被用到了,所以第二个数应该为3)。

接着后面只有两位,该规律到两位的时候就不符合了。直接判断得出结果。

代码中,使用map的第二个值来表示该数书否已经用过了,而getn函数用来求阶乘

int getn(int n) {
    int result = 1;
    for (int i = 1; i <= n; ++i)
        result = result * i;
    return result;
}
string getPermutation(int n, int k) {
    --k;
    map<int, int> m = { {1,1},{2,1},{3,1},{4,1},{5,1},{6,1},{7,1},{8,1},{9,1} };
    string result = "";
    for (int i = 1; i <= n; ++i) {
        int pos;
        //除的方式到最后两项就不成立了,当剩余两项的时候,直接赋值
        if (n - i > 1)
            pos = k / (getn(n - i)) + 1;
        else if (n - i == 1)
            pos = k + 1;
        else if (n - i == 0)
            pos = 1;
        int j = 0;
        //寻找第pos个数,如果遇到map中第二个数为0,则表示该数已经被用过了
        for (; j < pos; ++j) {
            if (m[j+1] == 0)
                ++pos;
        }
        result += to_string(j);
        m[j] = 0;
        if(n - i > 1)
            k = k % getn(n - i);
    }
    return result;
}

//这是测试用例
int main()
{
    for (int i = 1; i <= 24; ++i) {
        cout << getPermutation(4, i) << endl;
    }

    system("pause");
    return 0;
}
原文地址:https://www.cnblogs.com/likaiming/p/8401086.html