哈希表
一、认识哈希表
1.1 哈希表介绍
- 哈希表是基于数组进行实现的,相对于数组,它有很多的优势:
- 它可以提供非常快速的增删改查操作
- 无论多少数据,增删改查的效率都接近O(1),速度比树的查找还要快,并且相对于树的编码要简单许多
- 哈希表的劣势:
- 因为哈希表是无序的,所以不能以一种固定的方式(比如说从小到大)来遍历其中的元素
- 哈希表的 key值 是不能重复的
- 哈希表的实现方式其实就是通过哈希函数把唯一关键字转换成一个大数字(通过幂的连乘),为了避免数组越界,要通过哈希化把大数字压缩到数组下标范围中,然后把键和值再以数组的形式存入数组对应下标中
1.2 把唯一关键字转换成大数字
- 可以把每个字符转换成 ASCII 码,然后再通过幂的连乘把他们相加起来得到一个下标。
- 幂的连乘,选一个大于 10 的数字做 x,我们使用37,如 bag:98 * 27³ + 97 * 27² + 103
- 由于转换的数字过大我们需要压缩这个数字,这时候我们可以使用哈希化
1.3 认识哈希化
- 我们通过取余的操作把大数字压缩到数组下标范围中
- 下标值 = 大数字 % 数组大小(由于 JavaScript 在大数字用位操作符会发生问题,所以我们这里使用了取余操作)
- 当然这样做下标还是有可能会重复,但是重复的值很少,后面我们会讲出现这种情况怎么解决
下面我们来看看刚刚提到的几个概念
- 哈希化:将大数字压缩成数组下标范围内的值
- 哈希函数:把关键字的每一个字符转换成数字,返回哈希化压缩数字拿到数组下标
- 哈希表:最终将数据插入到的这个数组,我们就称为哈希表
二、地址冲突
2.1 什么是冲突?
- 当不同的单词通过哈希函数计算得出的下标相同时,我们称为冲突
- 我们需要针对这种冲突提出一些解决方案,即使冲突的可能性很小
2. 2 解决方案
2.2.1 链式地址法( JDK的 HashMAP 就是采用这种方式)
- 数组中的每一项都存放着一个数组或一个链表。(如果插入元素在头部的话,建议使用链表)
- 当下标相同的时候,就把元素放入数组或链表中。
- 当需要查找元素的时候,算出元素关键字的 hashCode,再线性查找即可
2.2.2 开放定址法
- 当下标相同的时候,向后寻找空白位置来放置冲突的元素
- 探测方式有三种
- 线性探测法
- 当前位置没有空位,把当前位置 + 1,继续查找,直达出现空位就放进去
- 存在的问题:
- 聚集问题,当同一片区域都没有空位的时候,称之为聚集
- 聚集会影响哈希表的性能,无论是增删改查都要探测多次
- 二次探测法
- 二次探测优化了线性探测的步长
- 如果说线性探测的步长是 1,比如从下标值x开始, 那么线性测试就是x+1, x+2, x+3依次探测
- 但是还是会出现步长不一的聚集
- 线性探测法
2.2.3 再哈希法
- 把关键字用另一个哈希函数再做一次哈希化,把这次哈希化的结果作为步长
- 对于指定的关键字,步长在探测中是不变的,不过不同关键字使用不同的步长
- 第二次哈希函数不能与第一次哈希函数相同,且不能输出为 0
- 比较好的步长哈希算法是:stepSize = constant - (key % constant)
- 其中constant是质数, 且小于数组的容量.
三、代码实现
哈希函数的实现
function hashFunc(str, max) {
// 1.初始化hashCode的值
var hashCode = 0
// 2.霍纳算法, 来计算hashCode的数值
for (var i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 3.取模运算
hashCode = hashCode % max
return hashCode
}
哈希表
function HashTable() {
this.storage = []
this.limit = 8 // 数组初始长度
this.count = 0 // 哈希表长度
HashTable.prototype.hashFunc = function (str, max) {
// 1.初始化hashCode的值
var hashCode = 0
// 2.霍纳算法, 来计算hashCode的数值
for (var i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 3.取模运算
hashCode = hashCode % max
return hashCode
}
}
添加 & 修改
HashTable.prototype.put = function (key, val) {
const hashCode = this.hashFunc(key)
const bucket = this.storage[hashCode]
if (buncket === undefine) {
bucket = []
this.storage[index] = bucket
}
// 是否修改了原来的值
let override = false
for (var i = 0; i < bucket.length; i++) {
let tuple = bucket[i]
if (tuple[0] === key) {
tuple[1] = value
override = true
}
}
if (!override) {
bucket.push([key, value])
this.count++
}
}
获取数据
HashTable.prototype.get = function (key) {
let index = this.hashFunc(key, this.limit)
const bucket = this.storage[index]
if (bucket === undefined)
return null
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === key)
return bucket[i][1]
}
return null
}
删除数据
HashTable.prototype.remove = function (key) {
let index = this.hashFunc(key, this.limit)
const bucket = this.storage[index]
if (bucket === undefined)
return null
for (let i = 0; i < bucket.length; i++) {
const tuple = bucket[i]
if (tuple[0] === key){
bucket.splice(i, 1)
this.count--
return tuple[1]
}
}
return null
}
其他方法
HashTable.prototype.isEmpty = function () {
return this.count === 0
}
HashTable.prototype.size = function () {
return this.count
}
哈希表的扩容
-
为什么要扩容?
- 当哈希表中的数据越来越多的时候,loadFactor 就会越来越大
- loadFactor(装填因子):当前哈希表的元素个数 / 哈希表的长度
- 当 loadFactor 越来越大的时候,每一个 index 对应的 bucket 内的元素就会越来越多,效率就会降低
- 所以我们需要在合适的情况下进行扩容,比如扩容两倍
-
什么情况下扩容?
- 一般会在 loadFactor > 0.75 的时候进行扩容,Java 的哈希表也是在这个情况下扩容
- 当 loadFactor < 0.25 的时候再进行缩小
哈希表扩容的实现
HashTable.prototype.resize = function(newLimit) {
const oldStorage = this.storage
this.limit = newLimit
this.count = 0
this.storage = []
oldStorage.forEach(bucket => {
if (bucket === undefine)
return
for (let i = 0; i < bucket.length; i++) {
const tuple = bucket[i]
this.put(tuple[0], tuple[1])
}
})
}
修改put方法
// 插入数据方法
HashTable.prototype.put = function (key, value) {
const index = this.hashFunc(key, this.limit)
const bucket = this.storage[index]
if (bucket === undefined) {
bucket = []
this.storage[index] = bucket
}
let override = false
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i]
if (tuple[0] === key) {
tuple[1] = value
override = true
}
}
if (!override) {
bucket.push([key, value])
this.count++
// 数组扩容
if (this.count > this.limit * 0.75) {
this.resize(this.limit * 2)
}
}
}
如果我们不断的删除数据呢?
- 如果不断的删除数据, 当loadFactor < 0.25的时候, 最好将数量限制在一半.
修改remove方法
// 删除数据
HashTable.prototype.remove = function (key) {
let index = this.hashFunc(key)
const bucket = this.storage[index]
if (bucket === undefined)
return false
for (let i = 0; i < bucket.length; i++) {
const tuple = bucket[i]
if (tuple[0] === key) {
this.count--
bucket.splice(i, 1)
if (this.limit > 8 && this.count < this.limit * 0.25) {
this.resize(Math.floor(this.limit / 2))
}
return tuple[1]
}
}
return false
}