综述:
使用递归求解问题有时往往令人费解,博主对递归也是头痛不已,以下问题,利用递归很容易求解。总的来说,递归需要设计成:处理单个问题,递归求解子问题,设置出口。需要铭记的是递归所做的工作和处理单个问题一样,所以只要单个问题取遍所有情况,那么递归同样也能取得所有情况。切记不要去想递归到底怎么一层层的调用,不管是读代码还是写代码,需要关注的是递归出口,程序怎么设计求解单个问题,一旦单个问题解决了,后续子问题就和解决单个问题一样。
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();
}
}
};
输入:[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();
}
}
};
输入:[1,1,2]
输出:[[1,1,2],[1,2,1],[2,1,1]]
这个问题我的解决方法是在问题3的基础上加上重复判断,如果已经包含某个子集,直接略过。
当然还有更好的方法,待完成。
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是否为回文。
总结:
以上可归为一类递归问题,思路相同,变化在于不同的要求,之所以后续没有给出详细解答,是因为希望学习能够举一反三,自己想明白和别人教明白是完全两回事。
参考资料:
欢迎大家扫描关注公众号:编程真相,获取更多精彩的编程技术文章!