散列搜索
结论:散列搜索的时间复杂度理论上为O(1)。
为什么会是O(1)?
为什么说是理论上为O(1)?
散列表是什么(What)
要解释上面两个问题,首先需要明白散列表是什么(What)。
散列表是元素集合,其中的元素以一种便于查找的方式存储。
散列表中的每个位置通常被称为槽,一个槽可以存储一个元素。
槽用一个从0开始的整数标志,例如0号槽、1号槽等等。初始情况下,散列表没有元素,每个槽都是空的。
本文以存储无符号整数的槽数为11的散列表为例,用Golang的切片来说明散列表的数据存储、插入和查找操作过程。 散列表初始化后如下图所示:
package main
import "fmt"
type HashTable struct {
ht []int
}
func (HT *HashTable) init(length int) {
// 创建槽数为11的散列表,限定存储无符号整数
HT.ht = make([]int, length)
for i := 0; i <= 10; i++ {
// 初始化,-1表示该槽尚未使用
HT.ht[i] = -1
}
}
func main() {
ht := HashTable{}
// 初始化为有11个槽的散列表
ht.init(11)
}
向散列表插值
散列函数将散列表中的元素与其所属位置对应起来。对槽数为m的散列表中的任一元素,散列函数返回一个介于0和m-1之间的整数。
假设有一个由整数元素14、16、21、24、33和30构成的集合。首先来看第一个最简单最常用的散列函数:“取余函数”,即用一个元素除以散列表的大小,并将得到的余数作为散列值(h(item) = item%11)。取余函数是一个很常见的函数,这是因为其取余结果肯定在槽编号范围内。根据取余散列函数计算出的元素和对应的散列值如下图所示:
计算出散列值后,就可以根据散列值将每个元素插入到相应的位置(即槽编号)。注意,在11个槽中,有6个被占用了。占用率被称作为载荷因子,记作λ,定义如下:λ=元素个数/散列表大小 本例是6/11
package main
import "fmt"
type HashTable struct {
ht []int
}
func (HT *HashTable) init(length int) {
// 创建槽数为11的散列表,限定存储无符号整数
HT.ht = make([]int, length)
for i := 0; i <= 10; i++ {
// 初始化,-1表示该槽尚未使用
HT.ht[i] = -1
}
}
func (HT *HashTable) set(item int) {
mod := mod(item, len(HT.ht))
HT.ht[mod] = item
}
func mod(item int, htLength int) int {
return item % htLength
}
func main() {
ht := HashTable{}
// 初始化为有11个槽的散列表
ht.init(11)
// 把10写入散列表
ht.set(10)
}
散列查询
搜索目标元素时,仅需使用散列函数计算出该元素的槽编号,并查看对应的槽中是否有值。因为计算散列值并找到对应位置所需的时间是固定的,所以搜索操作的时间复杂度是O(1)。如果一切正常,那么我们就已经找到常数阶的搜索算法。
package main
import "fmt"
type HashTable struct {
ht []int
}
func (HT *HashTable) init(length int) {
// 创建槽数为11的散列表,限定存储无符号整数
HT.ht = make([]int, length)
for i := 0; i <= 10; i++ {
// 初始化,-1表示该槽尚未使用
HT.ht[i] = -1
}
}
func (HT *HashTable) set(item int) {
mod := mod(item, len(HT.ht))
HT.ht[mod] = item
}
func (HT *HashTable) is_exist(item int) bool {
mod := mod(item, len(HT.ht))
if HT.ht[mod] == item {
return true
} else {
return false
}
}
func mod(item int, htLength int) int {
return item % htLength
}
func main() {
ht := HashTable{}
// 初始化为有11个槽的散列表
ht.init(11)
// 把10写入散列表
ht.set(10)
// 散列搜索10是否存在于散列表中
fmt.Println(ht.is_exist(10))
// true
// 把10写入散列表
ht.set(65)
// 散列搜索10是否存在于散列表中
fmt.Println(ht.is_exist(10))
// false
}
可能你已经看出来了,只有当每个元素的散列值不同,这个技巧才有用。如果集合中的下一个元素是21, 它的散列值是10(21%11==10),而65的散列值也是10,这就有问题了。散列函数会将两个元素放入同一个槽,这种情况被称为冲突,也叫碰撞。显然,冲突给散列函数带来了问题。解决冲突问题必然带来其他开销, 所以说理论上是O(1), 散列冲突解决办法点这里。