哈希表
本文使用开放地址方法和再哈希法实现哈希表,不同于线性探测方法,再哈希方法需要根据key来确定探索的步长,并使用质数容量确保每一个下标都可以被探测。
1. 再哈希方法需要解决的三个额外问题
- 如何保证哈希表的容量总是质数;
- 如何通过key产生探索步长;
- 如何证明所有的下表都已经被探索完了。
2. 解决方法
- 保证容量为质数
- 之前扩容或者缩容的时候,采用的都是直接变成原来的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)))
- 探测步长使用公式:
constant - ( key % constant)
getStep(hashcode: number){
return (this.limit - 1) - ( hashcode % (this.limit - 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