JS数据结构与算法—哈希表

346 阅读6分钟

对哈希表的认识

哈希表的优势:

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

哈希表的不足

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

什么是哈希表

哈希表的结构就是数组, 但是它神奇的地方在于对下标值的一种变换, 这种变换我们可以称之为哈希函数,通过哈希函数可以获取到HashCode.

  • 哈希化: 将大数字转化成数组范围内下标的过程, 我们就称之为哈希化.
  • 哈希函数: 通常我们会将单词转成大数字, 大数字在进行哈希化的代码实现放在一个函数中, 这个函数我们成为哈希函数.
  • 哈希表: 最终将数据插入到的这个数组, 我们就称之为是一个哈希表

解决冲突

链地址法(拉链法)

链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据, 而是一个链条,链条可以是数组也可以是链表。

开放地址法

开放地址法其实就是要寻找空白的位置来放置冲突的数据项。

  • 线性探索(线性的查找空白的单元)

    • 注意: 删除操作一个数据项时, 不可以将这个位置下标的内容设置为null,因为将它设置为null可能会影响我们之后查询其他操作, 所以通常删除一个位置的数据项时, 我们可以将它进行特殊处理(比如设置为-1)。

    • 线性探索存在的问题:聚集:如在没有任何数据的时候, 插入的是22-23-24-25-26, 那么意味着下标值:2-3-4-5-6的位置都有元素. 这种一连串填充单元就叫做聚集.

  • 二次探测(一次性探测比较常的距离,避免聚集带来的影响)

    • 次探测主要优化的是探测时的步长:线性探测, 我们可以看成是步长为1的探测, 比如从下标值x开始, 那么线性测试就是x+1, x+2, x+3依次探测。二次探测, 对步长做了优化, 比如从下标值x开始, x+1², x+2², x+3².

    • 二次探测存在的问题: 比如我们连续插入的是32-112-82-2-192, 那么它们依次累加的时候步长的相同的。也就是这种情况下会造成步长不一的一种聚集. 还是会影响效率.

  • 再哈希法(把关键字用另外一个哈希函数, 再做一次哈希化, 用这次哈希化的结果作为步长.)

    • 为了消除线性探测和二次探测中无论步长+1还是步长+平法中存在的问题, 还有一种最常用的解决方案: 再哈希法。
    • 和第一个哈希函数不同. (不要再使用上一次的哈希函数了, 不然结果还是原来的位置)
    • 不能输出为0(否则, 将没有步长. 每次探测都是原地踏步, 算法就进入了死循环)

哈希化的效率

  • 哈希表中执行插入和搜索操作可以达到O(1)的时间级,如果没有发生冲突,效率很高。如果发生冲突,存取时间就依赖后来的探测长度。
  • 平均探测长度以及平均存取时间,取决于填装因子,随着填装因子变大,探测长度也越来越长,效率下降。

装填因子

  • 装填因子表示当前哈希表中已经包含的数据项和整个哈希表长度的比值.
  • 装填因子 = 总数据项 / 哈希表长度.
  • 开放地址法的装填因子最大是多少呢? 1, 因为它必须寻找到空白的单元才能将元素放入.
  • 链地址法的装填因子呢? 可以大于1, 因为拉链法可以无限的延伸下去
  • 链地址法相对来说效率是好于开放地址法的,一般的真实开发中,使用链地址法的情况较多(因为它不会因为添加了某元素后性能急剧下降)

哈希函数

快速计算

  • 哈希表的主要优点是它的速度, 所以在速度上不能满足, 那么就达不到设计的目的了.
  • 提高速度的一个办法就是让哈希函数中尽量少的有乘法和除法. 因为它们的性能是比较低的.
  • 多项式优化:解决这类求值问题的高效算法――霍纳法则,使用大O表示时间复杂度的话, 我们直接从O(N²)降到了O(N)

均匀分布

  • 为了提供效率, 最好的情况还是让数据在哈希表中均匀分布。因此我们需要在使用常量的地方, 尽量使用质数。
  • 哈希表的长度使用质数,避免相同步长可以被容量除,导致算法循环直至程序崩溃。
  • N次幂的底数, 使用质数

实现哈希表

 class HashTable {
     constructor() {
         this.storage = [];
         this.count = 0;
         this.limit = 7;
     }
     //设计哈希函数
     // 1)将字符串转换成比较大的数字:hashCode
     // 2)将大的数字hashCode压缩到数组范围之内
     hashFunc(str, size) {
         //定义hashCode变量
         let hashCode = 0;
         //霍纳算法
         for (let i = 0; i < str.length; i++) {
             hashCode = hashCode * 37 + str.charCodeAt(i);
         }
         //取余
         let index = hashCode % size;
         return index;
     }
     put(key, value) {
         //获取key的index
         let index = this.hashFunc(key, this.limit);
         //根据索引值取出bucket
         let bucket = this.storage[index];
         //判断bucket是否为空
         if (bucket == null) {
             bucket = [];
             this.storage[index] = bucket;
         }
         //判断是否修改数据
         for (let i = 0; i < bucket.length; i++) {
             let tuple = bucket[i];
             if (tuple[0] == key) {
                 tuple[1] = value;
                 return;
             }
         }
         //添加操作
         bucket.push([key, value]);
         this.count++;
     }
     get(key) {
         //获取对应的index
         let index = this.hashFunc(key, this.limit);
         //获取对应的bucket
         let bucket = this.storage[index];
         //判断bucket是否为空
         if (bucket == null) return null;

         //查找对应bucket中对应的key
         for (let i = 0; i < bucket.length; i++) {
             let tuple = bucket[i];
             if (tuple[0] == key) {
                 return tuple[1];
             }
         }
         //遍历完还没找到,返回null
         return null;
     }
     remove(key) {
         //获取对应的index
         let index = this.hashFunc(key, this.limit);
         //获取对应的bucket
         let bucket = this.storage[index];
         //判断bucket是否为空
         if (bucket == null) {
             return null;
         }
         //遍历bucket
         for (let i = 0; i < bucket.length; i++) {
             let tuple = bucket[i];
             if (tuple[0] == key) {
                 bucket.splice(i, 1);
                 this.count--;
                 return tuple[1];
             }
         }
         //遍历完没找到,返回null
         return null;
     }
     isEmpty() {
         return this.count == 0;
     }
     size() {
         return this.count;
     }
 }
 let hashT = new HashTable();
 hashT.put('cba', 'one');
 hashT.put('bca', 'two');
 hashT.put('nba', 'three');
 hashT.put('bbb', 'four');
 hashT.put('mba', 'six');
 // console.log(hashT);
 console.log(hashT.get('cba'));
 console.log(hashT.remove('cba'));
 hashT.remove('cba');
 console.log(hashT);

哈希表扩容

为什么要扩容

  • 如上文封装的哈希表,所有数据都存储在长度为7的数组中,因为使用的是链地址法,loadFactor可以大于1,所以这个哈希表可以无限制的插入新数据,但是随着数据量的增多,每个index对应的bucket会越来越长,导致效率的降低,所以需要扩容。
  • loadFactor>0.75或loadFactor<0.25时进行调整

扩容

 resize(newLimit) {
      //将原有哈希表保存
      let oldStorage = this.storage;
      //清空哈希表
      this.storage = [];
      this.count = 0;
      this.limit = newLimit;
      //将原有数据遍历到新哈希表中
      for (let i = 0; i < oldStorage.length; i++) {
          let bucket = oldStorage[i];
          if (bucket == null) {
              continue;
          }
          for (let j = 0; j < bucket.length; j++) {
              let tuple = bucket[j];
              this.put(tuple[0], tuple[1]);
          }
      }
  }

扩容为质数(有利于均匀分布)

质数判断两种方法:第一种判断一直判断到num,第二种判断到num的平方根就截止,所以第二种方法更高效。所以后面进行扩容操作时,选择第二种方法判断质数。

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

最终扩容

在添加和删除方法中调用扩容

  class HashTable {
        constructor() {
            this.storage = [];
            this.count = 0;
            this.limit = 7;
        }
        //  哈希函数
        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是否为null
            if (bucket == null) {
                bucket = [];
                this.storage[index] = bucket;
            }
            //判断是否修改数据
            for (let i = 0; i < bucket.length; i++) {
                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) {
            // 获取对应index
            let index = this.hashFunc(key, this.limit);
            // 获取对应的bucket
            let bucket = this.storage[index];
            //判断bucket是否为null
            if (bucket == null) {
                return null;
            }
            //判断bucket中是否有该key
            for (let i = 0; i < bucket.length; i++) {
                let tuple = bucket[i];
                if (tuple[0] === key) {
                    return tuple[1];
                }
            }
            return null;
        }
        remove(key) {
            let index = this.hashFunc(key, this.limit);
            let bucket = this.storage[index];

            if (bucket === null) return null;

            for (let i = 0; i < bucket.length; i++) {
                let tuple = bucket[i];
                if (tuple[0] === key) {
                    bucket.splice(i, 1);
                    this.count--;
                    return tuple[1];
                }
            }
            return null;
            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);
            }
        }
        //判断哈希表是否为空
        isEmpty() {
            return this.count === 0;
        }
        //获取哈希表中元素的个数
        size() {
            return this.count;
        }
        //哈希表扩容
        resize(newLimit) {
            let oldStorage = this.storage;
            this.storage = [];
            this.count = 0;
            this.limit = newLimit;
            for (let i = 0; i < oldStorage.length; i++) {
                let bucket = oldStorage[i];
                if (bucket == null) {
                    continue;
                }
                for (let j = 0; j < bucket.length; j++) {
                    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;
        }
    }
                           ————————文章根据coderwhy老师的js数据结构课程记录