再解全排列问题的思考

444 阅读3分钟

今天刷剑指offer,写到了剑指 Offer 38. 字符串的排列,发现自己全排列问题并没有完全的掌握。

事实上在几个月前自己就为全排列问题专门写了博客,用到了深搜和广搜两种方法,当时觉得全排列对自己来说已经不成问题了,结果今天做到全排列相关的题的时候并不能在第一时间开始动手就写(但我相信给我点时间一定能写出来)。

上学期的算法课上有讲到全排列问题,当时看到书上的解法才发现自己之前的代码无论在时间还是空间上都烂极了,于是决定再写一篇全排列的博客巩固一下。

之前的全排列博客:深搜求解全排列广搜求解全排列

本文涉及的两道题目:leetcode 46. 全排列leetcode 47. 全排列 II

无重复元素的全排列

考虑数组[1,2,3,4],如上图所示,每个元素都可以作为排列的起始元素,那么就可以将整个问题的解划分为4类。而每一个类的解集即该类起始元素与剩余其余元素的全排列的组合。

class Solution {
public:
    vector<vector<int>> result;
    vector<int> nums;

    void help(int begin, int end) {
        if (begin == end) {
            result.push_back(nums);
        }
        for (int i = begin; i < end; ++i) {
            swap(nums[begin], nums[i]);
            help(begin+1, end);
            swap(nums[begin], nums[i]);
        }
    }

    vector<vector<int>> permute(vector<int>& nums) {
        this->nums = nums;
        help(0, nums.size());
        return result;
    }
};

上面的代码中,循环中遍历每一个元素,与数组的起始元素交换,接着将剩余元素通过help函数做全排列,当问题规模为0时,此时nums处于一个排列状态下,将其并入result结果集中。

有重复元素的全排列

考虑数组[1,1,3,4],如上图所示,整个结果集应该是划分为3类而不是4类,如果是4类即其中两类都以1开始,那么结果集中就会有相同的排列。问题的难点即在于如何去重。这道题目我的解法和leetcode官方题解的解法是不一样的(自认为自己的比题解的好),我的解法继承了无重复元素全排列的代码,通过visited数组去重。

通过上面的图片不难想到,只要在遍历过程中加上适当的筛选条件,让每个能够和起始元素交换的元素彼此之间不重复即可达到去重的目的,这里使用visited[n] = 1表示值等于n的元素已经与起始元素交换过,注意这里的n不是下标而是元素值。

class Solution {
public:
    vector<vector<int>> result;
    vector<int> nums;

    void help(int begin, int end) {
        if (begin == end) {
            result.push_back(nums);
        }
        vector<int> visited(21, 0);
        for (int i = begin; i < end; ++i) {
            if (visited[nums[i]+10]) continue;
            visited[nums[i]+10] = 1;
            swap(nums[begin], nums[i]);
            help(begin+1, end);
            swap(nums[begin], nums[i]);
        }
    }

    vector<vector<int>> permuteUnique(vector<int>& nums) {
        this->nums = nums;
        help(0, nums.size());
        return result;
    }
};

本题目给出了-10 <= nums[i] <= 10,因此vector<int> visited(21, 0);,在标记时需要在[-10,10][0,20]之间做一个转换。

在得到正确解法之前,我曾陷入一个误区,试图不通过标记数组来做到起始元素的去重。

sort(nums.begin(), nums.end());
for (int i = begin; i < end; ++i) {
    if (i > 0 && nums[i] == nums[i-1]) {
        continue;
    }
}

上面的代码确实做到了每次循环都是不同的元素,用来与起始元素交换,但是交换之后剩余的元素已经不满足有序的性质了,而help函数对于排列区间确是要求有序,我也曾想过通过临时数组作为函数的参数,但不仅空间开销大,而且代码量和理解难度上也都不如使用标记数组。