递归、回溯与分治

递归、回溯与分治举例说明

递归函数与回溯法

什么叫递归函数:编程语言中,函数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个规模较小的子问题,这些子问题 相互独立 且与原问题 性质相同 。求出子问题的解后进行合并,就可得到原问题的解。
一般步骤 :

  1. 分解 ,将要解决的问题划分成 若干规模较小 的同类问题;
  2. 求解 ,当子问题划分得 足够小 时,用较简单的方法解决;
  3. 合并 ,按原问题的要求,将子问题的解 逐层合并 构成原问题的解。在这里插入图片描述
    归并排序的复杂度分析:
    设有 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值