算法:用Java實現trie樹,並用於實現敏感詞過濾的功能

trie樹,可能大家都比較陌生。trie樹,又叫字典樹,是一種樹形結構,一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。它的優點是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,查詢效率比哈希樹高。

trie樹有3個基本性質:

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

我們今天準備用Java來實現trie樹,然後利用其高效的搜索功能,搜索句子裏面的敏感詞,並將敏感詞替換爲*。trie樹的搜索原理如下:

  1. 從根結點開始一次搜索
  2. 取得要查找關鍵詞的第一個字母,並根據該字母選擇對應的子樹並轉到該子樹繼續進行檢索
  3. 在相應的子樹上,取得要查找關鍵詞的第二個字母,並進一步選擇對應的子樹進行檢索
  4. 迭代上述過程……
  5. 在某個結點處,關鍵詞的所有字母已被取出,則讀取附在該結點上的信息,即完成查找
  6. 而敏感詞比對的話,如果比對過程中某一個字母在子樹中不存在,那麼第一個字母開頭的敏感詞就不存在,就從第一個字母的下一個字母爲首繼續找
  7. 如果句子結束而trie樹還沒查找完畢,也算是沒找到
  8. 如果某個字母已經找到了trie樹的葉結點了,那麼就找到了一個敏感詞,就把第一個找的字母和最後一個字母全部替換爲*,完成過濾功能

此外一個問題,那就是一個敏感詞是另一個敏感詞的前綴的問題。比如說有三個敏感詞:hello、hell、he。假設這三個敏感詞加入到trie樹,trie樹的最終效果就是隻增加了hello這一個單詞,也就只會過濾hello,hell和he就無法過濾了。

我的實現原理,就是每一個結點新增一個布爾類型的屬性,是否是結束字母(isEndLetter)。也就是在敏感詞加入到trie樹的過程中,加到了最後一個字母,那最後一個字母的結點,該布爾屬性設置爲true,不管是在沒有的樹枝上新增結點,還是在已有的樹枝上走,反正最後一個字母的結點(創建的或者是找到的),都設置該布爾類型爲true。當敏感詞過濾的時候,設置一個變量爲敏感詞查找的檢查點checkpoint,如果在某一個樹枝上找到了某個結點爲true,則記錄下來,後面又找到就再更新記錄(保證是最長的敏感詞)。如果找到樹的葉子結點,找到了全樹枝單詞,那麼全樹枝單詞就是敏感詞。如果沒有找到葉子結點下一個單詞就不對了,那麼就以上一個檢查點(當前checkpoint)找到的敏感詞爲準。

本次Java實現trie樹,根節點爲 ‘/’ ,測試的敏感詞爲:hello、hell、he、hero、see、sun、right(注意,敏感詞需要不包含 * 號,否則會拋出非法參數異常)。加上每個結點的isEndLetter的布爾值,畫成trie樹,就是下面的樣子:
在這裏插入圖片描述
接下來上代碼,我用Java編寫了TrieTree類,可以實現添加敏感詞,以及根據添加的敏感詞過濾句子,返回過濾後的句子,其中敏感詞的字母都會被替換成*號:

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @Author: LiYang
 * @Date: 2020/2/19 20:43
 * @Description: trie樹(字典樹)做敏感詞過濾
 */
public class TrieTree {

    /**
     * trie樹的結點類(TrieTree的靜態內部類)
     */
    private static class TrieNode {

        //當前結點的字母
        private char letter;

        //當前節點字母是否可以作爲結束字母
        private boolean isEndLetter = false;

        //當前結點的下一結點集合
        private Set<TrieNode> next;

        /**
         * 空構造方法
         */
        public TrieNode() {

        }

        /**
         * 入參letter的構造方法
         * @param letter
         */
        public TrieNode(char letter) {
            this.letter = letter;

            //爲每一個結點創建一個空的next集合
            this.next = new HashSet<>();
        }

        /**
         * 重寫equals方法,結點字母相等,結點即相等
         * @param obj
         * @return
         */
        @Override
        public boolean equals(Object obj) {
            //如果不是同一類,則絕對不等
            if (!(obj instanceof TrieNode)) {

                //返回不等
                return false;
            }

            //字母相等,則結點相等,否則不等
            return this.letter == ((TrieNode)obj).letter;
        }

        /**
         * 重寫了equals方法,就得跟着重寫hashCode方法
         * @return
         */
        @Override
        public int hashCode() {
            //以letter的數值代表hashcode
            return this.letter;
        }

        /**
         * 查找某字母的下級字母結點
         * @param letter 查找的下一個字母
         * @return 下一個字母的結點
         */
        public TrieNode findNext(char letter) {
            //遍歷查找
            for (TrieNode node : next) {

                //如果找到了
                if (node.letter == letter) {

                    //返回該結點
                    return node;
                }
            }

            //沒找到,返回null
            return null;
        }

    }


    /************** 接下來是TrieTree類的屬性和方法 **************/

    //trie樹的根結點,默認爲'/'
    private TrieNode root = new TrieNode('/');

    //trie樹的公共後綴(句子尾部的處理需要)
    private static final String PUBLIC_SUFFIX = "********";

    /**
     * 空構造方法
     */
    public TrieTree() {

    }

    /**
     * 帶初始敏感詞數組的構造方法
     * @param words
     */
    public TrieTree(String[] words) {
        //遍歷敏感詞數組
        for (int i = 0; i < words.length; i++) {

            //加入初始敏感詞
            addWord(words[i]);
        }
    }

    /**
     * 加入需要過濾的敏感詞(敏感詞不能帶*號)
     * @param word 待加入的敏感詞
     */
    public void addWord(String word) {
        //驗證敏感詞裏面是否帶有*號(否則程序可能會出錯)
        if (word.contains("*")) {

            //拋出非法參數異常
            throw new IllegalArgumentException("敏感詞中不能含有*號");
        }

        //將敏感詞轉化爲字符數組
        char[] charArray = word.toCharArray();

        //當前trie樹結點爲根結點
        TrieNode currentNode = root;

        //遍歷敏感詞每一個字母
        for (int i = 0; i < charArray.length; i++) {

            //在當前trie樹結點的下一個字母結點集合裏,尋找當前字母結點
            TrieNode nextNode = currentNode.findNext(charArray[i]);

            //如果當前字母存在
            if (nextNode != null) {

                //找到的當前字母結點作爲當前trie樹結點
                //接下來在當前字母結點的下一個字母集合裏找下一個字母
                currentNode = nextNode;

            //如果當前字母不存在
            } else {

                //爲當前字母創造trie樹結點
                TrieNode trieNode = new TrieNode(charArray[i]);

                //將當前字母創造的trie樹結點,加入到當前結點的下一個結點集合裏
                currentNode.next.add(trieNode);

                //以當前字母創造的trie樹結點作爲當前結點,繼續找下一個字母
                //有就繼續看下一個字母,沒有就創建,直到創建完整敏感詞樹枝
                currentNode = trieNode;
            }
        }

        //將最後一個字母,設置爲結束字母
        currentNode.isEndLetter = true;
    }

    /**
     * 過濾敏感詞(用當前TrieTree類的敏感詞庫來過濾)
     * @param sentence 待過濾的原始句子
     * @return 敏感詞字母被替換爲*的句子
     */
    public String filtrate(String sentence) {
        //要處理的句子,統一加上公共後綴(句子尾部的處理需要)
        sentence = sentence + PUBLIC_SUFFIX;

        //將原始句子轉化爲字符數組
        char[] charArray = sentence.toCharArray();

        //過濾後的句子的字符列表
        List<Character> filtrated = new ArrayList<>();

        //遍歷原始句子字符數組,從每一個字母開始向右同步走一遍trie樹
        for (int i = 0; i < charArray.length; i++) {

            //字母和trie樹的比對指標,就是同步比對到第幾個字母了
            int index = i;

            //結束字母比對檢查點,記錄找到的從起始字母
            //到所有結束字母中最大長度的結束字母的下標
            int checkpoint = -1;

            //當前比對的trie樹結點,用它的下一字母集合比對,初始化爲root
            TrieNode currentNode = root;

            //持續比對
            while (true) {

                //如果比對的字母已經超過了原始句子的字符數組邊界
                if (index > charArray.length - 1) {

                    //停止比對
                    break;
                }

                //當前trie樹結點的下一字母集合的元素個數
                int nextSize = currentNode.next.size();

                //將當前字母在當前trie樹結點的下一字母集合中尋找
                //是否存在當前字母的trie樹結點
                currentNode = currentNode.findNext(charArray[index]);

                //如果在trie樹上找到了
                if (currentNode != null) {

                    //如果是結束字母
                    if (currentNode.isEndLetter) {

                        //更新結束字母檢查點
                        checkpoint = index;
                    }

                    //指標+1,也就是繼續拿下一個字母
                    //和下一個trie樹結點繼續比對
                    index ++;

                //如果trie樹上沒找到,且還沒找到trie樹的盡頭,即葉結點
                } else if (nextSize > 0) {

                    //如果檢查點不爲初始值-1,也就是存在前綴敏感詞
                    if (checkpoint != -1) {

                        //首先我們得到前綴敏感詞的長度
                        int prefixWordLength = checkpoint - i + 1;

                        //遍歷敏感詞長度的次數
                        for (int j = 0; j < prefixWordLength; j++) {

                            //加入等長的*符號
                            filtrated.add('*');
                        }

                        //將原始句子的遍歷指標置爲前綴
                        //敏感詞最後一個字母的位置
                        i = checkpoint;

                    //如果檢查點爲i,也就是不存在前綴敏感詞
                    } else {

                        //說明以原始句子字母開頭的所有詞中不存在敏感詞
                        //將該字母原樣裝入到過濾後的句子的字符列表
                        filtrated.add(charArray[i]);
                    }

                    //結束比對
                    break;

                //如果trie樹上沒找到,且找到了trie樹的盡頭,即葉結點
                //那就是找到了以原始句子字母開頭的敏感詞
                //我們的處理方式是用 "*" 代替敏感詞的字母
                } else {

                    //首先,我們得到敏感詞的長度
                    int wordLength = index - i;

                    //遍歷敏感詞長度的次數
                    for (int j = 0; j < wordLength; j++) {

                        //加入等長的*符號
                        filtrated.add('*');
                    }

                    //將原始句子的遍歷指標置爲
                    //敏感詞最後一個字母的位置
                    i = index - 1;

                    //結束比對
                    break;
                }
            }
        }
        //至此,過濾後的句子的字符列表已創建完畢

        //創建StringBuffer對象
        StringBuffer sbuf = new StringBuffer();

        //遍歷過濾後的句子字符列表
        for (Character character : filtrated) {

            //將過濾後句子的字符拼接
            sbuf.append(character);
        }

        //返回原始句子過濾後的句子(截取尾部公共後綴前的句子)
        return sbuf.substring(0, sbuf.length() - PUBLIC_SUFFIX.length());
    }


    /**
     * 測試trie樹的敏感詞過濾功能
     * @param args
     */
    public static void main(String[] args) {
        //敏感詞字符串數組
        String[] words = {"hello", "hell", "he", "hero", "see", "sun"};

        //額外加入的敏感詞
        String extraWord = "right";

        //創建TrieTree實例,初始化加入words的六個敏感詞
        TrieTree trieTree = new TrieTree(words);

        //繼續加入第七個敏感詞
        trieTree.addWord(extraWord);

        //過濾前的句子
        String sentence = "hellofsxhelllgfeheldxghergasgherohgseevsunfright";

        //打印過濾前的句子
        System.out.println("過濾前:" + sentence);

        //調用TrieTree示例的方法,將句子進行敏感詞過濾
        String filtrated = trieTree.filtrate(sentence);

        //打印過濾後的句子,用作比對
        System.out.println("過濾後:" + filtrated);
    }

}

運行新的TrieTree類的main方法,控制檯打印如下,測試通過:

過濾前:hellofsxhelllgfeheldxghergasgherohgseevsunfright
過濾後:*****fsx****lgfe**ldxg**rgasg****hg***v***f*****
發佈了67 篇原創文章 · 獲贊 14 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章