效果
C++實現的代碼請移步:
用法和效果:
int main() { std::vector<std::string> words = { // 字母 "FUCK", // 全大寫 "FuCk", // 混合 "F&uc&k", // 特殊符號 "F&uc&&&k", // 連續特殊符號 "FUck", // 全角大小寫混合 "F。uc——k", // 全角特殊符號 "fUck", // 全角半角混合 "fU😊ck", // Emotion表情,測試 // 簡體中文 "微信", "微——信", // 全角符號 "微【】、。?《》信", // 全角重復詞 "微。信", "VX", "vx", // 小寫 "V&X", // 特殊字符 "微!., #$%&*()|?/@\"';[]{}+~-_=^<>信", // 30個特殊字符 遞歸 "扣扣", "扣_扣", "QQ", "Qq", }; Trie trie; trie.loadFromFile("word.txt"); trie.loadStopWordFromFile("stopwd.txt"); for (auto &item : words) { auto t1 = std::chrono::steady_clock::now(); std::wstring result = trie.replaceSensitive(SBCConvert::s2ws(item)); auto t2 = std::chrono::steady_clock::now(); double dr_ms = std::chrono::duration<double, std::milli>(t2 - t1).count(); std::cout << "[cost: " << dr_ms << " ms]" << item << " => " << SBCConvert::ws2s(result) << std::endl; } return 0; }
load 653 words load 46 stop words [cost: 0.011371 ms]FUCK => **** [cost: 0.003244 ms]FuCk => **** [cost: 0.007321 ms]F&uc&k => ****** [cost: 0.005006 ms]F&uc&&&k => ******** [cost: 0.003155 ms]FUck => **** [cost: 0.030613 ms]F。uc——k => F。uc——k [cost: 0.006558 ms]fUck => **** [cost: 0.006181 ms]fU😊ck => ***** [cost: 0.00511 ms]微信 => ** [cost: 0.006742 ms]微——信 => 微——信 [cost: 0.012082 ms]微【】、。?《》信 => ********* [cost: 0.004329 ms]微。信 => *** [cost: 0.004665 ms]VX => ** [cost: 0.003428 ms]vx => ** [cost: 0.005998 ms]V&X => *** [cost: 0.031304 ms]微!., #$%&*()|?/@"';[]{}+~-_=^<>信 => ******************************** [cost: 0.004827 ms]扣扣 => ** [cost: 0.00585 ms]扣_扣 => *** [cost: 0.00435 ms]QQ => ** [cost: 0.00346 ms]Qq => **
引言
很早之前就打算做這一塊,剛好最近有時間研究一下。網上一般都能找到很多資料,這里簡單說一下我的理解吧。
PS:手機號匹配使用正則表達式,不屬於敏感詞范疇,請注意。
為了屏蔽一些黃牛推銷廣告,類似QQ、微信、手機號、……等等,我們希望都能替換為*號。這里為了簡單起見,以微信舉例(並不是歧視),我們會遇到以下幾種情況:
- 中文
簡體字
:微信- 繁體字:微信
- 火星文(變形或者諧音):嶶信、威信
- 中間帶特殊符號
半角特殊符號
(ASCII以內) :*&! #@(){}[] 等等,如微 信,微&&信,微_信全角
(ASCII以外):中文的標點符號,一些emotion表情等,如微——信,微😊信。
首字母縮寫
:- 半角:如VX、WX。
- 全角:vx、Wx。
- ……
變化和組合確實太多了,所以我們在實現的時候需要有所取舍。如果我們要過濾簡體字、半角特殊符號和首字母縮寫
這三種情況的敏感詞,那要怎么處理呢?
字符串查找KMP
如果不考慮性能,直接使用string自帶的contians()函數,有多少個敏感詞做多少次的contains檢測即可。但是如果要過濾的文本有幾千字,敏感詞有上萬個,這個方案肯定是不合適的。
看一下trie樹是如何解決這個問題的。
Trie樹
Trie樹,即字典樹
或前綴樹
,是一種樹形結構。廣泛應用於統計和排序大量的字符串
(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計
。它的優點是最大限度地減少無謂的字符串比較,查詢效率比較高
。
Trie的核心思想是空間換時間,利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的
。
Trie樹的基本性質
- 根節點不包含字符,除根節點外每一個節點都只包含一個字符;
- 從根節點到某一節點,路徑上經過的字符連接起來,為該節點對應的字符串;
- 每個節點的所有子節點包含的字符都不相同
- 從第一字符開始有連續重復的字符只占用一個節點,比如上面的 to 和 ten,中重復的單詞 t 只占用了一個節點。
所以每個節點都應該有2個字段:
end_flag標志
:是否結束,即為葉子節點。map<key, value>
:存放當前節點的值和子節點的值。
基礎入門
先來看一個題目: LeetCode 208. 實現 Trie (前綴樹)
實現一個 Trie (前綴樹),包含
insert
,search
, 和startsWith
這三個操作。 示例:
Trie trie = new Trie(); trie.insert("apple"); trie.search("apple"); // 返回 true trie.search("app"); // 返回 false trie.startsWith("app"); // 返回 true trie.insert("app"); trie.search("app"); // 返回 true
說明:
- 你可以假設所有的輸入都是由小寫字母 a-z 構成的。
- 保證所有輸入均為非空字符串。
分析與實現
我們先來把它畫成一個trie樹(以題目中的apple和app2個單詞為例):
通過insert(”apple“)
和 insert("app“)
2次函數調用,我們構建了如上圖所示的trie樹。我們發現:
- apple和app共用1個前綴,沒有多余的節點創建,只是多了一個#結束節點。
- 有2個#節點,只要遇到#就代表本次匹配結束,有多少個關鍵詞就應該有多少個#節點。
當然,增加結束節點的另外一個好處:我們可以判斷是startsWith還是equal
。
數據結構定義
因為root不包含字符,所以可以分別定義一個TrieNode代表節點,Trie代表樹。
TrieNode:
#include <string> #include <unordered_map> // 代表一個節點 class TrieNode { public: // 添加子節點 void addSubNode(const char &c, TrieNode *subNode) { subNodes_[c] = subNode; } // 獲取子節點 TrieNode *getSubNode(const char &c) { return subNodes_[c]; } private: // 子節點字典(key是下級字符,value是下級節點) std::unordered_map<char, TrieNode *> subNodes_; };
Trie:
// 代表一顆Trie樹 class Trie { public: // Inserts a word into the trie. void insert(std::string word); // Returns if the word is in the trie. bool search(std::string word); // Returns if there is any word in the trie that starts with the given prefix. bool startsWith(std::string prefix); private: TrieNode *root_; // root節點,不存儲字符 };
insert實現
里面有一個root字段,代表根節點,再看一下insert操作:
void Trie::insert(std::string word) { TrieNode *curNode = root_; // 遍歷字符串,字符作為key(注意中文一般有3個字節,所以會變成3個節點,但是不影響匹配) for (int i = 0; i < word.length(); i++) { char c = word[i]; TrieNode *subNode = curNode->getSubNode(c); // 如果沒有這個節點則新建 if (subNode == nullptr) { subNode = new TrieNode(); curNode->addSubNode(c, subNode); } // 下鑽,指向子節點 curNode = subNode; } // 設置結束標識 curNode->addSubNode(‘#’, new TrieNode()); }
search實現
bool Trie::search(std::string word) { TrieNode *curNode = root_; for (int i = 0; i < word.length(); i++) { curNode = curNode->getSubNode(word[i]); if (curNode == nullptr) return false; } return curNode->getSubNode('#') != nullptr; }
startsWith實現
bool Trie::startsWith(std::string prefix) { TrieNode *curNode = root_; for (int i = 0; i < prefix.length(); i++) { curNode = curNode->getSubNode(prefix[i]); if (curNode == nullptr) return false; } // 和search的區別就在於這里,不判斷下一個節點是否是結束節點 return true; }
如果運行,會輸出如下結果:
int main() { Trie t; t.insert("apple"); printf("%d \n", t.search("apple")); // 返回 true printf("%d \n", t.search("app")); // 返回 false printf("%d \n", t.startsWith("app")); // 返回 true t.insert("app"); printf("%d \n", t.search("app")); // 返回 true printf("%d \n", t.search("this is apple")); // 返回 false,為什么? }
$ ./main 1 0 1 1 0
所以,我們會發現一個問題,最后一個“this is apple”明明包含“apple”,為什么還是返回false?
其實很簡單,上面都是默認敏感詞在整個字符串開始位置
,我們只需要增加一個指針,不斷往后搜索即可。
敏感詞搜索
思路:
- 首先 p1 指針指向 root,指針 p2 和 p3 指向字符串中的第一個字符。
- 算法從字符 t 開始,檢測有沒有以 t 作為前綴的敏感詞,在這里就直接判斷 root 中有沒有 t 這個子節點即可。這里沒有,所以將 p2 和 p3 同時右移。
- 一直移動p2和p3,發現存在以 a 作為前綴的敏感詞,那么
就只右移 p3 繼續判斷 p2 和 p3 之間的這個字符串是否是敏感詞(當然要判斷是否完整)
。如果在字符串中找到敏感詞,那么可以用其他字符串如 *** 代替。接下來不斷循環直到整個字符串遍歷完成就可以了。
代碼實現如下:
bool Trie::search(std::string word) { // TrieNode *curNode = root_; // for (int i = 0; i < word.length(); i++) { // curNode = curNode->getSubNode(word[i]); // if (curNode == nullptr) // return false; // } // return curNode->getSubNode(kEndFlag) != nullptr; // 轉換成小寫 transform(word.begin(), word.end(), word.begin(), ::tolower); bool is_contain = false; for (int p2 = 0; p2 < word.length(); ++p2) { int wordLen = getSensitiveLength(word, p2); if (wordLen > 0) { is_contain = true; break; } } return is_contain; } // 這里增加一個函數,返回敏感詞的位置和長度,便於替換或者檢測邏輯 int Trie::getSensitiveLength(std::string word, int startIndex) { TrieNode *p1 = root_; int wordLen = 0; bool endFlag = false; for (int p3 = startIndex; p3 < word.length(); ++p3) { const char &cur = word[p3]; auto subNode = p1->getSubNode(cur); if (subNode == nullptr) { break; } else { ++wordLen; // 直到找到尾巴的位置,才認為完整包含敏感詞 if (subNode->getSubNode(kEndFlag)) { endFlag = true; break; } else { p1 = subNode; } } } // 注意,處理一下沒找到尾巴的情況 if (!endFlag) { wordLen = 0; } return wordLen; }
關於時間復雜度:
- 構建敏感詞的時間復雜度是可以忽略不計的,因為構建完成后我們是可以無數次使用的。
- 如果字符串的長度為 n,則每個敏感詞查找的時間復雜度為
O(n)
。
性能測試
為了方便測試,我們再增加一個函數:
std::set<SensitiveWord> Trie::getSensitive(std::string word) { // 轉換成小寫 transform(word.begin(), word.end(), word.begin(), ::tolower); std::set<SensitiveWord> sensitiveSet; for (int i = 0; i < word.length(); ++i) { int wordLen = getSensitiveLength(word, i); if (wordLen > 0) { // 記錄找到的敏感詞的索引和長度 std::string sensitiveWord = word.substr(i, wordLen); SensitiveWord wordObj; wordObj.word = sensitiveWord; wordObj.startIndex = i; wordObj.len = wordLen; // 插入到set集合中返回 sensitiveSet.insert(wordObj); i = i + wordLen - 1; } } return sensitiveSet; }
測試代碼如下:
void test_time(Trie &t) { auto t1 = std::chrono::steady_clock::now(); auto r = t.getSensitive("SHit,你你你你是傻逼啊你,說你呢,你個大笨蛋。"); for (auto &&i : r) { std::cout << "[index=" << i.startIndex << ",len=" << i.len << ",word=" << i.word << "],"; } std::cout << std::endl; // run code auto t2 = std::chrono::steady_clock::now(); //毫秒級 double dr_ms = std::chrono::duration<double, std::milli>(t2 - t1).count(); std::cout << "耗時(毫秒): " << dr_ms << std::endl; } int main() { Trie t; t.insert("你是傻逼"); t.insert("你是傻逼啊"); t.insert("你是壞蛋"); t.insert("你個大笨蛋"); t.insert("我去年買了個表"); t.insert("shit"); test_time(t); }
輸出:
$ ./main [index=0,len=4,word=shit],[index=16,len=12,word=你是傻逼],[index=49,len=15,word=你個大笨蛋], 耗時(毫秒): 0.093765
我這邊比較好奇,看一下string自帶的find函數實現的版本:
void test_time_by_find() { auto t1 = std::chrono::steady_clock::now(); std::string origin = "SHit,你你你你是傻逼啊你,說你呢,你個大笨蛋。"; std::vector<std::string> words; words.push_back("你是傻逼"); words.push_back("你是傻逼啊"); words.push_back("你是壞蛋"); words.push_back("你個大笨蛋"); words.push_back("我去年買了個表"); words.push_back("shit"); for (auto &&i : words) { size_t n = origin.find(i); if (n != std::string::npos) { std::cout << "[index=" << n << ",len=" << i.length() << ",word=" << i << "],"; } } std::cout << std::endl; // run code auto t2 = std::chrono::steady_clock::now(); //毫秒級 double dr_ms = std::chrono::duration<double, std::milli>(t2 - t1).count(); std::cout << "耗時(毫秒): " << dr_ms << std::endl; }
輸出:
$ $ ./main [index=0,len=4,word=shit],[index=16,len=12,word=你是傻逼],[index=49,len=15,word=你個大笨蛋], 耗時(毫秒): 0.113505 [index=16,len=12,word=你是傻逼],[index=16,len=15,word=你是傻逼啊],[index=49,len=15,word=你個大笨蛋],[index=0,len=4,word=shit], 耗時(毫秒): 0.021829
上面那個是trie算法實現的,耗時0.113505毫秒,下面是string的find版本,耗時0.021829毫秒,還快了5倍?這是為什么?
中文替換為*的實現
通過 getSensitive()
函數,我們得到了敏感詞出現的位置和長度,那要怎么替換成*號呢?
SHit,你你你你是傻逼啊你,說你呢,你個大笨蛋。
int main() { Trie t; t.insert("你是傻逼"); t.insert("你個大笨蛋"); t.insert("shit"); std::string origin = "SHit,你你你你是傻逼啊你,說你呢,你個大笨蛋。"; auto r = t.getSensitive(origin); for (auto &&i : r) { std::cout << "[index=" << i.startIndex << ",len=" << i.len << ",word=" << i.word.c_str() << "]," << std::endl; } std::cout << t.replaceSensitive(origin) << std::endl; return 0; }
運行后我們得到了一組敏感詞的信息,包含起始位置,長度:
[index=0,len=4,word=shit], [index=16,len=12,word=你是傻逼], [index=49,len=15,word=你個大笨蛋],
這里有個問題,因為Linux下使用utf8,1個漢字實際占用3個字節
,這也就導致,我們如果直接遍歷進行替換,會發現*多出了2倍。
# 錯誤,漢字的部分*號翻到3倍 ****,你你你************啊你,說你呢,***************。 # 期望 ****,你你你****啊你,說你呢,*****。
在解決這個問題之前,我們先來了解一下Unicode、UTF8和漢字的關系。
Unicode編碼
Unicode( 統一碼、萬國碼、單一碼)是計算機科學領域里的一項業界標准,包括字符集、編碼方案等。Unicode 是為了解決傳統的字符編碼方案的局限而產生的,它為每種語言中的每個字符設定了統一並且唯一的 二進制編碼,以滿足跨語言、跨平台進行文本轉換、處理的要求。1990年開始研發,1994年正式公布。
我們來看一下It's 知乎日報
的Unicode編碼是怎么樣的?
I 0049 t 0074 ' 0027 s 0073 0020 知 77e5 乎 4e4e 日 65e5 報 62a5
每一個字符對應一個十六進制數字。
計算機只懂二進制,因此,嚴格按照unicode的方式(UCS-2),應該這樣存儲:
I 00000000 01001001 t 00000000 01110100 ' 00000000 00100111 s 00000000 01110011 00000000 00100000 知 01110111 11100101 乎 01001110 01001110 日 01100101 11100101 報 01100010 10100101
這個字符串總共占用了18個字節,但是對比中英文的二進制碼,可以發現,英文前9位都是0!浪費啊,浪費硬盤,浪費流量。怎么辦?UTF。
UTF8
UTF-8(8-bit Unicode Transformation Format)是一種針對Unicode的可變長度字符編碼,也是一種前綴碼,又稱萬國碼。由Ken Thompson於1992年創建。它可以用來表示Unicode標准中的任何字符,且其編碼中的第一個字節仍與ASCII兼容,這使得原來處理ASCII字符的軟件無須或只須做少部份修改,即可繼續使用。因此,它逐漸成為電子郵件、網頁及其他存儲或傳送文字的應用中,優先采用的編碼。
UTF-8是這樣做的:
-
單字節的字符,字節的第一位設為0
,對於英語文本,UTF-8碼只占用一個字節,和ASCII碼完全相同
; -
n個字節的字符(n>1),
第一個字節的前n位設為1,第n+1位設為0
,后面字節的前兩位都設為10
,這n個字節的其余空位填充該字符unicode碼,高位用0補足
。
這樣就形成了如下的UTF-8標記位:
0xxxxxxx 110xxxxx 10xxxxxx 1110xxxx 10xxxxxx 10xxxxxx 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx ... ...
於是,”It's 知乎日報“就變成了:
I 01001001 t 01110100 ' 00100111 s 01110011 00100000 知 11100111 10011111 10100101 乎 11100100 10111001 10001110 日 11100110 10010111 10100101 報 11100110 10001010 10100101
和上邊的方案對比一下,英文短了,每個中文字符卻多用了一個字節。但是整個字符串只用了17個字節,比上邊的18個短了一點點。
部分漢字編碼范圍
Unicode符號范圍(十六進制) | UTF-8編碼(二進制) |
---|---|
0000 0000-0000 007F(1個字節) | 0xxxxxxx |
0000 0080-0000 07FF(2個字節) | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF(3個字節) | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF(4個字節) | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
以漢字“嚴”為例,演示如何實現UTF-8編碼。
已知“嚴”的unicode是4E25(100111000100101),根據上表,可以發現 4E25處在第三行的范圍內(0000 0800-0000 FFFF),因此“嚴”的UTF-8編碼需要三個字節,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。
然后,從“嚴”的最后一個二進制位開始,依次從后向前填入格式中的x,多出的位補0。這樣就得到了,“嚴”的UTF-8編碼是 “11100100 10111000 10100101”,轉換成十六進制就是E4B8A5。
100 111000 100101 1110xxxx 10xxxxxx 10xxxxxx # 從后往前放 1110x100 10111000 10100101 # 多出的x補0 11100100 10111000 10100101
實現漢字替換為*
方法一:通過判斷中文和占用字節數手動處理
/** @fn * @brief linux下一個中文占用三個字節,windows占兩個字節 * 參考:https://blog.csdn.net/taiyang1987912/article/details/49736349 * @param [in]str: 字符串 * @return */ std::string chinese_or_english_append(const std::string &str) { std::string replacement; //char chinese[4] = {0}; int chinese_len = 0; for (int i = 0; i < str.length(); i++) { unsigned char chr = str[i]; int ret = chr & 0x80; if (ret != 0) { // chinese: the top is 1 if (chr >= 0x80) { if (chr >= 0xFC && chr <= 0xFD) { chinese_len = 6; } else if (chr >= 0xF8) { chinese_len = 5; } else if (chr >= 0xF0) { chinese_len = 4; } else if (chr >= 0xE0) { chinese_len = 3; } else if (chr >= 0xC0) { chinese_len = 2; } else { throw std::exception(); } } // 跳過 i += chinese_len - 1; //chinese[0] = str[i]; //chinese[1] = str[i + 1]; //chinese[2] = str[i + 2]; } else /** ascii **/ { } replacement.append("*"); } return replacement; } std::string Trie::replaceSensitive(const std::string &word) { std::set<SensitiveWord> words = getSensitive(word); std::string ret; int last_index = 0; for (auto &item : words) { std::string substr = word.substr(item.startIndex, item.len); std::string replacement = chinese_or_english_append(substr); // 原始內容 ret.append(word.substr(last_index, item.startIndex - last_index)); // 替換內容 ret.append(replacement); last_index = item.startIndex + item.len; } // append reset ret.append(word.substr(last_index, word.length() - last_index)); return ret; }
方法二(推薦)
:使用wstring代替string。遍歷wstring時,item為wchar_t類型(4個字節),直接使用wchar_t作為unordered_map的key,這樣無須特殊處理中文替換為*的問題。
停頓詞實現
遇到特殊字符,比如微&信
、微-信
、微&&信
如何繼續識別?其實很簡單,直接忽略該詞往下查找即可:
int Trie::getSensitiveLength(std::string word, int startIndex) { TrieNode *p1 = root_; int wordLen = 0; bool endFlag = false; for (int p3 = startIndex; p3 < word.length(); ++p3) { const char &cur = word[p3]; auto subNode = p1->getSubNode(cur); if (subNode == nullptr) { // 遇到停頓詞,跳過該詞繼續搜索字符串 if (cur == '&' || cur == '-'|| cur == ' ') { ++wordLen; continue; } break; } else { // ... } } // ... }
全角和半角實現
參考:C/C++ -- 判斷字符串中存在中文、sensitivewd-filter
除了特殊符號之外,還有一種特殊情況會使我們的過濾算法失效,那就是全角(除ASCII碼之外的),如下:
- 字母全角:
- 大寫的:ABCDEFG
- 小寫的:abcdefg
- 符號:
- 中文標點:【】、。?!()……——;‘等等
在游戲中,我們也能經常看見使用全角繞過敏感詞過濾的現象,比如混用半角和全角:Fuck,V【信等,那要怎么處理呢?
實際上,類似大小寫處理一樣,我們只需要把全角字母和一些中文符號
轉換成對應的半角
即可。其他非常規的全角特殊符號,直接在停頓詞里面增加即可。
通過查閱上述的ASCII編碼表和Unicode編碼表,我們發現他們存在一定的對應關系:
ASCII | UNICODE |
---|---|
! (0x21) | !(0xFF01) |
"(0x22) | “(0xFF02) |
#(0x23) | #(0xFF03) |
... | ... |
A(0x41) | A(0xFF21) |
B(0x42) | B(0xFF22) |
... | ... |
~(0x7E) | ~(0xFF5E) |
ASCII碼 !到~
之間的字符,和Unicode碼中 0xFF01到0xFF5E字符
一一對應 ,所以全角轉換到半角只需要減去一個固定的偏移
即可。
const wchar_t kSBCCharStart = 0xFF01; // 全角! const wchar_t kSBCCharEnd = 0xFF5E; // 全角~ // 全角空格的值,它沒有遵從與ASCII的相對偏移,必須單獨處理 const wchar_t kSBCSpace = 0x508; // 全角空格 // ASCII表中除空格外的可見字符與對應的全角字符的相對偏移 const wchar_t kConvertStep = kSBCCharEnd - kDBCCharEnd; int SBCConvert::qj2bj(const wchar_t &src) { // 偏移,轉換到對應ASCII的半角即可 if (src >= kSBCCharStart && src <= kSBCCharEnd) { return (wchar_t) (src - kConvertStep); } else if (src == kSBCSpace) { // 如果是全角空格 return kDBCSpace; } return src; }
上面為什么返回一個int值呢?
其實我們所看到的文字,背后的原理就是一串數字(Unicode編碼)而已
// Linux 環境下,string是utf8編碼 int main() { std::wstring str = L"嚴~A"; for (wchar_t &item : str) { int unicode = static_cast<int>(item); std::cout << std::hex << unicode << std::endl; } return 0; }
4e25 ff5e 41
同樣,也可以轉回去:
#include <string> #include <locale> #include <codecvt> std::string ws2s(const std::wstring &wstr) { using convert_typeX = std::codecvt_utf8<wchar_t>; std::wstring_convert<convert_typeX, wchar_t> converterX; return converterX.to_bytes(wstr); } int main() { wchar_t ws[4] = {}; ws[0] = 0x4e25; ws[1] = 0xff5e; ws[2] = 0x41; std::cout << ws2s(ws) << std::endl; return 0; }
嚴~A
敏感詞庫
總結
算法時間復雜度對比:
KMP | Trie樹 | DFA | AC自動機 |
---|---|---|---|
O(LenA + LenB) * m | log(n) | 未知 | 未知 |
最后,C++實現的代碼請移步:
參考
文章:
- KMP+Trie+AC自動機總結(字符串板子)
- AC自動機 算法詳解(圖解)及模板
- DFA 算法實現敏感詞過濾(字典樹)
- 字符串的模式匹配(KMP)算法
- kmp的next數組值得求法
- AC自動機總結(超詳細注釋)
- sensitive-stop-words
- sensitivewd-filter
- linux下c/c++實例之九識別中文字符
- Unicode 和 UTF-8 有什么區別?
- C/C++ -- 判斷字符串中存在中文
- 詳解KMP算法
視頻:
- B站視頻:「天勤公開課」KMP算法易懂版,點評:動畫牛逼,但是語速有點快。適合入門
- B站視頻:KMP算法計算next函數值(教材版,超簡單!),點評:看完上面一個,看這個復習一下公共前綴。
- KMP字符串匹配算法1,點評:介紹前綴概念,深入理解前后綴。圖解非常形象好理解,強烈推薦。
網站:
- 在線unicode字符表:unicode-table.com/