文章目录
-
- 前言
- [一、删除链表的倒数第 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
步,然后两个指针同时移动,直到先移动的指针到达链表末尾。具体步骤如下:
- 初始化两个指针
fast
和slow
,都指向链表的头节点。 - 让
fast
指针先移动n
步。 - 同时移动
fast
和slow
指针,直到fast
指针到达链表末尾。 - 此时,
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. 解题思路
这道题主要考察链表节点的操作。我们可以使用迭代的方法来交换相邻的节点。具体步骤如下:
- 初始化一个虚拟头节点
dummy
,用于构建新的链表。 - 使用一个指针
prev
指向当前需要交换的节点的前一个节点。 - 遍历链表,交换相邻的节点,并更新指针。
- 重复步骤 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. 解题思路
这道题主要考察链表的分组和翻转操作。我们可以使用递归或迭代的方法来实现。具体步骤如下:
- 初始化一个虚拟头节点
dummy
,用于构建新的链表。 - 使用一个指针
groupPrev
指向当前组的前一个节点。 - 遍历链表,每
k
个节点一组,翻转该组节点。 - 重复步骤 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. 解题思路
这道题主要考察链表的深拷贝和随机指针的处理。我们可以使用哈希表来存储原节点和新节点的映射关系。具体步骤如下:
- 遍历链表,创建新节点,并将原节点和新节点的映射关系存储在哈希表中。
- 再次遍历链表,设置新节点的
next
和random
指针。 - 返回新链表的头节点。
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. 解题思路
这道题主要考察链表的排序操作。我们可以使用归并排序来实现。具体步骤如下:
- 找到链表的中点,将链表分成两部分。
- 递归地对两部分链表进行排序。
- 合并两个有序链表。
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. 解题思路
这道题主要考察多个链表的合并操作。我们可以使用优先队列(最小堆)来实现。具体步骤如下:
- 初始化一个优先队列,将每个链表的头节点加入队列。
- 使用一个虚拟头节点
dummy
,用于构建新的链表。 - 每次从队列中取出最小的节点,将其添加到新链表中,并将该节点的下一个节点加入队列。
- 重复步骤 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 缓存。具体步骤如下:
- 使用哈希表存储键和节点的映射关系。
- 使用双向链表存储缓存中的节点,最近使用的节点放在链表头部,最久未使用的节点放在链表尾部。
- 实现
get
和put
操作,更新节点的位置。
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),每个操作(
get
和put
)的时间复杂度都是 O(1)。 - 空间复杂度 :O(capacity),需要使用哈希表和双向链表存储缓存中的节点。
以上就是力扣热题 100 中与链表相关的经典题目的详细解析,希望对大家有所帮助。在实际刷题过程中,建议大家多动手实践,理解解题思路的本质,这样才能更好地应对各种算法问题。