链表指针玩不转?从基础到双指针,JS 实战带你破局

提到链表,很多初学者都会被 "指针操作""边界条件" 搞得头疼:为什么删除头节点要单独处理?双指针怎么找倒数第 k 个节点?其实链表没那么难,它的核心是 "通过指针串联节点",掌握了指针的修改逻辑,再复杂的题也能拆解成基础操作。

什么是链表?从 "节点" 到 "串联" 的本质

链表是一种动态数据结构,由一个个 "节点" 通过指针串联而成,不需要连续的内存空间。就像一串珠子,每个珠子(节点)都有一个孔(指针),指向后面的珠子。

(1)链表的基本构成:节点(Node)

每个节点包含两部分:

  • val:存储的数据(数字、字符串等);
  • next:指向后一个节点的指针(引用),最后一个节点的nextnull(表示没有下一个节点)。

在 JS 中,我们用对象模拟节点:

javascript 复制代码
// 节点构造函数
function ListNode(val, next) {
  this.val = val === undefined ? 0 : val; // 节点值
  this.next = next === undefined ? null : next; // 指向下一个节点的指针
}

比如,创建一个简单的链表(1→2→3):

javascript 复制代码
// 创建节点
const node1 = new ListNode(1);
const node2 = new ListNode(2);
const node3 = new ListNode(3);

// 串联节点
node1.next = node2;
node2.next = node3;
// 此时链表为:node1 → node2 → node3 → null

(2)链表 vs 数组:为什么需要链表?

很多人会问:数组也能存数据,为什么要用链表?这要从两者的内存和操作特性说起:

特性 数组 链表
内存 连续空间,需预先分配 非连续,动态分配
访问效率 索引直接访问(O (1)) 从头遍历(O (n))
插入 / 删除效率 需移动元素(O (n)) 改指针即可(O (1),找到节点后)
灵活性 固定长度,扩容成本高 长度动态变化,无需扩容

简单说:数组适合 "频繁访问、少修改" 的场景;链表适合 "频繁插入删除、长度不确定" 的场景。比如实现队列、哈希表的链地址法,都离不开链表。

链表的核心操作:从 "增删查" 到模板化

链表的所有操作,本质都是 "指针的修改"。掌握以下基础操作的模板,遇到复杂题也能拆解成这些步骤。

遍历链表:从头走到尾的基本逻辑

遍历是所有操作的基础,核心是 "用一个指针从头节点开始,不断往后移,直到指向 null"。

javascript 复制代码
// 遍历链表,打印所有节点值
function traverse(head) {
  let cur = head; // 用cur指针遍历
  while (cur !== null) { // 只要cur不是null,就继续
    console.log(cur.val); // 访问当前节点值
    cur = cur.next; // 指针后移
  }
}

关键 :遍历过程中,curhead开始,每次移动都要判断cur !== null,避免访问null.next报错。

插入节点:头插、尾插、中间插的统一逻辑

插入节点的核心是 "先连后断":先让新节点的指针指向正确位置,再修改原有指针,避免节点丢失。

① 头插法(在链表头部插入)

javascript 复制代码
// 在head前插入新节点,返回新头节点
function insertAtHead(head, val) {
  const newNode = new ListNode(val); // 创建新节点
  newNode.next = head; // 新节点指向原头节点
  return newNode; // 新节点成为新头
}

② 尾插法(在链表尾部插入)

javascript 复制代码
// 在链表尾部插入新节点
function insertAtTail(head, val) {
  const newNode = new ListNode(val);
  if (head === null) { // 若链表为空,新节点就是头节点
    return newNode;
  }
  let cur = head;
  while (cur.next !== null) { // 找到尾节点(cur.next为null)
    cur = cur.next;
  }
  cur.next = newNode; // 尾节点指向新节点
  return head;
}

③ 中间插入(在指定节点后插入)

javascript 复制代码
// 在index(0开始)位置前插入新节点
function insertAtIndex(head, index, val) {
  // 用虚拟头节点统一逻辑(无需单独处理头节点)
  const dummyHead = new ListNode(0);
  dummyHead.next = head;
  
  let cur = dummyHead;
  // 找到index的前一个节点
  for (let i = 0; i < index; i++) {
    if (cur === null) return head; // 索引无效,直接返回
    cur = cur.next;
  }
  
  const newNode = new ListNode(val);
  newNode.next = cur.next; // 新节点指向cur的下一个
  cur.next = newNode; // cur指向新节点
  
  return dummyHead.next; // 虚拟头节点的next是新头
}

技巧 :插入操作中,用虚拟头节点(dummyHead) 可以避免 "插入头节点" 的特殊处理,让所有位置的插入逻辑统一。

删除节点:避免 "断链" 的关键

删除节点的核心是 "找到目标节点的前一个节点,修改它的指针,跳过目标节点"。

javascript 复制代码
// 删除值为val的所有节点
function removeElements(head, val) {
  const dummyHead = new ListNode(0);
  dummyHead.next = head;
  let cur = dummyHead;
  
  while (cur.next !== null) { // 遍历判断下一个节点是否要删
    if (cur.next.val === val) {
      cur.next = cur.next.next; // 跳过目标节点(删除)
    } else {
      cur = cur.next; // 不删则后移
    }
  }
  
  return dummyHead.next;
}

关键 :删除时,cur始终指向 "目标节点的前一个节点",通过cur.next = cur.next.next跳过目标节点,避免直接操作目标节点导致的断链。

面试高频:双指针技巧,让遍历效率翻倍

很多链表题用暴力解法(两次遍历)能做,但面试官更想看 "一次遍历" 的优化方案,双指针就是关键。

(1)找链表的中间节点:快慢指针同步走

快指针每次走 2 步,慢指针每次走 1 步,当快指针到尾时,慢指针刚好在中间。

javascript 复制代码
function findMiddle(head) {
  let fast = head;
  let slow = head;
  while (fast !== null && fast.next !== null) {
    fast = fast.next.next; // 快指针走2步
    slow = slow.next; // 慢指针走1步
  }
  return slow; // 慢指针就是中间节点
}

场景:判断回文链表、链表分半等场景常用。

(2)删除倒数第 n 个节点:让快指针先走 n 步

暴力解法需要先算长度,双指针只需一次遍历。看原题:

解题思路:双指针(快慢指针)

暴力解法需要先遍历链表获取长度(O (n)),再遍历到目标节点的前一个(O (n)),总时间 O (2n)。双指针可优化为 O (n):

  • 快指针fast先出发,比慢指针slow多走 n 步;
  • 两指针同时后移,当fast到达尾节点时,slow刚好指向 "倒数第 n+1 个节点";
  • 通过slow.next = slow.next.next删除目标节点。
javascript 复制代码
var removeNthFromEnd = function(head, n) {
    // 步骤1:创建虚拟头节点(处理头节点被删除的情况)
    const dummyHead = new ListNode(0);
    dummyHead.next = head;
    
    // 步骤2:初始化快慢指针,都指向虚拟头节点
    let fast = dummyHead;
    let slow = dummyHead;
    
    // 步骤3:快指针先走n步
    for (let i = 0; i < n; i++) {
        // 若n超过链表长度,直接返回原头节点(无效输入)
        if (fast.next === null) return head;
        fast = fast.next;
    }
    
    // 步骤4:快慢指针同时后移,直到快指针到尾节点
    while (fast.next !== null) {
        fast = fast.next;
        slow = slow.next;
    }
    
    // 步骤5:删除slow的下一个节点(倒数第n个)
    slow.next = slow.next.next;
    
    return dummyHead.next;
};

双指针的优势

只需一次遍历即可定位目标节点,时间复杂度 O (n),比暴力解法更高效,是链表中 "倒数第 k 个节点" 类问题的标准解法。

避坑指南:这些边界条件必须考虑

链表题容易出错,很多时候是没处理好边界情况,记住这几种场景:

  1. 空链表(head === null) :插入、删除前先判断,避免操作 null 指针;
  2. 单节点链表(head.next === null) :删除后会变成空链表,返回 null;
  3. 头节点操作:优先用虚拟头节点统一逻辑,减少特殊处理;
  4. 指针后移的条件 :遍历中cur = cur.next前,必须确保cur !== null
相关推荐
bigyoung1 分钟前
babel 自定义plugin中,如何判断一个ast中是否是jsx文件
前端·javascript·babel
hy.z_77719 分钟前
【数据结构】反射、枚举 和 lambda表达式
android·java·数据结构
墨染点香20 分钟前
LeetCode Hot100 【1.两数之和、2.两数相加、3.无重复字符的最长子串】
算法·leetcode·职场和发展
指尖的记忆31 分钟前
当代前端人的 “生存技能树”:从切图仔到全栈侠的魔幻升级
前端·程序员
草履虫建模42 分钟前
Ajax原理、用法与经典代码实例
java·前端·javascript·ajax·intellij-idea
时寒的笔记1 小时前
js入门01
开发语言·前端·javascript
陈随易1 小时前
MoonBit能给前端开发带来什么好处和实际案例演示
前端·后端·程序员
秋说1 小时前
【PTA数据结构 | C语言版】二叉树层序序列化
c语言·数据结构·算法
996幸存者1 小时前
uniapp图片上传组件封装,支持添加、压缩、上传(同时上传、顺序上传)、预览、删除
前端
Qter1 小时前
RedHat7.5运行qtcreator时出现qt.qpa.plugin: Could not load the Qt platform plugin "xcb
前端·后端