引言
哈希表是一种组合的数据结构,它通常的实现方式是数组加链表,或者数组加红黑树。哈希表是一种牺牲空间去换取时间的数据结构,需要在空间与时间上有取舍,哈希表是时间和空间之间的平衡。哈希表的核心是哈希函数,哈希表最关键的问题哈希冲突也是取决于哈希函数的设计。
哈希函数
1、什么是哈希函数
哈希函数是一种将“键”转换为“索引”的逻辑规则。它的设计好坏对哈希表的性能影响巨大。优良的哈希函数能够最大程度的减少哈希冲突,使得哈希表中的元素分布得尽可能的均匀,离散程度更大,这样哈希表就会性能优越;较差的哈希函数设计,带来的可能是一场灾难,哈希冲突严重,空间利用率低,时间复杂度呈线性恶化,造成频繁的扩容操作。
2、哈希函数的设计
对于哈希函数的设计,以下举了一些简单的例子,都基本类型转换成整型处理,并不是唯一的方法,仅供参考。以下简单列了几条设计原则。
- 一致性:如果a == b,则hash(a) == hash(b)
- 高效性:哈希函数计算高效简便
- 均匀行:哈希值均匀分布
1、整型
-
小范围正整数可直接使用
-
小范围负整数进行偏移
-
大整数可以与一个合适的素数取模
说明:上图(左)与一个不合适的合数取模,获得的索引冲突严重,不可取;而上图(右)与一个质数取模,明显获得的索引分布更均匀,离散程度更好。
2、字符串型
-
字符串型转换成整型处理
说明:其中B为一个常数,M为一个合适的质数。上图将一个字符串型加入一些规则映射成了一个整型。
3、代码实现简单hash函数
// 1.将字符串转成比较大的数字:hashCode
// 2.将大的数字hashCode压缩到数组分为(大小)之内
function hashFunc(str, size) {
// 1.定义hashCoed变量
let hashCode = 0
// 2.霍纳算法,来计算hashCode的值
for (var i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 3.取余操作
var index = hashCode % size
return index
}
// 测试哈希函数
console.log(hashFunc("abc", 7)) // 4
console.log(hashFunc("abcd", 7)) // 3
console.log(hashFunc("bcdf", 7)) // 5
console.log(hashFunc("qwer", 7)) // 2
哈希冲突的处理
1、链地址法(Separate Chaining)
说明:哈希冲突的元素可以用链表这种线性的数据结构保存,当然也可以用平衡树结构保存,Java8中的哈希表实现,当哈希冲突达到一定的程度,会将链表替换成红黑树,前提是哈希表中原本的元素具备可比较性。
2、开放地址法
-
线性探测
说明:当遭遇哈希冲突时,会按照规则顺延往下找空挡位置插入元素,这种方式如果哈希冲突比较严重,会造成寻找空挡位置效率变低。哈希表性能变差。这种方式就是要设置合适的哈希表容量。
-
平方探测
说明:这种方式相对于线性探测,加大了寻找空挡位置的步长。
-
二次哈希
哈希表动态空间处理
说明:当哈希冲突达到一个所能容忍的上界位置时(upperTol),对哈希表进行扩容操作,以减少哈希冲突;当哈希冲突降低到一个下界位置(lowerTol)时,对哈希表进行缩容操作,以节约空间。
哈希表实现
基于数组和链地址法实现
1、初始化哈希表
function HashTable() {
this.storage = [ ] // 数组中存放相关的元素
this.count = 0 // 表示当前已经存放了多少数据
this.limit = 7 // 用于标记数组中一共可以存放多少个元素
// 哈希函数(用来计算数据的下标值index)
hashTable.prototype.hashFunc = function (str, size) {
// 1.定义hashCoed变量
var hashCode = 0
// 2.霍纳算法,来计算hashCode的值
for (var i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 3.取余操作
var index = hashCode % size
return index
}
}
2、哈希表插入修改数据
// 插入修改操作
HashTable.prototype.put = function (key, value) {
// 1.根据key获取对应的index
var index = this.hashFunc(key, this.limit) // 这里直接使用之前封装的哈希函数
// 2.根据index获取对应的bucket
var bucket = this.storage[index]
// 3.判断该bucket是否为null
if (bucket == null) {
bucket = []
this.storage[index] = bucket
}
// 4.判断bucket里面是否有key的元组
// 有则是修改value值(因为用的是链地址法)
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (key == tuple[0]) {
tuple[1] = value
return
}
}
// 没有则直接在bucket里插入元组
bucket.push([key, value])
this.count += 1
}
3、哈希表获取数据
// 获取存放的数据
HashTable.prototype.get = function (key) {
// 1.获取key对应的index
var index = this.hashFunc(key, this.limit)
// 2.通过index获取到bucket
var bucket = this.storage[index]
// 3.判断bucket是否为null,如果为null则没有数据
if (bucket == null) {
return null
}
// 4.如果bucket存在,判断元组里是否有对应的key,有则返回数据
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] == key) {
return tuple[1]
}
}
// 5.没有找到,return null
return null
}
4、哈希表删除数据
// 删除数据
HashTable.prototype.remove = function (key) {
// 1.获取key对应的index
var index = this.hashFunc(key, this.limit)
// 2.通过index获取到bucket
var bucket = this.storage[index]
// 3.判断bucket是否为null,如果为null则返回false,表示没有要删除的数据
if (bucket == null) {
return false
}
// 4.如果bucket存在,判断元组里是否有对应的key,有则删除数据
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] == key) {
bucket.splice(i, 1)
this.count--
}
return tuple[1]
}
// 5.没有找到, return fasle
return false
}
测试哈希表
// 测试哈希表
var hashTable = new HashTable()
// 向哈希表插入3条数据
hashTable.put('abc', '我是abc')
hashTable.put('qwer', '我是qwer')
hashTable.put('hash', '我是hash')
// 获取key为'qwer'的数据
console.log(hashTable.get('qwer')) // "我是qwer"
console.log(hashTable) // 通过Chrome浏览器查看哈希表结构
// 删除 key为'abc'的数据并查看
console.log(hashTable.remove('abc')) // "我是abc"
最后附上完整代码:
function HashTable() {
this.storage = [] // 数组中存放相关的元素
this.count = 0 // 表示当前已经存放了多少数据
this.limit = 7 // 用于标记数组中一共可以存放多少个元素
// 哈希函数
HashTable.prototype.hashFunc = function (str, size) {
// 1.定义hashCoed变量
var hashCode = 0
// 2.霍纳算法,来计算hashCode的值
for (var i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 3.取余操作
var index = hashCode % size
return index
}
// 插入修改操作
HashTable.prototype.put = function (key, value) {
// 1.根据key获取对应的index
var index = this.hashFunc(key, this.limit) // 这里直接使用之前封装的哈希函数
// 2.根据index获取对应的bucket
var bucket = this.storage[index]
// 3.判断该bucket是否为null
if (bucket == null) {
bucket = []
this.storage[index] = bucket
}
// 4.判断bucket里面是否有key的元组
// 有则是修改value值(因为用的是链地址法)
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (key == tuple[0]) {
tuple[1] = value
return
}
}
// 没有则直接在bucket里插入元组
bucket.push([key, value])
this.count += 1
}
// 获取存放的数据
HashTable.prototype.get = function (key) {
// 1.获取key对应的index
var index = this.hashFunc(key, this.limit)
// 2.通过index获取到bucket
var bucket = this.storage[index]
// 3.判断bucket是否为null,如果为null则没有数据
if (bucket == null) {
return null
}
// 4.如果bucket存在,判断元组里是否有对应的key,有则返回数据
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] == key) {
return tuple[1]
}
}
// 5.没有找到,return null
return null
}
// 删除数据
HashTable.prototype.remove = function (key) {
// 1.获取key对应的index
var index = this.hashFunc(key, this.limit)
// 2.通过index获取到bucket
var bucket = this.storage[index]
// 3.判断bucket是否为null,如果为null则返回false,表示没有要删除的数据
if (bucket == null) {
return false
}
// 4.如果bucket存在,判断元组里是否有对应的key,有则删除数据
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] == key) {
bucket.splice(i, 1)
this.count--
}
return tuple[1]
}
// 5.没有找到, return fasle
return false
}
}
时间复杂度分析
哈希表的平均时间复杂度是O(1)级别的,这是哈希表用空间换来的性能,这是哈希表最大的优势。哈希表扩缩容时这种费时操作也是不常发生,平摊下去依然趋近于O(1)级别。