92c89a57e21f49d2f14f4424343a2773.jpg

散列表

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

散列表的一个常见应用是使用散列表来表示对象。JavaScript 语言内部就是使用散列表来表示每个对象。此时,对象的每个属性和方法 (成员) 被存储为 key 对象类型,每个 key 指向对应的对象成员。

散列函数

散列函数,又称散列算法、哈希函数,是一种从任何一种数据中创建小的数字 “指纹” 的方法。简单的说就是一种将任意长度的输入值转变成固定长度的输出值的函数。其中输出值称为散列值,它通常是用一个短的随机字母和数字组成的字符串来表示。常用的散列函数有MD5、SHA-1 等。

MD5

MD5信息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。

SHA-1

SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)是一种密码散列函数,SHA-1可以生成一个被称为消息摘要的160位(20字节)散列值,散列值通常的呈现形式为40个十六进制数。

**

实现散列表

定义 ValuePair 类

  1. class ValuePair {
  2. constructor(key, value) {
  3. this.key = key;
  4. this.value = value;
  5. }
  6. toString() {
  7. const key = typeof this.key === 'object' ? JSON.stringify(this.key) : this.key;
  8. return `[#${key}: ${this.value}]`;
  9. }
  10. }

ValuePair 类将原始的 key 和 value 转换成字符串,便于我们同时将原始的 key 和 value 保存在字典中

定义 defaultToString 函数

  1. const defaultToString = (item) => {
  2. if (item === null) {
  3. return 'NULL';
  4. } else if (item === 'undefined') {
  5. return 'UNDEFINED';
  6. } else if (typeof item === 'string' || item instanceof String) {
  7. return `${item}`;
  8. } else if (typeof item === 'object') {
  9. return JSON.stringify(item);
  10. }
  11. return item.toString();
  12. }

JavaScript 的对象 (Object) ,本质上是键值对的集合 (Hash 结构),传统上只能使用字符串作为键。因此,我们需要定义一个转换函数,将所有作为键名传入的对象转化为字符串,使得从 HashTable 类中搜索和获取值更简单。

定义 HashTable 类

  1. class HashTable {
  2. constructor(toStrFn = defaultToString) {
  3. this.toStrFn = toStrFn;
  4. this.table = {};
  5. }
  6. }

在 HashTable 类中,我们使用 Object 的实例来存储散列表中健值。

下面,我们为 散列表定义一些可用的方法:

loseloseHashCode(key) 将 key 生成散列值
hashCode(key) 将 key 生成散列值
put(key, value) 将键和值加入散列表
remove(key) 从散列表中移除一个值
get(key) 从散列表中获取一个值
getTable() 获取整个散列表对象
isEmpty() 判断散列表是否为空
size() 返回散列表中的健值数量
clear() 清空散列表
toString() 将散列表转换成字符串并返回

下面,我们逐一实现散列表的方法:

loseloseHashCode 将 key 生成散列值

  1. loseloseHashCode(key) {
  2. // key 是 number 类型,直接将其返回
  3. if (typeof key === 'number') {
  4. return key;
  5. }
  6. // 将key转换成字符串
  7. const tableKey = this.toStrFn(key);
  8. // hash 值
  9. let hash = 0;
  10. // 遍历字符串,将每个字符转换成 ASCII 值
  11. for (let i = 0; i < tableKey.length; i++) {
  12. hash += tableKey.charCodeAt(i)
  13. }
  14. // 返回一个较小数值的 hash 值
  15. return hash % 37;
  16. }

在 loseloseCode 方法中,我们首先检验 key 是否是 number 类型的值,如果是,则直接将其返回。然后,我们将 key 转换成字符串,根据组成 key 的每个字符的 ASCII 码值组合成一个 hash 值。为了得到较小的数值,我们会用 hash 值和一个任意数做除法的余数(%)——这样可以规避操作数超过数值变量最大表示范围的风险。

hashCode 将 key 生成散列值

  1. hashCode(key) {
  2. return this.loseloseHashCode(key);
  3. }

hashCode 方法简单地调用了 loseloseHashCode 方法,将 key 作为参数传入。

put 将键和值加入散列表

  1. put(key, value) {
  2. if (key != null && value != null) {
  3. const position = this.hashCode(key);
  4. this.table[position] = new ValuePair(key, value);
  5. return true;
  6. }
  7. return false;
  8. }

put 方法会将键和值添加到散列表中。

首先,我们会检验 key 和 value 是否合法,如果不合法就直接返回 false,表示这个值没有被添加(更新)到散列表中。

key 和 value 是合法的,则根据 key 参数,使用 hashCode 函数在表中找到一个位置,然后用 key 和 value 创建一个 ValuePair 实例,将原始的 key 和 value 保存到这个位置上。

remove 从散列表中移除一个值

  1. remove(key) {
  2. // 获取 hash 值,即在表中的位置
  3. const hash = this.hashCode(key);
  4. // 获取 hash 位置的存储的原始 key 和 value
  5. const valuePair = this.table[hash];
  6. if (valuePair != null) {
  7. // Reflect.deleteProperty方法等同于delete obj[name],用于删除对象的属性
  8. // delete this.table[hash];
  9. Reflect.deleteProperty(this.table, hash);
  10. return true;
  11. }
  12. return false;
  13. }

要从 HashTable 中移除一个值,首先需要知道值所在的位置,因此我们使用 hashCode 函数来获取 hash。我们在 hash 的位置获取到 valuePair,如果 valuePair 不是 null 或undefined,我们就将其删除。返回 true,表示删除成功,返回 false,表示没有从散列表中移除值。

get 从散列表中获取一个值

  1. get(key) {
  2. const hash = this.hashCode(key)
  3. const valuePair = this.table[hash];
  4. return valuePair == null ? undefined : valuePair.value;
  5. }

我们首先使用 hashCode 函数获取 key 参数的位置,然后从表中对应的位置获取到值并返回。

getTable 获取整个散列表对象

  1. getTalbe() {
  2. return this.table;
  3. }

isEmpty 判断散列表是否为空

  1. isEmpty() {
  2. return this.size() === 0;
  3. }

size 返回散列表中的健值数量

  1. size() {
  2. return Object.keys(this.table).length;
  3. }

clear 清空散列表

  1. clear() {
  2. this.table = {};
  3. }

toString 将散列表转换成字符串并返回

  1. toString() {
  2. if (this.isEmpty()) {
  3. return '';
  4. }
  5. const keys = Object.keys(this.table);
  6. let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
  7. for (let i = 1; i < keys.length; i++) {
  8. objString = `{${objString}, ${[keys[i]]} => ${this.table[keys[i]].toString()}}`
  9. }
  10. return objString;
  11. }

完整代码

  1. class ValuePair {
  2. constructor(key, value) {
  3. this.key = key;
  4. this.value = value;
  5. }
  6. toString() {
  7. const key = typeof this.key === 'object' ? JSON.stringify(this.key) : this.key;
  8. return `[#${key}: ${this.value}]`;
  9. }
  10. }
  11. const defaultToString = (item) => {
  12. if (item === null) {
  13. return 'NULL';
  14. } else if (item === 'undefined') {
  15. return 'UNDEFINED';
  16. } else if (typeof item === 'string' || item instanceof String) {
  17. return `${item}`;
  18. } else if (typeof item === 'object') {
  19. return JSON.stringify(item);
  20. }
  21. return item.toString();
  22. }
  23. class HashTable {
  24. constructor(toStrFn = defaultToString) {
  25. this.toStrFn = toStrFn;
  26. this.table = {};
  27. }
  28. // 将 key 转换成 hash
  29. loseloseHashCode(key) {
  30. if (typeof key === 'number') {
  31. return key;
  32. }
  33. const tableKey = this.toStrFn(key);
  34. let hash = 0;
  35. for (let i = 0; i < tableKey.length; i++) {
  36. hash += tableKey.charCodeAt(i);
  37. }
  38. return hash % 37;
  39. }
  40. // 将 key 转换成 hash
  41. hashCode(key) {
  42. return this.loseloseHashCode(key);
  43. }
  44. // 将键和值加入到散列表中
  45. put(key, value) {
  46. if (key != null && value != null) {
  47. const position = this.hashCode(key);
  48. this.table[position] = new ValuePair(key, value);
  49. return true;
  50. }
  51. return false;
  52. }
  53. // 从散列表总获取一个值
  54. get(key) {
  55. const hash = this.hashCode(key);
  56. const valuePair = this.table[hash];
  57. return valuePair == null ? undefined : valuePair.value;
  58. }
  59. // 从散列表中移除一个值
  60. remove(key) {
  61. const hash = this.hashCode(key);
  62. const valuePair = this.table[hash];
  63. if (valuePair != null) {
  64. Reflect.deleteProperty(this.table, hash);
  65. return true;
  66. }
  67. return false;
  68. }
  69. // 获取整个散列表对象
  70. getTable() {
  71. return this.table;
  72. }
  73. // 判断散列表是否为空
  74. isEmpty() {
  75. return this.size() === 0;
  76. }
  77. // 返回散列表中的元素个数
  78. size() {
  79. return Object.keys(this.table).length;
  80. }
  81. // 清空散列表
  82. clear() {
  83. this.table = {};
  84. }
  85. // 将散列表转换成字符串并将其返回
  86. toString() {
  87. if (this.isEmpty()) {
  88. return '';
  89. }
  90. const keys = Object.keys(this.table);
  91. let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
  92. for (let i = 1; i < keys.length; i++) {
  93. objString = `${objString}, {${keys[i]} => ${this.table[keys[i]].toString()}}`
  94. }
  95. return objString;
  96. }
  97. }

散列表中的冲突

有时候,一些键会有相同的散列值。不同的值在散列表中对应相同位置的时候,我们称其为冲突。例如,我们看下面的代码会输出怎样的结果:

  1. const hashTable = new HashTable();
  2. hashTable.put('Ygritte', 'ygritte@email.com')
  3. hashTable.put('Jonathan', 'Jonathan@email.com')
  4. hashTable.put('Jamie', 'Jamie@email.com')
  5. hashTable.put('Jack', 'Jack@email.com')
  6. hashTable.put('Jasmine', 'Jasmine@email.com')
  7. hashTable.put('Jake', 'Jake@email.com')
  8. hashTable.put('Nathan', 'Nathan@email.com')
  9. hashTable.put('Athelstan', 'Athelstan@email.com')
  10. hashTable.put('Sue', 'Sue@email.com')
  11. hashTable.put('Aethelwulf', 'Aethelwulf@email.com')
  12. hashTable.put('Sargeras', 'Sargeras@email.com')

通过对每个提到的名字调用 HashTable.hashCode 方法:

  1. console.log(hashTable.hashCode('Ygritte') + ' - Ygritte')
  2. console.log(hashTable.hashCode('Jonathan') + ' - Jonathan')
  3. console.log(hashTable.hashCode('Jamie') + ' - Jamie')
  4. console.log(hashTable.hashCode('Jack') + ' - Jack')
  5. console.log(hashTable.hashCode('Jasmine') + ' - Jasmine')
  6. console.log(hashTable.hashCode('Jake') + ' - Jake')
  7. console.log(hashTable.hashCode('Nathan') + ' - Nathan')
  8. console.log(hashTable.hashCode('Athelstan') + ' - Athelstan')
  9. console.log(hashTable.hashCode('Sue') + ' - Sue')
  10. console.log(hashTable.hashCode('Aethelwulf') + ' - Aethelwulf')
  11. console.log(hashTable.hashCode('Sargeras') + ' - Sargeras')

输入结果如下:

  1. 4 - Ygritte
  2. 5 - Jonathan
  3. 5 - Jamie
  4. 7 - Jack
  5. 8 - Jasmine
  6. 9 - Jake
  7. 10 - Nathan
  8. 7 - Athelstan
  9. 5 - Sue
  10. 5 - Aethelwulf
  11. 10 - Sargeras

可见,Jonathan、Jamie、Sue 和 Aethelwulf 有相同的散列值 (5),Jack 和 Athelstan 有相同的散列值 (7),Nathan 和 Sargeras 也有相同的散列值 (10)。

那么,将这些名字添加到散列表中,结果会怎样呢?

我们执行 hashTable.toString() 方法来查看散列表中的值,如下:

  1. console.log(hashTable.toString())
  2. {4 => [#Ygritte: ygritte@email.com]},
  3. {5 => [#Aethelwulf: Aethelwulf@email.com]},
  4. {7 => [#Athelstan: Athelstan@email.com]},
  5. {8 => [#Jasmine: Jasmine@email.com]},
  6. {9 => [#Jake: Jake@email.com]},
  7. {10 => [#Sargeras: Sargeras@email.com]}

我们发现,具有相同散列值的名字,被最后添加的名字给覆盖了。这显然不是我们想要的结果,我们使用一个数据结构来保存数据,其目的是要把它们全部保存起来,而不是会丢失数据。

那么我们如何解决散列冲突问题呢?

解决散列冲突的方法有很多,这里我们介绍分离链接和线性探查两种方法。

分离链接

分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在链表里面。它是解决散列冲突的最简单的方法,但缺点是在 HashTable 实例之外还需要额外的存储空间。

下面,我们使用代码来实现 分离链接法。

创建 HashTableSeparateChaining 类

  1. class HashTableSeparateChaining {
  2. constructor(toStrFn = defaultToString) {
  3. this.toStrFn = toStrFn;
  4. this.table = {};
  5. }
  6. }

对于 分离链接来说,我们只需要重写 put、get、remove 三个方法即可。

put 将键和值加入散列表

  1. put(key, value) {
  2. if (key != null && value != null) {
  3. const position = this.hashCode(key);
  4. if (this.table[position] == null) {
  5. this.table[position] = new LinkedList();
  6. }
  7. this.table[position].push(new ValuePair(key, value));
  8. return true;
  9. }
  10. return false;
  11. }

在 put 方法中,我们首先验证要加入新元素的位置是否已经被占据,如果是第一次向该位置加入元素,我们会在该位置上初始化一个 LinkedList 的实例,然后用 LinkedList 实例的 push 方法将 键和值 添加到 LinkedList 实例中。

get 从散列表中获取一个值

  1. get(key) {
  2. const position = this.hashCode(key);
  3. // 获取 position 位置上的 LinkedList 实例
  4. const linkedList = this.table[position];
  5. // 判断 position 位置上是否有 LinkedList 实例
  6. if (linkedList != null && !linkedList.isEmpty()) {
  7. // 获取链表的头部元素
  8. let current = linkedList.head();
  9. // 迭代链表,寻找我们需要的元素
  10. while(current != null) {
  11. // 链表的节点的数据域 element 上存储的是 valuePair 实例,即原始的 健/值
  12. if (current.element.key === key) {
  13. return current.element.value;
  14. }
  15. // 继续迭代链表,访问下一个节点
  16. current = current.next;
  17. }
  18. }
  19. return undefined;
  20. }

在 get 方法中,我们首先在 position 位置上检索是否存在 LinkedList 实例,如果没有,则返回 undefined 表示在 HashTable 中没有找到这个值。如果在 position 位置上有值存在,我们知道这是一个 LinkedList 实例,然后我们迭代这个链表来寻找我们需要的元素。

LinkedList 链表包含 element 属性和 next 指针,而 element 属性存储的是 ValuePair 实例,即 {key, value},因此 element 属性有 value 属性和 key 属性。我们可以通过 current.element.key 来获取链表中节点的 key 属性,并通过比较它来确定它是否就是我们要找的键,如果 key 值相同,就返回存储在节点里的 value 值,如果 key 不相同,则继续迭代链表,访问下一个节点: current = current.next 。

remove 从散列表中移除一个值

  1. remove(key) {
  2. // 生成 key 在 散列表中的位置
  3. const position = this.hashCode(key);
  4. // 获取 position 位置上的 LinkedList 实例
  5. const linkedList = this.table[position];
  6. // 如果 position 位置上 LinkedList 实例存在并且 LinkedList 实例不为空
  7. if (linkedList != null && !linkedList.isEmpty()) {
  8. // 链表头节点
  9. let current = linkedList.head();
  10. // 从链表头节点开始迭代链表,查找需要移除的值
  11. while(current != null) {
  12. if (current.element.key === key) {
  13. // 移除当前 key 在链表上的节点
  14. linkedList.remove(current.element);
  15. // 如果此时 链表为空,移除在 position 位置上的链表
  16. if (linkedList.isEmpty()) {
  17. // delete this.table[position];
  18. Reflect.deleteProperty(this.table, position);
  19. }
  20. return true
  21. }
  22. // 不是要移除的值,继续迭代下一个元素
  23. current = current.next;
  24. }
  25. }
  26. // 返回 false 表示没有从散列表中移除值
  27. return false;
  28. }

在 remove 方法中, 我们首先根据 key 生成的 hash 在散列表中查找该 hash 值对应位置上 LinkedList 实例。如果在该位置上没有 LinkedList 实例,我们会返回 false,表示没有从散列表中移除值。如果该位置上存在 LinkedList 实例,并且该 LinkedList 实例不为空,我们就从链表的头节点开始迭代链表,查找需要删除的值,如果在链表上找到了我们要删除的值,我们就将该值从链表中删除,并检查此时的链表是否是空链表,如果是空链表,就将该 hash 值对应位置上的 LinkedList 实例移除。最后返回 true,表示从散列表中移除值成功。

完整代码

LinkedList 类完整代码请参考 数据结构与算法学习之链表 一文。

  1. class ValuePair {
  2. constructor(key, value) {
  3. this.key = key;
  4. this.value = value;
  5. }
  6. toString() {
  7. const key = typeof this.key === 'object' ? JSON.stringify(this.key) : this.key;
  8. return `[#${key}: ${this.value}]`;
  9. }
  10. }
  11. const defaultToString = (item) => {
  12. if (item === null) {
  13. return 'NULL';
  14. } else if (item === 'undefined') {
  15. return 'UNDEFINED';
  16. } else if (typeof item === 'string' || item instanceof String) {
  17. return `${item}`;
  18. } else if (typeof item === 'object') {
  19. return JSON.stringify(item);
  20. }
  21. return item.toString();
  22. }
  23. class HashTableSeparateChaining {
  24. constructor(toStrFn = defaultToString) {
  25. this.toStrFn = toStrFn;
  26. this.table = {};
  27. }
  28. // 将 key 转换成 hash
  29. loseloseHashCode(key) {
  30. if (typeof key === 'number') {
  31. return key;
  32. }
  33. const tableKey = this.toStrFn(key);
  34. let hash = 0;
  35. for (let i = 0; i < tableKey.length; i++) {
  36. hash += tableKey.charCodeAt(i);
  37. }
  38. return hash % 37;
  39. }
  40. // 将 key 转换成 hash
  41. hashCode(key) {
  42. return this.loseloseHashCode(key);
  43. }
  44. // 将键和值加入到散列表中
  45. put(key, value) {
  46. if (key != null && value != null) {
  47. const position = this.hashCode(key);
  48. if (this.table[position] == null) {
  49. this.table[position] = new LinkedList();
  50. }
  51. this.table[position].push(new ValuePair(key, value));
  52. return true;
  53. }
  54. return false;
  55. }
  56. // 从散列表总获取一个值
  57. get(key) {
  58. const position = this.hashCode(key);
  59. const linkedList = this.table[position];
  60. if (linkedList != null && !linkedList.isEmpty()) {
  61. let current = linkedList.head();
  62. while(current != null) {
  63. if (current.element.key === key) {
  64. return current.element.value;
  65. }
  66. current = current.next;
  67. }
  68. }
  69. return undefined;
  70. }
  71. // 从散列表中移除一个值
  72. remove(key) {
  73. const position = this.hashCode(key);
  74. const linkedList = this.table[position];
  75. if (linkedList != null && !linkedList.isEmpty()) {
  76. let current = linkedList.head();
  77. while(current != null) {
  78. if (current.element.key === key) {
  79. linkedList.remove(current.element);
  80. if (linkedList.isEmpty()) {
  81. // delete this.table[position]
  82. Reflect.deleteProperty(this.table, position);
  83. }
  84. return true;
  85. }
  86. current = current.next;
  87. }
  88. }
  89. return false;
  90. }
  91. // 获取整个散列表对象
  92. getTable() {
  93. return this.table;
  94. }
  95. // 判断散列表是否为空
  96. isEmpty() {
  97. return this.size() === 0;
  98. }
  99. // 返回散列表中的元素个数
  100. size() {
  101. return Object.keys(this.table).length;
  102. }
  103. // 清空散列表
  104. clear() {
  105. this.table = {};
  106. }
  107. // 将散列表转换成字符串并将其返回
  108. toString() {
  109. if (this.isEmpty()) {
  110. return '';
  111. }
  112. const keys = Object.keys(this.table);
  113. let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
  114. for (let i = 1; i < keys.length; i++) {
  115. objString = `${objString}, {${keys[i]} => ${this.table[keys[i]].toString()}}`
  116. }
  117. return objString;
  118. }
  119. }

线性探查

线性探查,其处理冲突的方法是将元素直接存储到散列表中,而不是在单独的数据结构中。

线性探查的原理

在一个已经包含一些元素的散列表中,我们想要添加一个新的键和值。我们计算这个新键的 hash,并检查散列表中对应的位置是否被占据,如果没有,我们就将该值添加到正确的位置,如果被占据了,我们就迭代散列表,直到找到一个空闲的位置,将元素添加到这个空闲的位置。

例如我们想在散列表中索引为 position 的位置添加一个新元素,但是当前位置已经被占据,就尝试 position + 1 的位置,如果 position + 1 的位置也被占据了,就尝试 position + 2 的位置,以此类推,直到在散列表中找到一个空闲的位置。

线性探查的实现

线性探查的实现由两种方式,一种是软删除,另一种是挪动位置。

挪动位置

这种方式需要检验是否有必要将一个或多个元素移动到之前的位置。当搜索一个键的时候,这种方法可以避免找到一个空位置。如果移动元素是必要的,我们就需要在散列表中挪动健值对。

使用 挪动位置 的方式实现线性探查,我们只需要重写 put、get、remove 三个方法即可,其余方法与 HashTable 类一致。

创建 HashTableLinearProbing 类

  1. class HashTableLinearProbing {
  2. constructor(toStrFn = defaultToString) {
  3. this.toStrFn = toStrFn;
  4. this.table = {};
  5. }
  6. }

put 方法

  1. put(key, value) {
  2. if (key != null && value != null) {
  3. // 根据 key 生成在散列表中的位置
  4. const position = this.hashCode(key);
  5. // position 位置没有元素存在,则在该位置添加新元素
  6. if (this.table[position] == null) {
  7. this.table[position] = new ValuePair(key, value);
  8. } else {
  9. // 该 position 位置已经被占用,寻找下一个没有被占用的位置
  10. let index = position + 1;
  11. while(this.table[index] != null) {
  12. index++;
  13. }
  14. // 找到一个没有被占据的位置,在该位置上添加新元素
  15. this.table[index] = new ValuePair(key, value);
  16. }
  17. // 返回 true 表示元素成功添加到散列表中
  18. return true;
  19. }
  20. // 返回 false 表示元素没有添加到散列表中
  21. return false;
  22. }

在 put 方法中,我们首先根据 key 生成一个位置,然后验证散列表中这个位置是否有元素存在,如果没有元素存在,就在该位置添加新元素。

如果该位置已经被占据了,就找下一个没有被占据的位置,因此我们声明一个 index 变量并赋值为 position + 1。然后验证该位置是否被占据,如果也被占据了,就继续将 index 递增,直到找到一个没有被占据的位置,然后将元素添加到该位置上。

get 方法

  1. get(key) {
  2. // 根据 key 生成 hash 值,即在散列表中的位置
  3. const position = this.hashCode(key);
  4. // 散列表中 position 位置上有值
  5. if (this.table[position] != null) {
  6. // position 位置上的值恰好是我们要找的值,则返回这个值
  7. if (this.table[position].key === key) {
  8. return this.table[position].value;
  9. }
  10. // 不是我们要找的元素,继续在下一个位置查找
  11. let index = position + 1;
  12. // 按位置递增的顺序查找散列表上的元素直到找到我们要找的元素,或者找到一个空位置后,退出循环
  13. while(this.table[index] != null && this.table[index].key !== key) {
  14. index++;
  15. }
  16. // 退出循环后,验证元素的键是否是我们要找的键,如果是,则返回它的值
  17. if (this.table[index] != null && this.table[index].key === key) {
  18. return this.table[position].value
  19. }
  20. }
  21. // 如果迭代完整个散列表并且 index 上的位置是 undefined 或 null,说明要找的键不存在,返回 undefined
  22. // 要查找的的值不在散列表中,返回 undefined
  23. return undefined;
  24. }

在 get 方法中,我们首先根据 key 生成 hash 值来检查整个位置上是否有值,如果该位置上是 undefined 或 null,说明要查找的值不在散列表中,因此可以返回 undefined。如果在该位置上有值,就要检查我们要找的值是否就是原始位置上的值,如果是,就返回这个值。

如果该位置上的值不是我们要找的值,则继续在下一个位置查找。我们会按照位置递增的顺序查找散列表上的元素直到找到我们要找的元素,或者找到一个空位置,此时跳出 while 循环。

当退出 while 循环的时候,我们要验证元素的键是否是我们要找的键,如果是,就返回该键对应的值。如果迭代完整个散列表并且 index 的位置上是 undefined 或 null 的话,说明要找的键不存在,我们就返回 undefined。

remove 方法

  1. remove(key) {
  2. const position = this.hashCode(key);
  3. if (this.table[position] != null) {
  4. // 在 position 位置上找到要删除的元素,将该元素从散列表中删除
  5. if (this.table[position].key === key) {
  6. delete this.table[position];
  7. // 验证删除操作是否有副作用,如果有,需要将冲突的元素移动至一个之前的位置
  8. this.verifyRemoveSideEffect(key, position);
  9. return true;
  10. }
  11. // 在 position 位置上没有找到要删除的元素,迭代链表,直到找到要删除的元素
  12. let index = position + 1;
  13. while(this.table[index] != null && this.table[index].key !== key) {
  14. index++;
  15. }
  16. if (this.table[index] != null && this.table[index].key === key) {
  17. delete this.table[index];
  18. this.verifyRemoveSideEffect(key, position);
  19. return true;
  20. }
  21. }
  22. return false;
  23. }

verifyRemoveSideEffect 方法

  1. verifyRemoveSideEffect(key, removedPosition) {
  2. const hash = this.hashCode(key);
  3. let index = removedPosition + 1;
  4. while (this.table[index] != null) {
  5. const posHash = this.hashCode(this.table[index].key);
  6. if (posHash <= hash || posHash <= removedPosition) {
  7. this.table[removedPosition] = this.table[index];
  8. delete this.table[index];
  9. removedPosition = index;
  10. }
  11. index++;
  12. }
  13. }

完整代码

  1. class ValuePair {
  2. constructor(key, value) {
  3. this.key = key;
  4. this.value = value;
  5. }
  6. toString() {
  7. const key = typeof this.key === 'object' ? JSON.stringify(this.key) : this.key;
  8. return `[#${key}: ${this.value}]`;
  9. }
  10. }
  11. const defaultToString = (item) => {
  12. if (item === null) {
  13. return 'NULL';
  14. } else if (item === 'undefined') {
  15. return 'UNDEFINED';
  16. } else if (typeof item === 'string' || item instanceof String) {
  17. return `${item}`;
  18. } else if (typeof item === 'object') {
  19. return JSON.stringify(item);
  20. }
  21. return item.toString();
  22. }
  23. class HashTableLinearProbing {
  24. constructor(toStrFn = defaultToString) {
  25. this.toStrFn = toStrFn;
  26. this.table = {};
  27. }
  28. // 将 key 转换成 hash
  29. loseloseHashCode(key) {
  30. if (typeof key === 'number') {
  31. return key;
  32. }
  33. const tableKey = this.toStrFn(key);
  34. let hash = 0;
  35. for (let i = 0; i < tableKey.length; i++) {
  36. hash += tableKey.charCodeAt(i);
  37. }
  38. return hash % 37;
  39. }
  40. // 将 key 转换成 hash
  41. hashCode(key) {
  42. return this.loseloseHashCode(key);
  43. }
  44. // 将键和值加入到散列表中
  45. put(key, value) {
  46. if (key != null && value !=null) {
  47. const position = this.hashCode(key);
  48. if (this.table[position] == null) {
  49. this.table[position] = new ValuePair(key, value);
  50. } else {
  51. let index = position + 1;
  52. while(this.table[index] != null) {
  53. index++;
  54. }
  55. this.table[index] = new ValuePair(key, value);
  56. }
  57. return true;
  58. }
  59. return false;
  60. }
  61. // 从散列表总获取一个值
  62. get(key) {
  63. const position = this.hashCode(key);
  64. if (this.table[position] != null) {
  65. if (this.table[position].key === key) {
  66. return this.table[position].value;
  67. }
  68. let index = position + 1;
  69. while(this.table[index] != null && this.table[index].key !== key) {
  70. index++;
  71. }
  72. if (this.table[index] != null && this.table[index].key === key) {
  73. return this.table[position].value;
  74. }
  75. }
  76. return undefined;
  77. }
  78. // 从散列表中移除一个值
  79. remove(key) {
  80. const position = this.hashCode(key);
  81. if (this.table[position] != null) {
  82. if (this.table[position].key === key) {
  83. delete this.table[position];
  84. this.verifyRemoveSideEffect(key, position);
  85. return true;
  86. }
  87. let index = position + 1;
  88. while(this.table[index] != null && this.table[index].key !== key) {
  89. index++;
  90. }
  91. if (this.table[index] != null && this.table[index].key === key) {
  92. delete this.table[index];
  93. this.verifyRemoveSideEffect(key, position);
  94. return true;
  95. }
  96. }
  97. return false;
  98. }
  99. verifyRemoveSideEffect(key, removedPosition) {
  100. const hash = this.hashCode(key);
  101. let index = removedPosition + 1;
  102. while (this.table[index] != null) {
  103. const posHash = this.hashCode(this.table[index].key);
  104. if (posHash <= hash || posHash <= removedPosition) {
  105. this.table[removedPosition] = this.table[index];
  106. delete this.table[index];
  107. removedPosition = index;
  108. }
  109. index++;
  110. }
  111. }
  112. // 获取整个散列表对象
  113. getTable() {
  114. return this.table;
  115. }
  116. // 判断散列表是否为空
  117. isEmpty() {
  118. return this.size() === 0;
  119. }
  120. // 返回散列表中的元素个数
  121. size() {
  122. return Object.keys(this.table).length;
  123. }
  124. // 清空散列表
  125. clear() {
  126. this.table = {};
  127. }
  128. // 将散列表转换成字符串并将其返回
  129. toString() {
  130. if (this.isEmpty()) {
  131. return '';
  132. }
  133. const keys = Object.keys(this.table);
  134. let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
  135. for (let i = 1; i < keys.length; i++) {
  136. objString = `${objString}, {${keys[i]} => ${this.table[keys[i]].toString()}}`
  137. }
  138. return objString;
  139. }
  140. }

软删除

使用软删除的方式实现线性探查,我们需要使用一个特殊的值(标记)来表示健值对被删除了(惰性删除或软删除),而不是真的删除它。经过一段时间,散列表被操作过后,我们会得到一个标记了若干删除位置的散列表。