链表三剑客 + 自制链表:从“删点”到“造轮子”的硬核修炼手册

8 阅读4分钟

你以为链表只是指针乱跳?不,它是程序员的“舞蹈编排艺术”!


别看它没有数组那么“整齐划一”,但一旦你掌握了它的节奏,就能在 LeetCode 上如鱼得水,甚至亲手“造个轮子”出来!本文将带你一口气搞定四道经典链表题:

  1. 删除倒数第 N 个节点(LCR 021)
  2. 两两交换节点(LeetCode 24)
  3. 反转链表(LCR 024)
  4. 手写 MyLinkedList(LeetCode 707)

准备好了吗?系好安全带,我们出发!


🔥 第一式:快慢指针——精准“爆头”倒数第 N 个节点

题目回顾

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

比如:[1,2,3,4,5],n=2 → 删掉 4,得到 [1,2,3,5]

思考陷阱

很多人第一反应是:“先遍历一遍算长度,再走 len - n 步删掉。”
没错,但太慢了! 能不能一次遍历搞定?

✨ 快慢指针登场!

  • 设置两个指针 fastslow,都从虚拟头节点(dummy)出发。
  • 先让 fastn 步,拉开距离。
  • 然后 fastslow 同步前进,直到 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]

关键点

  • 不能改值,只能改指针!
  • 每次操作涉及三个节点:前驱、当前、下一个。

操作步骤(图解脑补)

  1. prev 指向上一组的尾巴(初始为 dummy)

  2. head 是当前要交换的第一个节点

  3. next = head.next 是第二个

  4. 调整指针:

    • head.next = next.next
    • next.next = head
    • prev.next = next
  5. 更新 prev = headhead = 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 —— 从用户变成造物主!

为什么这题重要?

  • 它逼你理解链表的增删查改底层逻辑
  • 是面试中“设计题”的入门款
  • 写完你会对 headtailsize 的关系有深刻体会

设计要点

  • size 记录长度,避免重复遍历
  • 维护 headtail,方便头插尾插
  • 所有操作都要考虑边界:空链表、头节点、尾节点
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自定义链表“长度要记,尾巴要管”

💬 最后说两句

链表就像人生的隐喻:

  • 没有随机访问 → 你不能跳过过程直达结果
  • 指针决定方向 → 你的选择塑造未来路径
  • 删除节点不可逆 → 有些事,做了就回不去了 😅

但只要掌握规律,你就能在指针的迷宫中游刃有余。