哈希表查找的本质

      周五,来聊哈希表。

      最近,在实际工作中,遇到如下问题:

      有两个数组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

      可以看到,当数据量很小时,线性查找和哈希查找都很快,但线性查找更快,这是因为,哈希表建立和哈希计算需要时间。

      但是,当数据量很大时,哈希表建立和哈希计算的时间就微不足道了,哈希表的查找速度快得惊人,以空间换取了时间。

      以后如果遇到查找,不要总是线性查找、二分查找,想想哈希表查找吧。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值