递归、回溯与分治举例说明
递归函数与回溯法
什么叫递归函数:编程语言中,函数Func(Type a,……)直接或间接调用函数本身,则该函数称为递归函数。递归函数不能定义为内联函数。
举例说明:
#include <stdio.h>
void compute_sum(int i,int &sum){
if(i>3){//设置终止条件
return;
}
sum+=i;
compute_sum(i+1,sum);//在compute_sum中调用了自身
}
回溯法: 又称为 试探法 ,但当 探索 到某一步时,发现原先 选择达不到目标 ,就 退回一步重新选择 ,这种 走不通就退回再走 的技术为回溯法。
例1:求子集
已知一组数(其中 无重复元素 ),求这组数可以组成的 所有子集 。
结果中不可有 重复的 子集。
例如: nums[] = [1, 2, 3]
结果为: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
分析1
在所有子集中,生成各个子集, [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]],即是否选中[1],是否选中[2],是否选中[3]的问题。
利用回溯方法生成 子集 ,即对于 每个元素 ,都有试探 放入 或 不放入 集合中的两个选择:
选择 放入 该元素, 递归的 进行后续元素的选择,完成放入该元素后续所有元素的试探;之后 将其拿出 ,即 再进行一次 选择 不放入 该元素, 递归的 进行后续元素的选择,完成不放入该元素后续所有元素的试探。
本来选择放入,再选择一次不放入 的这个过程,称为回溯试探法。
例如:
元素数组: nums = [1, 2, 3, 4, 5,…] ,子集生成数组item[] = []
对于 元素1 ,
选择 放入 item,item = [1],继续递归处理后续[2,3,4,5,…]元素;item = [1,…]
选择 不放入 item,item = [],继续递归处理后续[2,3,4,5,…]元素;item = […]
对每个元素进行放入与不放入的试探:
示例代码:
class Solution {
public:
vector <vector<int>>subsets(vector<int>&nums) {
vector<vector<int>>result;//存储结果的vector
vector<int>item;//回溯时,产生各个子集的数组
result.push_back(item);//初始化将空集填入result
generate(0, nums, item, result);//计算各个子集
return result;
}
void generate(int i, vector<int>&nums, vector<int>&item, vector<vector<int>>&result) {
if (i>=nums.size())//递归结束的条件:将所有的元素都已经试探了一遍
{
return;
}
item.push_back(nums[i]);//对于元素放入的试探
result.push_back(item);//将当前生成的子集填入result
generate(i + 1, nums, item, result);//第一次调用递归
item.pop_back();//对于元素不放入的试探
generate(i + 1, nums, item, result);//第二次调用递归
}
};
int main() {
vector<int > nums;
nums = {1,2,3};
Solution solve;
vector<vector<int>>result = solve.subsets(nums);
/*打印输出*/
for (int i = 0; i < result.size(); i++)
{
for (int j = 0; j < result[i].size(); j++)
{
printf("%d ", result[i][j]);
}
printf("\n");
}
/*输出为:
1
1 2
1 2 3
1 3
2
2 3
3
*/
分析2
下面考虑一种全新的思维:按位运算法。在开始的时候已经说过了,我们要找所有的子集,那么就是在找是否选[1],[2]或[3]的问题,假设现在若一个集合有三个元素A,B,C,则三个元素有2^3=8种组成方式,可以用0和7来表示这些集合。
A 元素为100 = 4;B 元素为010 = 2;C 元素为001 = 1。如 构造某一集合,即使用A,B,C对应的三个整数与该集合对应
的整数做& 运算,当为真 真时,将该元素push 进入集合。
class Solution {
public:
vector<vector<int>>subsets(vector<int>&nums) {
vector<vector<int>>result;
vector<int>item;
int all_set = 1 << nums.size();//设置全部集合的最大值+1,1<<n表示,2^n
for (int i = 0; i < all_set; i++)//遍历集合
{
item.clear();
for (int j = 0; j < nums.size(); ++j) {
if (i&(1 << j)) {//构造数字i代表的集合,各个元素存储值item
item.push_back(nums[j]);//只要出现的就存入result
}
}
result.push_back(item);
}
return result;
}
};
int main() {
vector<int > nums;
nums = {1,2,3};
Solution solve;
vector<vector<int>>result = solve.subsets(nums);
for (int i = 0; i < result.size(); i++)
{
for (int j = 0; j < result[i].size(); j++)
{
printf("%d ", result[i][j]);
}
printf("\n");
}
return 0;
}
/*
输出:
1
2
1 2
3
1 3
2 3
1 2 3
*/
例1:找子集(进阶)
已知一组数(其中 有重复元素 ),求这组数可以组成的 所有子集 。结果中 无重复的 子集。
例如: nums[] = [2, 1, 2, 2]
结果为: [[], [1], [1,2], [1,2,2], [1,2,2,2], [2], [2,2], [2,2,2]]
注意: [2,1,2] 与[1,2,2] 是重复的集合!
分析:
对于本题来说,与上题最大的不同就是,可能会有重复的集合,重复可能的两种情况:
1.不同位置的元素组成的集合是 同一个子集 , 顺序相同 :
例如: [2, 1, 2, 2] ,
选择第1,2,3个元素组成的子集:[2,1,2];
选择第1,2,4个元素组成的子集:[2,1,2]。
2.不同位置的元素组成的集合是 同一个子集 , 虽然 顺序不同 ,但仍然
代表了同一个子集,因为 集合中的元素是无序的 。
例如: [2, 1, 2, 2] ,
选择第1,2,3个元素组成的子集:[2, 1, 2];
选择第2,3,4个元素组成的子集:[1, 2, 2]。
解决方案:对此,如果将原始的数组先进行排序,就不会出现第二种情况的重复了,在针对第一种情况,使用set消除重复
示例代码:
class Solution {
public:
vector <vector<int>>subsets(vector<int>&nums) {
vector<vector<int>>result;//存储结果的vector
vector<int>item;//回溯时,产生各个子集的数组
set<vector<int>>res_set;//去重复使用的集合set
sort(nums.begin(),nums,end());//对数组进行排序
result.push_back(item);//初始化将空集填入result
generate(0, nums, item, result);//计算各个子集
return result;
}
void generate(int i, vector<int>&nums, vector<int>&item, vector<vector<int>>&result) {
if (i>=nums.size())//递归结束的条件:将所有的元素都已经试探了一遍
{
return;
}
item.push_back(nums[i]);//对于元素放入的试探
if(res_set.find(item)==res_set.end()){//使用set去重复
result.push_back(item);
res_set.insert(item);
}
generate(i + 1, nums, item, result);//第一次调用递归
item.pop_back();//对于元素不放入的试探
generate(i + 1, nums, item, result);//第二次调用递归
}
};
int main() {
vector<int > nums;
nums = {1,2,3};
Solution solve;
vector<vector<int>>result = solve.subsets(nums);
/*打印输出*/
for (int i = 0; i < result.size(); i++)
{
for (int j = 0; j < result[i].size(); j++)
{
printf("%d ", result[i][j]);
}
printf("\n");
}
例2:生成括号
已知n组括号,开发一个程序,生成这n组括号所有的 合法的 组合可能。
例如:n = 3
结果为: ["((()))", “(()())”, “(())()”, “()(())”, “()()()”]
分析:
n 组括号 ,有 多少种组合 可能?
n组括号,括号字符串长度为2*n,字符串中的每个字符有 两种选择 可能,“(”或“)”,故有2^2n种可能。
例如2组括号,所有的组合可能:
[’((((’, ‘((()’, ‘(()(’, ‘(())’, ‘()((’, ‘()()’, ‘())(’, ‘()))’, ‘)(((’, ‘)(()’,’)()(’, ‘)())’, ‘))((’, ‘))()’, ‘)))(’, ‘))))’ ]
在这16种可能中,满足 哪些 是合法的?有 多少种合法 可能?如何 生成它们?
我们可以使用递归来生成所有的可能组合。但是,怎么样的字符串才是符合我们要求的字符串呢?
在组成的 所有可能 中,哪些是 合法 的?
1.左括号与右括号的数量 不可超过n 。
2.每放一个左括号,才可放一个右括号,即右
括号 不可先于 左括号放置。
故递归需要 限制条件 :
1.左括号与右括号的数量, 最多 放置n个。
2.若左括号的数量<=右括号数量, 不可进行 放置右括号的递归。
示例代码:
class Solution {
public:
vector<string >genenrate(int n) {
vector<string >result;
genenrateCore("", n, n, result);
return result;
}
void genenrateCore(string item, int left, int right, vector<string>&result) {//left与right分别代表的是左括号还剩几个与右边括号还剩几个
if (left == 0 && right == 0) {//只有当left与right都等于0的时候,才能填入result,这就也避免了左括号多余右括号的情况
result.push_back(item);
return ;
}
if (left > 0) {
genenrateCore(item + "(", left - 1, right, result);
}
if (left < right) {//只有当left(左括号的剩余使用量)由于right(右括号的剩余使用量),避免了右括号出现在没有左括号的前面以及右括号太多
genenrateCore(item + ")", left, right-1, result);//才允许继续使用右括号
}
}
};
int main() {
Solution solve;
vector<string>result = solve.genenrate(3);
for (int i = 0; i < result.size(); i++)
{
cout << result[i] << endl;
}
return 0;
}
/*输出
((()))
(()())
(())()
()(())
()()()
*/
分治法之归并排序
首先:提问,对于两个已经排序的数组,怎么样将其合并成一个排序数组(注意:合并两个链表也是一个意思)
基本思路就是采用双指针,分别指向两个数组的首元素,进行逐一比较。
示例代码:
void merge_sort_two_vec(vector<int>&sub_vec1, vector<int>&sub_vec2, vector<int>&vec) {
/*代码中缺少对输入的判断*/
int i = 0;
int j = 0;
while (i < sub_vec1.size() && j < sub_vec2.size())
{
if (sub_vec1[i] < sub_vec2[j])
{
vec.push_back(sub_vec1[i]);
++i;
}
else
{
vec.push_back(sub_vec2[j]);
++j;
}
}
for (; i < sub_vec1.size(); i++)
{
vec.push_back(sub_vec1[i]);
}
for (; j < sub_vec2.size(); j++)
{
vec.push_back(sub_vec2[j]);
}
}
分治算法 :
将一个规模为N的问题 分解 为K个规模较小的子问题,这些子问题 相互独立 且与原问题 性质相同 。求出子问题的解后进行合并,就可得到原问题的解。
一般步骤 :
- 分解 ,将要解决的问题划分成 若干规模较小 的同类问题;
- 求解 ,当子问题划分得 足够小 时,用较简单的方法解决;
- 合并 ,按原问题的要求,将子问题的解 逐层合并 构成原问题的解。
归并排序的复杂度分析:
设有 n 个元素 ,n个元素 归并排序的时间 T(n)
总时间 = 分解时间 + 解决子问题时间 +合并时间
分解时间 : 即对 原问题拆解 为两个子问题的时间 复杂度 O(n)
解决子问题时间 : 即解决 两个子问题 的时间 2T(n/2)
合并时间 : 即对两个 已排序数组归并 的时间 复杂度 O(n)
T(n) = 2T(n/2) + 2O(n)
= 2T(n/2) + O(n)
= O(n + 2n/2 + 4n/4 +…+n*1)
= O(nlogn)
因此,归并排序的时间复杂度为O(nlogn)
根据递归与分治的思想,示例代码:
void merge_sort(vector<int>&vec) {
if (vec.size() < 2) {//问题足够小的时候,直接求解
return;
}
int mid = vec.size() / 2;
vector<int>sub_vec1;
vector<int>sub_vec2;
/*分*/
for (int i = 0; i < mid; i++)
{
sub_vec1.push_back(vec[i]);
}
for (int i = mid; i < vec.size(); i++)
{
sub_vec2.push_back(vec[i]);
}
merge_sort(sub_vec1);
merge_sort(sub_vec2);
vec.clear();
/*治*/
merge_sort_two_vec(sub_vec1, sub_vec2,vec);
}
void main(){
vector<int>vec={5,-7,9,8,1,4,-3,10,2,0};
merge_sort(vec);
for(int i=0;i<vec.size();++i){
printf("[%d]",vec[i]);
}
return 0;
}