手把手带你用虚拟头节点实现单链表,搞定所有边界问题

大家好,今天我们来聊聊数据结构中最基础也最容易踩坑的单链表实现。很多同学第一次写链表操作时,总会被 "空链表插入""删除头节点" 这类边界问题搞得焦头烂额,要么空指针报错,要么逻辑写得又臭又长。今天我就用「虚拟头节点」这个神器,带大家一次性搞定单链表的所有核心操作,写出优雅又健壮的代码。


一、先搞懂题目要求:我们要实现什么?

先来看这道经典的链表设计题,它要求我们实现一个支持以下操作的单链表:

  • get(index):获取链表中第 index 个节点的值,索引无效则返回 -1
  • addAtHead(val):在链表头部插入节点
  • addAtTail(val):在链表尾部追加节点
  • addAtIndex(index, val):在第 index 个节点前插入节点,支持头部、尾部和中间插入
  • deleteAtIndex(index):删除第 index 个节点

链表操作的核心难点,其实都集中在边界处理上:空链表怎么插入?删除头节点时怎么更新头指针?插入到尾部时怎么找到尾节点?而「虚拟头节点」就是解决这些问题的万能钥匙。


二、为什么要用虚拟头节点?

传统的单链表实现中,头节点是链表的第一个有效节点,这就导致:

  1. 空链表时,头指针为 null,插入头节点需要特殊判断
  2. 删除头节点时,需要单独处理头指针的更新
  3. 所有插入 / 删除操作,都要区分 "是否在头部" 的情况,代码冗余且容易出错

而虚拟头节点(Dummy Head)的思路,就是给链表添加一个不存储有效数据的哑节点,让它作为链表的固定起点。这样一来:

  • 空链表时,dummyHead.nextnull,不需要特殊处理
  • 所有节点(包括头节点)的插入 / 删除操作,都可以用统一的逻辑处理
  • 再也不用单独写 "头部操作" 的特殊分支了

就像下图展示的那样,不管是插入还是删除节点,我们都可以通过虚拟头节点找到目标节点的前一个节点,然后直接修改指针即可,完美规避了空指针和边界问题。


三、代码逐行解析:从基础到完整实现

接下来我们就用 JavaScript 实现这个链表,核心代码就是你提供的版本,我会逐行拆解每个操作的逻辑和细节。

1. 初始化链表

javascript

运行

ini 复制代码
var MyLinkedList = function() {
    // 虚拟头节点,处理边界情况
    this.dummyHead = {val: 0, next: null};
    // 链表长度,方便判断索引是否有效
    this.size = 0;
};

这里我们定义了两个核心属性:

  • dummyHead:虚拟头节点,值设为 0(无实际意义),初始 nextnull
  • size:记录链表长度,避免每次操作都遍历链表统计长度,提升效率

2. get (index):获取指定索引的节点值

javascript

运行

ini 复制代码
MyLinkedList.prototype.get = function(index) {
    // 索引无效直接返回 -1
    if (index < 0 || index >= this.size) return -1;
    // 从虚拟头节点的下一个节点(第一个有效节点)开始遍历
    let cur = this.dummyHead.next;
    for (let i = 0; i < index; i++) {
        cur = cur.next;
    }
    return cur.val;
};
  • 首先判断索引合法性,避免越界访问
  • 因为虚拟头节点不是有效节点,所以从 dummyHead.next 开始遍历 index 次,就能找到目标节点

3. addAtHead (val):头部插入节点

javascript

运行

kotlin 复制代码
MyLinkedList.prototype.addAtHead = function(val) {
    // 新节点的 next 指向原来的第一个有效节点
    const newNode = {val, next: this.dummyHead.next};
    // 虚拟头节点的 next 指向新节点,完成插入
    this.dummyHead.next = newNode;
    this.size++;
};

有了虚拟头节点,头部插入和中间插入的逻辑完全一致:

  1. 新节点的 next 指向原来的第一个节点(dummyHead.next
  2. 虚拟头节点的 next 更新为新节点,新节点就成了第一个有效节点
  3. 链表长度 +1

4. addAtTail (val):尾部追加节点

javascript

运行

ini 复制代码
MyLinkedList.prototype.addAtTail = function(val) {
    const newNode = {val, next: null};
    // 从虚拟头节点开始遍历,找到最后一个节点
    let cur = this.dummyHead;
    while (cur.next !== null) {
        cur = cur.next;
    }
    // 最后一个节点的 next 指向新节点
    cur.next = newNode;
    this.size++;
};
  • 因为单链表只能从前往后遍历,所以我们从虚拟头节点开始,一直走到 cur.nextnull 的节点,这就是链表的尾节点
  • 尾节点的 next 指向新节点,新节点的 next 设为 null,完成尾部插入

5. addAtIndex (index, val):指定位置插入节点

javascript

运行

ini 复制代码
MyLinkedList.prototype.addAtIndex = function(index, val) {
    // 索引大于链表长度,直接返回,不插入
    if (index > this.size) return;
    // 索引小于 0,默认插入到头部
    if (index < 0) index = 0;
    
    const newNode = {val, next: null};
    // 找到第 index 个节点的前一个节点(虚拟头节点开始遍历 index 次)
    let cur = this.dummyHead;
    for (let i = 0; i < index; i++) {
        cur = cur.next;
    }
    
    // 插入节点:先让新节点的 next 指向 cur 的下一个节点,再更新 cur.next
    newNode.next = cur.next;
    cur.next = newNode;
    this.size++;
};

这是最核心的插入操作,逻辑非常通用:

  1. 先处理索引的边界情况:index > size 不插入,index < 0 设为 0(头部插入)
  2. 找到目标位置的前一个节点 cur(比如要插入到索引 2,就找到索引 1 的节点)
  3. 先让新节点的 next 指向 cur.next,再让 cur.next 指向新节点,完成插入(注意顺序不能反!)
  4. 链表长度 +1

6. deleteAtIndex (index):删除指定索引的节点

javascript

运行

ini 复制代码
MyLinkedList.prototype.deleteAtIndex = function(index) {
    // 索引无效直接返回
    if (index < 0 || index >= this.size) return;
    
    // 找到要删除节点的前一个节点
    let cur = this.dummyHead;
    for (let i = 0; i < index; i++) {
        cur = cur.next;
    }
    
    // 让前一个节点的 next 跳过目标节点,指向它的下一个节点
    cur.next = cur.next.next;
    this.size--;
};

删除操作和插入操作逻辑对称:

  1. 先判断索引合法性,避免越界删除
  2. 找到要删除节点的前一个节点 cur
  3. cur.next 直接指向 cur.next.next,目标节点就从链表中脱离了(JS 会自动回收内存)
  4. 链表长度 -1

四、虚拟头节点的优势总结

看完代码,我们再回头看看虚拟头节点到底帮我们解决了哪些问题:

  1. 消除特殊分支 :头部插入、删除头节点的逻辑和中间节点完全一致,不用写 if (index === 0) 这种分支判断
  2. 简化边界处理 :空链表时,dummyHead.nextnull,插入节点的逻辑依然成立,不用额外判断空链表
  3. 代码复用性高addAtIndex 可以直接实现 addAtHeadaddAtTail,比如 addAtHead 等价于 addAtIndex(0, val)addAtTail 等价于 addAtIndex(size, val)

很多同学觉得链表难,其实就是没找到统一的处理方式,虚拟头节点就是那个能让所有操作逻辑统一起来的 "万能模板"。


五、结语:链表学习的小建议

单链表是数据结构的基础,也是面试中的高频考点。这道题覆盖了链表的所有核心操作,把它吃透,你就能理解链表操作的核心逻辑:所有插入和删除操作,本质上都是修改节点的指针,而关键就是找到目标节点的前一个节点

虚拟头节点这个技巧,在很多链表题中都能用到,比如反转链表、合并链表、删除倒数第 N 个节点等,学会它能帮你少踩很多坑。

如果觉得这篇文章对你有帮助,欢迎点赞收藏,也可以在评论区聊聊你写链表时踩过的坑~

相关推荐
许我半盏清茶10 小时前
JavaScript 原型与原型链完全指南
javascript
xuankuxiaoyao10 小时前
vue.js 设计与开发 ---路由
前端·javascript·vue.js
ZC跨境爬虫10 小时前
跟着 MDN 学CSS day_6:(伪类和伪元素详解)
前端·javascript·css·数据库·ui·html
搞科研的小刘选手10 小时前
【大连市计算机学会主办】第三届图像处理、智能控制与计算机工程国际学术会议(IPICE 2026)
图像处理·人工智能·深度学习·算法·计算机·数据挖掘·智能控制
人月神话-Lee10 小时前
【图像处理】高斯模糊——最优雅的模糊算法
图像处理·人工智能·算法·ios·ai编程·swift
大熊背10 小时前
双目拼接竖缝消除(ISP 分区锐化实操方案) 优化方案
人工智能·算法·双目拼接
Sylus_sui10 小时前
实现:每行固定 5 个、自动换行、最后一行左对齐、数量不固定
前端·javascript·css
_日拱一卒10 小时前
LeetCode:105从前序与中序遍历序列构造二叉树
算法·leetcode·职场和发展
MicroTech202510 小时前
微算法科技(NASDAQ :MLGO)发布基于NEQR技术的新型量子视频处理算法,重构智能视觉底层逻辑
科技·算法·音视频