javascript数据结构 -- 哈希表(五)

2,169 阅读4分钟

哈希表

本文使用开放地址方法和再哈希法实现哈希表,不同于线性探测方法,再哈希方法需要根据key来确定探索的步长,并使用质数容量确保每一个下标都可以被探测。

1. 再哈希方法需要解决的三个额外问题

  1. 如何保证哈希表的容量总是质数;
  2. 如何通过key产生探索步长;
  3. 如何证明所有的下表都已经被探索完了。

2. 解决方法

  1. 保证容量为质数
  • 之前扩容或者缩容的时候,采用的都是直接变成原来的2倍或者原来二分之一的做法
  • 要保证容量是质数只需要使用已有的findPrime方法即可:
  • this.resize(this.limit * 2); 改成--> this.resize(this.findPrime(this.limit * 2));
  • this.resize(Math.floor(this.limit / 2)) 改成--> this.resize(this.findPrime(~~(this.limit / 2)))
  1. 探测步长使用公式:constant - ( key % constant) 
getStep(hashcode: number){  
  return (this.limit - 1) - ( hashcode % (this.limit - 1));  
}  
  1. 这个需要一点数学知识:
  • 再哈希方法是定步长方法,这意味着如果在探索过程中,在探索所有下标之前有前后两次的下标值相同,则哈希表的所有下标值永远不能探索完
  • 就比如说哈希表的容量是15,而探索步长为5,那么不论从哪里起步,三步只能必成环,为什么是三步呢,因为15 / 5 = 3;
  • 然而,质数不能分解成为除了1和自身之外其它因数的乘积;这就意味着对于容量是19的哈希表来说,除了探索步长为1或者19,取任何小于19的正整数都不会成环
  • 而constant - ( key % constant)的取值范围为0-constant,也就是说,只要constant取一个小于19大于1的数就可以了,所以constant = this.limit -1;
  • 于是将原来的while循环改成do while循环,然后出循环的依据改成(index % this.limit) !== hashcode

3. 实现代码

type IElement = {  
  key: string;  
  value: any;  
  deleted: boolean;  
}  
  
class _Hash {  
  storage: Array<IElement> = [];  
  count = 0;  
  limit = 7; // 容量取质数,但是对于链地址法要求不严格  
  
  // 用来产生哈希值的哈希函数  
  hashFunc (str: string, size: number) {  
      let hashcode = 0; // 哈希函数计算值  
      // 使用霍纳算法计算hashcode  
      for (let i = 0; i < str.length; i++) {  
          hashcode = hashcode * 37 + str.charCodeAt(i);  
      }  
      // 下标收紧至数组容量范围内  
      const index = hashcode % size;  
      return index;  
  }  
  
  getStep(hashcode: number){  
    return (this.limit - 1) - ( hashcode % (this.limit - 1));  
  }  
  
  // 向storage中增加新的元素  
  put (key: string, value: any) {  
      // 使用哈希函数计算出下标之后收紧  
      const hashcode = this.hashFunc(key, this.limit);  
      let index = hashcode;  
      const endPosition = index + this.limit;  
      // 循环找一圈  
      do {  
        // 使用计算得到的下标值找到storage此下标的数组  
        // 注意这里有一个圈  
        const computedIndex = index % this.limit;  
        let _tmp = this.storage[computedIndex];  
        // 插入新的元素  
        if(!_tmp){  
          this.storage[computedIndex] = {  
              key,   
              value,   
              deleted: false  
            };  
            // 插入新元素之后不要忘记更新长度  
            this.count++;  
            // 判断是否需要扩容  
            if(this.count > this.limit * 0.75) this.resize(this.findPrime(this.limit * 2));  
            break;  
        } else if(_tmp.key === key){  
          // 这种情况是修改原有的元素  
          this.storage[computedIndex].value = value;  
          // 如果是修改,则count的值不变  
          if(this.storage[computedIndex].deleted){  
            // 如果是修改已经被删除的,则哈希表的有效长度需要发生改变  
            this.storage[computedIndex].deleted = false;  
            // 因为count表示的是有效元素的数目,所以这里也要++  
            this.count++;  
          }  
          break;  
        } else {  
          index+=this.getStep(hashcode);  
        }  
      }while((index % this.limit) !== hashcode);  
  
      // 处理没有空位的情况: 先进行扩容,然后再插入此数据  
      if(index === endPosition){  
        this.resize(this.findPrime(this.limit * 2));  
        this.put(key, value);  
      }  
  }  
  
  // 通过key获取storage中的元素  
  get (key: string): null | IElement{  
      // 使用哈希函数计算出下标之后收紧  
      const hashcode = this.hashFunc(key, this.limit);  
      let index = hashcode;  
      const endPosition = index + this.limit;  
      // 循环找一圈  
      do {  
        // 使用计算得到的下标值找到storage此下标的数组  
        // 注意这里有一个圈  
        const computedIndex = index % this.limit;  
        let _tmp = this.storage[computedIndex];  
        // 如果_tmp是空的,则可以立即判断为未插入  
        if(!_tmp){  
            return null  
        } else if(_tmp.key === key){  
          // 如果key对上,不能直接返回,而是先判断是否被标记删除  
          if(_tmp.deleted) return null;  
          // 确认没有被删除的话就将其返回  
          return _tmp;  
        } else {  
          // 如果没有找到,那就下一个  
          index+=this.getStep(hashcode);  
        }  
      } while((index % this.limit) !== hashcode);  
      // 转一圈还没找到,那就返回null  
      return null;  
  }  
  
  // 通过key删除某个元素:不是真的删除,而是将其标记删除  
  remove (key: string): null | IElement {  
      // 使用哈希函数计算出下标之后收紧  
      const hashcode = this.hashFunc(key, this.limit);  
      let index = hashcode;  
      const endPosition = index + this.limit;  
      // 循环找一圈  
      do {  
        // 使用计算得到的下标值找到storage此下标的数组  
        // 注意这里有一个圈  
        const computedIndex = index % this.limit;  
        let _tmp = this.storage[computedIndex];  
        if(!_tmp){  
            // 这种情况表示不存在这个key对应的元素  
            return null; // 表示删除失败  
        } else if(_tmp.key === key){  
          // 这种情况要先判断是否已经删除,如果已经删除直接continue  
          if(_tmp.deleted) continue;  
          // 如果deleted依然是false则表示可以标记删除,返回删除的元素  
          this.storage[computedIndex].deleted = true;  
          this.count--;  
          // 开发地址方法和链地址方法计算负载因子的思路是一样的  
          // this.count--和标记删除或真正删除没有关系,因为this.count表示的是哈希表中的有效元素的个数  
          // 判断是否需要缩容(这里保证了一个最小长度)  
          if(this.limit > 7 && this.count < this.limit * 0.25) this.resize(this.findPrime(~~(this.limit / 2)));  
          return _tmp;  
        } else {  
          index+=this.getStep(hashcode);  
        }  
      } while((index % this.limit) !== hashcode);  
      // 如果遍历一圈还没有找到,那就直接返回null表示删除失败  
      return null;  
  }  
  
  // 判断哈希表是否为空  
  isEmpty (): boolean {  
      return this.count === 0;  
  }  
  
  // 返回哈希表中的数目  
  size (): number {  
      return this.count;  
  }  
  
  // 哈希表改变容量大小  
  resize (newLimit: number): void {  
      // 暂存原始数据(由于遍历的时候不需要用到count和limit所以只需要存thie.storage就可以了)  
      const oldStorage = this.storage;  
      // 格式化数据  
      this.storage = [];  
      this.count = 0;  
      this.limit = newLimit;  
      // 遍历原来哈希表中的每一个元素,逐个插入到新的哈希表中去  
      oldStorage.forEach(  
          _tmp => {  
              // 如果_tmp为真并且其中的元素的deleted值不为true则将这个元素插入到新的哈希表中去  
              if (_tmp) {  
                const {key, value, deleted} = _tmp;  
                if(!deleted){  
                  this.put(key, value);  
                }  
              }  
          }  
      )  
  }  
  
  // 判断是否为质数  
  isPrime (limit: number): boolean {  
      const _tmp = ~~Math.sqrt(limit);  
      for (let index = 2; index < _tmp; index++) {  
          if( limit % index === 0 ) return false;  
      }  
      return true;  
  }  
  
  // 找到附近的质数  
  findPrime (nearby: number): number {  
      let prime = nearby;  
      while(!this.isPrime(prime)){  
          prime++;  
      }  
      return prime;  
  }  
}  
  
  
const h = new _Hash();  
h.put('a', 132);  
h.put('a', 700);  
h.remove('a');  
console.log(h.size()); // 0  
console.log(h.isEmpty()); // true  
h.put('a', 700);  
h.put('b', 700);  
h.put('c', 700);  
h.put('d', 700);  
h.put('e', 700);  
h.put('f', 700);  
h.put('g', 700);  
h.put('h', 700);  
console.log(h.size()); // 8  
console.log(h.get('p')); // null  
console.log(h.get('a')); // {key: 'a', value: 700, deleted: false}  
h.remove('a');  
h.remove('b');  
h.remove('c');  
h.remove('d');  
h.remove('e');  
h.remove('f');  
h.remove('g');  
console.log(h.size()); // 1