数据结构-哈希表(哈希表的实现)

155 阅读2分钟

上午写完了哈希表的理论知识,现在我们来手动实现一下哈希表。为了方便,我这里采用的是基于数组来实现哈希表的bucket子项,其实使用链表和数组所产生的效率问题在这里可以忽略不计。下面是实现过程:

class HashTable {
  // 属性
  storage = [] // 存储数据
  count = 0 // 记录hash表中的元素个数
  limit = 7 // 记录hash表的大小

  // 初始化
  constructor(limit) {
    this.limit = limit
  }
  // 方法
  // hash函数
  hashFunc(str, size) {
    let hashCode = 0

    for (let i = 0; i < str.length; i++) {
      hashCode = 37 * hashCode + str.charCodeAt(i)
    }

    // 取余操作
    let index = hashCode % size
    return index
  }

  // 插入&修改数据
  put(key, value) {
    // 根据key获取对应的index
    let index = this.hashFunc(key, this.limit)

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

    // 如果bucket为空,则直接插入
    if (bucket == null) {
      bucket = []
      this.storage[index] = bucket
    }

    // 判断是否是修改数据
    for (let i in bucket) {
      let tuple = bucket[i]
      if (tuple[0] === key) {
        tuple[1] = value
        return
      }
    }

    // 插入数据
    bucket.push([key, value])
    this.count++

    // 判断是否需要扩容
    if (this.count >= this.limit * 0.75) {
      let newSize = this.limit * 2
      let newPrime = this.#getPrime(newSize)
      this.resize(newPrime)
    }
  }

  // 获取数据
  get(key) {
    // 根据key获取对应的index
    let index = this.hashFunc(key, this.limit)

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

    // 如果bucket为空,则返回null
    if (bucket == null) {
      return null
    }

    // 遍历bucket
    for (let i in bucket) {
      let tuple = bucket[i]
      if (tuple[0] === key) {
        return tuple[1]
      }
    }

    // 没有找到 return null
    return null
  }

  // 删除数据
  remove(key) {
    // 根据key获取对应的index
    let index = this.hashFunc(key, this.limit)

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

    // 如果bucket为空,则返回null
    if (bucket == null) {
      return null
    }

    // 遍历bucket
    for (let i in bucket) {
      let tuple = bucket[i]
      if (tuple[0] === key) {
        bucket.splice(i, 1)
        this.count--
        // 缩小容量
        if (this.limit > 7 && this.count <= this.limit * 0.25) {
          let newSize = Math.floor(this.limit / 2)
          let newPrime = this.#getPrime(newSize)
          this.resize(newPrime)
        }
        return tuple[1]
      }
    }

    return null
  }

  // 判断哈希表是否为空
  isEmpty() {
    return this.count === 0
  }

  // 获取哈希表中的元素个数
  size() {
    return this.count
  }

  // 清空哈希表
  clear() {
    this.storage = []
    this.count = 0
  }

  // 哈希表扩容
  resize(newLimit) {
    let oldStorage = this.storage
    this.limit = newLimit
    this.storage = []
    this.count = 0
    for (let i in oldStorage) {
      let bucket = oldStorage[i]
      if (bucket == null) continue
      for (let j in bucket) {
        let tuple = bucket[j]
        this.put(tuple[0], tuple[1])
      }
    }
  }

  // 判断是否为质数
  #isPrime(num) {
    let temp = parseInt(Math.sqrt(num))
    for (let i = 2; i <= temp; i++) {
      if (num % i === 0) {
        return false
      }
    }
    return true
  }

  // 获取新质数
  #getPrime(num) {
    while (!this.#isPrime(num)) {
      num++
    }
    return num
  }
}

在理清楚哈希表的原理之后,写一个简单的哈希表还是很简单的。如上述代码,我们给hashTable的容量limit是7,如果插入的数据占满了容量怎么办呢?也许你会说那么就存bucket中不就行了么,反正可以无限延伸长度。但是如果bucket中的数据过多,那么一定会造成读取数据的效率降低,所以这时候就需要对哈希表进行扩容操作。

哈希表的扩容思想

为什么需要扩容

上述已经说了如果不进行扩容,那么当数据也来越多的时候,读取数据的效率会低。

如何扩容

扩容可以简单的将容量扩大两倍(之前不是说哈希表的长度最好为质数吗?这个问题我们之后再讨论)
但是这种情况下,所有数据项一定要同时进行修改,也就是要重新调用哈希函数,获取不同的index位置
例如hashCode = 12的数据项,当length = 8时,index = 4,当length = 16时,index = 12。
虽然这是一个耗时的过程,但是如果数组需要扩容,这一过程必不可少

什么情况下需要扩容

一般来说,当loadFactor(装填因子,上一篇有详细介绍) > 0.75时进行扩容。而当loadFactor < 0.25时进行缩容,具体的扩容缩容方法上述有实现。接下来我们来解决开头提到的容量质数问题。

容量质数

虽然说在链地址法中将容量设置为质数,没有在开放地址法中的重要,但是为了提高效率,我们还是来实现一下。
首先来实现一个算法,用于判断一个数是否为质数:

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

但是这个算法的效率并不高,因为对于每个数n,其实并不要判断从2到n-1。只需要判断它是否能整除它的平方根之前的整数就可以了:

    function isPrimePlus(num) {
      let temp = parseInt(Math.sqrt(num))
      for (let i = 2; i <= temp; i++) {
        if (num % i === 0) {
          return false
        }
      }
      return true
    }

回归主题,我们如何将容量扩充为质数,算法很简单:

  function getPrime(num) {
    while (!this.#isPrime(num)) {
      num++
    }
    return num
  }

简单来说就是只要这个数不是质数,那么就加一,直到加这个数为质数为止。在哈希表中主要是在数组容量改变时用到扩充为质数,详见上述哈希表的实现中插入和删除操作。

至此,哈希表的基本封装就完成了。