JS 数据结构 哈希表(HashTable)

哈希表

数组的特点(补充)

  1. 数组进行插入时,效率较低
  2. 数组进行查找操作的效率
    1. 如果是基于索引进行查找效率非常高
    2. 基于内容查找,效率较低
  3. 数组进行删除操作,效率较低

哈希表的特点

  • 哈希表通常是基于数组进行实现的
  • 优点
    • 提供非常快速的插入-删除-查找操作
    • 无论数据多少,插入和删除的效率都非常高(即 O(1)的时间级)
    • 哈希表的速度比树快,几乎瞬间可找到
    • 代码相对树简单
  • 缺点
    • 数据没有顺序,不能以一种固定的方式来遍历其中的元素
    • 哈希表的 key 是不允许重复的,不能放置相同的 key,用于保存不同的数据

探测方法

  1. 线性探测(缺点:连续的一串数字[聚集],导致性能增大)
  2. 二次探测(缺点:对于 32-12-42-2,也会导致性能增大)[探测步长是固定的:1,4,9,16…]
  3. 再哈希法(关键字用另外一种哈希函数,再次哈希化)

    stepSize[步长] = constant[质数] - (key % constant)

冲突解决方案

  1. 链地址法
  2. 开放地址法(寻找空白单元格,添加重复的数据)

填装因子

  • 表示当前哈希表中已经包含的数据项整个哈希表长度比值
  • 填装因子 = 总数据项 / 哈希表长度
  • 开放地址法 最大为:1 (必须寻找到空白的单元才能将元素放入)
  • 链地址法 可以大于 1 (可以无限延伸下去,效率逐渐变低)

冲突解决方案效率比较

  • 如果无冲突,效率很高
  • 如果有冲突,存取时间就依赖后来的探测长度
  • 平均探测长度以及平均存取时间,取决于填装因子(增大而越长,效率将会下降)
  1. 线性探测(探测序列 P 和填装因子 L)–填装因子越大,效率越低
    成功情况: P = 1 + 1 ( 1 − L ) 2 2 P= \frac{1+\frac{1}{(1-L)^2}}{2} P=21+(1L)21
    失败情况: P = 1 + 1 ( 1 − L ) 2 P= \frac{1+\frac{1}{(1-L)}}{2} P=21+(1L)1
  2. 二次探测/再哈希法–填装因子越大,效率越低
    成功情况: P = − l o g 2 1 − l o a d F a c t o r l o a d F a c t o r P=-log{_2} \frac{1-loadFactor}{loadFactor} P=log2loadFactor1loadFactor
    失败情况: P = 1 1 − l o a d F a c t o r P= \frac{1}{1-loadFactor} P=1loadFactor1
  3. 链地址法: P = N a r r a y S i z e P= \frac{N}{arraySize} P=arraySizeN
    成功情况: P = 1 + l o a d F a c t o r 2 P= 1+\frac{loadFactor}{2} P=1+2loadFactor
    失败情况: 1 + l o a d F a c t o r 1+loadFactor 1+loadFactor

设计哈希函数具备的优点

  1. 快速的计算
    • 快速获取到对应的 hashCode
    • 通过快速的计算获取到元素对应的 hashCode
  2. 均匀的分布
    • 多个元素映射到同一个位置时,都会影响效率
    • 优秀的哈希函数应该尽可能将元素映射到不同的位置,让元素均匀分布
class HashTable {
  constructor() {
    this.storage = []
    this.count = 0
    this.limit = 7 /* 当前总长度(尽可能为质数) */
  }

  // 1. 哈希函数
  hashFunc(str, size) {
    // 1. 定义hashCode变量
    var hashCode = 0

    // 2. 霍纳算法,计算hashCode的值
    for (var i = 0; i < str.length; i++) {
      hashCode = 37 * hashCode + str.charCodeAt(i)
    }

    // 3. 取余操作
    var index = hashCode % size

    // 4. 返回index(索引值)
    return index
  }

  // 2. 插入&修改数据
  put(key, value) {
    // 1. 根据key获取对应的索引值
    var index = this.hashFunc(key, this.limit)

    // 2. 根据index取出对应的bucket
    var bucket = this.storage[index]

    // 3. 判断bucket是否为空
    if (bucket == null) {
      bucket = []
      this.storage[index] = bucket
    }

    // 4. 判断是否为修改数据
    for (var i = 0; i < bucket.length; i++) {
      var tuple = bucket[i]
      if (tuple[0] == key) {
        tuple[1] = value
        return
      }
    }

    // 5. 进行添加数据
    bucket.push([key, value])
    this.count += 1
  }

  // 3. 获取数据
  get(key) {
    // 1. 获取对应的索引
    var index = this.hashFunc(key, this.limit)

    // 2. 获取对应的bucket
    var bucket = this.storage[index]

    // 3. 判断是否为空
    if (bucket == null) {
      return null /* 值不存在 */
    }

    // 4. 存在bucket,进行线性查找
    for (var i = 0; i < bucket.length; i++) {
      var tuple = bucket[i]
      if (tuple[0] == key) {
        return tuple[1]
      }
    }

    // 5. bucket内不存在该值
    return null
  }

  // 4. 删除数据
  remove(key) {
    // 1. 找到对应的索引值
    var index = this.hashFunc(key, this.limit)

    // 2. 找到对应的Bucket
    var bucket = this.storage[index]

    // 3. 判断bucket是否存在
    if (bucket == null) {
      return false
    }

    // 4. 寻找对应的数据,并删除
    for (var i = 0; i < bucket.length; i++) {
      var tuple = bucket[i]
      if (tuple[0] == key) {
        bucket.splice(i, 1)
        this.count -= 1
        return tuple[1]
      }
    }

    // 5. 不存在该数据
    return false
  }

  /* 其余方法 */
  // 5. 判断是否为空
  isEmpty() {
    return this.count == 0
  }
  // 6. 获取元素的个数
  size() {
    return this.count
  }
}

扩容

  • loadFactor>0.75 时,需要进行扩容
  • 可以扩容(容量增大一倍),但随数据增多导致效率降低
    • 扩容之后,所有数据一定要同时进行修改

判断:

  1. this.count > this.limit * 0.75 时,需要扩容

    if (this.count > this.limit * 0.75) {
      var newSize = this.limit * 2
      var newPrime = this.getPrime(newSize) /* 获取质数 */
      this.resize(newPrime)
    }
    
  2. this.limit > 7 && this.count < this.limit * 0.25 时,需要减容

    if (this.limit > 7 && this.count < this.limit * 0.25) {
      var newSize = Math.floor(this.limit / 2)
      var newPrime = this.getPrime(newSize) /* 获取质数 */
      this.resize(newPrime)
    }
    

代码部分:

resize (newLimit) {
  // 1. 保存旧的数据内容
  var oldStorage = this.storage

  // 2. 重置内容
  this.storage = []
  this.count = 0
  this.limit = newLimit

  // 3. 遍历oldStorage中所有的bucket
  for (let i = 0; i < oldStorage.length; i++) {
    // 3.1 取出对应的bucket
    var bucket = oldStorage[i]

    // 3.2 判断bucket是否为空
    if (bucket == null) {
      continue
    }

    // 3.3 取出数据(有数据),重新插入
    for (let j = 0; j < bucket.length; j++) {
      const tuple = bucket[j]
      this.put(tuple[0], tuple[1])
    }
  }
}
/* 判断某个数是否是质数 */
isPrime (num) {
  // 1. 获取平方根
  var temp = parseInt(Math.sqrt(num))

  // 2. 循环判断
  for (let i = 0; i <= temp; i++) {
    if (num % i == 0) {
      return false
    }
  }

  return true
}
/* 获取质数的方法 */
getPrime (num) {
  while (!this.isPrime(num)) {
    num++
  }
  return num
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值