目录
- 1.递归概述
- 2.何时使用递归
- 2.1.定义是递归的
- 2.2.数据结构是递归的
- 2.3.问题的求解方法是递归的
- 3.递归模型
- 4.应用
本文参考:
《数据结构教程》第 5 版 李春葆 主编
1.递归概述
(1)在定义一个过程或函数时,出现直接或者间接调用自己的成分,称之为递归(recursion)。若直接调用自己,称之为直接递归(direct recursion);若间接调用自己,称之为间接递归(indirect recursion)。
① 直接递归函数示例:求 n!,其中 n 为正整数。
//递归计算 n!
public int factorial(int n) {
if (n == 1) {
return 1;
} else {
//直接调用 factorial(n - 1),属于直接递归
return factorial(n - 1) * n;
}
}
② 间接递归示例:
(2)在算法设计中,任何间接递归算法都可以转换为直接递归算法来实现,所以下面主要讨论直接递归。如果一个递归函数中递归调用语句是最后一条执行语句,则称这种递归调用为尾递归(tail recursion),例如上面的求 n! 的递归就属于尾递归。一般来说,递归算法与非递归算法的转换方式如下:
- 尾递归算法:可以用循环语句转换为等价的非递归算法;
- 其他递归算法:可以通过栈来转换为等价的非递归算法;
例如将上述递归求解 n! 转换为非递归的代码如下:
//非递归计算 n!
public int factorial(int n) {
int res = 1;
while (n > 1) {
res *= n;
n--;
}
return res;
}
(4)递归算法通常把一个大的复杂问题层层转化为一个或多个与原问题相似的规模较小的问题来求解,递归策略只需少量的代码就可以描述出解题过程中所需要的多次重复计算,大大减少了算法的代码量。一般来说,能够用递归解决的问题应该满足以下 3 个条件:
- 需要解决的问题可以转化为一个或多个子问题来求解,而这些子问题的求解方法与原问题完全相同,只是在数量规模上不同;
- 递归调用的次数必须是有限的;
- 必须有结束递归的条件来终止递归;
(5)递归的优缺点:
- 优点:结构简单、清晰,易于阅读,方便其正确性证明;
- 缺点:算法执行中占用的内存空间较多,执行效率低,不容易优化;
2.何时使用递归
在以下 3 种情况下常常要用到递归方法。
2.1.定义是递归的
有许多数学公式、数列等的定义是递归的。例如,求 n! 和 Fibonacci 数列等。这些问题的求解过程可以将其递归定义直接转化为对应的递归算法。 例如求 Fibonacci 数列的递归算法如下:
//求 Fibonacci 数列的第 n 项,其中 n 为正整数
public int fibonacci(int n) {
if (n == 1 || n == 2) {
return 1;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
2.2.数据结构是递归的
(1)有些数据结构是递归的。例如二叉树就是一种递归数据结构,其定义如下:
public class TreeNode {
//节点值
int val;
//左子树
TreeNode left;
//右子树
TreeNode right;
TreeNode() {
}
TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
(2)对于递归数据结构,采用递归的方法编写算法既方便又有效、例如二叉树常见的前序遍历递归算法如下:
class Solution {
// res 用于保存前序遍历的结果
LinkedList<Integer> res = new LinkedList<>();
public List<Integer> preorderTraversal(TreeNode root) {
traverse(root);
return res;
}
public void traverse(TreeNode root) {
if (root == null) {
return;
}
//将当前根节点的值加入到 res 中
res.add(root.val);
//递归遍历左子树
traverse(root.left);
//递归遍历右子树
traverse(root.right);
}
}
有关二叉树遍历的具体知识可以查看【算法】二叉树遍历这篇文章。
2.3.问题的求解方法是递归的
(1)有些问题的解法是递归的。典型的有 Hanoi 问题求解,该问题描述如下:
(2)设 hanoi(n, x, y, z) 表示将 n 个盘片从 x 塔座借助 y 塔座移动到 z 塔座上,具体的递归分解过程如下:
代码实现如下:
// hanoi(n, x, y, z) 表示将 n 个盘片从 x 塔座借助 y 塔座移动到 z 塔座上
public void hanoi(int n, char X, char Y, char Z) {
if (n == 1) {
cnt++;
System.out.println("将第 " + n + " 个盘片从 " + X + " 移动到 " + Z);
} else {
//将 n - 1 个盘片从 x 塔座借助 z 塔座移动到 y 塔座上
hanoi(n - 1, X, Z, Y);
//将第 n 个盘片直接从 x 塔座移动到 z 塔座上
System.out.println("将第 " + n + " 个盘片从 " + X + " 移动到 " + Z);
//将 n - 1 个盘片从 y 塔座借助 x 塔座移动到 z 塔座上
hanoi(n - 1, Y, X, Z);
}
}
(3)设 hanoi(n, x, y, z) 的执行时间为 T(n),有 hanoi 递归算法得到以下递推式:
- T(n) = 1,当 n = 1 时;
- T(n) = 2T(n - 1) + 1,当 n > 1 时;
则有:
T(n) = 2T(n - 1) + 1 = 2(2T(n - 2) + 1) + 1
=
2
2
2^2
22T(n - 2) + 2 + 1
=
2
3
2^3
23T(n - 3) +
2
2
2^2
22 + 2 +1
= …
=
2
n
−
1
2^{n - 1}
2n−1T(1) +
2
n
−
2
2^{n - 2}
2n−2 + … +
2
2
2^2
22 + 2 + 1
=
2
n
2^n
2n - 1
= O(
2
n
2^n
2n)
3.递归模型
(1)递归模型是递归算法的抽象,它反映一个递归问题的递归结构。例如求 n! 递归算法对应的递归模型如下:
f(n) = 1 n = 1 //递归出口
f(n) = n * f(n - 1) n > 1 //递归体
(2)一般地,一个递归模型是由递归出口和递归体两部分组成,其中:
- 递归出口确定递归到何时结束;
- 递归体确定递归求解时的递推关系。
① 递归出口的一般格式如下:
f( s 1 s_1 s1) = m 1 m_1 m1
这里的 s 1 s_1 s1 与 m 1 m_1 m1 均为常量,有些递归问题可能有几个递归出口。
② 而递归体的一般格式如下:
(3)递归思路
例如,统计全国 GDP:
(4)为了讨论方便,简化上述递归模型为:
求 f(
s
n
s_n
sn) 的分解过程如下:
4.应用
(1)以 LeetCode 中的206.反转链表为例:
① 链表节点的定义如下,其数据结构是递归的。
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
② 这里假设 reverseList(head) 将以 head 为头节点的链表进行反转
,那么使用递归解题的关键在于:假设链表的其余部分已经被反转,现在应该如何反转它前面的部分?假设链表为:
n 1 n_1 n1 → n 2 n_2 n2 → … n k − 1 n_{k - 1} nk−1 → n k n_k nk → n k + 1 n_{k + 1} nk+1 → … → n m n_m nm → ∅
若从节点 n k + 1 n_{k+1} nk+1 到 n m n_m nm 已经被反转(即执行 reverseList( n k + 1 n_{k+1} nk+1)),而我们正处于 n k n_k nk:
n 1 n_1 n1 → n 2 n_2 n2 → … n k − 1 n_{k - 1} nk−1 → n k n_k nk → n k + 1 n_{k + 1} nk+1 ← … ← n m n_m nm
我们希望 n k + 1 n_{k+1} nk+1 的下一个节点指向 n k n_k nk。所以 n k n_k nk.next.next= n k n_k nk 。需要注意的是 n 1 n_1 n1 的下一个节点必须指向 ∅。如果忽略了这一点,链表中可能会产生环。
③ 具体的代码实现如下:
class Solution {
public ListNode reverseList(ListNode head) {
//递归终止条件
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList(head.next);
/*
以 head->head1->null 为例:
经过 head.next.next = head 之后,得到 head1->head->head1
经过 head.next = null 之后,得到 head1->head->null
这样便将 head 与 head1 进行了反转
*/
head.next.next = head;
head.next = null;
return newHead;
}
}
(2)大家可以去 LeetCode 上找相关的递归的题目来练习,或者也可以直接查看LeetCode算法刷题目录 (Java)这篇文章中的递归章节。如果大家发现文章中的错误之处,可在评论区中指出。