效果
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/