前缀树是父节点是子节点前缀的N叉树。

其主要性质是

  • 根节点不包括字符
  • 每个节点的子节点字符不同
  • 节点对应的字符串为从根节点到该节点路径上字符的组合

在gin中也存在着非常巧妙运用前缀树进行路由匹配的结构,本文将以gin路由为例学习一下前缀树

本文代码皆是参考gin@v1.7.4版本源码所构建的更适合理解改造版,并且省略了诸如标记是否是通配符的node wildChild等属性和方法

Gin前缀树结构

gin为每个方法都创建了前缀树。每个前缀树包含了根节点,以及方法名。

每个节点包含了

  • 当前路径
  • 全路径:也就是根节点到该节点的路径组成的字符串
  • 优先级:子节点越多优先级越高
  • 子节点:
  • 子节点首字母组成的字符串:用于快速确认新增或者查询的路径是否存在了
type engine struct {trees methodsTrees
}type methodsTrees []methodTreetype methodTree struct {method stringroot   *node
}type node struct {// 当前节点pathpath string// 子节点首字符组成的stringindices string// 按照优先级排序的子节点children []*node// 优先级priority uint32// 根节点到该节点的字符串fullPath string// 处理器handlers []Handler
}// 遍历树,若存在对应方法所在的树就返回
func (t methodsTrees) get(method string) *node {for _, tree := range t {if tree.method == method {return tree.root}}return nil
}

插入

前缀树插入过程如下

  1. 若根节点为空,直接设置根节点路径为新增路径,并返回
  2. 若当前节点路径与新增路径不存在公共前缀,那么就使当前节点的所有子节点作为当前节点的其中一个子节点,新增路径为另一个子节点
  3. 若新增路径的非公共前缀首字符在当前节点的子节点存在,那么就将该新增路径插入到该子节点,并返回
  4. 在新增路径与节点路径完全匹配的时候,覆盖当前节点处理器和路径
func (n *node) addRoute(path string, handlers []Handler) {fullPath := pathn.priority++// 若为空节点,则直接插入子节点if len(n.path) == 0 && len(n.children) == 0 {n.path = pathn.fullPath = fullPathn.handlers = handlersreturn}// 根据路径插入到节点n.addChild(path, fullPath, 0, handlers)
}func (n *node) addChild(path, fullPath string, parentFullPathIndex int, handlers []Handler) {// 找到新增路径与节点路径的最长公共前缀i := longestCommonPrefix(path, n.path)// 节点的path不是最长公共前缀,分裂原子节点和新增节点if i < len(n.path) {child := &node{path:     n.path[i:],indices:  n.indices,children: n.children,priority: n.priority - 1,fullPath: n.fullPath,}n.children = []*node{child}n.indices = string(n.path[i])n.path = path[:i]n.fullPath = fullPath[:parentFullPathIndex+i]}// 在新增路径存在公共前缀时,添加含有该路径的子节点if i < len(path) {path = path[i:]c := path[0]// 若该路径的非公共前缀首字符在节点children已经存在,那么就将该节点添加到子节点中for j, max := 0, len(n.indices); j < max; j++ {if c == n.indices[j] {parentFullPathIndex += len(n.path)j = n.incrementChildPriority(j)n.children[j].addChild(path, fullPath, parentFullPathIndex, handlers)return}}// 若该路径的非公共前缀首字符在节点children不存在,就直接添加为节点的子节点n.indices += string(c)child := &node{fullPath: fullPath, path: path, handlers: handlers}n.children = append(n.children, child)n.incrementChildPriority(len(n.indices) - 1)return}// 路径与节点路径完全匹配时,直接覆盖原全路径和处理器n.handlers = handlersn.fullPath = fullPathreturn
}// 增加子节点的优先级
func (n *node) incrementChildPriority(pos int) int {// 增加该子节点的优先级cs := n.childrencs[pos].priority++prio := cs[pos].priority// 重新根据优先级排序newPos := posfor ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]}// 重新组合indicesif newPos != pos {n.indices = n.indices[:newPos] + n.indices[pos:pos+1] + n.indices[newPos:pos] + n.indices[pos+1:]}return newPos
}func (e *engine) addRoute(method, path string, handlers []Handler) {// 1. 根据方法获取对应的前缀树。若root为nil,则初始化root := e.trees.get(method)if root == nil {root = &node{fullPath: "/",}e.trees = append(e.trees, methodTree{method: method,root:   root,})}// 2. 添加路径到前缀树root.addRoute(path, handlers)
}func longestCommonPrefix(a, b string) int {i := 0max := minInt(len(a), len(b))for i < max && a[i] == b[i] {i++}return i
}func minInt(x, y int) int {if x < y {return x}return y
}

匹配

标记为tsr就是在为true情况下并且可以Redirect Trailing Slash,那么就可以重定向到原url附加’/'的地址

前缀树匹配过程如下

  1. 若路径不完全匹配当前节点路径,尝试找到匹配的子节点,并在子节点中继续匹配;若无法找到匹配子节点且无法匹配的字符为’/',那就标记tsr并返回
  2. 若路径完全匹配当前节点路径。在处理器有效的情况下,就返回匹配到的处理器。若无效就尝试找到仅含有’/'路径的子节点,标记为tsr并返回
  3. 若路径完全不匹配当前节点路径,返回空
type nodeValue struct {handlers []Handlertsr      bool
}func (n *node) getValue(path string) *nodeValue {prefix := n.path// 若路径不完全匹配当前节点路径if len(path) > len(prefix) {// 若路径存在该前缀if path[:len(prefix)] == prefix {path = path[len(prefix):]// 尝试找到匹配该路径的子节点idxc := path[0]for i := 0; i < len(n.indices); i++ {if idxc == n.indices[i] {n.children[i].getValue(path)}}// 若无法找到匹配子节点且无法匹配的路径为'/',那么就标记tsrreturn &nodeValue{tsr: path == "/" && n.handlers != nil,}}}// 路径与节点路径完全匹配if path == prefix {// 若节点处理器存在,则返回if n.handlers != nil {return &nodeValue{handlers: n.handlers,}}for i := 0; i < len(n.indices); i++ {// 若存在'/'路径的子节点,则标记tsrif n.indices[i] == '/' {child := n.children[i]return &nodeValue{tsr: len(child.path) == 1 && child.handlers != nil,}}}}return nil
}

前缀树——以Gin路由为例相关推荐

  1. Seq2Seq+前缀树:检索任务新范式(以KgCLUE为例)

    ©PaperWeekly 原创 · 作者 |苏剑林 单位 |追一科技 研究方向 |NLP.神经网络 两年前,在<万能的seq2seq:基于seq2seq的阅读理解问答>和<" ...

  2. golang gin框架源码分析(二)---- 渐入佳境 摸索Engine ServeHTTP访问前缀树真正原理

    文章目录 全系列总结博客链接 前引 golang gin框架源码分析(二)---- 渐入佳境 摸索Engine ServeHTTP访问前缀树真正远原理 1.再列示例代码 从示例代码入手 2.r.Run ...

  3. Trie(前缀树/字典树)及其应用

    from:https://www.cnblogs.com/justinh/p/7716421.html Trie,又经常叫前缀树,字典树等等.它有很多变种,如后缀树,Radix Tree/Trie,P ...

  4. Java 实现 Trie (前缀树)

    LeetCode:https://leetcode-cn.com/problems/implement-trie-prefix-tree/ 什么是前缀树 Trie(发音类似 "try&quo ...

  5. trie树查找前缀串_Trie数据结构(前缀树)

    trie树查找前缀串 by Julia Geist Julia·盖斯特(Julia Geist) A Trie, (also known as a prefix tree) is a special ...

  6. Leetcode 208:实现Trie(前缀树)

    问题描述: 思路简述: 借用一张图 前缀树又叫做单词查找树,是一种树形数据结构,可以用于存储大量的字符串,优点是可以利用不同字符串的公共前缀来节省大量的存储空间,避免了顺序存储字符串列表的冗余,可以看 ...

  7. 【LeetCode】720. 词典中最长的单词 【前缀树】

    题目链接:https://leetcode-cn.com/problems/longest-word-in-dictionary/ 题目描述 给出一个字符串数组words组成的一本英语词典.从中找出最 ...

  8. golang学习笔记(19)-gin路由分组和中间件

    gin路由分组和中间件 这里写目录标题 gin路由分组和中间件 路由分组 中间件 使用中间件 创建中间件 简单中间件应用实验 路由分组 路由分组可以使路由结构更加清晰,更加方便管理路由. 官方演示代码 ...

  9. 算法补天系列之——前缀树+贪心算法

    介绍前缀树 经典的前缀树,字符都是在路上的,节点是为了边的存在而存在的,有路就可以复用. 那么前缀树对应的数据结构是什么样的呢? 对于节点构建结构,设置一个int pass(表示路径途径这个节点的次数 ...

最新文章

  1. 程序员网购18年不拆快递!意外离世后满屋快递被拆开,价值3500万!
  2. linux系统进程的内存布局
  3. pytorch随笔-6
  4. 程序设计囚犯与灯泡 C语言代码,100个囚犯和灯泡的那些事儿(下)
  5. 经典面试题(41):以下代码将输出的结果是什么?
  6. nginx1.8.1反向代理、负载均衡功能的实现
  7. 美国的时间格式 -- 复杂格式的日期转换 使用Date.parse(“复杂的格式”)方法。
  8. C++ 练习题(一:布尔表达式与真值表图文详解)
  9. Yum离线安装(一) --- RPM 打包技术与典型 SPEC 文件分析
  10. 12.15 小程序验证码点击刷新
  11. qt 导出word中插入图片
  12. 当前位置 计算机英语,计算机常用英语词汇,计算机常用英语词汇
  13. Java编程到底是用idea好还是eclipse好?
  14. thingworx学习
  15. C语言实现简单的线程池【转】
  16. Retina、非Retina、点、像素、iPhone分辨率
  17. 如何用ssh隧道绕过防火墙
  18. git 撤销未提交的修改
  19. 保研华东师范计算机专硕,华东师范大学2020年硕士生推免生缺口很大,考研的学子机会来了...
  20. swust oj 996 打印杨辉三角形

热门文章

  1. cesium 相机跟随
  2. 达梦数据库key文件更换
  3. 2022高频面试题之css篇
  4. php zen kaku代表什么,Convert kana one from another (zen-kaku, han-kaku and more) - PHP 7 中文文档...
  5. win8系统保护服务器,Win8整合SmartScreen升级功能保护系统安全
  6. React中的SVG陷阱
  7. 使用吉特哈布Actions对C++代码进行分析
  8. IDEA 启动tomcat 或者springboot 卡住问题
  9. 计算机视觉基础知识复习
  10. 无法定位程序输入点_except1于动态链接库