数据结构
本文中部分内容来自于极客时间王争老师的专栏《数据结构与算法之美》
栈(Stack)
概念
栈(stack)又名堆栈,它是一种运算受限
的线性
表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶
,相对地,把另一端称为栈底
。向一个栈插入新元素又称作进栈
、入栈
或压栈
,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈
或退栈
,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。具备后进先出(LIFO)的特性。
JavaScript 中的实现
JavaScript 中的数组实现了栈的特性。
let stack = [1, 2, 3]
// 入栈
stack.push(4) // 4
// 出栈
stack.pop() // 4
下面我们也可以来实现 一个自己的栈
class Stack {
constructor() {
this.items = []
}
push(item) {
this.items.push(item)
}
pop() {
return this.items.pop()
}
peek() {
return items[this.items.length - 1]
}
isEmpty() {
return this.items.length === 0
}
clear() {
this.items = []
}
size() {
return this.items.length
}
toString() {
return this.items.toString()
}
}
队列(Queue)
概念
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个元素称为入队,删除一个元素称为出队。因为队列只允许在一端进行插入,在另一端进行删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出线性表(FIFO——first in first out)
顺序队列
下面是一个以数组方式实现的一个简单的顺序队列,功能是实现了但是我认为这与队列的定义还是有些区别,因为没有指针的体现:
const Queue = (function () {
let items = []
class Queue {
constructor() {
// 队列的数据
items = []
}
// 向队列的尾部添加新的项
enqueue(item) {
items.push(item)
}
// 移除队列头部项并返回
dequeue() {
return items.shift()
}
// 返回队列最前面的项
front() {
return items[0]
}
// 返回队尾项
rear() {
return items[items.length - 1]
}
// 队列是否为空
isEmpty() {
return items.length === 0
}
// 队列长度
size() {
return items.length
}
// 清空队列
clear() {
items = []
}
// 查看队列中所有元素
toString() {
return items.toString()
}
}
return Queue
})()
const queue = new Queue()
queue.enqueue(1)
queue.enqueue(2)
queue.front()
queue.rear()
queue.size()
queue.isEmpty()
queue.toString()
queue.dequeue()
queue.clear()
下面是一个完全按照定义来实现的队列:
class Queue {
constructor(n) {
// 队列的数据
this.items = []
// 队头下标
this.head = 0
// 队尾下标
this.tail = 0
// 队列大小
this.n = n
}
// 向队列的尾部添加新的项
enqueue(item) {
// tail 等于 size 代表队列已满
// if (this.tail === this.n) return false
// this.items[this.tail] = item
// this.tail++
// return true
// 数据搬移进行优化,避免数组还有空闲但无法添加数据的情况
if (this.tail === this.n) {
// 队列到末尾了,没有空间了
if (this.head === 0) return false // 整个队列都占满了
// 数据搬移
for (let i = this.head; i < this.tail; i++) {
this.items[i - this.head] = this.items[i]
delete this.items[i]
}
// 数据搬移完成之后重新更新 head 和 tail
this.tail -= this.head
this.head = 0
}
this.items[this.tail] = item
this.tail++
return true
}
// 移除队列头部项并返回
dequeue() {
// head 等于 tail 代表队列为空
if (this.head === this.tail) return null
let res = this.items[this.head]
delete this.items[this.head]
this.head++
return res
}
// 返回队列最前面的项
front() {
return this.items[this.head]
}
// 返回队尾项
rear() {
return this.items[this.tail]
}
// 队列是否为空
isEmpty() {
return this.tail === this.head
}
// 队列长度
size() {
return this.n
}
// 清空队列
clear() {
this.items = []
this.head = 0
this.tail = 0
}
// 查看队列中所有元素
toString() {
return this.items.toString()
}
}
const queue = new Queue(10)
queue.enqueue(1)
queue.enqueue(2)
queue.front()
queue.rear()
queue.size()
queue.isEmpty()
queue.toString()
queue.dequeue()
queue.clear()
当front = rear时,队列中没有任何元素,称为空队列。当rear增加到指向分配的连续空间之外时,队列无法再插入新元素,但这时往往还有大量可用空间未被占用,这些空间是已经出队的队列元素曾经占用过得存储单元。虽然数据搬移是一种解决方案,但是入队性能会受到影响,在实际使用队列时,为了使队列空间能高效的重复使用,一般使用循环队列。
循环队列
循环队列,顾名思义,它长得像一个环。原本数组是有头有尾的,是一条直线。现在我们把首尾相连,扳成了一个环。
循环队列就是将队列存储空间的最后一个位置绕到第一个位置,形成逻辑上的环状空间,供队列循环使用。在循环队列结构中,当存储空间的最后一个位置已被使用而再要进入队运算时,只需要存储空间的第一个位置空闲,便可将元素加入到第一个位置,即将存储空间的第一个位置作为队尾。 循环队列可以更简单防止伪溢出的发生,但队列大小是固定的。
在循环队列中,当队列为空时,有 front = rear,而当所有队列空间全占满时,也有 front = rear。为了区别这两种情况,规定循环队列最多只能有MaxSize - 1 个队列元素,当循环队列中只剩下一个空存储单元时,队列就已经满了。因此,队列判空的条件是front = rear,而队列判满的条件是 front =(rear + 1) % MaxSize。
class CircularQueue {
constructor(n) {
// 队列的数据
this.items = []
// 队头下标
this.head = 0
// 队尾下标
this.tail = 0
// 队列大小
this.n = n
}
// 向队列的尾部添加新的项
enqueue(item) {
// 队列满了
if ((this.tail + 1) % this.n === this.head) {
return false
} else {
this.items[this.tail] = item
this.tail = (this.tail + 1) % this.n
return true
}
}
// 移除队列头部项并返回
dequeue() {
if (this.tail === this.head && !this.items[this.head]) {
// 队列为空
return false
} else {
let res = this.items[this.head]
delete this.items[this.head]
this.head = (this.head + 1) % this.n
return res
}
}
// 返回队列最前面的项
front() {
return this.items[this.head]
}
// 返回队尾项
rear() {
let rear = this.tail - 1
return this.items[rear < 0 ? this.n - 1 : rear]
}
// 队列是否为空
isEmpty() {
return this.tail === this.head
}
// 队列长度
size() {
return this.n
}
// 清空队列
clear() {
this.items = []
this.head = 0
this.tail = 0
}
// 查看队列中所有元素
toString() {
return this.items.toString()
}
}
const queue = new CircularQueue(10)
queue.enqueue(1)
queue.enqueue(2)
queue.front()
queue.rear()
queue.size()
queue.isEmpty()
queue.toString()
queue.dequeue()
queue.clear()
链表队列
基于链表实现的队列,我们同样需要两个指针:head 指针和 tail 指针。它们分别指向链表的第一个结点和最后一个结点。入队时,tail->next= new_node, tail = tail->next;出队时,head = head->next。
/**
* 单向链表顺序队列
*/
class LinkedListQueue {
constructor(n) {
// 队列的数据
this.list = new SinglyLinkedList() // 见链表单链表代码实现
// 队头下标
this.head = this.list.head
// 队尾下标
this.tail = this.list.head
// 队列大小
this.n = n
}
// 向队列的尾部添加新的项
enqueue(item) {
if (this.list.getLength() === this.n) return false
this.list.append(item)
this.tail = this.list.findLast()
return true
}
// 移除队列头部项并返回
dequeue() {
// head 等于 tail 代表队列为空
if (this.head === this.tail) return null
let data = this.head.next.data
this.head = this.head.next
this.list.remove(data)
return true
}
// 返回队列最前面的项
front() {
return this.head
}
// 返回队尾项
rear() {
return this.tail
}
// 队列是否为空
isEmpty() {
return this.tail === this.head
}
// 队列长度
size() {
return this.n
}
// 清空队列
clear() {
this.list = new SinglyLinkedList()
this.head = this.list.head
this.tail = this.list.head
}
// 查看队列中所有元素
toString() {
return this.list.display()
}
}
const llq = new LinkedListQueue(2)
llq.enqueue(1)
llq.enqueue(2)
llq.enqueue(3)
llq.toString()
llq.dequeue()
llq.dequeue()
llq.dequeue()
llq.toString()
链表(Linked List)
概念
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。
单链表
单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。
/**
* @class
* 单链表节点
*/
class Node {
constructor(data) {
// 节点的数据域
this.data = data
// 下一个节点的指针域
this.next = null
}
}
/**
* @class
* 单链表类
*/
class SinglyLinkedList {
constructor() {
// 表头节点
this.head = new Node('head')
// 链表的长度
this.size = 0
// 当前节点位置
this.currentNode = this.head
}
/**
* 查找元素所在的节点
* @param {*} item 查找的元素
* @return {Node} 节点对象
*/
find(item) {
let currentNode = this.head
while (currentNode && currentNode.data !== item) {
// 没找到当前节点,就切换到下一个节点继续找,直到找到为止
currentNode = currentNode.next
}
return currentNode
}
/**
* 获取链表的最后一个节点
*/
findLast() {
let currentNode = this.head
while (currentNode.next) {
currentNode = currentNode.next
}
return currentNode
}
/**
* 查找当前节点的前一个节点
* @param {*} item 当前节点信息
*/
findPre(item) {
// 从最开头开始找
let currentNode = this.head;
while (currentNode && currentNode.next && currentNode.next.data !== item) {
if (currentNode.next) {
currentNode = currentNode.next;
} else {
currentNode = null;
}
}
return currentNode;
}
/**
* 在链表的尾部添加元素
* @param {*} item 需要插入的节点
*/
append(item) {
let currentNode = this.findLast()
if (!currentNode) return
currentNode.next = new Node(item)
this.size++
}
/**
* 向链表中插入元素
* @param {*} item 需要插入的节点
* @param {*} preItem 前一个节点(可以不传这个参数)
*/
insert(item, preItem) {
if (preItem) {
// 插入到某个元素之后
let preNode = this.find(preItem)
if (!preNode) return // 没有找到这个节点
let newNode = new Node(item)
// 前一个节点原来的下一个节点变成了新节点的下一个节点
newNode.next = preNode.next
// 前一个节点现在的下一个节点变成了新节点
preNode.next = newNode
this.size++
} else {
// 默认插入到最后一个元素
this.append(item)
}
}
/**
* 在链表中删除一个节点
* @param {*} item 节点元素
*/
remove(item) {
// 找到当前节点
let currentNode = this.find(item)
if (!currentNode) {
// 找不到此节点
return
}
if (item === 'head') {
// 想要移除头节点
if (this.isEmpty() === false) {
// 不是空链,不允许删除头节点
return;
} else {
// 是空链则重置
this.clear();
return;
}
}
// 找到该节点的上一个节点
let preNode = this.findPre(item)
if (!preNode) {
// 找不到此节点
return
}
if (currentNode.next) {
// 当前节点的下一个节点如果存在
preNode.next = currentNode.next
} else {
preNode.next = null
}
this.size--
}
/**
* 判断链表是否为空
* @returns {boolean}
*/
isEmpty() {
return this.size === 0
}
/**
* 获取链表的长度
* @returns {Number}
*/
getLength() {
return this.size
}
/**
* 从当前节点向前移动 n 个节点
* @param {Number} n 移动的节点数
*/
advance(n) {
while ((n--) && this.currentNode.next) {
this.currentNode = this.currentNode.next;
}
return this.currentNode;
}
/**
* 显示当前节点
* @returns {Node} 当前节点
*/
show() {
return this.currentNode
}
/**
* 链表的遍历显示
* @returns {String}
*/
display() {
let res = '';
let currentNode = this.head;
while (currentNode) {
res += currentNode.data;
currentNode = currentNode.next;
if(currentNode) {
res += '->';
}
}
console.log(res);
return res
}
/**
* 清空链表
*/
clear() {
this.head.next = null;
this.currentNode = this.head
this.size = 0;
}
}
const sll = new SinglyLinkedList()
sll.display()
sll.append(1)
sll.append(3)
sll.append(4)
sll.insert(2, 1)
sll.remove(3)
sll.display()
双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
/**
* @class
* 双向链表节点
*/
class Node {
constructor(data) {
// 节点的数据域
this.data = data
// 下一个节点的指针域
this.next = null
// 上一个节点的指针域
this.prev = null
}
}
/**
* @class
* 双向链表类
*/
class DoublyLinkedList {
constructor() {
// 表头节点
this.head = new Node('head')
// 链表的长度
this.size = 0
// 当前节点位置
this.currentNode = this.head
}
/**
* 查找元素所在的节点
* @param {*} item 查找的元素
* @return {Node} 节点对象
*/
find(item) {
let currentNode = this.head
while (currentNode && currentNode.data !== item) {
// 没找到当前节点,就切换到下一个节点继续找,直到找到为止
currentNode = currentNode.next
}
return currentNode
}
/**
* 获取链表的最后一个节点
*/
findLast() {
let currentNode = this.head
while (currentNode.next) {
currentNode = currentNode.next
}
return currentNode
}
/**
* 查找当前节点的前一个节点
* @param {*} item 当前节点信息
*/
findPre(item) {
// 从最开头开始找
let currentNode = this.find(item)
let preNode = null
if (currentNode && currentNode.prev) {
preNode = currentNode.prev
}
return preNode
}
/**
* 在链表的尾部添加元素
* @param {*} item 需要插入的节点
*/
append(item) {
let currentNode = this.findLast()
if (!currentNode) return
let newNode = new Node(item)
newNode.prev = currentNode
currentNode.next = newNode
this.size++
}
/**
* 向链表中插入元素
* @param {*} item 需要插入的节点
* @param {*} preItem 前一个节点(可以不传这个参数)
*/
insert(item, preItem) {
if (preItem) {
// 插入到某个元素之后
let preNode = this.find(preItem)
if (!preNode) return // 没有找到这个节点
let nextNode = preNode.next
let newNode = new Node(item)
// 前一个节点原来的下一个节点变成了新节点的下一个节点
newNode.next = nextNode
// 前一个节点变成新节点的上一个节点
newNode.prev = preNode
if (nextNode) {
// 如果下一个节点存在,那么下一个节点的前一个节点就是新的节点
nextNode.prev = newNode
}
// 前一个节点现在的下一个节点变成了新节点
preNode.next = newNode
this.size++
} else {
// 默认插入到最后一个元素
this.append(item)
}
}
/**
* 在链表中删除一个节点
* @param {*} item 节点元素
*/
remove(item) {
// 找到当前节点
let currentNode = this.find(item)
if (!currentNode) {
// 找不到此节点
return
}
if (item === 'head') {
// 想要移除头节点
if (this.isEmpty() === false) {
// 不是空链,不允许删除头节点
return;
} else {
// 是空链则重置
this.clear();
return;
}
}
// 找到该节点的上一个节点
let preNode = currentNode.prev
if (!preNode) {
// 找不到此节点
return
}
if (currentNode.next) {
// 当前节点的下一个节点如果存在
preNode.next = currentNode.next
currentNode.next.prev = preNode
} else {
preNode.next = null
}
this.size--
}
/**
* 判断链表是否为空
* @returns {boolean}
*/
isEmpty() {
return this.size === 0
}
/**
* 获取链表的长度
* @returns {Number}
*/
getLength() {
return this.size
}
/**
* 从当前节点向前移动 n 个节点
* @param {Number} n 移动的节点数
*/
advance(n) {
while ((n--) && this.currentNode.next) {
this.currentNode = this.currentNode.next;
}
return this.currentNode;
}
/**
* 从当前节点往回移动 n 个节点
* @param {Number} n 移动的节点数
*/
back(n) {
while ((n--) && this.currentNode.prev) {
this.currentNode = this.currentNode.prev;
}
return this.currentNode;
}
/**
* 显示当前节点
* @returns {Node} 当前节点
*/
show() {
return this.currentNode
}
/**
* 链表的遍历显示
* @returns {String}
*/
display() {
let res = '';
let currentNode = this.head;
while (currentNode) {
res += currentNode.data;
currentNode = currentNode.next;
if(currentNode) {
res += '->';
}
}
console.log(res);
return res
}
/**
* 链表反向的遍历显示
* @returns {String}
*/
displayReverse() {
let res = '';
let currentNode = this.findLast();
while (currentNode) {
res += currentNode.data;
currentNode = currentNode.prev;
if(currentNode) {
res += '->';
}
}
console.log(res);
return res
}
/**
* 清空链表
*/
clear() {
this.head.next = null;
this.currentNode = this.head
this.size = 0;
}
}
const dll = new DoublyLinkedList()
dll.append(1)
dll.append(2)
dll.append(3)
dll.append(4)
dll.append(5)
dll.insert(6, 5)
dll.display()
dll.displayReverse()
循环链表
循环链表是另一种形式的链式存贮结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。
/**
* @class
* 双向链表节点
*/
class Node {
constructor(data) {
// 节点的数据域
this.data = data
// 下一个节点的指针域
this.next = null
// 上一个节点的指针域
this.prev = null
}
}
/**
* @class
* 双向循环链表类
*/
class CircularDoublyLinkedList {
constructor() {
// 表头节点
this.head = new Node('head')
this.head.next = this.head
this.head.prev = this.head
// 链表的长度
this.size = 0
// 当前节点位置
this.currentNode = this.head
}
/**
* 查找元素所在的节点
* @param {*} item 查找的元素
* @return {Node} 节点对象
*/
find(item) {
let currentNode = this.head
while (currentNode && currentNode.data !== item) {
// 没找到当前节点,就切换到下一个节点继续找,直到找到为止
currentNode = currentNode.next
}
return currentNode
}
/**
* 获取链表的最后一个节点
*/
findLast() {
return this.head.prev
}
/**
* 查找当前节点的前一个节点
* @param {*} item 当前节点信息
*/
findPre(item) {
// 从最开头开始找
let currentNode = this.find(item)
let preNode = null
if (currentNode) {
preNode = currentNode.prev
}
return preNode
}
/**
* 在链表的尾部添加元素
* @param {*} item 需要插入的节点
*/
append(item) {
let currentNode = this.findLast()
let newNode = new Node(item)
newNode.prev = currentNode
// 形成环
newNode.next = currentNode.next
currentNode.next.prev = newNode
currentNode.next = newNode
this.size++
}
/**
* 向链表中插入元素
* @param {*} item 需要插入的节点
* @param {*} preItem 前一个节点(可以不传这个参数)
*/
insert(item, preItem) {
if (preItem) {
// 插入到某个元素之后
let preNode = this.find(preItem)
if (!preNode) return // 没有找到这个节点
let nextNode = preNode.next
let newNode = new Node(item)
// 前一个节点原来的下一个节点变成了新节点的下一个节点
newNode.next = nextNode
// 前一个节点变成新节点的上一个节点
newNode.prev = preNode
// 下一个节点的前一个节点就是新的节点
nextNode.prev = newNode
// 前一个节点现在的下一个节点变成了新节点
preNode.next = newNode
this.size++
} else {
// 默认插入到最后一个元素
this.append(item)
}
}
/**
* 在链表中删除一个节点
* @param {*} item 节点元素
*/
remove(item) {
// 找到当前节点
let currentNode = this.find(item)
if (!currentNode) {
// 找不到此节点
return
}
if (item === 'head') {
// 想要移除头节点
if (this.isEmpty() === false) {
// 不是空链,不允许删除头节点
return;
} else {
// 是空链则重置
this.clear();
return;
}
}
// 找到该节点的上一个节点
let preNode = currentNode.prev
preNode.next = currentNode.next
currentNode.next.prev = preNode
this.size--
}
/**
* 判断链表是否为空
* @returns {boolean}
*/
isEmpty() {
return this.size === 0
}
/**
* 获取链表的长度
* @returns {Number}
*/
getLength() {
return this.size
}
/**
* 从当前节点向前移动 n 个节点
* @param {Number} n 移动的节点数
*/
advance(n) {
while ((n--) && this.currentNode.next) {
this.currentNode = this.currentNode.next;
}
return this.currentNode;
}
/**
* 从当前节点往回移动 n 个节点
* @param {Number} n 移动的节点数
*/
back(n) {
while ((n--) && this.currentNode.prev) {
this.currentNode = this.currentNode.prev;
}
return this.currentNode;
}
/**
* 显示当前节点
* @returns {Node} 当前节点
*/
show() {
return this.currentNode
}
/**
* 链表的遍历显示
* @returns {String}
*/
display() {
let res = 'head';
let currentNode = this.head;
let lastNode = this.findLast()
while (currentNode !== lastNode) {
currentNode = currentNode.next;
res += `->${currentNode.data}`;
}
console.log(res);
return res
}
/**
* 链表反向的遍历显示
* @returns {String}
*/
displayReverse() {
let res = '';
let currentNode = this.findLast();
while (currentNode.data !== 'head') {
res += `${currentNode.data}->`;
currentNode = currentNode.prev;
}
res += 'head'
console.log(res);
return res
}
/**
* 清空链表
*/
clear() {
this.head.next = this.head;
this.head.prev = this.head;
this.currentNode = this.head
this.size = 0;
}
}
const cdll = new CircularDoublyLinkedList()
cdll.append(1)
cdll.append(2)
cdll.append(3)
cdll.append(4)
cdll.append(5)
cdll.insert(6, 5)
cdll.display()
cdll.displayReverse()
跳表(Skip List)
概念
我们只需要对链表稍加改造,就可以支持类似“二分”的查找算法。我们把改造之后的数据结构叫做跳表(Skip list)。
每两个结点提取一个结点到上一级,我们把抽出来的那一级叫做索引或索引层。如下图所示,图中的 down 表示 down 指针,指向下一级结点。
下图是一个多级索引,遍历的效率会更高。
跳表这种数据结构对你来说,可能会比较陌生,因为一般的数据结构和算法书籍里都不怎么会讲。但是它确实是一种各方面性能都比较优秀的动态数据结构,可以支持快速地插入、删除、查找操作,写起来也不复杂,甚至可以替代红黑树(Red-black tree)。Redis 中的有序集合(Sorted Set)就用到了跳表。
JavaScript实现
// 跳表索引最大级数
const MAX_LEVEL = 16
/**
* 辅助实现跳表功能,类似链表中的节点
* @class
*/
class Node {
constructor(data) {
// 存放数据的属性
this.data = data === undefined ? -1 : data
// 当前节点处于整个跳表索引的级数
this.maxLevel = 0
// 表示下一个节点是什么
// 用数组来表示是因为在不同的索引层级该节点的下一个节点不一样
this.refer = new Array(MAX_LEVEL)
}
}
/**
* 跳表类
* @class
*/
class SkipList {
constructor() {
// 当前跳表索引的总级数
this.levelCount = 1
// Node类实例,指向整个链表的开始
this.head = new Node()
}
/**
* 在跳表插入数据的时候,随机生成索引的级数
*/
static randomLevel() {
let level = 1
for (let i = 0; i < MAX_LEVEL; i++) {
if (Math.random() < 0.5) {
// 50% 的概率,保证每一级索引节点的数量大概是上一级索引节点的2倍
level++
}
}
return level
}
/**
* 向跳表里面插入数据
* 跳表插入数据的时候索引是随机插入的
* @param {*} value 数据
*/
insert(value) {
// 生成随机索引级数
const level = SkipList.randomLevel()
// 创建新节点
const newNode = new Node(value)
// 设置索引级数
newNode.maxLevel = level
// 创建一个长度为 level 且每个元素都是一个 Node 实例的数组,用于存放待更新的节点
const update = new Array(level).fill(new Node())
// 把当前节点设置为起始节点,因为要从头开始遍历
let p = this.head
// 循环每一个索引层级
for (let i = level - 1; i >= 0; i--) {
while (p.refer[i] !== undefined && p.refer[i].data < value) {
// 找到当前节点应该插入到的位置的前一个位置
p = p.refer[i]
}
// 把找到的位置赋值给update,作为待更新的节点
update[i] = p
}
// 循环每一个索引层级
for (let i = 0; i < level; i++) {
// 把新节点的下一个节点设置为上一个节点的原下一个节点
newNode.refer[i] = update[i].refer[i]
// 把当前节点设置为上一个节点的新下一个节点
update[i].refer[i] = newNode
}
// 更新跳表所以层级计数
if (this.levelCount < level) {
this.levelCount = level
}
}
/**
* 查找跳表里面的某个数据节点,并返回
* @param {*} value 数据
* @returns {*}
*/
find(value) {
if (!value) {return null}
let p = this.head
// 快速查找到距离目标节点最近的节点
for (let i = this.levelCount - 1; i >= 0; i--) {
// 从上到下查找节点
while (p.refer[i] !== undefined && p.refer[i].data < value) {
p = p.refer[i]
}
}
if (p.refer[0] !== undefined && p.refer[0].data === value) {
return p.refer[0]
}
return null
}
/**
* 移除节点
* @param {*} value 数据
* @returns {*}
*/
remove(value) {
let _node
let p = this.head
const update = new Array(new Node())
// 找到需要更新的节点,也就是被删除节点的上一个节点
for (let i = this.levelCount - 1; i >= 0; i--) {
while (p.refer[i] !== undefined && p.refer[i].data < value) {
p = p.refer[i]
}
update[i] = p
}
if (p.refer[0] !== undefined && p.refer[0].data === value) {
_node = p.refer[0];
// 将当前节点删除之后对他前一个节点的下一个节点需要做一些修改
for (let i = 0; i <= this.levelCount - 1; i++) {
if (update[i].refer[i] !== undefined && update[i].refer[i].data === value) {
// 把要移除节点的上一个节点的下一个节点更新为要移除节点的下一个节点
update[i].refer[i] = update[i].refer[i].refer[i]
}
}
return _node
}
return null
}
/**
* 打印所有节点
*/
printAll() {
let p = this.head;
while (p.refer[0] !== undefined) {
console.log(p.refer[0].data)
p = p.refer[0]
}
}
}
test();
function test() {
let list = new SkipList();
let length = 20000;
//顺序插入
for (let i = 1; i <= 10; i++) {
list.insert(i);
}
//输出一次
list.printAll();
console.time('create length-10')
//插入剩下的
for (let i = 11; i <= length - 10; i++) {
list.insert(i);
}
console.timeEnd('create length-10')
//搜索 10次
for (let j = 0; j < 10; j++) {
let key = Math.floor(Math.random() * length + 1);
console.log(key, list.find(key))
}
//搜索不存在的值
console.log('null:', list.find(length + 1));
//搜索5000次统计时间
console.time('search 5000');
for (let j = 0; j < 5000; j++) {
let key = Math.floor(Math.random() * length + 1);
}
console.timeEnd('search 5000');
}
散列表(Hash Table)
概念
散列表的英文叫“Hash Table”,我们平时也叫它“哈希表”或者“Hash 表”。散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。
三点散列函数设计的基本要求:
- 散列函数计算得到的散列值是一个非负整数;
- 如果 key1 = key2,那 hash(key1) == hash(key2);
- 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。
第一点理解起来应该没有任何问题。因为数组下标是从 0 开始的,所以散列函数生成的散列值也要是非负整数。第二点也很好理解。相同的 key,经过散列函数得到的散列值也应该是相同的。第三点理解起来可能会有问题,我着重说一下。这个要求看起来合情合理,但是在真实的情况下,要想找到一个不同的 key 对应的散列值都不一样的散列函数,几乎是不可能的。即便像业界著名的MD5、SHA、CRC等哈希算法,也无法避免这种散列冲突。而且,因为数组的存储空间有限,也会加大散列冲突的概率。所以我们几乎无法找到一个完美的无冲突的散列函数,即便能找到,付出的时间成本、计算成本也是很大的,所以针对散列冲突问题,我们需要通过其他途径来解决。
散列冲突一般有两种解决办法:
- 开放寻址法:当数据量比较小、装载因子小的时候,适合采用开放寻址法。这也是 Java 中的ThreadLocalMap使用开放寻址法解决散列冲突的原因。
- 链表法(基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。一般用得比较多。)
散列表的查询效率并不能笼统地说成是 O(1)。它跟散列函数、装载因子(散列表的装载因子=填入表中的元素个数/散列表的长度)、散列冲突等都有关系。如果散列函数设计得不好,或者装载因子过高,都可能导致散列冲突发生的概率升高,查询效率下降。
在极端情况下,有些恶意的攻击者,还有可能通过精心构造的数据,使得所有的数据经过散列函数之后,都散列到同一个槽里。如果我们使用的是基于链表的冲突解决方法,那这个时候,散列表就会退化为链表,查询的时间复杂度就从 O(1) 急剧退化为 O(n)。
工业级散列表的要求:
- 支持快速地查询、插入、删除操作;
- 内存占用合理,不能浪费过多的内存空间;
- 性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况;
工业级散列表设计思路:
- 设计一个合适的散列函数;
- 定义装载因子阈值,并且设计动态扩容策略;
- 选择合适的散列冲突解决方法。
JavaScript实现
/****
* 带碰撞处理的Hash表
* 实际上在js中,单独实现一个Hash表感觉不是很有实用价值
* 如果需要通常是直接将Object,Map,Set来当Hash表用
*
* 总结:
* 我写的这个实现把store 从Object换成Array不会有运行性能上的区别
* 把hash函数改成生成一定范围的值的类型,然后初始化一个指定长度的数组因该会有一定的性能提升
* 把store换成Map,然后修改相关实现会获得飞越性的提升,因为在js中Map的实现对这种类型的操作做了优化
*/
class HashTable {
constructor() {
//创建一个没有原型链的对象
this.store = Object.create(null);
}
/**
* Donald E. Knuth在“计算机编程艺术第3卷”中提出的算法,主题是排序和搜索第6.4章。
* @param {*} string
* 翻译自别的语言的实现
* 需要注意的是由于js中没有int类型,number是dobule的标准实现
* 所以返回前的位运算实际和本来的设想不一致,也就是同样的实现,在别的语言中返回可能不同
*/
hash(string) {
let len = string.length;
let hash = len;
for (let i = 0; i < len; i++) {
hash = ((hash << 5) ^ (hash >> 27)) ^ string.charCodeAt(i);
}
return hash & 0x7FFFFFFF;
}
/**
* 是否发生散列冲突
* @param {*} item
*/
isCrash(item) {
return Object.prototype.toString.call(item) === "[object Map]"
}
/**
* 约定item必须要有key
* @param {*} item
*/
put(item) {
if (typeof item.key !== 'string') {
throw 'item must have key!'
}
let hash = this.hash(item.key);
// 碰撞处理
let crash = this.store[hash];
if (crash) {
if (crash.key === item.key) {
this.store[hash] = item;
return
}
if (!this.isCrash(crash)) {
this.store[hash] = new Map();
}
this.store[hash].set(item.key, item);
} else {
this.store[hash] = item;
}
}
get(key) {
let hash = this.hash(key);
let value = this.store[hash] || null;
if (this.isCrash(value)) {
return value.get(key);
} else {
return value
}
}
remove(key) {
let hash = this.hash(key);
let value = this.store[hash];
if (!value) {
return null;
}
if (this.isCrash(value)) {
value.delete(key);
} else {
delete this.store[hash];
}
}
clear() {
this.store = Object.create(null);
}
print() {
let values = Object.values(this.store);
values.forEach(element => {
if (this.isCrash(element)) {
element.forEach(item => {
console.log(item);
});
} else {
console.log(element)
}
});
}
}
/**
* 相比使用Object和Array做store 运行时的性能提升了三分之一
* 但当前这种用法没有直接使用Map方便,而且直接使用Map会快的多
*/
class HashTableBaseMap {
constructor() {
this.store = new Map();
}
/**
* Donald E. Knuth在“计算机编程艺术第3卷”中提出的算法,主题是排序和搜索第6.4章。
* @param {*} string
* 翻译自别的语言的实现
* 需要注意的是由于js中没有int类型,number是dobule的标准实现
* 所以返回前的位运算实际和本来的设想不一致,也就是同样的实现,在别的语言中返回可能不同
*/
hash(string) {
let len = string.length;
let hash = len;
for (let i = 0; i < len; i++) {
hash = ((hash << 5) ^ (hash >> 27)) ^ string.charCodeAt(i);
}
return hash & 0x7FFFFFFF;
}
isCrash(item) {
return Object.prototype.toString.call(item) === "[object Map]"
}
/**
* 约定item必须要有key
* @param {*} item
*/
put(item) {
if (typeof item.key !== 'string') {
throw 'item must have key!'
}
let hash = this.hash(item.key);
//碰撞处理
let crash = this.store.get(hash);
if (crash) {
if (crash.key === item.key) {
this.store.set(hash, item);
return
}
if (!this.isCrash(crash)) {
this.store[hash] = new Map();
}
this.store[hash].set(item.key, item);
} else {
this.store.set(hash, item);
}
}
get(key) {
let hash = this.hash(key);
let value = this.store.get(hash);
if (this.isCrash(value)) {
return value.get(key);
} else {
return value
}
}
remove(key) {
let hash = this.hash(key);
let value = this.store.get(hash);
if (!value) {
return null;
}
if (this.isCrash(value)) {
value.delete(key);
} else {
this.store.delete(hash)
}
}
clear() {
this.store = new Map();
}
print() {
this.store.forEach(element => {
if (this.isCrash(element)) {
element.forEach(item => {
console.log(item);
});
} else {
console.log(element)
}
});
}
}
/**
* 基础测试
*/
function baseTest() {
let hashTable = new HashTable();
for (let i = 0; i < 10; i++) {
hashTable.put({
key: 'test' + i,
value: 'some value' + i
});
}
console.log('step1:')
//随机获取5次
for (let j = 0; j < 5; j++) {
let key = 'test' + Math.floor(Math.random() * 10);
console.log(key);
console.log(hashTable.get(key))
}
//获得一次空值
console.log('get null:', hashTable.get('test10'))
//修改一次值
hashTable.put({
key: 'test1',
value: 'change'
});
//删除一次值
hashTable.remove('test2');
console.log('step2:')
//输出修改后所有的
hashTable.print();
}
/**
* 有序key存取,性能测试
*/
function ordKeyTest() {
let length = 1000000;
console.time('create')
let hashTable = new HashTable();
for (let i = 0; i < length; i++) {
//24位长度有序key
hashTable.put({
key: 'someTestSoSoSoSoLongKey' + i,
value: 'some value' + i
});
}
console.timeEnd('create')
let get = 100000;
console.time('get')
for (let j = 0; j < get; j++) {
let key = 'test' + Math.floor(Math.random() * 999999);
hashTable.get(key)
}
console.timeEnd('get')
}
/**
* 无序key性能测试
* 这个查找稍微有点不准,会有一定量随机字符串重复
* 实际结果,创建没有区别,大数据量下由于无序key有一些会碰撞,get的总体用的时间会多不少。
*/
function randKeyTest() {
let length = 1000000;
let keyList = [];
for (let i = 0; i < length; i++) {
keyList.push(randomString());
}
console.time('create')
let hashTable = new HashTable();
for (let i = 0; i < length; i++) {
hashTable.put({
key: keyList[i],
value: 'some value' + i
});
}
console.timeEnd('create')
let get = 100000;
console.time('get')
for (let j = 0; j < get; j++) {
let key = keyList[Math.floor(Math.random() * 999999)];
hashTable.get(key)
}
console.timeEnd('get')
}
/**
* 直接使用Object的性能测试
* 有序就不测了,估计不会有区别,只看不使用hash的无序key
* 结果:想达到同样的结果创建会比hash后的慢接近四分之三,获取用时差不多
*/
function randKeyTestFromObj() {
let length = 1000000;
let keyList = [];
for (let i = 0; i < length; i++) {
keyList.push(randomString());
}
console.time('create')
let hashTable = {};
for (let i = 0; i < length; i++) {
let key = keyList[i];
hashTable[key] = {
key: key,
value: 'some value' + i
}
}
console.timeEnd('create')
let get = 100000;
console.time('get')
for (let j = 0; j < get; j++) {
let key = keyList[Math.floor(Math.random() * 999999)];
hashTable[key]
}
console.timeEnd('get')
}
/**
* 直接使用Map的性能测试
* 结果:创建用时差不多,但是获取快了一个数量级(十倍不止)
*/
function randKeyTestFromMap() {
let length = 1000000;
let keyList = [];
for (let i = 0; i < length; i++) {
keyList.push(randomString());
}
console.time('create')
let hashTable = new Map();
for (let i = 0; i < length; i++) {
let key = keyList[i];
hashTable.set(key, {
key: key,
value: 'some value' + i
})
}
console.timeEnd('create')
let get = 100000;
console.time('get')
for (let j = 0; j < get; j++) {
let key = keyList[Math.floor(Math.random() * 999999)];
hashTable.get(key);
}
console.timeEnd('get')
}
// 生成指定长度的字符串
function randomString(len) {
len = len || 24;
var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
var maxPos = chars.length;
var pwd = '';
for (i = 0; i < len; i++) {
pwd += chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}