哈希表(散列表)

哈希表
前言

首先来看数组,我们知道数组可以通过下标值拿到某个元素,这个操作是非常快的(O(1)),但是数组的插入和删除的效率并不是很高,删除数组的某个元素我们需要将被删除元素的后面的元素往前移,插入则需要将数组元素往后移,这种移动操作是效率并不是很好。

这时候可能就会想能不能有一种数据结构既能通过某个index值快速查找到某个元素(数组的功劳),又能让插入和删除效率非常高(链表的功劳),所以就引入了哈希表(散列表)这个数据结构

哈希表
定义

哈希表就是通过key值(键值)直接访问数据的数据结构。它会通过一个函数来计算这个key值,将所需要查找的数据映射到表中一个位置来进行访问记录,这加快了查找速度。这个映射函数叫做散列(哈希)函数,存放记录的数组称为散列表(哈希表)

哈希函数通常是将一个key值转换成一个数组范围内的下标,然后我们就可以将数据存放在数组对应下标的元素当中。常见的哈希函数有这几种:除法散列法 ,平方散列法 ,斐波那契(Fibonacci)散列法。这里介绍一下除法散列法:通常这个key值是一个字符串类型,我们可以将其转换成一个大数字,然后再将这个大数字对哈希表总长度取余得到一个 index,

HashTable.prototype.hashFunction = (str, size) => {
    let hashCode = 0
    for (let i = 0; i < str.length; i++) {
      hashCode = 37 * hashCode + str.charCodeAt(i)
    }
    const index = hashCode % size

    return index
  }

我们把这个哈希函数记为f(key),key 经过 f(key)之后得到一个 index。下面我们来举一个例子:

我们将这个哈希函数表示为返回key的长度,即f(‘name’)返回4,f(‘school’)返回6,数组记为arr。

有如下几个数据,我们想把他们存放到哈希表中

key: 'name', value: 'user'
key: 'age', value: 100 

经过哈希函数的处理,f(‘name’)得到index为4,f(‘age’)得到index为3,所以我们将第一条数据存放到arr[4]中,第二条存放到arr[3]中,这样确实没有任何问题,但是我们还想要存放这样的一条数据

key: 'sex', value: 'man'

经过计算,f(‘sex’)得到index为3,这时候发现有问题了,arr[3]已经有值了,这就发生冲突了,从而引起一个问题:哈希表到底是应该存放原来的值还是最新的值还是两个都存放?这时候我们就需要用到解决冲突的方法

冲突解决

什么是冲突?就是通过哈希函数将key值转换成数组index的时候出现重复的index,这种现象称为冲突

遇见冲突,我们就需要解决它,下面就来介绍解决哈希冲突的方法

  1. 链地址法

  2. 开放地址法

  3. 公共溢出区法

    建立一个特殊存储空间,专门存放冲突的数据。此方法适用于数据和冲突比较少的情况

  4. 再哈希法

    多个hash函数,第一个发生冲突了使用第二个,第二个发生冲突了使用第三个…

链地址法

将重复的元素封装成一个链表/数组,也就是说数组每个index对应的元素不再是一个单独的元素而是一个链表/数组。

此时,数组的每个元素不再是一个单独的元素而变成了链表/数组。如果某个数据的key值经过哈希函数得到的index已经存在,首先通过这个index拿到index位置相应的链表/数组,然后将这个数据插入到链表/数组的末端或首端都可以,这样即保留了旧数据又添加了新数据。如果我们想要得到某个数据,首先通过哈希函数将key值转换成index,然后在通过这个index拿到相应的链表/数组,然后通过key值遍历链表拿到对应的数据,注意:key值不能重复

数组+链表

5Rod6s.png
数组+数组
5WNkWQ.png

开放地址法

就是寻找数组中的空白位置,即没有数据的index位置,来存放冲突的数据。常见有线性探测和二次探测

线性探测

从当先index位置开始往后找,依次让index + 1,直到找到一个空白位置来存放这个数据

5R75FK.png

但是线性探测会产生聚集问题:如图从index=0至index=30数组都存放了数据,此时再插入一个数据

key: 'abcd', value: 'abcd'

通过哈希函数计算index=4,index=4的位置已经有数据了,所以找index+1的位置,不巧的是index+1的位置也已经有数据,再找index+2的位置,index+3,index+4,index+5,……,直到找到index+27=31这个位置,这个位置的数组没有存放数据,所以我们可以将此数据存放到index=31这个位置。

数据虽然是存放了,但是我们发现这个查询操作肯定是非常耗时的,左图是只有几十个数据,那如果有几百个,几千个呢?那肯定会非常耗时,所以为了解决这个问题,就有了二次探测

5RxidJ.png

二次探测

二次探测其实和线性探测类似,只不过线性探测是每次步长 + 1,index +1,index +2,…;而二次探测是“先平方再相加”:index + 1^2,
index + 2^2, index + 3^2, …

通过二次探测增大了步长,降低了产生聚集问题的概率,但事实上二次探测还是会出现聚集问题,只不过相比于线性探测二次探测有了很大的改进

封装一个简单的哈希表

/* 
  封装哈希表,使用链地址法,哈希表每个元素存储的是数组/链表
    本例采用数组:[[[key, value], [key1,value1],...],...]
*/
function HashTable() {
  /**
   * 数组,存放相关元素,这里面的每一个元素用用来存放一个链表或数组的
   */
  this.storage = []

  /**
   * number,表示当前存放了多少条数据
   */
  this.count = 0

  /**
   * number,哈希表的总长度
   */
  this.limit = 10

  /**
   *
   * @param {*} str 字符串
   * @param {*} size 哈希表尺寸
   * @returns 下标值
   */
  HashTable.prototype.hashFunction = (str, size) => {
    let hashCode = 0
    for (let i = 0; i < str.length; i++) {
      hashCode = 37 * hashCode + str.charCodeAt(i)
    }
    const index = hashCode % size

    return index
  }

  // 添加或修改
  HashTable.prototype.put = (key, value) => {
    // 1. 获取下标值
    const index = this.hashFunction(key, this.limit)
    // 2. 拿到此下标值在哈希表中对应的链表或数组,这里用的是数组
    let bucket = this.storage[index]
    // 3. if bucket === undefined ----> add
    if (bucket === undefined) {
      bucket = []
      this.storage[index] = bucket
      //  add
      bucket.push([key, value])
      return (this.count += 1)
    }
    // 3. if bucket !== undefined   ----> add / update
    for (let i = 0; i < bucket.length; i++) {
      // update
      const element = bucket[i]
      if (element[0] === key) {
        return (element[1] = value)
      }
    }
    // add
    bucket.push([key, value])
    this.count += 1
    // 填充因子:已有数据的总数 / 哈希表总数,只要填充因子大于 0.75 就扩容 x 2
    if (this.count > this.limit * 0.75) {
      const newLimit = this.getPrime(this.limit * 2)
      this.resize(newLimit)
    }
  }

  // 获取元素
  HashTable.prototype.get = key => {
    // 1. 获取下标值
    const index = this.hashFunction(key, this.limit)
    // 2. 拿到此下标值在哈希表中对应的链表或数组
    const bucket = this.storage[index]
    // null
    if (bucket === undefined || bucket.length === 0) {
      return null
    }
    // search
    for (let i = 0; i < bucket.length; i++) {
      const element = bucket[i]
      if (key === element[0]) return element[1]
    }
    // not found
    return null
  }

  // 删除元素
  HashTable.prototype.remove = key => {
    // 1. 获取下标值
    const index = this.hashFunction(key, this.limit)
    // 2. 拿到此下标值在哈希表中对应的链表或数组
    const bucket = this.storage[index]
    // fail
    if (bucket === undefined || bucket.length === 0) {
      return false
    }
    for (let i = 0; i < bucket.length; i++) {
      const element = bucket[i]
      // success
      if (element[0] === key) {
        bucket.splice(i, 1)
        this.count -= 1
        // // 填充因子:已有数据的总数 / 哈希表总数,只要填充因子小于 0.25 就减容 / 2
        if (this.count < this.limit * 0.25 && this.limit > 7) {
          const newLimit = this.getPrime(this.limit / 2)
          this.resize(newLimit)
        }
        return true
      }
    }
    // fail
    return false
  }

  HashTable.prototype.isEmpty = () => this.count === 0

  HashTable.prototype.size = () => this.count

  // 哈希表扩容 / 减容
  HashTable.prototype.resize = newLimit => {
    const storage = this.storage
    // 1. 重置所有属性,为了重新执行插入操作
    this.storage = []
    this.count = 0
    this.limit = newLimit

    // 2. 遍历前哈希表每个元素
    for (let i = 0; i < storage.length; i++) {
      const bucket = storage[i]

      if (bucket === undefined || bucket.length === 0) continue

      // 3. 插入,此时 this.limit已经为 新的limit了
      for (let j = 0; j < bucket.length; j++) {
        this.put(bucket[i][0], bucket[i][1])
      }
    }
  }

  // 用质数作为哈希表的容量,质数就可以更好的减少冲突,且让数据在哈希表分布均匀
  HashTable.prototype.isPrime = num => {
    const temp = parseInt(Math.sqrt(num))

    for (let i = 2; i <= temp; i++) {
      if (num % i === 0) return false
    }
    return true
  }

  // 获取最近质数
  HashTable.prototype.getPrime = num => {
    while (!this.isPrime(num)) {
      num++
    }
    return num
  }
}


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值