「数据结构」图解哈希表(JavaScript实现)

627 阅读7分钟

引言

哈希表是一种组合的数据结构,它通常的实现方式是数组加链表,或者数组加红黑树。哈希表是一种牺牲空间去换取时间的数据结构,需要在空间与时间上有取舍,哈希表是时间和空间之间的平衡。哈希表的核心是哈希函数,哈希表最关键的问题哈希冲突也是取决于哈希函数的设计。

哈希函数

1、什么是哈希函数

哈希函数是一种将“”转换为“索引”的逻辑规则。它的设计好坏对哈希表的性能影响巨大。优良的哈希函数能够最大程度的减少哈希冲突,使得哈希表中的元素分布得尽可能的均匀,离散程度更大,这样哈希表就会性能优越;较差的哈希函数设计,带来的可能是一场灾难,哈希冲突严重,空间利用率低,时间复杂度呈线性恶化,造成频繁的扩容操作。

2、哈希函数的设计

对于哈希函数的设计,以下举了一些简单的例子,都基本类型转换成整型处理,并不是唯一的方法,仅供参考。以下简单列了几条设计原则。

  • 一致性:如果a == b,则hash(a) == hash(b)
  • 高效性:哈希函数计算高效简便
  • 均匀行:哈希值均匀分布

1、整型

  • 小范围正整数可直接使用

  • 小范围负整数进行偏移

img-06-19-14.png

  • 大整数可以与一个合适的素数取模

img-06-19-01.png

说明:上图(左)与一个不合适的合数取模,获得的索引冲突严重,不可取;而上图(右)与一个质数取模,明显获得的索引分布更均匀,离散程度更好。

2、字符串型

  • 字符串型转换成整型处理

img-06-19-10.png

说明:其中B为一个常数,M为一个合适的质数。上图将一个字符串型加入一些规则映射成了一个整型。

3、代码实现简单hash函数

 // 1.将字符串转成比较大的数字:hashCode
 // 2.将大的数字hashCode压缩到数组分为(大小)之内
 function hashFunc(str, size) {
   // 1.定义hashCoed变量
   let hashCode = 0
   // 2.霍纳算法,来计算hashCode的值
   for (var i = 0; i < str.length; i++) {
     hashCode = 37 * hashCode + str.charCodeAt(i)
   }
   // 3.取余操作
   var index = hashCode % size
   return index
 }
 // 测试哈希函数
 console.log(hashFunc("abc", 7))    // 4
 console.log(hashFunc("abcd", 7))   // 3
 console.log(hashFunc("bcdf", 7))   // 5
 console.log(hashFunc("qwer", 7))   // 2

哈希冲突的处理

1、链地址法(Separate Chaining)

img-06-19-27.png

说明:哈希冲突的元素可以用链表这种线性的数据结构保存,当然也可以用平衡树结构保存,Java8中的哈希表实现,当哈希冲突达到一定的程度,会将链表替换成红黑树,前提是哈希表中原本的元素具备可比较性。

2、开放地址法

  • 线性探测

img-06-19-19.png

说明:当遭遇哈希冲突时,会按照规则顺延往下找空挡位置插入元素,这种方式如果哈希冲突比较严重,会造成寻找空挡位置效率变低。哈希表性能变差。这种方式就是要设置合适的哈希表容量。

  • 平方探测

img-06-19-49.png

说明:这种方式相对于线性探测,加大了寻找空挡位置的步长。

  • 二次哈希

img-06-19-32.png

哈希表动态空间处理

img-06-19-51.png

说明:当哈希冲突达到一个所能容忍的上界位置时(upperTol),对哈希表进行扩容操作,以减少哈希冲突;当哈希冲突降低到一个下界位置(lowerTol)时,对哈希表进行缩容操作,以节约空间。

哈希表实现

基于数组和链地址法实现

1、初始化哈希表

 function HashTable() {
   this.storage = [ ]  // 数组中存放相关的元素       
   this.count = 0      // 表示当前已经存放了多少数据
   this.limit = 7      // 用于标记数组中一共可以存放多少个元素
   // 哈希函数(用来计算数据的下标值index)
   hashTable.prototype.hashFunc = function (str, size) {
     // 1.定义hashCoed变量
     var hashCode = 0
     // 2.霍纳算法,来计算hashCode的值
     for (var i = 0; i < str.length; i++) {
       hashCode = 37 * hashCode + str.charCodeAt(i)
     }
     // 3.取余操作
     var index = hashCode % size
     return index
   }
 }

2、哈希表插入修改数据

  // 插入修改操作
  HashTable.prototype.put = function (key, value) {
    // 1.根据key获取对应的index
    var index = this.hashFunc(key, this.limit)    // 这里直接使用之前封装的哈希函数
    // 2.根据index获取对应的bucket
    var bucket = this.storage[index]
    // 3.判断该bucket是否为null
    if (bucket == null) {
      bucket = []
      this.storage[index] = bucket
    }
    // 4.判断bucket里面是否有key的元组
    // 有则是修改value值(因为用的是链地址法)
    for (var i = 0; i < bucket.length; i++) {
      var tuple = bucket[i]
      if (key == tuple[0]) {
        tuple[1] = value
        return
      }
    }
    // 没有则直接在bucket里插入元组
    bucket.push([key, value])
    this.count += 1
  }

3、哈希表获取数据

 // 获取存放的数据
 HashTable.prototype.get = function (key) {
   // 1.获取key对应的index
   var index = this.hashFunc(key, this.limit)
   // 2.通过index获取到bucket
   var bucket = this.storage[index]
   // 3.判断bucket是否为null,如果为null则没有数据
   if (bucket == null) {
     return null
   }
   // 4.如果bucket存在,判断元组里是否有对应的key,有则返回数据
   for (var i = 0; i < bucket.length; i++) {
     var tuple = bucket[i]
     if (tuple[0] == key) {
       return tuple[1]
     }
   }
   // 5.没有找到,return null
   return null
 }

4、哈希表删除数据

 // 删除数据
 HashTable.prototype.remove = function (key) {
   // 1.获取key对应的index
   var index = this.hashFunc(key, this.limit)
   // 2.通过index获取到bucket
   var bucket = this.storage[index]
   // 3.判断bucket是否为null,如果为null则返回false,表示没有要删除的数据
   if (bucket == null) {
     return false
   }
   // 4.如果bucket存在,判断元组里是否有对应的key,有则删除数据
   for (var i = 0; i < bucket.length; i++) {
     var tuple = bucket[i]
     if (tuple[0] == key) {
       bucket.splice(i, 1)
       this.count--
     }
     return tuple[1]
   }
   // 5.没有找到, return fasle
   return false
 }

测试哈希表

 // 测试哈希表
 var hashTable = new HashTable()
 // 向哈希表插入3条数据
 hashTable.put('abc', '我是abc')
 hashTable.put('qwer', '我是qwer')
 hashTable.put('hash', '我是hash')
 // 获取key为'qwer'的数据
 console.log(hashTable.get('qwer'))   // "我是qwer"
 console.log(hashTable)               // 通过Chrome浏览器查看哈希表结构
 // 删除 key为'abc'的数据并查看
 console.log(hashTable.remove('abc')) // "我是abc"

img-06-19-17.png 最后附上完整代码

 function HashTable() {
   this.storage = []   // 数组中存放相关的元素       
   this.count = 0      // 表示当前已经存放了多少数据
   this.limit = 7      // 用于标记数组中一共可以存放多少个元素
   // 哈希函数
   HashTable.prototype.hashFunc = function (str, size) {
     // 1.定义hashCoed变量
     var hashCode = 0
     // 2.霍纳算法,来计算hashCode的值
     for (var i = 0; i < str.length; i++) {
       hashCode = 37 * hashCode + str.charCodeAt(i)
     }
     // 3.取余操作
     var index = hashCode % size
     return index
   }
   // 插入修改操作
   HashTable.prototype.put = function (key, value) {
     // 1.根据key获取对应的index
     var index = this.hashFunc(key, this.limit)    // 这里直接使用之前封装的哈希函数
     // 2.根据index获取对应的bucket
     var bucket = this.storage[index]
     // 3.判断该bucket是否为null
     if (bucket == null) {
       bucket = []
       this.storage[index] = bucket
     }
     // 4.判断bucket里面是否有key的元组
     // 有则是修改value值(因为用的是链地址法)
     for (var i = 0; i < bucket.length; i++) {
       var tuple = bucket[i]
       if (key == tuple[0]) {
         tuple[1] = value
         return
       }
     }
     // 没有则直接在bucket里插入元组
     bucket.push([key, value])
     this.count += 1
   }
     
   // 获取存放的数据
   HashTable.prototype.get = function (key) {
     // 1.获取key对应的index
     var index = this.hashFunc(key, this.limit)
     // 2.通过index获取到bucket
     var bucket = this.storage[index]
     // 3.判断bucket是否为null,如果为null则没有数据
     if (bucket == null) {
       return null
     }
     // 4.如果bucket存在,判断元组里是否有对应的key,有则返回数据
     for (var i = 0; i < bucket.length; i++) {
       var tuple = bucket[i]
       if (tuple[0] == key) {
         return tuple[1]
       }
     }
     // 5.没有找到,return null
     return null
   }
     
   // 删除数据
   HashTable.prototype.remove = function (key) {
     // 1.获取key对应的index
     var index = this.hashFunc(key, this.limit)
     // 2.通过index获取到bucket
     var bucket = this.storage[index]
     // 3.判断bucket是否为null,如果为null则返回false,表示没有要删除的数据
     if (bucket == null) {
       return false
     }
     // 4.如果bucket存在,判断元组里是否有对应的key,有则删除数据
     for (var i = 0; i < bucket.length; i++) {
       var tuple = bucket[i]
       if (tuple[0] == key) {
         bucket.splice(i, 1)
         this.count--
       }
       return tuple[1]
     }
     // 5.没有找到, return fasle
     return false
   }
 }

时间复杂度分析

哈希表的平均时间复杂度是O(1)级别的,这是哈希表用空间换来的性能,这是哈希表最大的优势。哈希表扩缩容时这种费时操作也是不常发生,平摊下去依然趋近于O(1)级别。