一.数组(Array)
最常用的数据结构之一,相信大家都很熟悉了,这里就不多讲
二.栈(Stack)
它的结构类似于数组,但和数组不同,他只能在结尾处添加和删除元素,即遵循先进后出原则(FILO),它的结尾被称之为栈顶,添加元素称之为压栈,删除称之为弹出
常见的应用,比如递归时的函数调用就是栈结构
数据结构实现:
class Stack {
// 声明私有变量使用#加变量名
#items = [];
// 压栈
push(...element) {
this.#items.push(...element);
}
// 弹出
pop() {
return this.#items.pop();
}
// 查询栈顶
peek() {
return this.#items[this.#items.length - 1];
}
// 查询是否为空
isEmpty() {
return this.#items.length === 0;
}
// 清空栈
clear() {
this.#items = [];
}
// 查询长度
size() {
return this.#items.length;
}
// 查询对象在栈中的位置
search(item) {
return this.#items.indexOf(item);
}
// 转为字符串
toString() {
return this.#items.toString()
}
}
三.队列(Queue)
和栈结构不同,队列遵循先进先出(FIFO)原则,即只能在数组的末尾添加元素,在数组的开头删除元素,在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
我们知道,数组的删除第一个元素,会让后面的所有元素往前移动,在队列中因为只会删除队头,即数组第一个元素,会导致性能的浪费,因此我们封装队列的时候不去用数组,而是使用对象来封装。
常见的应用,比如js的事件循环,发布订阅的消息列表,弹窗显示等
1.普通队列
class Queue {
constructor() {}
#items = {};
#count = 0;
#lowestCount = 0;
enqueue(...element) {
element.forEach(item => {
this.#items[this.#count] = item;
this.#count++
})
}
dequeue() {
if (this.isEmpty()) return
const head = this.#items[this.#lowestCount]
delete this.#items[this.#lowestCount]
// 注意,因为当前队头已经被删除了,所以需要将队头的指针++
this.#lowestCount++
return head;
}
front() {
return this.#items[this.#lowestCount]
}
isEmpty() {
return this.#count - this.#lowestCount === 0;
}
clear() {
this.#items = {};
}
size() {
return this.#count - this.#lowestCount;
}
}
2.优先队列
与普通队列不同,优先队列中的元素带有优先级,入队的时候,需要根据优先级插入到对应位置
这里写一个查询位置的方法好了,默认元素是一个数字类型, 实际中可能是对象,但代表优先级的属性八成也是数字,改改就行XD
class PriorityQueue {
#items = [];
search(item) {
// 值不为数字,报错
if (typeof item !== "number") {
throw new Error("PriorityQueue.search() only accepts numbers");
}
// 当队列为空时,直接返回0
if (this.#items.length === 0) {
console.log(0);
return 0;
}
// 当item大于队列中最大值时,返回队列长度
if (item >= this.#items[this.#items.length - 1]) {
return this.#items.length;
// 当item小于队列中最小值时,返回0
} else if (item <= this.#items[0]) {
return 0;
}
// 设置查找区域头尾指针
let i = this.#items.length;
let j = 0;
// 最终结果,一定是i<j
while (i >= j) {
let mid = Math.floor((i + j) / 2);
if (this.#items[mid] === item) return mid;
// 插入值小于中间值,则item小于中间值, 将中间值-1设为最大值
// 插入值大于中间值,则item大于中间值, 将中间值+1设为最小值
else if (item < this.#items[mid]) {
i = mid - 1
} else {
j = mid + 1;
}
}
return j;
}
enqueue(...arr) {
arr.forEach((item) => {
this.#items.splice(this.search(item), 0, item);
});
}
toString() {
return this.#items.toString();
}
}
偷个懒,这里使用的是数组(因为插入之后还是要循环并让所有的之后元素角标++),而且其他方法不加了
3.循环队列
循环队列即头尾相接的队列,当指针前进之后大于队列长度时,指针将回到队列头,实现则是改变出队的方法,将出队的元素再次入队即可
loopDequeue() {
const el = this.dequeue()
this.enqueue(el)
return el
}
4.双端队列
双端队列既可以在头部删除,在尾部添加,也可以在头部添加,在尾部删除,他和普通队列的不同也在添加删除的方法上
addFront(element) {
if (this.isEmpty()) {
this.#items[this.#count] = element;
this.#count++
return
}
this.#lowestCount--
this.#items[this.#lowestCount] = element;
}
removeFront() {
if (this.isEmpty()) return
const result = this.#items[this.#lowestCount]
delete this.#items[this.#lowestCount]
this.#lowestCount++
return result
}
peekFront() {
if (this.isEmpty()) return
return this.#items[this.#lowestCount]
}
addBack(element) {
this.#items[this.#count] = element;
this.#count++
}
removeBack() {
if (this.isEmpty()) return
const result = this.#items[this.#count - 1]
delete this.#items[this.#count - 1]
this.#count--
return result
}
peekBack() {
if (this.isEmpty()) return
return this.#items[this.#count - 1]
}
三.链表(linkedList)
与js不同,很多的语言中,数组的长度是无法改变的,插入和删除数组中的元素,往往需要创建一个新的数组,而链表则在插入和删除时非常灵活,可以在任意位置去做这两个操作,js中,使用unshift方法也是会让原数组所有元素向后移动,造成大量的性能损耗,例如上面的队列,在这种时候,我们就可以选择链表
1.单向链表
顾名思义,单向链表只有一个头和一个next指针,只能顺着这个头向下遍历
class linkList {
constructor() {
this.header = null;
this.count = 0;
}
push(element) {
const node = new Node(element);
if (this.header === null) {
this.header = node;
// 当链表中已经有节点的时候,需要拿到最后一个节点,此时我们可以循环,并遍历链表获取最后一个节点,将其next设置为新增节点即可
} else {
let currentNode = this.header;
while (currentNode.next) {
// 不断将下一个节点赋值给currentNode变量,当currentNode变量的next属性为null时,说明是最后一个节点,跳出循环并添加节点
currentNode = currentNode.next;
}
currentNode.next = node;
}
}
removeNode(index) {
if (index < 0) return;
else if (index === 0) {
const d = this.header;
this.header = this.header.next;
return d;
} else {
let i = 0;
let currentNode = this.header;
while (currentNode.next) {
if (i === index - 1) {
const d = currentNode.next;
currentNode.next = currentNode.next.next;
return d;
} else {
i++;
currentNode = currentNode.next;
}
}
}
}
removeData(index) {
if (index < 0) return;
else if (index === 0) {
const d = this.header.data;
this.header.data = null;
return d;
} else {
let i = 0;
let currentNode = this.header;
while (currentNode.next) {
if (i === index) {
const d = currentNode.data;
currentNode.data = null;
return d;
} else {
i++;
currentNode = currentNode.next;
}
}
}
}
}
2.双向链表
双向链表的头尾都可以开始遍历,他的节点也会多出prev指针,和next不同,分别指向上一个和下一个节点
// 除了next指针,新加prev指针
class DNode extends Node {
constructor(element) {
super(element)
this.prev = null
}
}
// 新加#footer私有属性,用于记录链表的尾部
class DLinkList {
#footer
#header
#count
constructor() {
this.#footer = null
this.#header = null
this.#count = 0
}
search(index) {
const flag = this.size() / 2 >= index
if(!index) {
return this.#header
} else if (index > 0) {
let currentNode = flag ? this.#footer : this.#header
// 从前往后的角标,前后往前应该是最后一个角标-当前角标即 size - 1 - index
let num = flag ? (this.size() - 1 - index) : index
if(index === (this.size() - 1)) return this.#footer
for (let i = 0; i < num; i++) {
currentNode = flag ? currentNode.prev : currentNode.next
}
return currentNode
}
}
push(data) {
const node = new DNode(data)
if(this.isEmpty()) {
this.#header = node
} else {
const currentNode = this.#footer
currentNode.next = node
node.prev = currentNode
}
this.#footer = node
this.#count++
}
insert(data, index) {
const getEmptyNode = () => new DNode(null)
if(index < 0) throw new Error('index必须大于0,而你传入的index为' + index)
if(this.isEmpty()) {
this.push(null)
}
// 当需要插入的index<size&&index>0时,意味着此时的插入将不会导致footer指针改变,
// 而index>=size时,footer将改变,插入元素必定是队尾
if(index < this.size()) {
if(index === 0) {
this.unshift(data)
} else {
const node = new DNode(data)
let prevNode = this.search(index - 1)
console.log('prevNode', prevNode);
let currentNode = prevNode.next
prevNode.next = node
node.prev = prevNode
node.next = currentNode
currentNode.prev = node
this.#count++
}
} else {
for (let i = (this.size() - 1); i < index; i++) {
i === index - 1 ? this.push(data) : this.push(null)
}
}
}
unshift(data) {
if(this.isEmpty()) return
let currentNode = this.#header
if(this.size() === 1) {
this.#header = null
this.#footer = null
} else {
currentNode.next.prev = null
this.#header = currentNode.next
}
this.#count--
return currentNode
}
pop() {
if(this.isEmpty()) return
let currentNode = this.#footer
if(this.size() === 1) {
this.#header = null
this.#footer = null
} else {
currentNode.prev.next = null
this.#footer = currentNode.prev
}
this.#count--
return currentNode
}
shift() {
if(this.isEmpty()) return
let currentNode = this.#header
if(this.size() === 1) {
this.#header = null
this.#footer = null
} else {
currentNode.next.prev = null
this.#header = currentNode.next
}
this.#count--
return currentNode
}
removeAt(index) {
if(this.isEmpty()) return
if(index >= this.size() || index < 0) return
else if (index === (this.size() - 1)) return this.pop()
else if (index === 0) return this.shift()
else {
const targetNode = this.search(index)
targetNode.prev.next = targetNode.next
targetNode.next.prev = targetNode.prev
this.#count--
return targetNode
}
}
isEmpty() {
return this.#count === 0
}
size() {
return this.#count
}
}
3.单向循环链表
单向循环链表的特点是最后一个节点next指向第一个节点,双向则还有第一个节点的prev指向最后一个节点,这里封装一个单向的,偷下懒,只写主要功能XD
class CLinkList {
constructor() {}
#header = null
#count = 0
search(index) {
if(!this.#header || index < 0) return
let currentNode = this.#header
index = index % this.#count
if(index === 0) return currentNode
else if(index < this.#count) {
for(let i = 0; i < index; i++) {
currentNode = currentNode.next
}
}
return currentNode
}
push(data) {
const node = new Node(data)
if(!this.#header) this.#header = node
else {
const currentNode = this.search(this.#count-1)
currentNode.next = node
}
node.next = this.#header
this.#count++
}
insert(data,index) {
if(index < 0) return
if(!this.#header) return this.push(data)
else {
const node = new Node(data)
if(index % this.#count === 0) {
const footerNode = this.search(this.#count-1)
footerNode.next = node
node.next = this.#header
this.#header = node
} else {
const prevNode = this.search(index-1)
const currentNode = prevNode.next
prevNode.next = node
node.next = currentNode
}
this.#count++
}
}
removeAt(index) {
if(!this.#header || index < 0) return
let currentNode
if(index % this.#count === 0) {
currentNode = this.#header
if(this.#count === 1) {
this.#header = null
} else {
const prevNode = this.search(this.#count-1)
prevNode.next = this.#header.next
this.#header = this.#header.next
}
} else {
const prevNode = this.search(index-1)
currentNode = prevNode.next
prevNode.next = currentNode.next
}
this.#count--
return currentNode
}
}