字典樹介紹
Paste_Image.png
- 又稱單詞查找樹,Trie樹,是一種樹形結(jié)構(gòu),是一種哈希樹的變種。典型應(yīng)用是用于統(tǒng)計,排序和保存大量的字符串(但不僅限于字符串),所以經(jīng)常被搜索引擎系統(tǒng)用于文本詞頻統(tǒng)計。它的優(yōu)點(diǎn)是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,查詢效率比哈希樹高。
特性
- 根節(jié)點(diǎn)不包含字符,除根節(jié)點(diǎn)外每一個節(jié)點(diǎn)都只包含一個字符
- 從根節(jié)點(diǎn)到某一節(jié)點(diǎn),路徑上經(jīng)過的字符連接起來,為該節(jié)點(diǎn)對應(yīng)的字符串
- 每個節(jié)點(diǎn)的所有子節(jié)點(diǎn)包含的字符都不相同。
顯然面對大量文本和大量敏感詞,利用字典樹過濾敏感詞是明智而有效的,可以大量的減少重復(fù)的抖動,從而降低時間復(fù)雜度。
算法描述
- 通過讀入的一個個敏感詞來構(gòu)造我們自己的字典樹,可以將敏感詞放到配置文件中...
- 首先創(chuàng)建字典樹的結(jié)點(diǎn)。
- 我們可以定義一個boolean成員變量isEnd來標(biāo)識當(dāng)前結(jié)點(diǎn)是否是敏感詞的結(jié)尾字,即該節(jié)點(diǎn)連上其上面的結(jié)點(diǎn)可以構(gòu)成一個敏感詞。
- 定義一個Map成員變量來存儲當(dāng)前結(jié)點(diǎn)的所有子節(jié)點(diǎn)(一層)(根節(jié)點(diǎn)不包含字符)
- 對外提供一些方法如:添加、獲得結(jié)點(diǎn)。以方便構(gòu)造字典樹
- 結(jié)點(diǎn)的類定義代碼如下:
private class TrieNode{
/**
* 標(biāo)識當(dāng)前結(jié)點(diǎn)是否是一個“關(guān)鍵詞”的最后一個結(jié)點(diǎn)
* true 關(guān)鍵詞的終結(jié) false 繼續(xù)
*/
private boolean isEnd = false;
/**
* 用map來存儲當(dāng)前結(jié)點(diǎn)的所有子節(jié)點(diǎn),非常的方便
* key 下一個字符 value 對應(yīng)的結(jié)點(diǎn)
*/
private Map<Character , TrieNode> subNodes = new HashMap<>();
/**
* 向指定位置添加結(jié)點(diǎn)樹
* @param key
* @param node
*/
public void addSubNode(Character key , TrieNode node){
subNodes.put(key , node);
}
/**
* 根據(jù)key獲得相應(yīng)的子節(jié)點(diǎn)
* @param key
* @return
*/
public TrieNode getSubNode(Character key){
return subNodes.get(key);
}
//判斷是否是關(guān)鍵字的結(jié)尾
public boolean isKeyWordEnd(){
return isEnd;
}
//設(shè)置為關(guān)鍵字的結(jié)尾
public void setKeyWordEnd(boolean isEnd){
this.isEnd = isEnd;
}
}
2.構(gòu)造字典樹
/**
* 核心算法一:構(gòu)建字典樹
* 根據(jù)輸入的字符串,逐步構(gòu)建字典樹
* @param textLine
*/
private void addDirTreeNode(String textLine){
//邊界處理
if(textLine == null)
return;
//臨時結(jié)點(diǎn)指向根結(jié)點(diǎn)
TrieNode tempNode = root;
for(int i = 0; i < textLine.length(); i++){
char charWord = textLine.charAt(i);
//直接跳過非法文字
if (isSymbol(charWord))
continue;
TrieNode node = tempNode.getSubNode(charWord);
if (node == null){
//說明tempNode第一次碰到該關(guān)鍵字結(jié)點(diǎn)
node = new TrieNode();
tempNode.addSubNode(charWord , node);
}
//tempNode指向下一個結(jié)點(diǎn),開始下一次循環(huán)
tempNode = node;
//到敏感詞的最后一個字時,標(biāo)記為紅色(關(guān)鍵詞結(jié)尾)
if (i == textLine.length() - 1)
tempNode.setKeyWordEnd(true);
}
}
3.過濾算法
- 定義三個指針
- tempNode : 指向字典樹的根節(jié)點(diǎn)。
- position :當(dāng)前比較的位置,開始下標(biāo)0
- begin : begin總是不斷向前,position匹配失敗的時候,需要回滾。開始下標(biāo)為0.
- position所在位置的字符,字典樹的根節(jié)點(diǎn)的所有子節(jié)點(diǎn)中沒有該字符,則說明該字符不可能構(gòu)成敏感詞,因此begin、position均可前進(jìn)一位,同時tempNode回溯到根節(jié)點(diǎn)。
if (tempNode == null){
//以begin開始的字符串不存在敏感詞
results.append(text.charAt(begin));
position = begin + 1;
begin = position;
tempNode = root;
}
- position向前不斷移動,并且和字典樹中的敏感詞一一對應(yīng),最終到tempNode指向isEnd為true的結(jié)點(diǎn)時,匹配成功,需要替換敏感詞,并且position需要前進(jìn)一位,begin移動到和position相同的位置。
else if (tempNode.isKeyWordEnd()){
results.append(DEFAULT_REPLACE_SENSITIVE);
position = position + 1;
begin = position;
tempNode = root;
}
- 過濾算法詳細(xì)代碼
/**
* 核心算法二:
* 過濾文本中的敏感詞匯
* @param text
* @return
*/
public String filterWords(String text){
if (StringUtils.isBlank(text))
return text;
StringBuilder results = new StringBuilder();
TrieNode tempNode = root;
int begin = 0;//回滾數(shù)
int position = 0;//當(dāng)前比較的位置
while (position < text.length()){
char word = text.charAt(position);
if (isSymbol(word)){
if (tempNode == root){
results.append(word);
++begin;
}
++position;
continue;
}
tempNode = tempNode.getSubNode(word);
if (tempNode == null){
//以begin開始的字符串不存在敏感詞
results.append(text.charAt(begin));
position = begin + 1;
begin = position;
tempNode = root;
}else if (tempNode.isKeyWordEnd()){
results.append(DEFAULT_REPLACE_SENSITIVE);
position = position + 1;
begin = position;
tempNode = root;
}else {
++position;
}
}
results.append(text.substring(begin));
return results.toString();
}
算法實(shí)現(xiàn)
優(yōu)化點(diǎn)
- 過濾非法字符(顏文字、空格...)即不可能組成敏感詞的字符,提高算法準(zhǔn)確性、性能...。
/**
* 判斷是否是非法字符(即不可能存在敏感詞匯的字)
* @param character
* @return true:非法字符
*/
private boolean isSymbol(Character character){
int ic = (int)character;
//東亞文字 0x2e80 —— 0x9fff
return !CharUtils.isAsciiAlphanumeric(character) && (ic < 0x2e80 || ic > 0x9fff);
}
全部源碼
import java.util.HashMap;
import java.util.Map;
public class Test {
//默認(rèn)敏感詞替換符
private static final String DEFAULT_REPLACEMENT = "敏感詞";
//根節(jié)點(diǎn)
private TrieNode rootNode = new TrieNode();
private class TrieNode {
/**
* true 關(guān)鍵詞的終結(jié) ; false 繼續(xù)
*/
private boolean end = false;
/**
* key下一個字符,value是對應(yīng)的節(jié)點(diǎn)
*/
private Map<Character, TrieNode> subNodes = new HashMap<Character ,TrieNode>();
/**
* 向指定位置添加節(jié)點(diǎn)樹
*/
void addSubNode(Character key, TrieNode node) {
subNodes.put(key, node);
}
/**
* 獲取下個節(jié)點(diǎn)
*/
TrieNode getSubNode(Character key) {
return subNodes.get(key);
}
boolean isKeywordEnd() {
return end;
}
void setKeywordEnd(boolean end) {
this.end = end;
}
}
//判斷是否是一個符號
private boolean isSymbol(char c) {
int ic = (int) c;
// 0x2E80-0x9FFF 東亞文字范圍
return !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')|| (c >= 'A' && c <= 'Z')) && (ic < 0x2E80 || ic > 0x9FFF);
}
/**
* 過濾敏感詞
*/
public String filter(String text) {
if (text.trim().length() == 0) {
return text;
}
String replacement = DEFAULT_REPLACEMENT;
StringBuilder result = new StringBuilder();
TrieNode tempNode = rootNode;
int begin = 0; // 回滾數(shù)
int position = 0; // 當(dāng)前比較的位置
while (position < text.length()) {
char c = text.charAt(position);
// 空格直接跳過
if (isSymbol(c)) {
if (tempNode == rootNode) {
result.append(c);
++begin;
}
++position;
continue;
}
tempNode = tempNode.getSubNode(c);
// 當(dāng)前位置的匹配結(jié)束
if (tempNode == null) {
// 以begin開始的字符串不存在敏感詞
result.append(text.charAt(begin));
// 跳到下一個字符開始測試
position = begin + 1;
begin = position;
// 回到樹初始節(jié)點(diǎn)
tempNode = rootNode;
} else if (tempNode.isKeywordEnd()) {
// 發(fā)現(xiàn)敏感詞, 從begin到position的位置用replacement替換掉
result.append(replacement);
position = position + 1;
begin = position;
tempNode = rootNode;
} else {
++position;
}
}
result.append(text.substring(begin));
return result.toString();
}
/**
* 構(gòu)造字典樹
* @param lineTxt
*/
private void addWord(String lineTxt) {
TrieNode tempNode = rootNode;
// 循環(huán)每個字節(jié)
for (int i = 0; i < lineTxt.length(); ++i) {
Character c = lineTxt.charAt(i);
// 過濾空格
if (isSymbol(c)) {
continue;
}
TrieNode node = tempNode.getSubNode(c);
if (node == null) { // 沒初始化
node = new TrieNode();
tempNode.addSubNode(c, node);
}
tempNode = node;
if (i == lineTxt.length() - 1) {
// 關(guān)鍵詞結(jié)束, 設(shè)置結(jié)束標(biāo)志
tempNode.setKeywordEnd(true);
}
}
}
public static void main(String[] argv) {
Test s = new Test();
s.addWord("sb");
s.addWord("zz");
System.out.print(s.filter("klsfjkzzlsO(∩_∩)Odjflksbj"));
}
}
- 運(yùn)行結(jié)果
klsfjk敏感詞lsO(∩_∩)Odjflk敏感詞j