字典樹原理分析及實現

1.背景

匹配算法的瓶頸之一在於如何判斷字典中是否含有字符串,如果用的是有序集合(TreeMap)的話,複雜度是O(logn),如果用散列表(HashMap),賬面上的時間複雜度雖然下降了,但內存複雜度上去了。我們要尋找一種速度又快,又省內存的數據結構。


2.字典樹概念:

又稱單詞查找樹,Trie樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。(看圖馬上理解)


3.字典樹特點
  • 根節點不包含字符,除根節點外每一個節點都只包含一個字符
  • 從根節點到某一節點,路徑上經過的字符連接起來,爲該節點對應的字符串
  • 每個節點的所有子節點包含的字符都不相同

4.字典樹的實現原理

從確定有限狀態自動機(DFA)的角度來講,每個節點都是一個狀態,狀態表示當前已經查詢到的前綴。從父節點到子節點的移動過程可以看作一次狀態轉移。以下是查詢步驟:

  • 我們輸入一個想要查詢的詞,如果有滿足條件的邊,狀態轉移;如果找不到,直接失敗
  • 完成了全部轉移時,拿到了最後一個字符的狀態,詢問該狀態是否爲終點狀態,如果是則查到了單詞,否則該單詞不在字典中

"刪改改查"都是一回事,以下不再贅述


5.字典樹節點結構

我們爲了方便直接用數組實現

private final int SIZE = 26;    //每個節點能包含的子節點數,即需要SIZE個指針來指向其孩子
private Node root;              //字典樹的根節點

/**
 *  @author: Ragty
 *  @Date: 2020/2/24 21:10
 *  @Description: 字典樹節點
 */
private class Node {
    private boolean status;   //標識該節點是否爲某一字符串終端節點
    private Node[] child;     //該節點的子節點

    public Node() {
        child = new Node[SIZE];
        status = false;
    }
}

6.字典樹的實現
/**
* @Author Ragty
* @Description 初始化一個節點
* @Date   2020/2/24 21:11
*/
public TrieTree() {
    root = new Node();
}



/**
 * @Author Ragty
 * @Description 在字典樹中插入一個單詞
 * @Date   2020/2/23 21:42
 */
public void insert(String word) {
    if (word == null || word.isEmpty()) {
        return;
    }

    Node pNode = this.root;
    for (int i = 0; i < word.length(); i++)
    {
        int index = word.charAt(i) - 'a';
        if (pNode.child[index] == null) {   //如果不存在節點,則新建一個節點插入
            Node tmpNode = new Node();
            pNode.child[index] = tmpNode;
        }
        pNode = pNode.child[index];         //指向下一層
    }
    pNode.status = true;
}



/**
 *  @author: Ragty
 *  @Date: 2020/2/24 21:15
 *  @Description: 檢查字典樹中是否完全包含字符串
 */
public boolean hasStr(String word) {
    Node pNode = this.root;

    //逐個字符去檢查
    for (int i = 0; i < word.length(); i++) {
        int index = word.charAt(i) - 'a';
        //在字典樹中沒有對應的節點,或者word字符串的最後一個字符在字典樹中檢測對應節點的isStr屬性爲false,則返回false
        if (pNode.child[index] == null
                || (i + 1 == word.length() && pNode.child[index].status == false)) {
            return false;
        }
        pNode = pNode.child[index];
    }

    return true;
}



/**
 *  @author: Ragty
 *  @Date: 2020/2/24 21:21
 *  @Description: 先序遍歷
 */
public void preWalk(Node root) {
    Node pNode = root;
    for (int i = 0; i < SIZE; i++) {
        if (pNode.child[i] != null) {
            System.out.print((char) ('a' + i) + "--");
            preWalk(pNode.child[i]);
        }
    }
}



/**
 *  @author: Ragty
 *  @Date: 2020/2/24 21:17
 *  @Description: 返回字典樹的根節點
 */
public Node getRoot() {
    return root;
}

7.測試
public static void main(String[] args) {
    TrieTree trieTree = new TrieTree();
    trieTree.insert("sad");
    trieTree.insert("say");
    trieTree.insert("to");
    trieTree.insert("too");

    System.out.println(trieTree.hasStr("say"));
    System.out.println(trieTree.hasStr("toooo"));

    Node root = trieTree.getRoot();
    trieTree.preWalk(root);

}

8.測試結果
true
false
s--a--d--y--t--o--o--

9.算法分析

當字典大小爲n時,雖然最壞情懷下字典樹的複雜度依然是O(logn)。但它的實際速度比二分查找快,這是因爲隨着路徑的深入,前綴匹配是遞進的過程,算法不必比較字符串的前綴,因此可以節省很多用來比較的時間。


10.算法改進

這裏我們查詢某個詞的時候還需要逐個對比,若我們將對象轉換爲散列值,散列函數輸出區間爲[0,65535]之間的整數,這時候我們直接訪問下標就可以訪問到對應的字符,不過這種做法只適用於第一行,否則會內存指數膨脹,後邊的按數組存放即可,查詢時直接二分法查詢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章