1. 链表基础与力扣刷题准备
链表作为数据结构中的"常青树",在力扣算法题库中占据了举足轻重的地位。与数组不同,链表通过指针将零散的内存块串联起来,这种非连续存储的特性让它在大数据量插入删除时展现出O(1)时间复杂度优势。但指针操作也带来了更高的出错概率——根据力扣官方统计,链表类题目的首次提交错误率高达63%,远高于数组类题目。
工欲善其事必先利其器,在开始刷题前需要明确几个关键工具:
- 力扣的链表可视化工具(直接输入题目编号后点击"可视化")
- 本地调试时的链表构造模板(C++示例):
cpp复制struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
// 快速构造链表
ListNode* createList(vector<int> vals) {
ListNode dummy(0);
ListNode* curr = &dummy;
for(int val : vals) {
curr->next = new ListNode(val);
curr = curr->next;
}
return dummy.next;
}
- 必须掌握的四个基础操作:
- 头插法(反转链表的基础)
- 尾插法(构建环形链表的关键)
- 快慢指针(检测环的标配)
- 虚拟头节点(处理边界情况的利器)
特别提醒:力扣的链表题目输入输出已经封装了序列化/反序列化过程,实际面试时需要手动实现这些转换,建议在本地练习时完整模拟面试场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 单链表高频题型精讲
2.1 反转链表的三重境界
反转链表(力扣206)看似简单,却暗藏玄机。让我们看看三种不同段位的解法:
青铜解法 - 堆栈法:
python复制def reverseList(head):
stack = []
while head:
stack.append(head)
head = head.next
dummy = ListNode(0)
curr = dummy
while stack:
curr.next = stack.pop()
curr = curr.next
curr.next = None
return dummy.next
时间复杂度O(n),空间复杂度O(n),虽然AC但面试官会皱眉。
黄金解法 - 迭代法:
java复制public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
这才是面试官期待的O(1)空间解法,但要注意指针操作的顺序。
王者解法 - 递归法:
javascript复制var reverseList = function(head) {
if (!head || !head.next) return head;
const p = reverseList(head.next);
head.next.next = head;
head.next = null;
return p;
};
递归栈空间O(n),但展示了对链表本质的理解。注意递归基条件和指针修改顺序。
2.2 环形链表检测的数学证明
力扣141和142题要求检测链表是否有环并找出入环点。快慢指针解法背后其实有严谨的数学推导:
设链表非环部分长度L,环长度C
当慢指针进入环时(走了L步),快指针已走2L步(位于环内(2L-L)%C = L%C处)
此后快指针每步追近1个单位,需要(C - L%C)步追上
总步数为L + (C - L%C) ≤ L + C = O(n)
找入环点的证明更精妙:
当首次相遇时,将快指针移回起点并同速前进,再次相遇点即为入环点。这是因为:
首次相遇时慢指针走了L + D(D为环内位置)
快指针走了L + D + nC = 2(L + D) ⇒ L + D = nC
因此从起点走L步正好到达入环点
python复制def detectCycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
fast = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
return None
3. 链表综合应用难题破解
3.1 LRU缓存机制的链表+哈希实现
力扣146题要求实现LRU缓存,这需要结合哈希表的快速查找和链表的快速删除特性:
cpp复制class LRUCache {
private:
struct DLinkedNode {
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
};
void addNode(DLinkedNode* node) {
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
void removeNode(DLinkedNode* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
void moveToHead(DLinkedNode* node) {
removeNode(node);
addNode(node);
}
DLinkedNode* popTail() {
DLinkedNode* res = tail->prev;
removeNode(res);
return res;
}
unordered_map<int, DLinkedNode*> cache;
int capacity;
DLinkedNode *head, *tail;
public:
LRUCache(int capacity) {
this->capacity = capacity;
head = new DLinkedNode();
tail = new DLinkedNode();
head->next = tail;
tail->prev = head;
}
int get(int key) {
if (cache.find(key) == cache.end()) return -1;
moveToHead(cache[key]);
return cache[key]->value;
}
void put(int key, int value) {
if (cache.find(key) != cache.end()) {
cache[key]->value = value;
moveToHead(cache[key]);
} else {
DLinkedNode* newNode = new DLinkedNode();
newNode->key = key;
newNode->value = value;
cache[key] = newNode;
addNode(newNode);
if (cache.size() > capacity) {
DLinkedNode* tail = popTail();
cache.erase(tail->key);
delete tail;
}
}
}
};
关键点在于:
- 双向链表实现O(1)时间插入删除
- 哈希表实现O(1)时间查找
- 虚拟头尾节点处理边界条件
- 注意内存管理(特别是C++实现)
3.2 合并K个升序链表的性能优化
力扣23题有多种解法,体现了算法优化的典型思路:
暴力解法:连续合并,时间复杂度O(kN)
分治解法:两两合并,时间复杂度O(Nlogk)
python复制def mergeKLists(lists):
def mergeTwoLists(l1, l2):
dummy = ListNode(0)
curr = dummy
while l1 and l2:
if l1.val < l2.val:
curr.next = l1
l1 = l1.next
else:
curr.next = l2
l2 = l2.next
curr = curr.next
curr.next = l1 if l1 else l2
return dummy.next
if not lists: return None
interval = 1
while interval < len(lists):
for i in range(0, len(lists)-interval, interval*2):
lists[i] = mergeTwoLists(lists[i], lists[i+interval])
interval *= 2
return lists[0]
优先队列解法:维护最小堆,时间复杂度O(Nlogk)
java复制public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> pq = new PriorityQueue<>((a,b)->a.val-b.val);
for (ListNode node : lists) {
if (node != null) pq.offer(node);
}
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while (!pq.isEmpty()) {
ListNode minNode = pq.poll();
curr.next = minNode;
curr = curr.next;
if (minNode.next != null) {
pq.offer(minNode.next);
}
}
return dummy.next;
}
实际测试中,当k>10时分治法开始显现优势,而优先队列解法代码更简洁。面试时应能分析比较不同解法的适用场景。
4. 链表解题的进阶技巧
4.1 虚拟头节点的妙用
处理链表头节点可能被修改的情况时,虚拟头节点(dummy node)能大幅简化代码:
python复制def removeElements(head, val):
dummy = ListNode(0)
dummy.next = head
curr = dummy
while curr.next:
if curr.next.val == val:
curr.next = curr.next.next
else:
curr = curr.next
return dummy.next
对比不使用dummy的写法:
python复制# 需要单独处理头节点
while head and head.val == val:
head = head.next
if not head: return None
curr = head
while curr.next:
# ...
虚拟头节点技巧在以下场景特别有用:
- 删除节点类题目(如力扣203)
- 合并链表类题目(如力扣21)
- 需要保持前驱指针的遍历操作
4.2 快慢指针的扩展应用
除了检测环,快慢指针还能解决更多问题:
寻找链表中点(力扣876):
cpp复制ListNode* middleNode(ListNode* head) {
ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
删除倒数第N个节点(力扣19):
javascript复制var removeNthFromEnd = function(head, n) {
let dummy = new ListNode(0);
dummy.next = head;
let fast = dummy, slow = dummy;
for (let i = 0; i <= n; i++) {
fast = fast.next;
}
while (fast) {
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;
return dummy.next;
};
判断回文链表(力扣234):
python复制def isPalindrome(head):
# 找中点
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 反转后半部分
prev = None
while slow:
next_node = slow.next
slow.next = prev
prev = slow
slow = next_node
# 比较前后半段
left, right = head, prev
while right:
if left.val != right.val:
return False
left = left.next
right = right.next
return True
4.3 链表排序的进阶实现
力扣148要求O(nlogn)时间排序链表,这需要改造传统排序算法:
归并排序实现:
java复制public ListNode sortList(ListNode head) {
if (head == null || head.next == null) return head;
// 快慢指针找中点
ListNode slow = head, fast = head, prev = null;
while (fast != null && fast.next != null) {
prev = slow;
slow = slow.next;
fast = fast.next.next;
}
prev.next = null; // 切断链表
ListNode l1 = sortList(head);
ListNode l2 = sortList(slow);
return merge(l1, l2);
}
private ListNode merge(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
curr.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
自底向上的O(1)空间实现:
cpp复制ListNode* sortList(ListNode* head) {
if (!head || !head->next) return head;
int len = 0;
ListNode *curr = head;
while (curr) {
len++;
curr = curr->next;
}
ListNode dummy(0);
dummy.next = head;
for (int step = 1; step < len; step <<= 1) {
ListNode *prev = &dummy, *curr = dummy.next;
while (curr) {
ListNode *left = curr;
ListNode *right = split(left, step);
curr = split(right, step);
prev = merge(left, right, prev);
}
}
return dummy.next;
}
ListNode* split(ListNode* head, int step) {
if (!head) return nullptr;
for (int i = 1; head->next && i < step; i++) {
head = head->next;
}
ListNode *right = head->next;
head->next = nullptr;
return right;
}
ListNode* merge(ListNode* l1, ListNode* l2, ListNode* prev) {
while (l1 && l2) {
if (l1->val < l2->val) {
prev->next = l1;
l1 = l1->next;
} else {
prev->next = l2;
l2 = l2->next;
}
prev = prev->next;
}
prev->next = l1 ? l1 : l2;
while (prev->next) prev = prev->next;
return prev;
}
这种实现虽然复杂,但在面试中能展现对算法本质的深刻理解。要注意链表分割和合并时的边界条件处理。
