前綴樹算法實現路由匹配原理解析


路由功能是web框架中一個很重要的功能,它將不同的請求轉發給不同的函數(handler)處理,很容易能想到,我們可以用一個字典保存它們之間的對應關系,字典的key存放path,value存放handler。當一個請求過來后,使用 routers.get(path, None) 就可以找到對應的handler。

利用字典實現路由可以參考我的這篇文章:動手實現web框架 。

使用字典有一個問題,不支持動態路由。如果路由像這樣呢?

/hello/:name/profile

name前面是通配符: ,表示這是個動態的值。一個解決辦法是使用前綴樹trie。

前綴樹

leetcode中有這個算法, 點這里 查看。

前綴樹前綴樹,首先是一棵樹。不同的是樹中一個節點的所有子孫都有相同的前綴。前綴樹將單詞中的每個字母依次插入樹中,插入前首先確認該單詞是否存在,不存在才創建新節點,如果一個單詞已經全部插入,則將末尾單詞設置為標志位。

type Node struct {
    isWord bool // 是否是單詞結尾
    next   map[string]*Node // 子節點
}

type Trie struct {
    root *Node
}

以單詞leetcode,leetd和code為例,首先一次插入leetcode中的每個單詞,然后插入leetd的時候,leet在樹中已經存在,跳過往下,現在要插入字母d,不存在,所以新建節點插入樹中,並將該節點的isWord置位true,表明到了單詞末尾。

最終插入結果為:

前綴樹

func (this *Trie) Insert(word string) {
    cur := this.root
    for _, w := range []rune(word) {
        c := string(w)
        if cur.next[c] == nil {
            cur.next[c] = &Node{next: make(map[string]*Node)}
        }
        cur = cur.next[c]
    }
    cur.isWord = true
}

那么,當我們要搜索單詞leetd的時候,從根節點開始查找,如果找到某條路徑是leetd,並且末尾的d是單詞標志位,則表示搜索成功。

func (this *Trie) Search(word string) bool {
    cur := this.root
    for _, w := range []rune(word) {
        c := string(w)
        if cur.next[c] == nil {
            return false
        }
        cur = cur.next[c]
    }
    return cur.isWord
}

明白了前綴樹的原理,我們來看看路由匹配是如何利用前綴樹來實現的。

路由前綴樹

go語言中gin框架的路由實現就是利用前綴樹,可以看看它的源代碼是如何實現的。

考慮一下,路由或者說路徑的特點,是以 / 分隔的單詞組成的,那我們將 / 的每一部分掛靠在前綴樹上就可以了。如下圖所示:

還有一點需要考慮,我們在用web框架定義路由的時候,常見的做法是根據不同的HTTP方法來定義。比如:

// 以go語言gin框架為例
g := gin.New()
g.GET("/hello", Hello)
g.POST("/form", Form)

對於同一個路徑,可能有多個方法支持。所以我們要以不同HTTP方法為樹根創建前綴樹。當一個GET請求過來的時候,就從GET樹上搜索,POST請求就從POST樹上搜索。

路由樹

除了為不同的HTTP方法定義樹之外,還要給那些是通配符的節點增加一個標志位。所以,我們的路由前綴樹結構看起來像這樣:

type node struct {
    path     string           // 路由路徑
    part     string           // 路由中由'/'分隔的部分
    children map[string]*node // 子節點
    isWild   bool             // 是否是通配符節點
}

type router struct {
    root  map[string]*node       // 路由樹根節點
    route map[string]HandlerFunc // 路由處理handler
}

依照上面的前綴樹算法的實現,照葫蘆畫瓢,我們可以寫出插入路由和搜索路由的方法:

// addRoute 綁定路由到handler
func (r *router) addRoute(method, path string, handler HandlerFunc) {
    parts := parsePath(path)
    if _, ok := r.root[method]; !ok {
        r.root[method] = &node{children: make(map[string]*node)}
    }
    root := r.root[method]
    key := method + "-" + path
    // 將parts插入到路由樹
    for _, part := range parts {
        if root.children[part] == nil {
            root.children[part] = &node{
                part:     part,
                children: make(map[string]*node),
                isWild:   part[0] == ':' || part[0] == '*'}
        }
        root = root.children[part]
    }
    root.path = path
    // 綁定路由和handler
    r.route[key] = handler
}

// getRoute 獲取路由樹節點以及路由變量
func (r *router) getRoute(method, path string) (node *node, params map[string]string) {
    params = map[string]string{}
    searchParts := parsePath(path)

    // get method trie
    var ok bool
    if node, ok = r.root[method]; !ok {
        return nil, nil
    }

    // 在該方法的路由樹上查找該路徑
    for i, part := range searchParts {
        var temp string
        // 查找child是否等於part
        for _, child := range node.children {
            if child.part == part || child.isWild {
                // 添加參數
                if child.part[0] == '*' {
                    params[child.part[1:]] = strings.Join(searchParts[i:], "/")
                }
                if child.part[0] == ':' {
                    params[child.part[1:]] = part
                }
                temp = child.part
            }

        }
        // 遇到通配符*,直接返回
        if temp[0] == '*' {
            return node.children[temp], params
        }
        node = node.children[temp]

    }

    return

}

上面的代碼是我自己實現的一個web框架 gaga 中路由前綴樹相關的代碼,有需要的可以去看看源代碼。另外,歡迎star 呀。

其中的 addRoute 用來將路由插入到對應method的路由樹中,如果節點是通配符,將其設置為 isWild , 同時綁定路由和handler方法。

getRoute 方法首先查找路由方法對應的路由前綴樹,然后在樹中查找是否存在該路徑。

總結

前綴樹trie算法不光可以用在路由的實現上,搜索引擎中自動補全的實現,拼寫檢查等等都是用trie實現的。trie樹查找的時間和空間復雜度都是線性的,效率很高,很適合路由這種場景使用。

路由的實現上,go語言中 httpRouter 這個庫除了使用前綴樹之外,還加入了優先級,有興趣的可以看看它的源碼了解下。

 

參考: https://studygolang.com/articles/26324


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM