周五,来聊哈希表。
最近,在实际工作中,遇到如下问题:
有两个数组totalId和whitelistId, 求totalId与whitelistId之差:只包含totalId中的元素,但不包含whitelistId中的元素。
直观程序如下:
func sub() {
for _, v1 := range totalId {
match := false
for _, v2 := range whitelistId{
if v1 == v2{
match = true
break
}
}
if !match {
// do logic
}
}
}
当数据量很大时,该程序有性能问题。原因在于:内层循环所用的查找是线性查找。
某一天,黄药师、欧阳锋、周伯通、郭靖、洪七公、一灯大师入住酒店,每人一个房间。
江南七怪柯镇恶,要去酒店找郭靖,怎么找呢?线性查找的思路是:逐个房间敲门询问,直到找到郭靖为止,如下:
对于初入门编程的人来说,这是最直接的思路,而且好像不容易找到其它思路。
然而,对于稍有点生活常识的人来说,线性查找的思路挺傻的,何不直接找出郭靖所在的房间号,然后直接去这个房间找郭靖呢?如下:
这就是哈希表查找的本质:根据要查找的目标,获得目标所在的位置(通过哈希计算获取),然后直接去这个位置中找。
可以在创建哈希表时,建立目标和地址的映射关系。也就是说,在他们入住酒店时,酒店做好登记:
hash[黄药师]=101
hash[欧阳锋]=102
hash[周伯通]=103
hash[郭靖]=104
hash[洪七公]=105
hash[一灯大师]=106
为了便于理解,我们看如下简单问题:
判断一个数是否在数组[10, 31, 12, 43, 24, 55, 56, 97, 48, 39]中?利用哈希表来做查找:
package main
import "fmt"
func hash(n int) int {
return n % 10
}
func isInSlice(n int, a []int) bool {
if a[hash(n)] == n { // hash search
return true
}
return false
}
func main() {
// hash table
a := []int{10, 31, 12, 43, 24, 55, 56, 97, 48, 39}
for i := 0; i < 100; i++ {
if isInSlice(i, a) {
fmt.Println(i)
}
}
}
结果:
10
12
24
31
39
43
48
55
56
97
这是最简单的哈希表查找。实际上,在哈希计算时,可能会出现哈希冲突,此时可考虑用连地址法解决。
在C++ STL中,map底层数据结构是红黑树, 查找的时间复杂度是O(logN),然而,在golang中,map底层数据结构是哈希表,查找的时间复杂度是O(1),我们以golang map为例,来处理前面提到的totalId减whitelistId的问题:
package main
import (
"fmt"
"math"
"math/rand"
"time"
"flag"
"strconv"
)
var totalId []int
var whitelistId []int
var N int = 1
func init() {
var s string
flag.StringVar(&s, "n", "1", "")
flag.Parse()
N, _ := strconv.ParseInt(s, 10, 64)
for i := 0; i < int(N); i++ {
tmp := rand.Intn(math.MaxInt32)
totalId = append(totalId, tmp)
}
for i := 0; i < int(N); i++ {
tmp := rand.Intn(math.MaxInt32)
whitelistId = append(whitelistId, tmp)
}
}
func test1() {
now:=time.Now()
for _, v1 := range totalId {
match := false
for _, v2 := range whitelistId{
if v1 == v2{
match = true
break
}
}
if !match {
// do logic
}
}
gap := time.Since(now)
fmt.Println(gap)
}
func test2() {
now:=time.Now()
m := make(map[int]int)
for _, v2 := range whitelistId{
m[v2] = 0
}
for _, v1 := range totalId {
if _, ok := m[v1]; ok {
continue
}
// do logic
}
gap := time.Since(now)
fmt.Println(gap)
}
func main() {
test1()
test2()
}
结果:
xxx$ go run a.go -n 1
223ns
2.407µs
xxx$ go run a.go -n 10
367ns
10.023µs
xxx$ go run a.go -n 100
7.954µs
46.514µs
xxx$ go run a.go -n 1000
698.669µs
196.518µs
xxx$ go run a.go -n 10000
59.258447ms
1.311882ms
xxx$ go run a.go -n 100000
5.725003064s
12.472086ms
xxx$ go run a.go -n 1000000
10m52.837831774s
285.868052ms
可以看到,当数据量很小时,线性查找和哈希查找都很快,但线性查找更快,这是因为,哈希表建立和哈希计算需要时间。
但是,当数据量很大时,哈希表建立和哈希计算的时间就微不足道了,哈希表的查找速度快得惊人,以空间换取了时间。
以后如果遇到查找,不要总是线性查找、二分查找,想想哈希表查找吧。