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

前言:线性的世界

数组大家都不陌生------连续存储、下标直达,像一排整齐的储物柜,拿第 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]

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

额外一提:数组的 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. 选型建议:小规模、偏读取 → 数组;大规模、偏增删 → 链表。栈和队列是加了"使用规则"的特殊数组/链表,按场景选择。

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


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


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

相关推荐
deli00711 分钟前
1 个汉字算几个 token?实测中英文 prompt 与代码的编码差异
前端
无限聚核17 分钟前
抖音 certificate.prepare 之后怎么验券?和美团 msconsume 差在哪,无限聚核
前端·微信小程序
2603_9658966227 分钟前
Grid 网格布局|CSS 二维页面排版体系
前端·css
有味道的男人43 分钟前
搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案
服务器·前端·javascript
lovingsoft1 小时前
Chrome Lighthouse 详解
前端·chrome
Qlly2 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝2 小时前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦2 小时前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡2 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI2 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战