前几天都看一个敏感词屏蔽算法的文章,写的挺好,顺着思路写了下去,实现了一下,算法效率还是杠杠的。。。
一、单词树介绍
利用的是单词树的算法,先看看什么叫单词树。单词树也叫trie 树也称为字典树。最大的特点就是共享字符串的公共前缀来达到节省空间的目的。
例如,字符串 "abc"和"abd"构成的单词树如下:
树的根节点不存任何数据,每整个个分支代表一个完整的字符串。像 abc 和 abd 有公共前缀 ab,所以我们可以共享节点 ab。如果再插入 abf,则变成这样:
这样看来能实现的功能就很显而易见了,例如词频统计,单词查找,还有就是游戏里的敏感词屏蔽。
二、实现思路
来具体说说实现的思路吧。
2.1 词频统计和单词查找
这两个都是同一种思路。即下面代码里的find_word_exists
函数,词频统计加个累计就好了。
关键在创建单词树的时候,需要添加子节点,另外还要标记单词是否在此处是完整单词。然后将一个个字符插入即可。
2.2 敏感词屏蔽
这个稍微复杂点。即下面代码里的sensitive_word_filter
函数。
需要三个指针来遍历实现,两个在检查的单词上,一个在单词树上。
1、首先指针 p1 指向 root,指针 p2 和 p3 指向字符串第一个字符
2、然后从字符串的 a 开始,检测有没有以 a 作为前缀的敏感词,直接判断 p1 的孩子节点中是否有 a 这个节点就可以了,显然这里没有。接着把指针 p2 和 p3 向右移动一格。
3、然后从字符串 b 开始查找,看看是否有以 b 作为前缀的字符串,p1 的孩子节点中有 b,这时,我们把 p1 指向节点 b,p2 向右移动一格,不过,p3不动。
4、判断 p1 的孩子节点中是否存在 p2 指向的字符c,显然有。我们把 p1 指向节点 c,p2 向右移动一格,p3不动。
5、判断 p1 的孩子节点中是否存在 p2 指向的字符d,这里没有。这意味着,不存在以字符b作为前缀的敏感词。这时我们把p2和p3都移向字符c,p1 还是还原到最开始指向 root。
6、和前面的步骤一样,判断有没以 c 作为前缀的字符串,显然这里没有,所以把 p2 和 p3 移到字符 d。
到这里应该差不多懂了。。。后面都一样。那开始动手实践。
三、代码实现
这里的词频统计,单词查找和敏感词屏蔽都实现了,如下;
#include <iostream>
#include <stdio.h>
using namespace std;
#pragma pack(1)
struct trie_node
{
static const int letter_count = 26;
int count; // 字符的次数
bool is_terminal; // 完整单词的标志
char letter; // 当前节点的字符
trie_node* childs[letter_count]; // 子节点
trie_node(): letter(0), count(1), is_terminal(false)
{
for(int i = 0; i < letter_count; ++i)
{
childs[i] = NULL;
}
}
};
#pragma pack()
class trie
{
private:
trie_node* _root_node;
public:
trie(): _root_node(NULL)
{
}
~trie()
{
delete_trie(_root_node);
}
trie_node* create()
{
trie_node* node = new trie_node();
return node;
}
void insert(const char* str)
{
if(NULL == _root_node || NULL == str)
{
_root_node = create();
}
trie_node* next_node = _root_node;
while(*str != 0)
{
int index = *str - 'a';
if(NULL == next_node->childs[index])
{
next_node->childs[index] = create();
}
else
{
next_node->childs[index]->count++;
}
next_node = next_node->childs[index];
next_node->letter = *str;
str++;
}
next_node->is_terminal = true;
}
bool find_word_exists(const char* str)
{
if(NULL == _root_node || NULL == str)
{
printf("condition is null\n");
return false;
}
trie_node* cur_node = _root_node;
do
{
cur_node = cur_node->childs[*str - 'a'];
if(NULL == cur_node)
{
return false;
}
str++;
}while (*str != 0);
return cur_node->is_terminal; /* 直接看当前是否有完整单词的标志 */
}
void sensitive_word_filter(char* str)
{
if(NULL == _root_node || NULL == str)
{
printf("condition is null\n");
return ;
}
char* pre = str;
char* cur = str;
trie_node* cur_node = _root_node;
do
{
int index = *cur - 'a';
if(NULL != cur_node->childs[index])
{
if(cur_node->childs[index]->is_terminal == true) /* 找到敏感词 */
{
while(pre != cur) /* 替换敏感词 */
{
*pre = '*';
pre++;
}
*pre = '*';
// 向后移动,重新开始单词树查找
cur++;
pre = cur;
cur_node = _root_node;
continue;
}
cur_node = cur_node->childs[index];
cur++;
}
else
{
/* 单词树需要重新开始查找。检测的文本向后移动一步(前面的指针)然后查找 */
pre++;
cur = pre;
cur_node = _root_node;
}
}while (*cur != 0);
return;
}
void delete_trie(trie_node* node)
{
if(NULL == node)
{
return ;
}
for (int i = 0; i < trie_node::letter_count; i++)
{
if(NULL != node->childs[i])
{
delete_trie(node->childs[i]);
}
}
delete node;
}
};
int main(int argc, char** argv)
{
if(argc < 2)
{
printf("Usage: ./a.out word\n");
return -1;
}
char* word = NULL;
if(NULL != argv[1])
{
word = argv[1];
}
else
{
return -2;
}
trie trie_tree = trie();
trie_tree.insert("apps");
trie_tree.insert("apply");
trie_tree.insert("append");
trie_tree.insert("back");
trie_tree.insert("backen");
trie_tree.insert("basic");
/*1. 词频统计,和单词查找*/
bool is_find = trie_tree.find_word_exists(word);
if(is_find)
{
printf("find word\n");
}
else
{
printf("not find\n");
}
/*2. 敏感词屏蔽*/
trie_tree.sensitive_word_filter(word);
printf("word = %s\n", word);
return 0;
}
./a.out apps
运行结果:
find word
word = ****
./a.out backhahaha
运行结果:
not find
word = ****hahaha
原理参考链接:https://blog.csdn.net/m0_37907797/article/details/103272967