JavaScript 数据结构之哈希表(散列表)

2020-12-14  本文已影响0人  前端程序猿

一、 哈希表的介绍

1. 哈希表的优势

哈希表通常是基于数组实现的,但相对于数组,它有很多优势

2. 哈希表的缺点

哈希表的实现是基于数组的下标值的一种变换,这种变换通过哈希函数实现

3. 哈希表的一些概念

4. 解决冲突的方法

通过哈希函数得到的下标值,有可能会重复, 常见有两种解决方案

5. 链地址法

示意图:

hash_link.jpg

当哈希表产生冲突时,链地址法是常见的解决冲突的方法,数组中不再存储单一的值,也是存储一个链表(或数组),将冲突的值依次追加到链表(或数组)中即可

6. 开发地址法

开发地址法的主要工作方式是寻找空白的位置来添加重复的数据

寻找空白位置,有三种不同的方法

开发地址法中查找元素时,也需要安装步长线性查找,但遇到空白位置时,就可以停止查找

开发地址法中删除元素时,不能直接把该元素的位置设为 null, 否则会影响查找操作,应该设置成一个特殊值,当查找时,遇到这个值,才能知道,后面可能还有查找的相关数据

7. 哈希化的效率

填充因子 = 总数据项 / 哈希表长度

开发地址法的填充因子,最大值为 1
链地址法的填充因子可以大于 1, 因为数组中存放的是链表,链表上又可以添加多个数据

探测次数与填充因子有关,随着填充因子的增长, 需要探测的次数会越来越多

开发地址法中, 线性探测的效率最低, 二次探测和再哈希法的效率差不多, 这三种算法的探测次数都随着填充因子的增大,呈现指数增长的趋势

链地址法,随填充因子的增大,探测次数与填充因子,呈现线性增长的趋势

因此,链地址法的效率比开放地址法的效率高很多

二、 哈希函数

1. 优秀的哈希函数

一个好的哈希函数,应该满足:

2. 霍纳法则

用于对多项式求值问题

表达式: Pn(x)= anx^n + a(n-1)x^(n-1) + … + a1x + a0 (时间复杂度为 O(n^2))

霍纳法则表示法: ((…(((anx + an - 1)x + an - 2)x + an -3)…)x + a1)x + a0 (时间复杂度为 O(n))

关于霍纳法则,需要去了解其相关介绍

3. 质数的使用

使用质数可以使数据尽可能均匀的分布

使用质数的地方有

4. 代码实现

function hashFunc(str, max) {
  let hashCode = 0;
  const primeNum = 37; // 质数

  // 霍纳法则,计算 hashCode
  for (let i = 0; i < str.length; i++) {
    hashCode = primeNum * hashCode + str.charCodeAt(i);
  }

  return hashCode % max;
}

三、哈希表的实现

class HashTable {
  constructor() {
    this.storage = [];
    this.count = 0;
    this.limit = 7; // 默认数组长度
  }

  // 判断是否是质数
  isPrime(num) {
    const temp = Math.floor(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 += 1;
    }
    return num;
  }

  // 哈希函数
  hashFunc(str, max) {
    let hashCode = 0;

    // 霍纳算法
    for (let i = 0; i < str.length; i++) {
      hashCode = hashCode * 37 + str.charCodeAt(i);
    }

    return hashCode % max;
  }

  // 插入数据
  put(key, value) {
    // 通过哈希函数计算数组下标
    const index = this.hashFunc(key, this.limit);
    let bucket = this.storage[index];

    if (!bucket) {
      // 计算得到的下标还未存放数据
      bucket = [];
      this.storage[index] = bucket;
    }

    let isOverwrite = false; // 是否为修改操作

    for (const tuple of bucket) {
      // 循环判断是否为修改操作
      if (tuple[0] === key) {
        tuple[1] = value;
        isOverwrite = true;
      }
    }

    if (!isOverwrite) {
      // 不是修改操作,执行插入操作
      bucket.push([key, value]);
      this.count++;

      // 如果填充因子大于 0.75 需要对数组进行扩容
      if (this.count / this.length > 0.75) {
        const primeNum = this.getPrime(this.limit * 2);
        this.resize(primeNum);
      }
    }
  }

  // 获取数据
  get(key) {
    const index = this.hashFunc(key, this.limit);
    const bucket = this.storage[index];
    if (!bucket) {
      return null;
    }
    for (const tuple of bucket) {
      if (tuple[0] === key) {
        return tuple[1];
      }
    }
  }

  // 删除数据
  remove(key) {
    const index = this.hashFunc(key, this.limit);
    const bucket = this.storage[index];
    if (!bucket) {
      return null;
    }
    for (let i = 0; i < bucket.length; i++) {
      const tuple = bucket[i];
      if (tuple[0] === key) {
        bucket.splice(i, 1);
        this.count -= 1;
        // 如果填充因子小于0.25,需要缩小数组容量
        if (this.count / this.limit < 0.25 && this.limit > 7) {
          const primeNum = this.getPrime(Math.floor(this.limit / 2));
          this.resize(primeNum);
        }
        return tuple[1];
      }
    }
    return null;
  }

  // 重新设置数组长度
  resize(newLimit) {
    const oldStorage = this.storage;
    this.limit = newLimit;
    this.count = 0;
    this.storage = [];
    // 将旧数据从新添加到哈希表中
    oldStorage.forEach((bucket) => {
      if (!bucket) {
        return;
      }
      for (const tuple of bucket) {
        this.put(...tuple);
      }
    });
  }

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

  size() {
    return this.count;
  }
}
上一篇 下一篇

猜你喜欢

热点阅读