public class ListNode { int val; ListNode next; ListNode(int x) { val = x; } }
1. 在 O(1) 时间删除链表节点
Leetcode 237. Delete Node in a Linked List
题目描述:给定单链表中需要删除的节点(不是尾节点),在 O(1) 时间删除该节点。
分析:本题与《编程之美》上的「从无头单链表中删除节点」类似。主要思想都是「狸猫换太子」,即用下一个节点数据覆盖要删除的节点,然后删除下一个节点。但是如果节点是尾节点时,该方法就行不通了。
代码如下:
// 在 O(1) 时间从无头单链表中删除节点
public void deleteNode(ListNode node) { // 不能为空,不能为尾节点 if (null == node || null == node.next) { return; } node.val = node.next.val; node.next = node.next.next; }
2. 逆转单链表
LeetCode 206. Reverse Linked List
题目描述:输出一个单链表的逆序反转后的链表。
分析:非递归的算法很简单,用三个临时指针 prev、cur、next 在链表上循环一遍即可。递归算法是先逆转下一个节点,再逆转当前节点。
下面是两种算法的代码:
// 逆转单链表,循环方法
public ListNode reverseByLoop(ListNode head) { if (null == head || null == head.next) { return head; } ListNode prev = null; ListNode next = null; // 用 head 作为 cur 指针 while (null != head) { next = head.next; head.next = prev; prev = head; head = next; } return prev; } // 逆转单链表,递归方法 public ListNode reverseByRecursion(ListNode head) { // 第一个条件判断异常,第二个条件是结束递归 if (null == head || null == head.next) { return head; } ListNode newHead = reverseByRecursion(head.next); head.next.next = head; head.next = null; return newHead; }
单链表反转补充:
next域:存储直接后继位置的域称为指针域,它是存放结点的直接后继的地址(位置)的指针域(链域)。
data域+ next域:组成数据ai的存储映射,称为结点;
注意:①链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。
②每个结点只有一个链域的链表称为单链表(Single Linked List)。
所谓的链表就好像火车车厢一样,从火车头开始,每一节车厢之后都连着后一节车厢。
要实现单链表存储,首先是创建一结点类,其Java代码如下:
- class Node {
- private int Data;// 数据域
- private Node Next;// 指针域
- public Node(int Data) {
- // super();
- this.Data = Data;
- }
- public int getData() {
- return Data;
- }
- public void setData(int Data) {
- this.Data = Data;
- }
- public Node getNext() {
- return Next;
- }
- public void setNext(Node Next) {
- this.Next = Next;
- }
- }
(1)递归反转法:在反转当前节点之前先反转后续节点。这样从头结点开始,层层深入直到尾结点才开始反转指针域的指向。简单的说就是从尾结点开始,逆向反转各个结点的指针域指向,其过程图如下所示:
head:是前一结点的指针域(PS:前一结点的指针域指向当前结点)
head.getNext():是当前结点的指针域(PS:当前结点的指针域指向下一结点)
reHead:是反转后新链表的头结点(即原来单链表的尾结点)
- package javatest1;
- public class javatest1 {
- public static void main(String[] args) {
- Node head = new Node(0);
- Node node1 = new Node(1);
- Node node2 = new Node(2);
- Node node3 = new Node(3);
- head.setNext(node1);
- node1.setNext(node2);
- node2.setNext(node3);
- // 打印反转前的链表
- Node h = head;
- while (null != h) {
- System.out.print(h.getData() + " ");
- h = h.getNext();
- }
- // 调用反转方法
- head = Reverse1(head);
- System.out.println("\n**************************");
- // 打印反转后的结果
- while (null != head) {
- System.out.print(head.getData() + " ");
- head = head.getNext();
- }
- }
- /**
- * 递归,在反转当前节点之前先反转后续节点
- */
- public static Node Reverse1(Node head) {
- // head看作是前一结点,head.getNext()是当前结点,reHead是反转后新链表的头结点
- if (head == null || head.getNext() == null) {
- return head;// 若为空链或者当前结点在尾结点,则直接还回
- }
- Node reHead = Reverse1(head.getNext());// 先反转后续节点head.getNext()
- head.getNext().setNext(head);// 将当前结点的指针域指向前一结点
- head.setNext(null);// 前一结点的指针域令为null;
- return reHead;// 反转后新链表的头结点
- }
- }
- class Node {
- private int Data;// 数据域
- private Node Next;// 指针域
- public Node(int Data) {
- // super();
- this.Data = Data;
- }
- public int getData() {
- return Data;
- }
- public void setData(int Data) {
- this.Data = Data;
- }
- public Node getNext() {
- return Next;
- }
- public void setNext(Node Next) {
- this.Next = Next;
- }
- }
pre:上一结点
cur: 当前结点
tmp: 临时结点,用于保存当前结点的指针域(即下一结点)
- package javatest1;
- public class JavaTest1 {
- public static void main(String[] args) {
- Node head = new Node(0);
- Node node1 = new Node(1);
- Node node2 = new Node(2);
- Node node3 = new Node(3);
- head.setNext(node1);
- node1.setNext(node2);
- node2.setNext(node3);
- // 打印反转前的链表
- Node h = head;
- while (null != h) {
- System.out.print(h.getData() + " ");
- h = h.getNext();
- }
- // 调用反转方法
- // head = reverse1(head);
- head = reverse2(head);
- System.out.println("\n**************************");
- // 打印反转后的结果
- while (null != head) {
- System.out.print(head.getData() + " ");
- head = head.getNext();
- }
- }
- /**
- * 遍历,将当前节点的下一个节点缓存后更改当前节点指针
- */
- public static Node reverse2(Node head) {
- if (head == null)
- return head;
- Node pre = head;// 上一结点
- Node cur = head.getNext();// 当前结点
- Node tmp;// 临时结点,用于保存当前结点的指针域(即下一结点)
- while (cur != null) {// 当前结点为null,说明位于尾结点
- tmp = cur.getNext();
- cur.setNext(pre);// 反转指针域的指向
- // 指针往下移动
- pre = cur;
- cur = tmp;
- }
- // 最后将原链表的头节点的指针域置为null,还回新链表的头结点,即原链表的尾结点
- head.setNext(null);
- return pre;
- }
- }
- class Node {
- private int Data;// 数据域
- private Node Next;// 指针域
- public Node(int Data) {
- // super();
- this.Data = Data;
- }
- public int getData() {
- return Data;
- }
- public void setData(int Data) {
- this.Data = Data;
- }
- public Node getNext() {
- return Next;
- }
- public void setNext(Node Next) {
- this.Next = Next;
- }
- }
3. 删除单链表倒数第 n 个节点
LeetCode 19. Remove Nth Node From End of List
题目描述:删除单链表倒数第 n 个节点,1 <= n <= length,尽量在一次遍历中完成。
分析:看到题目时的第一想法是先遍历一次计算出单链表的长度 length,然后在遍历第二次删除第 length - n + 1 个节点,但是这需要遍历两次。正常的删除第 n 个节点只需要遍历一次就可以,如何只遍历一次找到倒数第 n 个节点呢?可以设置两个指针 p1、p2,首先 p1 和 p2 都指向 head,p2 移动到第 n 个节点,然后 p1 和 p2 同时向后移动,当 p2 移动到末尾时,p1 刚好指向倒数第 n 个节点。因为最后要删除倒数第 n 个节点,所以可以找到倒数第 n + 1 个节点,方便删除节点。
代码如下:
// 遍历一次,删除单链表倒数第 n 个节点
public ListNode removeNthFromEnd(ListNode head, int n) { if (null == head) { return head; } ListNode p1 = head; ListNode p2 = head; // 1. p2 移动到第 n + 1 个节点 for (int i = 0; i < n; i ++>) { p2 = p2.next; } // n == 链表长度时,p2 指向第 n + 1 节点为空,倒数第 n 个节点就是头节点 if (null == p2) { p1 = head.next; return p1; } // p1 和 p2 同时向后移动,直到 p2 到达尾节点 while (null != p2.next) { p1 = p1.next; p2 = p2.next; } // 此时 p1 指向倒数第 n + 1 个节点,删除它的下一个节点 p1.next = p1.next.next; return head; }
4. 求单链表的中间节点
题目描述:求单链表的中间节点,如果链表的长度为偶数,返回中间两个节点的任意一个,若为奇数,则返回中间节点。
分析:这道题的思路和第 3 题「删除单链表倒数第 n 个节点」很相似。如果要求只能遍历一遍链表的花,也通过两个指针来完成。两个指针从头节点开始,慢指针每次向后移动一步,快指针每次向后移动两步,直到快指针移动到尾节点时,慢指针移动到中间节点。
// 遍历一次,找出单链表的中间节点
public ListNode findMiddleNode(ListNode head) { if (null == head) { return; } ListNode slow = head; ListNode fast = head; //如果要求在单链表长度为偶数的情况下,返回中间两个节点的第一个,可以用下面的循环条件 //while(null != fast.next && null != fast.next.next) while (null != fast && null != fast.next) { fast = fast.next.next; slow = slow.next; } return slow; }
5. 判断单链表是否存在环
LeetCode 141. Linked List Cycle
题目描述:判断一个单链表是否有环
分析:还是通过快慢指针来解决,两个指针从头节点开始,慢指针每次向后移动一步,快指针每次向后移动两步,如果存在环,那么两个指针一定会在环内相遇。
代码如下:
// 判断单链表是否有环
public boolean hasCycle(ListNode head) { if (null == head) { return false; } ListNode slow = head; ListNode fast = head; while (null != fast.next && null != fast.next.next) { fast = fast.next.next; slow = slow.next; if (fast == slow) { return true; } } return false; }
6. 单链表是否有环扩展:找到环的入口点
LeetCode 142. Linked List Cycle II
题目描述:判断单链表是否有环,如果有,找到环的入口点
分析:由上题可知,按照 p2 每次两步,p1 每次一步的方式走,发现 p2 和 p1 重合,确定了单向链表有环路了。接下来,让 p2 回到链表的头部,重新走,每次步长不是走 2 了,而是走 1,那么当 p1 和 p2 再次相遇的时候,就是在环路的入口点。
假设起点到环入口的距离尾 a,p1 和 p2 第一次相遇的相交点 M 与环入口的距离为 b,环的周长为 L,当 p1 和 p2 第一次相遇时,假设 p1 走了 n 步。其中 p1 和 p2 第一次相遇时,p1 在环内走过的步数为 b,因为当 p1 走到环入口时,p2 已经在环内了,假设此时 p2 走到环入口的步数为 c,那么 p1 再走 c 步 p2 刚好追上来和 p1 相遇,c < L,所以此时 p1 肯定还没走完一圈。那么根据上面的假设,有下面的关系:
p1 走的路径:a + b = n
p2 走的路径:a + b + k * L = 2n
,假设此时 p2 比 p1 多走了 k 圈环路,k >= 1
根据上面的两个等式可以得出k * L = n = a + b
,那么从相交点 M 开始,p1 再走 a(a = k * L - b) 步,就相当于走了 k 圈,然后回退 b 步,注意环入口到相交点的距离刚好为 b,所以 p1 再走 a 步时到达环入口;而 p2 从头开始走 a 的话也到达了环入口,与 p1 相遇。
而在后面这个步骤中,p1 和 p2 前 a 步走的路径不同,再次相遇时必然在环的入口点。
代码如下:
// 找到环的入口点
public ListNode findLoopPort(ListNode head) { if (null == head) { return null; } ListNode p1 = head; ListNode p2 = head; boolean hasCycle = false; // 1. 判断是否有环 while (null != p2.next && null != p2.next.next) { p1 = p1.next; p2 = p2.next.next; if (p1 == p2) { hasCycle = true; break; } } if (!hasCycle) { return null; } // p2 从头开始走,步长变为 1 p2 = head; while (p1 != p2) { p1 = p1.next; p2 = p2.next; } return p1; }
7. 判断两个无环单链表是否相交
题目描述:给出两个无环单链表
A: a1 → a2
↘
c1 → c2 → c3 → null
↗
B: b1 → b2 → b3
判断 A 和 B 是否相交。
分析:
1.最直接的方法是判断 A 链表的每个节点是否在 B 链表中,但是这种方法的时间复杂度为 O(Length(A) * Length(B))。
2.转化为环的问题。把 B 链表接在 A 链表后面,如果得到的链表有环,则说明两个链表相交。可以之前讨论过的快慢指针来判断是否有环,但是这里还有更简单的方法。如果 B 链表和 A 链表相交,把 B 链表接在 A 链表后面时,B 链表的所有节点都在环内,所以此时只需要遍历 B 链表,看是否会回到起点就可以判断是否相交。这个方法需要先遍历一次 A 链表,找到尾节点,然后还要遍历一次 B 链表,判断是否形成环,时间复杂度为 O(Length(A) + Length(B))。
3.除了转化为环的问题,还可以利用“如果两个链表相交于某一节点,那么之后的节点都是共有的”这个特点,如果两个链表相交,那么最后一个节点一定是共有的。所以可以得出另外一种解法,先遍历 A 链表,记住尾节点,然后遍历 B 链表,比较两个链表的尾节点,如果相同则相交,不同则不相交。时间复杂度为 O(Length(A) + Length(B)),空间复杂度为 O(1),思路比解法 2 更简单。
解法 3 的代码如下:
// 判断两个无环单链表是否相交
public boolean isIntersect(ListNode headA, ListNode headB) { if (null == headA || null == headB) { return false; } if (headA == headB) { return true; } while (null != headA.next) { headA = headA.next; } while (null != headB.next) { headB = headB.next; } return headA == headB; }
8. 两个链表相交扩展:判断两个有环单链表是否相交
题目描述:上面的问题是针对无环链表的,如果是链表有环呢?
分析:如果两个有环单链表相交,那么它们一定共有一个环。因此可以先用之前快慢指针的方式找到两个链表中位于环内的两个节点,如果相交的话,两个节点在一个环内,那么移动其中一个节点,在一次循环内肯定可以与另外一个节点相遇。
代码如下:
// 判断两个有环单链表是否相交
public boolean isisIntersectWithLoop(ListNode headA, ListNode headB) { if (null == headA || null == headB) { return false; } if (headA == headB) { return true; } headA = hasCycle(headA); headB = hasCycle(headB); // 没有环,则退出 if (null == headA || headB) { return false; } ListNode p = headB.next; // p 在环内循环一次,直到与 headA 相遇 while (p != headB) { if (p == headA) { return true; } p = p.next; } return false; } // 判断单链表是否有环,并返回环内的某一节点 public ListNode hasCycle(ListNode head) { if (null == head) { return null; } ListNode slow = head; ListNode fast = head; while (null != fast.next && null != fast.next.next) { fast = fast.next.next; slow = slow.next; if (fast == slow) { return slow; } } return null; }
9. 两个链表相交扩展:求两个无环单链表的第一个相交点
LeetCode 160. Intersection of Two Linked Lists
题目描述:找到两个无环单链表第一个相交点,如果不相交返回空,要求在线性时间复杂度和常量空间复杂度内完成。
分析:
下面所说的对齐:表示指针到链表末尾的距离相同。
-
分为先判断是否有环,再求第一个相交点的方式。分别遍历 A 链表和 B 链表,判断它们的最后一个节点是否相交。然后利用对齐的思想,计算两个链表的长度(这个可以放在之前的遍历中做),分别用 p1 和 p2 指向两个链表的头,然后将较长链表的 p1 (假设为 p1)向后移动
LB - LA
个节点。这样 p1 和 p2 对齐了,然后同时向后移动 p1 和 p2,直到p1 == p2
,相遇的点就是第一个节点。 -
解法 1 中为了对齐需要计算链表的长度,有没有什么方法可以不用计算链表长度呢?假设 A 链表和 B 链表的长度为 LA 和 LB,假设 LB >= LA,两个指针 p1 和 p2 分别指向 A 链表和 B 链表的头节点。同时向后移动,当 p1 移动 A 链表的末尾时,p2 距离 B 链表的末尾的距离为
LB - LA
,此时可以看出我们已经得到了长度差,如何利用这个长度差对齐呢。这时将 p1 移动到 B 链表的头部,两个指针继续移动,当 p2 移动到 B 链表的末尾时,p1 刚好移动了LB - LA
步。此时再将 p2 移动到 A 链表的头部,这样 p1 和 p2 就对齐了,然后继续移动,直到p1 == p2
。如果两个链表不相交,p1 和 p2 移动会同时移动到末尾都指向空,而相交的话,第一次相等时就是第一个相交点。这种方法的时间复杂度为 O (2 * (Length(B))),最多要遍历两次长度较长的链表。
�解法 2 的代码如下:
// 求两个无环单链表的第一个相交点
public ListNode getIntersectionNode(ListNode headA, ListNode headB) { if (null == headA || null == headB) { return null; } if (headA == headB) { return headA; } ListNode p1 = headA; ListNode p2 = headB; while (p1 != p2) { // 遍历完所在链表后从另外一个链表再开始 // 当 p1 和 p2 都换到另一个链表时,它们对齐了: // (1)如果链表相交,p1 == p2 时为第一个相交点 // (2)如果链表不相交,p1 和 p2 同时移动到末尾,p1 = p2 = null,然后退出循环 p1 = (null == p1) ? headB : p1.next; p2 = (null == p2) ? headA : p2.next; } return p1; }
10. 总结
回过头来,会发现上面的链表问题主要用到了「狸猫换太子」、「对齐」以及「两个指针」的方式来提高效率。其中利用两个指针来提供效率的方式经常用到,在遇到链表问题时可以多考虑下这种思路。推荐大家记住这几种典型的链表问题,以后很多类似的题目都可以转换到熟悉的问题再解决。
https://www.jianshu.com/p/a2d53142860c?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation