力扣热题 100:链表专题经典题解析(后7道)

文章目录

    • 前言
    • [一、删除链表的倒数第 N 个节点(题目 19)](#一、删除链表的倒数第 N 个节点(题目 19))
      • [1. 题目描述](#1. 题目描述)
      • [2. 示例](#2. 示例)
      • [3. 解题思路](#3. 解题思路)
      • [4. 代码实现(Java)](#4. 代码实现(Java))
      • [5. 复杂度分析](#5. 复杂度分析)
    • [二、两两交换链表中的节点(题目 24)](#二、两两交换链表中的节点(题目 24))
      • [1. 题目描述](#1. 题目描述)
      • [2. 示例](#2. 示例)
      • [3. 解题思路](#3. 解题思路)
      • [4. 代码实现(Java)](#4. 代码实现(Java))
      • [5. 复杂度分析](#5. 复杂度分析)
    • [三、K 个一组翻转链表(题目 25)](#三、K 个一组翻转链表(题目 25))
      • [1. 题目描述](#1. 题目描述)
      • [2. 示例](#2. 示例)
      • [3. 解题思路](#3. 解题思路)
      • [4. 代码实现(Java)](#4. 代码实现(Java))
      • [5. 复杂度分析](#5. 复杂度分析)
    • [四、随机链表的复制(题目 138)](#四、随机链表的复制(题目 138))
      • [1. 题目描述](#1. 题目描述)
      • [2. 示例](#2. 示例)
      • [3. 解题思路](#3. 解题思路)
      • [4. 代码实现(Java)](#4. 代码实现(Java))
      • [5. 复杂度分析](#5. 复杂度分析)
    • [五、排序链表(题目 147)](#五、排序链表(题目 147))
      • [1. 题目描述](#1. 题目描述)
      • [2. 示例](#2. 示例)
      • [3. 解题思路](#3. 解题思路)
      • [4. 代码实现(Java)](#4. 代码实现(Java))
      • [5. 复杂度分析](#5. 复杂度分析)
    • [六、合并 K 个升序链表(题目 23)](#六、合并 K 个升序链表(题目 23))
      • [1. 题目描述](#1. 题目描述)
      • [2. 示例](#2. 示例)
      • [3. 解题思路](#3. 解题思路)
      • [4. 代码实现(Java)](#4. 代码实现(Java))
      • [5. 复杂度分析](#5. 复杂度分析)
    • [七、LRU 缓存(题目 146)](#七、LRU 缓存(题目 146))
      • [1. 题目描述](#1. 题目描述)
      • [2. 示例](#2. 示例)
      • [3. 解题思路](#3. 解题思路)
      • [4. 代码实现(Java)](#4. 代码实现(Java))
      • [5. 复杂度分析](#5. 复杂度分析)

前言

在力扣(LeetCode)平台上,链表相关的题目一直是考察重点,也是许多开发者提升算法能力的必刷内容。今天,我们就来详细解析链表专题中的几道经典题目,帮助大家更好地理解解题思路和技巧。

一、删除链表的倒数第 N 个节点(题目 19)

1. 题目描述

给你一个链表,删除链表的倒数第 n 个节点,并返回链表的头节点。

2. 示例

示例 1:

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

输出:[1, 2, 3, 5]

解释:删除倒数第 2 个节点后,链表变为 [1, 2, 3, 5]

3. 解题思路

这道题主要考察双指针的应用。我们可以使用两个指针,一个指针先移动 n 步,然后两个指针同时移动,直到先移动的指针到达链表末尾。具体步骤如下:

  1. 初始化两个指针 fastslow,都指向链表的头节点。
  2. fast 指针先移动 n 步。
  3. 同时移动 fastslow 指针,直到 fast 指针到达链表末尾。
  4. 此时,slow 指针指向倒数第 n 个节点的前一个节点,删除该节点。

4. 代码实现(Java)

java 复制代码
public class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode fast = dummy;
        ListNode slow = dummy;
        for (int i = 0; i < n; i++) {
            fast = fast.next;
        }
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        slow.next = slow.next.next;
        return dummy.next;
    }
}

5. 复杂度分析

  • 时间复杂度 :O(n),其中 n 是链表的长度。我们只需要遍历链表一次。
  • 空间复杂度 :O(1),我们只使用了常数级别的额外空间。

二、两两交换链表中的节点(题目 24)

1. 题目描述

给你一个链表,两两交换其中相邻的节点,并返回交换后的链表。你需要在不修改节点值的情况下完成本题。

2. 示例

示例 1:

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

输出:[2, 1, 4, 3]

解释:两两交换后,链表变为 [2, 1, 4, 3]

3. 解题思路

这道题主要考察链表节点的操作。我们可以使用迭代的方法来交换相邻的节点。具体步骤如下:

  1. 初始化一个虚拟头节点 dummy,用于构建新的链表。
  2. 使用一个指针 prev 指向当前需要交换的节点的前一个节点。
  3. 遍历链表,交换相邻的节点,并更新指针。
  4. 重复步骤 3,直到遍历完整个链表。

4. 代码实现(Java)

java 复制代码
public class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode prev = dummy;
        while (prev.next != null && prev.next.next != null) {
            ListNode first = prev.next;
            ListNode second = first.next;
            prev.next = second;
            first.next = second.next;
            second.next = first;
            prev = first;
        }
        return dummy.next;
    }
}

5. 复杂度分析

  • 时间复杂度 :O(n),其中 n 是链表的长度。我们只需要遍历链表一次。
  • 空间复杂度 :O(1),我们只使用了常数级别的额外空间。

三、K 个一组翻转链表(题目 25)

1. 题目描述

给你一个链表,每 k 个节点一组翻转链表,返回翻转后的链表。如果剩余节点少于 k 个,则保持原样。

2. 示例

示例 1:

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

输出:[2, 1, 4, 3, 5]

解释:每 2 个节点一组翻转后,链表变为 [2, 1, 4, 3, 5]

3. 解题思路

这道题主要考察链表的分组和翻转操作。我们可以使用递归或迭代的方法来实现。具体步骤如下:

  1. 初始化一个虚拟头节点 dummy,用于构建新的链表。
  2. 使用一个指针 groupPrev 指向当前组的前一个节点。
  3. 遍历链表,每 k 个节点一组,翻转该组节点。
  4. 重复步骤 3,直到遍历完整个链表。

4. 代码实现(Java)

java 复制代码
public class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode groupPrev = dummy;
        while (true) {
            ListNode kth = groupPrev;
            for (int i = 0; i < k; i++) {
                kth = kth.next;
                if (kth == null) {
                    return dummy.next;
                }
            }
            ListNode groupNext = kth.next;
            ListNode prev = groupNext;
            ListNode curr = groupPrev.next;
            while (curr != groupNext) {
                ListNode temp = curr.next;
                curr.next = prev;
                prev = curr;
                curr = temp;
            }
            groupPrev.next = prev;
            groupPrev = groupNext;
        }
    }
}

5. 复杂度分析

  • 时间复杂度 :O(n),其中 n 是链表的长度。我们只需要遍历链表一次。
  • 空间复杂度 :O(1),我们只使用了常数级别的额外空间。

四、随机链表的复制(题目 138)

1. 题目描述

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。返回这个链表的深拷贝。

2. 示例

示例 1:

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

输出:[[1, null], [2, 1], [3, 2], [4, 3], [5, 4]]

解释:复制后的链表与原链表相同。

3. 解题思路

这道题主要考察链表的深拷贝和随机指针的处理。我们可以使用哈希表来存储原节点和新节点的映射关系。具体步骤如下:

  1. 遍历链表,创建新节点,并将原节点和新节点的映射关系存储在哈希表中。
  2. 再次遍历链表,设置新节点的 nextrandom 指针。
  3. 返回新链表的头节点。

4. 代码实现(Java)

java 复制代码
public class Solution {
    public RandomListNode copyRandomList(RandomListNode head) {
        if (head == null) {
            return null;
        }
        Map<RandomListNode, RandomListNode> map = new HashMap<>();
        RandomListNode current = head;
        while (current != null) {
            map.put(current, new RandomListNode(current.label));
            current = current.next;
        }
        current = head;
        while (current != null) {
            map.get(current).next = map.get(current.next);
            map.get(current).random = map.get(current.random);
            current = current.next;
        }
        return map.get(head);
    }
}

5. 复杂度分析

  • 时间复杂度 :O(n),其中 n 是链表的长度。我们需要遍历链表两次。
  • 空间复杂度 :O(n),需要使用哈希表存储原节点和新节点的映射关系。

五、排序链表(题目 147)

1. 题目描述

给你一个单链表的头节点 head,请你将其按升序排序并返回排序后的链表。

2. 示例

示例 1:

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

输出:[1, 2, 3, 4]

解释:排序后的链表为 [1, 2, 3, 4]

3. 解题思路

这道题主要考察链表的排序操作。我们可以使用归并排序来实现。具体步骤如下:

  1. 找到链表的中点,将链表分成两部分。
  2. 递归地对两部分链表进行排序。
  3. 合并两个有序链表。

4. 代码实现(Java)

java 复制代码
public class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode mid = getMid(head);
        ListNode left = sortList(head);
        ListNode right = sortList(mid);
        return merge(left, right);
    }

    private ListNode getMid(ListNode head) {
        ListNode slow = head, fast = head.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        ListNode mid = slow.next;
        slow.next = null;
        return mid;
    }

    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                current.next = l1;
                l1 = l1.next;
            } else {
                current.next = l2;
                l2 = l2.next;
            }
            current = current.next;
        }
        current.next = (l1 != null) ? l1 : l2;
        return dummy.next;
    }
}

5. 复杂度分析

  • 时间复杂度 :O(n log n),其中 n 是链表的长度。我们需要将链表分成两部分,递归地排序,然后合并。
  • 空间复杂度 :O(log n),需要使用递归栈空间。

六、合并 K 个升序链表(题目 23)

1. 题目描述

给你一个由 k 个升序链表组成的数组 lists,请你将它们合并成一个升序链表,并返回合并后的链表。

2. 示例

示例 1:

输入:lists = [[1, 4, 5], [1, 3, 4], [2, 6]]

输出:[1, 1, 2, 3, 4, 4, 5, 6]

解释:合并后的链表为 [1, 1, 2, 3, 4, 4, 5, 6]

3. 解题思路

这道题主要考察多个链表的合并操作。我们可以使用优先队列(最小堆)来实现。具体步骤如下:

  1. 初始化一个优先队列,将每个链表的头节点加入队列。
  2. 使用一个虚拟头节点 dummy,用于构建新的链表。
  3. 每次从队列中取出最小的节点,将其添加到新链表中,并将该节点的下一个节点加入队列。
  4. 重复步骤 3,直到队列为空。

4. 代码实现(Java)

java 复制代码
import java.util.PriorityQueue;

public class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        PriorityQueue<ListNode> queue = new PriorityQueue<>((a, b) -> a.val - b.val);
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;
        for (ListNode list : lists) {
            if (list != null) {
                queue.offer(list);
            }
        }
        while (!queue.isEmpty()) {
            ListNode node = queue.poll();
            current.next = node;
            current = current.next;
            if (node.next != null) {
                queue.offer(node.next);
            }
        }
        return dummy.next;
    }
}

5. 复杂度分析

  • 时间复杂度 :O(n log k),其中 n 是所有链表的总长度,k 是链表的数量。我们需要将每个节点加入优先队列,并从队列中取出最小的节点。
  • 空间复杂度 :O(k),需要使用优先队列存储每个链表的头节点。

七、LRU 缓存(题目 146)

1. 题目描述

请你设计并实现一个遵循最近最少使用(LRU)缓存机制的数据结构。它应支持以下操作:获取数据(get)和写入数据(put)。

  • 当获取数据时,如果键(key)存在于缓存中,则获取键的值(总是正数),否则返回 -1。
  • 当写入数据时,如果键已经存在,则变更其数据值;如果键不存在,则插入该组「键-值」。当缓存容量达到上限时,它应该在写入新数据之前,删除最久未使用的数据值,从而为新的数据值留出空间。

2. 示例

示例 1:

输入:

"LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"

\[2\], \[1, 1\], \[2, 2\], \[1\], \[3, 3\], \[2\], \[4, 4\], \[1\], \[3\], \[4\]

输出:

null, null, null, 1, null, -1, null, -1, 3, 4

解释:

LRUCache cache = new LRUCache(2);

cache.put(1, 1); // 缓存是 {1=1}

cache.put(2, 2); // 缓存是 {1=1, 2=2}

cache.get(1); // 返回 1

cache.put(3, 3); // 缓存是 {2=2, 3=3}

cache.get(2); // 返回 -1

cache.put(4, 4); // 缓存是 {3=3, 4=4}

cache.get(1); // 返回 -1

cache.get(3); // 返回 3

cache.get(4); // 返回 4

3. 解题思路

这道题主要考察缓存的设计和链表的操作。我们可以使用哈希表和双向链表来实现 LRU 缓存。具体步骤如下:

  1. 使用哈希表存储键和节点的映射关系。
  2. 使用双向链表存储缓存中的节点,最近使用的节点放在链表头部,最久未使用的节点放在链表尾部。
  3. 实现 getput 操作,更新节点的位置。

4. 代码实现(Java)

java 复制代码
import java.util.HashMap;

public class LRUCache {
    private int capacity;
    private HashMap<Integer, DLinkedNode> cache;
    private DLinkedNode head, tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        cache = new HashMap<>();
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        if (cache.containsKey(key)) {
            DLinkedNode node = cache.get(key);
            moveToHead(node);
            return node.value;
        }
        return -1;
    }

    public void put(int key, int value) {
        if (cache.containsKey(key)) {
            DLinkedNode node = cache.get(key);
            node.value = value;
            moveToHead(node);
        } else {
            DLinkedNode node = new DLinkedNode(key, value);
            cache.put(key, node);
            addToHead(node);
            if (cache.size() > capacity) {
                DLinkedNode removed = removeTail();
                cache.remove(removed.key);
            }
        }
    }

    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private DLinkedNode removeTail() {
        DLinkedNode node = tail.prev;
        removeNode(node);
        return node;
    }

    private class DLinkedNode {
        int key, value;
        DLinkedNode prev, next;

        public DLinkedNode() {
        }

        public DLinkedNode(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
}

5. 复杂度分析

  • 时间复杂度 :O(1),每个操作(getput)的时间复杂度都是 O(1)。
  • 空间复杂度 :O(capacity),需要使用哈希表和双向链表存储缓存中的节点。

以上就是力扣热题 100 中与链表相关的经典题目的详细解析,希望对大家有所帮助。在实际刷题过程中,建议大家多动手实践,理解解题思路的本质,这样才能更好地应对各种算法问题。

相关推荐
AI多Agent协作实战派14 分钟前
AI多Agent协作系统实战(十七):凌晨4点,我的AI系统在“假装工作“——3个bug同时爆炸的5小时
java·前端·bug
gaolei_eit22 分钟前
Java+Ai+vue
java·spring·maven
qq_25183645732 分钟前
基于java Web 动漫视频网站毕业论文
java·开发语言·前端
LayZhangStrive37 分钟前
JUC相关的函数、注解、变量杂记
java·面试·多线程·juc
未秃头的程序猿42 分钟前
给公司做了个AI客服Agent,用的Spring AI 1.0,3天上线领导拍板了
java·后端·ai编程
来一碗刘肉面1 小时前
栈的应用(表达式求值)
数据结构·算法
曹牧1 小时前
Eclipse 批量文本替换
java·ide·eclipse
程序员清风1 小时前
OpenAI官方发布最新提示词技巧!
java·后端·面试
梅头脑1 小时前
写了3年CRUD,volatile和synchronized的区别还是答不清——直到我画出了这张三性两锁边界图
java
暮暮祈安1 小时前
Celery 新手入门指南
java·数据库·python·flask·httpx