链表是一种重要的数据结构,在工程项目中广泛使用。对于链表,要搞清楚是否有头节点,即第一个节点不存任何数据,只是表示链表的头部,而首节点才是链表第一个真正存放数据的节点。通常情况下,对有头节点的链表进行操作比较方便。如果原来的链表没有头节点,可以 new 一个 dummyHead 作为头节点,这样往往能减少大量边界条件的判断。下面通过几个例题来学习链表。
1.力扣206.反转链表[1]
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
反转链表是链表中最常考到的问题。可以使用头插法反转链表。具体解法如下:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null; // 首节点
while (head != null) {
ListNode next = head.next; // 临时存储下次要操作的节点
head.next = pre; // 头插法的要点是每次在首节点的前面插入
pre = head; // 插入后的节点变成新的首节点
head = next; // 取下次要操作的节点
}
return pre;
}
}
或者定义一个 dummyHead,每次在 dummyHead 的后面插入:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode dummyHead = new ListNode(0, null);
ListNode p = head;
while (p != null) {
ListNode next = p.next;
p.next = dummyHead.next;
dummyHead.next = p;
p = next;
}
return dummyHead.next;
}
}
2.力扣92. 反转链表 II[2]
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
示例 1:
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]
这道题和上一题基本一样,只是需要先将指针移动到要操作的区间。
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
// 定义一个哑节点,这样即使 left = 0 也不需要特殊处理
ListNode dummy = new ListNode(0, head);
// pre 用来指向要翻转的第一个节点
ListNode pre = dummy;
// 将指针移动到要反转的第一个节点的上一个节点
for (int i = 1; i < left; i++) pre = pre.next;
// cur 用来指向要翻转的第一个节点,始终将 cur 的下一个节点进行头插
ListNode cur = pre.next;
// 头插法
for (int i = 0; i < right - left; i++) {
ListNode next = cur.next; // 将 cur 的下一个节点取出来,准备头插
cur.next = next.next; // cur 连接到后面剩余的节点
// 头插
next.next = pre.next;
pre.next = next;
}
return dummy.next;
}
}
具体过程如下:
next = cur.next:
cur.next = next.next:
next.next = pre.next:
pre.next = next:
3.力扣25.K 个一组翻转链表[3]
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
进阶:
你可以设计一个只使用常数额外空间的算法来解决此问题吗?
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
示例 2:
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
若不考虑进阶的要求,可以用 Map 来记录下标:
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null) return head;
int idx = 0;
Map<Integer, ListNode> map = new HashMap<>();
ListNode p = head;
while (p != null) {
map.put(idx++, p);
p = p.next;
}
int cnt = idx / k; // idx是节点总数,cnt表示有几个段要翻转
for (int i = 0; i < cnt; i++) {
// 每段第一个节点连接到下一段第一个,防止最后一段不够k个导致不用翻转
map.get(i * k).next = map.get((i+1) * k);
// 从倒数第1个节点翻转到倒数第k-1个
for (int j = 1; j < k; j++) {
map.get((i+1) * k - j).next = map.get((i+1) * k - j - 1);
}
if (i > 0) { // 把前面一段的第一个节点的next连接到当前一段的最后一个节点
map.get((i-1) * k).next = map.get((i+1) * k - 1);
}
}
return map.get(k - 1);
}
}
若考虑进阶的要求,则不能借助 Map 记录下标:
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || head.next == null){
return head;
}
// dummy->1->2->3->4->5
ListNode dummy=new ListNode(0, head);
ListNode pre=dummy; // pre指每次要翻转的链表的头结点的上一个节点
ListNode end=dummy; // end指每次要翻转的链表的尾节点
while(end.next!=null){
//循环k次,找到需要翻转的链表的结尾
for(int i=0;i<k&&end != null;i++){
end=end.next;
}
//如果end==null,即需要翻转的链表的节点数小于k,不执行翻转。
if(end==null) break;
ListNode next=end.next; // 下一次要翻转的链表
end.next=null; //然后断开链表
ListNode start=pre.next; //记录下要翻转链表的头节点
pre.next=reverse(start);
//翻转后头节点变到最后。通过.next把断开的链表重新链接。
start.next=next;
//将pre和end初始化,换成下次要翻转的链表的头结点的上一个节点。即start
pre=start;
end=start;
}
return dummy.next;
}
private ListNode reverse(ListNode head) {
ListNode pre = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = pre;
pre = curr;
curr = next;
}
return pre;
}
}
欢迎关注公众号。
Reference
[1]力扣206. 反转链表:https://leetcode-cn.com/problems/reverse-linked-list/
[2]力扣92. 反转链表 II:https://leetcode-cn.com/problems/reverse-linked-list-ii/
[3]力扣25.K 个一组翻转链表:https://leetcode-cn.com/problems/reverse-nodes-in-k-group/