一:什么是哈希表?
哈希表是一种常见的数据结构,它通过将键映射到值的方式来存储和组织数据。具体来说,哈希表使用哈希函数将每个键映射到一个索引(在数组中的位置),然后将该键值对存储在该索引处。当需要检索某个键的值时,哈希表会再次使用哈希函数将该键转换为相应的索引,并在该索引处查找其关联的值。由于哈希函数通常能够较快地计算出索引,因此哈希表在插入和查找操作上具有很高的效率。
在 JavaScript 中,哈希表通常是通过对象来实现的。对象的属性名就是键,而属性值则是与之关联的值。然而,在某些情况下,如果需要更多的控制权或者需要处理大量的数据,可以使用第三方库或自己实现哈希表数据结构。
二:哈希化
哈希化是将任意长度的消息(例如一个字符串、文件或数据记录)转换为固定长度的值(通常是较小的整数),该值称为哈希值。哈希函数执行哈希化操作,它将输入键映射到唯一的索引值,这个索引值可以用来在哈希表中查找对应的值。
哈希化具有以下特点:
- 哈希化是单向的:从哈希值无法推断出原始输入值,而且很难找到两个不同的输入值生成相同的哈希值。
- 哈希化是确定性的:对于给定的输入,哈希函数总是返回相同的哈希值。这个特性使得哈希表能够高效地进行查找和插入等操作。
- 哈希化是均匀的:好的哈希函数应该将输入数据均匀地分布在哈希空间中,以避免碰撞(即两个不同的输入值生成相同的哈希值)。
总而言之
哈希化
:将大数字转化成数组范围内下标的过程,我们就称之为哈希化
哈希函数
:通常我们会将单词转成大数字,大数字在进行哈希化的代码实现放在一个函数中,这个函数我们称为哈希函数
哈希表
:最终将数据插入到的这个数组,对整个结构的封装,我们就称之为是一个哈希表
三:哈希函数
好的哈希函数应该具备:
快速的计算
:哈希表的优势就在于效率,所以快速获取到对应的hashCode非常重要。我们需要通过快速的计算来获取到元素对应的hashCode。
均匀的分布
:哈希表中,无论是链地址法还是开放地址法,当多个元素映射到同一个位置的时候,都会影响效率,所以,优秀的哈希函数应该尽可能将元素映射到不同的位置,让元素在哈希表中均匀的分布。
哈希函数设计
- 将字符串转换成比较大的数字:hashCode
- 将大的数字hashCode压缩到数组范围之内
- 传入两个参数:str 字符串;size 数组大小
function hashFunc(str, size) { //参数1 字符串 参数2 数组大小
//1.定义hashcode
let hashCode = 0;
//2.霍纳法则,来计算hashCode的值
// 将str转成Unicode编码
for (let i = 0; i <str.length ; i++) {
// 哈希表的长度使用质数 43 47 47 31 等等都可,目前最多使用的还是37
hashCode = 37*hashCode + str.charCodeAt(i)
}
//3.取余操作
let index = hashCode % size
return index
}
四:实现哈希表(链地址法)
1.逻辑
实现的哈希表(基于storage的数组)每个index对应的是一个是数组(bucket)。
bucket中存放什么呢?我们最好将key和value都放进去,我们继续使用一个数组
最终我们的哈希表的数据格式是这样:[[[k,v],[k,v],[k,v]],[[k,v],[k,v]],[[k,v]]]
2.扩容和扩容时机
将所有的数据项放在长度为7的数组中的,因为我们使用的是链地址法,loadFactor可以大于1,所以这个哈希表可以无限制的插入新数据。
但是随着数据量的增多,每一个index对应的bucket会越来越长,也就造成效率的降低。所以,需要在合适的情况对数组进行扩容。比如扩容两倍。
扩容时机
比较常见的情况是填装因子 > 0.75 的时候扩容
3.属性和方法
定义属性:
storage: 作为我们的数组,数组中存放相关的元素;
count:表示当前数组中已经存放了多少数据,用于计算填装因子的值,以便扩容等操作;
limit:用于标记数组的长度;
定义方法:
- put(key,value):新增和修改方法。哈希表的插入和修改是同一个函数,因为,当使用者传入一个<key,value>时,如果原来不存在该key,那么就是插入操作;如果已经存在该key,那么就是修改操作
- 根据传入的key获取对应的hashCode,也就是数组的index;
从哈希表的index位置中取出bucket(一个数组);
查看上一步取出的数组是否为null ,如果为null,则表示之前在该位置没有放置过任何的内容,那么就新建一个数组[];
查看是否之前已经放置过key对应的value:如果放置过,那么就是依次替换操作,而不是插入新数据;
如果不是修改操作,那么插入新的数据 :在数组中push新的[key,value]即可。注意: 这里需要将count+1,因为数据增加了一项。最后判断是否需要扩容: loadFactor > 0.75 的时候扩容
是则加一再判断,直到是质数,则跳出返回这个质数
- get(key):获取数据方法
- 根据传入的key获取对应的hashCode,也就是数组的index;
- 根据index拿到对应的bucket;
- 判断bucket是否为null,如果是null则返回null;
- 如果不是null,则遍历bucket,判断里面是否包含key对应的数据,有则返回;
- 如果bucket中没有找到,那么返回null;
- remove(key):删除方法
- 根据传入的key获取对应的hashCode,也就是数组的index;
- 根据index拿到对应的bucket;
- 判断bucket是否存在,如果不存在则返回null;
- 如果存在则遍历bucket,找到里面包含key对应的数据,有则删除,并且数量减一,同时需要判断是否需要缩容,返回被删除的数据;
- 如果不存在则返回null
- isEmpty():判断是否为空
- 返回 count是否等于0
- size():获取哈希表大小
- 返回count值
- resize(newLimit):扩容和缩容方法
- 先存储原数组
- 原属性重置
- 遍历原数组,把里面每个索引中的每个bucket中的每一个值都取出来重新添加进扩容后的数组中
- isPrime(num):判断是否为质数
- 将传入的num开平方根,然后遍历2和开平方根之后的值之间的数值,如果传入的num值可以和这中间一个值除尽,那么就跳出返回false不是质数。遍历结束返回true,是质数。
- getPrime(num):获取质数size
- 循环遍历判断传入的值是否为质数,如果不
4.代码
//哈希表
function HashTable ()
{
//属性
//哈希表的基准数组
this.storage = []
//哈希表中的元素总数,[key,val]
this.count = 0
//基准数组的大小
this.limit = 7
//哈希函数: 将str字符串转换成大的数字hashcode,再将大的数字压缩到数组大小(limit)范围以内index
HashTable.prototype.hashFunc = function (str, size)
{
//1.定义hashcode
let hashCode = 0
//2.霍纳法则来计算hashcode
//将str转换成Unicode编码
for (let i = 0; i < str.length; i++) {
//哈希表的长度使用质数43 47 等等,
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//3.取余操作
let index = hashCode % size
return index
}
//put方法,添加&修改:如果storage中原先不存在key即插入添加操作,如果存在即修改操作
HashTable.prototype.put = function (key, value)
{
//1.根据key调用哈希函数来计算index值
let index = this.hashFunc(key, this.limit)
//2.根据索引值index获取在storage中对应的bucket数组
let bucket = this.storage[index]
//3.判断bucket是否为null,如果是null,那就创建[]
if (bucket === null || bucket === undefined) {
bucket = []
//并且storage[index]创建[]
this.storage[index] = bucket
}
//4.bucket不为空,遍历bucket,找到对应key,如果存在key,那就修改,不存在即添加插入
//拉链法中的元素存储结构为,[[[k,v],[k2,v2],[k3,v3]]],即bucket数组,[[k,v],[k2,v2],[k3,v3]]
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i]
if (tuple[0] === key) {
tuple[1] = value
return
}
}
//5.如果没有跳出,那就说明没有找到对应的key,那就添加
bucket.push([key, value])
//6.计数器this.count++
this.count++
//7.判断是否需要扩容 loadFactor(填装因子) > 0.75时候扩容
if (this.count > this.limit * 0.75) {
//扩容后新的数组大小为原来的两倍
const newSize = this.limit * 2
//getPrime获取质数方法,目的为了使哈希表的长度为质数
const primeSize = this.getPrime(newSize)
//调用resize扩容或缩容的方法
this.resize(primeSize)
}
}
//get方法:通过key来获取哈希table中的value值
HashTable.prototype.get = function (key)
{
//1.通过key来调用哈希函数获取index
let index = this.hashFunc(key, this.limit)
//2.通过index来获取到对应的bucket
let bucket = this.storage[index]
//3.判断bucket是否为空
if (bucket === null || bucket === undefined) {
return null
}
//4.如果存在遍历获取
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i]
if (tuple[0] === key) {
return tuple[1]
}
}
//5.如果在bucket中没有找到,那么返回null
return null
}
//remove方法:删除方法
HashTable.prototype.remove = function (key)
{
//1.通过Key来调用哈希函数获取index
let index = this.hashFunc(key, this.limit)
//2.通过Index获取对应的bucket
let bucket = this.storage[index]
//3.判断bucket是否为空
if (bucket === null || bucket === undefined) {
return null
}
//4.如果存在就遍历查抄,删除
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i]
if (tuple[0] === key) {
bucket.splice(i, 1)
this.count--
//判断是否需要缩容
if (this.count < this.limit * 0.25) {
const newSize = Math.floor(this.limit / 2)
const primeSize = this.getPrime(newSize)
this.resize(primeSize)
}
return tuple[1]
}
}
//5.如果bucket中不存在返回null
return null
}
//isEmpty 判断是否为空方法
HashTable.prototype.isEmpyt = function ()
{
return this.count === 0
}
//size方法 获取size的方法
HashTable.prototype.size = function ()
{
return this.count
}
//resize方法 扩容或者缩容的方法
HashTable.prototype.resize = function (newLimit)
{
//存储原数组
const oldStorage = this.storage
//原属性重置
this.storage = []
this.limit = newLimit
this.count = 0
//遍历原数组,把每个索引的bucket中的每个值都取出来添加到扩容或者缩容的数组中
for (let i = 0; i < oldStorage.length; i++) {
//获取到bucket
const bucket = oldStorage[i]
//判断bucket是否为空
if (bucket === null || bucket === undefined) {
continue
}
//遍历每个bucket的每个值
for (let j = 0; j < bucket.length; j++) {
let tuple = bucket[j]
this.put(tuple[0], tuple[1])
}
}
}
//isPrime方法 判断是否是质数
HashTable.prototype.isPrime = function (num)
{
const temp = parseInt(Math.sqrt(num))
for (let i = 2; i < temp; i++) {
if (temp % 2 === 0) {
return false
}
}
return true
}
//getPrime 获取到质数size
HashTable.prototype.getPrime = function (num)
{
while (!this.isPrime(num)) {
num++
}
return num
}
}
const hashTable = new HashTable()
hashTable.put('abc', 18)
hashTable.put('aaa', 20)
hashTable.put('vvv', 1111)
console.log(hashTable);
console.log(hashTable.get('vvv'));
hashTable.remove('aaa')
console.log(hashTable);