哈希表
对哈希表有一些基本的了解之后,下面开始着手实现。首先,使用相对简单的链地址法实现此功能。
结构介绍
哈希表中应该具有下面的属性:
- storage: 是一个数组,其下标由哈希函数产生
- count: 表示哈希表中现有的元素个数
- limit: 表示的是哈希表的容量
哈希表中的方法有:
- hashFunc: 用来产生哈希值的函数
- put: 向storage中新增元素的方法
- get: 根据key从哈希表中取值的方法
- remove: 根据key从哈希表中删除某个元素
- isEmpty: 判断哈希表是否为空
- size: 获取哈希表中元素的个数
hash表中存储的数据结构
- key ---> value
- 存储的时候: key ---> index ---> insert value
- 取值的时候: key ---> index ---> get value
桶(bucket)
链地址方法中,使用哈希函数计算并得到的index首次索引哈希表得到的结果叫做桶,下标相同的element放在同一个桶中。
基本实现
type IElement = {
key: string;
value: any;
}
class _Hash {
storage: Array<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;
}
// 向storage中增加新的元素
put (key: string, value: any) {
// 使用哈希函数计算出下标之后收紧
const index = this.hashFunc(key, this.limit);
// 使用计算得到的下标值找到storage此下标的数组
let bucket = this.storage[index];
// 如果这个下标还未曾有元素插入过,则为undefined,这个时候先初始化一个数组出来,然后将此元素放进去
if(!bucket){
bucket = this.storage[index] = [];
}
// 对于哈希表来说修改元素和新插入一个元素没有区别的
const oldItemIndex = bucket.findIndex(
item => item.key === key
)
if(oldItemIndex !== -1){
bucket[oldItemIndex] = value;
} else {
bucket.push({
key,
value,
})
// 插入新元素之后不要忘记更新长度
this.count++;
}
}
// 通过key获取storage中的元素
get (key: string): null | IElement{
// 使用哈希函数计算下标然后收紧(对于相同的key,哈希函数计算出来的值总是一样的)
const index = this.hashFunc(key, this.limit);
// 前几布和插入/修改元素的时候是完全相同的
const bucket = this.storage[index];
// 如果bucket是undefined,说明还没有元素在这个位置上,这个时候直接返回一个null
if(!bucket) return null;
const target = bucket.find(
item => item.key === key
)
return target || null;
}
// 通过key删除某个元素
remove (key: string): null | IElement {
let target: null | IElement = null;
const index = this.hashFunc(key, this.limit);
const bucket = this.storage[index];
if(!bucket) return target;
const targetIndex = bucket.findIndex(
item => item.key === key
)
if(targetIndex !== -1){
target = bucket.splice(targetIndex, 1)[0];
// 删除元素之后一定要记得将长度减1
this.count--;
return target;
} else {
return target;
}
}
// 判断哈希表是否为空
isEmpty (): boolean {
return this.count === 0;
}
// 返回哈希表中的数目
size (): number {
return this.count;
}
}
哈希表扩容(基本)
loadFactor(负载因子)
- 显然,使用链地址方法实现的哈希表可以存储无限多的元素,因为每一个bucket中都可以放入无限多的元素;
- 但是问题在于,操作bucket中的元素使用的是遍历数组的方式,如果一个桶中放入了过多的元素,那么这种哈希表还不如数组,因为数组的缺点它都有,优点也被舍弃了;
- 必须使用一个标准来衡量一个桶中有过多元素的现象,使用的是loadFactor(负载因子),其定义为:
已经存储的元素的数量 / 哈希表中桶的个数
; - 最理想的情况就是一个桶中放一个元素,此时loadFactor = 1.
扩容时机
一般来说为了保证哈希表的性能,在loadFactor > 0.75
之后就需要进行扩容了。这种判断只会出现在新加元素之后,也就是put方法中:
// 插入新元素之后不要忘记更新长度
this.count++;
// 判断是否需要扩容
if(this.count > this.limit * 0.75) this.resize(this.limit * 2);
扩容方法
简单的将容量增加到原来的两倍就可以了,但复杂一点的是将容量增加到原来两倍附近的一个质数上
扩容之后
扩容是一件非常耗能的事情,扩容之后需要遍历哈希表中的每一个元素,然后逐个插入到新的哈希表中去。这是必要的!
扩容实现
// 哈希表改变容量大小
resize (newLimit: number): void {
// 暂存原始数据(由于遍历的时候不需要用到count和limit所以只需要存thie.storage就可以了)
const oldStorage = this.storage;
// 格式化数据
this.count = 0;
this.limit = newLimit;
// 遍历原来哈希表中的每一个元素,逐个插入到新的哈希表中去
oldStorage.forEach(
bucket => {
bucket?.forEach(
item => {
const { key, value } = item;
this.put(key, value);
}
)
}
)
}
哈希表缩容
有扩容就有缩容,缩容的时机为:在每一次删除元素之后,如果哈希表的元素数量小于桶的四分之一,就需要进行缩容以释放不用的空间。
this.count--;
// 判断是否需要缩容(这里保证了一个最小长度)
if(this.limit > 7 && this.count < this.limit * 0.25) this.resize(Math.floor(this.limit / 2));
扩(缩)容优化(使用质数扩容)
上面使用直接将容量扩展为原来两倍的做法扩容,更好的做法是:在两倍容量附近找一个最近的质数,以此质数作为容量
判断是否为质数
// 判断是否为质数
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;
}
最终代码
type IElement = {
key: string;
value: any;
}
class _Hash {
storage: Array<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;
}
// 向storage中增加新的元素
put (key: string, value: any) {
// 使用哈希函数计算出下标之后收紧
const index = this.hashFunc(key, this.limit);
// 使用计算得到的下标值找到storage此下标的数组
let bucket = this.storage[index];
// 如果这个下标还未曾有元素插入过,则为undefined,这个时候先初始化一个数组出来,然后将此元素放进去
if(!bucket){
bucket = this.storage[index] = [];
}
// 对于哈希表来说修改元素和新插入一个元素没有区别的
const oldItemIndex = bucket.findIndex(
item => item.key === key
)
if(oldItemIndex !== -1){
bucket[oldItemIndex] = {key, value};
} else {
bucket.push({
key,
value,
})
// 插入新元素之后不要忘记更新长度
this.count++;
// 判断是否需要扩容
if(this.count > this.limit * 0.75) this.resize(this.limit * 2)
}
}
// 通过key获取storage中的元素
get (key: string): null | IElement{
// 使用哈希函数计算下标然后收紧(对于相同的key,哈希函数计算出来的值总是一样的)
const index = this.hashFunc(key, this.limit);
// 前几布和插入/修改元素的时候是完全相同的
const bucket = this.storage[index];
// 如果bucket是undefined,说明还没有元素在这个位置上,这个时候直接返回一个null
if(!bucket) return null;
const target = bucket.find(
item => item.key === key
)
return target || null;
}
// 通过key删除某个元素
remove (key: string): null | IElement {
let target: null | IElement = null;
const index = this.hashFunc(key, this.limit);
const bucket = this.storage[index];
if(!bucket) return target;
const targetIndex = bucket.findIndex(
item => item.key === key
)
if(targetIndex !== -1){
target = bucket.splice(targetIndex, 1)[0];
// 删除元素之后一定要记得将长度减1
this.count--;
// 判断是否需要缩容(这里保证了一个最小长度)
if(this.limit > 7 && this.count < this.limit * 0.25) this.resize(Math.floor(this.limit / 2));
return target;
} else {
return target;
}
}
// 判断哈希表是否为空
isEmpty (): boolean {
return this.count === 0;
}
// 返回哈希表中的数目
size (): number {
return this.count;
}
// 哈希表改变容量大小
resize (newLimit: number): void {
// 暂存原始数据(由于遍历的时候不需要用到count和limit所以只需要存thie.storage就可以了)
const oldStorage = this.storage;
// 格式化数据
this.count = 0;
this.limit = newLimit;
// 遍历原来哈希表中的每一个元素,逐个插入到新的哈希表中去
oldStorage.forEach(
bucket => {
bucket?.forEach(
item => {
const { key, value } = item;
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