To iterate is human, to recurse, divine.
人理解迭代,神理解递归。
什么是递归
递归算法(英语:recursion algorithm)在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。递归式方法可以被用于解决很多的计算机科学问题,因此它是计算机科学中十分重要的一个概念。
简单地说,就是如果在函数中存在着调用函数本身的情况,这种现象就叫递归。
「递归」,先有「递」再有「归」,「递」的意思是将问题拆解成子问题来解决, 子问题再拆解成子子问题,...,直到被拆解的子问题无需再拆分成更细的子问题(即可以求解),「归」是说最小的子问题解决了,那么它的上一层子问题也就解决了,上一层的子问题解决了,上上层子问题自然也就解决了
递归的两个条件:
- 可以通过递归调用来缩小问题规模,且新问题与原问题有着相同的形式。(自身调用)
- 存在一种简单情境,可以使递归在简单情境下退出。(递归出口)
递归算法的一般形式:
def f(mode):
if end_condition: # 递归出口
end
else:
f(mode_small) # 递归本身,递归
阶乘
一个数的阶乘是递归简单而典型的例子,阶乘的递推公式为:factorial(n)=n*factorial(n-1),其中n为非负整数,且0!=1,1!=1
def factorial(i):
"""阶乘"""
if i < 0:
return
elif i <= 2:
return i
else:
return i * factorial(i-1)
递归过程
这个调用的过程和栈的工作原理一样,递归调用就是通过栈这种数据结构完成的。整个过程实际上就是一个栈的入栈和出栈的问题。
递归中的"递"就是入栈,"归"就是出栈。
入栈和出栈 的过程:
def 递归(n):
print("递进:" + str(n))
if n > 0:
递归(n-1)
print("回归:" + str(n))
递归(3)
output:
递进:3
递进:2
递进:1
递进:0
回归:0
回归:1
回归:2
回归:3
递归过程:
递归算法解决思路
套路:
- 明确你这个函数想要干什么
- 寻找递归结束条件
- 找出函数的等价关系式(递归中最难的一步)
经典例题
1. 斐波那契数列
形如 1、1、2、3、5、8、13、21、34 ...的数列
- 由这个数列我们可以容易发现其递推公式:f(n)=f(n-1)+f(n-2)
- 递归结束条件:当n=1或者n=2时,f(1)=f(2)=1
def fibonacci(i):
"""斐波那契数列"""
if i <= 2:
return 1
else:
return fibonacci(i-1) + fibonacci(i-2)
2.猴子吃桃子
猴子第一天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个,第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃前一天剩下的一半零一个。到第10天早上想再吃时,见只剩下一个桃子了。求第一天共摘多少个桃子?
- 最后一天时,只剩下一个桃子 (结束条件)
- 当天的桃子等于上一天加一的和乘二
(monkey(n-1)+1)*2
def monkey(n):
"""猴子分桃"""
if n == 1:
return 1
else:
return (monkey(n-1)+1)*2
3.汉诺塔问题
相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘(如下图)。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。
解题方法:
将最上面的n-1个圆盘从初始位移动到过渡位
将初始位的最底下的一个圆盘移动到目标位
将过渡位的n-1个圆盘移动到目标位
- 当只剩下一个盘子时,直接移动到C (结束条件)
- 将n-1只盘子看成整体,通过c移动到b,然后a移动到c,最后b处的n-1只盘子经过a移动到c
def hanoi(n, a, b, c): # a为初始位,b为过渡位,c为目标位
"""汉诺塔"""
if n == 1:
print(a, '-->', c)
else:
hanoi(n-1, a, c, b) # 初始位为a,通过c移动到b
print(a, '-->', c)
hanoi(n-1, b, a, c) # 初始位为b,通过a移动到c
4. 反转单链表
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
- 结束条件:当链表为空表或者只有一个节点
- 递的操作:
1.得到尾部节点:new_head = self.reverseList(head.next)
2.翻转当前节点:head.next.next = head
3.拆掉当前节点的next:head.next = None
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
"""反转单链表"""
if head is None or head.next is None:
return head
new_head = self.reverseList(head.next)
head.next.next = head
head.next = None
return new_head
适用递归的问题
- 数据的定义是按递归定义的。如Fibonacci函数。
- 问题解法按递归算法实现。如Hanoi问题。
- 数据的结构形式是按递归定义的。如二叉树、广义表等。
递归和递推的异同
递推和递归有着很多的相似之处,递推甚至可以看做是递归的反方向,但对比其细节是存在很多不同的。
递归法:把问题转化为规模更小的子问题解决,思考的重点在于建立原问题和子问题之间的联系。有的问题有很明确的递归结构,但是需要仔细的思考,才能正确的转化为结构相同的子问题。
递推法:根据已知信息不断的计算出未知信息,直到得到结果,思考的重点在于“步步为营”。
尾递归
尾递归相对传统递归,其是一种特例。在尾递归中,先执行某部分的计算,然后开始调用递归,所以你可以得到当前的计算结果,而这个结果也将作为参数传入下一次递归。这也就是说函数调用出现在调用者函数的尾部,因为是尾部,所以其有一个优越于传统递归之处在于无需去保存任何局部变量,从内存消耗上,实现节约特性。
普通递归调用:
def recursion(n):
if n==1:
return n
else:
return n+recursion(n-1)
调用这个函数recursion(5),编译器会执行:
recursion(5)
5+recursion(4)
5+(4+recursion(3))
5+(4+(3+recursion(2)))
5+(4+(3+(2+recursion(1))))
5+(4+(3+(2+1)))
15
此处编译器会分配递归栈来保存中间结果下来看尾递归实现:
def tail_recursion(n,total=0):
if n==0:
return total
else:
return tail_recursion(n-1, total+n)
此时,编译器做的工作:
tail_recursion(5,0)
tail_recursion(4,5)
tail_recursion(3,9)
tail_recursion(2,12)
tail_recursion(1,14)
tail_recursion(0,15)
15
你可以看到当前时刻的计算值作为第二个参数传入下一个递归,使得系统不再需要保留之前计算结果。
但是python本身不支持尾递归(没有对尾递归做优化),而且对递归的次数有限制,当递归深度超过1000时,会抛出异常。
总结
递归是非常基础的算法,思考递归时要抛弃程序设计的细节
明确递归函数功能
递归程序出口设计
递归要有规模递减
递归思想在动态规划,回溯算法,二叉树的深度优先搜索等都有密切的涉及。