JavaScript实现哈希表

840 阅读2分钟

认识哈希表

哈希表通常是基于数组实现的,但是相对于数组,它存在更多优势:

  • 哈希表可以提供非常快速的 插入-删除-查找 操作。
  • 无论多少数据,插入和删除值都只需接近常量的时间,即 O(1)  的时间复杂度。实际上,只需要几个机器指令即可完成。
  • 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。
  • 哈希表相对于树来说编码要简单得多。

哈希表同样存在不足之处:

  • 哈希表中的数据是没有顺序的,所以不能以一种固定的方式(比如从小到大 )来遍历其中的元素。
  • 通常情况下,哈希表中的 key 是不允许重复的,不能放置相同的 key,用于保存不同的元素。

封装哈希表

哈希表常见操作

  • put(key, value) 插入或修改操作。
  • get(key) 获取哈希表中特定位置的元素。
  • remove(key) 删除哈希表中特定位置的元素。
  • isEmpty() 如果哈希表中不包含任何元素,返回 trun,如果哈希表长度大于 0 则返回 false
  • size() 返回哈希表包含的元素个数。
  • resize(value) 对哈希表进行扩容操作。

哈希函数的简单实现

首先使用霍纳法则计算 hashCode 的值,通过取余操作实现哈希化,此处先简单地指定数组的大小。

hashFn(string, limit = 7) {

  // 自己采用的一个质数(无强制要求,质数即可)
  const PRIME = 31;

  // 1、定义存储 hashCode 的变量
  let hashCode = 0;

  // 2、使用霍纳法则(秦九韶算法),计算 hashCode 的值
  for (let item of string) {
    hashCode = PRIME * hashCode + item.charCodeAt();
  }

  // 3、对 hashCode 取余,并返回
  return hashCode % limit;
}

选择质数作为哈希表容量

质数判断

1 不是质数

  • 方法一:针对质数的特点:只能被 1 和 number 整除,不能被 2 ~ (number-1)整除。遍历 2 ~ (num-1) 。

    这种方法虽然能实现质数的判断,但是效率不高。

    function isPrime(number) {
      if (number <= 1) return false;
      for (let i = 2; i < number; i++) {
        if (number % i === 0) {
          return false;
        }
      }
      return true;
    }
    
    • 方法二:只需要遍历 2 ~ num 的平方根即可。该方法性能较好。
    function isPrime(number) {
      if (number <= 1 || number === 4) return false;
      const temp = Math.ceil(Math.sqrt(number));
      for (let i = 2; i < temp; i++) {
        if (number % i === 0) {
          return false;
        }
      }
      return true;
    }
    

哈希表完整实现

class HashTable {
    constructor() {
      this.storage = []; // 哈希表存储数据的变量
      this.count = 0; // 当前存放的元素个数
      this.limit = 7; // 哈希表长度(初始设为质数 7)

      // 装填因子(已有个数/总个数)
      this.loadFactor = 0.75;
      this.minLoadFactor = 0.25;
    }

    // getPrime(number) 根据传入的 number 获取最临近的质数
    getPrime(number) {
      while (!isPrime(number)) {
        number++;
      }
      return number;
    }

    // put(key, value) 往哈希表里添加数据
    put(key, value) {
      // 1、根据 key 获取要映射到 storage 里面的 index(通过哈希函数获取)
      const index = hashFn(key, this.limit);

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

      // 3、判断是否存在 bucket
      if (bucket === undefined) {
        bucket = []; // 不存在则创建
        this.storage[index] = bucket;
      }

      // 4、判断是插入数据操作还是修改数据操作
      for (let i = 0; i < bucket.length; i++) {
        let tuple = bucket[i]; // tuple 的格式:[key, value]
        if (tuple[0] === key) {
          // 如果 key 相等,则修改数据
          tuple[1] = value;
          return; // 修改完 tuple 里数据,return 终止,不再往下执行。
        }
      }

      // 5、bucket 新增数据
      bucket.push([key, value]); // bucket 存储元组 tuple,格式为 [key, value]
      this.count++;

      // 判断哈希表是否要扩容,若装填因子 > 0.75,则扩容
      if (this.count / this.limit > this.loadFactor) {
        this.resize(this.getPrime(this.limit * 2));
      }
    }

    // 根据 get(key) 获取 value
    get(key) {
      const index = hashFn(key, this.limit);
      const bucket = this.storage[index];

      if (bucket === undefined) {
        return null;
      }

      for (const tuple of bucket) {
        if (tuple[0] === key) {
          return tuple[1];
        }
      }
      return null;
    }

    // remove(key) 删除指定 key 的数据
    remove(key) {
      const index = hashFn(key, this.limit);
      const bucket = this.storage[index];

      if (bucket === undefined) {
        return null;
      }

      // 遍历 bucket,找到对应位置的 tuple,将其删除
      for (let i = 0, len = bucket.length; i < len; i++) {
        const tuple = bucket[i];
        if (tuple[0] === key) {
          bucket.splice(i, 1); // 删除对应位置的数组项
          this.count--;

          // 根据装填因子的大小,判断是否要进行哈希表压缩
          if (this.limit > 7 && this.count / this.limit < this.minLoadFactor) {
            this.resize(this.getPrime(Math.floor(this.limit / 2)));
          }

          return tuple;
        }
      }
    }

    isEmpty() {
      return this.count === 0;
    }

    size() {
      return this.count;
    }

    // 重新调整哈希表大小,扩容或压缩
    resize(newLimit) {
      // 1、保存旧的 storage 数组内容
      const oldStorage = this.storage;

      // 2、重置所有属性
      this.storage = [];
      this.count = 0;
      this.limit = newLimit;

      // 3、遍历 oldStorage,取出所有数据,重新 put 到 this.storage
      for (const bucket of oldStorage) {
        if (bucket) {
          for (const b of bucket) {
            this.put(b[0], b[1]);
          }
        }
      }
    }
 }