字典树-Trie

简介

在计算机科学中,trie,又称前缀树字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。典型应用是用于统计,排序和保存大量[字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

  • trie包含三个单词(seepainpaint)的数据结构如下
trie.png

CRD

  • 节点至少要包含:
    1. 是否是结尾
    2. 子节点

例如插入一个paint单词,如果用户查询pain,尽管 paint 包含了 pain,但是Trie中仍然不包含 pain 这个单词。

插入

遍历字符串给相应节点,注意处理尾节点给记录

查找

遍历节点找字符,注意找最后一个字符时需要判断是不是结尾

删除

一个单词是另一个单词的前缀
无分支
单词的除了最后一个字母,其他的字母有多个分支
/// Trie里面的节点
type Node struct {
    isWord bool
    size   int
    //只考虑字母
    children [26]*Node
}

func (n *Node) Size() int {
    return n.size
}

func NewNode() *Node {
    return &Node{
        isWord:   false,
        children: [26]*Node{},
    }
}

type Trie struct {
    size int
    root *Node
}

func NewTrie() *Trie {
    return &Trie{
        size: 0,
        root: NewNode(),
    }
}

func (t *Trie) Size() int {
    return t.size
}

func (t *Trie) IsEmpty() bool {
    return 0 == t.size
}

func (t *Trie) Add(word string) {
    current := t.root
    for _, s := range word {
        next := current.children[s-'a']
        if next == nil {
            current.children[s-'a'].size++
            current.children[s-'a'] = NewNode()
        }
        current = current.children[s-'a']
    }
    if !current.isWord {
        t.size++
        current.isWord = true
    }
}

func (t *Trie) Contain(word string) bool {
    current := t.root
    for _, s := range word {
        next := current.children[s-'a']
        if next == nil {
            return false
        }
        current = next
    }
    return current.isWord
}

func (t *Trie) Delete(word string) bool {
    var multiChildNode *Node
    multiChildNodeIndex := -1
    current := t.root
    for i, s := range word {
        child := current.children[s-'a']
        if child == nil {
            return false
        }
        if child.Size() > 0 {
            multiChildNodeIndex = i
            multiChildNode = child
        }
        current = child
    }
    //图1
    if current.size > 0 {
        if current.isWord {
            current.isWord = false
            current.size--
            t.size--
            return true
        }
        return false
    }
    //图2
    if multiChildNodeIndex == -1 {
        t.root.children[word[0]-'a'] = nil
        t.size--
        return true
    }
    //图3
    if multiChildNodeIndex != len(word)-1 {
        multiChildNode.children[word[multiChildNodeIndex+1]-'a'] = nil
        t.size--
        return true
    }
    return false
}

Trie查询效率非常高,但是对空间的消耗还是挺大的,这也是典型的空间换时间。

可以使用 压缩字典树(Compressed Trie) ,但是维护相对来说复杂一些。

如果我们不止存储英文单词,还有其他特殊字符,那么维护子节点的集合可能会更多。

可以对Trie字典树做些限制,比如每个节点只能有3个子节点,左边的节点是小于父节点的,中间的节点是等于父节点的,右边的子节点是大于父节点的,这就是三分搜索Trie字典树(Ternary Search Trie)。

GinのRoute

上文已经说到Trie适合用于引擎系统用于文本词频统计,核心是做字符串比较的,同样,它也适用于路由匹配,看看Gin是怎么做的吧

路由类型

param 与 catchAll 使用的区别就是 : 与 * 的区别。* 会把路由后面的所有内容赋值给参数 key;但 : 可以多次使用。比如:/user/:id/:no 是合法的,但 /user/*id/:no 是非法的,因为 * 后面所有内容会赋值给参数 id。

//Gin的路由类型
type nodeType uint8

const (
    static nodeType = iota  // 纯静态路由
    root                    // 根节点
    param                   // url中出现':id'
    catchAll                // url中出现'*'
)

type node struct {
    path      string          // 当前节点相对路径(与祖先节点的 path 拼接可得到完整路径)                
    indices   string          // 所有孩子节点的path[0]组成的字符串,通过这个可以找children的位置            
    wildChild bool            // 是否有*或者:           
    nType     nodeType        // 路由类型     
    priority  uint32          // 优先级,当前节点及子孙节点的实际路由数量
    children  []*node         // children
    handlers  HandlersChain   // 业务逻辑包含中间件
    fullPath  string          // 全url路径
}

pathindices

关于 path 和 indices,其实是使用了前缀树的逻辑。

举个栗子:
如果我们有两个路由,分别是 /index,/inter,则根节点为 {path: "/in", indices: "dt"...},两个子节点为{path: "dex", indices: ""},{path: "ter", indices: ""}

路由树例子

r.GET("/", func(context *gin.Context) {})
r.GET("/index", func(context *gin.Context) {})
r.GET("/inter", func(context *gin.Context) {})
r.GET("/go", func(context *gin.Context) {})
r.GET("/game/:id", func(context *gin.Context) {})
路由树

相关流程图

todo

源码


// 创建新的路由with业务(包含中间件)
// 不是线程安全的
func (n *node) addRoute(path string, handlers HandlersChain) {
    fullPath := path
    n.priority++

    // Empty tree
    if len(n.path) == 0 && len(n.children) == 0 {
        n.insertChild(path, fullPath, handlers)
        n.nType = root
        return
    }

    parentFullPathIndex := 0

walk:
    for {
        // 找到最长的公共前缀
        // 公共前缀不能包含*和:
        i := longestCommonPrefix(path, n.path)

        // 如果根节点的path包含新输入的path,需要分割path,再进行组合
        if i < len(n.path) {
            child := node{                 
                path:      n.path[i:],       // 用户输入后面字符串
                wildChild: n.wildChild,
                indices:   n.indices,
                children:  n.children,
                handlers:  n.handlers,
                priority:  n.priority - 1,
                fullPath:  n.fullPath,
            }

            n.children = []*node{&child}
            n.indices = bytesToStr([]byte{n.path[i]})
            n.path = path[:i]  //最长公共字符串
            n.handlers = nil 
            n.wildChild = false
            n.fullPath = fullPath[:parentFullPathIndex+i]
        }

        // 新建一个children
        if i < len(path) {
            path = path[i:]

            if n.wildChild {
                parentFullPathIndex += len(n.path)
                n = n.children[0]
                n.priority++

                // Check if the wildcard matches
                if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
                    // Adding a child to a catchAll is not possible
                    n.nType != catchAll &&
                    // Check for longer wildcard, e.g. :name and :names
                    (len(n.path) >= len(path) || path[len(n.path)] == '/') {
                    continue walk
                }

                pathSeg := path
                if n.nType != catchAll {
                    pathSeg = strings.SplitN(path, "/", 2)[0]
                }
                prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
                panic("'" + pathSeg +
                    "' in new path '" + fullPath +
                    "' conflicts with existing wildcard '" + n.path +
                    "' in existing prefix '" + prefix +
                    "'")
            }

            c := path[0]

            // 处理参数后面的 /
            if n.nType == param && c == '/' && len(n.children) == 1 {
                parentFullPathIndex += len(n.path)
                n = n.children[0]
                n.priority++
                continue walk
            }

            
            for i, max := 0, len(n.indices); i < max; i++ {
                if c == n.indices[i] {
                    parentFullPathIndex += len(n.path)
                    i = n.incrementChildPrio(i)
                    n = n.children[i]
                    continue walk
                }
            }

            // 插入子节点
            if c != ':' && c != '*' {
                n.indices += bytesToStr([]byte{c})
                child := &node{
                    fullPath: fullPath,
                }
                n.children = append(n.children, child)
                n.incrementChildPrio(len(n.indices) - 1)
                n = child
            }
            n.insertChild(path, fullPath, handlers)
            return
        }

        // 否则就是当前节点
        if n.handlers != nil {
            panic("handlers are already registered for path '" + fullPath + "'")
        }
        n.handlers = handlers
        n.fullPath = fullPath
        return
    }
}

// 根据url拿到handle的句柄,url参数放在map里面
func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
walk: // Outer loop for walking the tree
    for {
        prefix := n.path
        if len(path) > len(prefix) {
            if path[:len(prefix)] == prefix {
                path = path[len(prefix):]
                // 如果该节点没有*和:继续向子节点找
                if !n.wildChild {
                    idxc := path[0]
                    for i, c := range []byte(n.indices) {
                        if c == idxc {
                            n = n.children[i]
                            continue walk
                        }
                    }

                    // 什么都没有发现
                    value.tsr = (path == "/" && n.handlers != nil)
                    return
                }

                // 处理url参数
                n = n.children[0]
                switch n.nType {
                case param:
                    // Find param end (either '/' or path end)
                    end := 0
                    for end < len(path) && path[end] != '/' {
                        end++
                    }

                    // Save param value
                    if params != nil {
                        if value.params == nil {
                            value.params = params
                        }
                        // Expand slice within preallocated capacity
                        i := len(*value.params)
                        *value.params = (*value.params)[:i+1]
                        val := path[:end]
                        if unescape {
                            if v, err := url.QueryUnescape(val); err == nil {
                                val = v
                            }
                        }
                        (*value.params)[i] = Param{
                            Key:   n.path[1:],
                            Value: val,
                        }
                    }

                    // we need to go deeper!
                    if end < len(path) {
                        if len(n.children) > 0 {
                            path = path[end:]
                            n = n.children[0]
                            continue walk
                        }

                        // ... but we can't
                        value.tsr = (len(path) == end+1)
                        return
                    }

                    if value.handlers = n.handlers; value.handlers != nil {
                        value.fullPath = n.fullPath
                        return
                    }
                    if len(n.children) == 1 {
                        // No handle found. Check if a handle for this path + a
                        // trailing slash exists for TSR recommendation
                        n = n.children[0]
                        value.tsr = (n.path == "/" && n.handlers != nil)
                    }
                    return

                case catchAll:
                    // Save param value
                    if params != nil {
                        if value.params == nil {
                            value.params = params
                        }
                        // Expand slice within preallocated capacity
                        i := len(*value.params)
                        *value.params = (*value.params)[:i+1]
                        val := path
                        if unescape {
                            if v, err := url.QueryUnescape(path); err == nil {
                                val = v
                            }
                        }
                        (*value.params)[i] = Param{
                            Key:   n.path[2:],
                            Value: val,
                        }
                    }

                    value.handlers = n.handlers
                    value.fullPath = n.fullPath
                    return

                default:
                    panic("invalid node type")
                }
            }
        }

        if path == prefix {
            // 已经找到句柄
            if value.handlers = n.handlers; value.handlers != nil {
                value.fullPath = n.fullPath
                return
            }

            
            if path == "/" && n.wildChild && n.nType != root {
                value.tsr = true
                return
            }

        
            for i, c := range []byte(n.indices) {
                if c == '/' {
                    n = n.children[i]
                    value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
                        (n.nType == catchAll && n.children[0].handlers != nil)
                    return
                }
            }

            return
        }

        
        value.tsr = (path == "/") ||
            (len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
                path == prefix[:len(prefix)-1] && n.handlers != nil)
        return
    }
}

参考&致谢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268