數據結構系列——Trie樹

Trie樹,即字典樹,又稱單詞查找樹或鍵樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計和排序大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。它的優點是:最大限度地減少無謂的字符串比較,查詢效率比哈希表高。

Trie樹結構

Trie的核心思想是空間換時間:利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的。

舉例:將tea,ten,to,in,inn,int幾個單詞構建成一個Trie樹,看一下具體的Tried樹的結構:

圖:Trie樹結構
Trie樹結構

從圖中可以看出Trie樹的某些特性:

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

Trie樹的插入、刪除、查找的操作都是一樣的,只需要簡單的對樹進行一遍遍歷即可,時間複雜度:O(n)(n是字符串的長度)。
對於Tried樹的實現可以使用數組和鏈表兩種方式:

  • 數組:由於我們知道一個Tried樹節點的子節點的數量是固定26個(針對不同情況會不同,比如兼容數字,則是36等),所以可以使用固定長度的數組來保存節點的子節點
    • 優點:在對子節點進行查找時速度快
    • 缺點:浪費空間,不管子節點有多少個,總是需要分配26個空間
  • 鏈表:使用鏈表的話我們需要在每個子節點中保存其兄弟節點的鏈接,當我們在一個節點的子節點中查找是否存在一個字符時,需要先找到其子節點,然後順着子節點的鏈表從左往右進行遍歷
    • 優點:節省空間,有多少個子節點就佔用多少空間,不會造成空間浪費
    • 缺點:對子節點進行查找相對較慢,需要進行鏈表遍歷,同時實現也較數組麻煩

下面是最簡單的Trie樹的實現,採用數組的方式。

/**
 * <p>
 * 最簡單的Trie樹結構,僅表示出Trie樹的結構,實際應用需進行擴展
 * </p>
 *
 * @author Vicky
 * @email [email protected]
 * @2015年11月23日
 *
 */
public class Trie {
    protected TrieNode root = new TrieNode('a');// TrieTree的根節點

    /**
     * 插入
     * 
     * @param word
     */
    public void insertWord(String word) {
        TrieNode index = this.root;
        for (char c : word.toLowerCase().toCharArray()) {
            index = index.addChild(c);
        }
        return;
    }

    /**
     * TrieTree的節點
     */
    private class TrieNode {
        /** 該節點的字符 */
        private final char nodeChar;//
        /** 一個TrieTree的節點的子節點 */
        private TrieNode[] childNodes = null;

        public TrieNode(char nodeChar) {
            super();
            this.nodeChar = nodeChar;
        }

        public TrieNode addChild(char ch) {
            int index = ch - 'a';
            if (null == childNodes) {
                this.childNodes = new TrieNode[26];
            }
            if (null == childNodes[index]) {
                childNodes[index] = new TrieNode(ch);
            }
            return childNodes[index];
        }

        @Override
        public String toString() {
            return "TrieNode [nodeChar=" + nodeChar + "]";
        }

    }

    public static void main(String[] args) {
        Trie trie = new Trie();
        trie.insertWord("Vicky");
    }
}
Trie樹應用

Trie樹的應用主要是集中於字符串的處理。
(1)字符串檢索

  • 精確查找:給定一組字符串,查找某個字符串是否出現過
  • 前綴匹配:給定一組字符串,查找以某個字符串爲前綴的字符串集合

(2)最長公共前綴

  • 查找一組字符串的最長公共前綴,只需要將這組字符串構建成Trie樹,然後從跟節點開始遍歷,直到出現多個節點爲止(即出現分叉)

(3)排序

  • 將一組字符串按照字典序進行排序,只需構建成Trie樹,然後按照先序遍歷即可

。。。

下面我們針對Trie樹的應用,選擇一個相對簡單且有代表性的案例:從一組單詞中查找所有以“vi”開頭的字符串,同時查找“Vicky”是否出現過。(由於我們只保存26個字符,所以不區分大小寫,也不支持非單詞字符)。

從網上download下一個單詞表,根據單詞表構建Trie樹,並進行查找,單詞表可以從https://github.com/dwyl/english-words進行下載。

package com.vicky.datastructure.tree.trie;

/**
 * <p>
 * 一個支持前綴查找以及精確查找的Trie樹
 * </p>
 *
 * @author Vicky
 * @email [email protected]
 * @2015年11月23日
 *
 */
public class PrefixTrie {
    private TrieNode root = new TrieNode('a');// TrieTree的根節點

    /**
     * 插入
     * 
     * @param word
     */
    public void insertWord(String word) {
        TrieNode index = this.root;
        for (char c : word.toLowerCase().toCharArray()) {
            index = index.addChild(c);
            index.addPrefixCount();
        }
        index.addCount();
        return;
    }

    /**
     * 查找
     * 
     * @param word
     * @return
     */
    public boolean selectWord(String word) {
        TrieNode index = this.root;
        for (char c : word.toLowerCase().toCharArray()) {
            index = index.getChild(c);
            if (null == index) {
                return false;
            }
        }
        return index.getCount() > 0;
    }

    /**
     * 查找前綴出現的次數
     * 
     * @param prefix
     * @return
     */
    public int selectPrefixCount(String prefix) {
        TrieNode index = this.root;
        for (char c : prefix.toLowerCase().toCharArray()) {
            index = index.getChild(c);
            if (null == index) {
                return 0;
            }
        }
        return index.getPrefixCount();
    }

    /**
     * TrieTree的節點
     */
    private class TrieNode {
        /** 該節點的字符 */
        private final char nodeChar;//
        /** 一個TrieTree的節點的子節點 */
        private TrieNode[] childNodes = null;
        private int count = 0;// 單詞數量,用於判斷一個單詞是否存在
        private int prefixCount = 0;// 前綴數量,用於查找該前綴出現的次數

        public TrieNode(char nodeChar) {
            super();
            this.nodeChar = nodeChar;
        }

        public TrieNode addChild(char ch) {
            int index = ch - 'a';
            if (null == childNodes) {
                this.childNodes = new TrieNode[26];
            }
            if (null == childNodes[index]) {
                childNodes[index] = new TrieNode(ch);
            }
            return childNodes[index];
        }

        public TrieNode getChild(char ch) {
            int index = ch - 'a';
            if (null == childNodes || null == childNodes[index]) {
                return null;
            }
            return childNodes[index];
        }

        public void addCount() {
            this.count++;
        }

        public int getCount() {
            return this.count;
        }

        public void addPrefixCount() {
            this.prefixCount++;
        }

        public int getPrefixCount() {
            return this.prefixCount;
        }

        @Override
        public String toString() {
            return "TrieNode [nodeChar=" + nodeChar + "]";
        }

    }
}

package com.vicky.datastructure.tree.trie;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.regex.Pattern;

import org.junit.Before;
import org.junit.Test;

public class TrieUsedTest {
    private PrefixTrie trie;

    @Before
    public void before() throws IOException {
        Pattern pattern = Pattern.compile("[a-zA-Z]+");

        // 從文件中讀取單詞,構建TriedTree
        InputStreamReader read = new InputStreamReader(this.getClass().getResourceAsStream("words.txt"));
        BufferedReader reader = new BufferedReader(read);
        trie = new PrefixTrie();
        String line = null;
        while (null != (line = reader.readLine())) {
            line = line.trim();
            if (!pattern.matcher(line).matches()) {// 去除非法單詞,如包含“-”
                continue;
            }
            trie.insertWord(line);
        }
    }

    /**
     * 測試使用TriedTree搜索前綴出現的次數
     */
    @Test
    public void searchPrefixWords() {
        String prefix = "vi";
        System.out.println(trie.selectPrefixCount(prefix));
        System.out.println(trie.selectWord("Vicky"));
    }
}

代碼中爲了支持精確查找和前綴查找兩種方式,我們對TrieNode進行修改,增加了private int count = 0;private int prefixCount = 0;兩個變量,分別用於保存單詞出現的次數,以及前綴出現的次數。測試結果可通過使用文本編輯器進行查找對比。

更多關於Trie樹的應用場景可閱讀參考文章1。

參考文章

數據結構之Trie樹
從Trie樹(字典樹)談到後綴樹(10.28修訂)
6天通喫樹結構—— 第五天 Trie樹


歡迎訪問我的個人博客,尋找更多樂趣~

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