从栈到队列,再到链表:前端开发者必知的线性数据结构

掌握这些基础,你的代码能力会上一个台阶

作为前端开发者,我们每天都在和数组打交道。pushpopshiftunshiftsplice 这些方法用得行云流水。但你是否想过,这些操作背后隐藏着怎样的数据结构原理?

今天,我们就来聊聊线性数据结构中的"三剑客"------栈、队列和链表。理解了它们,你写出的代码会更加优雅高效。

一、栈(Stack):后进先出的"冰柜"

栈就像冰柜里的雪糕------后放进去的先拿出来 。这是一种典型的 LIFO(Last In First Out) 数据结构。

栈的特性

栈可以看成是操作受限的数组 ,一个特殊的数组

  • 只能在栈顶(数组尾部)进行操作
  • 入栈:push
  • 出栈:pop
  • 查看栈顶元素:stack[stack.length - 1](即 peek 操作)

代码实现

javascript

arduino 复制代码
const stack = []; // 空栈

// 入栈
stack.push("东北大板");
stack.push("可爱多");
stack.push("冰工厂");
stack.push("巧乐兹");

// 出栈
while(stack.length) {
    const top = stack[stack.length - 1];
    console.log('取出来的是:', top);
    stack.pop();
}

console.log(stack); // []

栈的应用场景

  • 函数调用栈
  • 浏览器的后退功能
  • 括号匹配校验
  • 撤销操作(Ctrl+Z)

二、队列(Queue):先进先出的"排队"

队列就像在奶茶店排队------先来的先服务 。这是典型的 FIFO(First In First Out) 数据结构。

队列的特性

队列也是一种操作受限的数组

  • 只能在队尾入队
  • 只能在队首出队
  • 入队:push
  • 出队:shift

代码实现

javascript

arduino 复制代码
const queue = []; // 空队列

// 入队
queue.push("许");
queue.push("叶");
queue.push("戴");

// 出队
while(queue.length) {
    const top = queue[0];
    console.log('取出来的是:', top);
    queue.shift();
}

console.log(queue); // []

队列的应用场景

  • 任务队列(宏任务、微任务)
  • 消息队列
  • 广度优先搜索(BFS)

三、数组的增删改查:不得不说的复杂度

splice 方法详解

splice 是数组增删改的"瑞士军刀",它的本质是 slice + replace

javascript

scss 复制代码
// 语法:splice(start_index, del_count, ...addedItems)

const arr = [1, 2];
console.log(arr.splice(1, 0, 3)); // [] (删除0个,返回空数组)
console.log(arr); // [1, 3, 2]

arr.splice(1, 1); // 删除索引1处的元素
console.log(arr); // [1, 2]

⚠️ 注意 :数组的增删改方法都不是纯函数,它们会直接修改原数组!

线性关系:O(n) 的代价

假设数组的长度是 n,当我们在数组中间进行增加或删除操作时:

  • 需要移动的元素数量随着 n 的增加而增加
  • 呈一个线性关系
  • 时间复杂度为 O(n)

这就是数组的"痛点"------插入和删除的效率会随着数据量增长而线性下降。

四、链表(LinkedList):灵活连接的"链条"

为了解决数组增删效率低的问题,链表应运而生。链表通过指针连接各个节点,实现了高效的增删操作。

链表节点的定义

javascript

ini 复制代码
function ListNode(val) {
    this.val = val;
    this.next = null;
}

const node = new ListNode(1);
node.next = new ListNode(2);
console.log(node); // {val: 1, next: {val: 2, next: null}}

链表的添加与删除

添加元素 :前驱节点的 next 指向新节点,新节点的 next 指向后继节点。

删除元素 :前驱节点的 next 直接指向后继节点。

本质都是对 next 指针的操作!

链表的复杂度分析

操作 时间复杂度 说明
访问 O(n) 需要从头遍历,麻烦
插入(已知位置) O(1) 只需改变指针指向
删除(已知位置) O(1) 只需改变指针指向
查找目标位置 O(n) 这是插入/删除的前置成本

💡 关键理解 :链表虽然有高效的增删操作(O(1)),但前提是你已经明确了要插入或删除的目标位置 。找到这个位置本身需要 O(n) 的遍历时间。也就是说,链表的总操作是 O(n)(查找)+ O(1)(操作)= O(n)

数组 vs 链表 完整对比

对比维度 数组 链表
访问元素 O(1) ✅ O(n) ❌
头部插入 O(n) ❌ O(1) ✅
头部删除 O(n) ❌ O(1) ✅
中间插入 O(n) O(n)(查找+指针)
内存结构 连续(或哈希) 离散
内存申请 扩容时一次性申请 每次新增都申请

什么时候用数组?

  • 数据量较小
  • 频繁的遍历和索引访问
  • 对内存连续性有要求

什么时候用链表?

  • 数据量较大
  • 频繁的头部的插入和删除操作
  • 对内存连续性不敏感

五、JS 数组的"特殊身份"(重要!)

需要注意的是,JS 数组未必是真正的数组!这一点经常被前端开发者忽略。

javascript

ini 复制代码
// 情况一:每一项类型一致 → 连续内存(真正的数组)
const arr1 = [1, 2, 3, 4, 5];

// 情况二:每一项类型不一致 → 离散内存
const arr2 = ['haha', 1, {a: 1}];

原理解析

  • 当数组元素类型一致 时,JS 会分配连续内存,此时它具备传统数组的特征
  • 当数组元素类型不一致 时,JS 底层使用哈希分配内存空间,数据存储在离散的位置,通过哈希表映射访问

⚠️ 在后一种情况下,JS 数组不再具有数组的连续内存特征,但仍然可以通过下标访问 ------ 这就是哈希表的功劳!

这也是为什么 JS 数组可以这么"灵活"的原因 ------ 它底层做了很多透明处理。

六、排序小贴士

使用 sort 方法时,一定要传比较函数,否则会按 ASCII 码排序:

javascript

css 复制代码
let arr = [10, 2, 5];
arr.sort((a, b) => a - b); // [2, 5, 10]

// 不传比较函数的后果:
let arr2 = [10, 2, 5];
arr2.sort(); // [10, 2, 5] ❌ 按字符串排序了!

总结:一张图看懂三种数据结构

text

perl 复制代码
┌─────────────────────────────────────────────────────────┐
│                    线性数据结构                          │
├─────────────┬─────────────┬─────────────────────────────┤
│     栈      │    队列     │           链表              │
├─────────────┼─────────────┼─────────────────────────────┤
│   LIFO      │   FIFO      │   离散存储 + 指针连接        │
│   push/pop  │   push/shift│   增删 O(1)* 访问 O(n)      │
│   栈顶操作  │   两端操作   │   需要手动实现节点           │
└─────────────┴─────────────┴─────────────────────────────┘

核心要点回顾

  • 栈和队列:都是操作受限的数组,通过限制操作方式保证了数据的有序性
  • 数组增删:O(n) 的线性关系,数据量越大代价越高
  • splice:增删改一体,但会修改原数组(非纯函数)
  • 链表:离散存储,增删高效(O(1)),但访问低效(O(n))
  • JS 数组:类型一致才是真数组,类型不一致时底层是哈希表

一句话总结

数组用空间换时间(连续内存换快速访问),链表用时间换空间(遍历访问换灵活增删)------ 根据场景选择合适的数据结构,才是聪明的开发者。

相关推荐
PedroQue991 小时前
uni-app路由管理神器:vue-router风格体验
前端·uni-app
用户1733598075371 小时前
花两周用 Vue 3 做了个 PDF 工具站,我在生产环境踩了 8 个坑
前端·vue.js
风骏时光牛马1 小时前
TypeScript 泛型与工具类型实战:企业级通用数据请求封装完整案例
前端
阿猫的故乡1 小时前
Vue自定义指令从入门到实用:自动聚焦、权限控制、防抖、懒加载……全案例教学
前端·javascript·vue.js
嘟嘟07171 小时前
吃透 JS 八大数据类型与内存原理,从代码到底层一站式复习
前端
问心无愧05131 小时前
ctf show web入门157 158
前端·笔记
该用户已成仙1 小时前
vue3 使用 vuedraggable 报错 TypeError: isFunction2 is not a function
前端·javascript·vue.js
aidou13141 小时前
Kotlin中实现星级评价选择功能(仅支持整数)
前端·kotlin·自定义view·imageview·ontouchevent·customratingbar
良逍Ai出海1 小时前
我用 Codex 搭了一个 WordPress 独立站
前端