当前位置:   article > 正文

数据结构与算法:链表、树、图、堆、散列表_树 链表

树 链表

1 链表

链表是线性数据结构(数据元素之间存在着“一对一”关系),链表中的每个元素是一个包含数据data和引用字段的对象,引用字段只有next为单向链表,同时又prev和next为双向链表。

1.1 链表基本操作

链表读取第 i 个节点查找指定值的节点,均需要从头遍历,时间复杂度为O(n)。

在不考虑查找节点的过程的情况下更新(替换data),插入(新节点next指针指向插入位置节点,前一节点next指针指向新节点)、删除(前一节点next指针指向下一节点,当前节点置空),时间复杂度均为O(1)。

1.2 设计链表

设计单向链表的实现。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。假设链表中的所有节点都是 0 ~ size - 1 的。

  1. class LinkNode<T> {
  2. val: T;
  3. next: LinkNode<T> | null;
  4. constructor(val: T, next?: LinkNode<T> | null) {
  5. this.val = val;
  6. this.next = next ?? null;
  7. }
  8. }
  9. class MyLinkedList<T> {
  10. size: number; // 链表长度
  11. head: LinkNode<T> | null; // 头结点
  12. constructor() {
  13. this.size = 0;
  14. this.head = null;
  15. }
  16. /**
  17. * 获取链表中 index 位置的节点。如果索引无效,则返回 null。
  18. * @param index
  19. */
  20. getNode(index: number): LinkNode<T> | null {
  21. if (index < 0 || index >= this.size) {
  22. return null;
  23. }
  24. // index 有效,所以 node.next 和 node.val 是存在的。
  25. let node = this.head as LinkNode<T>;
  26. for (let i = 0; i < index; i++) {
  27. node = node.next as LinkNode<T>;
  28. }
  29. return node;
  30. }
  31. /**
  32. * 获取链表中 index 位置的节点值。如果索引无效,则返回-1。
  33. * @param index
  34. */
  35. get(index: number): T | -1 {
  36. const node = this.getNode(index);
  37. return node?.val ?? -1;
  38. }
  39. /**
  40. * 在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  41. * @param val
  42. */
  43. addAtHead(val: T): void {
  44. const newHead = new LinkNode(val);
  45. newHead.next = this.head;
  46. this.head = newHead;
  47. this.size = this.size + 1;
  48. }
  49. /**
  50. * 将值为 val 的节点追加到链表的最后一个元素。
  51. * @param val
  52. */
  53. addAtTail(val: T): void {
  54. const oldTailNode = this.getNode(this.size - 1);
  55. const newTailNode = new LinkNode(val);
  56. this.size = this.size + 1;
  57. if (oldTailNode === null) {
  58. this.head = new LinkNode(val);
  59. return;
  60. }
  61. oldTailNode.next = newTailNode;
  62. }
  63. /**
  64. * 在链表中的 index 位置之前添加值为 val  的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果 index 小于0,则在头部插入节点。
  65. * @param index
  66. * @param val
  67. */
  68. addAtIndex(index: number, val: T): void {
  69. if (index > this.size) return;
  70. // 尾插
  71. if (index === this.size) {
  72. this.addAtTail(val);
  73. return;
  74. }
  75. // 头插
  76. if (index < 0) {
  77. this.addAtHead(val);
  78. return;
  79. }
  80. const prevNode = this.getNode(index - 1) as LinkNode<T>;
  81. const node = new LinkNode(val);
  82. node.next = prevNode.next;
  83. prevNode.next = node;
  84. this.size = this.size + 1;
  85. }
  86. /**
  87. * 如果索引 index 有效,则删除链表中的第 index 个节点。
  88. * @param index
  89. */
  90. deleteAtIndex(index: number): void {
  91. // index 无效
  92. if (index < 0 || index >= this.size || this.size === 0) return;
  93. this.size = this.size - 1;
  94. // 删除头节点
  95. if (index === 0) {
  96. this.head = this.head?.next as LinkNode<T> | null;
  97. return;
  98. }
  99. const prevNode = this.getNode(index - 1) as LinkNode<T>;
  100. const deleteNode = prevNode.next as LinkNode<T>;
  101. prevNode.next = deleteNode.next;
  102. }
  103. }

1.3 剑指 offer 链表算法题(typescript 版)

从尾到头打印链表
链表倒数第k个节点
反转链表
合并两个排序链表
复杂链表的复制
删除链表中的重复节点
两个链表的第一个公共节点
链表中环的入口节点

2 树

2.1 基本概念

树是非线性逻辑结构,是n(n≥0)个节点的有限集。当n=0时,称为空树。树的每个节点有一个节点值和包含所有子节点的列表,从图的角度看,即N个节点的N - 1条边的有向无环图。树的最大层级数,被称为树的高度或深度。树中最多子节点数称为树的度。

而最常用且典型的是二叉树,即每个节点最多只有两个子节点。两个孩子节点的顺序是固定的(与二度树不同)。

满二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上。

完全二叉树是具有与同深度的按层级顺序1 到 n 编号的满二叉树完全对应的节点编号的二叉树。

二叉树是逻辑结构物理结构可以是链式存储结构,或者是数组。

链式存储结构:存储数据的data变量、指向左孩子的left指针、指向右孩子的right指针。

数组:按层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或右孩子空缺,则数组的相应位置也空出来

如上图,当前节点的下标是parent,那么它的左孩子节点下标就是2 × parent + 1;右孩子节点下标就是2 × parent + 2。反过来,假设一个左孩子节点的下标是lChild,那么它的父节点下标就是(lChild - 1)/ 2,同理,假设一个右孩子节点的下标是rChild,那么它的父节点下标就是(rChild - 2)/ 2,因此任何一个节点下标为child,其父节点下标为 Math.floor((child - 1) / 2)。数组存储更适合完全二叉树和满二叉树,对于稀疏二叉树,无疑是浪费空间的。

二叉树的应用主要是查找和维持相对顺序,比如二叉查找树(二叉搜索树)(左孩子 < 父节点 < 右孩子)和二叉堆(大根堆:左孩子 < 父节点 > 右孩子,小根堆:左孩子 > 父节点 < 右孩子)。对于一个节点分布相对均衡的二叉查找树来说,如果节点总数是n,那么搜索节点的时间复杂度就是O(logn),和树的深度是一样的。而如果考虑插入有序的序列,树的高度和搜索时间都会退化成O(n),就需要采用二叉树的自平衡,其应用有:红黑树、AVL树(二叉平衡树)等 。

2.2 二叉树的实现与遍历

二叉树遍历包括前序遍历,中序遍历,后序遍历,层序遍历。实现方法为递归遍历迭代遍历

二叉树的实现和递归遍历

  1. class BinaryTreeNode {
  2. key: string | number; // 节点的键
  3. parent: BinaryTreeNode | null; // 节点的父节点
  4. value: any; // 节点的值
  5. left: BinaryTreeNode | null; // 指向节点左孩子的指针
  6. right: BinaryTreeNode | null; // 指向节点右孩子的指针
  7. constructor(key: string | number, value = key, parent = null) {
  8. this.key = key;
  9. this.value = value;
  10. this.parent = parent;
  11. this.left = null;
  12. this.right = null;
  13. }
  14. get isLeaf() {
  15. return this.left === null && this.right === null;
  16. }
  17. get hasChildren() {
  18. return !this.isLeaf;
  19. }
  20. }
  21. class BinaryTree {
  22. // 根节点
  23. root: BinaryTreeNode;
  24. constructor(key: string, value = key) {
  25. this.root = new BinaryTreeNode(key, value);
  26. }
  27. /**
  28. * 中序遍历 (首先遍历左子树,然后访问根节点,最后遍历右子树)
  29. * @param node
  30. */
  31. *inOrderTraversal(node = this.root) {
  32. if (node) {
  33. const { left, right } = node;
  34. if (left) yield* this.inOrderTraversal(left);
  35. yield node;
  36. if (right) yield* this.inOrderTraversal(right);
  37. }
  38. }
  39. /**
  40. * 后序遍历 (首先遍历左子树,然后遍历右子树,最后访问根节点)
  41. * @param node
  42. */
  43. *postOrderTraversal(node = this.root) {
  44. if (node) {
  45. const { left, right } = node;
  46. if (left) yield* this.postOrderTraversal(left);
  47. if (right) yield* this.postOrderTraversal(right);
  48. yield node;
  49. }
  50. }
  51. /**
  52. * 前序遍历 (首先访问根节点,然后遍历左子树,最后遍历右子树)
  53. * @param node
  54. */
  55. *preOrderTraversal(node = this.root) {
  56. if (node) {
  57. const { left, right } = node;
  58. yield node;
  59. if (left) yield* this.preOrderTraversal(left);
  60. if (right) yield* this.preOrderTraversal(right);
  61. }
  62. }
  63. /**
  64. * 插入一个节点作为给定父节点的子节点
  65. * @param parentNodeKey
  66. * @param key
  67. * @param value
  68. * @param param3
  69. * @returns
  70. */
  71. insert(parentNodeKey: any, key: string, value = key, { left, right } = { left: true, right: true }) {
  72. for (const node of this.preOrderTraversal()) {
  73. if (node.key === parentNodeKey) {
  74. // 插入到某父节点下,如果不指定则插入到左右孩子中的空的那个
  75. const canInsertLeft = left && node.left === null;
  76. const canInsertRight = right && node.right === null;
  77. if (!canInsertLeft && !canInsertRight) return false; // 该父节点孩子节点不空余
  78. if (canInsertLeft) {
  79. node.left = new BinaryTreeNode(key, value, node);
  80. return true;
  81. }
  82. if (canInsertRight) {
  83. node.right = new BinaryTreeNode(key, value, node);
  84. return true;
  85. }
  86. }
  87. }
  88. return false;
  89. }
  90. /**
  91. * 从二叉树中删除一个节点及其子节点
  92. * @param key
  93. * @returns
  94. */
  95. remove(key: string) {
  96. for (const node of this.preOrderTraversal()) {
  97. if (node.left.key === key) {
  98. node.left = null;
  99. return true;
  100. }
  101. if (node.right.key === key) {
  102. node.right = null;
  103. return true;
  104. }
  105. }
  106. return false;
  107. }
  108. /**
  109. * 检索给定节点
  110. * @param key
  111. * @returns
  112. */
  113. find(key: string) {
  114. for (const node of this.preOrderTraversal()) {
  115. if (node.key === key) return node;
  116. }
  117. return undefined;
  118. }
  119. }

前序遍历(迭代):

  1. /**
  2. * 前序遍历 (首先访问根节点,然后遍历左子树,最后遍历右子树)
  3. */
  4. function preOrderTraversal(root: BinaryTreeNode | null) {
  5. if (root === null) return [];
  6. const result: number[] = [];
  7. const stack = [root];
  8. while (stack.length !== 0) {
  9. const current = stack.pop()!;
  10. result.push(current.value);
  11. const lChild = current.left;
  12. const rChild = current.right;
  13. // 栈后进先出,因此先右孩子入栈,然后左孩子入栈
  14. if (rChild !== null) stack.push(rChild);
  15. if (lChild !== null) stack.push(lChild);
  16. }
  17. return result;
  18. }

中序遍历(迭代):

  1. /**
  2. * 中序遍历 (首先遍历左子树,然后访问根节点,最后遍历右子树)
  3. */
  4. function inOrderTraversal(root: BinaryTreeNode | null) {
  5. if (root === null) return [];
  6. const result: number[] = [];
  7. const stack: BinaryTreeNode[] = [];
  8. let current: BinaryTreeNode | null = root;
  9. while (stack.length !== 0 || current !== null) {
  10. while (current !== null) {
  11. // 一直遍历到最左孩子节点
  12. stack.push(current);
  13. current = current.left;
  14. }
  15. if (stack.length !== 0) {
  16. // 栈非空
  17. const node = stack.pop()!;
  18. result.push(node.value);
  19. current = node.right; // 当前 -> 右
  20. }
  21. }
  22. return result;
  23. }

后序遍历(迭代):

  1. /**
  2. * 后序遍历 (首先遍历左子树,然后遍历右子树,最后访问根节点)
  3. * @param node
  4. */
  5. function postOrderTraversal(root: BinaryTreeNode | null) {
  6. if (root === null) return [];
  7. const result: number[] = [];
  8. // 转变成遍历 根节点 -> 右节点 -> 左节点
  9. // 遍历结果数组进行reverse, 即得到左节点 -> 右节点 -> 根节点
  10. let stack = [root];
  11. while (stack.length !== 0) {
  12. let current = stack.pop()!;
  13. result.push(current.value);
  14. let lChild = current.left;
  15. let rChild = current.right;
  16. // 交换和前序的顺序
  17. if (lChild !== null) stack.push(lChild);
  18. if (rChild !== null) stack.push(rChild);
  19. }
  20. return result.reverse(); // 反转即可
  21. }

层序遍历

  1. /**
  2. * 层序遍历 (逐层遍历树结构,借助队列实现)
  3. * @param node
  4. */
  5. function leverOrderTraversal(root: BinaryTreeNode | null) {
  6. if (root === null) return [];
  7. const result: number[] = [];
  8. const queue = [root]; // 队列(先入先出)
  9. while (queue.length !== 0) {
  10. // 队列非空
  11. const current = queue.shift()!; // 队首出队
  12. result.push(current.value);
  13. const leftChild = current.left;
  14. const rightChild = current.right;
  15. if (leftChild !== null) queue.push(leftChild);
  16. if (rightChild !== null) queue.push(rightChild);
  17. }
  18. return result;
  19. }

2.3 剑指offer树算法题(typescript 版)

二叉树的深度
重建二叉树
从上到下打印二叉树
镜像二叉树
平衡二叉树
二叉树中和为某一值的路径
对称二叉树
二叉搜索树的后序遍历序列
二叉搜索树的第k大节点
树的子结构
二叉树的下一个节点
序列化二叉树
二叉搜索树与双向链表

3 图

3.1 图的基本操作

图是一种非线性逻辑结构,由一组节点或顶点以及一组表示这些节点之间连接的边组成。图可以是有向的或无向的,而它们的边可以分配数字权重。

3.2 图的设计与实现

  1. interface Edge {
  2. a: GraphNode; // 边的起始节点
  3. b: GraphNode; // 边的目标节点
  4. weight?: number; // 边的可选数字权重值
  5. }
  6. interface GraphNode {
  7. key: string; // 节点的键
  8. value: any; // 节点的值
  9. }
  10. class Graph {
  11. directed: boolean;
  12. nodes: GraphNode[];
  13. edges: Map<string, Edge>;
  14. constructor(directed = true) {
  15. this.directed = directed;
  16. this.nodes = [];
  17. this.edges = new Map();
  18. }
  19. /**
  20. * 插入具有特定键和值的新节点
  21. * @param key
  22. * @param value
  23. */
  24. addNode(key: string, value = key) {
  25. this.nodes.push({ key, value });
  26. }
  27. /**
  28. * 在两个给定节点之间插入一条新边,可选择设置其权重
  29. * @param a
  30. * @param b
  31. * @param weight
  32. */
  33. addEdge(a: GraphNode, b: GraphNode, weight?: number) {
  34. this.edges.set(JSON.stringify([a, b]), { a, b, weight });
  35. if (!this.directed) this.edges.set(JSON.stringify([b, a]), { a: b, b: a, weight });
  36. }
  37. /**
  38. * 删除具有指定键的节点
  39. * @param key
  40. */
  41. removeNode(key: string) {
  42. this.nodes = this.nodes.filter((n: { key: string }) => n.key !== key);
  43. [...this.edges.values()].forEach(({ a, b }) => {
  44. if (a.key === key || b.key === key) this.edges.delete(JSON.stringify([a, b]));
  45. });
  46. }
  47. /**
  48. * 删除两个给定节点之间的边
  49. * @param a
  50. * @param b
  51. */
  52. removeEdge(a: any, b: any) {
  53. this.edges.delete(JSON.stringify([a, b]));
  54. if (!this.directed) this.edges.delete(JSON.stringify([b, a]));
  55. }
  56. /**
  57. * 检索具有给定键的节点
  58. * @param key
  59. * @returns
  60. */
  61. findNode(key: string) {
  62. return this.nodes.find((x: { key: string }) => x.key === key);
  63. }
  64. /**
  65. * 检查图在两个给定节点之间是否有边
  66. * @param a
  67. * @param b
  68. * @returns
  69. */
  70. hasEdge(a: any, b: any) {
  71. return this.edges.has(JSON.stringify([a, b]));
  72. }
  73. /**
  74. * 设置给定边的权重
  75. * @param a
  76. * @param b
  77. * @param weight
  78. */
  79. setEdgeWeight(a: any, b: any, weight: any) {
  80. this.edges.set(JSON.stringify([a, b]), { a, b, weight });
  81. if (!this.directed) this.edges.set(JSON.stringify([b, a]), { a: b, b: a, weight });
  82. }
  83. /**
  84. * 获取给定边的权重
  85. * @param a
  86. * @param b
  87. * @returns
  88. */
  89. getEdgeWeight(a: any, b: any) {
  90. return this.edges.get(JSON.stringify([a, b]))?.weight;
  91. }
  92. /**
  93. * 从给定节点查找存在边的所有节点
  94. * @param key
  95. * @returns
  96. */
  97. adjacent(key: string) {
  98. return [...this.edges.values()].reduce((acc, { a, b }) => {
  99. if (a.key === key) acc.push(b);
  100. return acc;
  101. }, [] as GraphNode[]);
  102. }
  103. /**
  104. * 计算给定节点的总边数
  105. * @param key
  106. * @returns
  107. */
  108. inDegree(key: string) {
  109. return [...this.edges.values()].reduce((acc, { a, b }) => (b.key === key ? acc + 1 : acc), 0);
  110. }
  111. /**
  112. * 计算给定节点的总边数
  113. * @param key
  114. * @returns
  115. */
  116. outDegree(key: string) {
  117. return [...this.edges.values()].reduce((acc, { a, b }) => (a.key === key ? acc + 1 : acc), 0);
  118. }
  119. }

3.3 深度优先搜索和广度优先搜索

深度优先搜索 (DFS) 是一种用于遍历或搜索树或图数据结构的算法。一个从根开始(在图的情况下选择某个任意节点作为根)并在回溯之前沿着每个分支尽可能地探索。

广度优先搜索 (BFS) 则是从树根(或图的某个任意节点,有时称为“搜索key”)开始,首先探索邻居节点,然后再移动到下一级邻居。

3.4 剑指 offer 搜索题(typescript 版)

矩阵中的路径

机器人的运动范围

4 堆

4.1 堆的基本操作

堆是具有以下性质的完全二叉树,有两种堆:

        大顶堆(大根堆):每个节点值大于等于左、右孩子节点的值,大根堆的堆顶是整个堆中的最大元素

        小顶堆(小根堆):每个节点值小于等于左、右孩子节点的值,小根堆的堆顶是整个堆中的最小元素。

存储形式:数组

应用:优先级队列(多个定时器任务问题)、求前n个最大/最小的数。

堆的基本操作包括(均依赖于堆的自我调整使其满足大/小根堆特性):

  1. 插入节点:插入位置是在堆的末尾,然后对该节点进行上浮操作(上浮即和它的父节点比较大小);
  2. 删除节点:删除位置在堆顶,然后将堆尾元素放到堆顶,对此元素进行下沉操作(下沉即和它的左、右孩子比较大小),不断递归,直到无法下沉;
  3. 构建堆:把一个无序的完全二叉树调整为大/小根堆,从下往上、从左往右的对所有非叶子节点进行下沉操作

4.2 设计堆

利用数组,实现具有插入,删除操作的大根或小根堆。

  1. class Heap {
  2. container: number[];
  3. cmp: Function;
  4. /**
  5. * 默认是大顶堆
  6. * @param type
  7. */
  8. constructor(type: 'max' | 'min' = 'max') {
  9. this.container = [];
  10. this.cmp = type === 'max' ? (x, y) => x > y : (x, y) => x < y;
  11. }
  12. /**
  13. * 对堆中的两个节点进行交换
  14. * @param i
  15. * @param j
  16. */
  17. swap(i: number, j: number) {
  18. [this.container[i], this.container[j]] = [this.container[j], this.container[i]];
  19. }
  20. /**
  21. * 插入节点,在堆末尾插入,并对节点进行上浮操作
  22. * @param data
  23. * @returns
  24. */
  25. insert(data: number) {
  26. this.container.push(data);
  27. // 上浮操作
  28. let index = this.container.length - 1;
  29. while (index) {
  30. // 直到遍历到堆顶
  31. // 父节点位置
  32. const parent = Math.floor((index - 1) / 2);
  33. if (!this.cmp(this.container[index], this.container[parent])) {
  34. // 大顶堆:当前节点不大于父节点,到达最终位置
  35. return;
  36. }
  37. // 交换
  38. this.swap(index, parent);
  39. index = parent;
  40. }
  41. }
  42. /**
  43. * 删除节点,删除堆顶元素与堆尾元素后的堆尾所在元素,再对堆顶元素执行下沉操作
  44. * @returns
  45. */
  46. delete(): number {
  47. if (this.isEmpty()) return NaN;
  48. // 将堆顶元素与堆尾元素进行交换,并删除堆尾元素
  49. const size = this.getSize();
  50. this.swap(0, size - 1);
  51. const top = this.container.pop()!;
  52. // 当前节点位置
  53. let index = 0;
  54. // 交换节点位置,大顶堆:子节点中的较大者
  55. let exchange = index * 2 + 1;
  56. while (exchange < size) {
  57. // 右子节点位置
  58. let right = index * 2 + 2;
  59. if (right < length && this.cmp(this.container[right], this.container[exchange])) {
  60. // 大顶堆:存在右节点且右节点较大
  61. exchange = right;
  62. }
  63. if (!this.cmp(this.container[exchange], this.container[index])) {
  64. // 大顶堆:子节点较大者小于当前节点
  65. return NaN;
  66. }
  67. // 交换
  68. this.swap(exchange, index);
  69. index = exchange;
  70. exchange = index * 2 + 1;
  71. }
  72. return top;
  73. }
  74. /**
  75. * 获取堆顶元素,堆空则返回 NaN
  76. * @returns
  77. */
  78. top(): number {
  79. if (this.isEmpty()) return NaN;
  80. return this.container[0];
  81. }
  82. /**
  83. * 判断堆是否为空
  84. * @returns
  85. */
  86. isEmpty(): boolean {
  87. return this.getSize() === 0;
  88. }
  89. /**
  90. * 堆中元素个数
  91. * @returns
  92. */
  93. getSize(): number {
  94. return this.container.length;
  95. }
  96. }

4.3 剑指 offer 堆算法题( typescript 版)

数据流中的中位数

5 散列表

5.1 Hash表的基本操作

Hash表是一种是使用哈希函数来组织数据,支持快速插入和搜索的线性数据结构。关键是通过哈希函数将键映射到存储桶。哈希函数的选取取决于键的值范围和桶的数量。

        a) 插入新的键,哈希函数计算被存储的桶;

        b) 搜索一个键,使用相同的哈希函数计算所在桶, 然后在桶中搜索。

5.2 设计Hash表

关键是选择哈希函数和进行冲突处理

哈希函数:分配一个地址存储值。理想情况下,每个键都应该有一个对应唯一的散列值。

冲突处理:哈希函数的本质就是从 A 映射到 B。但是多个 A 键可能映射到相同的 B。

将哈希key转换为字符串的函数如下:

  1. /**
  2. * @description: 将 item 也就是 key 统一转换为字符串
  3. */
  4. export function defaultToString(item: any): string {
  5. // 对于 null undefined和字符串的处理
  6. if (item === null) {
  7. return 'NULL';
  8. } else if (item === undefined) {
  9. return 'UNDEFINED';
  10. } else if (typeof item === 'string' || item instanceof String) {
  11. return `${item}`;
  12. }
  13. // 其他情况时调用数据结构自带的 toString 方法
  14. return item.toString();
  15. }

冲突解决策略

        1) 单独链接法(链表法):对于相同的散列值,我们将它们放到一个桶中,每个桶是相互独立的

  1. // 单独链接法(链表)
  2. export class HashTableSeparateChaining<K, V> {
  3. protected table: Map<number, MyLinkedList<{ key: K; value: V }>>;
  4. constructor(protected toStrFn: (key: K) => string = defaultToString) {
  5. this.table = new Map();
  6. }
  7. /**
  8. * @description: 哈希函数(djb2函数(或者loselose函数)
  9. */
  10. private hashCodeHelper(key: K): number {
  11. if (typeof key === 'number') {
  12. return key;
  13. }
  14. const tableKey = this.toStrFn(key);
  15. let hash = 5381;
  16. for (let i = 0; i < tableKey.length; i++) {
  17. hash = hash * 33 + tableKey.charCodeAt(i);
  18. }
  19. return hash % 1013;
  20. }
  21. /**
  22. * @description: 哈希函数封装
  23. */
  24. hashCode(key: K): number {
  25. return this.hashCodeHelper(key);
  26. }
  27. /**
  28. * @description: 更新散列表
  29. */
  30. put(key: K, value: V): boolean {
  31. if (key !== null && value !== null) {
  32. const position = this.hashCode(key);
  33. // 当该 hashcode 不存在时,先创建一个链表
  34. if (this.table.get(position) == null) {
  35. this.table.set(position, new MyLinkedList<{ key: K; value: V }>());
  36. }
  37. // 再给链表push值
  38. this.table.get(position)!.addAtTail({ key, value });
  39. return true;
  40. }
  41. return false;
  42. }
  43. /**
  44. * @description: 根据键获取值
  45. */
  46. get(key: K): V | undefined {
  47. const position = this.hashCode(key);
  48. const linkedList = this.table.get(position);
  49. if (linkedList && linkedList.size !== 0) {
  50. let current = linkedList.head;
  51. // 去链表中迭代查找键值对
  52. while (current !== null) {
  53. if (current.val.key === key) {
  54. return current.val.value;
  55. }
  56. current = current.next;
  57. }
  58. }
  59. }
  60. /**
  61. * @description: 根据键移除值
  62. */
  63. remove(key: K): boolean {
  64. const position = this.hashCode(key);
  65. const linkedList = this.table.get(position);
  66. if (linkedList && linkedList.size !== 0) {
  67. let current = linkedList.head;
  68. let index = 0;
  69. while (current !== null) {
  70. if (current.val.key === key) {
  71. break;
  72. }
  73. index = index + 1;
  74. current = current.next;
  75. }
  76. linkedList.deleteAtIndex(index);
  77. // 关键的一点,当链表为空以后,需要在 table 中删除掉 hashcode
  78. if (linkedList.size === 0) {
  79. this.table.delete(position);
  80. }
  81. return true;
  82. }
  83. return false;
  84. }
  85. /**
  86. * @description: 返回是否为空散列表
  87. */
  88. isEmpty(): boolean {
  89. return this.size() === 0;
  90. }
  91. /**
  92. * @description: 散列表的大小
  93. */
  94. size(): number {
  95. let count = 0;
  96. // 迭代每个链表,累计求和
  97. for (const [hashCode, linkedList] of this.table) {
  98. count += linkedList.size;
  99. }
  100. return count;
  101. }
  102. /**
  103. * @description: 清空散列表
  104. */
  105. clear() {
  106. this.table.clear();
  107. }
  108. /**
  109. * @description: 返回内部table
  110. */
  111. getTable() {
  112. return this.table;
  113. }
  114. /**
  115. * @description: 替代默认的toString
  116. */
  117. toString(): string {
  118. if (this.isEmpty()) {
  119. return '';
  120. }
  121. let objStringList: string[] = [];
  122. for (const [hashCode, linkedList] of this.table) {
  123. let node = linkedList.head;
  124. while (node) {
  125. objStringList.push(`{${node.val.key} => ${node.val.value}}`);
  126. node = node.next;
  127. }
  128. }
  129. return objStringList.join(',');
  130. }
  131. }

        2) 开放地址法(线性探测):每当有碰撞,则根据我们探查的策略找到一个空的槽为止。

  1. // 开放地址法(线性探测)
  2. export default class HashTableLinearProbing<K, V> {
  3. protected table: Map<number, { key: K; value: V }>;
  4. constructor(protected toStrFn: (key: K) => string = defaultToString) {
  5. this.table = new Map();
  6. }
  7. /**
  8. * @description: 哈希函数(djb2函数(或者loselose函数))
  9. */
  10. private hashCodeHelper(key: K): number {
  11. if (typeof key === 'number') {
  12. return key;
  13. }
  14. const tableKey = this.toStrFn(key);
  15. let hash = 5381;
  16. for (let i = 0; i < tableKey.length; i++) {
  17. hash = hash * 33 + tableKey.charCodeAt(i);
  18. }
  19. return hash % 1013;
  20. }
  21. /**
  22. * @description: 哈希函数封装
  23. */
  24. hashCode(key: K): number {
  25. return this.hashCodeHelper(key);
  26. }
  27. /**
  28. * @description: 更新散列表
  29. */
  30. put(key: K, value: V): boolean {
  31. if (key !== null && value !== null) {
  32. const position = this.hashCode(key);
  33. if (this.table.get(position) == null) {
  34. // 当hashcode位置为空时,可以直接添加
  35. this.table.set(position, { key, value });
  36. } else {
  37. // 否则需要迭代查找最近的空位置再添加
  38. let index = position + 1;
  39. while (this.table.get(index) !== null) {
  40. index++;
  41. }
  42. this.table.set(index, { key, value });
  43. }
  44. return true;
  45. }
  46. return false;
  47. }
  48. /**
  49. * @description: 根据键获取值
  50. */
  51. get(key: K): V | undefined {
  52. const position = this.hashCode(key);
  53. if (this.table.get(position)) {
  54. // 如果查到的hashcode位置就是要查的key,则直接返回
  55. if (this.table.get(position)!.key === key) {
  56. return this.table.get(position)!.value;
  57. }
  58. // 否则需要迭代着向下查找
  59. let index = position + 1;
  60. while (this.table.get(index) != null && this.table.get(index)!.key !== key) {
  61. index++;
  62. }
  63. if (this.table.get(index) !== null && this.table.get(index)!.key === key) {
  64. return this.table.get(position)!.value;
  65. }
  66. }
  67. // 最后也没查到,就返回 undefined
  68. return undefined;
  69. }
  70. /**
  71. * @description: 根据键移除值
  72. */
  73. remove(key: K): boolean {
  74. const position = this.hashCode(key);
  75. if (this.table.get(position)) {
  76. // 同理,如果hashcode对应位置就是要查的key,则直接删除
  77. if (this.table.get(position)!.key === key) {
  78. this.table.delete(position);
  79. // 删除后处理副作用
  80. this.verifyRemoveSideEffect(key, position);
  81. return true;
  82. }
  83. // 同理,如果hashcode对应的位置不是要查的key,就迭代查到
  84. let index = position + 1;
  85. while (this.table.get(index) !== null && this.table.get(index)!.key !== key) {
  86. index++;
  87. }
  88. if (this.table.get(index) !== null && this.table.get(index)!.key === key) {
  89. this.table.delete(index);
  90. // 同样在删除后处理副作用
  91. this.verifyRemoveSideEffect(key, index);
  92. return true;
  93. }
  94. }
  95. return false;
  96. }
  97. /**
  98. * @description: 处理移除键值对后的副作用
  99. */
  100. private verifyRemoveSideEffect(key: K, removedPosition: number) {
  101. const hash = this.hashCode(key);
  102. let index = removedPosition + 1;
  103. // 迭代着处理后面的每一个键值对
  104. while (this.table.get(index) !== null) {
  105. const posHash = this.hashCode(this.table.get(index)!.key);
  106. // 挨个向前挪动,关键点在于,hashcode值比较小的键值对尽量先向前补位
  107. // 详细的说:如果当前元素的 hash 值小于或等于原始的 hash 值
  108. // 或者当前元素的 hash 值小于或等于 removedPosition(也就是上一个被移除 key 的 hash 值),
  109. // 表示我们需要将当前元素移动至 removedPosition 的位置
  110. if (posHash <= hash || posHash <= removedPosition) {
  111. this.table.set(removedPosition, this.table.get(index)!);
  112. this.table.delete(index);
  113. removedPosition = index;
  114. }
  115. index++;
  116. }
  117. }
  118. /**
  119. * @description: 返回是否为空散列表
  120. */
  121. isEmpty(): boolean {
  122. return this.size() === 0;
  123. }
  124. /**
  125. * @description: 散列表的大小
  126. */
  127. size(): number {
  128. return this.table.size;
  129. }
  130. /**
  131. * @description: 清空散列表
  132. */
  133. clear() {
  134. this.table.clear();
  135. }
  136. /**
  137. * @description: 返回内部table
  138. */
  139. getTable(): Map<number, { key: K; value: V }> {
  140. return this.table;
  141. }
  142. /**
  143. * @description: 替代默认的toString
  144. */
  145. toString(): string {
  146. if (this.isEmpty()) {
  147. return '';
  148. }
  149. let objStringList: string[] = [];
  150. for (const [hashCode, { key, value }] of this.table) {
  151. objStringList.push(`{${key} => ${value}}`);
  152. }
  153. return objStringList.join(',');
  154. }
  155. }

        上述实现中使用到的 djb2函数(或者loselose函数),原理是借助字符串各个位上的 UTF-16 Unicode 值进行计算,然后对 特定值取余即为哈希值。

        3) 双散列法:使用两个哈希函数计算散列值,选择碰撞更少的地址。

JavaScript内置哈希表的典型设计是:

键值可以是任何具有可哈希码(映射函数获取存储区索引)可哈希化的类型。每个桶包含一个数组,用于在初始时将所有值存储在同一个桶中。 如果在同一个桶中有太多的值,这些值将被保留在一个高度平衡的二叉树搜索树BST中。

插入和搜索的平均时间复杂度仍为 O(1)。最坏情况下插入和搜索的时间复杂度是 O(logN)。使用高度平衡的 BST是在插入和搜索之间的一种权衡。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Guff_9hys/article/detail/761395
推荐阅读
相关标签
  

闽ICP备14008679号