步步为营系列链表篇

230 阅读3分钟

相关基操

查找链表的的中间节点1

[1,2,3,4,5] -> 3 [1,2,3,4,5,6] -> 3

class Solution {
    public ListNode findMiddleListNode(ListNode head) {
        ListNode slow = head,fast = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }
}

查找链表的的中间节点2

[1,2,3,4,5] -> 3 [1,2,3,4,5,6] -> 4

class Solution {
    public ListNode findMiddleListNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }
}

打印链表工具类

public class ListNodeUtil {
    public static void printlnListNode(ListNode head) {
        printlnListNode(head,"");
    }

    public static void printlnListNode(ListNode head, String howTime) {
        if(null != head) {
            System.out.print(howTime + "[");
        }else {
            System.out.print(howTime + "null\n");
            return;
        }
        System.out.print(head.val);
        ListNode next = head.next;
        while (null != next) {
            System.out.print(",");
            System.out.print(next.val);
            next = next.next;
        }
        System.out.print("]\n");
    }
}

头插法和尾插法

public class ListSimple {
    public static void main(String[] args) {
        int[] nums = new int[]{1,2,3,4,5,6};
        // 尾插法构建链表
        ListNode head = null;
        ListNode lastNode = null;
        for (int num : nums) {
            ListNode listNode = new ListNode(num);
            if(head == null) {
                head = listNode;
            }else {
                lastNode.next = listNode;
            }
            lastNode = listNode;
        }
        // 头插法反转链表
        ListNode newHead = null;
        while (head != null) {
            ListNode next = head.next;
            head.next = newHead;
            newHead = head;
            head = next;
        }
    }
}

基操实战

002. 两数相加

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        int carryNum = 0;
        ListNode head = null;
        ListNode tempNode = null;
        while(l1 != null || l2 != null || carryNum != 0) {
            int val1 = l1 == null ? 0 : l1.val;
            int val2 = l2 == null ? 0 : l2.val;
            // 判断相加后的数,取个数位,进位数存起来
            int value = (val1 + val2 + carryNum) % 10;
            carryNum = (val1 + val2 + carryNum) / 10;
            // 尾插法
            ListNode listNode = new ListNode(value);
            if(tempNode == null) {
                head = listNode;
            }else {
                tempNode.next = listNode;
            }
            tempNode = listNode;
            if(l1 != null) l1 = l1.next;
            if(l2 != null) l2 = l2.next;
        }
        return head;
    }
}

061. 旋转链表

本质就是向后倒数K长度链表拼接剩下的前半段,比较 k 和 链表的余数,连接原本头尾之后,遍历到 k - 1 个结点断开首尾即可。

class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        if(k == 0 || head == null || head.next == null) return head;
        ListNode listNode = head;
        int length = 1;
        // 遍历获取链表长度
        while(listNode.next != null) {
            length++;
            listNode = listNode.next;
        }
        // 计算位移偏移量
        k = (k > length) ? k % length : k;
        if(k == 0) return head;
        // 构建环链表
        listNode.next = head;
        // 移动到倒数k+1位置
        for(int i = 0; i < length - k; i++) {
            listNode = listNode.next;
        } 
        ListNode newHead = listNode.next;
        listNode.next = null;
        return newHead;
    }
}

083. 删除排序链表中的重复元素

非递归版本尾插法,单结点记录不重复的结点。

class Solution {
    public static ListNode deleteDuplicates(ListNode head) {
        if(head == null) return null;
        ListNode temp = head;
        while(temp.next != null) {
            if(temp.next.val == temp.val) {
                temp.next = temp.next.next;
            }else {
                temp = temp.next;
            }
        }
        return head;
    }
}

086. 分隔链表

小于 x 和大于等于 x 分别用尾插法构建链表,最后连起来。

class Solution {
    public ListNode partition(ListNode head, int x) {
        if(head == null || head.next == null) return head;
        ListNode head1 = null;
        ListNode last1 = null;
        ListNode head2 = null;
        ListNode last2 = null;
        ListNode newHead = head;
        while (newHead != null) {
            ListNode newHeadNext = newHead.next;
            if(newHead.val < x) {
                if(head1 == null) head1 = newHead;
                else last1.next = newHead;
                newHead.next = null;
                last1 = newHead;
            }else {
                if(head2 == null) head2 = newHead;
                else last2.next = newHead;
                newHead.next = null;
                last2 = newHead;
            }
            newHead = newHeadNext;
        }
        if(last1 != null) last1.next = head2;
        else return head2;
        return head1;
    }
}

092. 反转链表 II

迭代反转比递归反转的好处就是消耗内存比较小。

class Solution {
    public ListNode reverseBetween(ListNode head, int m, int n) {
        // 虚拟头结点防止越界
        ListNode newHead = new ListNode(-1);
        newHead.next = head;
        ListNode prevNode = newHead;
        // prevNode 移动到要反转的前一个结点
        for (int i = 1; i < m; i++) { // 链表位置从 1 开始
           prevNode = prevNode.next;
        }
        // 开始反转的结点
        head = prevNode.next;
        // 迭代反转链表
        for (int i = m; i < n; i++) {
            ListNode next = head.next;
            head.next = next.next;
            next.next = prevNode.next;
            prevNode.next = next;
        }
        return newHead.next;
    }
}

143. 重排链表

找到中间结点,迭代反转后半部分,遍历拼接。

class Solution {
    public void reorderList(ListNode head) {
        if(head == null || head.next == null) return;
        ListNode slow = head,fast = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        // 找到要重排的头结点
        ListNode newHead = slow.next;
        // 迭代反转重排链表
        while (newHead.next != null) {
            ListNode next = newHead.next;
            newHead.next = next.next;
            next.next = slow.next;
            slow.next = next;
        }
        // 断开链表 防止死循环
        ListNode reverseHead = slow.next;
        slow.next = null;
        fast = head;
        // 遍历拼接
        while (fast != null && reverseHead != null) {
            ListNode next = fast.next;
            ListNode reverseNext = reverseHead.next;
            fast.next = reverseHead;
            reverseHead.next = next;
            fast = next;
            reverseHead = reverseNext;
        }
    }    
}

160. 相交链表

设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if(headA == null || headB == null) return null;
        ListNode l1 = headA,l2 = headB;
        while (l1 != l2) {
            l1 = (l1 == null) ? headB : l1.next;
            l2 = (l2 == null) ? headA : l2.next;
        }
        return l1;        
    }
}

递归专题

递归三部曲

  1. 找到终止条件
  2. 找到返回值
  3. 一级递归该做什么

021. 合并两个有序链表

终止条件为两个链表为空,返回头结点为最小的那个结点,一级递归需要对两个链表的值进行比较并合并,同时考虑后续结点的处理。

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1 == null) return list2;
        if(list2 == null) return list1;
        if(list1.val < list2.val) {
            list1.next = mergeTwoLists(list1.next,list2);
            return list1;
        }else {
            list2.next = mergeTwoLists(list1,list2.next);
            return list2;
        }
    }
}

024. 两两交换链表中的节点

终止条件为剩余结点为空或者为单数(不需要交换),返回值为最开始交换的第二个结点,一级递归需要交换相邻的两个结点,同时考虑处理后续结点(也就是结点 next 的指向)。

class Solution {
    public ListNode swapPairs(ListNode head) {
        if(head == null || head.next == null) return head;
        ListNode newHead = head.next;
        head.next = swapPairs(newHead.next);
        newHead.next = head;
        return newHead;
    }
}

082. 删除排序链表中的重复元素 II

while 循环来处理重复元素结点,if 判断主要用来判断当前结点是否重复。

class Solution {
    public ListNode deleteDuplicates2(ListNode head) {
        if(head == null || head.next == null) return head;
        ListNode tempNode = head.next;
        if(tempNode.val == head.val) {
            while (tempNode != null && tempNode.val == head.val) {
                tempNode = tempNode.next;
            }
            head = deleteDuplicates2(tempNode);
        }else {
            head.next = deleteDuplicates2(tempNode);
        }
        return head;
    }
}

083. 删除排序链表中的重复元素

递归版本,从后向前比较,不相同返回头结点。

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;
    }
}

206. 反转链表

递归返回最后的结点(也就是头结点),同时在递归的过程中反转链表(下一个结点指向当前结点,当前结点的下一个结点置为空)。

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;
    }
}

328. 奇偶链表

记录奇偶链表的头结点,尾插法形成奇偶链表,奇链表尾结点指向偶链表头结点。

class Solution {
    public ListNode oddEvenList(ListNode head) {
        if(head == null) return null;
        ListNode oddLast = head;
        ListNode evenHead = head.next;
        ListNode even = evenHead;
        while(even != null && even.next != null) {
            oddLast.next = oddLast.next.next;
            oddLast = oddLast.next;
            even.next = even.next.next;
            even = even.next;
        }
        oddLast.next = evenHead;
        return head;
    }
}

快慢指针专题

中间节点

2095. 删除链表的中间节点

变异快慢指针,快指针先出发一步,慢指针会在常规中间结点之前一步停下来。

class Solution {
    public ListNode deleteMiddle(ListNode head) {
        if(head == null || head.next == null) return null;
        ListNode slow = head,fast = head.next;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        slow.next = slow.next.next;
        return head;
    }
}

第 N 个结点

019. 删除链表的倒数第 N 个结点

双指针法解决,虚拟头结点防止删除头结点,快指针先出发 N+1 步(此时链表有虚拟头结点,长度+1),同时移动快慢指针,快指针为空时,慢指针刚好在要删除的节点之前。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        if(head == null) return null;
        ListNode newHead = new ListNode(-1);
        newHead.next = head;
        ListNode fast = newHead,slow = newHead;
        for (int i = 0; i <= n; i++) {
            fast = fast.next;
        }
        while(fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        slow.next = slow.next.next;
        return newHead.next;
    }
}

092. 反转链表 II

遍历到 m - 1 位置结点,对 m - n 位置的结点迭代反转。

class Solution {
    public ListNode reverseBetween2(ListNode head, int m, int n) {
        // 虚拟头结点防止越界
        ListNode newHead = new ListNode(-1);
        newHead.next = head;
        ListNode prevNode = newHead;
        // prevNode 移动到要反转的前一个结点
        for (int i = 1; i < m; i++) { // 链表位置从 1 开始
           prevNode = prevNode.next;
        }
        // 开始反转的结点
        head = prevNode.next;
        // 迭代反转链表
        for (int i = m; i < n; i++) {
            ListNode next = head.next;
            head.next = next.next;
            next.next = prevNode.next;
            prevNode.next = next;
        }
        return newHead.next;
    }
}

1669. 合并两个链表

找到 a - 1 和 b + 1 位置的结点,拼接 list2 即可。

class Solution {
    public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
        ListNode listNode = list1;
        ListNode prevNode = null;
        ListNode lastNode = null;
        for (int i = 0; i < b; i++) {
            if(i == a - 1) prevNode = listNode;
            listNode = listNode.next;
        }
        lastNode = listNode.next;
        if(prevNode != null) prevNode.next = list2;
        while (list2.next != null) {
            list2 = list2.next;
        }
        list2.next = lastNode;
        return list1;        
    }
}

1721. 交换链表中的节点

添加虚拟头结点(长度 + 1),快指针先出发 k 步,保存当前的结点(正数 k 位置),同时移动快慢指针,快指针为空时,慢指针刚好在倒数 k 位置,此时交换值即可。

class Solution {
    public ListNode swapNodes(ListNode head, int k) {
        if(head == null || head.next == null) return head;
        ListNode newHead = new ListNode(-1);
        newHead.next = head;
        ListNode slow = newHead,fast = newHead;
        int n = k;
        while (n > 0) {
            fast = fast.next;
            n--;
        }
        ListNode tempNode = fast;
        while (fast != null) {
            slow = slow.next;
            fast = fast.next;
        }
        if(tempNode != null) {
            int tempVal = slow.val;
            slow.val = tempNode.val;
            tempNode.val = tempVal;
        }
        return newHead.next;
    }    
}

环形链表

142. 环形链表 II

[寻找环链表入口点] 快慢指针数学原理剖析

环路检测及入口定位原理分析图

设 x 为头结点到环入口结点的距离,假设链表有环,快慢指针必定相遇。设 y 为环入口结点移动到相遇点的距离,设 z 为相遇点移动到环入口结点的距离,设 c 为环的周长,得:c = y + z。

假设快指针移动到相遇点经过的距离为 F ,F = x + y + m * c(m 为 快指针绕环的圈数),假设慢指针移动到相遇点的距离为 S ,S = x + y + n * c (n 为 慢指针绕环的圈数);在快指针步长为 2,慢指针步长为 1 的情况下,有 F = 2 * S。综上所述得到:2 * (x + y + n * c) = x + y + m * c,移项之后使得:x = (m - 2 * n - 1) * c + z。此时将快指针指向头结点,快指针和慢指针同时移动 (m - 2 * n - 1) * c 距离,快指针此时肯定在环中,则快指针在环中移动的距离为 (m - 2 * n - 1) * c - x,即为 z。慢指针开始移动时已经在环中,移动 (m - 2 * n - 1) * c 距离还是在原来的位置,同样为 z。此时快指针和慢指针又在相遇点相遇,可以这样看,快指针从环入口结点出发,慢指针从相遇点出发,快指针少移动了 x 距离,慢指针多移动了 z 距离,使得 x = z。

综上所述,快慢指针相遇点与头结点距离环入口结点长度相等。

class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head,slow = head,temp = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if(slow == fast) {
                fast = head;
                while(fast != slow) {
                    fast = fast.next;
                    slow = slow.next;
                }
                return slow;
            }
        }
        return null;
    } 
}