1. 程式人生 > >資料結構:字典樹的基本使用

資料結構:字典樹的基本使用

概述:

  說來也奇怪,最近碰到的很多問題都需要用字典樹來解決,索性就來研究一番。在這篇部落格中,我會通過一些例項來講解一下字典樹的一些基本使用。例如:建立、新增、查詢、按字典序排序、按數值大小進行排序(對於一些數值序列的排序)等等。

關於字典的實際應用例項,請參見本人的另一篇部落格:《演算法:兩種對拼音進行智慧切分的方法

基本使用:

0.要點說明:

  為了便於以下對字典樹的說明,我這裡的節點Node可能會有一些對於讀者而並不必要的成員。例如,fre, visited, minLength, prefixCount等等。這裡讀者可以根據自己的需求自行增減。

String name; // 結點的字元名稱
    int fre; // 單詞的詞頻
    boolean end; // 是否是單詞結尾
    boolean root; // 是否是根結點
    Node[] children; // 子節點資訊
    boolean visited; // 是否已經遍歷過了
    int minLength; // 通過該節點的最小的數字長度
    int prefixCount = 0; // 有多少單詞通過這個節點,即節點字元出現的次數
    Node parent; // 當前節點的父節點

1.建立一棵新的字典樹

  對於建立一棵空的字典樹,其實相對來說是比較容易的。因為,我們不需要對樹進行一些元素新增或是移除。我們只是對字典樹中的一些必要的成員進行了一些初始化的工作。下面是程式碼部分:

Node root;
    int depth;
    public TrieTree(String name) {
        root = new Node(name);
        root.setFre(0);
        depth = 0;
        root.setEnd(false);
        root.setRoot(true);
    }

2.插入一個新的節點元素

  對於在字典樹的新增元素的關鍵地方,應該就是我們要在何是停止,即新增完成的條件是什麼?

  我們元素新增完成的條件是,我們在對新增的元素(如:"12345")進行遍歷,直到遍歷到字串的末尾。

  在我們對新增的字串str進行插入字典樹的過程中,比如說已經遍歷到位置i,如果str.chartAt(i)在字典樹中已經存在,則我們可以直接pass,繼續遍歷i+1的位置;如果str.chartAt(i)在字典樹中是不存在的,那麼我們就必須新增此節點,再將新增的此節點掛載到上一個點的後面,然後繼續遍歷i+1位置上元素str.chartAt(i+1)。然後在插入的字串最後的位置上設定此節點為結束節點(即在此位置可以構成單詞,或是完整新增的數字)。具體程式碼如下:

public void insert(String number) {
        Node node = root;
        char[] numberCells = number.toCharArray();
        for (int i = 0; i < numberCells.length; i++) {
            int num = Integer.parseInt(String.valueOf(numberCells[i]));
            if (node.getChildren()[num] != null) {
                if (numberCells.length < node.getChildren()[num].getMinLength()) {
                    node.getChildren()[num].setMinLength(numberCells.length);
                }
                if (i == numberCells.length - 1) {
                    Node endNode = node.getChildren()[num];
                    endNode.setFre(endNode.getFre() + 1);
                    endNode.setEnd(true);
                }
                node.getChildren()[num].prefixCountIncrement();
            } else {
                Node newNode = new Node(numberCells[i] + "");
                newNode.setParent(node);
                if (i == numberCells.length - 1) {
                    newNode.setFre(1);
                    newNode.setEnd(true);
                    newNode.setRoot(false);
                }
                newNode.setMinLength(numberCells.length);
                node.getChildren()[num] = newNode;
                depth = Math.max(i + 1, depth);
            }
            
            node = node.getChildren()[num];
        }
    }

3.返回Trie中某一節點被新增的次數

  此功能的應用點在於,詞頻統計。我們在每次新增一個元素時都會在原來的基本上,對詞頻進行自增處理。如果新增的詞在之前的字典樹中是不存在的,就設定初始值為1,如果原本有這個節點,就在原來的詞頻上+1.在上一步(插入一個新的節點元素)中可以看到具體操作。那麼這裡介紹一下查詢詞頻的操作。程式碼如下:

public int searchFre(String number) {
        int fre = -1;
        
        Node node = root;
        char[] numberCells = number.toCharArray();
        for (int i = 0; i < numberCells.length; i++) {
            int num = Integer.parseInt(String.valueOf(numberCells[i]));
            if (node.getChildren()[num] != null) {
                node = node.getChildren()[num];
                fre = node.getFre();
            } else {
                fre = -1;
                break;
            }
        }
        
        return fre;
    }

4.計算有多少個單詞以prefix為字首

  對於字首統計的操作,我們也需要在插入的過程中進行統計。這是因為,如果我們在插入的時候不進行統計,那麼我們就必須在每次查詢一個字首的時候,去遍歷字首結束節點以下的所有子節點。這樣勢必會增加時間上的複雜度,是一種不理想的方式。不過,因為有時,我們並不會只是要求計算有多少以prefix為字首的串。所以,可能遍歷是在所難免。還是要看需求吧。以下程式碼是查詢過程:

public int countPrefix(String prefix) {
        if (prefix == null || prefix.length() == 0) {
            return -1;
        }
        Node node = root;
        char[] letters = prefix.toCharArray();
        for (int i = 0; i < prefix.length(); i++) {
            if (node.getChildren()[Integer.parseInt(String.valueOf(letters[i]))] == null) {
                return 0;
            } else {
                node = node.getChildren()[Integer.parseInt(String.valueOf(letters[i]))];
            }
        }
        
        return node.getPrefixCount();
    }

5.獲得trie的深度

  對於樹深度的問題,對於其實際的應用點,我目前還未知曉。只是在寫其他功能的時候想到了,就附帶了吧。

  這個深度,也是要在新增節點的時候去實時更新的。這樣可以減小查詢時的時間複雜度。查詢程式碼如下:

public int depth() {
        return depth;
    }
It's too easy, isn't it?

6.對字典樹進行字典序排序(即深度優先搜尋)

  就以我們的數字字典樹為例。因為我們在構造樹的過程就是一個以字典序為基礎的過程,所以我們的遍歷就可以直接對樹進行順序遍歷就Ok。對於字典樹而言,順序遍歷的過程,其實就是對樹的深度遍歷。如果大家還記得深度遍歷的過程,相信大家可以很容易地寫出此程式碼。我的編碼過程如下(使用了遞迴):

public void dictOrder(Node node, String prefix) {
        if (node != null) {
            if (node.isEnd()) {
                System.out.println(prefix + node.getName());
            }
            for (Node children : node.getChildren()) {
                if (children == null) {
                    continue;
                }
                
                dictOrder(children, prefix + (node.isRoot() ? "" : node.getName()));
            }
        }
    }

7.對數字字典樹按實際數值大小排序(即廣度優先搜尋)

  在第6步中,我們看到對樹深度優先搜尋的過程就是對樹進行按字典序排序。那麼可能你也會問另一個問題,那麼是廣度優先搜尋又會是怎麼樣的結果呢?廣度優先搜尋的另一個叫法,我們可以說是對樹的分層遍歷。既然是對樹進行分層,那麼就是說"123"要排在"1234"的前面。而在第6步中我們也說到了,字典樹本身就是以字典序為基礎進行新增。也就是"123"必然是在"124"的前面。Ok,基於這樣的分析,我們可以得到一個很容易理解的結論:對字典樹進行廣度優先搜尋的過程就是對字典樹進行按數值大小進行排序。具體的實現程式碼如下:

/**
     * 對數字字典樹按實際數值大小排序(即分層列印)
     * TrieTree
     */
    public void sortNumberOrder(Node node, String prefix) {
        Queue<Node> queuing = new LinkedList<Node>();
        queuing.offer(node);
        
        while (!queuing.isEmpty()) {
            Node currentNode = queuing.poll();
            if (currentNode.isEnd()) {
                System.out.println(getNodePath(currentNode));
            }
            Node[] children = currentNode.getChildren();
            for (Node sonNode : children) {
                if (sonNode != null) {
                    queuing.offer(sonNode);
                }
            }
        }
    }
    
    /**
     * 獲得某一節點的上層節點,即字首字串
     * @param node
     * @return
     */
    public String getNodePath(Node node) {
        StringBuffer path = new StringBuffer();
        Node currentNode = node;
        while (currentNode.getParent() != null) {
            path.append(currentNode.getName());
            currentNode = currentNode.getParent();
        }
        
        return path.reverse().toString();
    }

原始碼下載: