Trie樹詳解

字典樹(Trie)筆記

特別聲明

本文只是一篇筆記類的文章,所以不存在什么抄襲之類的。

以下為我研究時參考過的鏈接(有很多,這里我只列出我記得的):

Trie(字典樹)的應用——查找聯系人

trie樹

Trie樹:應用于統計和排序

牛人源碼,研究代碼來源

1、字典樹的概念

字典樹,因為它的搜索快捷的特性被單詞搜索系統使用,故又稱單詞查找樹。它是一種樹形結構的數據結構。之所以快速,是因為它用空間代替了速度。

2、字典樹的特點:

字典樹有三個基本性質:

1、根節點不包含字符,除根節點外每一個節點都只包含一個字符
2、從根節點到某一個節點,路徑上經過的字符連接起來,就是該節點對應的字符串
3、每個節點的所有子節點包含的字符都不相同。

3、一個包含以下字符串的字典樹結構如下圖所示:

add
adbc
bye
Trie樹.png

4、字典樹的應用場景

1)、字符串的快速查找

給出N個單詞組成的熟詞表,以及一篇全用小寫英文書寫的文章,請你按最早出現的順序寫出所有不在熟詞表中的生詞。
在這道題中,我們可以用數組枚舉,用哈希,用字典樹,先把熟詞建一棵樹,然后讀入文章進行比較,這種方法效率是比較高的。

2)、字典樹在“串”排序方面的應用

給定N個互不相同的僅由一個單詞構成的英文名,讓你將他們按字典序從小到大輸出
用字典樹進行排序,采用數組的方式創建字典樹,這棵樹的每個節點的所有兒子
很顯然地按照其字母大小排序,對這棵樹進行先序遍歷即可。

3)、字典樹在最長公共前綴問題的應用

對所有串建立字典樹,對于兩個串的最長公共前綴的長度即他們所在的節點的公共祖先個數,于是,問題就轉化為最近公共祖先問題。

5、字典樹的數據結構

由以上描述我們可以知道,字典樹的數據結構如下:

class TrieNode {
    char c;
    int occurances;
    Map<Character, TrieNode> children;
}

對以上屬性的描述:

1、c, 保存的是該節點的數據,只能是一個字符(注意是一個)
2、occurances, 從單詞意思應該知道是發生頻率的意思。occurances 是指 當前節點所對應的字符串在字典樹里面出現的次數。
3、children, 就是當前節點的子節點,保存的是它的下一個節點的字符。

7、根據字符串常用的功能,字典樹類要實現的特性

1)查詢是否包含某個字符串
2)查詢某個字符串出現的頻率
3)插入某個字符串
4)刪除某個字符串
5)獲取整個字典樹的規模,即字典樹中包含的不同字符串的個數

基于以上考慮,可以建立一個接口,Trie類只需要實現這個接口即可

8、基于6所描述的特性創建抽象類如下:

public abstract class AbTrie {

    // 判斷字典樹中是否有該字符串。
    public abstract boolean contains(String word);

    // 返回該字符串在字典樹中出現的次數。
    public abstract int frequency(String word);

    // 插入一個字符串。
    public abstract int insert(String word);

    // 刪除一個字符串。
    public abstract boolean remove(String word);

    // 整個字典樹中不同字符串的個數,也就是保存的不同字符串的個數。
    public abstract int size();
}

各個抽象方法的描述已經很詳細的解釋了,這里不再贅述

9、下面講解接口中各個方法的實現原理

1)插入字符串

1、從頭到尾遍歷字符串的每一個字符
2、從根節點開始插入,若該字符存在,那就不用插入新節點,要是不存在,則插入新節點
3、然后順著插入的節點一直按照上述方法插入剩余的節點
4、為了統計每一個字符串出現的次數,應該在最后一個節點插入后occurances++,表示這個字符串出現的次數增加一次

2)刪除一個字符串

1、從root結點的孩子開始(因為每一個字符串的第一個字符肯定在root節點的孩子里),判斷該當前節點是否為空,若為空且沒有到達所要刪除字符串的最后一個字符,則不存在該字符串。若已經到達葉子結點但是并沒有遍歷完整個字符串,說明整個字符串也不存在,例如要刪除的是'harlan1994',而有'harlan'.

2、只有當要刪除的字符串找到時并且最后一個字符正好是葉子節點時才需要刪除,而且任何刪除動作,只能發生在葉子節點。例如要刪除'byebye',但是字典里還有'byebyeha',說明byebye不需要刪除,只需要更改occurances=0即可標志字典里已經不存在'byebye'這個字符串了

3、當遍歷到最后一個字符時,也就是說字典里存在該字符,必須將當前節點的occurances設為0,這樣標志著當前節點代表的這個字符串已經不存在了,而要不要刪除,需要考慮2中所提到的情況,也就是說,只有刪除只發生在葉子節點上。

3)獲取字符串出現的次數

1、我們在設計數據結構的時候就有了一個occurances屬性
2、只需要判斷該字符串是否存在,若存在則返回對應字符下的occurances即可

4)是否存在某個字符串

1、查詢字符串是從第一個字符開始的
2、當查詢的位置已經超過了字符串的長度,比如要查的是“adc”,但是我們查到樹的深度已經超過了c,那么肯定是不存在的
3、如果查詢的位置剛好為字符串的長度,這時,就可以判斷當前節點的符合要求孩子是否存在,若存在,則字符串存在,否則不存在
4、其余情況則需要繼續深入查詢,若符合要求的孩子節點存在,則繼續查詢,否則不存在。

5)整棵Trie樹的大小,即不同字符串的個數

1、返回Trie數據結構中的size屬性即可。
2、size屬性會在insert,remove兩個操作后進行更新

10、代碼實現

1)插入字符串

int insert(String s, int pos) {
        
    //如果插入空串,則直接返回
    //此方法調用時從pos=0開始的遞歸調用,pos指的是插入的第pos個字符
    if (s == null || pos >= s.length())
        return 0;

    // 如果當前節點沒有孩子節點,則new一個
    if (children == null)
        children = new HashMap<Character, TrieNode>();

    //創建一個TrieNode
    char c = s.charAt(pos);
    TrieNode n = children.get(c);

    //確保字符保存在即將要插入的節點中
    if (n == null) {
        n = new TrieNode(c);
        children.put(c, n);
    }

    //插入的結束時直到最后一個字符插入,返回的結果是該字符串出現的次數
    //否則繼續插入下一個字符
    if (pos == s.length() - 1) {
        n.occurances++;
        return n.occurances;
    } else {
        return n.insert(s, pos + 1);
    }
}

2)刪除字符串

boolean remove(String s, int pos) {
    if (children == null || s == null)
        return false;

    //取出第pos個字符,若不存在,則返回false
    char c = s.charAt(pos);
    TrieNode n = children.get(c);
    if (n == null)
        return false;

    //遞歸出口是已經到了字符串的最后一個字符,秀嘎occurances=0,代表已經刪除了
    //否則繼續遞歸到最后一個字符
    boolean ret;
    if (pos == s.length() - 1) {
        int before = n.occurances;
        n.occurances = 0;
        ret = before > 0;
    } else {
        ret = n.remove(s, pos + 1);
    }

    // 1. If we want to remove 'hello', but there is a 'helloa', you do not
    // need to remove the saved chars, because its occurances has been
    // settled
    // 0 witch means the string s no longer exists.
    // 2.its occurances must be 0, for exmaple,
    // when you want to remove 'harlan1994', but there is no such sequence,
    // there is only 'harlan'
    // so when we reach the last char 'n',the pos != s.length() - 1, its
    // occurances can't be
    // settled into 0, and it > 0, so it is not the sequence that need to be
    // removed.
    // if we just removed a leaf, prune upwards.

    //刪除之后,必須刪除不必要的字符
    //比如保存的“Harlan”被刪除了,那么如果n保存在葉子節點,意味著它雖然被標記著不存在了,但是還占著空間
    //所以必須刪除,但是如果“Harlan”刪除了,但是Trie里面還保存這“Harlan1994”,那么久不需要刪除字符了
    if (n.children == null && n.occurances == 0) {
        children.remove(n.c);
        if (children.size() == 0)
            children = null;
    }

    return ret;
}

3)求一個字符串出現的次數

TrieNode lookup(String s, int pos) {
    if (s == null)
        return null;

    //如果找的次數已經超過了字符的長度,說明,已經遞歸到超過字符串的深度了,表明字符串不存在
    if (pos >= s.length() || children == null)
        return null;

    //如果剛好到了字符串最后一個,則只需要返回最后一個字符對應的結點,若節點為空,則表明不存在該字符串
    else if (pos == s.length() - 1)
        return children.get(s.charAt(pos));

    //否則繼續遞歸查詢下去,直到沒有孩子節點了
    else {
        TrieNode n = children.get(s.charAt(pos));
        return n == null ? null : n.lookup(s, pos + 1);
    }
}

以上kookup方法返回值是一個TrieNode,要找某個字符串出現的次數,只需要看其中的n.occurances即可。
要看是否包含某個字符串,只需要看是否為空節點即可。

11、下面來一個應用,題目如下:

不考慮字母大小寫,在一篇文章中只有英文,不包含其余任何字符,求這篇文章中不同單詞的個數。
并求所給單詞的出現次數。

1)建立一個測試類Sample,添加兩個方法分別求以上兩個問題
2)添加一個求取文件內容,并添加字符串到字典樹中的方法,關鍵代碼如下:

    ...

    private void init() {
        try {
            InputStream in = new FileInputStream(new File(
                    "E:\\Eclipse\\trie\\src\\com\\harlan\\trie\\bible.txt"));
            addToDictionary(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void addToDictionary(InputStream f) throws IOException,
            FileNotFoundException {
        long t = System.currentTimeMillis();
        final int bufSize = 1000;
        int read;
        int numWords = 0;
        InputStreamReader fr = null;
        try {
            fr = new InputStreamReader(f);
            char[] buf = new char[bufSize];
            while ((read = fr.read(buf)) != -1) {
                // TODO modify this split regex to actually be useful
                String[] words = new String(buf, 0, read).split("\\W");
                for (String s : words) {
                    mTrie.insert(s);
                    numWords++;
                }
            }
        } finally {
            if (fr != null) {
                try {
                    fr.close();
                } catch (IOException e) {
                }
            }
        }
        System.out.println("Read from file and inserted " + numWords
                + " words into trie in " + (System.currentTimeMillis() - t)
                / 1000.0 + " seconds.");
    }

    public int getSize() {
        if (mTrie != null) {
            return mTrie.size();
        }
        return 0;
    }

    public int getCount(String s) {
        return mTrie.frequency(s);
    }

測試結果截圖如下:

測試.png

源碼鏈接:https://github.com/Harlan1994/Trie

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容