深入吃透前端线性数据结构:数组、栈、队列、链表核心原理与实战

目录


栈和队列

基于对数组的理解和掌握,线性数据结构(栈,队列,链表)

非线性数据结构,树 & 图

线性数据结构

数组和链表

数组开箱即用,采用连续内存存储,依靠下标可以直接访问元素。

链表的内存空间不连续,依靠指针串联各个数据单元。


栈和队列

栈与队列都属于操作受限的特殊线性结构,既可以基于数组实现,也可以基于链表实现。

栈和队列特性

栈和队列可以理解为操作受限的数组,二者对元素的读写位置做了严格约束:

  1. :仅能在栈顶完成元素操作,对应数组尾部。遵循 LIFO(后进先出) 规则。
  2. 队列 :队尾入队、队首出队。遵循 FIFO(先进先出) 规则。

除了数组,栈和队列也完全可以使用链表来实现。

灵活增删的数组

数组是 JS 中最常用的存储结构,pushunshiftsplice 是日常开发最常用的增删方法,下面结合内存与性能逐一分析。

增加元素的方法

push

在数组尾部添加元素。

数组初始化会分配固定容量,当 数组长度 >= 容量上限 时,会触发数组扩容 :重新申请更大内存、拷贝原有数据、释放旧空间,带来一定性能开销。

而链表动态申请内存,不存在扩容问题。

unshift

在数组头部添加元素。

从内存视角来看,头部新增元素需要移动数组内所有原有元素,数组长度越大,性能损耗越明显,不建议高频使用。

splice

集删除、新增、替换功能于一体,功能等同于 slice + 替换,也是面试高频方法。

标准语法(参考 MDN)

js 复制代码
array.splice(start_index, delete_count, ...items_to_add)
  • start_index:操作起始下标
  • delete_count:要删除的元素数量,传 0 代表只新增不删除
  • ...items_to_add:可选参数,需要插入的新元素

重要特性 :数组所有增删方法都不是纯函数 ,会直接修改原数组;splice 的返回值是被删除元素组成的数组,无元素删除时返回空数组。

代码演示:

js 复制代码
const arr = [1,2];
// 下标1位置插入3,不删除元素
console.log(arr.splice(1,0,3)); // []
console.log(arr); // [1,3,2]

// 删除下标1的元素
arr.splice(1,1);
console.log(arr); // [1,2]

栈 stack

栈是遵循 LIFO(Last In First Out 后进先出) 的特殊数组,也是操作受限的线性结构。

可以用生活场景理解:冰柜里的雪糕,后放进去的会摆在最上方,只能优先取出顶部雪糕。

栈核心操作

仅允许在数组尾部(栈顶)操作元素:

  • push:入栈,向栈顶添加元素
  • pop:出栈,从栈顶删除元素并返回该元素
  • 查看栈顶:stack[stack.length - 1]
  • 循环出栈经典写法:while(stack.length)

完整示例代码:

js 复制代码
// 定义栈,基于数组实现
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); // []

队列 queue

队列遵循 FIFO(First In First Out 先进先出) 规则,同样是操作受限的数组。

规则:队尾使用 push 入队,队首使用 shift 出队。

示例代码:

js 复制代码
// 定义队列
const queue = [];

// 队尾入队
queue.push('张三');
queue.push('李四');
queue.push('王五');

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

补充:数组结合 shift 实现队列存在性能问题,shift 会移动数组所有元素,数据量越大性能越差,大数据场景推荐使用链表实现队列。

链表

数组与链表性能&场景对比

数组进行增删操作时,若操作位置不在尾部,需要移动大量元素。假设数组长度为 n,增删操作需要移动的元素数量随数组长度线性增长,时间复杂度为 O(n)

  1. 数组

    • 内存:元素类型一致时,内存连续;存在初始化、扩容的内存申请开销
    • 优势:支持下标索引访问、遍历速度极快
    • 劣势:非尾部位置增删效率低,需要移动元素
    • 适用场景:数据规模小、频繁查询、少量增删
  2. 链表

    • 内存:每新增一个节点都需要单独申请内存,平均开销稳定;节点内存离散不连续
    • 优势:增删效率高,仅修改指针指向
    • 劣势:无下标,访问元素必须从头逐个遍历
    • 适用场景:数据规模大、频繁增删、查询操作少

基础概念

数组和链表本质都是有序列表,同属于线性结构:每个节点有且仅有一个前驱节点、一个后继节点。

  • 数组:依靠索引访问元素
  • 链表:依靠 head 头节点串联所有节点,结构为 head -> 普通节点 ... -> tail 尾节点

链表的最小单元是节点,每个节点包含数据域和指针域:

  • val:当前节点存储的数据
  • next:指针,指向下一个节点,尾节点的 nextnull

JS 中链表可以用嵌套对象表示:

js 复制代码
{
    val:1,
    next:{
        val:2,
        next: null
    }
}

访问规则:想要获取链表中任意元素,必须从 head 头节点开始 ,不断通过 next 指针向后遍历,直到找到目标节点;尾节点特征为 next = null

链表基础代码实现

js 复制代码
// 定义节点构造函数
function ListNode(val){
    this.val = val;
    this.next = null;
}

// 手动创建链表 1 -> 2
const node = new ListNode(1);
node.next = new ListNode(2);
console.log(node);
// {val: 1, next: {val: 2, next: null}}

JS 数组内存补充

很多人认为 JS 数组内存一定连续,这是误区:

  1. 数组每一项类型一致:底层按照传统数组存储,内存连续
  2. 数组每一项类型混杂 :内存不再连续,底层改用哈希表分配空间,失去连续数组的特性

代码示例:

js 复制代码
// 类型统一,连续内存存储
const arr = [1,2,3,4];

// 类型混杂,非连续内存,哈希表存储
const arr2 = ['a',1,{b:2}];
// 依旧可以通过下标正常访问
console.log(arr2[1],arr2[2]);

数组排序补充避坑

数组默认 sort 方法会按照 ASCII 码进行排序,数字排序会出错,数字排序必须传入自定义回调函数

js 复制代码
let arr = [10,2,5];
// 错误写法:默认ASCII排序,结果 [10, 2, 5]
// arr.sort(); 

// 正确写法:数字升序排序
arr.sort((o1,o2)=>o1-o2);
console.log(arr); // [ 2, 5, 10 ]

链表元素的添加

链表添加元素的本质是修改 next 指针

核心思路:先找到目标位置的前驱节点 ,修改前驱节点的 next 指向新节点,再将新节点的 next 指向原后继节点即可。

整个过程不需要移动任何数据,仅修改指针,增删动作的时间复杂度为 O(1)

删除

链表删除元素同样依靠修改指针实现。

核心思路:找到待删除节点的前驱节点 ,将前驱节点的 next 直接指向待删除节点的后继节点,断开与待删除节点的链接,完成删除操作。

总结:链表的痛点是「查找节点」需要遍历,时间复杂度 O(n);一旦定位到目标位置,增删操作效率极高。


全文总结

  1. 数组、栈、队列、链表都属于线性数据结构,元素呈一对一线性关系;树、图属于非线性数据结构,元素关系更复杂。
  2. 栈和队列并非独立存储结构,是操作受限的数组/链表:栈后进先出,队列先进先出。
  3. JS 数组 push/unshift/splice 都会修改原数组,不属于纯函数;unshift、数组结合 shift 实现队列,大数据场景性能较差。
  4. JS 数组内存并非一定连续,元素类型混杂时底层切换为哈希表存储。
  5. 数组查询快、增删慢,适合小数据、高频查询场景;链表增删快、查询慢,适合大数据、高频增删场景。
  6. 链表由节点和指针组成,增删仅需修改指针指向,无内存扩容、元素移动的开销。

核心知识点复盘

  1. 栈:基于数组实现,使用 push 入栈、pop 出栈,经典遍历方式 while(stack.length),遵循后进先出。
  2. 队列:基于数组实现,push 入队、shift 出队,遵循先进先出,大数据量下不推荐数组实现。
  3. splice:数组万能增删改方法,参数依次为起始下标、删除个数、新增元素,返回被删除元素组成的数组。
  4. 数组:随机访问 O(1),头部/中间增删 O(n);内存是否连续由元素类型决定。
  5. 链表:访问元素需要遍历 O(n),定位节点后增删 O(1),内存离散分布,动态申请空间无扩容问题。

常见问题/避坑指南

  1. 尽量避免高频使用 unshift,数组头部添加元素会移动所有元素,大数据量严重影响性能。
  2. 数组数字排序必须给 sort 传入回调函数,否则会按照 ASCII 码排序,结果不符合预期。
  3. 不要刻意混用数组元素类型,会让数组失去连续内存优势,降低访问性能。
  4. 海量数据场景下,不要用数组实现队列,shift 方法性能低效,优先选择链表实现队列。
  5. 误区纠正:栈和队列不是独立的数据存储结构,只是对操作规则做了限制的线性结构。
  6. 误区纠正:链表增删本身速度很快,耗时主要消耗在「遍历查找目标节点」环节。
  7. 误区纠正:所有数组原生增删方法都会修改原数组,splice 也不是纯函数。
相关推荐
ikoala1 小时前
Codex 不得不装的 12 个插件,都在这了
前端·javascript·后端
道友可好2 小时前
用 Linter 驾驭 AI:机械化执行的艺术
前端·人工智能·后端
流浪码农~2 小时前
Element Plus DatePicker 动态设置每周起始日
前端·vue.js·elementui
8Qi82 小时前
LeetCode 32:最长有效括号 —— 栈 + 标记法 题解
java·数据结构·算法·leetcode·职场和发展··括号匹配
jason_yang2 小时前
刚发版就背锅?前端版本控制就靠他version-rocket
前端
如果超人不会飞2 小时前
TinyVue NavMenu导航菜单组件使用指南
前端·vue.js
Jason_chen2 小时前
Linux 3.0 串口机制深度解析:传统8250驱动与基础RS-232/485支持
linux·前端
TPBoreas2 小时前
前端面试问题打靶
前端
赵庆明老师2 小时前
JS检查提交的文件是否合规
开发语言·前端·javascript