由簡入繁--Trie樹實戰

You only get one shot, do not miss your chance to blow. 
你只有一發子彈,不要錯過引爆全場的機會。

日誌

2019年3月24日 trie實戰(一)統計字符串中指定字符出現的次數node

2019年3月25日 trie實戰 (二)基於AC自動機的敏感詞過濾系統git

引言

學習不能只侷限於實現,更重要的是學會本身思考,觸類旁通。學的是思想,如何轉化成本身的東西。github

trie樹又稱「字典樹」。關鍵詞提示功能在平常生活中很是經常使用,一般只須要輸出前綴,它就會給出相應的提示。呢具體是怎麼實現的呢?本文主要分享了基於trie樹的一個簡易的搜索提示以及trie樹經常使用的應用場景。全部源碼均已上傳至github:連接算法

ps:Trie 樹的本質,就是利用字符串之間的公共前綴,將重複的前綴合並在一塊兒。

模擬搜索關鍵詞提示功能

本次實現其實也能夠改造一下,將用戶習慣(輸入內容)存成一顆trie樹數組

以how,hi,her,hello,so,see爲例緩存

聲明一個tire類

這裏偷了個小懶,整了一個內部類。bash

public class TrieNode {
        /**
         * 字符
         */
        public char data;
        /**
         * 子節點
         */
        TrieNode[] children;
        /**
         * 標識
         */
        boolean isEndingChar;

        TrieNode(char data) {
            children = new TrieNode[26];
            isEndingChar = false;
            this.data = data;
        }
    }複製代碼

初始化

一般根節點是不存儲任何信息的,起一個佔位符的做用學習

/**
     * 根節點
     */
    private TrieNode root;

    /**
     * 預製單詞數量
     */
    private int count;

    /**
     * 提示詞列表
     */
    private List<String> list;

    /**
     * 輸入值
     */
    private String pattern;

    /**
     * 存儲一個無心義的字符
     */
    private TrieTree() {
        root = new TrieNode('/');
        count = 0;
        list = new ArrayList<>();
    }複製代碼

插入

這裏存儲的是ASCII碼,相對而言要省內存一些。測試

private void insert(char[] txt) {
        TrieNode p = root;
        for (char c : txt) {
            //當前字符的ASCII碼 - 'a'的 ASCII碼
            int index = c - 'a';
            if (null == p.children[index]) {
                TrieNode node = new TrieNode(c);
                p.children[index] = node;
            }
            p = p.children[index];
        }
        ++count;
        p.isEndingChar = true;
    }複製代碼

查詢

private boolean contains(String pattern) {
        char[] patChars = pattern.toCharArray();
        TrieNode p = root;
        for (char patChar : patChars) {
            int index = patChar - 'a';
            if (null == p.children[index])
                return false;
            p = p.children[index];
        }
        return p.isEndingChar;
    }複製代碼

模糊提示匹配

private void match() {
        char[] patChars = pattern.toCharArray();
        TrieNode p = root;
        for (char patChar : patChars) {
            int index = patChar - 'a';
            if (null == p.children[index])
                return;
            p = p.children[index];
        }
        //開始遍歷 p,將全部匹配的字符加入strs
        traversal(p, "");
    }複製代碼

遞歸遍歷節點

private void traversal(TrieNode trieNode, String str) {
        if (null != trieNode) {
            str += trieNode.data;
            if (trieNode.isEndingChar) {
                String curStr = pattern.length() == 1 ? 
		str : pattern + str.substring(pattern.length() - 1);
                if (!list.contains(curStr))
                    list.add(curStr);
                return;
            }
            for (int i = 0; i < trieNode.children.length; i++) {
                traversal(trieNode.children[i], str);
            }
        }
    }複製代碼

測試代碼

人爲構造一個tire樹優化

ps:這裏的存儲會致使樹很高,好比 l l o,其實能夠合成llo,也就是縮點優化。這裏暫時不實現了。

private void initTries() {
//        how,hi,her,hello,so,see
//                   /
//              h         s
//           e  i  o    o   e
//         l         w        e
//      l
//   o
        char[] how = "how".toCharArray();
        insert(how);
        char[] hi = "hi".toCharArray();
        insert(hi);
        char[] her = "her".toCharArray();
        insert(her);
        char[] hello = "hello".toCharArray();
        insert(hello);
        char[] so = "so".toCharArray();
        insert(so);
        char[] see = "see".toCharArray();
        insert(see);
    }複製代碼

測試代碼

public static void main(String[] args) {
        TrieTree trieTree = new TrieTree();
        trieTree.initTries();
        String str = "hello";
        boolean res = trieTree.contains(str);
        System.out.println("trie樹是否包含" + str + "返回結果:" + res);

        trieTree.pattern = "h";
        trieTree.match();
        System.out.println("單字符模糊匹配 " + trieTree.pattern + ":");
        trieTree.printAll();

        trieTree.list.clear();
        trieTree.pattern = "he";
        trieTree.match();
        System.out.println("多字符模糊匹配 " + trieTree.pattern + ":");
        trieTree.printAll();
    }複製代碼

測試結果


統計字符串中指定字符出現的次數

仍是以26個字母爲大前提.字典樹正是由於它搜索快捷的特性,纔會深受搜索引擎的喜好。只要有空間(確實很耗內存),就能隨心所欲(快)。

思考

這裏主要是分享這樣的一種思想,如何利用現有代碼,根據需求,將其進行改形成知足的需求的代碼。有時候不須要重複造輪子,可是關鍵時刻須要會用輪子。

改造TireNode類

這裏加了一個frequency屬性,爲了統計高頻詞彙。而且將children由數組改爲map,更便於存儲,至關而言,更節省空間。

private class TrieNode {
        /**
         * 字符
         */
        public char data;
        /**
         * 出現頻率
         */
        int frequency;

        boolean isEndingChar;
        /**
         * 子節點
         */
        Map<Character, TrieNode> children;

        TrieNode(char data) {
            this.data = data;
            children = new HashMap<>();
            isEndingChar = false;
        }
    }複製代碼

初始化

/**
     * 根節點
     */
    private TrieNode root;
    /**
     * 計數
     */
    private int count;

    /**
     * 無參構造方法
     */
    private TrieTreeAlgo() {
        root = new TrieNode('/');
        count = 0;
    }複製代碼

改造插入方法

  • frequency用來計數,計算該字符的頻率
  • isEndingChar和以前同樣,用來判斷是不是該單詞的結尾

private void insert(String txt) {
        TrieNode p = root;
        char[] txtChar = txt.toCharArray();
        for (Character c : txtChar) {
            if (!p.children.containsKey(c)) {
                TrieNode trieNode = new TrieNode(c);
                p.children.put(c, trieNode);
            }
            p = p.children.get(c);
            ++p.frequency;
        }
        ++count;
        p.isEndingChar = true;
    }複製代碼

統計方法

增長一個統計方法,計算某一單詞的出現頻率,當isEndingChar==true,說明已經匹配到該單詞了,而且到末尾,而後該字符頻率數量減去子節點的個數便可

private int frequency(String pattern) {
        char[] patChars = pattern.toCharArray();
        TrieNode p = root;
        for (char patChar : patChars) {
            if (p.children.containsKey(patChar)) {
                p = p.children.get(patChar);
            }
        }
        if (p.isEndingChar) return p.frequency - p.children.size();
        return -1;
    }複製代碼

測試代碼

初始化要插入字典樹的單詞(這裏其實能夠擴展一下下,插入一篇文章,插入用戶常輸入詞彙等等。)

private void initTries() {
        String txt = "he her hello home so see say just so so hello world";
        String[] strs = txt.split(" ");
        for (String str : strs) {
            insert(str);
        }
    }複製代碼

測試代碼

  1. so 一個高頻詞彙
  2. he 一個普通單詞,而且裏面的單詞還有含有它的,好比her,hello
  3. hel一個不存在的單詞

public static void main(String[] args) {
        TrieTreeAlgo trieTreeAlgo = new TrieTreeAlgo();
        trieTreeAlgo.initTries();
        System.out.println("共計" + trieTreeAlgo.count + "個單詞。");
        String so = "so";
        int soCount = trieTreeAlgo.frequency(so);
        System.out.println(so + "出現的次數爲:" + (soCount > 0 ? soCount : 0));
        String he = "he";
        int heCount = trieTreeAlgo.frequency(he);
        System.out.println(he + "出現的次數爲:" + (heCount > 0 ? heCount : 0));
        String hel = "hel";
        int helCount = trieTreeAlgo.frequency(hel);
        System.out.println(hel + "出現的次數爲:" + (helCount > 0 ? helCount : 0));
    }複製代碼

測試結果


基於AC自動機的敏感詞過濾系統

既然有了關鍵詞匹配提示,那麼相對應的,天然也應該有敏感詞過濾,隨着互聯網的日益發達,用戶的素質良莠不齊,動不動就罵人,若是這在一個網站上顯示,確定是很差的,因此對此現象,基於AC自動機的敏感詞過濾系統就誕生了。

ps:偷偷告訴你個祕密:這是一個閹割壓縮版的敏感詞過濾系統

思考

AC 自動機實際上就是在 Trie 樹之上,加了相似 KMP 的 next 數組(只不過這裏的next數組是構建在Trie樹上)。仍是要改造的,在trie樹的基礎上加了一個fail的指針,當匹配不上的時候,儘量的在樹上滑動,說人話就是大大減小了遍歷的次數,提高了匹配效率。

ps 這是一種後綴字符串匹配算法

改造

在原有基礎上,加了一個fail的指針,而且AC自動機的跳轉是經過fail指針來實現的。

private class AcNode {
        /**
         * 字符
         */
        public char data;
        /**
         * 子節點
         */
        Map<Character, AcNode> children;
        /**
         * 結束標識
         */
        boolean isEndingChar;
        /**
         * 失敗指針
         */
        AcNode fail;

        AcNode(char data) {
            this.data = data;
            children = new HashMap<>();
            isEndingChar = false;
        }
    }複製代碼

初始化

/**
     * 根節點
     */
    private AcNode root;

    private AhoCorasick() {
        root = new AcNode('/');
    }複製代碼

插入

private void insert(String txt) {
        AcNode p = root;
        char[] txtChar = txt.toCharArray();
        for (Character c : txtChar) {
            if (!p.children.containsKey(c)) {
                AcNode trieNode = new AcNode(c);
                p.children.put(c, trieNode);
            }
            p = p.children.get(c);
        }
        p.isEndingChar = true;
    }複製代碼

構建失敗指針

這個方法是關鍵。

private void buildFailurePointer() {
        Queue<AcNode> queue = new LinkedList<>();
        root.fail = null;
        queue.offer(root);
        while (!queue.isEmpty()) {
            AcNode p = queue.poll();
            for (char c : p.children.keySet()) {
                AcNode pChild = p.children.get(c);
                if (null == pChild) continue;
                if (root == p) {
                    pChild.fail = root;
                } else {
                    AcNode q = p.fail;
                    while (null != q) {
                        AcNode qChild = q.children.get(p.data);
                        if (null != qChild) {
                            pChild.fail = qChild;
                            break;
                        }
                        q = q.fail;
                    }
                    if (null == q) {
                        pChild.fail = root;
                    }
                }
                queue.offer(pChild);
            }
        }
    }複製代碼

匹配

private boolean match(String txt) {
        char[] txtChars = txt.toCharArray();
        AcNode p = root;
        for (char c : txtChars) {
            while (p != root && null == p.children.get(c)) {
                p = p.fail;
            }
            p = p.children.get(c);
            //若是沒有匹配,從root從新開始
            if (null == p) p = root;
            AcNode temp = p;
            while (temp != root) {
                if (temp.isEndingChar) {
                    return true;
                }
                temp = temp.fail;
            }
        }
        return false;
    }複製代碼

構建敏感詞Trie樹

private void generate() {
        String[] strs = new String[]{"so", "hel", "oh", "llo"};
        for (int i = 0; i < strs.length; i++) {
            insert(strs[i]);
        }
    }複製代碼

測試代碼

這裏加了一個Map,用來作緩存,若是已經匹配上了,直接替換就能夠了,提高效率。mapCache的value就是key出現的次數,起一個計數的做用。

public static void main(String[] args) {
        AhoCorasick ac = new AhoCorasick();
        ac.generate();
        ac.buildFailurePointer();
        String txt = "he her hello home so see say just so so hello world";
        System.out.println("主串");
        System.out.println("[" + txt + "]");
        System.out.println("敏感詞:");
        System.out.println("so,hel,oh,llo");
        String[] strs = txt.split(" ");
        Map<String, Integer> mapCache = new HashMap<>();
        for (int i = 0; i < strs.length; i++) {
            if (mapCache.containsKey(strs[i])) {
                int index = mapCache.get(strs[i]);
                mapCache.put(strs[i], ++index);
                strs[i] = "****";
            } else {
                boolean res = ac.match(strs[i]);
                //若是匹配到,將其替換成****
                if (res) {
                    mapCache.put(strs[i], 1);
                    strs[i] = "****";
                }
            }
        }
        System.out.println("通過敏感詞系統過濾後...");
        System.out.println(Arrays.toString(strs));
        for (String str:mapCache.keySet()){
            System.out.println(str + "出現的次數爲" + mapCache.get(str));
        }
    }複製代碼

測試結果


end


您的點贊和關注是對我最大的支持,謝謝!
相關文章
相關標籤/搜索