目录
栈和队列
基于对数组的理解和掌握,线性数据结构(栈,队列,链表)
非线性数据结构,树 & 图
线性数据结构
数组和链表
数组开箱即用,采用连续内存存储,依靠下标可以直接访问元素。
链表的内存空间不连续,依靠指针串联各个数据单元。
栈和队列
栈与队列都属于操作受限的特殊线性结构,既可以基于数组实现,也可以基于链表实现。
栈和队列特性
栈和队列可以理解为操作受限的数组,二者对元素的读写位置做了严格约束:
- 栈 :仅能在栈顶完成元素操作,对应数组尾部。遵循 LIFO(后进先出) 规则。
- 队列 :队尾入队、队首出队。遵循 FIFO(先进先出) 规则。
除了数组,栈和队列也完全可以使用链表来实现。
灵活增删的数组
数组是 JS 中最常用的存储结构,push、unshift、splice 是日常开发最常用的增删方法,下面结合内存与性能逐一分析。
增加元素的方法
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)。
-
数组
- 内存:元素类型一致时,内存连续;存在初始化、扩容的内存申请开销
- 优势:支持下标索引访问、遍历速度极快
- 劣势:非尾部位置增删效率低,需要移动元素
- 适用场景:数据规模小、频繁查询、少量增删
-
链表
- 内存:每新增一个节点都需要单独申请内存,平均开销稳定;节点内存离散不连续
- 优势:增删效率高,仅修改指针指向
- 劣势:无下标,访问元素必须从头逐个遍历
- 适用场景:数据规模大、频繁增删、查询操作少
基础概念
数组和链表本质都是有序列表,同属于线性结构:每个节点有且仅有一个前驱节点、一个后继节点。
- 数组:依靠索引访问元素
- 链表:依靠
head头节点串联所有节点,结构为head -> 普通节点 ... -> tail 尾节点
链表的最小单元是节点,每个节点包含数据域和指针域:
val:当前节点存储的数据next:指针,指向下一个节点,尾节点的next为null
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 数组内存一定连续,这是误区:
- 数组每一项类型一致:底层按照传统数组存储,内存连续
- 数组每一项类型混杂 :内存不再连续,底层改用哈希表分配空间,失去连续数组的特性
代码示例:
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);一旦定位到目标位置,增删操作效率极高。
全文总结
- 数组、栈、队列、链表都属于线性数据结构,元素呈一对一线性关系;树、图属于非线性数据结构,元素关系更复杂。
- 栈和队列并非独立存储结构,是操作受限的数组/链表:栈后进先出,队列先进先出。
- JS 数组
push/unshift/splice都会修改原数组,不属于纯函数;unshift、数组结合shift实现队列,大数据场景性能较差。 - JS 数组内存并非一定连续,元素类型混杂时底层切换为哈希表存储。
- 数组查询快、增删慢,适合小数据、高频查询场景;链表增删快、查询慢,适合大数据、高频增删场景。
- 链表由节点和指针组成,增删仅需修改指针指向,无内存扩容、元素移动的开销。
核心知识点复盘
- 栈:基于数组实现,使用
push入栈、pop出栈,经典遍历方式while(stack.length),遵循后进先出。 - 队列:基于数组实现,
push入队、shift出队,遵循先进先出,大数据量下不推荐数组实现。 - splice:数组万能增删改方法,参数依次为起始下标、删除个数、新增元素,返回被删除元素组成的数组。
- 数组:随机访问 O(1),头部/中间增删 O(n);内存是否连续由元素类型决定。
- 链表:访问元素需要遍历 O(n),定位节点后增删 O(1),内存离散分布,动态申请空间无扩容问题。
常见问题/避坑指南
- 尽量避免高频使用
unshift,数组头部添加元素会移动所有元素,大数据量严重影响性能。 - 数组数字排序必须给
sort传入回调函数,否则会按照 ASCII 码排序,结果不符合预期。 - 不要刻意混用数组元素类型,会让数组失去连续内存优势,降低访问性能。
- 海量数据场景下,不要用数组实现队列,
shift方法性能低效,优先选择链表实现队列。 - 误区纠正:栈和队列不是独立的数据存储结构,只是对操作规则做了限制的线性结构。
- 误区纠正:链表增删本身速度很快,耗时主要消耗在「遍历查找目标节点」环节。
- 误区纠正:所有数组原生增删方法都会修改原数组,
splice也不是纯函数。