美文网首页
JavaScript 数据结构之哈希表(散列表)

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

作者: 前端程序猿 | 来源:发表于2020-12-14 22:24 被阅读0次

一、 哈希表的介绍

1. 哈希表的优势

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

  • 快速的插入、删除、查找操作, 时间复杂度接近常量值,时间复杂度为 O(1)
  • 哈希表的速度比树还要快,代码相对树来说简单很多

2. 哈希表的缺点

  • 哈希表中的数据没有顺序,因此不能通过固定的方式遍历元素
  • 哈希表是通过空间来换取时间是数据结构,通过占用更多的内存空间来提高操作效率

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

3. 哈希表的一些概念

  • 哈希化: 将大数字转化为数组范围内下标的过程,我们称之为哈希化
  • 哈希函数:大数字在进行哈希化的实现逻辑,封装在一个函数中,我们称这个函数为哈希函数
  • 哈希表:通过哈希函数得到下标,并将数据插入到数组中,以及其它操作,对整个结构的封装,我们称之为哈希表

4. 解决冲突的方法

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

  • 链地址法(也叫拉链法)
  • 开放地址法

5. 链地址法

示意图:

hash_link.jpg

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

6. 开发地址法

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

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

  • 线性探测:
    • 插入元素产生冲突时,也就是位置上已经存在元素,以步长为 1,往后寻找空白位置,并插入数据
    • 线性探测,在插入连续的数据时,会产生聚集,导致冲突数据位置之间的距离很远,影响哈希表的操作效率
  • 二次探测
    • 二次探测对步长做了优化,比如下标从 x 开始,那么步长可以是 x + 1, x + 2^2, x + 3^3 ......, 这样探测的距离比较远,可以避免连续数据产生的聚集问题
    • 二次探测的问题是,不断的插入连续的数据,也会产生步长不一的聚集
    • 效率比线性探测高
  • 再哈希法
    • 为了解决,线性探测和二次探测可能产生的聚集问题,我们可以使用再哈希法
    • 再哈希法,是使用数据的关键字,用另一个哈希函数,再做一次哈希,用这次哈希化的结果作为步长
    • 再哈希函数的算法
      • stepSize = constant - (key % constant)
      • constant 为小于数组长度的质数
      • key 数据的关键字

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

开发地址法中删除元素时,不能直接把该元素的位置设为 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;
  }
}

相关文章

网友评论

      本文标题:JavaScript 数据结构之哈希表(散列表)

      本文链接:https://www.haomeiwen.com/subject/wrnbgktx.html