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:連接算法
本次實現其實也能夠改造一下,將用戶習慣(輸入內容)存成一顆trie樹數組
以how,hi,her,hello,so,see爲例緩存
這裏偷了個小懶,整了一個內部類。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個字母爲大前提.字典樹正是由於它搜索快捷的特性,纔會深受搜索引擎的喜好。只要有空間(確實很耗內存),就能隨心所欲(快)。
這裏主要是分享這樣的一種思想,如何利用現有代碼,根據需求,將其進行改形成知足的需求的代碼。有時候不須要重複造輪子,可是關鍵時刻須要會用輪子。
這裏加了一個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;
}複製代碼
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);
}
}複製代碼
測試代碼
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自動機的敏感詞過濾系統就誕生了。
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;
}複製代碼
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));
}
}複製代碼
您的點贊和關注是對我最大的支持,謝謝!