上午写完了哈希表的理论知识,现在我们来手动实现一下哈希表。为了方便,我这里采用的是基于数组来实现哈希表的bucket子项,其实使用链表和数组所产生的效率问题在这里可以忽略不计。下面是实现过程:
class HashTable {
// 属性
storage = [] // 存储数据
count = 0 // 记录hash表中的元素个数
limit = 7 // 记录hash表的大小
// 初始化
constructor(limit) {
this.limit = limit
}
// 方法
// hash函数
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为空,则直接插入
if (bucket == null) {
bucket = []
this.storage[index] = bucket
}
// 判断是否是修改数据
for (let i in bucket) {
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) {
// 根据key获取对应的index
let index = this.hashFunc(key, this.limit)
// 根据index取出对应的bucket
let bucket = this.storage[index]
// 如果bucket为空,则返回null
if (bucket == null) {
return null
}
// 遍历bucket
for (let i in bucket) {
let tuple = bucket[i]
if (tuple[0] === key) {
return tuple[1]
}
}
// 没有找到 return null
return null
}
// 删除数据
remove(key) {
// 根据key获取对应的index
let index = this.hashFunc(key, this.limit)
// 根据index取出对应的bucket
let bucket = this.storage[index]
// 如果bucket为空,则返回null
if (bucket == null) {
return null
}
// 遍历bucket
for (let i in bucket) {
let tuple = bucket[i]
if (tuple[0] === key) {
bucket.splice(i, 1)
this.count--
// 缩小容量
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)
}
return tuple[1]
}
}
return null
}
// 判断哈希表是否为空
isEmpty() {
return this.count === 0
}
// 获取哈希表中的元素个数
size() {
return this.count
}
// 清空哈希表
clear() {
this.storage = []
this.count = 0
}
// 哈希表扩容
resize(newLimit) {
let oldStorage = this.storage
this.limit = newLimit
this.storage = []
this.count = 0
for (let i in oldStorage) {
let bucket = oldStorage[i]
if (bucket == null) continue
for (let j in bucket) {
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
}
}
在理清楚哈希表的原理之后,写一个简单的哈希表还是很简单的。如上述代码,我们给hashTable的容量limit是7,如果插入的数据占满了容量怎么办呢?也许你会说那么就存bucket中不就行了么,反正可以无限延伸长度。但是如果bucket中的数据过多,那么一定会造成读取数据的效率降低,所以这时候就需要对哈希表进行扩容操作。
哈希表的扩容思想
为什么需要扩容
上述已经说了如果不进行扩容,那么当数据也来越多的时候,读取数据的效率会低。
如何扩容
扩容可以简单的将容量扩大两倍(之前不是说哈希表的长度最好为质数吗?这个问题我们之后再讨论)
但是这种情况下,所有数据项一定要同时进行修改,也就是要重新调用哈希函数,获取不同的index位置
例如hashCode = 12的数据项,当length = 8时,index = 4,当length = 16时,index = 12。
虽然这是一个耗时的过程,但是如果数组需要扩容,这一过程必不可少
什么情况下需要扩容
一般来说,当loadFactor(装填因子,上一篇有详细介绍) > 0.75时进行扩容。而当loadFactor < 0.25时进行缩容,具体的扩容缩容方法上述有实现。接下来我们来解决开头提到的容量质数问题。
容量质数
虽然说在链地址法中将容量设置为质数,没有在开放地址法中的重要,但是为了提高效率,我们还是来实现一下。
首先来实现一个算法,用于判断一个数是否为质数:
function isPrime(num) {
for(let i = 2; i < num; i++){
if(num % i == 0){
return false
}
}
return true
}
但是这个算法的效率并不高,因为对于每个数n,其实并不要判断从2到n-1。只需要判断它是否能整除它的平方根之前的整数就可以了:
function isPrimePlus(num) {
let temp = parseInt(Math.sqrt(num))
for (let i = 2; i <= temp; i++) {
if (num % i === 0) {
return false
}
}
return true
}
回归主题,我们如何将容量扩充为质数,算法很简单:
function getPrime(num) {
while (!this.#isPrime(num)) {
num++
}
return num
}
简单来说就是只要这个数不是质数,那么就加一,直到加这个数为质数为止。在哈希表中主要是在数组容量改变时用到扩充为质数,详见上述哈希表的实现中插入和删除操作。
至此,哈希表的基本封装就完成了。