简单哈希列表的实现与优化

96 阅读1分钟

平时在开发中很少用到哈希表,久而久之对于这个数据结构就陌生了,今天就来手写一个简单哈希表的实现,回顾一下知识

image.png

  1. 首先我们需要实现一个生成对应哈希表中存储位置hashCode值的函数
// 链地址法
class HashTable {
  constructor() {
    this.storage = []// 生成的哈希表容器
    this.limit = 7// 哈希表容量,需要一个大于5的质数,如果不是用质数来做模运算的话,很多生活中的数据分布,会集中在某些点上
    this.count = 0// 哈希表当前存放数据计量,由于链地址法中的每个bucket可以无限存放数据,当大于当前哈希表容量长度this.limit,就会导致查找性能降低,失去哈希查找的意义,所以一般我们在this.count>this.limit*0.75的情况下进行扩容;同理,当删除数据之后,一般存在:this.count<this.limit*0.25为了不占用过多内存,我们也需要减容
  }

  /**
   * 使用霍纳算法获取hashCode
   * @param {string} str 根据传入的字符串生成hashCode
   *  @param {int} size 哈希表初始容量,大于5
   */
  getHashCode (str, size = 7) {
    let hashCode = 0
    for (let i = 0; i < str.length; i++) {
      hashCode = hashCode * 37 + str.charCodeAt(i)
    }
    // 取余操作,获取最终存放位置
    const index = hashCode % size
    return index
  }
 }

2.有步骤1我们拿到了存放数据的桶在哈希表中的位置,现在我们实现根据传入的key和value将数据放入桶内,并将桶放在对应的哈希表中,做到更新数据或者新增数据

class HashTable {
  //以下为新增代码
   /**
 * 插入或修改数据
 * @param {str} key 根据key获取索引值
 * @param {str} val 要存入的值
 */
  put (key, val) {
    // 根据key获取对应的index
    const index = this.getHashCode(key, this.limit)
    console.log('index,', key, index)
    // 获取对应index存放数据的桶
    let bucket
    if (!this.storage[index]) {
      bucket = []
      this.storage[index] = bucket
    } else {
      bucket = this.storage[index]
    }
    // 遍历桶,修改数据
    for (let idx = 0; idx < bucket.length; idx++) {
      const item = bucket[idx]
      if (item.key === key) {
        item.value = val
        return
      }
    }
    // 新增数据
    bucket.push({ key, value: val })
    this.count += 1
  }
 }

3.根据传入的key,找到对应的桶的存放位置,遍历桶,当ke相对应时,实现删除数据

class HashTable {
  //以下为新增代码
   /**
  * 根据传入的key删除
  * @param {str} key
  */
  remove (key) {
    // 根据key获取对应的index
    const index = this.getHashCode(key, this.limit)
    // 获取对应index存放数据的桶
    const bucket = this.storage[index]
    if (!bucket) {
      // 存放数据的桶不存在直接返回null
      return null
    } else {
      // 遍历桶,查找对应key的val
      for (let idx = 0; idx < bucket.length; idx++) {
        const item = bucket[idx]
        if (item.key === key) {
          bucket.splice(idx, 1)
          this.count -= 1
          return item.value
        }
      }
      return null
    }
  }
}

4.当新增数据或者删除数据时,我们需要判断当前容量是否合适,适当的进行扩容或者减容,由于哈希表容量必须是一个质数,所以我们相应的需要在判断时作处理

class HashTable {
  //以下为新增代码
  /**
 * 扩容
 * @param {bool} isAdd 是否新增
 */
  resize (isAdd) {
    // return false
    // 是否达到扩容的条件
    const flag = this.count > this.limit * 0.75 || (this.limit > 7 && this.count < this.limit * 0.25 && !isAdd)
    if (flag) {
      const newSize = isAdd ? this.limit * 2 : Math.floor(this.limit / 2)
      // 哈希表的容量最好是一个质数
      if (this.isPrime(newSize)) {
        this.limit = newSize
      } else {
        this.limit = this.getPrime(newSize)
      }
      this.reStorage()
    }
  }

  /**
   * 重新存储
   */
  reStorage () {
    const oldStorage = this.storage
    this.storage = []
    this.count = 0
    for (let idx = 0; idx < oldStorage.length; idx++) {
      const bucket = oldStorage[idx]
      console.log('bucket', idx, bucket)
      if (!bucket) {
        continue
      } else {
        // 将bucket里的每一个数据重新存放到哈希表
        for (let i = 0; i < bucket.length; i++) {
          const item = bucket[i]
          this.put(item.key, item.value)
        }
      }
    }
  }

  /**
   * 判断一个数是否为质数
   * @param {int} num 传入的整数
   */
  isPrime (num) {
    // 将num开根,提高效率
    const target = Math.sqrt(num)
    for (var i = 2; i < target; i++) {
      if (num % i === 0) {
        return false
      }
    }
    return true
  }

  /**
   * 根据传入的一个数获取最接近的质数
   * @param {int} num 传入的整数
   */
  getPrime (num) {
    while (!this.isPrime(num)) {
      num++
    }
    return num
  }

经过上面的步骤我们就实现了一个简单的基于链地址法的哈希表,哈希表在数据量比较庞大时对于数据的处理效率会高于一般的数据结构如数组,以下是测试结果以及全部代码,欢迎多批评!

新增过多会扩容

const hashTable = new HashTable()
hashTable.put('abc', 1)
hashTable.put('cba', 2)
hashTable.put('acb', 3)
hashTable.put('abcd', 1)
hashTable.put('cabdd', 2)
hashTable.put('abscd', 1)
hashTable.put('cabad', 2)
hashTable.put('abdcd', 1)
hashTable.put('casbd', 2)
console.log('storage===', hashTable.storage)
console.log('count===', hashTable.count)
console.log('limit===', hashTable.limit)

image.png

删除过多会减容

const hashTable = new HashTable()
hashTable.put('abc', 1)
hashTable.put('cba', 2)
hashTable.put('acb', 3)
hashTable.put('abcd', 1)
hashTable.put('cabdd', 2)
hashTable.put('abscd', 1)
hashTable.put('cabad', 2)
hashTable.put('abdcd', 1)
hashTable.put('casbd', 2)
hashTable.remove('cabad')
hashTable.remove('abdcd')
hashTable.remove('casbd')
hashTable.remove('abc')
hashTable.remove('cba')
hashTable.remove('acb')
console.log('storage===', hashTable.storage)
console.log('count===', hashTable.count)
console.log('limit===', hashTable.limit)

image.png

全部代码

// 链地址法
class HashTable {
  constructor() {
    this.storage = []// 生成的哈希表容器
    this.limit = 7// 哈希表容量,需要一个大于5的质数,具体为什么可以了解下哈希表的相关特性
    this.count = 0// 哈希表当前存放数据计量,由于链地址法中的每个bucket可以无限存放数据,当大于当前哈希表容量长度this.limit,就会导致查找性能降低,失去哈希查找的意义,所以一般我们在this.count>this.limit*0.75的情况下进行扩容;同理,当删除数据之后,一般存在:this.count<this.limit*0.25为了不占用过多内存,我们也需要减容
  }

  /**
   * 使用霍纳算法获取hashCode
   * @param {string} str 根据传入的字符串生成hashCode
   *  @param {int} size 哈希表初始容量,大于5
   */
  getHashCode (str, size = 7) {
    let hashCode = 0
    for (let i = 0; i < str.length; i++) {
      hashCode = hashCode * 37 + str.charCodeAt(i)
    }
    // 取余操作,获取最终存放位置
    const index = hashCode % size
    return index
  }

  /**
 * 插入或修改数据
 * @param {str} key 根据key获取索引值
 * @param {str} val 要存入的值
 */
  put (key, val) {
    // 根据key获取对应的index
    const index = this.getHashCode(key, this.limit)
    console.log('index,', key, index)
    // 获取对应index存放数据的桶
    let bucket
    if (!this.storage[index]) {
      bucket = []
      this.storage[index] = bucket
    } else {
      bucket = this.storage[index]
    }
    // 遍历桶
    for (let idx = 0; idx < bucket.length; idx++) {
      const item = bucket[idx]
      if (item.key === key) {
        // 修改数据
        item.value = val
        return
      }
    }
    // 新增数据
    bucket.push({ key, value: val })
    this.count += 1
    this.resize(true)
  }

  /**
 * 根据传入的key获取value
 * @param {str} key
 */
  get (key) {
    // 根据key获取对应的index
    const index = this.getHashCode(key, this.limit)
    // 获取对应index存放数据的桶
    const bucket = this.storage[index]
    if (!bucket) {
      // 存放数据的桶不存在直接返回null
      return null
    } else {
      // 遍历桶,查找对应key的val
      for (let idx = 0; idx < bucket.length; idx++) {
        const item = bucket[idx]
        if (item.key === key) {
          return item.value
        }
      }
      return null
    }
  }

  /**
  * 根据传入的key删除
  * @param {str} key
  */
  remove (key) {
    // 根据key获取对应的index
    const index = this.getHashCode(key, this.limit)
    // 获取对应index存放数据的桶
    const bucket = this.storage[index]
    if (!bucket) {
      // 存放数据的桶不存在直接返回null
      return null
    } else {
      // 遍历桶,查找对应key的val
      for (let idx = 0; idx < bucket.length; idx++) {
        const item = bucket[idx]
        if (item.key === key) {
          bucket.splice(idx, 1)
          this.count -= 1
          this.resize(false)
          return item.value
        }
      }
      return null
    }
  }

  /**
 * 扩容
 * @param {bool} isAdd 是否新增
 */
  resize (isAdd) {
    // return false
    // 是否达到扩容的条件
    const flag = this.count > this.limit * 0.75 || (this.limit > 7 && this.count < this.limit * 0.25 && !isAdd)
    if (flag) {
      const newSize = isAdd ? this.limit * 2 : Math.floor(this.limit / 2)
      // 哈希表的容量最好是一个质数
      if (this.isPrime(newSize)) {
        this.limit = newSize
      } else {
        this.limit = this.getPrime(newSize)
      }
      this.reStorage()
    }
  }

  /**
   * 重新存储
   */
  reStorage () {
    const oldStorage = this.storage
    this.storage = []
    this.count = 0
    for (let idx = 0; idx < oldStorage.length; idx++) {
      const bucket = oldStorage[idx]
      console.log('bucket', idx, bucket)
      if (!bucket) {
        continue
      } else {
        // 将bucket里的每一个数据重新存放到哈希表
        for (let i = 0; i < bucket.length; i++) {
          const item = bucket[i]
          this.put(item.key, item.value)
        }
      }
    }
  }
  
  /**
   * 判断一个数是否为质数
   * @param {int} num 传入的整数
   */
  isPrime (num) {
    // 将num开根向上取整,提高效率
    const target = Math.sqrt(num)
    for (var i = 2; i < target; i++) {
      if (num % i === 0) {
        return false
      }
    }
    return true
  }

  /**
   * 根据传入的一个数获取最接近的质数
   * @param {int} num 传入的整数
   */
  getPrime (num) {
    while (!this.isPrime(num)) {
      num++
    }
    return num
  }
}