目录
递归
- 递归:就是函数调用自身;本质是把一个大型复杂的问题分解成多个与原问题相似的小问题来求解;递归的过程就是出入栈的过程,自顶向下(对应函数入栈,每深入一层都要占取一块栈数据区域,当递归结束条件将不再入栈),自底向上(对应函数出栈);递归实现代码比基于循环实现代码要简洁
- 函数调用是有空间和时间消耗(效率低):每一次函数调用,操作系统都需要在内存栈中分配内存空间以保存参数、返回地址和临时变量,而每个进程的栈容量是有限的,当递归调用层级太多时,会超出栈的容量,从而导致调用栈溢出;而且往栈里压入数据和弹出数据都需要时间;
- 若多个小问题存在重叠部分,就存在“重复计算”的问题,如用递归求解“斐波那契数列”,一般改用自底向上的循环(递推)完成;
【注】通常基于递归实现的代码既简洁又容易实现,若面试官没有特殊要求,则应聘者可以优先采用递归的方法编程
解递归的两大要素
- 第一要素:递归终止条件(不然的话会一直调用自己进入无底洞)
- 第二要素:函数等价关系式(即不断缩小参数的范围,并通过一些辅助的变量或操作,使原函数结果不变)
经典例题
斐波那契数列
题目:求斐波那契数列的第n项,斐波那契数列的定义是 f(n) = f(n-1) + f(n - 2)
思路:采用从上往下递归求解该问题会产生子问题重复计算,且当n较大时,必须要往下递归到n<=1才将结果慢慢返回,递归压栈会使栈空间不足而溢出。采用从下往上循环的方式来替代递归(递推)
#递归:自顶向下
class Solution:
def feibonacci(n):
if n <= 2: #递归终止条件
return 1
else:
return feibonacci(n-1) + feibonacci(n-2) #等价关系式
#非递归:自底向上(循环)
class Solution:
def feibonacci(self, n: int) -> int:
if (n<=1): #递归终止条件
return n
if (n==2):
return 1
current = 0 #即f(n)
prev1 = 1 #即f(n-1)
prev2 = 1 #即f(n-2)
for i in range(3,n+1): #list(range(0,3))=[0,1,2],不包括3
current = (prev1 + prev2)%1000000007 #取模,防止整形溢出
prev2 = prev1 #将prev2向右移动一位
prev1 = current #将pre1向右移动一位
return current
小青蛙跳台阶
题目:一只青蛙⼀次可以跳上1级台阶,也可以跳上2级。求该⻘蛙跳上一个n级台阶总共有多少种跳法
思路:当n>2时,第一次跳的时候就有两种不同的选择:一是第一次只跳1级,此时跳法数目等于后面剩下的n-1阶台阶的跳法数目,即f(n-1);二是第一次跳2级,此时跳法数目等于后面剩下的n-2级台阶的跳法数目,即f(n-2)。因此,n级台阶的不同跳法总数f(n) = f(n-1) + f(n-2)。其实就是斐波那契数列了
#递归:自顶向下
class Solution:
def numWays(n):
if n <= 1: #递归终止条件
return n
else:
return numWays(n-1) + numWays(n-2) #等价关系式
#非递归:自底向上(循环)
class Solution:
def numWays(self, n: int) -> int:
if (n<=1):
return 1
if (n==2):
return 2
current = 2 #初始值和斐波那契有所区别
pre1 = 2
pre2 = 1
for i in range(3,n+1):
current = pre1+pre2
pre2 = pre1
pre1 = current
return current
解码问题
题目:一条包含字母 A-Z 的消息通过以下方式进行了编码:
'A' -> 1
'B' -> 2
…
'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数
思路:实际上本题也可以看作是加了约束条件的小青蛙跳台阶问题,每次解码一个数字或者两个数字
#动态规划——从简单的小青蛙跳台阶问题延伸而来(递归的思想)
class Solution:
def numDecodings(self, s: str) -> int:
n = len(s)
if n==0: #如果序列为空无法解码
return 0
dp = [1,0] #dp[0]=1的作用,在于两位时,dp[2]=dp[1]+dp[0]
if s[0]!='0': #为0时无法解码
dp[1] = 1 #dp[1]=1表示第一位的解码方法数
else:
dp[1] = 0
for i in range(1,n): #遍历输入序列s
dp.append(0)
if s[i]!='0': #表示当前位可以单独解码
dp[i+1] = dp[i] #dp[i+1]解码数由dp[i]决定
if s[i-1:i+1]>='10' and s[i-1:i+1]<='26': #表示当前位可以与前一位相结合解码
dp[i+1] += dp[i-1] #dp[i+1]解码数由dp[i-1]决定
return dp[-1] #返回末尾值,即所有解码方法总数
反转单链表
题目:例如链表为:1->2->3->4;反转后为 4->3->2->1
思路:https://zhuanlan.zhihu.com/p/60117407(递归先入栈,再出栈!!!)
class ListNode: #先建立节点对象
def __init__(self,x):
# 这是数据域val
self.val = x
# 这是节点的next域,也就是下一个节点的引用(python中没有指针的概念,用引用来建立节点之间的关系)
self.next = None
def reverseList(self, head: ListNode) -> ListNode:
if not head:
return None
if not head.next: #递归终止条件
return head
headNode = self.reverseList(head.next) #递归(链表会一直往后传递,返回最后节点,即此时节点作为反转链表头)
head.next.next = head #此时head为倒数第二个节点,让原来指向None的最后一个节点指向倒数第二个节点
head.next = None #截断倒数第二各节点到最后一个节点的指针
return headNode
汉诺塔问题
题目:有三个塔 A、B、C,开始时,塔 A 上放着 n 个盘子,它们从下往上按照从大到小的顺序叠放。现在要求将塔 A 中所有的盘子搬到塔 C 上,让你打印出搬运的步骤(在搬运过程中,每次只能搬运一个盘子,另外,任何时候,无论在哪个塔上,大盘子不能放在小盘子上面)
思路:https://www.jianshu.com/p/bb273651d3f0(把大问题分解成多个相似的子问题)
- 当有一个圆盘时,直接将A上面的圆盘移动到C上即可
- 当有两个圆盘时,需要借助B,先将最上面的圆盘移动到B上,再将A上的圆盘移动到C上,最后将B上的圆盘移动到C上
- 当有n个圆盘时,将上面的n-1个圆盘看成一个整体,这样问题又回到了两个圆盘情况,现在我们只需要实现将上面n-1个圆盘组成的整体从A移动到B,再从B移动到C即可;
- 我们注意到:将 n-1 个圆盘从A移动到B,从B移动到C 和将 n 个圆盘从A移动到C是同一类问题;
def move(n,a,b,c):
if n==1: #递归终止条件
print (a+'-->'+c)
else:
move(n-1,a,c,b) #函数move(n,a,b,c),表示n个圆盘借助B从A移动到C
print(a+'-->'+c)
move(n-1,b,a,c)