【递归刷题】
1. 递归思想
什么是递归?
函数自己调用自己的情况。
为什么会用到递归?
递归的本质:
在解决一个主问题的时候,衍生出了相同的子问题,解决这个子问题的时候,又衍生出了相同的子问题。如此循环。
用同样的方法解决主问题和子问题。
如何理解递归?
在最开始接触递归的时候,相必大家都遭受过递归展开图的毒打。但是在解决递归问题的时候,我们只需要在分析题目的时候,找到相同子问题即可!在实现一个递归代码的时候,千万不要被递归展开图带跑了方向。递归只是一个框架型算法。
所以如何避免实现递归代码的时候陷入展开死胡同?这需要我们从宏观角度看待递归问题。
- 知道但不要在意递归的细节展开图!!!
- 把递归的函数当成一个黑盒子
- 相信这个黑盒子一定能完成这个任务:(怎么完成的:先递,递到出口,再归,递归结束,问题解决。)
如何写好一个递归?
- 先找到相同的子问题-》函数头的设计
- 只关心某一个子问题是如何解决的-》函数体的书写
- 避免写死递归:注意一下递归函数的出口即可。递归函数的出口就是子问题不能再衍生子问题的时候。
2. 汉诺塔问题
如何解决这个问题?
先从具体例子入手分析一下:
绿盘子是第N个盘子,红盘子是第N-1个盘子,向上类推
从上面三个例子中抽象出解决汉诺塔问题的办法:
当N=2,N=3时,怎么把N个盘子移动从A柱子移动到C柱子?
- 先把前N-1个盘子移动到B柱子上,
- 再把第N个盘子移动到C柱子上,
- 最后把B柱子上的N-1个盘子再移动到C柱子上
依次类推,当N=4,N=5 … …的时候都是如此。
再仔细看N=3中的第一步:怎么把第N个盘子上面的N-1个盘子从A柱子移动到B柱子上?
- 先把前N-2个盘子移到C柱子上
- 再把第N-1个盘子移到B柱子上
- 最后把C柱子上的前N-2个盘子再移动到B柱子上
到这,让我们尝试从例子中抽象出解决汉诺塔问题的方法:已知三个柱子和N个盘子,借助中间柱子,把N个盘子从所在柱子移到目标柱子。
先把第N个盘子上面的的N-1个盘子移到中间柱子,然后把第N个盘子移到目标柱子,最后把中间柱子上的盘子移到目标柱子上。
其中,把N-1个盘子移到目标柱子(此时目标柱子变成了上面问题中的中间柱子,对应的上面问题中的目标柱子,变成了中间柱子)也是同样的:
先把第N-1个盘子上面的的N-2个盘子移到中间柱子,然后把第N-1个盘子移到目标柱子,最后把中间柱子上的盘子移到目标柱子上。所在柱子,目标柱子,中间柱子都是相对的
。
显然,解决汉诺塔问题的时候,解决主问题时,又衍生出和主问题相同的子问题,所以汉诺塔问题可以用递归思想解决。
实现递归代码:
-
相同子问题:把N个盘子,借助中间柱子,从所在柱子移到目标柱子
-
子问题的解决办法:
以N=3为例:
- 递归函数的出口:
题解代码如下:
class Solution {
public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
dfs(A,B,C,A.size()) ;
}
void dfs(List<Integer> A, List<Integer> B, List<Integer> C,int N){
//A是盘子所在柱子,B是中间柱子,C是目标柱子,N是盘子
if(N==1){
C.add(A.remove(A.size()-1));
return;
}
//先把N-1个盘子从A柱子,借助C柱子,移到B柱子
dfs(A,C,B, N-1);
//然后把第N个盘子,直接移到C柱子:也就是,把A表中最后一个元素移到C表中
C.add(A.remove(A.size()-1));
//最后把N-1个盘子从B柱子,借助A柱子,移到C柱子
dfs(B,A,C, N-1);
}
}
3. 合并两个有序单链表
去力扣闯关
合并链表的过程其实就是重新连接已知所有结点的过程。
如何解决这个问题?
先从具体例子分析:
已知两个升序单链表,A,B,合并A,B,返回新的升序链表C的头结点。
先比较A的头结点中值和B的头结点中值,哪个结点中的值更小,就让这个节点做新的升序链表C的头结点,即返回这个节点,并让这个节点指向C的第二个结点。
如何得到C的第二个结点?
在上图所示的例子中,链表C的头结点是B链表的头结点,且在确认C的第二个节点的时候,B链表的头结点同时更新为了B链表原来的第二个结点,也就是诞生了新的B链表。
所以如何得到C的第二个结点呢?
已知两个升序单链表,A,B(新B),合并A,B,返回新的升序链表C(新C)的头结点。
先比较A的头结点中值和B的头结点中值,哪个结点中的值更小,就让这个节点做新的升序链表C的头结点,即返回这个节点,并让这个节点指向C的第二个结点。
依次类推。
显然,解决合并两个有序单链表问题的时候,解决主问题时,又衍生出和主问题相同的子问题,所以该问题可以用递归思想解决。
实现递归代码
-
先找到相同的子问题-》已知两个升序单链表,A,B,合并A,B,返回新的升序链表C的头结点。
-
只关心某一个子问题是如何解决的-》
-
避免写死递归:
题解代码如下:
/**
* Definition for singly-linked list.
* 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; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
//递归出口
if(list1 == null){
return list2;
}
if(list2 == null){
return list1;
}
//某一个子问题是如何解决的:
//先比较A的头结点中值和B的头结点中值,哪个结点中的值更小,
//就让这个节点做新的升序链表C的头结点,
//即返回这个节点,并让这个节点指向C的第二个结点。
if(list2.val<=list1.val){
list2.next = mergeTwoLists(list1,list2.next);
return list2;
}else{
list1.next = mergeTwoLists(list1.next,list2);
return list1;
}
}
}
4. 就地反转单链表
如何解决这个问题?
先从具体例子入手:
但是,如果我们逆置前两个结点的时候,其中第二个结点是已经逆置完成的单链表的头结点的话,是不是就可以忽略逆置完前两个结点,找不到第三个结点的问题了呢?
因此,直接逆置单链表的前两个结点即可。
至于第二个结点如何变成,已经逆置完成的单链表的头结点,这不就是原问题衍生出来的相同的子问题吗?此时要逆置链表的头结点变成了原链表的第二个结点。
依次类推。
显然,解决逆置一个单链表问题的时候,解决主问题时,又衍生出和主问题相同的子问题,所以该问题可以用递归思想解决。
实现递归代码
-
先找到相同的子问题-》已知单链表的头结点,逆置单链表后,再返回逆置好的单链表的头结点
-
只关心某一个子问题是如何解决的-》
-
避免写死递归:注意一下递归函数的出口即可。递归函数的出口就是子问题不能再衍生子问题的时候:即链表中只有一个结点或没有结点的时候,不需要逆置。
题解代码:
/**
* Definition for singly-linked list.
* 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; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
/*
//空链表
if(head == null){
return head;
}
//只有一个结点,不用逆置
if(head!=null && head.next == null){
return head;
}
*/
//把空链表和只有一个结点的情况合并
if(head == null || head.next == null){
return head;
}
//先拿到头结点后面的节点,该节点是已经逆置好的单链表的头结点
ListNode newHead = reverseList(head.next);
//逆置头结点和后面的节点
head.next.next = head;//后面结点的地址域指向head,
head.next = null;//head的地址域设为null。
//最后返回逆置好的单链表。
return newHead;//要注意的是:逆置好的单链表的头结点不是原来的头结点
}
}
5.两两交换链表中的节点
去力扣闯关
交换链表中的节点,意味着要修改链表的地址域。而不是数值域。
如何解决这个问题
从具体例子入手:交换链表的前两个结点,然后返回交换后链表的头结点。
如何确定第四个结点:同样的方法,交换第三个结点和第四个结点,返回第四个结点。
显然,解决两两交换链表中的结点问题的时候,解决主问题时,又衍生出和主问题相同的子问题,所以该问题可以用递归思想解决。
递归实现代码
-
先找到相同的子问题-》已知一个单链表,链表中的两个结点为一组,进行交换操作,然后返回交换后新链表的头结点。
-
只关心某一个子问题是如何解决的-》第二个结点指向头结点,头结点指向第四个结点,(这里第四个结点是交换后的新链表的头结点),返回第二个节点。
-
避免写死递归:
题解代码:
/**
* Definition for singly-linked list.
* 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; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
//递归出口
//当链表中只有一个结点,或空链表时
if(head == null||head.next == null){
return head;
}
//子问题的解决办法
ListNode tmp = swapPairs(head.next.next);//确定第四个结点
ListNode newHead = head.next; //提前保存第二个结点
head.next.next = head;
head.next = tmp;
return newHead;
}
}
6.快速幂
快速幂算法:即快速的求出x的n次方是多少的算法。
如何解决这个问题:
既然这个问题是快速幂,那暴力解法(时间复杂度O(n))就不多聊了。。。
先从具体例子入手,例如:如何快速计算316的值?(当n是偶数时)
316=38 * 38,那如何计算 38呢? 38 = 34 * 34,依次类推。
再看一个例子,例如:如何计算 321的值?(当n是奇数时)
321 = 310 * 310 * 3,那如何计算 310的值,依次类推。
显然,解决主问题时,又衍生出和主问题相同的子问题,所以该问题可以用递归思想解决。
递归实现代码:
-
先找到相同的子问题-》计算x的n次幂的值,n是
-2^31^ <= n <= 2^31^-1
之间的整数。
-
只关心某一个子问题是如何解决的-》如何求x的n次幂:
-
当n是正数时,如果n是奇数,x的n次幂等于x的n/2次幂乘以x的n/2次幂;如果n是偶数,x的n次幂等于x的n/2次幂乘以x的n/2次幂乘以x。
-
当n是负数时,如果n是奇数,x的n次幂等于1 / (x的-n/2次幂乘以x的-n/2次幂);如果n是偶数,x的n次幂等于1 / (x的-n/2次幂乘以x的-n/2次幂乘以x)。
, -
这里还有一个细节问题:力扣中给的函数接口里,n是double类型的,double n 存储的整数范围是 -2^31^ <= n <= 2^31^-1
。也就是当n = -231,要计算x的 -231次幂的值时,计算机实际上计算的是1 / (x的231次幂)的值。但是,double类型的n根本无法存储231 这个数,所以我们需要用long作为n的数据类型。
- 避免写死递归:递归函数的出口就是子问题不能再衍生子问题的时候,当n等于0时,x的0次方等于1。
题解代码:
class Solution {
public double myPow(double x, long n) {
if(n==0){
return 1;//x的0次方等于1
}
if(n<0){
double tmp = myPow(x,-n/2);
return (n%2 == 0) ? (1/(tmp*tmp)):(1/(tmp*tmp*x));
}else{
double tmp = myPow(x,n/2);
return n%2 == 0 ? tmp*tmp : tmp*tmp*x;
}
}
}
本篇到这就结束了~
刷算法的日子真的是痛苦极了,但是找工作的时候第一关就是笔试,不刷算法又没办法哈哈~
哦哦,还要长期坚持刷,因为我深知以我的脑子速成不了这玩意呜呜~
接下来打算再刷刷深度优先遍历方面的题。
我是码代码的西西弗,一起冲鸭~