数组、链表、栈、队列四种数据结构……
一 数组
数组是最简单、最为人熟悉的数据结构,是一种线性表的顺序存储结构,它的特点是用一组地址连续的存储单元依次存储数据元素。
在 js 中,数组里的元素可以是不同类型的数据。
数组的特性
- 查询快:数组是线性查存储,通过元素的下标,就可以方便的更新元素和查找元素,其时间复杂度为
O(1)。 - 增删慢:数组的长度是固定的,我们想要增加/删除一个元素,需要移动该元素后的其他元素,其时间复杂度为
O(n)。(js 语言中数组可以扩容,长度不是固定的)
二 链表(LinkedList)
链表虽是一种线性结构,但其存储方式是不连续的,而是将零散的内存块串联在一起。
链表相关的名词
| 名词 | 说明 |
|---|---|
| 首元节点 | 链表中存储第一个数据元素的节点 |
| 头节点 | 首元节点之前的一个节点,其 next 引用指向首元节点 |
| 头指针 | 指向链表中的第一个节点的指针 |
链表存储方式为,以 head 为头节点,头节点是链表的开始,可以不存放数据,代表链表的第一个节点,每个节点都有一个 next 的向下引用,指向下一个节点,直到最后一个节点。
链表在内存中存在的特性是,每个内存块成为一个节点,除了存储数据,还会存储指向下一个节点的指针,也即每个节点由 data(数据)和 next(指向下个节点的引用)组成。
链表的特性
- 增删快:链表增删只需要改变指针指向,复杂度为
O(1)。 - 查询慢:随机访问需要每次从头开始遍历,复杂度为
O(n)。 - 相对数组而言,要保存指针使占用的内存空间较大。
1. 单向链表
单向链表有两个特殊的节点,即首节点和尾节点,首节点表示链表起始点,尾节点表示链表结束。
2. 单向循环链表
3. 双向链表
每个节点有两个指针,指向前一个节点地址的前驱指针prev和后一个节点的后继指针next。首节点的前驱指针和尾节点的后继指针均指向空地址。
双向链表的特点
- 相较单向链表,要存两个指针使消耗的内存空间更多。
- 增删操作比单向链表更快。
- 对于一个有序链表,双向链表的按值查询效率比单链表要高。
4. 双向循环链表
5. js 代码实现链表
// 创建节点的类
class Node {
constructor(element) {
this.element = element;
this.next = null;
}
}
// 链表类
class LinkList {
constructor() {
this.length = 0;
this.head = null;
}
// 追加 若链表为空,则设置head, 若不为空,则将尾节点的next指向element,length加1
append(element) {
let node = new Node(element);
let current;
if (!this.head) {
this.head = node;
} else {
current = this.head;
while(current.next) {
current = current.next
}
current.next = node;
}
++this.length;
}
/*
* 若 position 为0,将 element 的指针指向 head
* 若不为0,设置 previous 和 next 的指向
* length 加 1
*/
insert(position, element) {
let current;
let previous;
let index = 0;
let node = new Node(element);
if (position >= 0 && position <= this.length) {
if (position === 0) {
node.next = this.head;
this.head = node;
} else {
current = this.head;
while(index < position) {
previous = current;
current = current.next;
index++;
}
node.next = current;
previous.next = node;
}
this.length++;
}
}
/*
* 根据索引删除
* 改变 position - 1 处的指针指向
*/
removeAt(position) {
let current;
let previous;
let index = 0;
if (position > -1 && position < this.length) {
current = this.head;
if (position === 0) {
this.head = current.next;
} else {
while(index < position) {
previous = current;
current = current.next;
index++;
}
previsou.next = current.next
}
--this.length;
return true;
}
return false;
}
/*
* 删除元素
*/
remove(element) {
let index = this.indexOf(element);
this.removeAt(index);
}
/*
* 查找索引
*/
indexOf(element) {
let index = 0;
let current = this.head;
while(current) {
if(current.element === element) {
return index
}
current = current.next;
index++;
}
return -1;
}
/*
* 链表是否为空
*/
isEmpty() {
return this.length === 0;
}
/*
* 查看链表长度
*/
size() {
return this.length;
}
}
三 栈(stack)
栈只允许在有序的线性数据集合的一端(栈顶)进行增删操作,按照后进先出的原理运作。
栈的复杂度
- 访问:
O(n) - 增删:
O(1)
栈溢出
栈溢出是指定义的数据所需的内存超过了执行栈的最大存储范围,此时系统会抛出错误。比如死循环,数据量较大的递归。
可使用尾递归来优化递归导致的栈溢出。
数组实现的栈
// 栈类
class Stack() {
constructor() {
this.data = [];
}
// 入栈方法
push(element) {
this.data.push(element);
return this.data.length;
}
// 出栈方法
pop() {
if(this.data.length) {
this.data.pop();
return this.data.length;
}
return false;
}
// 查询栈顶方法
peek() {
return this.data[this.data.length - 1];
}
// 是否为空
isEmpty() {
return this.data.length === 0;
}
// 清空栈
clear() {
this.data = [];
}
}
四 队列(Queue)
队列按照先进先出的原理运作,只允许在尾端进行添加操作,在头部进行删除操作。
队列的复杂度
- 访问:
O(n) - 增删:
O(1)
队列溢出
- 真溢出:由于存储空间不够而产生的溢出。可使用扩容的方式解决。
- 假溢出:队列中尚余足够的空间,但元素不能入队。一般是由队列的存储结构或操作方式不当所致。如下图所示:
由于只能从队尾添加元素,所以不能入队,解决方法是,删除一个元素后,所有元素应向前移一位。
数组实现的队列
// 队列
class Queue {
constructor() {
this.data = [];
}
// 入队
push(element) {
this.data.push(element);
return this.data.length;
}
// 出队
shift() {
if(this.data.length) {
this.data.shift();
return this.data.length;
}
}
// 队首元素
peek() {
return this.data[0];
}
// 清空队列
clear() {
this.data = [];
}
// 队列是否为空
isEmpty() {
return this.data.length === 0;
}
}