第四部分:递归
143.重排链表(中等)
题目:给定一个单链表 L
的头节点 head
,单链表 L
表示为:
L0 → L1 → … → Ln - 1 → Ln
请将其重新排列后变为:
L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
输入:head = [1,2,3,4] 输出:[1,4,2,3]
示例 2:
输入:head = [1,2,3,4,5] 输出:[1,5,2,4,3]
第一种思路:
第一个比较容易想到的一个方法是利用线性表存储该链表,然后利用线性表可以下标访问的特点,直接按顺序访问指定元素,重建该链表即可,这里就不详细解释了。
class Solution {
public void reorderList(ListNode head) {
// 检查链表是否为空
if (head == null) {
return; // 如果为空,则直接返回
}
// 创建一个列表来保存链表中的节点
List<ListNode> list = new ArrayList<ListNode>();
ListNode node = head;
// 遍历链表并将每个节点添加到列表中
while (node != null) {
list.add(node); // 将当前节点添加到列表
node = node.next; // 移动到下一个节点
}
// 定义两个指针分别指向列表的开始和结束
int i = 0, j = list.size() - 1;
// 交替合并节点,直到两个指针相遇
while (i < j) {
list.get(i).next = list.get(j); // 将前半部分的节点指向后半部分的节点
i++; // 移动到前半部分的下一个节点
// 检查指针是否相遇
if (i == j) {
break; // 如果指针相遇,结束合并
}
list.get(j).next = list.get(i); // 将后半部分的节点指向前半部分的下一个节点
j--; // 移动到后半部分的上一个节点
}
// 在最后一个节点上设置 next 为 null,以终止链表
list.get(i).next = null; // 处理最后一个节点的链接
}
}
第二种思路:
一开始的想法就是直接想像 《24.两两交换链表中的节点》一样套用递归的模板,写了一些代码后发现有点困难卡住了。看了解答知晓了递归也可以当作一个小步骤实现。
目标链表即为将原链表的左半端和反转后的右半端合并后的结果。
这样任务即可划分为三步:
找到原链表的中点(参考「876. 链表的中间结点」)。
我们可以使用快慢指针来 O(N) 地找到链表的中间节点。
将原链表的右半端反转(参考「206. 反转链表」)。
我们可以使用迭代法实现链表的反转。
将原链表的两端合并。
因为两链表长度相差不超过 1,因此直接合并即可。
找中间节点:
采用快慢指针法。在遍历链表时,慢指针每次移动一格,快指针每次移动两格。当快指针到达链表尾部时,慢指针正好在中间。
通过这种方式找到中间节点后,可以将链表分为前半部分和后半部分。
反转后半部分链表:
将链表的后半部分进行反转,这样可以方便地将两部分链表交替合并。
反转链表可以使用迭代方法或递归方法,这里采用迭代的方法,逐个改变指针的方向。
交替合并链表:
将前半部分和反转后的后半部分交替合并。具体操作是,从前半部分取一个节点,然后从后半部分取一个节点,重复这个过程,直到合并完所有节点。
这个过程可以使用递归来实现,每次先取出一个节点,然后递归合并剩下的部分。
/**
* 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 void reorderList(ListNode head) {
if (head == null || head.next == null) return; // 检查链表是否为空或只有一个节点
// 第一步:找到链表的中间节点
ListNode mid = findMiddle(head);
// 第二步:反转链表的后半部分
ListNode secondHalf = reverseList(mid.next);
mid.next = null; // 将链表分为两个部分
// 第三步:合并两个部分
mergeLists(head, secondHalf);
}
// 查找链表的中间节点
private ListNode findMiddle(ListNode head) {
ListNode slow = head; // 慢指针
ListNode fast = head; // 快指针
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针移动一步
fast = fast.next.next; // 快指针移动两步
}
return slow; // 返回中间节点
}
// 反转给定的链表
private ListNode reverseList(ListNode head) {
ListNode prev = null; // 前一个节点
ListNode curr = head; // 当前节点
while (curr != null) {
ListNode nextTemp = curr.next; // 保存下一个节点
curr.next = prev; // 反转当前节点的指针
prev = curr; // 更新前一个节点为当前节点
curr = nextTemp; // 移动到下一个节点
}
return prev; // 返回反转后的链表头节点
}
// 合并两个链表
private void mergeLists(ListNode first, ListNode second) {
if (second == null) return; // 如果第二个链表为空,直接返回
ListNode temp1 = first.next; // 保存第一个链表的下一个节点
ListNode temp2 = second.next; // 保存第二个链表的下一个节点
first.next = second; // 将第二个链表的节点插入到第一个节点后
second.next = temp1; // 将第一个链表的下一个节点插入到第二个节点后
// 递归合并剩余的部分
mergeLists(temp1, temp2);
}
}
206.反转链表(简单)
题目:给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]
第一种思路:
基本情况:
检查链表是否为空(
head == null
)或只有一个节点(head.next == null
)。如果是,直接返回head
,因为它已经是反转后的状态。递归反转:
通过递归调用
reverseList(head.next)
,将当前节点的下一个节点开始的子链表反转。此时,newHead
将指向反转后的子链表的头节点。反转连接:
在回溯的过程中,
head.next.next = head;
将当前节点head
连接到它的后继节点中,即将原先的下一个节点的指针指回到当前节点,完成反转。将
head.next
设为null
,以切断链表,避免形成环。返回新头节点:
最终返回
newHead
,它是反转后的链表的新头节点。
/**
* 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 || head.next == null)
return head;
// 递归调用,将链表的剩余部分进行反转
ListNode newHead = reverseList(head.next);
// 将当前节点的下一个节点的指向改为当前节点
head.next.next = head; // 反转当前节点与其下一个节点的连接
head.next = null; // 断开当前节点的下一个节点的引用,避免形成循环
// 返回反转后的新头节点
return newHead; // newHead 是递归过程中最初的最后一个节点
}
}
2.两数相加(中等)
题目:给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4] 输出:[7,0,8] 解释:342 + 465 = 807.
第一种思路:
整体思路是模拟加法过程,逐位计算并处理进位,最终获得两个数字的和,以链表形式返回。
输入表示:两个非负整数以链表形式表示,链表的每个节点存储一个数字,每个链表的头节点表示最低位。
初始化:
创建一个虚拟头节点,用于构建结果链表。
定义一个指针
current
来指向结果链表的最后一个节点。初始化进位(
carry
)为0。主循环:
使用
while
循环遍历两个链表,直到两个链表都遍历完且进位为0。在每次循环中,计算当前位的和(包括进位和当前节点的值)。
计算和进位:
如果链表
l1
或l2
不为空,将相应节点的值加到和(sum
)中,并移动指针。更新进位为
sum / 10
,当前位的下一个节点的值为sum % 10
。新节点处理:
创建新节点保存当前位的值,并将其链接到结果链表中。
返回结果:
循环结束后,返回虚拟头节点的下一个节点,得到完整的结果链表。
addTwoNumbers 方法:
dummyHead: 创建一个虚拟头节点,用于简化链表操作。
current: 当前节点指针,从
dummyHead
开始,逐步填充结果节点。carry: 进位变量,用于存储两数相加时的进位。
循环条件
while (l1 != null || l2 != null || carry != 0)
确保在处理完所有节点后仍然考虑进位。对于每个节点,分别累加两个链表的值和进位,创建新的节点并将其添加到结果链表中。
/**
* 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 addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(0); // 创建一个虚拟头节点,便于处理结果链表
ListNode current = dummyHead; // 当前节点指向虚拟头节点
int carry = 0; // 初始化进位为0
// 当l1或l2不为空,或者还有进位时,继续循环
while (l1 != null || l2 != null || carry != 0) {
int sum = carry; // 将当前的进位值加到sum中
// 如果l1不为空,将l1的值加到sum中
if (l1 != null) {
sum += l1.val; // 加上l1当前节点的值
l1 = l1.next; // 移动到l1的下一个节点
}
// 如果l2不为空,将l2的值加到sum中
if (l2 != null) {
sum += l2.val; // 加上l2当前节点的值
l2 = l2.next; // 移动到l2的下一个节点
}
carry = sum / 10; // 计算新的进位(sum的整除10)
current.next = new ListNode(sum % 10); // 创建新节点,值为sum的余数(当前位的值)
current = current.next; // 移动当前节点指针到新创建的节点
}
return dummyHead.next; // 返回结果链表,跳过虚拟头节点
}
}