在数据结构的世界里,线性结构是最基础也最常用的一类,其中数组、栈、队列、链表更是前端开发者必须掌握的核心内容。本文将从数组特性出发,逐步拆解栈、队列、链表的设计思想、操作逻辑,并结合 JavaScript 代码实例,深入剖析它们的底层逻辑与使用场景。
一、线性数据结构的基础:数组
数组是最常见的线性数据结构,也是栈、队列的实现基础,理解数组的特性是掌握后续结构的关键。
1. 数组的核心特性
- 连续存储 + 下标访问:数组在内存中是连续分配的存储空间,通过下标(索引)可以直接访问元素,这让数组的读取操作效率极高(时间复杂度 O (1))。
- JS 数组的特殊性:JS 中的数组并非 “纯数组”—— 若数组内每一项类型一致,底层是连续内存;若类型不同,底层会用哈希分配空间,此时不再具备传统数组的连续特性。
2. 数组的增删操作
数组的增删方法(push、unshift、splice)均会修改原数组(非纯函数),且增删操作的时间复杂度为 O (n),因为元素移动的数量会随数组长度 n 线性增加。
代码示例:数组的增删改操作
javascript
运行
// 初始化数组
let arr = [1, 2, 3, 4];
// 1. push:向数组尾部添加元素(需注意数组扩容问题)
arr.push(5);
console.log(arr); // [1,2,3,4,5]
// 解析:push 若超出数组初始容量,JS 会自动扩容(重新申请内存、复制元素),带来额外开销
// 2. unshift:向数组头部添加元素(内存视角:所有元素需向后移动)
arr.unshift(0);
console.log(arr); // [0,1,2,3,4,5]
// 3. splice:删、增、改三合一(slice + replace)
// 语法:arr.splice(start, deleteCount, item1, item2...)
arr.splice(2, 1, 'a'); // 从索引2开始,删除1个元素,插入'a'
console.log(arr); // [0,1,'a',3,4,5]
// 4. 纯数组排序(结合6.js示例)
let numArr = [10,17,5,2,4];
// 注意:sort默认按ASCII码排序,必须传比较函数实现数值排序
numArr.sort((a,b) => a - b);
console.log(numArr); // [2,4,5,10,17]
// 解析:比较函数 (a,b)=>a-b 表示升序:若a-b<0,a排在b前;=0则位置不变;>0则b排在a前
// 若要降序,只需改为 (a,b)=>b-a
二、操作受限的数组:栈(Stack)
栈可以理解为 “特殊的数组”,核心特性是后进先出(LIFO, Last In First Out) ,仅能操作栈顶元素,如同羽毛球筒 —— 最后放进去的球最先拿出来。
1. 栈的核心操作
push:向栈顶添加元素(对应数组尾部追加);pop:从栈顶移除元素(对应数组尾部删除);peek:查看栈顶元素(对应arr[arr.length - 1]);- 遍历栈:通过
while(stack.length)循环,直到栈为空。
代码示例:栈的实现与使用
javascript
运行
// 用数组模拟栈
class Stack {
constructor() {
this.items = []; // 存储栈元素
}
// 入栈:向栈顶添加元素
push(element) {
this.items.push(element);
}
// 出栈:移除并返回栈顶元素
pop() {
if (this.isEmpty()) return null; // 栈空时返回null
return this.items.pop();
}
// 查看栈顶元素(不修改栈)
peek() {
if (this.isEmpty()) return null;
return this.items[this.items.length - 1];
}
// 判断栈是否为空
isEmpty() {
return this.items.length === 0;
}
// 获取栈长度
size() {
return this.items.length;
}
}
// 测试栈操作
const stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.peek()); // 3(栈顶元素)
console.log(stack.pop()); // 3(出栈)
console.log(stack.size()); // 2
// 遍历栈
while (stack.length) {
console.log(stack.pop()); // 依次输出2、1
}
三、操作受限的数组:队列(Queue)
队列同样是 “特殊的数组”,核心特性是先进先出(FIFO, First In First Out) ,如同排队买票 —— 队尾入队、队头出队。
1. 队列的核心操作
队列的操作受限为:push(队尾入队)、shift(队头出队),需注意 shift 操作会导致数组元素整体前移,时间复杂度为 O (n)。
代码示例:队列的实现与使用
javascript
运行
// 用数组模拟队列
class Queue {
constructor() {
this.items = []; // 存储队列元素
}
// 入队:向队尾添加元素
enqueue(element) {
this.items.push(element);
}
// 出队:移除并返回队头元素
dequeue() {
if (this.isEmpty()) return null;
return this.items.shift(); // 队头出队(数组头部删除)
}
// 查看队头元素
front() {
if (this.isEmpty()) return null;
return this.items[0];
}
// 判断队列是否为空
isEmpty() {
return this.items.length === 0;
}
// 获取队列长度
size() {
return this.items.length;
}
}
// 测试队列操作
const queue = new Queue();
queue.enqueue('A');
queue.enqueue('B');
queue.enqueue('C');
console.log(queue.front()); // A(队头元素)
console.log(queue.dequeue()); // A(出队)
console.log(queue.size()); // 2
四、灵活的线性结构:链表(Linked List)
链表是与数组互补的线性结构,解决了数组增删效率低的问题。它的节点在内存中非连续分布,通过 “指针”(next 属性)连接,核心由 head(头节点)、tail(尾节点,next 为 null)和中间节点组成。
1. 链表的核心特性
-
节点结构:每个节点包含
val(值)和next(指向下一节点的指针),JS 中用对象模拟:javascript
运行
// 链表节点示例 const node = { val: 1, next: { val: 2, next: null // 尾节点的next为null } }; -
head 节点的作用:访问链表任意元素必须从 head 开始,逐个遍历
next直到目标节点; -
性能特点:
- 增删操作:只需修改指针指向(时间复杂度 O (1)),无需移动元素;
- 访问操作:需遍历节点(时间复杂度 O (n)),无索引直接访问;
- 内存分配:每次新增节点都单独申请内存,平均开销稳定,适合大规模数据。
2. 链表 vs 数组
表格
| 特性 | 数组 | 链表 |
|---|---|---|
| 存储方式 | 连续(纯数组)/ 离散 | 离散 |
| 访问效率 | O (1)(索引) | O (n)(遍历) |
| 增删效率 | O (n)(移动元素) | O (1)(修改指针) |
| 内存开销 | 扩容时额外开销 | 每个节点存指针,略高 |
| 适用场景 | 小规模、频繁读取 | 大规模、频繁增删 |
3. 链表的增删操作
链表增删的核心是操作前驱节点的 next 指针,无需移动其他元素。
代码示例:链表节点的添加与删除
javascript
运行
// 定义链表节点类
class ListNode {
constructor(val) {
this.val = val;
this.next = null;
}
}
// 初始化链表:1 -> 2 -> 3
const head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
// 1. 向链表中间添加节点(在2和3之间插入4)
const newNode = new ListNode(4);
// 找到前驱节点(值为2的节点)
const prevNode = head.next;
// 修改指针:前驱节点的next指向新节点,新节点的next指向原后继节点
newNode.next = prevNode.next;
prevNode.next = newNode;
// 此时链表:1 -> 2 -> 4 -> 3
// 2. 删除链表节点(删除4)
prevNode.next = newNode.next; // 前驱节点直接指向4的后继节点(3)
newNode.next = null; // 释放被删除节点的指针
// 此时链表:1 -> 2 -> 3
// 遍历链表
function traverseLinkedList(head) {
let current = head;
while (current) {
console.log(current.val); // 依次输出1、2、3
current = current.next;
}
}
traverseLinkedList(head);
五、总结
线性数据结构是前端算法的基础,核心要点可归纳为:
- 数组是基础,优势是快速访问,劣势是增删需移动元素;
- 栈和队列是 “操作受限的数组”,分别遵循 LIFO 和 FIFO 规则;
- 链表弥补了数组增删效率低的问题,通过指针实现离散存储,优势是增删快,劣势是访问需遍历;
- JS 数组的特殊性(纯数组 / 哈希存储)、
sort方法的排序逻辑(需传比较函数)是实际开发中易踩坑的点。
掌握这些结构的底层逻辑,能帮助我们在实际开发中选择更合适的数据结构(如用栈处理嵌套结构、用队列处理任务排队、用链表处理高频增删场景),提升代码的性能与可读性。