Stacks(栈)
栈是一种先进后出的数据结构,例如:浏览器的历史记录
JS 中的数组(Array)已经实现了栈的所有功能
例子:判断是否回文字符串
let letters = [] // 栈
let word = 'racecar'
let rword = ''
for(let i = 0; i < word.length; i++) {
letters.push(word[i])
}
for(let i = 0; i < word.length; i++) {
rword += letters.pop()
}
if (word === rword) {
console.log(`${word} 是回文字符串`)
}
简单模拟栈的实现
class Stack {
count = 0
storage = {}
// 推入数据
push(value) {
this.storage[this.count] = value;
this.count++;
}
// 推出最后推入的数据
pop() {
if (this.count === 0) {
return undefined;
}
this.count--;
const result = this.storage[this.count];
delete this.storage[this.count]
return result;
}
// 栈中元素个数
size() {
return this.count;
}
// 查看栈中最后一个元素
peek() {
return this.storage[this.count - 1];
}
}
测试:
let myStack = new Stack();
myStack.push(1);
myStack.push(2);
console.log(myStack.peek())
console.log(myStack.pop())
console.log(myStack.peek())
myStack.push('abcd')
console.log(myStack.size())
console.log(myStack.peek())
console.log(myStack.pop())
console.log(myStack.peek())
Set
Set 中的元素永不重复, 而且没有排列顺序
ES6已经原生支持Set了
简单模拟Set的实现
class MySet { // 和原生Set区分
collection = []
has(element) {
return this.collection.indexOf(element) !== -1
}
values() {
return this.collection
}
add(element) {
if (!this.has(element)) {
this.collection.push(element);
return true;
}
return false;
}
remove(element) {
if (this.has(element)) {
const index = this.collection.indexOf(element);
this.collection.splice(index, 1);
return true;
}
return false;
}
size() {
return this.collection.length
}
// 返回2个Set的并集
union(otherSet) {
let unionSet = new MySet();
let firstSet = this.values()
let secondSet = otherSet.values()
firstSet.forEach((e) => unionSet.add(e))
secondSet.forEach((e) => unionSet.add(e))
return unionSet
}
// 返回2个Set的交集
intersection(otherSet) {
let intersectionSet = new MySet();
let firstSet = this.values()
firstSet.forEach((e) => {
if (otherSet.has(e)) {
intersectionSet.add(e)
}
})
return intersectionSet
}
// 返回一个新的Set: 里面的元素在当前Set中,但不在otherSet中
difference(otherSet) {
let differenceSet = new MySet();
let firstSet = this.values()
firstSet.forEach((e) => {
if (!otherSet.has(e)) {
differenceSet.add(e)
}
})
return differenceSet
}
// 检查当前Set是否属于otherSet的子集
subset(otherSet) {
let firstSet = this.values()
return firstSet.every(value => {
return otherSet.has(value)
})
}
}
测试
let setA = new MySet();
let setB = new MySet();
setA.add('a')
setB.add('b')
setB.add('c')
setB.add('a')
setB.add('d')
console.log(setA.subset(setB))
console.log(setA.intersection(setB).values())
Queue(队列)
先进先出, 比如买东西排队
JS中也能用数组实现队列, 但是如果想精确点, 也得自己实现
class Queue {
collection = []
print() {
console.log(this.collection)
}
// 把元素推入队列尾部
enqueue(element) {
this.collection.push(element)
}
// 把第一个元素推出队列, 并返回推出的元素
dequeue() {
return this.collection.shift()
}
front() {
return this.collection[0]
}
size() {
return this.collection.length
}
isEmpty() {
return this.collection === 0
}
}
测试
let queue = new Queue()
queue.enqueue('a')
queue.enqueue('b')
queue.enqueue('c')
queue.print()
queue.dequeue()
queue.front()
queue.print()
PriorityQueue(优先队列)
除了传入元素, 还需要传入对应的优先级:
如果所有元素的优先级相同, 就和普通队列一样; 但是如果不同, 高优先级的元素会被插入到队列的开始
class PriorityQueue extends Queue {
enqueue(element) {
const { collection } = this;
if (this.isEmpty()) {
collection.push(element)
} else {
let added = false;
for(let i = 0; i < collection.length; i++) {
if (element[i] < collection[i][1]) {
collection.splice(i, 0, element)
added = true;
break;
}
}
if (!added) {
collection.push(element)
}
}
}
dequeue() {
const value = this.collection.shift()
return value[0]
}
}
测试
const pq = new PriorityQueue();
pq.enqueue(['bbb', 2])
pq.enqueue(['ccc', 3])
pq.enqueue(['aaa', 1])
pq.print()
pq.dequeue()
pq.front()
pq.print()
Binary Search Tree(二叉搜索树)
树的特点是: 保存的数据看起来很像自然界中的树, 下面就是树的图形化表示, 其中保存数据的圆圈叫做节点, 最上面的节点A叫做根节点, 叶节点指在树的最末端, 而且一定没有子节点。
树有很多种类型, 下面我们介绍其中一种: 二叉搜索树。
理论上, 树可以有任意个数的分支, 比如上图的C节点有F, G, H 3个子节点, 而二叉树, 一个节点就只有2个分支, 如下图就是一个二叉树, 每个节点有且只有2个分支:
二叉树是有序的数据结构, 每个左子节点一定小于父节点, 每个右子节点一定大于父节点(一般不需要等于), 因为这种特性, 搜索次数可以一直减半, 所以这个算法的插入/删除(本质是查找)的时间复杂度是: O(logn), 它比线性复杂度(遍历)要好得多, 但是比哈希表要慢。
下面我们看看JS的实现:
class Node {
constructor(data, left = null, right = null) {
this.data = data;
this.left = left;
this.right = right;
}
}
class BST {
constructor() {
this.root = null
}
add(data) {
const node = this.root
if (node === null) {
this.root = new Node(data)
return
} else {
const searchTree = (node) => {
if (data < node.data) {
if (node.left === null) {
node.left = new Node(data)
return
} else {
return searchTree(node.left)
}
} else if (data > node.data) {
if (node.right === null) {
node.right = new Node(data)
return
} else {
return searchTree(node.right)
}
} else {
return null;
}
}
return searchTree(node)
}
}
findMin() {
let current = this.root;
while(current.left !== null) {
current = current.left;
}
return current.data
}
findMax() {
let current = this.root;
while (current.right !== null) {
current = current.right;
}
return current.data
}
find(data) {
let current = this.root;
while (current.data !== data) {
if (data < current.data) {
current = current.left;
} else {
current = current.right;
}
if (current === null) {
return null;
}
}
return current
}
// 是否包含该数据
isPresent(data) {
let current = this.root;
while(current) {
if (data === current.data) {
return true;
}
if (data < current.data) {
current = current.left
} else {
current = current.right
}
}
return false;
}
remove(data) {
const removeNode = (node, data) => {
if (node === null) {
return null;
}
if (data === node.data) {
// 没有子节点
if (node.left === null && node.right === null) {
return null
}
// 没有左子节点
if (node.left === null) {
return node.right
}
// 没有右子节点
if (node.right === null) {
return node.left
}
// 左右子节点都存在
// 如下图, 要想移除3节点, 就得用3的右节点的最左节点(4)替换, 就会变成右边的样子
let tempNode = node.right;
while (tempNode.left !== null) {
tempNode = tempNode.left;
}
node.data = tempNode.data;
node.right = removeNode(node.right, tempNode.data);
return node;
} else if(data < node.data) {
node.left = removeNode(node.left, data)
return node;
} else {
node.right = removeNode(node.right, data)
return node;
}
}
// 传入了根节点和要删除的数据
this.root = removeNode(this.root, data)
}
}
测试
const bst = new BST ()
bst.add(4)
bst.add(2)
bst.add(6)
bst.add(1)
bst.add(3)
bst.add(5)
bst.add(7)
bst.remove(4)
console.log(bst.findMin())
console.log(bst.findMax())
bst.remove(7)
console.log(bst.findMax())
console.log(bst.isPresent(4))
二叉搜索树的遍历和高度
BST的高度就是根节点到某个叶节点的距离, 比如下图中, 相对于9, 9的高度是0
, 而4和17节点的高度是1
, 5, 7, 20的高度是3
。
对任意BST一定有最小高度和最大高度, 如果 最大高度 - 最小高度 不大于1
, 那么BST平衡。
如上图, 这个BST的最小高度就是从根节点到第1个(没有2个子节点的)叶节点17的高度1
, 最大高度就是从根节点开始到任意最底部的(没有子节点的)叶节点的高度。 很明显这个BST的最大高度是3
, 那么这个树不平衡. 这个树之所以不平衡, 就是17少了个左节点。
如果我们加一个10节点(即: 17的左节点), 此时最小高度是2
, 最大高度是3
, 此时树就平衡了。
为什么要树平衡? 因为平衡树的查询效率更高, 这里不作展开
我们可以实现一种BST, 在增加, 删除节点的时候自动平衡自身, 这会提高BST的查询效率
JS实现二叉搜索树的遍历和高度:
class THBST extends BST {
// 是否平衡
isBalanced() {
return this.findMinHeight() >= this.findMaxHeight() - 1
}
// 最小高度
findMinHeight(node = this.root) {
if (node === null) {
return -1
}
// left 和 right 到最后一定会返回 -1
let left = this.findMinHeight(node.left)
let right = this.findMinHeight(node.right)
// 如果左树最小高度小,那么整个树的最小高度就是左树最小高度+1
if (left < right) {
return left + 1
} else { // 反之,整个树的最小高度就是右树最小高度+1
return right + 1
}
}
// 最大高度
findMaxHeight(node = this.root) {
if (node === null) {
return -1
}
let left = this.findMaxHeight(node.left)
let right = this.findMaxHeight(node.right)
if (left > right) {
return left + 1
} else {
return right + 1
}
}
// 顺序遍历: 从最左节点开始(按大小顺序)直到最右节点
inOrder() {
if (this.root === null) {
return null;
} else {
let result = [];
function traverseInOrder(node) {
node.left && traverseInOrder(node.left)
result.push(node.data)
node.right && traverseInOrder(node.right)
}
traverseInOrder(this.root)
return result;
}
}
// 前序遍历: 从根节点开始, 深度优先遍历(先遍历根节点, 然后往叶节点延伸)
preOrder() {
if (this.root === null) {
return null;
} else {
let result = [];
function traversePreOrder(node) {
result.push(node.data)
node.left && traversePreOrder(node.left)
node.right && traversePreOrder(node.right)
}
traversePreOrder(this.root)
return result;
}
}
// 后续遍历: 在遍历根节点之前, 先从最左开始遍历叶节点(先递归左侧, 再递归右侧)
postOrder() {
if (this.root === null) {
return null;
} else {
let result = [];
function traversePostOrder(node) {
node.left && traversePostOrder(node.left)
node.right && traversePostOrder(node.right)
result.push(node.data)
}
traversePostOrder(this.root)
return result;
}
}
// 分级遍历: 广度优先搜索, 在查找下一个级别前, 先把本级别元素都遍历到
levelOrder() {
let result = []
let queue = []
if (this.root !== null) {
queue.push(this.root)
while(queue.length > 0) {
let node = queue.shift()
result.push(node.data)
if (node.left !== null) {
queue.push(node.left)
}
if (node.right !== null) {
queue.push(node.right)
}
}
return result;
} else {
return null;
}
}
}
依照下图构造数据并测试:
const thbst = new THBST()
thbst.add(9)
thbst.add(4)
thbst.add(17)
thbst.add(3)
thbst.add(6)
thbst.add(21)
thbst.add(5)
thbst.add(7)
thbst.add(20)
console.log(thbst.findMinHeight())
console.log(thbst.findMaxHeight())
console.log(thbst.isBalanced())
thbst.add(10)
console.log(thbst.findMinHeight())
console.log(thbst.findMaxHeight())
console.log(thbst.isBalanced())
console.log(thbst.inOrder()) // [3, 4, 5, 6, 7, 9, 10, 17, 20, 21]
console.log(thbst.preOrder()) // [9, 4, 3, 6, 5, 7, 17, 10, 21, 20]
console.log(thbst.postOrder()) // [3, 5, 7, 6, 4, 10, 20, 21, 17, 9]
console.log(thbst.levelOrder()) // [9, 4, 17, 3, 6, 10, 21, 5, 7, 20]
Hash Tables(哈希表)
哈希表是用于实现关联数组或者是键值对映射的一种数据结构, 由于其效率较高, 常用于 map 、 object 等数据结构的实现。
每次(查找、插入、删除)操作的平均复杂度和哈希表中的元素个数无关, 时间复杂度为O (1), 所以速度非常快。
绝大多数的开发语言都原生自带这个功能, 比如 JavaScript 的 object 就是哈希表的实现, 下面就看看具体细节:
// 生成索引
function hash(str, max) {
let hash = 0
for(let i = 0; i < str.length; i++) {
hash += str.charCodeAt(i)
}
return hash % max;
}
class HashTable {
constructor(limit) {
this.storage = []
this.limit = limit
}
print() {
console.log(this.storage)
}
add(key, value) {
const { storage } = this
// 调用hash函数得出哈希表中存储该value的索引
let index = hash(key, this.limit)
if (storage[index] === undefined) {
storage[index] = [ [key, value] ]
} else {
let inserted = false;
for(let i = 0; i < storage[index][length]; i++) {
if (storage[index][i][0] === key) { // 更新
storage[index][i][1] = value
inserted = true
}
}
if (inserted === false) { // 添加
storage[index].push([ key, value ])
}
}
}
remove(key) {
const { storage } = this
let index = hash(key, this.limit)
// 对应的可能不止一个
if (storage[index].length === 1 && storage[index][0][0] === key) {
delete storage[index]
} else {
for (let i = 0; i < storage[index].length; i++) {
if (storage[index][i][0] === key) {
delete storage[index][i]
}
}
}
}
// 查找数据
lookup(key) {
const { storage } = this
let index = hash(key, this.limit)
if (storage[index] === undefined) {
return undefined
} else {
for (let i = 0; i < storage[index].length; i++) {
if (storage[index][i][0] === key) {
return storage[index][i][1]
}
}
}
}
}
测试
let ht = new HashTable(10)
ht.add('aaa', '111')
ht.add('bbb', '222')
ht.add('ccc', '333')
ht.add('ddd', '444')
console.log(ht.lookup('ccc')) // 333
ht.print()
Linked List(链表)
可以认为链表的每个节点存储了一段数据, 链表每个节点都有两部分: 数据部分和地址部分。
链表有不同的形态, 主要分为三种: 单向链表、双向链表、循环链表。 如下图就是一个单向链表:
单向链表和数组的对比
查找性能
单向链表的查找操作通常是这样的:
- 从头节点进入,开始比对节点的值,如果不同则通过指针进入下一个节点
- 重复上面的动作,直到找到相同的值,或者节点的指针指向null
链表的查找性能与数组一样,都是时间复杂度为O(n).
插入删除性能
链表与数组最大的不同就在于删除、插入的性能优势,由于链表是非连续的内存,所以不用像数组一样在插入、删除操作的时候需要进行大面积的成员位移,比如在a、b节点之间插入一个新节点c, 链表只需要:
- a断开指向b的指针,将指针指向c
- c节点将指针指向b,完毕
这个插入操作仅仅需要移动一下指针即可,插入、删除的时间复杂度只有O(1).
读取性能
链表相比之下也有劣势,那就是读取操作远不如数组,数组的读取操作之所以高效,是因为它是一块连续内存,数组的读取可以通过寻址公式快速定位,而链表由于非连续内存,所以必须通过指针一个一个节点遍历.
比如,对于一个数组,我们要读取第三个成员,我们仅需要arr[2]就能快速获取成员,而链表则需要从头部节点进入,在通过指针进入后续节点才能读取.
JS实现单链表
class Node {
constructor(element) {
this.element = element
this.next = null;
}
}
class LinkedList {
length = 0;
head = null;
size() {
return this.length;
}
// 返回链表第1个元素
head() {
return this.head;
}
// 在链表尾部添加元素
add(element) {
const node = new Node(element)
if (this.head === null) {
this.head = node
} else {
let currentNode = this.head;
while(currentNode.next) {
currentNode = currentNode.next;
}
currentNode.next = node;
}
this.length++
}
remove(element) {
let currentNode = this.head
let prevNode;
if (currentNode.element === element) {
this.head = currentNode.next
} else {
while (currentNode.element !== element) {
prevNode = currentNode
currentNode = currentNode.next
}
prevNode.next = currentNode.next
}
this.length--; // 这里不是很严谨
}
isEmpty() {
return this.length === 0
}
indexOf(element) {
let currentNode = this.head;
let index = -1;
while (currentNode) {
index++;
if (currentNode.element === element) {
return index;
}
currentNode = currentNode.next;
}
return -1;
}
// 获取索引对应的元素
elementAt(index) {
let currentNode = this.head;
let count = 0;
while (count < index) {
count++;
currentNode = currentNode.next;
}
return currentNode.element
}
// 在链表某个具体位置添加元素
addAt(index, element) {
let node = new Node(element)
let currentNode = this.head;
let prevNode;
let currentIndex = 0;
if (index > this.length) {
return false;
}
if (index === 0) {
node.next = currentNode;
this.head = node;
} else {
while (currentIndex < index) {
currentIndex++;
prevNode = currentNode
currentNode = currentNode.next
}
node.next = currentNode
prevNode.next = node;
}
this.length++;
}
// 和addAt方法类似
removeAt(index) {
let currentNode = this.head;
let prevNode;
let currentIndex = 0;
if (index < 0 || index >= this.length) {
return null;
}
if (index === 0) {
this.head = currentIndex.next;
} else {
while (currentIndex < index) {
currentIndex++;
prevNode = currentNode
currentNode = currentNode.next
}
prevNode.next = currentNode.next;
}
this.length--;
return currentNode.element
}
}
测试
const list = new LinkedList();
list.add('aaa')
list.add('bbb')
list.add('ccc')
list.add('ddd')
list.add('eee')
console.log(list.size()) // 5
console.log(list.removeAt(3)) // ddd
console.log(list.size()) // 4
Trie(字典树)
也叫前缀树, 是树的一种分支, 用于存储关联数据的数据结构。
trie 也有分支, 一个分支能存储一个完整的单词, 所以 trie 结构常用于存储单词/字符串, 比如: 检查某个单词/字符串是否存在于某个数据集合中。
在 trie 中,每个节点都代表一个字母/字符, 如下图:
星标志着这个位置是一个单词的终点节点, 即到星标志的位置,就存在一个完整的单词
JS代码实现:
class Node {
constructor() {
// 如上图, 对于根节点, 它的keys就是 new Map(['b', new Node()], ['d', new Node()], ['s', new Node()])
this.keys = new Map();
this.end = false;
}
setEnd() {
this.end = true;
}
isEnd() {
return this.end;
}
}
class Trie {
constructor() {
this.root = new Node()
}
add(str, node = this.root) {
if (str.length === 0) {
node.setEnd()
return;
} else if (!node.keys.has(str[0])) {
node.keys.set(str[0], new Node())
return this.add(str.substr(1), node.keys.get(str[0]))
} else {
return this.add(str.substr(1), node.keys.get(str[0]))
}
}
// word 是否在 Trie 中
isWord(word) {
let node = this.root;
while (word.length > 1) { // 大于1
if (!node.keys.has(word[0])) {
return false;
} else {
node = node.keys.get(word[0])
word = word.substr(1)
}
}
// while中是大于1, 所以这里是判断最后一位是否符合要求
return (node.keys.has(word) && node.keys.get(word).isEnd()) ? true : false;
}
// 打印出所有单词(分支)
print() {
let words = []
let search = (node, str) => {
if (node.keys.size !== 0) {
for (let letter of node.keys.keys()) {
search(node.keys.get(letter), str.concat(letter))
}
if (node.isEnd()) {
words.push(str)
}
} else {
str.length > 0 ? words.push(str): undefined;
return;
}
}
search(this.root, '')
return words.length > 0 ? words : null
}
}
测试
const myTrie = new Trie();
myTrie.add('ball')
myTrie.add('bat')
myTrie.add('doll')
myTrie.add('dork')
myTrie.add('do')
myTrie.add('dorm')
myTrie.add('send')
myTrie.add('sense')
console.log(myTrie.isWord('doll')) // true
console.log(myTrie.isWord('dor')) // false
console.log(myTrie.isWord('dora')) // false
console.log(myTrie.print()) // ['ball', 'bat', 'doll', 'dork', 'dorm', 'do', 'send', 'sense']
Graphs(图)
图的存储方式有很多种: 邻接矩阵, 邻接表, 十字链表, 邻接多重表等.
最常用的2种: 邻接矩阵和邻接表,这里说明一下邻接表。
邻接表结构:
-
存储图中顶点 ----> 数组/对象
-
边的指向关系 ----> 链表/数组
如下图:
领接表的代码实现:
class Graph {
constructor(points) {
// 点
this.points = points
// 边的数量
this.sideNum = 0;
// 每个点都对应一个链表/数组
this.adjArray = Array.from({ length: points.length }, () => [])
}
addPoint(id, point) {
let index1 = this.points.findIndex(v => v === id)
this.adjArray[index1].push(point)
/*
对于无向图, 假设A和B相连:
如果你只想添加1次A和B(A-B或B-A)的相连关系, 就需要下面语句,
但是如果你添加了2次(A-B和B-A)相连关系, 就不需要下面语句了
*/
// let index2 = this.points.findIndex(v => v === point)
// this.adjArray[index2].push(id)
this.sideNum++
}
addPoints(id, points) {
points.forEach(item => {
this.addPoint(id, item)
})
}
getPoints(id) {
let index = this.points.findIndex(v => v === id)
return this.adjArray[index]
}
// 打印格式化数据
print() {
let str = ''
this.points.forEach((item, index) => {
str += item + ' ---> [' + this.adjArray[index].join(', ') + '] \n'
})
return str
}
getMap(points) {
let arr = points.reduce((arr, item) => {
return arr.concat(this.getPoints(item))
}, [])
return arr.reduce((map, item) => {
if (!map[item]) {
map[item] = 1
} else {
map[item]++
}
return map
}, {})
}
// 交集
getUnions(points) {
let map = this.getMap(points)
let len = points.length
return Object.keys(map).filter(key => map[key] >= len)
}
// 并集
getCollect(points) {
let map = this.getMap(points)
return Object.keys(map).filter(key => map[key])
}
}
测试,实现如下图结构:
const demo = new Graph(['v0', 'v1', 'v2', 'v3', 'v4'])
// 注册点
demo.addPoints('v0', ['v2', 'v3']);
demo.addPoints('v1', ['v3', 'v4']);
demo.addPoints('v2', ['v0', 'v3', 'v4']);
demo.addPoints('v3', ['v0', 'v1', 'v2']);
demo.addPoints('v4', ['v1', 'v2']);
// -----------------------------------
// console.log(demo.getPoints('v0'))
// console.log(demo.getPoints('v1'))
// console.log(demo.getPoints('v2'))
// console.log(demo.getPoints('v3'))
// console.log(demo.getPoints('v4'))
console.log(demo.print())
若有收获,就点个赞吧