基础算法整理(六)

转载请注明出处:https://hts0000.github.io/

欢迎与我联系:hts_0000@sina.com

Hash表/哈希表

Hash表通过Hash函数映射的方式,将一个稀疏的集合存储到一个紧凑的集合中。比如一个集合的数据范围是1~10^9次方,但里面的数只有10^5个,我们就可以通过一个Hash函数(通常是取模)的方式将集合中的数映射到一个10^5的集合中,减少存储空间的浪费。

但是将一个大集合映射到小集合,必然造成信息的损失,表现到Hash表中就是Hash冲突。通过Hash后,大集合中的某些数必定被映射到了同一个点上。因此我们无法确认该点是否存在某些值,因为有多个值同时被映射过来了。

解决Hash冲突的经典解决方式(都需要额外判断):

  • 拉链法
  • 开放寻址法

拉链法 每一个点存储的是一个链式结构,冲突时将新值链接到前面或后面。确认是否存在该值时,可以依次判断链上的每一个节点。

开放寻址法 如果该点存在冲突,则往下或往上寻找空的位置,将新值写入。确认是否存在该值时,先找到映射的位置,然后向上或向下依次判断每一个值。

代码模板

拉链法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
    "fmt"
    "bufio"
    "os"
)

// 拉链法

const N int = 100003
var h, e, ne [N]int
var idx int

func insert(x int) {
    // x 为负数时 % N 结果为负数,加上 N 变为正数,再 % N
    k := (x % N + N) % N
    e[idx] = x
    ne[idx] = h[k]
    h[k] = idx
    idx++
}

func query(x int) bool {
    k := (x % N + N) % N
    for i := h[k]; i != -1; i = ne[i] {
        if e[i] == x {
            return true
        }
    }
    return false
}

func main() {
    in := bufio.NewReader(os.Stdin)
    out := bufio.NewWriter(os.Stdout)
    defer out.Flush()
    
    // 将h所有槽位初始化为-1,表示空节点
    for i := 0; i < N; i++ { h[i] = -1 }
    var n int
    fmt.Fscan(in, &n)
    for ; n > 0; n-- {
        var op string
        var x int
        fmt.Fscan(in, &op, &x)
        if op == "I" {
            insert(x)
        } else {
            if query(x) {
                fmt.Fprintln(out, "Yes")
            } else {
                fmt.Fprintln(out, "No")
            }
        }
    }
}

开放寻址法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
    "fmt"
    "bufio"
    "os"
)

// 开放寻址法通常开数据范围2~3倍的空间
const N int = 200003
// 用一个不存在于数据范围内的数来表示该点没有值
const null int = 0x3f3f3f3f
var h [N]int

// 在 h 中寻找 x 应该插入的位置
func find(x int) int {
    k := (x % N + N) % N
    // 在h中找一个x能插入的位置,如果x已经插入过了,返回插入的位置
    for h[k] != null && h[k] != x {
        k++
        // 如果到最后了,尝试从头开始查找插入的问题
        if k == N { k = 0 }
    }
    return k
}

func main() {
    in := bufio.NewReader(os.Stdin)
    out := bufio.NewWriter(os.Stdout)
    defer out.Flush()

    // 初始化将所有点标记为空
    for i := 0; i < N; i++ { h[i] = null }
    var n int
    fmt.Fscan(in, &n)
    for ; n > 0; n-- {
        var op string
        var x int
        fmt.Fscan(in, &op, &x)
        k := find(x)
        if op == "I" {
            h[k] = x
        } else {
            if h[k] != null {
                fmt.Fprintln(out, "Yes")
            } else {
                fmt.Fprintln(out, "No")
            }
        }
    }
}

经典模板题

840. 模拟散列表

字符串Hash

字符串hash是一种快速判断字符串是否相等的方法。Golang中strings.Index等库均用到了字符串Hash——RK算法。详细可以看Golang中的字符串匹配——RK算法

代码模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
    "fmt"
    "bufio"
    "os"
)

const N int = 100010
// P表示P进制,题目提到字符串中只包含大小写英文字母和数字,因此128可以完全存下
// ASCII码总共128,131是大于128的最小素数,用素数是因为可以有效的降低冲突的概率
// 这里还有个细节是用到了uint64
// 当字符串足够长时,产生的hash值可能会非常大,int可能会存不下,但是uint64也不一定存的下
// 经验告诉我们,当P取131或13331时,hash值mod一个2^64,在99.99%的情况下不会发生冲突
// 因此用uint64还有个好处,当uint64发生溢出时,就相当于mod了2^64,可以减少许多次计算
// 因此这个字符串hash方法,是假定不会发生冲突的
const P uint64 = 131
// h存储的是s所有前缀的hash值,预处理好之后,求某一段的hash就只需要O(1)的时间
// p存储的是P^i次方数,方便O(1)时间计算hash
var h, p [N]uint64

func get(l, r int) uint64 {
    // 为什么是 h[r] - h[l-1] * p[r-l+1]?
    // 这个公式的作用是求出 r ~ l 这一段的hash值
    // h[l-1] * p[r-l+1] 是把 1 ~ l - 1 这一段放大到与 1 ~ r 对齐同一位
    // 比如 ABCDE 与 ABC 前三个字符是一样的,只差两位
    // ABC 的 hash 值乘上 P^2 变成 ABC00,再用 ABCDE - ABC00,得到 DE 的 hash 值
    // 然后再相减
    return h[r] - h[l-1] * p[r-l+1]
}

func main() {
    in := bufio.NewReader(os.Stdin)
    out := bufio.NewWriter(os.Stdout)
    defer out.Flush()
    
    var n, m int
    fmt.Fscan(in, &n, &m)
    var s string
    fmt.Fscan(in, &s)
    s = " " + s
    
    // 预处理前缀hash和P进制每一位数
    p[0] = 1
    for i := 1; i <= n; i++ {
        p[i] = p[i-1] * P
        h[i] = h[i-1] * P + uint64(s[i])
    }
    for ; m > 0; m-- {
        var l1, r1, l2, r2 int
        fmt.Fscan(in, &l1, &r1, &l2, &r2)
        // 因为hash冲突极大概率不存在,因此判断hash值相同就认为字符串相同
        if get(l1, r1) == get(l2, r2) {
            fmt.Fprintln(out, "Yes")
        } else {
            fmt.Fprintln(out, "No")
        }
    }
}

经典模板题

841. 字符串哈希

求大于x的最小素数

代码模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 快速求比128数大的最小素数
for i := 128; ; i++ {
    flag := true
    for j := 2; j * j <= i; j++ {
        if i % j == 0 {
            flag = false
            break
        }
    }
    if flag {
        fmt.Fprintln(out, i)
        break
    }
}