链表相关的核心点
- null/nil 异常处理
- dummy node 哑巴节点
- 快慢指针
- 插入一个节点到排序链表
- 从一个链表中移除一个节点
- 翻转链表
- 合并两个链表
- 找到链表的中间节点
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
非递归解法
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode current = head;
while (current != null && current.next != null) {
if (current.val == current.next.val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return head;
}
}递归解法
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
head.next = deleteDuplicates(head.next);
return head.val == head.next.val ? head.next : head;
}
}给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现的数字。
思路:链表头结点可能被删除,所以用 dummy node 辅助删除
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode a = dummy;
ListNode b = head.next;
while (b != null) {
if (a.next.val != b.val) {
a = a.next;
b = b.next;
} else {
while (b != null && a.next.val == b.val) {
b = b.next;
}
a.next = b;
b = b == null ? null : b.next;
}
}
return dummy.next;
}
}注意点 • A->B->C 删除 B,A.next = C • 删除用一个 Dummy Node 节点辅助(允许头节点可变) • 访问 b.next一定要保证 X != null
反转一个单链表。
思路:用一个 newHead 做头结点,将链表依次插到头结点后面。头插法
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = new ListNode(-1);
while (head != null) {
ListNode next = head.next;
head.next = newHead.next;
newHead.next = head;
head = next;
}
return newHead.next;
}
}反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
思路:先遍历到 m 处,翻转,再拼接后续,注意指针处理
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
ListNode dummy = new ListNode(-1); // 哑巴节点,指向链表的头部
dummy.next = head;
ListNode pre = dummy; // pre 指向要翻转子链表的第一个节点
for (int i = 1; i < m; ++i) {
pre = pre.next;
}
head = pre.next; // head指向翻转子链表的首部
ListNode next;
for (int i = m; i < n; ++i) {
next = head.next;
// head节点连接next节点之后链表部分,也就是向后移动一位
head.next = next.next;
// next节点移动到需要反转链表部分的首部
next.next = pre.next;
// pre继续为需要反转头节点的前驱节点
pre.next = next;
}
return dummy.next;
}
}将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:递归
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
} else if (l2 == null) {
return l1;
} else if (l1.val <= l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
思路:构造两个链表,beforeHead小于x的节点,afterHead大于x的节点,最后将他们连在一起
class Solution {
public ListNode partition(ListNode head, int x) {
// 构造两个链表,beforeHead小于x的节点,afterHead大于x的节点,最后将他们连在一起
// 这里的技巧就是给两个链表都搞一个哑巴节点,也就是指向头结点的节点
ListNode beforeHead = new ListNode(-1);
ListNode before = beforeHead;
ListNode afterHead = new ListNode(-1);
ListNode after = afterHead;
while (head != null) {
if (head.val < x) {
before.next = head;
before = before.next;
} else {
after.next = head;
after = after.next;
}
head = head.next;
}
after.next = null;
before.next = afterHead.next;
return beforeHead.next;
}
}哑巴节点使用场景
当头节点不确定的时候,使用哑巴节点
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
思路:归并排序,找中点和合并操作
class Solution {
public ListNode sortList(ListNode head) {
// 递归结束条件
if (head == null || head.next == null) {
return head;
}
ListNode slow = head, fast = head.next;
// 找到链表中点
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode rightHead = slow.next;
slow.next = null;
ListNode left = sortList(head);
ListNode right = sortList(rightHead);
return mergeLists(left, right);
}
private ListNode mergeLists(ListNode left, ListNode right) {
ListNode dummy = new ListNode(-1);
ListNode curNode = dummy;
while (left != null && right != null) {
if (left.val < right.val) {
curNode.next = left;
left = left.next;
} else {
curNode.next = right;
right = right.next;
}
curNode = curNode.next;
}
curNode.next = (right != null) ? right : left;
return dummy.next;
}
}下面是另一种解法类似归并排序,相当于用迭代模拟归并的过程。 时间复杂度O(nlogn),空间复杂度O(1) 有点复杂可以不看.详解见https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
ListNode currentHead, mergeHead1, mergeHead2, preNode, dummy;
currentHead = head;
int length = 0, currentMergeNumber = 1;
while (currentHead != null) {
currentHead = currentHead.next;
length++;
}
dummy = new ListNode(0);
dummy.next = head;
while (currentMergeNumber < length) {
preNode = dummy;
currentHead = dummy.next;
while (currentHead != null) {
int i = currentMergeNumber;
mergeHead1 = currentHead;
while (i > 0 && currentHead != null) {
currentHead = currentHead.next;
i--;
}
if (i > 0) {
break;
}
i = currentMergeNumber;
mergeHead2 = currentHead;
while (i > 0 && currentHead != null) {
currentHead = currentHead.next;
i--;
}
int lengthOfHead1 = currentMergeNumber, lengthOfHead2 = currentMergeNumber - i;
while (lengthOfHead1 > 0 && lengthOfHead2 > 0) {
if (mergeHead1.val < mergeHead2.val) {
preNode.next = mergeHead1;
mergeHead1 = mergeHead1.next;
lengthOfHead1--;
} else {
preNode.next = mergeHead2;
mergeHead2 = mergeHead2.next;
lengthOfHead2--;
}
preNode = preNode.next;
}
preNode.next = lengthOfHead1 == 0 ? mergeHead2 : mergeHead1;
while (lengthOfHead1 > 0 || lengthOfHead2 > 0) {
preNode = preNode.next;
lengthOfHead1--;
lengthOfHead2--;
}
preNode.next = currentHead;
}
currentMergeNumber *= 2;
}
return dummy.next;
}
}注意点
- 快慢指针 判断 fast 及 fast.Next 是否为 null 值
- 递归 mergeSort 需要断开中间节点
- 递归返回条件为 head 为 null 或者 head.Next 为 null
思路:找到中点断开,翻转后面部分,然后合并前后两个链表
class Solution {
public void reorderList(ListNode head) {
if (head == null || head.next == null) {
return;
}
ListNode slow = head, fast = head.next;
// 找到链表中点
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 将后一半链表翻转, 用fast记录后一段的头结点
fast = reverse(slow.next);
// 将链表切断
slow.next = null;
// 合并链表
mergeLists(head, fast);
}
private void mergeLists(ListNode l1, ListNode l2) {
ListNode head = l1, temp;
while (l1 != null && l2 != null) {
temp = l2.next;
l2.next = l1.next;
l1.next = l2;
l2 = temp;
l1 = l1.next.next;
}
// 将未合并完的l2接到l1后面
if (l2 != null) {
l1.next = l2;
}
l1 = head;
}
private ListNode reverse(ListNode head) {
ListNode newHead = null;
while (head != null) {
ListNode nextNode = head.next;
head.next = newHead;
newHead = head;
head = nextNode;
}
return newHead;
}
}给定一个链表,判断链表中是否有环。
思路:快慢指针,快慢指针相同则有环,如果有环每走一步快慢指针距离会减 1,最终重合。
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) {
return false;
}
ListNode p = head, q = head;
boolean flag = false;
while (p.next != null && q.next != null) {
p = p.next;
if (q.next.next != null) {
q = q.next.next;
} else {
break;
}
if (p == q) {
flag = true;
break;
}
}
return flag;
}
}给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回
null。
思路:快慢指针,快慢相遇之后,慢指针回到头,快慢指针步调一致一起移动,相遇点即为入环点
2(F + a) = F + N(a + b) + a
2F + 2a = F + 2a + b + (N - 1)(a + b)
F = b + (N - 1)(a + b)
F是到达入口点长度
N为fast跑第几圈会与slow相遇
详解见https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/huan-xing-lian-biao-ii-by-leetcode/
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode fast = head.next, slow = head;
while (fast != null && fast.next != null) {
if (fast == slow) { // 第一次相遇,slow回到head, fast从相遇点下一个节点开始走,
slow = head;
fast = fast.next;
while (fast != slow) { // 第二次相遇的地方就是环的入口
fast = fast.next;
slow = slow.next;
}
return slow;
}
fast = fast.next.next;
slow = slow.next;
}
return null;
}
}坑点
- 指针比较时直接比较对象,不要用值比较,链表中有可能存在重复值情况
- 第一次相交后,快指针需要从下一个节点开始和头指针一起匀速移动
另外一种方式是 fast=head,slow=head
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode fast = head, slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) { // 第一次相遇,slow回到head, fast从相遇点下一个节点开始走,
slow = head;
while (fast != slow) { // 第二次相遇的地方就是环的入口
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
return null;
}
}这两种方式不同点在于,一般用 fast=head.Next 较多,因为这样可以知道中点的上一个节点,可以用来删除等操作。
- fast 如果初始化为 head.Next 则中点在 slow.Next
- fast 初始化为 head,则中点在 slow
请判断一个链表是否为回文链表。
切成两半,把后半段反转,然后比较两半是否相等。
class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode slow = head, fast = head.next;
// 找到链表中点
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
if (fast != null) {
slow = slow.next;
}
// 将链表切成两半
cut(head, slow);
// 将后一半链表翻转,然后判断两半链表是否相等
return isEqual(head, reverse(slow));
}
private boolean isEqual(ListNode l1, ListNode l2) {
while (l1 != null && l2 != null) {
if (l1.val != l2.val) {
return false;
}
l1 = l1.next;
l2 = l2.next;
}
return true;
}
private void cut(ListNode head, ListNode cutNode) {
while (head.next != cutNode) {
head = head.next;
}
head.next = null;
}
private ListNode reverse(ListNode head) {
ListNode newHead = null;
while (head != null) {
ListNode nextNode = head.next;
head.next = newHead;
newHead = head;
head = nextNode;
}
return newHead;
}
}给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。 要求返回这个链表的 深拷贝。
class Solution {
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
Node ptr = head;
// 将原链表每个节点旁边增加一个节点
while (ptr != null) {
Node newNode = new Node(ptr.val);
newNode.next = ptr.next;
ptr.next = newNode;
ptr = newNode.next;
}
ptr = head;
// 将复制链表的random指向对应的位置
while (ptr != null) {
ptr.next.random = (ptr.random != null) ? ptr.random.next : null;
ptr = ptr.next.next;
}
// 将复制链表的next指向对应的位置
Node ptrOld = head, ptrNew = head.next, newHead = head.next;
while (ptrOld != null) {
ptrOld.next = ptrOld.next.next;
ptrNew.next = (ptrNew.next != null) ? ptrNew.next.next : null;
ptrOld = ptrOld.next;
ptrNew = ptrNew.next;
}
return newHead;
}
}链表必须要掌握的一些点,通过下面练习题,基本大部分的链表类的题目都是手到擒来~
- null/nil 异常处理
- dummy node 哑巴节点
- 快慢指针
- 插入一个节点到排序链表
- 从一个链表中移除一个节点
- 翻转链表
- 合并两个链表
- 找到链表的中间节点