算法基础之递归回溯

文章介绍了递归的概念,包括边界条件和递归模式,并通过故事解释了递归的原理。递归算法虽然简洁,但效率较低。分治法与递归密切相关,常用于算法设计。回溯法是递归的一种形式,适用于搜索和优化问题。文中还举例说明了递归在解决汉诺塔问题中的应用。
摘要由CSDN通过智能技术生成

    递归(Recursion)就是子程序(或函数)直接调用自己或通过一系列调用语句间接调用自己,是一种描述问题和解决问题的基本方法。(如二叉树的定义)

  直接或间接地调用自身的算法称为递归算法。用函数自身给出定义的函数称为递归函数

递归有两个基本要素:

⑴ 边界条件:确定递归到何时终止;

⑵ 递归模式:大问题是如何分解为小问题的。

用一个通俗的故事来解释什么是递归就是:从前有座山,山中有座庙,庙里有个老和尚,老和尚在给小和尚讲故事:“ 从前有座山,山中有座庙,庙里有个老和尚,老和尚在给小和尚讲故事:“ 从前有座山,山中有座庙,庙里有个老和尚,老和尚在给小和尚讲故事:“ 从前有座山,山中有座庙,庙里有个老和尚,老和尚在给小和尚讲故事:“太困了不讲了”,于是都回去睡觉了。

由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。

分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。

递归问题解题思路

  • 大问题转小问题,定义小问题函数
  • 找到递归出口
  • 递推函数关系式

    一个递归函数的调用过程类似于多个函数的嵌套调用,只不过调用函数和被调用函数是同一个函数。为了保证递归函数的正确执行,系统需设立一个工作栈。具体地说,递归调用的内部执行过程如下:

1)运行开始时,首先为递归调用建立一个工作栈,其结构包括值参、局部变量和返回地址;

2)每次执行递归调用之前,把递归函数的值参和局部变量的当前值以及调用后的返回地址压栈;

3)每次递归调用结束后,将栈顶元素出栈,使相应的值参和局部变量恢复为调用前的值,然后转向返回地址指定的位置继续执行。

优缺点

递归算法结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,因此,它为设计算法和调试程序带来很大方便,是算法设计中的一种强有力的工具。

但是,因为递归算法是一种自身调用自身的算法,随着递归深度的增加,工作栈所需要的空间增大,递归调用时的辅助操作增多,因此,递归算法的运行效率较低

力扣

面试官 | 什么是递归算法?它有什么用? - Java中文社群_老王的个人空间 - OSCHINA - 中文开源技术交流社区

回溯 

很喜欢一个博主的一句话:

                理解回溯比较困难的是理解「回到过去」,现实世界里我们无法回到过去,但是在算法的世界里可以

        回溯是递归的一种形式,回溯算法实际是一个类似枚举的搜索尝试方法,其采用了一种“走不通就掉头”的思想,作为其控制结构(迷途知返的策略)。它的主题思想是在搜索尝试中找问题的解,当不满足求解条件就”回溯”返回,尝试别的路径。

        回溯法是在包含问题的所有解的解空间树中。按照深度优先的策略,从根结点出发搜索解空间树,算法搜索至解空间树的任一结点时,总是先判断该结点是否满足问题的约束条件。如果满足进入该子树,继续按深度优先的策略进行搜索。否则,不去搜索以该结点为根的子树,而是逐层向其祖先结点回溯。

回溯法就是对隐式图的深度优先搜索算法。

(1) 适用问题:求解搜索问题和优化问题

(2) 搜索空间:树结点对应部分解向量,树叶对应可行解

(3) 搜索过程:采用系统的方法隐含遍历搜索树

(4) 搜索策略:深度优先,宽度优先,函数优先,宽深结合等

(5) 结点分支判定条件:

满足约束条件---分支扩张解向量

不满足约束条件,回溯到该结点的父结点

(6) 结点状态:动态生成

白结点(尚未访问)

灰结点(正在访问该结点为根的子树);

黑结点(该结点为根的子树遍历完成)

(7) 存储:当前路径

 

void backtracing(参数列表){
    if(终止条件){
        存放结果;
        return;
    }

    for(循环条件){
               pass
               backtracking(路径, 选择路径);
                回溯,撤销选择;
    }
}

如何正确的做一道题

  • 从简入手: 先从简单暴力(时间复杂度高)的方法入手。
  • 优化: 思考如何在第一步的基础上,如何优化算法,降低时间复杂度。
  • 构思代码: 有了以上两步,我们此时应该已经有了一个正确的想法,此时我们应该构思代码,有那几部分,每部分实现什么功能,代码怎么写。而不是直接闷头去写代码,因为很多时候没想清楚,直接去写代码,会导致写了一半发现思路不对,写的代码都是错误的。
  • 写代码: 实现第三步代码。
  • (Debug): 如果我们的题目没有通过测试,应该检查代码是不是有bug、思路对不对等。
  • 总结与反思: 题目通过了,我们应该总结一下这道题考察的知识点、切入的角度、同类型的题目等,同时思考有没有更优的办法。

经典习题(持续更新)

1

一只青蛙可以一次跳 1 级台阶或一次跳 2 级台阶,例如:跳上第 1 级台阶只有一种跳法:直接跳 1 级即可。跳上第 2 级台阶有两种跳法:每次跳 1 级,跳两次;或者一次跳 2 级。问要跳上第 n 级台阶有多少种跳法?

 思路按三步走:

第一,大问题转化为小问题,若要跳到第n阶台阶,则只能从n-1阶或者n-2阶跳,而n-1只能由n-2或n-3跳……

第二,找到递归出口,就是n为跳第一阶或者第二阶的时候,此时,f(1)=1,f(2)=2;

第三,找到递推函数关系式,由上易得f(n) = f(n - 1) + f(n-2);

/*一只青蛙可以一次跳 1 级台阶或一次跳 2 级台阶,例如:
跳上第 1 级台阶只有一种跳法:直接跳 1 级即可。跳上第 2 级台阶
有两种跳法:每次跳 1 级,跳两次;或者一次跳 2 级。
问要跳上第 n 级台阶有多少种跳法? */

#include<stdio.h>
int f(int n)
{
	if(n==1) //递归出口 
		return 1;
	if (n==2) //递归出口 
		return 2;
	return f(n-1)+f(n-2);  //递归表达式 
}
int main()
{
	int n;scanf("%d",&n);
	int num = f(n);
	printf("%d",num);
}

此时间复杂度同斐波那契数列为指数级,显然是不能接受的,递归过程中有大量重复计算,且f(n)中n值越低的计算的次数越多, 随着 n 的增大,f (n) 的时间复杂度自然呈指数上升了,但博主还是入门阶段,暂时给不了优化方案,不过我会好好学习,以后写上的!!!

2.

全排列

给定一串无重复的字符或数字,输出其有所有排列,并统计其共有排列个数。

力扣https://leetcode.cn/problems/permutations/

第一,大问题转化为小问题,若要找到n个不重复数的全排列,则先找第一个个位置上能放的数,然后再找n-1的全排列……以此类推,则最后回归到找第一个数的全排列

第二,找到递归出口,就是找到第n个位置。

第三,找到递推函数关系式,

#include<stdio.h>
const int N = 10;
int arr[N],a[N];
bool book[N];
void sort(int a[],int n)
{
	for(int i = 0;i<n;i++){  //一个基础的冒泡排序 
		for(int j = 0;j<n;j++){
			if(i!=j){
				if(a[i]<a[j]){
					int temp =a[i];
					a[i] = a[j];
					a[j] = temp;
				}
			}
		}
	}
}
int dfs(int a[] , int n,int step)
{
	if(step == n) //递归出口 
	{
		for(int i=0;i<n;i++)
			printf("%d ",arr[i]);
		printf("\n");
	}
	for(int i=0;i<n;i++)
	{
		if(book[i] == false )
		{
			arr[step] = a[i]; 
			book[i] = true; 
			dfs(a,n,step+1); //递归到下一层 
			book[i] = false; //递归结束,i+1下一层继续开始 
		}
	}
}
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=0;i<n;i++) scanf("%d",&a[i]);
	sort(a,n);//冒泡排序 
	dfs(a,n,0);//深搜 
}

3.

将左边的树反转成右边的二叉树

力扣

 还在学树,欸嘿嘿,晚点!

4.

递归函数的经典问题——汉诺塔问题

世界刚被创建的时候有一座钻石宝塔(塔A),其上有64个金碟。所有碟子按从大到小的次序从塔底堆放至塔顶。紧挨着这座塔有另外两个钻石宝塔(塔B和塔C)。从世界创始之日起,婆罗门的牧师们就一直在试图把塔A上的碟子移动到塔C上去,其间借助于塔B的帮助。每次只能移动一个碟子,任何时候都不能把一个碟子放在比它小的碟子上面。当牧师们完成任务时,世界末日也就到了。

汉诺塔问题可以通过以下三个步骤实现:

1)将塔A上的n-1个碟子借助塔C先移到塔B上。

2)把塔A上剩下的一个碟子移到塔C上。

3)将n-1个碟子从塔B借助塔A移到塔C上。显然,这是一个递归求解的过程

#include<stdio.h>
void move(char x,char y)
{
	printf("把 %c 移到 %c\n",x,y);
}
void hanoi(int n,char A,char B,char C)
{
	if(n==1)
	move(A,C);
	else
	{
		hanoi(n-1,A,B,C);
		move(A,B);
		hanoi(n-1,B,A,C);
	}
	
}
int main()
{
	int n;
	scanf("%d",&n);
	hanoi(n,'A','B','C');
	
}

5.

Ackerman函数

当一个函数及它的一个变量是由函数自身定义时,称这个函数是双递归函数

Ackerman函数A(nm)定义如下:

A(n m) 的自变量 m 的每一个值都定义了一个单变量函数:
M=0 时, A(n,0)=n+2
M=1 时, A(n,1)=A(A(n-1,1),0)=A(n-1,1)+2 ,和 A(1,1)=2 A(n,1)=2*n
M=2 时, A(n,2)=A(A(n-1,2),1)=2A(n-1,2) ,和 A(1,2)=A(A(0,2),1)=A(1,1)=2 ,故 A(n,2)= 2^n 
M=3 时,类似的可以推出
M=4 时, A(n,4) 的增长速度非常快,以至于没有适当的数学式子来表示这一函数。
定义单变量的 Ackerman 函数 A(n) 为, A(n)=A(n n)
定义其拟逆函数 α(n) 为: α(n)=min{k A(k)≥n} 。即 α(n) 是使 n≤A (k) 成立的最小的 k 值。
α(n) 在复杂度分析中常遇到。对于通常所见到的正整数 n ,有 α(n)≤4 。但在理论上 α(n) 没有上界,随着 n 的增加,它以难以想象的 速度趋向正无穷大。

7.

整数划分问题:

将正整数n表示成一系列正整数之和:n=n1+n2++nk,其中n1≥n2≥nk≥1k≥1。正整数n的这种表示称为正整数n的划分。求正整数n的不同划分个数。 例如正整数6有如下11种不同的划分:

 6  

5+1

4+24+1+1

3+33+2+13+1+1+1

2+2+22+2+1+12+1+1+1+1

1+1+1+1+1+1

本例中,如果设p(n)为正整数n的划分数,则难以找到递归关系,

因此考虑增加一个自变量:将最大加数n1不大于m的划分个数记作q(n,m)。可以建立q(n,m)的如下递归关系。

#include<stdio.h>
int divide(int n,int m) //m为n能划分的最大的数 
{
	if(n<1||m<1)
		return 0;
	if(n==1||m==1)
		return 1;
	if(n<m)
		return divide(n,n);
	if(n == m) 
		return divide(n,m-1)+1;
	if(n>m&&m>1)
		return divide(n,m-1)+divide(n-m,m);
}
int main()
{
	int n,p;scanf("%d",&n);
    p = divide(n,n);
    printf("%d",p);
    return 0;
}

8.

细胞分裂 有一个细胞 每一个小时分裂一次,一次分裂一个子细胞,第三个小时后会死亡。那么 n 个小时候有多少细胞?

暂无

9.

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

输入:candidates =[2,3,6,7],target =7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

力扣

10.究极无敌令人头秃的n皇后问题

先放这腾位置

11.电话号码的数字排列

力扣

12.子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

力扣

13.括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

力扣

14.单词搜索

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

力扣

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值