算法 - 递归
递归
递归适用于那些可以“自然地分解为结构相似的更小子问题”的问题。
下面用一段话详细说明其题目类型和适用场景:
递归算法的题目类型主要集中在以下几类:
-
树形结构问题:如树的遍历(前序、中序、后序)、计算树高、判断对称性等,因为树本身就是用递归定义的(每个子树也是一棵树)。
-
分治算法:如归并排序、快速排序、二分查找,其核心思想就是将大问题分解为相互独立的子问题,分别解决后再合并结果。
-
回溯算法:如八皇后、全排列、组合总和等,在尝试所有可能的分步时,每一步都可以看作一个递归调用,失败后“回溯”到上一步。
-
动态规划:虽然常用迭代优化,但其核心思想源于递归——将问题分解为重叠子问题,比如斐波那契数列、爬楼梯问题。
-
递归定义的数据结构:如链表、JSON/XML解析,链表可以看作“一个节点+指向另一个链表的指针”,这种自相似性非常适合递归。
判断一个题目是否适用于递归,主要看它是否满足三个条件:
-
问题可分解:原问题能够被分解成一个或几个规模更小、但结构与原问题完全相同的子问题。
-
子问题可解:这些子问题可以用同样的递归方法来解决。
-
有终止条件:存在一个简单情况(递归基),能直接得到答案而无需再递归。
题目练习
面试题 08.06. 汉诺塔问题 - 力扣(LeetCode)
解法(递归):
算法思路:
这是一道递归方法的经典题目,我们可以先从最简单的情况考虑:
-
假设 n = 1,只有一个盘子,很简单,直接把它从
A
中拿出来,移到C
上; -
如果 n = 2 呢?这时候我们就要借助
B
了,因为小盘子必须时刻都在大盘子上面,共需要 3 步(为了方便叙述,记A
中的盘子从上到下为 1 号,2 号):-
a. 1 号盘子放到
B
上; -
b. 2 号盘子放到
C
上; -
c. 1 号盘子放到
C
上。至此,C
中的盘子从上到下为 1 号,2 号。
-
-
如果 n > 2 呢?这是我们需要用到 n = 2\ 时的策略,将
A
上面的两个盘子挪到B
上,再将最大的盘子挪到C
上,最后将B
上的小盘子挪到C
上就完成了所有步骤。
因为 A
中最后处理的是最大的盘子,所以在移动过程中不存在大盘子在小盘子上面的情况。
-
对于规模为 n 的问题,我们需要将
A
柱上的 n 个盘子移动到C
柱上。 -
规模为 n 的问题可以被拆分为规模为 n - 1 的子问题:
-
a. 将
A
柱上的上面 n - 1 个盘子移动到B
柱上。 -
b. 将
A
柱上的最大盘子移动到C
柱上,然后将B
柱上的 n - 1 个盘子移动到C
柱上。
-
-
当问题的规模变为 n = 1 时,即只有一个盘子时,我们可以直接将其从
A
柱移动到C
柱。
需要注意的是,步骤 2.b 考虑的是总体问题中的子问题 b 情况。在处理子问题的子问题 b 时,我们应该将 A
柱中的最上面的盘子移动到 C
柱,然后再将 B
柱上的盘子移动到 C
柱。在处理总体问题的子问题 b 时,A
柱中的最大盘子仍然是最上面的盘子,因此这种做法是通用的。
算法流程:
递归函数设计:void hanota(vector<int>& A, vector<int>& B, vector<int>& C, int n)
-
返回值:无;
-
参数:三个柱子上的盘子,当前需要处理的盘子个数(当前问题规模);
-
函数作用:将
A
中的上面 n 个盘子挪到C
中。
递归函数流程:
-
当前问题规模为 n = 1 时,直接将
A
中的最上面盘子挪到C
中并返回; -
递归将
A
中最上面的 n - 1 个盘子挪到B
中; - 将
A
中最上面的一个盘子挪到C
中; - 将
B
中上面 n - 1 个盘子挪到C
中。
class Solution {
public:void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {dfs(A, B, C, A.size());return;}void dfs(vector<int>& a, vector<int>& b, vector<int>& c, int n){if(n == 1) {c.push_back(a.back());a.pop_back();return;}dfs(a, c, b, n - 1);c.push_back(a.back());a.pop_back();dfs(b, a, c, n - 1);}
};
21. 合并两个有序链表 - 力扣(LeetCode)
解法(递归):
算法思路:
- 递归函数的含义:交给你两个链表的头结点,帮你把它们合并起来,并且返回合并后的头结点;
- 函数体:选择两个头结点中较小的结点作为最终合并后的头结点,然后将剩下的链表交给递归函数去处理;
- 递归出口:当某一个链表为空的时候,返回另外一个链表。
注意注意注意:链表的题一定要画图,搞清楚指针的操作!
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x), next(next) {}* };*/
class Solution {
public:ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {if(l1 == nullptr) return l2;if(l2 == nullptr) return l1;if(l1->val <= l2->val){l1->next = mergeTwoLists(l1->next, l2);return l1;}else{l2->next = mergeTwoLists(l1, l2->next);return l2;}}
};
206. 反转链表 - 力扣(LeetCode)
解法(递归):
算法思路:
- 递归函数的含义:交给你一个链表的头指针,帮你逆序之后,返回逆序后的头结点;
- 函数体:先把当前结点之后的链表逆序,逆序完之后,把当前结点添加到逆序后的链表后面即可;
- 递归出口:当前结点为空或者当前只有一个结点的时候,不用逆序,直接返回。
注意注意注意:链表的题一定要画图,搞清楚指针的操作!
class Solution {
public:ListNode* reverseList(ListNode* head) {if(head == nullptr || head->next == nullptr) return head;ListNode* newhead = reverseList(head->next);head->next->next = head;head->next = nullptr;return newhead;}
};
注意:
在递归反转链表的解法中,newhead
的核心作用就是记录反转后新链表的头节点(也就是原链表的尾节点)。
具体来说:
- 当递归触达原链表的最后一个节点时(即
head->next == nullptr
),这个节点就是反转后新链表的头节点,此时返回该节点作为newhead
的初始值。 - 在递归回溯的过程中,
newhead
会被逐层向上传递(始终保持是新链表的头节点),不会被修改。 - 真正的反转操作是通过
head->next->next = head
完成的(让当前节点的下一个节点指向自己),而newhead
仅仅是作为 “结果标识” 向上传递,直到最终返回整个反转链表的头节点。
24. 两两交换链表中的节点 - 力扣(LeetCode)
解法(递归):
算法思路:
-
递归函数的含义:交给你一个链表,将这个链表两两交换一下,然后返回交换后的头结点;
-
函数体:先去处理一下第二个结点往后的链表,然后再把当前的两个结点交换一下,连接上后面处理后的链表;
- 递归出口:当前结点为空或者当前只有一个结点的时候,不用交换,直接返回。
注意注意注意:链表的题一定要画图,搞清楚指针的操作!
class Solution {
public:ListNode* swapPairs(ListNode* head) {if (head == nullptr || head->next == nullptr)return head;auto ret = head->next;auto tmp = swapPairs(head->next->next);head->next->next = head;head->next = tmp;return ret;}
};
50. Pow(x, n) - 力扣(LeetCode)
解法(递归 - 快速幂):
算法思路:
- 递归函数的含义:求出
x
的n
次方是多少,然后返回; - 函数体:先求出
x
的n / 2
次方是多少,然后根据n
的奇偶,得出x
的n
次方是多少; - 递归出口:当
n
为0
的时候,返回1
即可。
#include<iostream>
#include<vector>using namespace std;class Solution {
public:double QuickPwd(double x, int n){if (n == 0){return 1.0;}double tmp = QuickPwd(x, n / 2);return n % 2 ? tmp * tmp * x : tmp * tmp;}double myPow(double x, int n) {long long N=n;return N > 0 ? QuickPwd(x, N) : 1 / QuickPwd(x, -N);}
};