栈队列链表,三个故事就懂了

前言:线性的世界

数组大家都不陌生------连续存储、下标直达,像一排整齐的储物柜,拿第 3 个格子里的东西,O(1) 一步到位。

但它不是万能的。插入和删除时,元素们就得集体"搬家",时间复杂度飙到 O(n)。

今天我们不聊数组本身,而是从它衍生出的三兄弟:队列链表 。它们都是线性数据结构------每个节点有且仅有一个前驱、有且仅有一个后继。理解它们,是后续攻克树和图(非线性结构)的基石。


一、栈(Stack)------ 冰柜里的雪糕

什么是栈?

栈是一种 LIFO (Last In First Out,后进先出)的数据结构。你可以把它当成一个操作受限的数组 ------只能在尾部(栈顶)进行增删。

想象一个冰柜:

你往冰柜里塞雪糕------东北大板塞进去,西北大板再塞进去,中南大板又塞进去......等到要吃的时候,手只能从最上面拿。最后放进去的,最先被拿出来。 这就是栈。

代码视角

js 复制代码
// push 入栈 --- pop 出栈 --- peek 查看栈顶
const stack = []; // 空栈

stack.push('东北大板');
stack.push('西北大板');
stack.push('中南大板');
stack.push('中北大板');

// 出栈 ------ 后进先出
while (stack.length) {
    const top = stack[stack.length - 1]; // peek:看一眼栈顶
    console.log('取出来的是', top);      // 中北大板 → 中南大板 → 西北大板 → 东北大板
    stack.pop();
}

console.log(stack); // []

三个核心操作

操作 方法 说明
入栈 push() 元素放入栈顶(数组尾部)
出栈 pop() 移除并返回栈顶元素
窥顶 stack[length - 1] 只看不删,瞅一眼栈顶是谁

受限制,反而是种保护------你永远只跟栈顶打交道,逻辑干净,不会误操作栈底元素。许多场景(函数调用栈、括号匹配、撤销/重做)恰恰需要这种"只在一头操作"的约束。


二、队列(Queue)------ 食堂打饭的队伍

什么是队列?

队列是 FIFO (First In First Out,先进先出)。依然是操作受限的数组,但限制的位置不同 ------只能从队尾入队 ,从队首出队

食堂排队打饭:先来的人先打到饭走人(出队),后来的人只能在队尾接着排(入队)。插队?不存在的。

代码视角

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

queue.push('li');    // 入队(队尾)
queue.push('huang');
queue.push('zhang');

while (queue.length) {
    const top = queue[0];          // 队首元素
    console.log(top, 'zaocan');    // li → huang → zhang
    queue.shift();                 // 出队(队首)
}

console.log(queue); // []

核心操作

操作 方法 说明
入队 push() 元素从队尾进入
出队 shift() 元素从队首离开
窥首 queue[0] 看队首是谁

栈 vs 队列:一张表搞清

栈(Stack) 队列(Queue)
规则 LIFO 后进先出 FIFO 先进先出
push() 栈顶 push() 队尾
pop() 栈顶 shift() 队首
比喻 冰柜拿雪糕 🍦 食堂排队 🍚
典型场景 函数调用栈、撤销操作 任务调度、消息队列

三、JS 数组------你以为的"数组"未必是数组

在深入链表之前,有一个容易被忽略的真相需要先揭开:

js 复制代码
const arr = [1, 2, 3, 4];        // 真正的数组:类型一致 → 连续内存 → 下标 O(1)

const arr1 = ['haha', 1, { a: 1 }]; // "伪数组":类型不同 → 底层用哈希映射 → 不再连续

JS 的数组比较"宽容"------你什么都往里塞,它也不抱怨。但代价是:如果元素类型不一致,JS 底层就不再使用连续内存存储,而是通过哈希表映射。此时它名义上叫"数组",实际上已经失去了数组最核心的优势------连续存储 + 下标快速定位。

这就是为什么算法题里,我们默认数组各项是同一类型------只有这样才能享受真正的 O(1) 随机访问。

同样,关于数组的增删方法,补充几个常用但容易踩坑的:

js 复制代码
// splice(start_index, delete_count, ...items_to_add)
const arr = [1, 2];

arr.splice(1, 0, 3); // 在索引 1 处删除 0 个元素,插入 3
console.log(arr);     // [1, 3, 2]

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

⚠️ pushunshiftsplicepopshift 都会原地修改原数组,不是纯函数。如果你不希望污染原始数据,记得先浅拷贝一份。

额外一提:数组的 sort() 方法默认按 ASCII 排序,所以:

js 复制代码
let arr = [10, 2, 5];
arr.sort();            // ❌ [10, 2, 5] --- '10' 的 ASCII < '2' 的 ASCII
arr.sort((a, b) => a - b); // ✅ [2, 5, 10] --- 传入比较函数才靠谱

四、链表(Linked List)------ 手拉手的节点链

为什么需要链表?

数组有个硬伤:扩容

arr.length >= capacity 时,JS 引擎需要重新申请一块更大的连续内存,再把原有元素整体拷贝过去。对于小规模数据无所谓,但如果数据规模巨大,这种"全体搬家"的开销就很可观了。

链表的思路则完全不同:每次新增一个元素,才申请一小块内存,用指针把它们串起来。不需要连续内存,也不需要扩容搬迁。

但天下没有免费的午餐------链表在遍历和随机访问上付出了代价。

链表长什么样?

链表中的每个数据单位叫节点(Node),每个节点有两个部分:

  • val:存数据
  • next:指向下一个节点的指针

节点分布在内存的各个角落(离散),靠 next 指针串成一条链。

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

const node = new ListNode(1);
node.next = new ListNode(2);

// 结构示意:
// {
//     val: 1,
//     next: {
//         val: 2,
//         next: null
//     }
// }

console.log(node);

head(头节点)→ node1 → node2 → ... → tail(尾节点,next 为 null)

访问链表中的任何一个元素,都必须从 head 出发,顺着 next 逐个往下找,一直找到目标节点为止。 没有"按下标直达"这种好事。

链表的增删

增删操作的本质,就是对 next 指针的重新指向:

  • 插入 :找到前驱节点 ,把新节点的 next 指向前驱原本的 next,再把前驱的 next 指向新节点。
  • 删除 :找到前驱节点 ,把它的 next 直接跳过目标节点,指向目标节点的 next

🚌 一个小比喻:坐公交车别"坐过站"------如果先让前驱指向新节点,你就丢失了原来后继节点的引用。正确顺序是先让新节点指向后继,再让前驱指向新节点。

数组 vs 链表:终结对比

维度 数组(Array) 链表(Linked List)
内存 连续存储 离散存储,指针串联
随机访问 O(1) 下标直达 ⚡ O(n) 从头遍历 🐢
增删(已知位置) O(n) 需要移动后续元素 O(1) 只改指针指向 ✨
扩容开销 需要整体搬移 每次只申请一个节点
适用规模 小规模、读多写少 大规模、频繁增删
scss 复制代码
复杂度总结:
┌──────────────────────────────────────┐
│  操作      数组      链表            │
│  随机访问   O(1)     O(n)            │
│  头部增删   O(n)     O(1)            │
│  尾部增删   O(1)*    O(n) / O(1)**   │
│  中间增删   O(n)     O(n) 定位+O(1)  │
│                                      │
│  * 不触发扩容时                       │
│  ** 维护尾指针时                       │
└──────────────────────────────────────┘

五、总结

  1. (Stack)= LIFO,只能在一头操作。像一个冰柜,后放的雪糕先拿出来。核心 API:push / pop / peek
  2. 队列 (Queue)= FIFO,队尾入、队首出。像食堂排队打饭,先来先走。核心 API:push / shift
  3. 链表 (Linked List)= 节点用 next 指针串起来的离散结构。增删快(O(1) 改指针),访问慢(O(n) 从头找)。
  4. JS 数组 当元素类型一致时才是真正的连续数组;类型混杂时底层退化为哈希映射,丧失数组特性。
  5. 选型建议:小规模、偏读取 → 数组;大规模、偏增删 → 链表。栈和队列是加了"使用规则"的特殊数组/链表,按场景选择。

数据结构没有银弹,每种结构都是特定场景下的"最优解"。理解了它们的脾气,写代码时才能"对症下药"。


下一篇预告:从线性到非线性------树的遍历与图的初探。


如果这篇文章对你有帮助,欢迎点赞、收藏、评论三连!有任何疑问欢迎在评论区交流 👏

相关推荐
ViavaCos1 小时前
pnpm v11 的安全策略,让我踩了个坑
前端
To_OC1 小时前
从一段定时器代码,重新捋清 JS 同步、异步与 Promise
前端·javascript·代码规范
持敬chijing1 小时前
Web渗透之前后端漏洞-XSS漏洞原理攻击防御全流程
前端·安全·web安全·网络安全·网络攻击模型·安全威胁分析·xss
程序员黑豆1 小时前
AI全栈开发 - Java:注释
前端·后端·ai编程
痕忆丶2 小时前
Typora 的替代marktext,marktext切换中文
前端
羊羊小栈2 小时前
Uplift营销供应链协同决策系统(基于Uplift因果推断与运筹优化算法)
前端·人工智能·算法·毕业设计·大作业
阿猫的故乡2 小时前
Vue组合式函数(Composables)从入门到实战:鼠标跟踪、请求封装、本地存储……全案例拆解
前端·vue.js·计算机外设
Upsy-Daisy2 小时前
Hermes Agent 学习笔记 02:安装、配置与第一次运行
java·前端·数据库
一壶纱2 小时前
一个用于 UniApp 项目的 Pinia 持久化插件
前端·javascript·vue.js