2023-10-21
计算机基础
00
请注意,本文编写于 473 天前,最后修改于 2 天前,其中某些信息可能已经过时。

目录

队列
链表
单向链表
环状链表、排序链表
双向链表
链表用途
集合
字典
散列表
AVL树
红黑树
Huffman树
B树
树的用途
图的表示法
图的遍历方法
图的算法
迪杰斯拉特算法
Floyd算法
最小生成树
Prim算法
Kruskal算法

本文主要参考《JavaScript数据结构与算法第三版》一书, 系统学习梳理JavaScript数据结构(栈、队列、链表、集合、字典、散列表、二叉搜索树、堆、图)及与数据结构相关的算法

栈的简易实现

ts
class Stack<T = any> { // 这里为了leetcode调试方便暂不使用es6私有属性 // #size = 0; _size = 0; get size() { return this._size; } set size(val: number) { this._size = val; } // accessor size = 0; 私有属性的简写形式 // 候选阶段,还不能使用 _items: { [key: number]: T } = Object.create(null); get isEmpty() { return this.size === 0; } constructor() {} push(element: T) { this._items[this.size] = element; this.size++; } pop(): T | undefined { if (this.isEmpty) return; const result = this._items[--this._size]; delete this._items[this._size]; return result; } peek(): T | undefined { if (this.isEmpty) return; return this._items[this._size - 1]; } clear() { this._items = Object.create(null); this._size = 0; } toString() { if (this.isEmpty) { return ''; } let str = `${this._items[0]}`; for (let i = 0; i < this.size; i++) { str += `, ${this._items[i]}`; } return str; } }

栈的用途

  1. 10进制转2进制 image.png
  2. v8 JS调用栈
  3. leetcode算法题
    • 20. 有效的括号
      1. 最长有效括号

队列

栈是后进先出, 队列是先进先出, 双端队列可以类比数组了两头都可以进和出。

ts
class Queue<T = any> { #count = 0; #lowest = 0; #items: { [key: number]: T } = Object.create(null); get isEmpty() { return this.#count === this.#lowest; } get size() { return this.#count - this.#lowest; } enqueue(element: T) { this.#items[++this.#count] = element; } dequeue() { if (this.isEmpty) return; const result = this.#items[this.#lowest + 1]; this.#lowest++; return result; } peek(): T | undefined { if (this.isEmpty) return; return this.#items[this.#lowest + 1]; } clear() { this.#count = 0; this.#lowest = 0; this.#items = Object.create({}); } toString() { if (this.isEmpty) { return ''; } let str = `${this.#items[this.#lowest + 1]}`; for (let i = this.#lowest + 2; i <= this.#count; i++) { str += `, ${this.#items[i]}`; } return str; } }

双端队列

ts
// class Deque extends Queue // 由于私有属性不能继承, 这里写一下抽象代码 class Deque<T> { addFront(element: T): void {} addBack(element: T): void {} removeFront(): void {} removeBack(): void {} peekFront(): T {} peekBack(): T {} }

队列的用途

  1. JS 事件循环
  2. 回文检查器
  3. leetcode

链表

JavaScript数组非常灵活,但数组的缺点是中间节点的删除或修改成本很高,因为需要移动元素。

链表存储有序的元素集合,但不同于数组,链表中的元素在内存中,并不是连续放置的。 每一个元素有存储元素本身的节点和指向下一个元素的引用组成。

单向链表

ts
export class MyNode<T = any> { public next: null | MyNode<T> = null; constructor(public element: T) {} } export class LinkedList<T = any> { public head: null | MyNode<T> = null; public count = 0; constructor(public equalsFn = (a: T, b: T) => a === b) {} push(element: T) { const node = new MyNode(element); let current: undefined | MyNode<T>; if (this.head == null) { this.head = node; } else { current = this.head; while (current.next) { current = current.next; } current.next = new MyNode(element); } ++this.count; } getElementAt(index: number) { if (index >= 0 && index <= this.count) { let node = this.head; for (let i = 0; i < index && node != null; i++) { node = node.next; } return node; } return null; } insert(element: T, index: number) { if (index >= 0 && index <= this.count) { const node = new MyNode(element); if (index === 0) { const current = this.head; node.next = current || null; this.head = node; } else { const prev = this.getElementAt(index - 1)!; node.next = prev.next; prev.next = node; } this.count++; return true; } return false; } removeAt(index: number) { if (index >= 0 && index < this.count) { let current = this.head; if (index === 0) { this.head = current!.next || null; } else { const prev = this.getElementAt(index - 1)!; current = prev.next!; prev.next = current!.next; } this.count--; return current!.element; } return undefined; } indexOf(element: T) { let current = this.head; for (let i = 0; i < this.size() && current != null; i++) { if (this.equalsFn(element, current.element)) { return i; } current = current.next || null; } return -1; } isEmpty() { return this.size() === 0; } size() { return this.count; } getHead() { return this.head; } clear() { this.head = null; this.count = 0; } toString() { if (this.head == null) { return ''; } let objString = `${this.head.element}`; let current = this.head.next; for (let i = 1; i < this.size() && current != null; i++) { objString = `${objString},${current.element}`; current = current.next; } return objString; } }

环状链表、排序链表

ts
export class CircularLinkedList<T = any> extends LinkedList { constructor() { super(); } insert(element: T, index: number) { if (index >= 0 && index <= this.count) { const node = new MyNode(element); let current = this.head; if (index === 0) { if (this.head === null) { this.head = node; node.next = this.head; } else { node.next = current; current = this.getElementAt(this.size())!; this.head = node; current.next = this.head; } } else { // 这种场景没有变化 const prev = this.getElementAt(index - 1)!; node.next = prev.next; prev.next = node; } this.count++; return true; } return false; } removeAt(index: number) { if (index >= 0 && index < this.count) { let current = this.head!; if (index === 0) { // 需要维护环结构 if (this.size() === 1) { this.head = null; } else { const removed = this.head!; current = this.getElementAt(this.size())!; this.head = this.head!.next; current.next = this.head; current = removed; } } else { // 不需要维护环结构 const prev = this.getElementAt(index - 1)!; prev.next = prev.next!.next; } this.count--; return current.element; } return undefined; } } const Compare = { LESS_THAN: -1, BIGGER_THEN: 1, }; function defaultCompare(a, b) { if (a === b) { return 0; } return a < b ? Compare.LESS_THAN : Compare.BIGGER_THEN; } export class SortedListList<T = any> extends LinkedList { constructor( public equalsFn = (a: T, b: T) => a === b, public compareFn = defaultCompare ) { super(equalsFn); } insert(element: T, index = 0) { if (this.isEmpty()) { return super.insert(element, index); } const pos = this.getIndexNextSortedElement(element); return super.insert(element, pos); } getIndexNextSortedElement(element: T): number { let current = this.head!; let i = 0; for (; i < this.size(); i++) { const comp = this.compareFn(element, current.element); if (comp === Compare.LESS_THAN) { return i; } current = current.next!; } return i; } }

双向链表

ts
export class LinkNode<T = any> { public next: LinkNode<T> | null = null; public prev: LinkNode<T> | null = null; constructor(public value: T) { this.value = value; } toString() { return `Node(${this.value})`; } } export class DoubleLinkList<T = any> { public get size() { return this.#size; } #size = 0; private head: LinkNode<T> | null = null; private tail: LinkNode<T> | null = null; private init(element: T) { this.tail = this.head = new LinkNode(element); this.head.next = null; this.head.prev = null; this.#size = 1; } append(element: T) { if (this.tail === null) { this.init(element); return; } const node = new LinkNode(element); this.tail.next = node; node.prev = this.tail; this.tail = this.tail.next; this.tail.next = null; this.#size += 1; } appendFront(element: T) { if (this.head === null) { this.init(element); return; } const node = new LinkNode(element); this.head.prev = node; node.next = this.head; this.head = this.head.prev; this.#size += 1; } remove(element: T | null = null) { // 链表是空 if (this.head === null) { return null; } // 默认删除最后一个 let node: LinkNode<T>; if (element === null) { node = this.tail!; } else { node = this.findNode(element); } // 只有一个节点 移除 if (this.head === this.tail) { if (this.head.value === node.value) { this.head = this.tail = null; this.#size = 0; return node; } return null; } if (node === this.head) { return this.unshift(); } if (node === this.tail) { return this.pop(); } // 移除中间节点 node.next!.prev = node.prev; node.prev!.next = node.next; this.#size -= 1; return node; } findNode(element: T): LinkNode<T> { let current = this.head!; while (current.next) { if (element === current.value) { return current; } current = current.next; } throw new Error(); // } unshift() { // 没有元素 if (this.head === null) return null; // 只有一个元素 或 没有元素 const node = this.head; if (this.tail === this.head) { this.head = this.tail = null; } else { this.head = this.head.next!; this.head.prev = null; } this.#size -= 1; return node; } pop() { // 没有元素 if (this.tail === null) return null; const node = this.tail; // 只有一个元素 if (this.tail === this.head!) { this.head = this.tail = null; } else { this.tail = this.tail.prev!; this.tail.next = null; } this.#size -= 1; return node; } toString() { const result: string[] = []; let start = this.head; while (start) { result.push(start.toString()); start = start.next; } return result.join(' <==> '); } }

链表用途

  1. 判断一个链表是否有环
    • 设置两个变量 一个跑1步,一个跑两步,如果两者相等则存在环
  2. 移除链表倒说第N个元素
    • 设置两个变量 一个先跑N步,当先跑的到头, 则另一个变量就是需要操作的
    • leetcode 19
  3. leetcode
    • leetcode 2. 两数之和
    • leetcode 21. 合并两个有序链表
    • leetcode 23. 合并 K 个升序链表
    • leetcode 25. K 个一组翻转链表
  4. 通过链表实现栈
ts
import { DoubleLinkList } from './doubleLinkedList'; export class StackLinkedList<T> { public items = new DoubleLinkList<T>(); constructor() {} push(element: T) { this.items.append(element); } pop() { if (this.items.size) { return undefined; } return this.items.remove(); } }
  1. ReactJS使用了大量的链表 effectList
  2. 缓存算法 LFU (高速缓存的替换策略、虚拟内存替换算法)
  3. Linux内存管理 Buddy内存交换算法

集合

集合是由一组无序且唯一(即不能重复)的项组成。

JavaScript 提供了Set

  • add()
  • delete()
  • has()
  • clear()
  • size
  • values()

集合的运算 交叉并补

字典

字典和集合很类似,集合以【值、值】的形式储存元素,字典以【键、值】的形式存储元素,字典也称作映射、符号表或关联数组

与Set类似,JavaScript提供了Map类

散列表

散列表也称作HashMap、HashTable

散列算法的作用是尽可能快地在数据结构中找到一个值。在之前的数据结构中获得一个值 (使用get方法),需要迭代整个数据结构来找到它。如果使用散列函数,就知道值的具体位置,因此能够快速检索到该值。 散列函数的作用是给定一个键值, 然后返回值在表中的地址。

散列表有一些在计算机科学中应用的例子。因为它是字典的一种实现,所以可以用作关联数组。它也可以用来对数据库进行索引。当我们在关系型数据库(如MySQL、Microsoft SQL Server、Oracle,等等)中创建一个新的表时,一个不错的做法是同时创建一个索引来 更快地查询到记录的key。在这种情况下,散列表可以用来保存键和对表中记录的引用。另一个很常见的应用是使用散列表来表示对象。JavaScript语言内部就是使用散列表来表示每个对象。此时,对象的每个属性和方法(成员)被存储为key对象类型,每个key指向对应的对象成员。

在散列表中hash可能存在冲突问题, 可以通过分离链表、线性探查、双散列表的方式解决冲突

  1. 分离链表

image.png

  1. 线性探查

线性探查技术有两种方式

  • 软删除,随着操作的进行,散列表的效率会降低
  • 删除后移动位置, 虽然优化了查找效率,但添加时候增加的工作量

JavaScript数组会动态扩容。

创建性能更好的散列函数要考虑的因素

  • 插入和检索的时间
  • 较低的冲突

比lose lose更好的散列函数是djb2

  1. 双散列表

如果存在hash冲突就继续使用散列算法

WeakMap 弱引用、key只能是对象、不提供遍历方法

  • 数组插入节点开销比较大;链表插入节点降低了开销,但增加了查找的开销。有没有折中的方案,插入小于数组开销查找小于链表开销? 这就用到了二叉树搜索树
  • 二叉树: 树中的节点组多有两个
  • 二叉搜索树BST: 二叉树的一种,左边只能存储比父节点小的元素,右边存储比节点大的元素。

二叉搜索树示例代码, 测试用例

ts
export class TreeNode<T = any> { public left: TreeNode<T> | null = null; public right: TreeNode<T> | null = null; constructor(public key: T) {} } export class BinarySearchTree<T = any> { public root: TreeNode<T> | null = null; constructor(public compareFn = (a, b) => a - b) { this.root = null; } insert(key: T): void { if (this.root === null) { this.root = new TreeNode(key); } else { this.insertNode(this.root!, key); } } insertNode(node: TreeNode, key: T) { if (this.compareFn(key, node.key) > 0) { if (node.right === null) { node.right = new TreeNode(key); } else { this.insertNode(node.right, key); } } else { if (node.left === null) { node.left = new TreeNode(key); } else { this.insertNode(node.left, key); } } } search(key: T, node = this.root): boolean { if (!node) return false; let current = node; const diff = this.compareFn(key, current.key); if (diff === 0) { return true; } const currentNode = diff > 0 ? current.right : current.left; return this.search(key, currentNode!); } min(): TreeNode<T> { if (this.root == null) throw new Error(); let current = this.root; while (current.left) { current = current.left; } return current; } max(): TreeNode<T> { if (this.root == null) throw new Error(); let current = this.root; while (current.right) { current = current.right; } return current; } remove(key: T) { this.root = this.removeNode(this.root, key); } removeNode(node: TreeNode<T> | null, key: T): TreeNode<T> | null { if (!node) return null; const diff = this.compareFn(key, node.key); if (diff > 0) { node.right = this.removeNode(node.right, key); return node; } if (diff < 0) { node.left = this.removeNode(node.left, key); return node; } if (node.left === null && node.right === null) { return null; } if (node.left === null) { return node.right; } if (node.right === null) { return node.left; } const aux = this.minNode(node.right); node.key = aux.key; this.removeNode(node.right, aux.key); return node; } minNode(node: TreeNode<T>): TreeNode<T> { let current = node; while (current.left) { current = current.left; } return current; } // 先序遍历 打印结构化文档 preOrderTraverse(callback: Function): void { this.preOrderTraverseNode(this.root, callback); } // 中序遍历 从小到大排序 inOrderTraverse(callback: Function): void { this.inOrderTraverseNode(this.root, callback); } // 后序遍历 统计文件夹大小 postOrderTraverse(callback: Function): void { this.postOrderTraverseNode(this.root, callback); } preOrderTraverseNode(node: TreeNode<T> | null, callback: Function) { if (node !== null) { callback(node.key); this.preOrderTraverseNode(node.left, callback); this.preOrderTraverseNode(node.right, callback); } } inOrderTraverseNode(node: TreeNode<T> | null, callback: Function) { if (node !== null) { this.inOrderTraverseNode(node.left, callback); callback(node.key); this.inOrderTraverseNode(node.right, callback); } } postOrderTraverseNode(node: TreeNode<T> | null, callback: Function) { if (node !== null) { this.postOrderTraverseNode(node.left, callback); this.postOrderTraverseNode(node.right, callback); callback(node.key); } } }

如果不对二叉搜索树进行维护,极端情况二叉搜索树会蜕变成链表结构,查找效率蜕变为O(n)O(n)。这就用到了AVL自平衡树。

AVL树

AVL树是一种BST,在插入元素或者删除元素需要维护树的平衡,任何一个节点左子树和右子树的高度最多差1。

AVLTree可以继承BST,覆盖insertinsertNoderemoveNode方法,其他的BST方法都会被AVLTree继承

有四种场景

  • LL 向右单旋转 比如依次插入[3,2,1]
  • RR 向左单旋转 比如一次插入[1,2,3]
  • LR 向右双旋转 (先LL旋转,在RR旋转) 比如往<这种结构插入叶子结点, 先LL变成 结构1,然后 LL旋转 image.png
  • RL 向左双旋转 (先RR旋转,在LL旋转)比如往>这种结构插入叶子结点, 先LL变成 结构1,然后 LL旋转 image.png

AVL树的简易实现

ts
import { BinarySearchTree, TreeNode } from './binarySearchTree'; enum BalanceFactor { UNBALANCED_RIGHT = -2, SLIGHTLY_UNBALANCED_RIGHT = -1, BALANCED = 0, SLIGHTLY_UNBALANCED_LEFT = 1, UNBALANCED_LEFT = 2, } export default class AVLTree<T = any> extends BinarySearchTree { constructor(public compareFn = (a, b) => a - b) { super(compareFn); this.root = null; } getNodeHeight(node: TreeNode<T> | null = null) { if (node === null) { return -1; } return ( Math.max(this.getNodeHeight(node.left), this.getNodeHeight(node.right)) + 1 ); } getBalanceFactor(node: TreeNode<T>) { const heightDifference: BalanceFactor = this.getNodeHeight(node.left) - this.getNodeHeight(node.right); return heightDifference; } // 向右单旋转 // 该node的Height为+2 要插入的数字是1 rotationLL(node: TreeNode<T>) { // 3 // 2 => 2 // 1 1 3 const tmp = node.left!; // 2 node.left = tmp.right; // 2.5 tmp.right = node; // 3 return tmp; // 2 } // 向左单旋转 // 该node的Height为-2 要插入的数字是3 rotationRR(node: TreeNode<T>) { // 1 // 2 => 2 // 3 1 3 const tmp = node.right!; // 2 node.right = tmp.left; // 1.5 tmp.left = node; // 1 return tmp; } // 向右双旋转 感觉应该叫 向左旋转+向右旋转才对 ?? // 该node的Height为2 要插入的数字是 2 rotationLR(node: TreeNode<T>) { // 3 3 2 // 1 c4 rotateLeft=> 2 c4 rotateRight=> 1 3 // c1 2 1 c3 c1 c2 c3 c4 // c1 c3 c1 c2 node.left = this.rotationRR(node.left!); return this.rotationLL(node); } // 向左双旋转 // 该node的Height为-2 要插入的数字是 2 rotationRL(node: TreeNode<T>) { // 1 1 2 // c1 3 rotateRight=> c1 2 rotateLeft=> 1 3 // 2 c4 c2 3 c1 c2 c3 c4 // c2 c3 c3 c4 node.right = this.rotationLL(node.right!); return this.rotationRR(node); } insert(key: T) { this.root = this.insertNode(this.root, key); } insertNode(node: TreeNode<T> | null, key: T): TreeNode<T> { // 插入同BST if (node == null) { return new TreeNode(key); } else if (this.compareFN(key, node.key) < 0) { node.left = this.insertNode(node.left, key); } else if (this.compareFN(key, node.key) > 0) { node.right = this.insertNode(node.right, key); } else { return node; // 重复的键 } // 如果需要对树进行平衡操作 const balanceFactor = this.getBalanceFactor(node); if (balanceFactor === BalanceFactor.UNBALANCED_LEFT) { if (this.compareFN(key, node.left!.key) < 0) { node = this.rotationLL(node); } else { return this.rotationLR(node); } } if (balanceFactor === BalanceFactor.UNBALANCED_RIGHT) { if (this.compareFn(key, node.right!.key) > 0) { node = this.rotationRR(node); } else { return this.rotationRL(node); } } return node; } removeNode(node: TreeNode<T> | null, key: T) { node = super.removeNode(node, key); if (node === null) return null; // 检查树是否平衡 const balanceFactor = this.getBalanceFactor(node); if (balanceFactor === BalanceFactor.UNBALANCED_LEFT) { const balanceFactorLeft = this.getBalanceFactor(node.left!); if ( balanceFactorLeft === BalanceFactor.BALANCED || balanceFactorLeft === BalanceFactor.SLIGHTLY_UNBALANCED_LEFT ) { return this.rotationLL(node); } if (balanceFactorLeft === BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT) { return this.rotationLR(node.left!); } } if (balanceFactor === BalanceFactor.UNBALANCED_RIGHT) { const balanceFactorRight = this.getBalanceFactor(node.right!); if ( balanceFactorRight === BalanceFactor.BALANCED || balanceFactorRight === BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT ) { return this.rotationRR(node); } if (balanceFactorRight === BalanceFactor.SLIGHTLY_UNBALANCED_LEFT) { return this.rotationRL(node.right!); } } return node; } }

测试用例

如何知道二叉树的结构呢?

给每一个节点标记权重,叶子结点权重为0, 非叶子节点的权重等与左子节点路径长度减去右子节点路径长度

image.png

所以新插入节点要往上更新权重,如果跟节点权重为+2-2则需要平衡

红黑树

对于搜索很频繁、插入删除很少的场景AVL树的应用场景,但对于插入删除频繁,查询很少的场景就要使用红黑树了。

红黑树的规则

  1. 每个节点不是红的就是黑的
  2. 树的根节点是黑的
  3. 所有叶子结点都是黑的(用Null引用表示的结点)
  4. 如果一个节点是红的,则它的两个字节点都是黑的,
  5. 不能出现连续红,可以出现连续黑,
  6. 从给定节点到它的后代节点,所有路径包含相同的黑节点

插入节点

  1. 先查找,确定插入的位置
  2. 如果新节点是根 -- 染为黑色
  3. 新节点非根 -- 染为红色
  4. 新插入的节点满足红黑树定义 则插入结束
  5. 不满足红黑树定义则需要调整
  6. 黑叔 旋转+染色 旋转规则同AVL
    • LL型 父换爷 + 染色
    • RR型 父换爷 + 染色
    • LR型 子换爷 + 染色
    • RL型 子换爷 + 染色
  7. 红叔 染色+变新 染色规则看叔叔的脸色
    • 叔父爷染色,爷变为新节点

image.png

红黑树的简易实现

ts
import { BinarySearchTree, TreeNode } from './binarySearchTree'; /** * 对于搜索频次很少,插入删除频繁的场景使用红黑树 * 红黑树的规则 * 1. 根叶黑 * 2. 左根右 * 3. 不红红 * 4. 黑路通 */ enum Colors { RED = 'RED', BLACK = 'BLACK', } export class RedBlackNode<T = any> extends TreeNode { public parent: RedBlackNode<T> | null = null; public left: RedBlackNode<T> | null = null; public right: RedBlackNode<T> | null = null; constructor( public key: T, public color = Colors.BLACK ) { super(key); } isRED() { return this.color === Colors.RED; } } export class RedBlackTree<T = any> extends BinarySearchTree { root: (RedBlackNode<T> & {}) | null = null; constructor(public compareFn = (a, b) => a - b) { super(compareFn); this.root = null; } insert(key: T) { if (this.root === null) { this.root = new RedBlackNode(key); this.root.color = Colors.BLACK; } else { const newNode = this.insertNode(this.root, key); this.fixTreeProperties(newNode); } } insertNode(node: RedBlackNode<T>, key: T): RedBlackNode<T> | null { if (this.compareFn(key, node.key) < 0) { if (node.left === null) { node.left = new RedBlackNode(key, Colors.RED); node.left.parent = node; return node.left; } else { return this.insertNode(node.left, key); } } else { if (node.right === null) { node.right = new RedBlackNode(key, Colors.RED); node.right.parent = node; return node.right; } else { return this.insertNode(node.right, key); } } } fixTreeProperties(node: RedBlackNode<T> | null) { // 是否破坏 不红红 原则 while (node?.parent?.isRED() && node.color !== Colors.BLACK) { let parent = node.parent; const grandParent = parent.parent; // A 父节点是左侧子节点 if (grandParent && grandParent.left === parent) { const uncle = grandParent.right; // 叔节点也是红色--只需要重新染色 if (uncle && uncle.color === Colors.RED) { grandParent.color = Colors.RED; parent.color = Colors.BLACK; uncle.color = Colors.BLACK; node = grandParent; // 将爷节点视为新插入的节点 } else { // 2A 节点是右侧子节点--左旋转 if (parent.right === node) { // LR型 this.rotationRR(parent); this.rotationLL(grandParent); // 染色 儿和爷节点 node.color = Colors.BLACK; node.right!.color = Colors.RED; } else { // 3A 节点是左侧子节点--右旋转 // 黑叔 右单旋父换爷 染色 this.rotationLL(grandParent); node.parent!.color = Colors.BLACK; node.parent!.right!.color = Colors.RED; } } } else { // B父节点是右侧子节点 插入30 //叔父爷同时变色 const uncle = grandParent!.left; if (uncle && uncle.color === Colors.RED) { grandParent!.color = Colors.RED; parent.color = Colors.BLACK; uncle.color = Colors.BLACK; node = grandParent; } else { // 2B 节点是左侧子节点--右旋转 if (parent.left === node) { this.rotationLL(parent); this.rotationRR(grandParent!); // 染色 儿和爷节点 todo check node.color = Colors.BLACK; node.left!.color = Colors.RED; } else { // 3B 节点是右侧子节点--左旋转 this.rotationRR(grandParent!); node.parent.color = Colors.BLACK; node.parent.left!.color = Colors.RED; } } } } this.root!.color = Colors.BLACK; } rotationLL(node: RedBlackNode<T>): RedBlackNode<T> { const tmp = node.left!; node.left = tmp.right; if (tmp.right && tmp.right.key) { tmp.right.parent = node; } tmp.parent = node.parent; if (!node.parent) { this.root = tmp; } else { if (node === node.parent.left) { node.parent.left = tmp; } else { node.parent.right = tmp; } } tmp.right = node; node.parent = tmp; return tmp; } rotationRR(node: RedBlackNode<T>): RedBlackNode<T> { const tmp = node.right!; node.right = tmp.left; if (tmp.left?.key) { tmp.left.parent = node; } tmp.parent = node.parent; if (!node.parent) { this.root = tmp; } else { if (node === node.parent.left) { node.parent.left = tmp; } else { node.parent.right = tmp; } } tmp.left = node; node.parent = tmp; return tmp; } // todo removeNode }

测试用例

参考资料: 红黑树的插入

Huffman树

HuffmanTree翻译成中文是赫夫曼树,也叫哈夫曼树,还叫霍夫曼树。

它的定义是:

给定n个权值作为n的叶子结点,构造一颗二叉树,使得该树的WPL(带权路径长度)达到最小,称这样的二叉树为最优二叉树。

赫夫曼树是带权路径长度最短的树,权值较大的节点离根较近。

赫夫曼树的一些概念

  • 路径和路径长度:在一颗树中从一个节点往下,可以达到孩子节点或孙子节点之间的通路,成为路径。通路中分支的路径数目称之为路径的长度,若规定跟节点的层数为1,则从跟节点到第L层节点的路径长度为L-1
  • 节点的权: 若树中的节点赋给一个某种有含义的树枝,则这个数值就是节点的权。
  • 带权路径长度: 节点的权值 * (节点所在的层L-1),

举个例子,下图中间的树wpl最小,才是 HuffmanTree

image.png

注意:权重最小的树可能有多个, 如上图3与7交换位置,wpl不变,都是赫夫曼树。

那么赫夫曼树有什么用呢? 它是文件无损压缩的原理! 我们先来用代码实现一下,再解释文件压缩的原理。

给定一个数列{13, 7, 8, 3, 29, 6 1}, 要求转为一个HuffmanTree

思路分析:

  1. 从小到大排序,每一个数据都是一个节点,每一个节点可以看成是一个最简单的二叉树。
  2. 取出根节点权值最小的两颗二叉树,
  3. 组成一个新的二叉树,该新的二叉树根节点的权值是前面两个二叉树根节点权值之和
  4. 再将这颗新的二叉树,以跟节点权值大小再次排序,不断重复1,2,3,4的步骤,直到数列中所有的数据都被处理,就得到一颗HuffmanTree

代码实现

ts
export class TreeNode<T> { /** * char目前用不到,文件压缩的时候会用到 * null表示非叶子节点 */ public char: string | null = null; public left: TreeNode<T> | null = null; public right: TreeNode<T> | null = null; constructor(public key: T | null = null) {} } export function createHuffmanTree(arr: number[]) { arr.sort((a, b) => a - b); const trees: TreeNode<number>[] = arr.map((it) => new TreeNode(it)); while (trees.length > 1) { const leftNode = trees.shift()!; const rightNode = trees.shift()!; const parentNode = new TreeNode(leftNode.key! + rightNode.key!); parentNode.left = leftNode; parentNode.right = rightNode; binaryInsert(trees, parentNode); } return trees[0]; function binaryInsert( trees: TreeNode<number>[], tree: TreeNode<number>, left = 0, right = trees.length - 1 ) { if (!trees.length) { trees.push(tree); return; } if (tree.key === trees[left].key) { trees.splice(left + 1, 0, tree); return; } if (tree.key === trees[right].key) { trees.splice(right + 1, 0, tree); return; } if (tree.key! < trees[left].key!) { trees.splice(left, 0, tree); return; } if (tree.key! > trees[right].key!) { trees.splice(right + 1, 0, tree); return; } let middle = (left + right) >> 1; if (tree.key === trees[middle].key) { trees.splice(middle + 1, 0, tree); return; } if (tree.key! > trees[middle].key!) { return binaryInsert(trees, tree, middle + 1, right - 1); } return binaryInsert(trees, tree, left + 1, middle - 1); } }

文件压缩原理

为了简单期间我们不考虑中文,只考虑ASCI码,即8bit表示一个字符,那么 一个文件的大小由字符数决定, 那么如何能减少文件大小呢?

一个简单的方案是允许<=8bit表示一个字符(可变长编码),经过一种编码转换后的字符数小于原字符数,从而达到压缩文件的效果。

可变长编码还要满足前缀编码要求,即不能出现两个相同前缀的的字符,这样解码时字符识别就不会有冲突,赫夫曼编码正好满足前缀编码的要求。

Huffman编码

  • huffman编码是一种编码方式,属于一种程序算法
  • huffman编码是Huffman树在电讯通信中的经典应用之一
  • 它广泛用于数据文件压缩。其压缩率通常在20%-90%之间
  • 它是可变字长编码(VCL)的一种。

为什么Huffman编码满足前缀编码要求

  • 在Huffman树中规定向左的路径为0,向右的路径为1
  • 只有叶子节点才参与编码,由于使用频繁的字符放在与根节点较近的叶子上,即使用的编码较短,这就实现了文件压缩的效果。
  • 因为非叶子节点不表示字符,所以对叶子节点的编码就没有公共前缀问题,即满足前缀编码

文件压缩具体如何实现呢?

  1. 将字符串转换为字符数组
  2. 计算每一个字符出现的频次(这个频次就是Huffman树中的节点权重),生成 字符=》权重权重=》字符的map映射,。
  3. 从小到大排序权重,生成 HuffmanTree
  4. 再次读取字符数组,根据 HuffmanTree 生成新的字符数组(要短很多)
  5. 将新字符数组和HuffmanTree写入文件,完成压缩

解压缩

  1. 读取HuffmanTree,读取bit流,根据HuffmanTree的路径和char得到原字符,写入到文件中, 完成解压。

完整实现过程可以看这个视频

注意

  • 一些格式的文件如.ppt.webp本身已经进行了压缩,再次使用霍夫曼编码压缩基本没有效果
  • 一些文本格式文件如.xml.txt压缩效果会比较明显,尤其对重复内容比较多的文件来说。

B树

多叉树

二叉树查找效率较高,如果二叉树的节点少,没有什么问题。由于二叉树需要加载到内存,节点比较多,就存在如下问题 image.png

  1. 构建二叉树时需要多次进行IO操作(海量的数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响
  2. 节点很多,会造成二叉树高度很大,会降低操作速度

如果我们允许每个节点可以有更多数据项和更多子节点,就可以降低树的高度和IO操作, 这就是多叉树。

举例来说2-3树就是一个多叉树

image.png

  • 每个节点最多存放两个元素
  • 每个节点最多有三个子节点

B树

B是balance,平衡的意思,2-3树 2-3-4树也是B树。

B树通过重新组织节点,降低树的高度,并减少IO读写次数来提升效率

image.png

  • 文件系统及数据库系统的设计者利用磁盘预读原理,将一个节点的大小设为等于一个页(4K),这样每个节点只需要一次IO就可以完全载入

  • 将树的度设置为1024,在600亿个元素中,最多只需要4次IO操作就可以读取想要的元素,B树广泛应用于文件存储系统及数据库系统中

    • 树的度: 指其中节点的度最大值
    • 节点的度: 节点的子节点数量
  • B树的阶: 节点的最多子节点个数叫做阶。比如2-3树的阶就是3,2-3-4树的阶就是4;

  • B树的搜索:从根节点开始,对节点内的元素进行二分查找,如果找到就结束,否则进入查找元素所属范围的子节点再进行二分查找,直到找到或者到达叶子节点;

  • B树的所有节点都会存放数据;

除了B树外,还有B+树和B*树

  • B+树是B树的变种,只有叶子节点才存储数据,叶子节点是链表结构,链表中的数据也是有序的
  • 非叶子节点中存放的是索引,而不是要操作的数据,每个非叶子节点都会存放叶子节点的索引,也叫稀疏索引;
  • B+树一般用于文件系统;
  • B*树又是B+树的变体,就是在B+树的基础上,在非根非叶子节点之间增加了指向兄弟节点的指针。

在B树中比较核心的是2-3树

2-3树的特点

  1. 所有的叶子节点都在同一层(B树的特征)
  2. 每个节点最多储存两个元素,每个元素最多3个节点
  3. 有两个子节点的节点叫做二节点,二节点要么没有子节点,要么只有两个子节点
  4. 有三个子节点的节点叫做三节点,三节点要么没有子节点,要么有三个子节点。
  5. 2-3树由二节点和三节点构成的树

以数列{16,24,12,32,14,26,34,10,8,28,38,20}构建2-3树的过程

2-3tree.webp

本来打算自己实现一下2-3树,但是分析过来发现状态很多需要维护parentleftrightcentervals 等很多状态,一时半伙搞不定,但最主要的原因是这些工作都是体力活,没有特别的难点,所以暂且不尝试实现了。

树的用途

  • 比特币的交易树 MerkleTree
  • 以太坊的状态树 MPT(Merkle Patricia Tree)
  • MySql 索引的实现 B树的应用
  • DOM, AST 等应用都是对人脑分层分类认知的建模, 都是树
  • 文件压缩算法 (霍夫曼树)

堆是一种完全二叉树, 堆也叫二叉堆。

二叉堆是计算机数据结构中非常著名的数据结构, 它能高效快速的找到最大值和最小值,常被用于优先队列。它也被用于著名的堆排序算法中。

二叉堆的两个特性

  • 完全二叉树
  • 不是最小堆就是最大堆

堆的应用

图的分类

  • 有向图
  • 无向图

图的表示法

  1. 邻接矩阵
    • 缺点 比较耗费空间,删除节点很麻烦
    • 优点 逻辑严谨,可以表示有向图
  2. 邻接表 邻接矩阵的改进,节省空间
  3. 关联矩阵 适用于边比顶点多的场景 (也比较耗费空间)

图的示例代码

ts
export class Graph<T = any> { private vertices = new Set<T>(); // 顶点 private adjList = new Map<T, Set<T>>(); // 邻接表 constructor(public isDirected = false) {} addVertices(v: T) { if (!this.vertices.has(v)) { this.vertices.add(v); this.adjList.set(v, new Set<T>()); } } addEdge(v: T, w: T) { this.vertices.add(v); this.vertices.add(w); let u = this.adjList.get(v); if (!u) { u = new Set<T>(); this.adjList.set(v, u); } u.add(w); if (!this.isDirected) { let u2 = this.adjList.get(w); if (!u2) { u2 = new Set<T>(); this.adjList.set(w, u2); } u2.add(v); } } getVertices() { return this.vertices; } getAdjList() { return this.adjList; } toString() { let s = ''; this.vertices.forEach((v, i) => { s += `${v} -> `; this.adjList.get(v)!.forEach((w) => { s += w; }); s += `\n`; }); return s.length ? s.substring(0, s.length - 1) : s; } }

图的遍历方法

  1. 广度优先 队列
  2. 深度优先 递归+栈
ts
import { Queue } from './queue'; import { Stack } from './Stack'; export function breadthFirstSearch<T extends string = any>( graph: Graph<T>, startVertex: T, callback: Function ) { const vertices = graph.getVertices(); const adjList = graph.getAdjList(); const queue = new Queue<T>(); const colors = [...vertices].reduce( (sum, it) => { sum[it] = Colors.WHITE; return sum; }, {} as { [key: string]: Colors } ); queue.enqueue(startVertex); colors[startVertex] = Colors.GREY; callback(startVertex); while (queue.size) { const it = queue.dequeue()!; colors[it] = Colors.BLACK; const u = adjList.get(it)!; u.forEach((_it) => { if (colors[_it] === Colors.WHITE) { queue.enqueue(_it); colors[_it] = Colors.GREY; callback(_it); } }); } } export function depthFirstSearch<T = any>( graph: Graph, v: T, callback: Function ) { const stack = new Stack<T>(); const vertices = graph.getVertices(); const adjList = graph.getAdjList(); const colors = [...vertices].reduce( (sum, it) => { sum[it] = Colors.WHITE; return sum; }, {} as { [key: string]: Colors } ); dfs(v); function dfs(it: T) { colors[it] = Colors.GREY; stack.push(it); callback(it); const u = adjList.get(it)!; u.forEach((it) => { if (colors[it] === Colors.WHITE) { dfs(it); } }); stack.pop(); } }

图的算法

最短路径问题

  1. BFS广度优先算法 标记回溯点, 标记节点访问状态(未访问、已访问,已完全访问)
  2. dijkstra
  3. floyd

迪杰斯拉特算法

image

ts
export function dijkstra(graph: number[][], point: number) { // 记录从point到其他点的距离 // s是已完成计算的点, u时未完成计算的点 // 0表示未知距离 const s = new Set<[number, number]>(); const u = new Set<[number, number]>(); s.add([0, graph[point][0]]); graph[point].slice(1).forEach((it, index) => { u.add([index + 1, it]); }); const visited = {} as { [key: number]: boolean }; visited[0] = true; while (u.size) { let minIndex = 0; let minDistance = graph[point][minIndex] || Number.MAX_VALUE; [...u].forEach(([i, dis]) => { if (!visited[i] && dis && minDistance > dis) { minIndex = i; minDistance = dis; } }); graph[point].forEach((it, index) => { if (!visited[index] && it && minDistance > it) { minIndex = index; minDistance = it; } }); s.add([minIndex, minDistance]); visited[minIndex] = true; u.forEach((it) => { if (it[0] === minIndex) { u.delete(it); } }); // 比较 point --> i 与 point --> minIndex --> i之间的距离 u.forEach((it) => { const [i, distances] = it; if (graph[point][minIndex] && graph[minIndex][i]) { const newDistance = graph[point][minIndex] + graph[minIndex][i]; if (distances === 0) { it[1] = newDistance; // graph[point][i] = newDistance; } else if (distances > newDistance) { it[1] = newDistance; // graph[point][i] = newDistance; } } }); } return [...s].reduce((sum, it) => { const [index, dis] = it; sum[index] = dis; return sum; }, [] as number[]); }

Floyd算法

ts
/** * Flody算法:求出每一对顶点之间的最短路径 * 使用动态规划思想,将问题的求解分成多个阶段 * 对于n个顶点的图G,求任意一对顶点Vi -> Vj之间的最短路径可分为如下几个阶段: * #初始:不允许在其他顶点中转,最短路径是? * #0: 若允许在V0中转,最短路径是? * #1: 若允许在V0、V1中转,最短路径是? * 。。。。。。 * #n-1: 若允许在V0、V1、。。。、Vn-1中转,最短路径是? */ export function floyed(graph: number[][]) { // graph[i][j] 记录 i到j 的最短距离 // path[i][j] = x 记录从 i-->j 需要 x 中转 const path = Array.from( { length: graph.length, }, () => Array.from( { length: graph[0].length, }, () => -1 ) ); // 需要使用 0 - i个顶点进行中转 for (let k = 0; k < graph.length; k++) { // 遍历所有顶点之间的距离 for (let i = 0; i < graph.length; i++) { for (let j = 0; j < graph.length; j++) { if (i === j) continue; if (k === j || k === i) continue; if (graph[i][k] === Infinity || graph[k][j] === Infinity) { continue; } const newDistance = graph[i][k] + graph[k][j]; if (graph[i][j] > newDistance) { graph[i][j] = newDistance; path[i][j] = k; } } } } return { graph, path, }; }

最短路径算法总结 image.png

有权图的最短路径还有一种算法 DV矢量算法

最小生成树

图与树的区别

树其实是一种特殊的图,只有具备以下三个特点的图才是树

  1. 图必须是连通的
  2. 图必须是无环的
  3. 图必须是无项的

如果一个颗树有N个定点,那么它有且只有N-1条边,多一条边就会包含环结构,少一条边就没有连通所有顶点。

生成树

  1. 保留所有(N个)节点,保留(N-1)个边
  2. 保留的图是连通的,且没有回路

如果子图满足以上两个性质就是生成树

最小生成树

给定一个图,不一定只有一个生成树,在所有的生成树当中最小权重之和的那个树,才是最小生成树 miniTree.png

最小生成树有什么用呢? 比如有一个村庄想铺水泥树,要求每两家之间必须有一条水泥路,且铺设的总费用最低。

image.png

这就是最小生成树要解决的问题

最小生成树的算法

  1. Prim算法
  2. Kruskal算法

Prim算法

以前面的图结构为例

  1. 用集合U记录生成树的节点, 随便选择一个节点,假设是v1
  2. v1标记为红色,寻找与所有蓝色节点连接的最短边
  3. v1-v4最短,选择v4,将其染成红色,继续寻找红色节点与蓝色节点的最短连线。
  4. v1-v2 最短,循环执行步骤三直至将所有节点染成红色,这里依次选择的是 v4-v3v4-v7v7-v6v7-v5,这些节点与连线组成的图就是最小生成树。

算法实现

ts
export function prim(graph: number[][]) { const visited: boolean[] = []; // 已经选择的顶点 const result: { edge: [number, number]; weight: number; }[] = []; const { length } = graph; for (let i = 0; i < length; i++) { visited[i] = false; } visited[0] = true; for (let i = 0; i < length - 1; i++) { const [s, u] = minEdge(graph, visited); visited[u] = true; visited[s] = true; result.push({ edge: [s, u], weight: graph[s][u], }); } return result; function minEdge(graph: number[][], visited: boolean[]) { // vertices1 已访问的顶点 未访问的顶点 const [vertices1, vertices2] = visited.reduce( (sum, it, key) => { it ? sum[0].push(key) : sum[1].push(key); return sum; }, [[], []] as [number[], number[]] ); let s = 0, u = 0, minDistance = Infinity; for (let i = 0; i < vertices1.length; i++) { for (let j = 0; j < vertices2.length; j++) { const newDistance = graph[vertices1[i]][vertices2[j]]; if (newDistance && newDistance < minDistance) { minDistance = newDistance; s = i; u = j; } } } if (vertices1[s] > vertices2[u]) { return [vertices2[u], vertices1[s]]; } else { return [vertices1[s], vertices2[u]]; } } }

Kruskal算法

还是以前面的图为例子

image.png

  1. 创建一个队列 记录所有的边和权重, 这个队列按照权重从小到大排列
  2. 假设只有顶点没有边,每个顶点当成一棵树
  3. 从队列中取出一条边,如果连接的两个顶点不在一棵树上,则合并两颗树
  4. 如果取出的边在同一棵树则丢弃这条边,继续从队列中取出一条边
  5. 如果边的数量等于顶点数量减1,说明找到最小生成树树了,返回并终止程序。

代码实现

ts
export function kruskal(graph: number[][]) { type Item = { edge: [number, number]; weight: number; }; const bst = new BinarySearchTree<Item>((a: Item, b: Item) => { if (a.weight !== b.weight) { return a.weight - b.weight; } if (a.edge[0] !== b.edge[0]) { return a.edge[0] - b.edge[0]; } return a.edge[1] - b.edge[1]; }); for (let i = 0; i < graph.length - 1; i++) { for (let j = i + 1; j < graph.length; j++) { const weight = graph[i][j]; if (![0, Infinity].includes(weight)) { bst.insert({ edge: [i, j], weight, }); } } } const queue = new Queue<Item>(); bst.inOrderTraverse((it: Item) => { queue.enqueue(it); }); const treesVertices: number[][] = []; const result: { edge: [number, number]; weight: number; }[] = []; // 顶点 与 树(treesVertices的索引) 的映射关系 const map = Array.from({ length: graph.length }, () => -1); enum State { ONE_TREE, TWO_TREE, ONE_OTHER, OTHER, } while (result.length < graph.length - 1) { const { edge, weight } = queue.dequeue()!; const state = check(edge); // 0 表示两个顶点在已有的同一个树上 // 1 两个顶点在已有的不同的树上 // 2 一个顶点在已有的树上 // 3 两个顶点均不在一有的树上 if (state === State.ONE_TREE) { continue; } result.push({ edge, weight, }); switch (state) { case State.TWO_TREE: // 合并两个树 let treeIndex = map[edge[0]]; let treeIndex2 = map[edge[1]]; if (map[edge[0]] > map[edge[1]]) { treeIndex = map[edge[1]]; treeIndex2 = map[edge[0]]; } treesVertices[treeIndex2].forEach((v) => { map[v] = treeIndex; treesVertices[treeIndex].push(v); }); treesVertices.splice(treeIndex2, 1); break; case State.ONE_OTHER: // 这棵树加入新顶点 if (map[edge[0]] === -1) { map[edge[0]] = map[edge[1]]; treesVertices[map[edge[1]]].push(edge[0]); } else { map[edge[1]] = map[edge[0]]; treesVertices[map[edge[0]]].push(edge[1]); } break; case State.OTHER: // 新生成一棵树并加入 const treeVertices = edge.slice(); map[edge[0]] = treesVertices.length; map[edge[1]] = treesVertices.length; treesVertices.push(treeVertices); break; } } return result; function check(edge: [number, number]): State { const [a, b] = edge; if (map[a] === -1 || map[b] === -1) { if (map[a] === map[b]) { return State.OTHER; } else { return State.ONE_OTHER; } } if (map[a] === map[b]) { return State.ONE_TREE; } else { return State.TWO_TREE; } } }

本文作者:郭郭同学

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!