第四部分:递归
前情:什么情况判断应该使用递归?
在编程中,使用递归的情况通常包括以下几种场景:
-
问题可以被分解为相似的子问题:
-
递归特别适合那些可以将问题分解为相似的小问题的情形。例如,计算斐波那契数列、合并排序、快速排序等。
-
-
树形结构的问题:
-
处理树形结构的算法(如遍历二叉树、查找、插入以及删除操作)通常使用递归,因为树本身具有递归的性质。
-
-
简化代码:
-
有些问题通过递归实现的代码比通过迭代实现的代码更可读,更容易理解。例如,遍历图或链表结构时,递归的实现可能更直观。
-
-
自然定义的算法:
-
一些算法本质上是递归的,例如动态规划中的某些解法,计算阶乘或最小公倍数等。
-
-
需回溯的场景:
-
回溯算法通常使用递归,针对组合、排列、子集等问题,递归能够更自然地表示选择和撤销的过程。
-
24.两两交换链表中的节点(中等)
题目:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4] 输出:[2,1,4,3]
第一种思路:
如果链表中没有节点,或者链表中只有一个节点,此时无法进行交换。
如果链表中至少有两个节点,则在两两交换链表中的节点之后,原始链表的头节点变成新的链表的第二个节点,原始链表的第二个节点变成新的链表的头节点。链表中的其余节点的两两交换可以递归地实现。在对链表中的其余节点递归地两两交换之后,更新节点之间的指针关系,即可完成整个链表的两两交换。
形象的思考过程:
假设链表是:1 -> 2 -> 3 -> 4
第一次调用:
当前
head
是 1,node
是 2。之后会递归处理 3 -> 4 的交换。递归下去:
对于子链表 3 -> 4,将会变成 4 -> 3。
连接回来:
完成 1 和 2 的交换后,原链表将变成 2 -> 1 -> 4 -> 3。
这样,最终链表的结果就是 2 -> 1 -> 4 -> 3,递归一次又一次返回,构建出最终的结果。
思路分解:
基本情况(递归终止条件):
首先检查链表是否为空或只有一个节点。如果是,直接返回头节点,因为没有节点需要交换。这确保了递归的终止条件。
if (head == null || head.next == null) { return head; // 递归基础情况:无节点或只有一个节点 }
定义变量:
找到当前两节点中的第二个节点,即
node = head.next
。在这里,node
是新链表的头节点。递归调用:
对于剩余的链表,递归调用
swapPairs(node.next)
来处理剩下的节点。这里的node.next
代表的是当前节点node
的下一个节点,这样可以确保每次递归中处理的都是未交换的节点。head.next = swapPairs(node.next); // 递归处理后续的节点对
更新指针关系:
将
node
(当前的第二个节点)指向head
(当前的第一个节点)。这样完成了当前两个节点的交换。node.next = head; // 更新指针,完成交换
返回新头节点:
最后,返回新的头节点,即
node
。这样,在向上返回的过程中,会逐级构建交换后的链表。
return node; // 返回新的链表头
/**
* 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) {
// 首先检查头节点是否为 null
if (head == null || head.next == null) {
return head; // 如果为空或只有一个节点,直接返回
}
// 存储原始链表头节点的下一个节点,即新链表的头节点
ListNode node = head.next;
// 重新链接
head.next = swapPairs(node.next); // 递归调用以处理下一个节点对
node.next = head; // 将当前节点与其前一个节点连接
return node; // 返回新的头节点
}
}
390.消除游戏(中等)
题目:列表 arr
由在范围 [1, n]
中的所有整数组成,并按严格递增排序。请你对 arr
应用下述算法:
-
从左到右,删除第一个数字,然后每隔一个数字删除一个,直到到达列表末尾。
-
重复上面的步骤,但这次是从右到左。也就是,删除最右侧的数字,然后剩下的数字每隔一个删除一个。
-
不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字。
给你整数 n
,返回 arr
最后剩下的数字。
示例 1:
输入:n = 9 输出:6 解释: arr = [1, 2, 3, 4, 5, 6, 7, 8, 9] arr = [2, 4, 6, 8] arr = [2, 6] arr = [6]
第一种思路:
这部分主要在练习递归类型的题目,所有就直接往递归方面想了,确实在敲的过程中慢慢更熟练其逻辑思路了,但是写完之后点击提交显示“最后执行的输入n =1000000 ,3373 / 3377 个通过的测试用例”,不是几个意思啊,我当然知道数值过大会超时,但是归入递归里的题目你整这么大的数做什么,建议递归但是不能用递归是吧!!!
虽然如此,但是案例基本都通过了,说明我的思路应该没有问题:
初始化数据结构:
使用一个动态数组(
List
)来存储从1
到n
的数字。递归删除元素:
定义一个递归函数
delete
,它接受当前数组和一个控制删除方向的变量flog
。
flog
变量用于指示是从左到右 (flog = 1
) 还是从右到左 (flog = -1
) 进行删除。基准条件:
如果数组只剩下一个元素,直接返回这个元素,这作为递归的终止条件。
根据方向删除元素:
当
flog
为1
时,遍历数组并每次删除一个元素(从头到尾)。当
flog
为-1
时,倒序遍历数组并逐步删除元素(从尾到头)。切换删除方向:
在每次递归调用结束后,切换
flog
的值,以改变后续的删除方向。!!!!这里需要注意第四步的根据方向删除元素,从头到尾 i++ 是因为ArrayList集合在删除元素后下标会动态的变化,所以一次操作 i 只用移动一个单位,但是从尾到头时,操作删除后ArrayList集合各元素的下标没有发生变化,所有 i 此时就要一次移动两个单位了。
class Solution {
// 主函数,返回最后剩下的数字
public int lastRemaining(int n) {
// 创建一个包含从 1 到 n 的数字的列表
List<Integer> arr = new ArrayList<>();
for (int i = 0; i < n; i++)
arr.add(i + 1); // 将数字添加到列表中
// 标记变量,控制删除的方向:1表示从左到右,-1表示从右到左
int flog = 1;
// 进行删除操作,并返回最后剩下的数字
int re = delete(arr, flog);
return re; // 返回结果
}
// 递归删除函数
public int delete(List<Integer> arr, int flog) {
// 如果列表中只剩下一个数字,返回该数字
if (arr.size() == 1) return arr.get(0);
// 如果 flog 为 1,执行从左到右的删除
if (flog == 1) {
// 遍历列表,删除每个第一个数字
for (int i = 0; i < arr.size(); i++)
arr.remove(i); // 删除每隔一个的数字
} else {
// 如果 flog 为 -1,执行从右到左的删除
for (int i = arr.size() - 1; i >= 0; i -= 2)
arr.remove(i); // 删除每隔一个的数字
}
// 切换删除方向
flog = flog * -1;
// 递归调用 delete 函数
return delete(arr, flog);
}
}
官方的解答只有一种用数学分析的,这里就不介绍了。