数字/字符串排列组合(Leetcode) 总结

综述:

使用递归求解问题有时往往令人费解,博主对递归也是头痛不已,以下问题,利用递归很容易求解。总的来说,递归需要设计成:处理单个问题,递归求解子问题,设置出口。需要铭记的是递归所做的工作和处理单个问题一样,所以只要单个问题取遍所有情况,那么递归同样也能取得所有情况。切记不要去想递归到底怎么一层层的调用,不管是读代码还是写代码,需要关注的是递归出口,程序怎么设计求解单个问题,一旦单个问题解决了,后续子问题就和解决单个问题一样。

1.数组的全部子集

输入:[1,2,3]

输出:[[3],[1],[2],[1,2,3],[1,3],[2,3],[1,2],[]]

这个题目如何设计递归?我们可以把输入数据分为第一个元素和剩下的元素(这样分析正符合递归的思想),那么输入的子集中只有两种情况:

1.包含第一个元素

2.不包含第一个元素

对应着程序就是取第一个元素,不取第一个元素。那么程序该如何设计?

我们可以想象:

取第一个元素,相当于将第一个元素加入到后续递归的问题中,即后续递归解中一定包含第一个元素;

不取第一个元素,那其实更简单,只需要递归求解剩下的所有元素,即略过第一个元素。

那么我们这样就已经取遍所有情况了吗?我说是的,读者不明白可以再多思考一下。别人讲明白和自己想明白完全不是一回事,后者往往更重要。

这里直接贴出Leetcode中的递归方案,并加以解释:

class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        //这里排序并不必须,只是为了子集中元素以递增排序
        sort(nums.begin(), nums.end());
        vector<vector<int>> subs;
        vector<int> sub;  
        genSubsets(nums, 0, sub, subs);
        return subs; 
    }
    void genSubsets(vector<int>& nums, int start, vector<int>& sub, vector<vector<int>>& subs) {
        //我们发现这里好像并没有递归出口,因为终止条件由下面for循环控制
        subs.push_back(sub);        
        for (int i = start; i < nums.size(); i++) {
            //情况一:子集包含元素nums[i]
            sub.push_back(nums[i]);
            //为何是i+1,因为已经解决了第i个元素,需要递归从第i+1个元素开始求解
            genSubsets(nums, i + 1, sub, subs);
            //情况二:子集不包含nums[i],即略过第i个元素
            //可以想象,不去管上一条递归语句,当下一次循环到i+1时,第i个元素已经略过
            sub.pop_back();
        }
    }
};

2.数组的全部子集(包含重复元素)

输入:[1,2,2]

输出:[[1],[2],[1,2,2],[2,2],[1,2],[]]

这和上面的问题只多了重复的元素,在上面for循环的开始加上一句:

if(i > start && nums[i] == nums[i-1]) continue;

并且,对输入数组先排序,这里就是必须的了,为何加上这个就能避免重复?一定要特别关注i>start,这说明了如果去重,必须第i-1个元素也在当前循环中,意思是for循环已经处理完第i-1个元素了,这时for循环里对i-1的递归也结束了,最后一条语句sub.pop_back()也执行完了,现在for循环要处理第i个元素了。那这时为什么要略过第i个元素呢?举个例子,因为start<=i-1,我们关注从start开始的循环,那么当循环到第i-1个元素结束时,子集中一定包含{nums[start],……,nums[i-1]},现在请注意,循环第i-1个元素结束时,第i-1个元素已经弹出,下一次循环到第i个元素时,如果nums[i]==nums[i-1]我们不略过第i元素,那么必定会产生{nums[start],……,nums[i]},这里面一定不包括nums[i-1](已经弹出),这样就产生了重复。

读者可能会问,那么像{1,2,2}这种时怎么产生的呢?这个问题留给读者自己思考。

3.数组全排列问题

输入:[1,2,2]

输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

这是一个非常典型的递归问题,关键是如何将它转化为一个递归问题。对于单个问题,如何能够取得它的每一种情况?只有在单个问题中考虑所有的情况,自然递归子问题也能取遍所有情况。借图说明问题

 

我们还可以将输入数组看成两部分,就是第一个元素和剩余元素,我经常强调这样考虑问题的重要性。看出解决这个问题分两步:

1.对于排列,与其说第一个元素,倒不如说第一个位置。第一步就是在第一个位置取遍所有的元素

2.第二步就是按照第一步处理方式递归求解剩下的元素。

怎样才能在第一个位置取遍所有元素?我们可以将第一个元素每一次都和后面的元素交换位置,那么第一个位置就取遍了所有的元素(对应图片中的(1)->(2)),既然第一个位置所有解都已经解决,我们就求解剩下的位置,即递归求解和第一个元素交换后的剩下的元素,处理方式当然和处理第一个元素一样。第一个元素和后面元素每次交换后,每次后面的元素不可能相同,因此不用考虑递归时会出现重复的情况。结合下面代码,可能更好理解。

class Solution{
public:
	vector<vector<int>> permute(vector<int>& nums) {
		//sort(nums.begin(), nums.end());
		vector<vector<int>> subs;
		vector<int> sub;
		genSubsets(nums, 0, subs);
		return subs;
	}
	void genSubsets(vector<int>& nums, int start, vector<vector<int>>& subs) {
		if (start >= nums.size()){
			subs.push_back(nums);
			return;
		}
		for (int i = start; i < nums.size(); i++) {
			swap(nums[start], nums[i]);
			//sub.push_back(nums[i]);
			genSubsets(nums, start + 1, subs);
			swap(nums[start], nums[i]);
			//sub.pop_back();
		}
	}
};

4.数据全排列问题(包含重复元素)

输入:[1,1,2]

输出:[[1,1,2],[1,2,1],[2,1,1]]

这个问题我的解决方法是在问题3的基础上加上重复判断,如果已经包含某个子集,直接略过。

当然还有更好的方法,待完成。

5.数组子集的和为指定元素的全部组合(数组元素可重复使用)

For example, given candidate set [2, 3, 6, 7] and target 7
A solution set is: 

[
  [7],
  [2, 2, 3]
]

同样也是类似的递归方法

6.数组子集的和为指定元素的全部组合(数组元素不可重复使用)

For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8
A solution set is: 

[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]

和5稍有不同,递归时需要从下一个元素遍历。

7.回文分区问题

给出一个字符串,找出其所有的回文分割情况

For example, given s = "aab",
Return

[
  ["aa","b"],
  ["a","a","b"]
]

同样也是类似的递归求解,不同的是需要判断从start到i是否为回文。

总结:

以上可归为一类递归问题,思路相同,变化在于不同的要求,之所以后续没有给出详细解答,是因为希望学习能够举一反三,自己想明白和别人教明白是完全两回事。

参考资料:

字符串全排列

Leetcode

 欢迎大家扫描关注公众号:编程真相,获取更多精彩的编程技术文章!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值