Go36-34,35-并发安全字典(sync.Map)

并发安全字典(sync.Map)

之前的几篇,几乎已经把Go语言自带的同步工具都讲过了。这篇要讲的是一个并发安全的高级数据结构:sync.Map。

原生字典

Go语言自带的字典类型map,就是原生字典,并不是并发安全的。
在使用原生字典的时候,应该在启动goroutine之前就完成字典的初始化和赋值。或者更高级的做法是,可以在goroutine中,在首次使用的时候通过sync.Once来并发安全的完成初始化和赋值的操作,达到一个延迟初始化的优化效果。之后在使用字典的时候,就只能获取其中的内容,不能再对其进行修改了。这个在讲sync.Once时,在最后有示例。
如果是需要一个并发安全的,可以修改内容的原生字典,也不是太麻烦。加上互斥锁或读写锁就可以轻松实现了。下面就是一个自制的简易并发安全字典:

package main

import (
    "fmt"
    "sync"
)

// 一个自制的简易并发安全字典
type ConcurrentMap struct {
    m map[interface{}]interface{}
    mu sync.RWMutex
}

func NewConcurrentMap() *ConcurrentMap {
    return &ConcurrentMap{
        m: make(map[interface{}]interface{}),
    }
}

func (cm *ConcurrentMap) Delete(key interface{}) {
    cm.mu.Lock()
    defer cm.mu.Unlock()
    delete(cm.m, key)
}

func (cm *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {
    cm.mu.RLock()
    defer cm.mu.RUnlock()
    value, ok = cm.m[key]
    return
}

func (cm *ConcurrentMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
    cm.mu.Lock()
    defer cm.mu.Unlock()
    actual, loaded = cm.m[key]
    if loaded {
        return
    }
    cm.m[key] = value
    actual = value
    return
}

func (cm *ConcurrentMap) Store(key, value interface{}) {
    cm.mu.Lock()
    defer cm.mu.Unlock()
    cm.m[key] = value
}

func (cm *ConcurrentMap) Range(f func(key, value interface{}) bool) {
    cm.mu.RLock()
    defer cm.mu.RUnlock()
    for k, v := range cm.m {
        if !f(k, v) {
            break
        }
    }
}

func main() {
    pairs := []struct{
        k string
        v int
    }{
        {"k1", 1},
        {"k2", 2},
        {"k3", 3},
        {"k4", 4},
    }

    {
        fmt.Println("创建map")
        cm := NewConcurrentMap()
        for i := range pairs {
            cm.Store(pairs[i].k, pairs[i].v)
        }
        fmt.Println(cm.m)

        fmt.Println("遍历输出map")
        cm.Range (func(k, v interface{}) bool {
            fmt.Printf("%v: %v\n", k, v)
            return true
        })

        fmt.Println("Load 和 LoadOrStore 方法")
        key := "k3"
        value, ok := cm.Load(key)
        fmt.Printf("Load %v: %v: %v\n", ok, key, value)
        value, ok = cm.LoadOrStore(key, 5)
        fmt.Printf("LoadOrStore %v: %v: %v\n", ok, key, value)
        key = "k5"
        value, ok = cm.Load(key)
        fmt.Printf("Load %v: %v: %v\n", ok, key, value)
        value, ok = cm.LoadOrStore(key, 5)
        fmt.Printf("LoadOrStore %v: %v: %v\n", ok, key, value)
        fmt.Println(cm.m)

        fmt.Println("Delete 方法")
        key = "k2"
        cm.Delete(key)
        fmt.Println(cm.m)
        key = "k21"
        cm.Delete(key)
        fmt.Println(cm.m)
    }
    fmt.Println("Over")
}

并发安全字典

Go言语官方是在Go 1.9中才正式加入了并发安全的字典类型sync.Map。所以在这之前,已经有很多库提供了类似的数据结构,其中有一些的性能在很大程度上有效的避免了的锁的依赖,性能相对会好一些。不过这些都是过去了,现在有了官方的并发安全的字典类型sync.Map可以使用。
下面就是使用并发安全字典的示例:

package main

import (
    "fmt"
    "sync"
)

func main() {
    pairs := []struct{
        k string
        v int
    }{
        {"k1", 1},
        {"k2", 2},
        {"k3", 3},
        {"k4", 4},
    }

    {
        fmt.Println("创建map")
        cm := sync.Map{}
        for i := range pairs {
            cm.Store(pairs[i].k, pairs[i].v)
        }

        fmt.Println("遍历输出map")
        cm.Range (func(k, v interface{}) bool {
            fmt.Printf("%v: %v\n", k, v)
            return true
        })

        fmt.Println("Load 和 LoadOrStore 方法")
        key := "k3"
        value, ok := cm.Load(key)
        fmt.Printf("Load %v: %v: %v\n", ok, key, value)
        value, ok = cm.LoadOrStore(key, 5)
        fmt.Printf("LoadOrStore %v: %v: %v\n", ok, key, value)
        key = "k5"
        value, ok = cm.Load(key)
        fmt.Printf("Load %v: %v: %v\n", ok, key, value)
        value, ok = cm.LoadOrStore(key, 5)
        fmt.Printf("LoadOrStore %v: %v: %v\n", ok, key, value)

        fmt.Println("Delete 方法")
        key = "k2"
        cm.Delete(key)
        key = "k21"
        cm.Delete(key)

        fmt.Println("遍历输出map")
        cm.Range (func(k, v interface{}) bool {
            fmt.Printf("%v: %v\n", k, v)
            return true
        })
    }
    fmt.Println("Over")
}

这里用起来和上面原生加锁的字典没太大差别,其实上面的例子里的方法名称和功能应该就是参照这里写的。

并发安全字典与原生字典的比较

这里从3个方面对并发安全字典和原生字典进行了比较。
算法复杂度
这个字典类型提供了一些常用的键值存取操作方法,并保证了这些操作的并发安全。同时,它的存、取、删除等操作都可以基本保证在常数时间内执行完毕。就是说,它的算法复杂度与原生字典一样都是O(1)的。
效率和锁
与单纯使用原生字典和互斥锁的用法相比,使用并发安全字典可以显著减少锁的争夺。虽然并发安全字典本身也用到了锁,但是它在尽可能的避免使用锁。使用锁,就以为着要把一些并发的操作强制串行化,这就会降低程序的性能。因此,在讲原子操作的时候,就建议能用原子操作就不要用锁。不过当时也说了,原子操作支持的数据类型有局限性。
编译器的支持
无论在何种场景下使用并发安全字典,都需要记得,它与原生字典是明显不同的。并发安全字典只有Go语言标准库中的一员,而原生字典是语言层面的东西。正是因为这个原因,Go语言的编译器不会对它的键和值进行特殊的类型检查。它所有的方法涉及的键和值类型都是空接口。所以,必须在程序中自行保证它的键类型和值类型的正确性。

并发安全字典key类型的要求

原生字典的key不能是如下的类型:

  • 不能是,函数类型
  • 不能是,字典类型
  • 不能是,切片类型

并发安全字典的key的类型也是一样的限制。
在并发安全字典内部,使用的存储介质仍然是原生字典,上面这些类型都不能用。但是由于并发安全字典的key类型的实际类型是空接口,就是可以是任何类型,所以编译器是允许使用任何类型的。如果使用了上述key不支持的类型,编译阶段是发现不了的。只要在程序运行期间才能确定键的实际类型,此时如果是不正确的key值实际类型肯定会引发panic。
因此,一定不要违反字典对key类型的限制。应该在每次操作并发安全字典的时候,都去显式的检查key值的实际类型。无论是存、取、删除都是如此。
更好的做法是,把针对同一个并发安全字典的几种操作都集中起来,然后统一的编写检查代码。如果是把并发安全字典封装在一个结构体里,是一个很好的选择。
总之,必须保证key的类型是可比较的(或者说是可判等的)。如果实在确定不了,那么可以先通过调用reflect.TypeOf函数得到一个键对应的反射类型值(即:reflect.Type类型),然后再调用这个值的Comparable方法,得到确切的判断结果。Comparable方法返回一个布尔值,判断类型是否是可比较的,即:判断是否可以作为key的类型。

保证key和value的类型正确

简单说,可以使用类型断言表达式或者反射操作来保证类型正确。为了进一步明确并发安全字典中key值的实际类型,这里大致有两个方案。

只能存储特定的类型

这个是方案一:让并发安全字典只能存储某个特定类型的key。
指定key只能是某个类型,一旦完全确定了key的类型,就可以在存、取、删除的时候,使用类型断言表达式去对key的类型做检查了。一般这种检查并不繁琐,而且要是把并发安全字典封装在一个结构体里,就更方便了。这时Go语言编译器就可以帮助完成检查的工作。就像下面这样:

// 这是一个key为字符串类型,value为数字的字典
type StrIntMap struct {
    m sync.Map
}

func (ms *StrIntMap) Delete(key int) {
    ms.m.Delete(key)
}

func (ms *StrIntMap) Load(key int) (value int, ok bool) {
    v, ok := ms.m.Load(key)
    if v != nil {
        value = v.(int)
    }
    return
}

func (ms *StrIntMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
    a, loaded := ms.m.LoadOrStore(key, value)
    actual = a.(int)
    return
}

func (ms *StrIntMap) Range(f func(key string, value int) bool) {
    f1 := func(key, value interface{}) bool {
        return f(key.(string), value.(int))
    }
    ms.m.Range(f1)
}

func (ms *StrIntMap) Store(key string, value int) {
    ms.m.Store(key, value)
}

把并发安全字典封装到结构体中之后,要把原本字典中公开的方法在结构体上再实现一遍。只要简单的调用并发安全字典里的方法就可以了。由于自己写的结构体的方法的参数列表里已经明确的定义了变量的类型,所以就不用再做类型检查了,并且使用编译器编写代码的时候类型错误也会有提示。基于上面的情况,使用这些方法取出key和value的时候,完全不用担心类型不正确,因为正确性在最初存入的时候就已经保证了。这里在取出值的时候,由于sync.Map返回的是空接口,所以需要用类型断言做一下类型转换。这里用类型断言的时候只使用一个返回值,直接转,不用担心会有错误。

使用反射

上一个方案虽然很好,但是有一点不方便。就是封装了一大堆内容,实现了一个key-value的类型。如果还是需要另外一个类型的字典,还得再封装同样的一大堆的内容。这样的话,在需求多样化之后,工作量反而更大,而且会有很多雷同的代码。
这里需要一个这样效果的方案:既要保持sync.Map类型原有的灵活性,又可以约束key和value的类型。
这个是方案二:就是通过反射来实现做类型的判断

结构体的类型

这次在设计结构体类型的时候,只包含sync.Map类型的字段就不够了,需要向下面这样:

type ConcurrentMap struct {
    m         sync.Map
    keyType   reflect.Type
    valueType reflect.Type
}

这次多定义了2个字段keyType和valueType,分别用于保存key类型和value类型。类型都是reflect.Type,就是反射类型。这个类型可以代表Go语言的任何数据类型,并且类型的值也非常容易获得:就是通过reflect.TypeOf函数并把某个样本值传入即可,比如:reflect.TypeOf(int(3)),就是int类型的反射类型值。

构造函数

首先提供一个方法,让外部的代码可以创建一个结构体:

func NewConcurrentMap(keyType, valueType reflect.Type) (*ConcurrentMap, error) {
    if keyType == nil {
        return nil, fmt.Errorf("key 类型为 nil")
    }
    if !keyType.Comparable() { // 判断类型是否可以比较,就是是否是内做key的类型
        return nil, fmt.Errorf("不可比较的类型: %s", keyType)
    }
    if valueType == nil {
        return nil, fmt.Errorf("value 类型为 nil")
    }
    cm := &ConcurrentMap{
        keyType:   keyType,
        valueType: valueType,
    }
    return cm, nil
}

创建成功,则返回结构体的指针和nil的错误类型。创建失败,则返回nil和具体的错误类型。
先判断传入的类型是否为nil。另外对于key,基于对key类型的限制,必须是可比较的,这里用reflect包里的Comparable方法就可以进行判断了。

Load方法

结构体有了,接下来就实现所有的方法。首先是Load方法:

func (cm *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {
    if reflect.TypeOf(key) != cm.keyType{
        return
    }
    return cm.m.Load(key)
}

这里的类型检查的代码非常简单。如果参数key的反射类型与keyTpye字段的反射类型不相等,那么就直接返回空值。类型都不对,那么字典里是一定没有这个key的,所以就直接返回查询不到的结果。这时,返回的是nil和false,完全符合Load方法原本的含义。

Store方法

Stroe方法接收2个空接口类型,没有返回值:

func (cm *ConcurrentMap) Store(key, value interface{}) {
    if reflect.TypeOf(key) != cm.keyType {
        panic(fmt.Errorf("key类型不符合: 实际类型: %v, 需要类型: %v\n", reflect.TypeOf(key), cm.keyType))
    }
    if reflect.TypeOf(value) != cm.valueType {
        panic(fmt.Errorf("value类型不符合: 实际类型: %v, 需要类型: %v\n", reflect.TypeOf(value), cm.valueType))
    }
}

这里的做法是一旦类型检查不符,就直接引发panic。这么做主要是由于Store方法没有结果声明,就是无返回值,所以在参数值有问题的时候,无法通过比较平和的方式告知调用方。这么做也是符合Store方法的原本含义的。
也可以把结构体的Store方法改的和sysc.Map里的Store方法稍微不一样一点,就是加上结果声明,返回一个error类型。这样当参数值类型不正确的时候,就返回响应的错误,而不引发panic。作为示例,就先用panic的方式吧。在实际的应用场景里可以再做优化和改进。

完整的定义

其他的方法就都差不多了,下面展示了所有定义的方法:

type ConcurrentMap struct {
    m         sync.Map
    keyType   reflect.Type
    valueType reflect.Type
}

func NewConcurrentMap(keyType, valueType reflect.Type) (*ConcurrentMap, error) {
    if keyType == nil {
        return nil, fmt.Errorf("key 类型为 nil")
    }
    if !keyType.Comparable() { // 判断类型是否可以比较,就是是否是内做key的类型
        return nil, fmt.Errorf("不可比较的类型: %s", keyType)
    }
    if valueType == nil {
        return nil, fmt.Errorf("value 类型为 nil")
    }
    cm := &ConcurrentMap{
        keyType:   keyType,
        valueType: valueType,
    }
    return cm, nil
}

func (cm *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {
    if reflect.TypeOf(key) != cm.keyType {
        return
    }
    return cm.m.Load(key)
}

func (cm *ConcurrentMap) Store(key, value interface{}) {
    if reflect.TypeOf(key) != cm.keyType {
        panic(fmt.Errorf("key类型不符合: 实际类型: %v, 需要类型: %v\n", reflect.TypeOf(key), cm.keyType))
    }
    if reflect.TypeOf(value) != cm.valueType {
        panic(fmt.Errorf("value类型不符合: 实际类型: %v, 需要类型: %v\n", reflect.TypeOf(value), cm.valueType))
    }
    cm.m.Store(key, value)
}

func (cm *ConcurrentMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
    if reflect.TypeOf(key) != cm.keyType {
        panic(fmt.Errorf("key类型不符合: 实际类型: %v, 需要类型: %v\n", reflect.TypeOf(key), cm.keyType))
    }
    if reflect.TypeOf(value) != cm.valueType {
        panic(fmt.Errorf("value类型不符合: 实际类型: %v, 需要类型: %v\n", reflect.TypeOf(value), cm.valueType))
    }
    actual, loaded = cm.m.LoadOrStore(key, value)
    return
}

func (cm *ConcurrentMap) Delete(key interface{}) {
    if reflect.TypeOf(key) != cm.keyType {
        return
    }
    cm.m.Delete(key)
}

func (cm *ConcurrentMap) Range(f func(key, value interface{}) bool) {
    cm.m.Range(f)
}

总结

第一种方案,适用于可以完全确定key和value的具体类型的情况。这是,可以利用Go语言的编译器去做类型检查,并用类型断言表达式作为辅助。缺陷是一次定义只能满足一种字典类型,一旦字典类型多样化,就需要编写大量的重复代码。不过还有一个好处,就是在编译阶段就可以发现类型不正确的问题。
第二种方案,在程序运行之前不用明确key和value的类型,只要在初始化并发安全字典的时候,动态的给定key和value的类型即可。主要是用到了reflect包中的方法和数据类型。灵活性高了,一次就能满足所有的字典类型。但是那些反射操作或多或少都会降低程序的性能。而且如果有类型不符合的情况,无法在编译阶段发现。

并发安全字典内部的实现

保证并发安全,还要保证效率,主要的问题就是要尽量避免使用锁。
sync.Map类型在内部使用了大量的原子操作来存取key和value,并使用了两个原生的字典作为存储介质。

只读字典

其中一个原生的字典是sync.Map的read字段,类型是sync/atomic.Value类型。这个原生字典可以被看作一个快照,总会在条件满足时,去重新保存所属的sync.Map值中包含的所有key-value对。这里叫它只读字典,它虽然不会增减其中的key,但是允许改变其中的key所对应的value。所以这个只读特性只是对于所有的key的。
因为是sync/atomic.Value类型,所以都是原子操作,不用锁。另外,这个只读字典在存储key-value对的时候,对value还做了一层封装。先把值转换为了unsafe.Pointer类型的值,然后再把unsafe.Pointer也封装,并存储在其中的原生字典中。如此一来,在变更某个key所对应的value的时候,就可以使用原子操作了。

脏字典

另一个原生字典是sync.Map的dirty字段,它的key类型是空接口。它存储key-value对的方式与read字段中的原生字典一致,并且也是把value的值先做转换和封装后再进行存储的。这里叫它脏字典
有点不太好理解,看下源码里的类型定义帮助理解只读字典和脏字典的类型:

type Map struct {
    mu Mutex
    read atomic.Value // readOnly
    dirty map[interface{}]*entry
    misses int
}

// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
    m       map[interface{}]*entry
    amended bool // true if the dirty map contains some key not in m.
}

type entry struct {
    p unsafe.Pointer // *interface{}
}

存取值的过程

了解下sync.Map是如何巧妙的使用它的2个原生字典的。分析各个方法在执行时具体进行的操作,再判断这些操作对性能的影响。

获取值的过程
sync.Map在查找指定的key所对应的value的时候,会先去只读字典中查找,只读字典是原子操作,不要要锁。只有当只读字典里没有,再去脏字典里查找,访问脏字典需要加锁。

存储值的过程
在存key-value的时候,只要只读字典中已经存在这个key了,并且未被标记为删除,就会把新值直接存到value里然后直接返回,还是不需要加锁。否则,才会加锁后,把新的key-value加到脏字典里。这个时候还要抹去只读字典里的删除标记。

删除标记
当一个kye-value对应该被删除,但是仍然存在于只读字典中的时候,会用标记来标记删除,但是还不会直接武林删除。
这种情况会在重建脏字典以后的一段时间内出现。不过,过不了多久,还是会被真正的删除掉的。在查找和遍历的时候,有删除标记的key-value对会忽略的。

删除的过程
删除key-value对,sync.Map会先检查只读字典中是否有对应的key。如果没有,可能会在脏字典中,那就加锁然后试图从脏字典里删掉该key-value对。最后,sync.Map会把key-value对中指向value的那个指针置为nil,这是另一种逻辑删除的方式,脏字点重建之后自然就真正删除了。

重建脏字典
只读字典和脏字典之间是会互相转换的。在脏字典中查找key-value对足够次数时,sync.Map会把脏字典直接作为只读字典,保存到read字段中。然后把dirty字段置为nil。之后一旦再有新的key-value存入,就会依据只读字典去重建脏字典,会把只读字典标记为删除的key-value对过滤掉。这些转换操作都有脏字典,所以都需要加锁。

小结
sync.Map的只读字典和脏字典中的key-value对并不是实时同步的。由于只读字典的key不能改变,所以其中的key-value对可能是不全的。而脏字典中的key-value对总是全的,不多也不少。新的key-value对只读字典里没有;已经标记删除的key-value对物理上还在只读字典里,只是加了标记。
在读操作为主,而写操作很少的情况下,并发安全字典的性能会更好。在几个写操作中,新增key-value对的操作对并发安全字典的性能影响最大,其次是删除操作。修改操作影响略小,只读字典里如果有这个key的话,并且没有标记删除,直接在只读字典里把value改掉,不用加锁,对性能影响就会很小。

原文地址:http://blog.51cto.com/steed/2348335

时间: 2024-10-30 11:26:58

Go36-34,35-并发安全字典(sync.Map)的相关文章

Go语言 sync.Map(在并发中使用)

Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的. 需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构. sync.Map 有以下特性: 无须初始化,直接声明即可. sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Loa

sync.Map源码分析

sync.Map源码分析 背景 众所周知,go普通的map是不支持并发的,换而言之,不是线程(goroutine)安全的.博主是从golang 1.4开始使用的,那时候map的并发读是没有支持,但是并发写会出现脏数据.golang 1.6之后,并发地读写会直接panic: fatal error: concurrent map read and map write package main func main() { m := make(map[int]int) go func() { for

读/写锁的实现和应用(高并发状态下的map实现)

程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁.在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源.但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存).这就需要一个读/写锁来解决这个问题. 按照上面的叙述,简单的实现出一个读/写锁 public class ReadWriteLock{ private int readers = 0; pr

sizzle.js学习笔记利用闭包模拟实现数据结构:字典(Map)

sizzle.js学习笔记利用闭包模拟实现数据结构:字典(Map) 这几天学习和查看了jQuery和Property这两个很流行的前端库的御用选择器组件Sizzle.js的源代码,收获还是相对多的!之前一直做使用Java语言开发,其丰富的组件类库使得开发效率那叫一个快呀!突然转来做JavaScript一时间还有点儿不适应(快半年了),不过自从看见那么多漂亮的网站和对JavaScript接触的越来越多,也发现了其中的一些乐趣.正如自己一直坚信的那样,编程语言仅仅是工具,重要的是编程思想!使用Jav

[转] Javascript中数组与字典(即map)的使用

简述: 简单记录一下数据结构Map和数组, 其实在Javascript这种弱类型的脚本语言中,数组同时也就是字典,下面主要就是字典数组的简易使用 代码: 1. 数组中添加map [html] view plaincopy <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> &l

HDU1251 统计难题(字典树|map

Ignatius最近遇到一个难题,老师交给他很多单词(只有小写字母组成,不会有重复的单词出现),现在老师要他统计出以某个字符串为前缀的单词数量(单词本身也是自己的前缀). Input输入数据的第一部分是一张单词表,每行一个单词,单词的长度不超过10,它们代表的是老师交给Ignatius统计的单词,一个空行代表单词表的结束.第二部分是一连串的提问,每行一个提问,每个提问都是一个字符串. 注意:本题只有一组测试数据,处理到文件结束.Output对于每个提问,给出以该字符串为前缀的单词的数量.Samp

Redis源码阅读笔记(2)——字典(Map)实现原理

因为redis是用c写的,c中没有自带的map,所以redis自己实现了map,来看一下redis是怎么实现的. 1.redis字典基本数据类型 redis是用哈希表作为字典的底层实现,dictht是哈希表的定义: typedef struct dictht { // 哈希表节点指针数组(俗称桶,bucket) dictEntry **table; // 指针数组的大小 unsigned long size; // 指针数组的长度掩码,用于计算索引值 unsigned long sizemask

ACM学习历程—HDU 4287 Intelligent IME(字典树 || map)

Description We all use cell phone today. And we must be familiar with the intelligent English input method on the cell phone. To be specific, the number buttons may correspond to some English letters respectively, as shown below: 2 : a, b, c    3 : d

ZOJ 3674 Search in the Wiki(字典树 + map + vector)

题目链接:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=4917 题意:每个单词都一些tips单词.先输入n个单词和他们的tips.然后m组查询,每次查询一些单词,按字典序输出这些单词的公有tips.(每个单词都都只包含小写大写字母) 思路:对第i个单词,用vector数组g,g[i]来存这个单词的所有tips.对于所有单词建立字典树,在单词的结尾结点存好该单词的tips在g数组中存的一维下标i.最后用map来计数每组询问中