你以为链表只是指针乱跳?不,它是程序员的“舞蹈编排艺术”!
别看它没有数组那么“整齐划一”,但一旦你掌握了它的节奏,就能在 LeetCode 上如鱼得水,甚至亲手“造个轮子”出来!本文将带你一口气搞定四道经典链表题:
- 删除倒数第 N 个节点(LCR 021)
- 两两交换节点(LeetCode 24)
- 反转链表(LCR 024)
- 手写 MyLinkedList(LeetCode 707)
准备好了吗?系好安全带,我们出发!
🔥 第一式:快慢指针——精准“爆头”倒数第 N 个节点
题目回顾
给你一个链表,删除倒数第 n 个节点,返回新链表头。
比如:[1,2,3,4,5],n=2 → 删掉 4,得到 [1,2,3,5]
思考陷阱
很多人第一反应是:“先遍历一遍算长度,再走 len - n 步删掉。”
没错,但太慢了! 能不能一次遍历搞定?
✨ 快慢指针登场!
- 设置两个指针
fast和slow,都从虚拟头节点(dummy)出发。 - 先让
fast走n步,拉开距离。 - 然后
fast和slow同步前进,直到fast到达最后一个节点。 - 此时
slow恰好停在要删除节点的前一个位置!
var removeNthFromEnd = function(head, n) {
let dummy = new ListNode(0);
dummy.next = head;
let fast = dummy, slow = dummy;
// 快指针先走 n 步
while (n--) fast = fast.next;
// 一起走,直到 fast 到末尾
while (fast.next) {
fast = fast.next;
slow = slow.next;
}
// 删除 slow 的下一个节点
slow.next = slow.next.next;
return dummy.next; // 别忘了返回 dummy.next!
};
💡 为什么用 dummy 节点?
防止删除头节点时边界处理爆炸!这是链表题的“万能保险”。
💃 第二式:节点“蹦迪”——两两交换,原地 shuffle!
题目回顾
把链表相邻两个节点交换:
[1,2,3,4]→[2,1,4,3]
关键点
- 不能改值,只能改指针!
- 每次操作涉及三个节点:前驱、当前、下一个。
操作步骤(图解脑补)
-
prev指向上一组的尾巴(初始为 dummy) -
head是当前要交换的第一个节点 -
next = head.next是第二个 -
调整指针:
head.next = next.nextnext.next = headprev.next = next
-
更新
prev = head,head = head.next,继续下一轮
var swapPairs = function(head) {
const dummy = new ListNode(0);
dummy.next = head;
let prev = dummy;
while (head && head.next) {
const next = head.next;
// 交换核心三连
head.next = next.next;
next.next = head;
prev.next = next;
// 移动指针
prev = head;
head = head.next;
}
return dummy.next;
};
🎶 想象一下:链表在跳双人舞,每对节点互相鞠躬换位,优雅又高效!
🌀 第三式:链表“乾坤大挪移”——反转术
题目回顾
反转整个链表:
[1,2,3]→[3,2,1]
两种流派:迭代 vs 递归
迭代法(推荐!清晰稳定)
- 用
pre记录前一个节点,cur当前,temp临时存下一个 - 每次把
cur.next指向pre,然后整体右移
var reverseList = function(head) {
let pre = null, cur = head;
while (cur) {
let temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre; // 最后 pre 就是新头
};
递归法(炫技专用)
- 递归到底,然后一层层“回溯反转”
function reverse(cur, pre) {
if (!cur) return pre;
let temp = cur.next;
cur.next = pre;
return reverse(temp, cur);
}
var reverseList = function(head) {
return reverse(head, null);
};
⚠️ 面试建议:先写迭代,再提递归。毕竟栈溢出不是闹着玩的!
🛠️ 终极挑战:手搓 MyLinkedList —— 从用户变成造物主!
为什么这题重要?
- 它逼你理解链表的增删查改底层逻辑
- 是面试中“设计题”的入门款
- 写完你会对
head、tail、size的关系有深刻体会
设计要点
- 用
size记录长度,避免重复遍历 - 维护
head和tail,方便头插尾插 - 所有操作都要考虑边界:空链表、头节点、尾节点
var MyLinkedList = function() {
this.size = 0;
this.head = null;
this.tail = null;
};
// 辅助函数:获取 index 处的节点
MyLinkedList.prototype.getNode = function(index) {
if (index < 0 || index >= this.size) return null;
let cur = this.head;
for (let i = 0; i < index; i++) cur = cur.next;
return cur;
};
MyLinkedList.prototype.get = function(index) {
const node = this.getNode(index);
return node ? node.val : -1;
};
MyLinkedList.prototype.addAtHead = function(val) {
const newNode = { val, next: this.head };
this.head = newNode;
if (this.size === 0) this.tail = newNode;
this.size++;
};
MyLinkedList.prototype.addAtTail = function(val) {
const newNode = { val, next: null };
if (this.size === 0) {
this.head = this.tail = newNode;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.size++;
};
MyLinkedList.prototype.addAtIndex = function(index, val) {
if (index < 0 || index > this.size) return;
if (index === 0) return this.addAtHead(val);
if (index === this.size) return this.addAtTail(val);
const prev = this.getNode(index - 1);
const newNode = { val, next: prev.next };
prev.next = newNode;
this.size++;
};
MyLinkedList.prototype.deleteAtIndex = function(index) {
if (index < 0 || index >= this.size) return;
if (index === 0) {
this.head = this.head.next;
if (this.size === 1) this.tail = null;
} else {
const prev = this.getNode(index - 1);
prev.next = prev.next.next;
if (index === this.size - 1) this.tail = prev;
}
this.size--;
};
🧠 经验之谈:
写deleteAtIndex时,一定要更新 tail!否则addAtTail会接错地方,debug 到怀疑人生。
🎯 总结:链表修炼心法
| 技巧 | 应用场景 | 口诀 |
|---|---|---|
| 虚拟头节点(dummy) | 删除/插入头节点 | “有 dummy,不怕头” |
| 快慢指针 | 找中点、倒数第 K 个 | “快走 N 步,慢跟到底” |
| 三指针反转 | 反转链表 | “断、连、移,三步走” |
| 维护 size + tail | 自定义链表 | “长度要记,尾巴要管” |
💬 最后说两句
链表就像人生的隐喻:
- 没有随机访问 → 你不能跳过过程直达结果
- 指针决定方向 → 你的选择塑造未来路径
- 删除节点不可逆 → 有些事,做了就回不去了 😅
但只要掌握规律,你就能在指针的迷宫中游刃有余。