一.概述
数据结构是计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来最优效率的算法,其实算法并不是一个很高级的东西,它充斥在每一种代码组织方式中;而且各种语言关于数据结构方面的内容都是大同小异的,会了一种语言的数据结构方面的内容就可以快速掌握其它语言的的数据结构;有一部分人对JavaScript有偏见,觉得这种语言没有档次,但又有多少人在学完JavaScript敢说精通它。
二.见识数据结构
(1)>>数组
你没有看错,常见的数组就是一种数据结构,对于数组就是掌握一下它的各种方法,如push(),pop(),shift(),unshift(),indexOf(),sort(),slice(),splice()等,在这里就不赘述了。
(2)>>栈
A.图解
遵循的是后进先出
B.封装代码
class Stack {//加#是为了保证其是私有属性,不让外界随意调用#items = [];pop() {return this.#items.pop()}push(data) {return this.#items.push(data)}//返回栈顶peek() {return this.#items.at(-1)}isEmpty() {return this.#items.length > 0 ? false : true}size() {return this.#items.length}clear() {this.#items = []}toString(){return this.#items.join(' ')}
}
C.应用场景
将十进制数转换为其它进制的数时用到了辗转相除法,刚好是把得到的余数从后到前输出得到对应进制下的数,如果把这些数从前到后推到栈里,再取出来岂不妙哉。
function convert(decNumber, binaryNumber) {let number = decNumberlet stack = new Stack()let string = ''//防止转换成十六进制的时候乱码let baseString = '0123456789ABCDEF'while (number > 0) {stack.push(number % binaryNumber)number = Math.floor(number / binaryNumber)}while (!(stack.isEmpty())) {string += baseString[stack.pop()]}return string
}console.log(convert(50, 2));
(3)>>队列
A.图解
先进先出,后端进,前端出,类似于排队
B.封装代码
有缺点的封装方式
class Queue {#items = [];//一旦元素变多,还用shift会造成运行效率低deleteQueue() {return this.#items.shift()}enterQueue(data) {return this.#items.push(data)}//返回队头frontQueue() {return this.#items.at(0)}isEmpty() {return this.#items.length > 0 ? false : true}size() {return this.#items.length}clear() {this.#items = []}toString() {return this.#items.join(' ')}
}
修正之后的封装方式
class Queue {#items = {};#count = 0;#lowCount = 0;//出队deleteQueue() {if (this.isEmpty()) {return undefined}let res = this.#items[this.#lowCount]delete this.#items[this.#lowCount]this.#lowCount++return res}//进队enterQueue(data) {this.#items[this.#count] = datathis.#count++}//返回队头frontQueue() {return this.#items[this.#lowCount]}isEmpty() {return this.size === 0}size() {return this.#count - this.#lowCount}clear() {this.#items = {}this.#count = 0this.#lowCount = 0}toString() {let str = ""for (let i = this.#lowCount; i < this.#count; i++) {str += `${this.#items[i]} `}return str.trim()}
}
C.应用场景
类似于击鼓传花,传指定次数,最终东西在谁手上谁淘汰,一直淘汰到队列里只剩一个人为最终的胜者
function game(list, num) {let queue = new Queue()for (let i = 0; i < list.length; i++) {queue.enterQueue(list[i])}while (queue.size() > 1) {for (i = 0; i < num; i++) {queue.enterQueue(queue.deleteQueue())}console.log(queue.deleteQueue(), '淘汰');}console.log(queue.deleteQueue());//最终获胜者return queue.deleteQueue()
}game(['aks', 'uej', 'jsm', 'duj', 'llq'], 7)
D.双端队列
与一般的队列有所不同的情况是,可以从队头进队,队尾可以出队,简而言之就是两端都能进出队伍。
class DeQueue {#items = {};#count = 0;#lowCount = 0;removeFront() {if (this.isEmpty()) {return undefined}let res = this.#items[this.#lowCount]delete this.#items[this.#lowCount]this.#lowCount++return res}addBack(data) {this.#items[this.#count] = datathis.#count++}//在队头添加addFront(data) {if (this.isEmpty()) {this.addBack()} else {if (this.#lowCount > 0) {this.#lowCount--this.#items[this.#lowCount] = data} else {for (let i = this.#count; i > 0; i--) {this.#items[i] = this.#items[i - 1]}this.#items[0] = datathis.#count++}}}//在队尾删除removeBack() {if (this.isEmpty()) {return} else {this.#count--let res = this.#items[this.#count]delete this.#items[this.#count]return res}}//返回队头peekFront() {return this.#items[this.#lowCount]}//返回队尾peekBack() {return this.#items.at(-1)}isEmpty() {return this.size === 0}size() {return this.#count - this.#lowCount}clear() {this.#items = {}this.#count = 0this.#lowCount = 0}toString() {let str = ""for (let i = this.#lowCount; i < this.#count; i++) {str += `${this.#items[i]} `}return str.trim()}
}//回文应用
function test(str) {//将输入有空格的字符串转化为无空格的const lowStr = str.toLocaleLowerCase().split(' ').join('') let dequeue = new DeQueue()for (let i = 0; i < lowStr.length; i++) {dequeue.addBack(lowStr[i])}while (dequeue.size() > 1) {if (dequeue.removeFront() !== dequeue.removeBack()) {console.log('不是回文');break} else {console.log('是回文结构');}}
}test('dadadb') //不是回文
(4)>>链表
1.单链表
A.图解
可以把next看成一个指针,其能指向表中下一个存储的数据
B.封装代码
//根据节点的需要创建相应的元素
class Node {constructor(element) {this.element = elementthis.next = null}
}
//单链表
class LinkList {constructor() {this.head = nullthis.count = 0}//从表尾放入新节点push(element) {const node = new Node(element)if (this.head === null) {this.head = node} else {let current = this.headwhile (current.next != null) {current = current.next}current.next = node}this.count++}size() {return this.count}isEmpty() {return this.size() === 0}//指定位置删除removeAt(index) {if (index >= 0 && index < this.count) {let current = this.headif (index === 0) {this.head = this.head.next} else {let previousfor (let i = 0; i < index; i++) {previous = currentcurrent = current.next}previous.next = current.next}this.count--return current.element}return undefined}//同样也是指定位置删除,但用到了后面封装的getNodeAt方法removeAt2(index) {if (index >= 0 && index < this.count) {let current = this.headif (index === 0) {this.head = this.head.next} else {let previous = this.getNodeAt(index - 1)current = previous.nextprevious.next = current.next}this.count--return current.element}return undefined}//根据索引值找到相应的节点getNodeAt(index) {if (index >= 0 && index < this.count) {let node = this.headfor (let i = 0; i < index; i++) {node = node.next}return node}return undefined}indexOf(element) {let current = this.headfor (let i = 0; i < this.count; i++) {//防止其直接输入一个对象变量if (JSON.stringify(element) === JSON.stringify(current.element)) {return i}current = current.next}return -1}//指定元素删除remove(element) {let index = this.indexOf(element)let removeElement = this.removeAt(index)return removeElement}insert(element, index) {if (index >= 0 && index <= this.count) {const node = new Node(element)if (index === 0) {let current = this.headnode.next = currentthis.head = nodethis.count++} else {let previous = this.getNodeAt(index - 1)let current = previous.nextprevious.next = nodenode.next = currentthis.count++}return true}return false}
}
2.双向链表
A.图解
节点除了存储数据以外,还有两个指针分别指向前一个节点地址(prev)和下一个节点地址(next)
B.封装代码
//前人栽树,后人乘凉,继承自之前的Node类
class DoubleNode extends Node {constructor(element) {super(element)this.prev = null}
}//继承自LinkList类加方法重写
class DoubleLinkList extends LinkList {constructor() {super()this.tail = null}push(element) {const doubleNode = new DoubleNode(element)if (this.head === null) {this.head = doubleNodethis.tail = doubleNode} else {this.tail.next = doubleNodedoubleNode.prev = this.tailthis.tail = doubleNode}this.count++}insert(element, index) {if (index >= 0 && index <= this.count) {const doubleNode = new DoubleNode(element)if (index === 0) {//分链表是否有元素if (this.head === null) {this.head = doubleNodethis.tail = doubleNode} else {this.head.prev = doubleNodedoubleNode.next = this.headthis.head = doubleNode}} else if (index === this.count) {this.tail.next = doubleLinkListdoubleLinkList.prev = this.tailthis.tail = doubleLinkList} else {let previouslet current = this.headfor (let i = 0; i < index; i++) {current = current.nextprevious = current.prev}current.prev = doubleNodedoubleNode.next = currentdoubleNode.prev = previousprevious.next = doubleNode}this.count++return true}return false}removeAt(index) {if (index >= 0 && index < this.count) {let current = this.headif (index === 0) {this.head = current.nextif (this.count === 1) {this.tail = null} else {this.head.prev = null}} else if (index === this.count - 1) {current = this.tailthis.tail = current.prevthis.tail.next = null} else {current = this.getNodeAt(index)let previous = current.prevlet future = current.nextprevious.next = futurefuture.prev = previous}this.count--return current.element}return undefined}}
3.循环链表
A.图解
B.封装代码
//循环链表类继承自单链表类
class CirculateLinkList extends LinkList {constructor() {super()}push(element) {const node = new Node(element)let currentif (this.head === null) {this.head = node} else {current = this.getNodeAt(this.size() - 1)current.next = node}node.next = this.headthis.count++}insert(element, index) {if (index >= 0 && index <= this.count) {const node = new Node(element)let current = this.headif (index === 0) {if (this.head === null) {this.head = nodenode.next = this.head} else {node.next = this.headcurrent = this.getNodeAt(this.size() - 1)this.head = nodecurrent.next = node}} else {if (index === this.count) {current = this.getNodeAt(index - 1)current.next = nodenode.next = this.head} else {let previous = this.getNodeAt(index - 1)previous.next = nodenode.next = previous.next}}this.count++return true}return false}removeAt(index) {if (index >= 0 && index < this.count) {let current = this.headif (index === 0) {if (this.count === 1) {this.head = null} else {let last = this.getNodeAt(this.size() - 1)this.head = current.nextlast.next = this.head}} else {let previous = this.getNodeAt(index - 1)current = previous.nextprevious.next = current.next}this.count--return current.element}return undefined}
}
(5)>>集合
Set结构,其实已经是JavaScript里封装好的,它是以值:值进行存储,且存入的值不能够重复
A.封装代码
class Set {items = {}add(element) {if (!this.has(element)) {this.items[element] = elementreturn true}return false}delete(element) {if (this.has(element)) {delete this.items[element]return true}return false}has(element) {return element in this.items}clear() {this.items = {}}size() {return Object.keys(this.items).length}values() {return Object.values(this.items)}
}const mySet = new Set()
let arr = [2, 1, 3, 3, 4, 5, 2, 1]
arr.forEach((item) => {mySet.add(item)
})
console.log(mySet.values());//[ 1, 2, 3, 4, 5 ]
B.原装set使用
Set结构生成的是一个迭代器的结构,并且由于是集合,可以进行相关的并集交集差集等运算,可以进行相关的,详细的相关使用请查看下方代码
let mySet = new Set()
mySet.add(1)
mySet.add(2)
mySet.add(3)let item = mySet.values() //生成的是一个迭代器
console.log(item); //[Set Iterator] { 1, 2, 3 }
console.log(item.next()); //{ value: 1, done: false }
console.log(item.next()); //{ value: 2, done: false }
console.log(item.next()); //{ value: 3, done: false }// for (let i of item) {
// console.log(i);
// } //1 2 3// console.log([...item]); //[ 1, 2, 3 ]// const A = new Set([1, 2, 3, 4])
// const B = new Set([2, 3, 4, 5])
// //取并集
// const C = new Set([...A, ...B])
// console.log(C); //Set(5) { 1, 2, 3, 4, 5 }// //取交集
// const D = new Set([...A].filter(item => B.has(item)))
// console.log(D); //Set(3) { 2, 3, 4 }// //取差集
// const E = new Set([...A].filter(item => !B.has(item)))
// console.log(E); //Set(1) { 1 }
(6)>>字典
字典和集合很相似,集合以[值,值]的形式存储元素,字 典则是以[键,值]的形式来存储元素。字典也称作映射、符号表或关联数组。
A.字典的封装
class Dictionary {table = {}//保证当存入的键是对象的时候,强制它转换为json字符串的形式toStrFn(item) {if (item === null) {return "null"} else if (item === undefined) {return "undefined"} else if (typeof item === 'string' || item instanceof String) {return item}return JSON.stringify(item)}hasKey(key) {return this.table[this.toStrFn(key)] != null}set(key, value) {if (key != null && value != null) {const tableKey = this.toStrFn(key)// this.table[tableKey] = valuethis.table[tableKey] = new ValuePair(key, value)return true}return false}get(key) {let result = this.table[this.toStrFn(key)]return result == null ? undefined : result.value}remove(key) {if (this.hasKey(key)) {delete this.table[this.toStrFn(key)]return true}return false}keys() {return this.keyValues().map(item => item.key)}values() {return this.keyValues().map(item => item.value)}keyValues() {return Object.values(this.table)}size() {return Object.keys(this.table).length}isEmpty() {return this.size() === 0}clear() {this.table = {}}forEach(ch) {const valuePair = this.keyValues()for (let i = 0; i < valuePair; i++) {ch(valuePair[i].key, valuePair[i].value)}}
}class ValuePair {constructor(key, value) {this.key = keythis.value = value}
}
B.散列表
HashMap 类,它是 Dictionary 类的一种散列表 实现方式。.散列算法的作用是尽可能快地在数据结构中找到一个值。这样在面对海量数据的时候可以加快查询的速度
class HashTable {table = {}toStrFn(item) {if (item === null) {return "null"} else if (item === undefined) {return "undefined"} else if (typeof item === 'string' || item instanceof String) {return item}return JSON.stringify(item)}// hashCode(key) {// if (typeof key === 'number') {// return key// } else {// const tableKey = this.toStrFn(key)// let hash = 0// for (let i = 0; i < tableKey.length; i++) {// hash += tableKey.charCodeAt(i)// }// return hash % 35// }// }//将对应字符串或对象转成的字符串的ASCII值进行转换生成相应的数值hashCode(key) {const tableKey = this.toStrFn(key)let hash = 5381for (let i = 0; i < tableKey.length; i++) {hash = (hash * 33) + tableKey.charCodeAt(i)}return hash % 1013}set(key, value) {if (key != null && value != null) {let position = this.hashCode(key)this.table[position] = new ValuePair(key, value)}}get(key) {let result = this.table[this.hashCode(key)]return result == null ? undefined : result.value}remove(key) {if (this.table[this.hashCode(key)] != null) {delete this.table[this.hashCode(key)]return true}return false}
}class ValuePair {constructor(key, value) {this.key = keythis.value = value}
}
3.ES6中的Map
var mymap = new Map()
mymap.set("name","kerwin")
mymap.set({a:1},"aaaaaa")mymap.get("name")
mymap.delete("name")
(7)>>树
树是一种分层数据的抽象模型。
1.二叉树
二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。
2.二叉搜索树
二叉搜索树(BST)是二叉树的一种,但是只允许你在左侧节点存储(比父节点)小的值, 在右侧节点存储(比父节点)大的值。
>>关于遍历
中序遍历是一种以上行顺序访问 BST 所有节点的遍历方式,也就是以从最小到最大的顺序 访问所有节点。 中序遍历的一种应用就是对树进行排序操作。
先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构 化的文档。(访问根节点,遍历左子树,遍历右子树)
后序遍历则是先访问节点的后代节点,再访问节点本身。后序遍历的一种应用是计算一个目 录及其子目录中所有文件所占空间的大小。(简而言之就是在树中从最末尾的那一代开始从左到右打印,一直到整个树都被遍历完成)
>>关于移除要考虑的情况
A.封装代码
class Node {constructor(key) {this.key = keythis.left = nullthis.right = null}
}class BST {constructor() {this.root = null}insert(key) {if (this.root == null) {this.root = new Node(key)} else {this.insertNode(this.root, key)}}insertNode(node, key) {if (this.compareFn(key, node.key) === -1) {if (node.left == null) {node.left = new Node(key)} else {this.insertNode(node.left, key)}} else {if (node.right == null) {node.right = new Node(key)} else {this.insertNode(node.right, key)}}}compareFn(a, b) {if (a === b) {return 0}return a < b ? -1 : 1}//中序遍历,注意递归调用的顺序middleMap(callback) {this.middleMapNode(this.root, callback)}middleMapNode(node, callback) {if (node != null) {this.middleMapNode(node.left, callback)callback(node.key)this.middleMapNode(node.right, callback)}}//先序遍历prevMap(callback) {this.prevMapNode(this.root, callback)}prevMapNode(node, callback) {if (node != null) {callback(node.key)this.prevMapNode(node.left, callback)this.prevMapNode(node.right, callback)}}//后序遍历behindMap(callback) {this.behindMapNode(this.root, callback)}behindMapNode(node, callback) {if (node != null) {this.behindMapNode(node.left, callback)this.behindMapNode(node.right, callback)callback(node.key)}}min() {return this.minNode(this.root)}minNode(node) {let current = nodewhile (current != null && current.left != null) {current = current.left}return current.key}max() {return this.maxNode(this.root)}maxNode(node) {let current = nodewhile (current != null && current.right != null) {current = current.right}return current.key}search(key) {return this.searchNode(this.root, key)}searchNode(node, key) {if (node === null) {return false}if (this.compareFn(key, node.key) === -1) {return this.searchNode(node.left, key)} else if (this.compareFn(key, node.key) === 1) {return this.searchNode(node.right, key)} else {return true}}remove(key) {this.removeNode(this.root, key)}removeNode(node, key) {if (node == null) {return null}if (this.compareFn(key, node.key) === -1) {node.left = this.removeNode(node.left, key)console.log(node);return node} else if (this.compareFn(key, node.key) === 1) {node.right = this.removeNode(node.right, key)console.log(node);return node} else {if (node.left == null && node.right == null) {node = nullreturn node}if (node.left == null) {node = node.rightreturn node} else if (node.right == null) {node = node.leftreturn node}const target = this.minNode(node.right)node.key = target.keynode.right = this.removeNode(node.right, target.key)return node}}
}const tree = new BST()
tree.insert(6)
tree.insert(4)
tree.insert(8)
tree.insert(3)
tree.insert(5)
tree.middleMap((value) => console.log(value)) //3 4 5 6 8
tree.prevMap((value) => console.log(value)) //6 4 3 5 8
tree.behindMap((value) => console.log(value)) //3 5 4 8 6
(8)>>二叉堆
二叉堆是一种特殊的二叉树,有两种特性
它是一棵完全二叉树,表示树的每一层都有左侧和右侧子节点(除了最后一层的叶节点), 并且最后一层的叶节点尽可能都是左侧子节点,这叫作结构特性。
二叉堆不是最小堆就是最大堆。最小堆允许你快速导出树的最小值,最大堆允许你快速 导出树的最大值。所有的节点都大于等于(最大堆)或小于等于(最小堆)每个它的子 节点。这叫作堆特性。
1.最小堆
//把二叉堆的数据按数组的格式存储起来
class minHeap {heap = []//根据现有节点计算左子节点getLeftSonIndex(index) {return 2 * index + 1}getRightSonIndex(index) {return 2 * index + 2}getParentIndex(index) {if (index === 0) {return undefined} else {return Math.floor((index - 1) / 2)}}insert(value) {if (value != null) {this.heap.push(value)this.shiftUp(this.heap.length - 1)return true}return false}shiftUp(index) {let parent = this.getParentIndex(index)while (index > 0 && this.compareFn(this.heap[parent], this.heap[index]) === 1) {this.swap(this.heap, parent, index)index = parentparent = this.getParentIndex(index)}}compareFn(a, b) {if (a === b) {return 0}return a < b ? -1 : 1}swap(arr, a, b) {let temp = arr[a]arr[a] = arr[b]arr[b] = temp}size() {return this.heap.length}isEmpty() {return this.size() === 0}findMinimun() {return this.heap[0]}//去除根节点及后面的操作extractFirst() {if (this.isEmpty()) {return undefined}if (this.size() === 1) {return this.heap.shift()}const removedValue = this.heap[0]this.heap[0] = this.heap.pop()this.shiftDown(0)return removedValue}shiftDown(index) {let current = indexlet left = this.getLeftSonIndex(index)let right = this.getRightSonIndex(index)if (left < this.size() && this.compareFn(this.heap[current], this.heap[left]) === 1) {current = left}if (right < this.size() && this.compareFn(this.heap[current], this.heap[right]) === 1) {current = right}if (index !== current) {this.swap(this.heap, index, current)this.shiftDown(current)}}
}
2.最大堆
可以偷个懒,直接继承并随便改写一下比较两数大小的方法
class maxHeap extends minHeap {constructor() {super()}compareFn(a, b) {if (a === b) {return 0}return a > b ? -1 : 1}
}
三.总结
数据结构是个神奇的东西,它充斥在代码中,却仿佛离我们那么遥远,学习数据结构不仅是在面对不同的数据时要施加不同的策略,而且可以提高我们的代码阅读能力。当然了,对于数据结构的学习依然任重而道远,各位同仁加油吧,希望可以点赞收藏加关注嘿嘿。