1、附上题目链接
-
翻转链表类型:
206. 反转链表
92. 反转链表 II
25. K 个一组翻转链表 -
快慢指针类型:
61. 旋转链表
19. 删除链表的倒数第 N 个结点 -
有序链表合并:
23. 合并K个升序链表 -
找到链表中点:
876. 链表的中间结点 -
复制带随机指针的链表
138. 复制带随机指针的链表 -
链表排序
147. 对链表进行插入排序
148. 排序链表 -
删除链表重复元素(建议这三道一起做)暂未讲解
203. 移除链表元素
83. 删除排序链表中的重复元素
82. 删除排序链表中的重复元素 II -
节点交换 暂未讲解
24. 两两交换链表中的节点
2、代码关键点详解
2.1 关键点:翻转链表
2.1.1 翻转链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
/**************** 迭代 *****************/
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode next = null;
while (head != null) {
next = head.next; //临时节点,暂存当前节点的下一节点,用于后移
head.next = pre; //将当前节点指向它前面的节点
pre = head; //前指针后移
head = next; //当前指针后移
}
return pre;
}
/**************** 递归 *****************/
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode next = head.next; // 保存当前节点的下一节点
ListNode reverseHead = reverseList(next); // 翻转剩下部分
next.next = head; //将翻转后的结果 和 第一个节点相连接
head.next = null; //断链
return reverseHead;
}
}
关键点1:迭代
- 注意:这个代码递归和迭代方式都很简单,但是要记住迭代方式的算法代码,因为在其他算法题中经常用得到。
2.1.2 翻转链表 II
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
ListNode res = new ListNode(0);
res.next = head;
ListNode node = res;
//找到需要反转的那一段的上一个节点。
for (int i = 1; i < m; i++) {
node = node.next;
}
//node.next就是需要反转的这段的起点。
ListNode nextHead = node.next;
ListNode next = null;
ListNode pre = null;
//反转m到n这一段
for (int i = m; i <= n; i++) { // <= n !!!!!!!!!!
next = nextHead.next;
nextHead.next = pre;
pre = nextHead;
nextHead = next;
}
//将反转的起点的next指向next。
node.next.next = next; // !!!!!!!!!!!!!
//需要反转的那一段的上一个节点的next节点指向反转后链表的头结点
node.next = pre; // !!!!!!!!!!!!!
return res.next;
}
}
关键点1:翻转流程
- 找到需要反转的那一段的上一个节点
- 反转m到n这一段
for (int i = m; i <= n; i++)
- 将反转的起点的next指向next
node.next.next = next; // !!!!!!!!!!!!!
- 需要反转的那一段的上一个节点的next节点指向反转后链表的头结点
node.next = pre; // !!!!!!!!!!!!!
2.1.3 K 个一组翻转链表
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null) return head;
ListNode l1 = head, l2 = head;
// 区间 [l1, l2) 包含 k 个待反转元素
for (int i = 0; i < k; i++) {
// 不足 k 个,不需要反转,base case
if (l2 == null) return head;
l2 = l2.next;
}
// 反转前 k 个元素
ListNode newHead = reverse(l1, l2);
// 递归反转后续链表并连接起来
l1.next = reverseKGroup(l2, k);
return newHead;
}
/** 反转区间 [l1, l2) 的元素,注意是左闭右开 */
public ListNode reverse(ListNode l1, ListNode l2) {
ListNode cur = l1;
ListNode pre = null, next = null;
while (cur != l2) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
关键点1:每 k 个节点一组进行翻转
ListNode l1 = head, l2 = head;
for (int i = 0; i < k; i++) {
if (l2 == null) return l1;
l2 = l2.next;
}
2.2 关键点:快慢指针
2.2.1 旋转链表
class Solution {
public ListNode rotateRight(ListNode head, int k) {
// base case
if (head == null || head.next == null || k <= 0) return head;
// 链表长度 和 链表尾指针
int len = 0;
ListNode tail = head;
while (tail != null && tail.next != null) { // tail.next != null 保证尾指针不为null !!!!!
len++;
tail = tail.next;
}
len++;
// 链表旋转
ListNode first = head, second = head;
k = k % len;
if (k == 0) return head; // 不用旋转
while (k > 0) { // k > 0 保证快指针找到旋转节点的前一个位置
first = first.next;
k--;
}
while (first != null && first.next != null) { // first.next != null 保证快指针找到旋转节点的前一个位置
first = first.next;
second = second.next;
}
ListNode secondHead = second.next; // 后半部分头结点
second.next = null; // 前半部分 和 后半部分断开
tail.next = head; // 将第二部分的尾 和 第一部分的头相连
return secondHead;
}
}
关键点1:k值
- 由于本题没有注明 k 值得范围,而且根据 示例 可以看出,当 k 值大于链表长度时,采用对链表长度取模的方式进行旋转
k = k % len;
。
关键点2:旋转
- 对于该题,采用快慢指针的方法进行旋转,先通过快指针找到旋转节点的前一个位置,然后以旋转点为头,将前后链表断开,并将链表的尾指针指向头指针即可
关键点3:快慢指针
- 将快指针和慢指针均初始化为同一节点(如:
ListNode first = head, second = head;
)
2.2.2 删除链表的倒数第 N 个结点
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
/* 双指针 + 伪头节点 推荐 */
if (head == null || n <= 0) return head;
ListNode pre = new ListNode(0); //伪头结点
pre.next = head;
ListNode fast = pre, slow = pre; //维护两个双指针,将指针指向pre。若指向head,而我们恰好要删除head,会出现异常
while (n > 0) {
// if (fast.next == null) return head.next; // 注意!!!!!!!
if (fast == null) return head.next; // 注意!!!!!!!
fast = fast.next; //将第一个指针先向前移动 n 步
n--;
}
while (fast != null && fast.next != null) { // 注意 fast.next != null !!!!!!!
slow = slow.next; // 同时移动快慢指针,直到快指针指向null
fast = fast.next;
}
if (slow != null && slow.next != null) {
slow.next = slow.next.next;
}
return pre.next;
}
}
关键点1:伪头结点
- 为了防止删除的头结点,无法返回头指针,所以我们提前设置一个伪头结点
ListNode pre = new ListNode(0); //伪头结点
pre.next = head;
。
关键点2:当删除节点长度大于等于链表长度时
- 对于该题,当删除节点长度大于等于链表长度时,都是删除的头结点。
if (fast == null) return head.next;
关键点3:待删除节点查询
- 该题采用快慢指针找待删除节点的方式和上一题相同,都是先找到待删除节点的上一个节点指针
2.3 关键点:有序链表合并
2.3.1 合并K个升序链表
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists == null || lists.length == 0) return null; // 边界条件
return merge(lists, 0, lists.length - 1);
}
//归并
private ListNode merge(ListNode[] lists, int left, int right) {
// 停止条件
if(left == right) return lists[left];
//归并
int mid = left + (right - left) / 2;
ListNode l1 = merge(lists, left, mid);
ListNode l2 = merge(lists, mid + 1, right);
return mergeTwoLists(l1, l2);
}
//原地合并两个链表 !!!!!
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 停止条件
if(l1 == null) return l2;
if(l2 == null) return l1;
// 递归
if(l1.val < l2.val){
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
关键点1:合并两个有序链表
- 采用递归的方式原地合并两个有序链表,代码很重要哟,记不住就背过吧
2.4 关键点:找到链表中点
2.4.1 链表的中间结点
- 代码略
关键点1:找到链表中点
- 链表长度为奇数时,slow 为中间节点指针
- 链表长度为偶数时,slow 为中间偏左节点指针
public ListNode middleNode(ListNode head) {
ListNode fast = head.next;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
- 链表长度为奇数时,slow 为中间节点指针
- 链表长度为偶数时,slow 为中间偏右节点指针
public ListNode middleNode(ListNode head) {
ListNode fast = head, slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
2.5 关键点:链表复制
2.5.1 复制带随机指针的链表
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
if (head == null) return head;
Node cur = head, copyCur = null, next = null; // 当前指针,复制链表当前指针,当前节点下一个节点指针
// 复制next指针 1 - 1' - 2 - 2'
while (cur != null) {
// 当前节点下一个节点指针
next = cur.next;
copyCur = new Node(cur.val);
// 将当前节点和拷贝节点连接
cur.next = copyCur;
copyCur.next = next;
// 更新
cur = next;
}
// 复制随机指针
cur = head;
while (cur != null) {
cur.next.random = cur.random != null ? cur.random.next : null;
cur = cur.next.next;
}
// 拆开
Node copyHead = null;
cur = head; copyCur = null; next = null;
while (cur != null) {
// 当前节点下一个节点指针
next = cur.next.next;
if (copyHead == null) {
copyHead = cur.next;
copyCur = copyHead;
} else {
copyCur.next = cur.next;
copyCur = copyCur.next;
}
cur.next = next;
cur = cur.next;
}
return copyHead;
}
}
关键点1:next 节点复制
- 复制next指针,将复制节点连接在当前节点后边,形成这种格式 1 - 1’ - 2 - 2’ - null
关键点2:random 节点复制
- 复制随机指针
cur.next.random = cur.random != null ? cur.random.next : null;
2.6 关键点:链表排序
2.6.1 对链表进行插入排序
class Solution {
public ListNode insertionSortList(ListNode head) {
if (head == null) return head;
ListNode res = new ListNode(Integer.MIN_VALUE);
res.next = head;
ListNode sortTail = head; // 已排序部分尾指针
ListNode cur = head.next; // 当前指针
ListNode pre = res; // 待插入位置的前指针
while (cur != null) {
if (cur.val >= sortTail.val) {
cur = cur.next;
sortTail = sortTail.next;
} else {
pre = res;
// 找到 pre.val < cur.val <= pre.next.val 的点
while (pre != sortTail && pre.next.val < cur.val) {
pre = pre.next;
}
sortTail.next = cur.next; // 直接将已排序好的尾指针指向当前指针下一节点
cur.next = pre.next;
pre.next = cur;
cur = sortTail.next;
}
}
return res.next;
}
}
关键点1:维护一个待插入位置的前指针 和 已排序链表的尾指针
ListNode sortTail = head; // 已排序部分尾指针
ListNode pre = res; // 待插入位置的前指针
2.6.2 排序链表
class Solution {
/******************* 归并排序 自顶向下*********************/
// public ListNode sortList(ListNode head) {
// if (head == null || head.next == null) return head; //边界条件
// // 链表断开
// ListNode fast = head.next, slow = head;
// while (fast != null && fast.next != null) {
// fast = fast.next.next;
// slow = slow.next;
// }
// ListNode temp = slow.next; //第二部分链表头 如:123-45、123-456
// slow.next = null; //第一部分链表
// // 链表排序
// ListNode left = sortList(head); //第一部分链表排序
// ListNode right = sortList(temp); //第二部分链表排序
// // 链表合并
// return merge(left, right);
// }
// /* 合并两个有序链表 */
// public ListNode merge(ListNode l1, ListNode l2){
// if (l1 == null) return l2;
// if (l2 == null) return l1;
// if (l1.val < l2.val) {
// l1.next = merge(l1.next, l2);
// return l1;
// } else {
// l2.next = merge(l1, l2.next);
// return l2;
// }
// }
/******************* 归并排序 自底向上*********************/
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) return head;
int len = getListNodeLen(head); // 统计链表长度
ListNode res = new ListNode(0); res.next = head;
int itrv = 1;
while (itrv < len) {
ListNode curRes = res, cur = res.next; // cur = res.next 注意不要写成 head !!!
// 1. 找到合并链表的 l1 和 l2 头节点
while (cur != null) {
ListNode l1= cur;
// 1.1 找到合并链表的 l1 头节点
int loop = itrv;
while (cur != null && loop > 0) {
cur = cur.next;
loop--;
}
if (loop > 0) break; // 无 l2
// 1.2 找到合并链表的 l1 头节点
ListNode l2 = cur;
loop = itrv;
while (cur != null && loop > 0) {
cur = cur.next;
loop--;
}
// 2. 合并
int len1 = itrv, len2 = itrv - loop;
while (len1 > 0 && len2 > 0) {
if (l1.val < l2.val) {
curRes.next = l1;
l1 = l1.next;
len1--;
} else {
curRes.next = l2;
l2 = l2.next;
len2--;
}
curRes = curRes.next;
}
l1 = len1 > 0 ? l1 : l2;
len1 = len1 > 0 ? len1 : len2;
while (len1 > 0) {
curRes.next = l1;
l1 = l1.next;
curRes = curRes.next;
len1--;
}
curRes.next = cur; // 3. 和下一段链表拼接上
}
itrv *= 2;
}
return res.next;
}
// 统计链表长度
private int getListNodeLen(ListNode head) {
ListNode cur = head;
int len = 0;
while (cur != null) {
len++;
cur = cur.next;
}
return len;
}
}
关键点1:归并排序
- 自上向下:空间复杂度(n)
- 自下向上:空间复杂度(1)