链表专项

173 阅读5分钟

反转链表

题目

image.png

版本1 正确

    public ListNode reverseList(ListNode head) {
        if (head == null) {
            return head;
        }

        // 翻转链表
        ListNode newHead = new ListNode(-1);
        newHead.next = head;
        // head永远指向翻转完链表的最后一个元素
        while (head.next != null) {
            ListNode next = head.next;
            head.next = next.next;
            next.next = newHead.next;
            newHead.next = next;
        }

        return newHead.next;
    }

删除链表的节点

题目

image.png

版本1 正确

    public ListNode deleteNode(ListNode head, int val) {
        if (head == null) {
            return head;
        }
        ListNode temp = head;

        if (temp.val == val) {
            return temp.next;
        }

        while (temp.next != null) {
            ListNode next = temp.next;
            if (next.val == val) {
                temp.next = next.next;
                next.next = null;
                break;
            }
            temp = temp.next;
        }

        return head;

    }

从尾到头打印链表

题目

image.png

版本1 正确

    public int[] reversePrint(ListNode head) {
        if (head == null) {
            return new int[0];
        }
        List<Integer> ans = new ArrayList<>();
        while (head != null) {
            ans.add(head.val);
            head = head.next;
        }

        Collections.reverse(ans);
        return ans.stream().mapToInt(Integer::intValue).toArray();

    }

正确的原因

(1) 先存到list中, 然后翻转, 然后转成数组输出

版本2 正确

    public int[] reversePrint(ListNode head) {
        if (head == null) {
            return new int[0];
        }
        List<Integer> ans = new ArrayList<>();
        while (head != null) {
            ans.add(head.val);
            head = head.next;
        }
        
        int [] arrayAns = new int[ans.size()];
        for (int i = ans.size() - 1; i >= 0; i --) {
            arrayAns[ans.size() - 1 - i] = ans.get(i);
        }
        
        return arrayAns;

    }

正确的原因

(1) 不采用现成的api, 自己实现翻转

合并两个排序的链表

题目

image.png

版本1 正确

    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        // 合并两个递增的链表
        // 不创建新的节点
        if (l1 == null) {
            return l2;
        }
        if (l2 == null) {
            return l1;
        }
        ListNode ans = l1.val > l2.val ? l2 : l1;
        // 将l1指向第一个元素小的链表
        if (l1.val > l2.val) {
            ListNode temp = l2;
            l2 = l1;
            l1 = temp;
        }

        while (l1 != null) {
            // l1走
            while (l1.next != null && l1.next.val < l2.val) {
                l1 = l1.next;
            }
            if (l1.next == null) {
                l1.next = l2;
                break;
            } else {
                // 交换l1和l2
                ListNode temp = l1.next;
                l1.next = l2;
                l1 = l2;
                l2 = temp;
            }
        }

        return ans;

    }

正确的原因

(1) l1指向的元素永远比l2的小, 然后l1运动, 遇见比l1.next比l2.val大的时候, 就把l2链接到l1后面, 然后l1变成l2, l2变成l1.next, 此时依旧是l1指向的元素永远比l2的小

  • 例如
  • 初始状态
  • l1 : 1 -> 2 -> 3 -> 8 -> null
  • l2 : 4 -> 5 -> 6 -> null
  • 第一次结束后:
  • l1指向3, l2指向4, 交换链接后
  • l1 : 1 -> 2 -> 3-> 4 -> 5-> 6 -> null, 此时l1指向3
  • l2 : 8 -> null 此时l2指向8

最后一定是l1.next == null的情况, 可以跳出循环

链表中倒数第K个节点

题目

image.png

版本1 正确

    public ListNode getKthFromEnd(ListNode head, int k) {

        // 倒数第K个节点
        // 利用快慢指针
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null) {
            fast = fast.next;
            k --;
            if (k < 0) {
                slow = slow.next;
            }
        }

        return slow;
    }
    

正确的原因

(1) 利用快慢指针即可, 注意慢指针啥时候开始走

删除链表中倒数第n个节点

题目

image.png

版本1 正确

    public ListNode removeNthFromEnd(ListNode head, int n) {

        // 删除链表的倒数第n个节点
        // 利用快慢指针, 慢指针比快指针慢n步触发

        ListNode fast = head;
        ListNode slow = head;
        ListNode preNode = null;
        while (fast != null) {
            fast = fast.next;
            n --;
            if (n < 0) {
                preNode = slow;
                slow = slow.next;
            }
        }
        if (preNode == null) {
            // 就表示要删除第一个节点
            return head.next;
        }

        // 断开preNode和slow之间的连接即可
        preNode.next = slow.next;
        slow.next = null;

        return head;
    }

正确的原因

(1) 题目限制了, n一定是小于链表节点的总数目的, 因此当preNode == null的时候, 就表示要删除的其实是头节点.

两个链表的第一个公共节点

题目

image.png

版本1 正确

    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 先获得两个链表的长度, 然后长链表先走几步
        // 然后两个链表一起走, 相遇的节点就是第一个公共节点
        ListNode tempA = headA;
        ListNode tempB = headB;
        int lenA = 0;
        while (tempA != null) {
            lenA ++;
            tempA = tempA.next;
        }

        int lenB = 0;
        while (tempB != null) {
            lenB ++;
            tempB = tempB.next;
        }

        if (lenA > lenB) {
            // headA先走几步
            for (int i = 0; i < lenA - lenB; i++) {
                headA = headA.next;
            }
        }
        if (lenB > lenA) {
            // headB先走几步
            for (int i = 0; i < lenB - lenA; i++) {
                headB = headB.next;
            }
        }

        // 两个一起走
        while (headA != headB) {
            headA = headA.next;
            headB = headB.next;
        }
        return headA;

    }

正确的原因

(1) 长链表先走几步, 然后同时走, 遇见的相同的节点就是公共节点

环形链表

题目

image.png

版本1 正确

    public boolean hasCycle(ListNode head) {
        if (head == null) {
            return Boolean.FALSE;
        }

        // 判断链表是否是环形
        // 利用快慢指针
        ListNode fast = head;
        ListNode slow = head;

        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) {
                break;
            }
        }

        if (fast.next == null || fast.next.next == null) {
            return Boolean.FALSE;
        }

        return Boolean.TRUE;

    }

正确的原因

(1) 快慢指针, 判断指针是否相遇即可

环形链表II

题目

image.png

版本1 正确

    public ListNode detectCycle(ListNode head) {
        if (head == null) {
            return null;
        }


        // 环形链表
        // 首先判断是否有环形, 如果没有返回null
        // 如果有, 返回构成环形的节点

        // 利用快慢指针 判断链表是否是环形的
        ListNode fast = head;
        ListNode slow = head;

        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) {
                // 如果快慢指针相遇了, 表示构成了环形
                break;
            }
        }

        // 千万注意 这里不能用slow != fast来判断, 因为假设链表就一个元素[1], 那么此时slow和fast是相等的
        if (fast.next == null || fast.next.next == null) {
            // 表示上面的循环是因为fast到头而结束的, 没有构成环形
            return null;
        }

        // 寻找环形的节点
        // 令fast此时从头开始走
        fast = head;
        // 同时走, 相遇的节点就是环形的节点
        while (fast != slow) {
            fast = fast.next;
            slow = slow.next;
        }

        return slow;

    }

正确的原因

(1) 注意判断快慢指针相遇后的条件, 不能用fast != slow, 一定要用fast.next == null || fast.next.next == null

回文链表

题目

image.png

版本1 正确

    public boolean isPalindrome(ListNode head) {
        if (head.next == null) {
            return Boolean.TRUE;
        }


        // 寻找到链表的中间节点
        // 然后将后半部分翻转, 再比较前半部分和后半部分对应的值
        ListNode fast = head;
        ListNode slow = head;
        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        if (fast.next == null) {
            // 奇数个节点
            slow.next = reverseList(slow.next);
        } else {
            // 偶数个节点
            slow.next = reverseList(slow.next);
        }

        fast = slow.next;
        slow = head;
        while (fast != null && slow != null) {
            if (fast.val != slow.val) {
                return Boolean.FALSE;
            }
            fast = fast.next;
            slow = slow.next;
        }
        return Boolean.TRUE;
    }

    public ListNode reverseList(ListNode head) {
        if (head == null) {
            return head;
        }

        // 翻转链表
        ListNode newHead = new ListNode(-1);
        newHead.next = head;
        // head永远指向翻转完链表的最后一个元素
        while (head.next != null) {
            ListNode next = head.next;
            head.next = next.next;
            next.next = newHead.next;
            newHead.next = next;
        }

        return newHead.next;
    }

正确的原因

(1) 寻找构成回文的链表的两部分, 将后半部分翻转, 就可以同时比较前半部分和后半部分的值了

(2) 后半部分的链表, 必须经过反转后再链接

复杂链表的复制

题目

image.png

版本1 正确

    public Node copyRandomList(Node head) {
        if (head == null) {
            return head;
        }

        // 用一个map, key为原始节点, value为新创建的节点
        Map<Node, Node> map = new HashMap<>();
        Node cur = head;
        while (cur != null) {
            Node newNode = new Node(cur.val);
            map.put(cur, newNode);
            cur = cur.next;
        }

        cur = head;
        // 赋予链接关系
        while (cur != null) {
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random);
            cur = cur.next;
        }

        return map.get(head);


    }

正确的原因

(1) 先创建完所有的节点, 再完成链接关系的赋值

旋转链表

题目

image.png

版本1 正确

    public ListNode rotateRight(ListNode head, int k) {
        if (head == null) {
            return head;
        }

        // 就是寻找到倒数第k个节点, 然后将尾巴节点链接到头部
        // 新的头指针就是倒数第k个节点
        // 例如 1 -> 2 -> 3 -> 4 -> 5 旋转2
        // 就是找到倒数第二个节点4, 然后把5链1, 4作为头节点
        // 变成4 -> 5 -> 1 -> 2 -> 3

        // 先求链表的长度
        int len = 0;
        ListNode cur = head;
        while (cur != null) {
            len ++;
            cur = cur.next;
        }
        // 旋转len长度等于没旋转
        k = k % len;
        if (k == 0) {
            return head;
        }

        // 旋转小于len次
        ListNode fast = head;
        ListNode slow = head;

        while (fast.next != null) {
            fast = fast.next;
            k --;
            if (k < 0) {
                slow = slow.next;
            }
        }

        fast.next = head;
        ListNode ans = slow.next;
        slow.next = null;
        return ans;
    }

正确的原因

(1) 问题转化成寻找倒数第k个节点, 注意旋转次数过多的情况

分割链表

题目

image.png

版本1 正确

    public ListNode[] splitListToParts(ListNode root, int k) {

        // 先计算出每个链表的大小应该是多少
        int len = 0;
        ListNode cur = root;
        while (cur != null) {
            len ++;
            cur = cur.next;
        }
        ListNode [] ans = new ListNode[k];
        int extra = len % k;
        int base = len / k;

        // cur指向每个分组的开始
        cur = root;
        for (int i = 0; i < ans.length; i ++) {
            if (cur == null) {
                ans[i] = null;
            } else {
                // temp指向每个分组的结尾
                ListNode temp = cur;
                for(int j = 1; j < base; j ++) {
                    temp = temp.next;
                }
                if (extra > 0) {
                    if (base != 0) {
                        temp = temp.next;
                    }
                    extra --;
                }

                ListNode result = cur;
                if (temp != null) {
                    // 修改cur
                    cur = temp.next;
                    temp.next = null;
                }

                // 赋予值
                ans[i] = result;
            }

        }
        return ans;
    }

正确的原因

(1) 先计算出每个分组应该有几个元素, 然后利用两个指针cur和temp指向分组的开头和结尾, 注意元素的个数和指针移动的对应关系, 边界比较麻烦.

分隔链表

题目

image.png

版本1 正确

    public ListNode partition(ListNode head, int x) {
        if (head == null || head.next == null) {
            return head;
        }
        int target = x;

        // cur作为临时指针, 用来遍历链表中的每个元素
        ListNode cur = head;
        // dif指向小于target节点的最后一个, dif.next >= target
        ListNode dif = null;
        if (cur.val < target) {
            dif = cur;
        }

        while (cur.next != null) {
            if (cur.next.val >= target) {
                cur = cur.next;
            } else {
                // 当cur.next小于target的时候
                if (dif == null) {
                    // dif还没有赋值, 表示遇见了第一个小于target的数字
                    dif = cur.next;
                    cur.next = cur.next.next;
                    dif.next = head;
                    head = dif;
                    // 这里cur不需要移动, 因为cur.next已经发生了变化
                } else {
                    if (cur == dif) {
                        // 表面此时cur经过的元素都是小于target的
                        cur = cur.next;
                        dif = dif.next;
                    } else {
                        // 将cur.next插入到dif之后
                        ListNode next = cur.next;
                        cur.next = next.next;
                        next.next = dif.next;
                        dif.next = next;
                        dif = next;
                    }

                }
            }
        }
        return head;
    }

正确的原因

(1) 利用一个指针将链表分割开来, 小于x的就插入到指针到后面, 否则就不移动

两两交换链表中的节点

题目

image.png

版本1 正确

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

        ListNode newHead = head.next;

        // 相邻两个节点翻转
        ListNode cur = head;
        ListNode pre = null;
        while (cur != null) {
            ListNode next = cur.next;
                        if (next == null) {
                break;
            }
            ListNode nextNext = next.next;
            if (pre == null) {
                cur.next = nextNext;
                next.next = cur;

                pre = cur;
                cur = nextNext;

            } else {
                pre.next = next;
                cur.next = nextNext;
                next.next = cur;
                pre = cur;
                cur = nextNext;

            }

        }

        return newHead;

    }
}

正确的原因

(1) 注意pre节点的赋值

对链表进行插入排序

题目

image.png

版本1 正确

    public  ListNode insertionSortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        // 链表排序
        // 采用两个指针分别指向有序链表的头部和尾部
        // 一个cur指针指向链表中的每个元素
        ListNode sortHead = head;
        ListNode sortTail = head;
        // head永远是sortTail的下一个元素, 维持这一点
        head = sortTail.next;
        while (head != null) {
            if (head.val <= sortHead.val) {
                // 将head对应的节点, 插入到有序链表的头部
                sortTail.next = head.next;
                head.next = sortHead;
                sortHead = head;

            } else if (head.val >= sortTail.val) {
                // 将head对应的节点, 插入到有序链表的尾部
                // head本身就是有序链表的下一个节点, 只需要转移sortTail指针即可
                sortTail = head;
            } else {
                // 将head对应的节点, 插入到有序链表的中间
                // 寻找head应该插入的位置
                ListNode cur = sortHead;
                while (cur.next != null && cur.next.val < head.val) {
                    cur = cur.next;
                }
                // 将head插入到cur之后
                ListNode temp = cur.next;
                cur.next = head;
                sortTail.next = head.next;
                head.next = temp;

            }

            head = sortTail.next;
        }

        return sortHead;
    }

正确的原因

(1) 明确两个指针指向有序链表的头部和尾部, 然后head永远指向的是有序链表尾部的下一个, 才能保证不出错.

链表排序

题目

image.png

版本1 快排 其实本质就是链表分区

    public ListNode sortList(ListNode head) {

        // 链表的快排, 其实就是链表分区的问题, 给出一个值, 将链表分成2部分
        // 然后对于另外两个部分, 再递归进行快排
        return quirkSort(head);

    }

    public ListNode quirkSort(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        // 选择head作为基准值
        int target = head.val;

        // cur作为临时指针, 用来遍历链表中的每个元素
        ListNode cur = head;
        // dif指向小于target节点的最后一个, dif.next >= target
        ListNode dif = null;
        if (cur.val < target) {
            dif = cur;
        }

        while (cur.next != null) {
            if (cur.next.val >= target) {
                cur = cur.next;
            } else {
                // 当cur.next小于target的时候
                if (dif == null) {
                    // dif还没有赋值, 表示遇见了第一个小于target的数字
                    dif = cur.next;
                    cur.next = cur.next.next;
                    dif.next = head;
                    head = dif;
                    // 这里cur不需要移动, 因为cur.next已经发生了变化
                } else {
                    if (cur == dif) {
                        // 表面此时cur经过的元素都是小于target的
                        cur = cur.next;
                        dif = dif.next;
                    } else {
                        // 将cur.next插入到dif之后
                        ListNode next = cur.next;
                        cur.next = next.next;
                        next.next = dif.next;
                        dif.next = next;
                        dif = next;
                    }
                }
            }
        }

        ListNode leftHead = null;
        ListNode rightHead = null;
        // dif如果存在, 那么dif.next一定存在
        ListNode base = dif == null ? head : dif.next;
        // dif可能为null, 也就是head是链表中的最小值
        if (dif == null) {
            // head就是base, 只需要判断head的右侧链表
            rightHead = quirkSort(base.next);
        } else {
            // 左侧链表
            dif.next = null;
            leftHead = quirkSort(head);

            // 右侧链表
            rightHead = quirkSort(base.next);

        }

        // 然后拼接一次
        if (leftHead == null) {
            base.next = rightHead;
            return base;
        }

        ListNode leftTail = leftHead;
        while (leftTail.next != null) {
            leftTail = leftTail.next;
        }
        leftTail.next = base;
        base.next = rightHead;
        return leftHead;
    }

正确的原因

(1) 每一轮快排, 其实就是链表分区的问题, 链表分区完成后, 需要根据不同的情况, 递归对左右链表进行处理.

(2) 递归处理的时候, 记得讨论dif存不存在的情况

版本2 归并排序

    public ListNode sortList(ListNode head) {
        // 拆分链表的终止条件
        if (head == null || head.next == null) {
            return head;
        }

        // 链表最适合的排序方法, 是归并排序
        // 将链表从中间节点分成两半, 最后再归并两个有序链表

        // 链表的拆分 采用快慢指针的方式
        ListNode fast = head;
        ListNode slow = head;
        while (fast.next != null && fast.next.next != null) {
            // 快指针走两步, 慢指针走一步
            fast = fast.next.next;
            slow = slow.next;
        }

        // slow指针的节点就是中间节点
        ListNode mid = slow.next;
        // 将原链表断成两个链表
        slow.next = null;
        ListNode leftHead = sortList(head);
        ListNode rightHead = sortList(mid);

        // 合并两个有序链表
        return mergeSortList(leftHead, rightHead);


    }

    public ListNode mergeSortList(ListNode l1, ListNode l2) {
        // 合并两个有序链表
        if (l1 == null) {
            return l2;
        }
        if (l2 == null) {
            return l1;
        }

        // 以l1为基准, 让l1指向头节点小的链表
        if (l1.val > l2.val) {
            ListNode temp = l2;
            l2 = l1;
            l1 = temp;
        }
        ListNode head = l1;

        // l1指针不断扫描链表中比l2指针小的元素
        // 当l1.next > l2的时候, 将l1的部分连接到l2上, 然后交换l1和l2
        while (l1 != null && l2 != null) {

            while (l1.next != null && l1.next.val <= l2.val) {
                l1 = l1.next;
            }
            // temp是大于l2的元素
            ListNode temp = l1.next;
            if (temp == null) {
                l1.next = l2;
                break;
            } else {
                l1.next = l2;
                l1 = l2;
                l2 = temp;
            }
        }

        return head;
    }

正确的原因

(1) 其实本质就是寻找链表的中间节点, 然后断开分成两部分链表, 将原链表不断拆分

(2) 合并的时候, 就是合并两个有序链表的解法

所以综上所述, 就是寻找链表中间节点 + 合并两个有序链表