吊打链表系列

PS:由于笔者近期工作较忙,所以更新进度较慢,但是请大家放心,我一定抽空更新,喜欢的还请点个赞、点个关注~

本文默认读者具有基础的链表知识,所以对基础知识部分一笔带过,本文的主要目的是为了以练促学,通过在做题过程中不断巩固知识点。


1 链表简介

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。

在这里插入图片描述


2 常用技巧

2.1 虚拟头指针

我们有时候需要对链表元素进行增删,而链表的增删操作是通过前驱节点完成的,当我们需要对头节点进行操作时,由于头节点没有前驱,所以需要额外增加处理逻辑。为了统一处理逻辑,我们在头节点之前增加一个虚拟头指针来指向第一个节点,这样,我们处理时便可通过头节点的前驱——虚拟头指针,来完成对头简单的操作。

2.2 穿针引线

2.3 快慢指针

2.4 切割链表


3 以练促学

3.1 反转链表系列

3.1.1 反转链表

题目链接: 206. 反转链表

  • 思路:三指针—— curr, prev, next,其中 curr 一直在链表上滑动,在滑动过程中我们改变 curr 的指向,使其指向从后一个节点 (curr.next) 变为前一个节点 prev。注意在改变指向前要先保留相关节点信息,如 next。
  • 代码:
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null)
            return null;

        ListNode curr = head, prev = null, next = null;

        while (curr != null) {
            next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }

        return prev;
    }
}
  • 复杂度:时间复杂度 O(N) 空间复杂度 O(1)

3.1.2 反转链表2

题目链接:92. 反转链表 II
与206题相比,本题是在某一个区间段内进行反转,其实 206 题可以看作本题的特例——在 [0, end-1] 范围内反转。

  • 思路1:反转指定区间,然后再将反转后的区间与原链表未反转部分进行链接。注意我们都是使用前一个节点来完成链表连接的。
  • 代码1
class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        if (head == null || left == right)
            return head;

        if (left > right)
            return null;
        
        // 思路:
        // 1. 找到 left 的前一个节点 beforeReverse,反转后的最后一个节点(即反转前的第一个节点) lastAfterReverse
        // 2. 然后对 [left,right] 范围内反转
        // 3. beforeReverse 连接反转后的头,lastAfterReverse 连接后半部分未反转的头(next 指针)

        ListNode dummyHead = new ListNode();
        dummyHead.next = head;

        // 1. 先走 left-1 步,使得 curr 到达 left
        ListNode curr = head, prev = dummyHead;
        for (int i = 1; i < left; i++) {
            prev = prev.next;
            curr = curr.next;
        }
		// beforeReverse 为反转区间的前驱
		// lastAfterReverse 为反转后的区间最后一个元素,也即反转前的区间第一个元素
		// next 指针,用于记录当前元素的下一个
		
        ListNode beforeReverse = prev, lastAfterReverse = curr, next = null;
        prev = null;
        for (int i = left; i <= right; i++) {
            next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
		// 将反转后的区间与原链表未反转部分相连接
        beforeReverse.next = prev;
        lastAfterReverse.next = next;

        return dummyHead.next;
    }
}
  • 思路2: 头插法:定义两个指针 guardian 和 curr ,首先移动 left - 1 步,将 guardian 和 curr 分别移动到第一个要反转的节点前面和第一个要反转的节点上。然后依次删除 curr 后的节点,并将该删除的节点添加到 guardian 和 guardian.next 之间,当删除 right - left 次之后,即完成了指定区间的反转。
    在这里插入图片描述
  • 代码2
class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        if (head == null || left == right)
            return head;

        if (left > right)
            return null;

        // 思路:头插法

        ListNode dummyHead = new ListNode();
        dummyHead.next = head;
        ListNode guardian = dummyHead, curr = head;
        // 循环结束,guardian 来到要反转区域的前驱,curr 为待反转区域的第一个
        for (int i = 1; i < left; i++) {
            guardian = guardian.next;
            curr = curr.next;
        }
        
        // 我们将反转区域除第一个节点外的每个节点都删除,并添加到 guardian 和 guardian.next 之间,即可完成反转
        for (int i = left; i < right; i++) {
            ListNode removed = curr.next;
            curr.next = removed.next; // 断开 removed 节点
            removed.next = guardian.next;
            guardian.next = removed;
        }
        
        return dummyHead.next;
    }
}
  • 思路3:curr 走到待反转的第一个,然后反转 (right - left + 1) 个节点,与普通的反转链表几乎一摸一样,注意亮点:1、反转的次数;2、反转后,要与后面的链表连接起来。

  • 代码3:

    class Solution {
        public ListNode reverseBetween(ListNode head, int left, int right) {
            ListNode dummyHead = new ListNode(0, head);
            ListNode prev = dummyHead, curr = head;
            for (int i = 0; i < left - 1; i++) {
                prev.next = curr;
                prev = prev.next;
                curr = curr.next;
            }
    
            prev.next = reverse(curr, right - left + 1);
    
            return dummyHead.next;
        }
    
        // 从 head 节点开始,反转 k 个节点
        private ListNode reverse(ListNode head, int k) {
            ListNode prev = null, curr = head, next = null;
            ListNode last = head; // 反转后的最后一个节点
            while (k-- > 0) {
                next = curr.next;
                curr.next = prev;
                prev = curr;
                curr = next;
            }
    
            // 与后面的链表链接起来
            last.next = curr;
            return prev;
        }
    }
    

3.1.3 两两交换链表中的节点

题目地址:24. 两两交换链表中的节点

  • 思路1:递归。默认后面的节点都是已经交换好的,考虑两个节点之间的交换,然后将新的第二个节点与后面交换好的节点进行连接。
  • 代码1:
class Solution {
    public ListNode swapPairs(ListNode head) {
        // 如果为空或者只有一个节点,直接返回
        if (head == null || head.next == null) {
            return head;
        }
        // head 为当前节点,next 为下一个待交换的节点,nextStart 为后面节点的开始
        ListNode next = head.next, nextStart = head.next.next;

        next.next = head;
        // 递归返回后面反转后的头节点
        head.next = swapPairs(nextStart);

        return next;
    }
}
  • 思路2:K 个一组翻转链表的特殊版本——两个一组翻转
class Solution {
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) return head;
        int cnt = 0;
        ListNode dummyHead = new ListNode(-1, head);
        ListNode ll = dummyHead, rr = head;
        while (rr != null) {
            cnt++;
            rr = rr.next;
            if (cnt % 2 == 0) {
                ll = reverseTwo(ll, rr);
            }
        }

        return dummyHead.next;
    }

    private ListNode reverseTwo(ListNode ll, ListNode rr) {
        ListNode prev = null, curr = ll.next, next = null;
        ListNode last = curr; // 反转后的最后一个节点
        while (curr != rr) {
            next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }

        ll.next = prev;
        last.next = rr;

        return last;
    }
}

3.1.4 K 个一组翻转链表

题目链接:25. K 个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

进阶:

    你可以设计一个只使用常数额外空间的算法来解决此问题吗?
    你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

 

示例 1:

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

在这里插入图片描述

示例 2:

输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]

在这里插入图片描述

示例 3:

输入:head = [1,2,3,4,5], k = 1
输出:[1,2,3,4,5]

示例 4:

输入:head = [1], k = 1
输出:[1]

提示:

    列表中节点的数量在范围 sz 内
    1 <= sz <= 5000
    0 <= Node.val <= 1000
    1 <= k <= sz

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reverse-nodes-in-k-group
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
  • 思路:由于进阶要求空间复杂度为 O(1),所以递归的方法就不行了。我们可以考虑利用“哨兵”节点进行 K 个一组的反转:reverse 函数的作用是反转链表中以 left.next 为起始节点的连续 K 个链表节点,并返回反转后的最后一个节点(作为下一段待反转链表段的左哨兵)。其中 left 为待反转的 K 个节点的前一个节点,right 为待反转的 K 个节点的后一个节点。

  • 代码:

    class Solution {
        public ListNode reverseKGroup(ListNode head, int k) {
            if (head == null || head.next == null || k < 2)
                return head;
            
            ListNode dummy = new ListNode();
            dummy.next = head;
            ListNode left = dummy, right = head;
            int cnt = 0;
            while (right != null) {
                cnt++;
                right = right.next;
                if (cnt % k == 0) {
                    left = reverse(left, right);
                }
            }
            return dummy.next;
    
        }
    
        // left 为待反转段的前继节点,right 为待反转段的后继节点
        // 反转 [left + 1, right-1] 范围内的节点,并返最后一个节点
        private ListNode reverse(ListNode left, ListNode right) {
            ListNode curr = left.next, prev = null, next = null;
            ListNode beforeFirst = left, last = curr;// beforeFirst 为待反转的前一个节点,last 为(反转前的第一个)反转后的最后一个
    
            while (curr != right) {
                next = curr.next;
                curr.next = prev;
                prev = curr;
                curr = next;
            }
            // 反转后,前后都重新链接起来
            beforeFirst.next = prev;
            last.next = curr;
            
            return last;
        }
    }
    
  • 时间复杂度:每个节点最多访问两次(查看是否满足 K 个一组的时候遍历了一次,反转的时候遍历了一次),所以整体时间复杂度为 O(N)

  • 空间复杂度:O(1)


3.2 合并链表系列

3.2.3 合并K个升序链表

题目链接:合并K个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

 

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

示例 2:

输入:lists = []
输出:[]

示例 3:

输入:lists = [[]]
输出:[]

 

提示:

    k == lists.length
    0 <= k <= 10^4
    0 <= lists[i].length <= 500
    -10^4 <= lists[i][j] <= 10^4
    lists[i] 按 升序 排列
    lists[i].length 的总和不超过 10^4

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/merge-k-sorted-lists
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路1:优先级队列
  • 优先级队列(小根堆)—— 构造一个小根堆,堆顶为值最小的节点;然后将所有链表的非空头节点入堆,每次取堆顶的节点添加到最终返回的链表当中,然后将该堆顶节点的下一个非空节点继续入堆。依次类推,直至堆为空。整体示意图如下图,蓝色的代表堆,堆中存放的是没个链表当前遍历到的节点。
    在这里插入图片描述

  • 代码:

    class Solution {
        public ListNode mergeKLists(ListNode[] lists) {
            if (lists == null || lists.length == 0) {
                return null;
            }
    
            ListNode dummyHead = new ListNode(), prev = dummyHead;
            PriorityQueue<ListNode> pq = new PriorityQueue<>((o1, o2) -> o1.val - o2.val);
            for (ListNode node : lists) {
                if (node != null) {
                    pq.offer(node);
                }
            }
    
            while (!pq.isEmpty()) {
                ListNode curr = pq.poll();
                prev.next = curr;
                prev = prev.next;
                if (curr.next != null) {
                    pq.offer(curr.next);
                }
            }
    
            return dummyHead.next;
        }
    }
    
  • 时间复杂度:假设有 K 个链表,每个链表有 N 个节点。建立小根堆的时间复杂度为 O(KlogK),合并链表的过程,每一个节点都会遍历到,对应的时间复杂度为 O(KN),且遍历的时候涉及出入堆的操作,出入堆的时间复杂度为 O(logK),所以整体的时间复杂度为 O(KNlogK)

  • 空间复杂度:O(K)


思路2: 分治

其实类似于归并排序,整体思路如下图所示:
在这里插入图片描述

  • 代码:
    class Solution {
        public ListNode mergeKLists(ListNode[] lists) {
            // 分治
            if (lists == null || lists.length == 0) {
                return null;
            }
            int n = lists.length;
    
            return mergeKLists(lists, 0, n - 1);
        }
    
        private ListNode mergeKLists(ListNode[] lists, int left, int right) {
            if (left < right) {
                int mid = left + ((right - left) >> 1);
                ListNode l = mergeKLists(lists, left, mid);
                ListNode r = mergeKLists(lists, mid + 1, right);
                return merge2Lists(l, r);
            } else if (left == right) {
                return lists[left];
            } else {
                return null;
            }
        }
    
        private ListNode merge2Lists(ListNode l1, ListNode l2) {
            if (l1 == null) return l2;
            if (l2 == null) return l1;
            ListNode dummyHead = new ListNode(), prev = dummyHead;
            while (l1 != null && l2 != null) {
                if (l1.val <= l2.val) {
                    prev.next = l1;
                    l1 = l1.next;
                } else {
                    prev.next = l2;
                    l2 = l2.next;
                }
                prev = prev.next;
            }
            if (l1 != null) prev.next = l1;
            if (l2 != null) prev.next = l2;
    
            return dummyHead.next;
        }
    }
    
  • 时间复杂度:

3.3 相交/成环系列

3.4 链表排序系列

3.4.1 分隔链表

题目链接:86. 分隔链表

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

你应当 保留 两个分区中每个节点的初始相对位置。

 

示例 1:


输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]
示例 2:

输入:head = [2,1], x = 2
输出:[1,2]
 

提示:

链表中节点的数目在范围 [0, 200] 内
-100 <= Node.val <= 100
-200 <= x <= 200


来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/partition-list
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
  • 思路:有点像合并链表的“逆操作”——一个链表拆成两个,小于 x 的链表和大于等于 x 的链表,然后再将大的连在小的后面。注意大小链表均需要虚拟头指针。

  • 代码

    class Solution {
        public ListNode partition(ListNode head, int x) {
            ListNode dummyLess = new ListNode();
            ListNode dummyMore = new ListNode();
            ListNode less = dummyLess, more = dummyMore;
            ListNode curr = head;
            while (curr != null) {
                if (curr.val < x) {
                    less.next = curr;
                    less = less.next;
                } else {
                    more.next = curr;
                    more = more.next;
                }
                curr = curr.next;
            }
            // 注意要断开链表,如例子中的 有 5 后面还跟着 2
            more.next = null;
            less.next = dummyMore.next;
            return dummyLess.next;
        }
    }
    

3.4.2 重排链表

题目链接: 143. 重排链表

给定一个单链表 L 的头节点 head ,单链表 L 表示为:

L0 → L1 → … → Ln - 1 → Ln
请将其重新排列后变为:

L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

 

示例 1:



输入:head = [1,2,3,4]
输出:[1,4,2,3]
示例 2:



输入:head = [1,2,3,4,5]
输出:[1,5,2,4,3]


来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reorder-list
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
  • 思路:1、从中间断开链表;2、反转右半段;3、将右半段间隔插入左半段

  • 代码

    class Solution {
        public void reorderList(ListNode head) {
            // 1. 找到中点,并断开链表
            ListNode mid = getMid(head);
            ListNode left = head, right = mid.next;
            mid.next = null; // 断开链表
    
            // 2. 反转右半段
            right = reverse(right);
    
            // 3. 将反转的右半段间隔插入左半段
            merge(left, right);
        }
    
        private ListNode getMid(ListNode head) {
            if (head == null || head.next == null) return head;
    
            ListNode slow = head, fast = head;
            while (fast != null && fast.next != null) {
                fast = fast.next.next;
                if (fast == null) break;
                slow = slow.next;
            }
            return slow;
        }
    
        private ListNode reverse(ListNode head) {
            if (head == null || head.next == null) return head;
            ListNode prev = null, curr = head, next = null;
            while (curr != null) {
                next = curr.next;
                curr.next = prev;
                prev = curr;
                curr = next;
            }
            return prev;
        }
    
        private void merge(ListNode left, ListNode right) {
            if (left == null || right == null) {
                return;
            }
    
            while (right != null) {
                ListNode nextRight = right.next;
                // 将 right 插入 left
                right.next = left.next;
                left.next = right;
                // 更新 left 和 right 指针
                left = right.next;
                right = nextRight;
            }
        }
    }
    

3.4.3 排序链表

题目链接:148. 排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
示例 1:
在这里插入图片描述
输入:head = [4,2,1,3]
输出:[1,2,3,4]

示例 2:
在这里插入图片描述
输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]

示例 3:
输入:head = []
输出:[]

提示:
链表中节点的数目在范围 [0, 5 * 104] 内
-105 <= Node.val <= 105

进阶:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sort-list
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  1. 思路:归并排序,自顶向下(递归实现)的时间复杂度为 O(NlogN) ,满足要求,但是空间复杂度为 O(logN) 不满足要求。所以我们需要用自底向上的归并排序。
  2. 代码
    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode() {}
     *     ListNode(int val) { this.val = val; }
     *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
     * }
     */
    class Solution {
        public ListNode sortList(ListNode head) {
            if (head == null) {
                return null;
            }
    
            ListNode curr = head;
            int length = 0;
            while (curr != null) {
                curr = curr.next;
                length++;
            }
    
            ListNode dummyHead = new ListNode();
            dummyHead.next = head;
    
            for (int lenSub = 1; lenSub < length; lenSub <<= 1) {
                ListNode prev = dummyHead;
                curr = prev.next;
    
                while (curr != null) {
                    // 找到第一段要合并的链表并与第二段要合并的链表断开
                    ListNode head1 = curr; // 第一段的链表头
                    // curr 走到第一段要合并链表的尾部
                    for (int i = 1; i < lenSub && curr != null && curr.next != null; i++) {
                        curr = curr.next;
                    }
    
                    ListNode head2 = curr.next;
                    // 断开第一条链表
                    curr.next = null;
    
                    // 找到第二段要合并的链表并与后面下一次合并的链表断开
                    curr = head2;
                    for (int i = 1; i < lenSub && curr != null && curr.next != null; i++) {
                        curr = curr.next;
                    }
                    ListNode nextStart = null;
                    if (curr != null) { // 如果第二条链表的结尾不是空,记录下一次进行合并的头部
                        nextStart = curr.next;
                        curr.next = null; // 断开第二段
                    }
    
                    // 合并两段
                    ListNode merged = merge(head1, head2);
    
                    prev.next = merged;
                    while (prev.next != null) { // 走到已合并链表的最后一个节点,准备连接后面的链表
                        prev = prev.next;
                    }
    
                    curr = nextStart; // 继续本轮(长度为 lenSub)后面链表的的排序
                }
    
            }
    
            return dummyHead.next;
        }
    
        private ListNode merge(ListNode l1, ListNode l2) {
            ListNode dummyHead = new ListNode(), prev = dummyHead;
            while (l1 != null && l2 != null) {
                if (l1.val <= l2.val) {
                    prev.next = l1;
                    l1 = l1.next;
                } else {
                    prev.next = l2;
                    l2 = l2.next;
                }
                prev = prev.next;
            }
    
            if (l1 != null) prev.next = l1;
            if (l2 != null) prev.next = l2;
            return dummyHead.next;
        }
    }
    
    
  3. 复杂度
  • 时间复杂度: O(NlogN)
  • 空间复杂度: O(1)

3.5 删除节点系列

3.6 设计系列

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值