哈希表原理
哈希表是一种能够快速索引到数据的数据结构,与此类似的是数组,和数组不同的是数组是通过数字下标来索引到数据,而哈希表可以通过任意数据结构来定位。
哈希表的原理如上图所示,本质上还是需要数组,但是我们可以通过设计哈希函数来将任意数据结构来映射成数组下标。
在学习哈希表时,我们的重点工作是设计一个合适的哈希函数。设计哈希函数有很多方法,并没有一个标准答案,不同场景有不同的最佳实践方式,但是还是有很多范式可以参考的,本文先不讨论这方面。(其实是我现在也没会多少 o( ̄︶ ̄)o)
当然这种设计方式会出现一个问题,就是“哈希冲突”,所谓“哈希冲突”就是哈希函数将超过一个元素映射到同一个位置上,而对待哈希冲突的态度应该是怎么解决而不是避免。
解决哈希冲突方法主流的有以下几种:
- 开放定址法(线性探测)
- 再哈希法
当出现哈希冲突的时候那就再换个哈希函数再哈希一遍就好了,使用这种方法的时候可以多设置几个哈希函数。
但是还是要考虑最坏情况,就是经过所有的哈希方法之后还是会有冲突,所以还是要用其他哈希方法做兜底。 - 建立公共溢出区
公共溢出区可以用其他数据结构来维护 - 链式地址法(拉链法)
这个方法是最常用的,原理是在哈希表的基础上,数组存储的是链表
这里提供一种使用拉链法处理冲突的哈希表设计范式,有很多地方可以根据具体场景进行优化
class Node<T> {
val: T
next: Node<T> | null
constructor(val: T) {
this.val = val
this.next = null
}
//* 在当前节点后面插入一个节点
insert(node: Node<T>) {
node.next = this.next
this.next = node
}
}
class HashTable<T> {
cnt: number
data: Array<Node<T> | null>
constructor(n: number) {
this.data = (new Array(n) as any).fill(null)
this.cnt = 0
}
insert(s: T) {
let ind = this.hash_func(s) % this.data.length
const node = new Node(s)
this.cnt++
if (!this.data[ind]) {
this.data[ind] = node
return
}
let p = this.data[ind] as Node<T>
while (p.next && p.next.val !== s) p = p.next
if (p.next === null) {
// 走到最后还没找到
p.insert(node)
if (this.cnt > this.data.length * 3) this.expand()
}
}
find (s: T) {
const ind = this.hash_func(s) % this.data.length
let p = this.data[ind]
while (p && p.val !== s) p = p.next
return p !== null
}
private expand() {
//* 开辟新的哈希表
const n = this.data.length * 2
const h = new HashTable<T>(n)
//* 数据迁移
let p: Node<T> | null
this.data.forEach(d => {
p = d
while (p) {
h.insert(p.val)
p = p.next
}
})
this.data = h.data
}
//* 计算哈希值
private hash_func(s: T): number {
let str = JSON.stringify(s), hash: number = 0, seed = 131
for (let i = 0; i < str.length; i++) {
hash = hash * seed + str[i].charCodeAt(0)
}
return hash & 0x7fffffff
}
}
//* 测试
const h = new HashTable<string>(5) // string -> number
h.insert('aaa')
h.insert('bbb')
h.insert('ccc')
h.insert('ddd')
h.insert('eee')
h.insert('fff')
h.insert('ggg')
console.log(h.data)
console.log(h.find('ggg'))
</