高效的关键字过滤及查找算法(Trie KO Hash)

时间:2022-08-29 16:49:35

   在实际当中,聊天过滤工作 最终可能会消耗掉可观的(有时是惊人的)资源总量--无论是从开发方面还是从原始计算能力方面来说。有些知名SNS类游戏仅在聊天过滤上就耗费了超过50%的计算资源。因此对于这部分的优化工作就显得特别重要。

最近做游戏服务器,需要用到聊天过滤,首先想到的是使用HashSet<string> 的方式。

基本的思路是把所有需要过滤的关键字保存在HashSet<string>中。

对于用户输入的消息进行暴力分割。

如:今天你好吗

单个字符分割为: 今/天/你/好/吗

2个字符分割为: 今天/天你/你好/好吗

3个字符分割为: 今天你/天你好/你好吗

..........以此类推

对于大的字符串。我们对分割把分割的长度限制为需过滤关键字的长度。

然后对分割的了字符串进行匹配。

 

高效的关键字过滤及查找算法(Trie KO Hash)高效的关键字过滤及查找算法(Trie KO Hash)View Code
     public   class  HashFilter
    {
        
int  m_maxLen;  // 关键字最大长度
        HashSet < string >  m_keys  =   new  HashSet < string > ();
        
        
///   <summary>
        
///  插入新的Key.
        
///   </summary>
        
///   <param name="name"></param>
         public   void  AddKey( string  key)
        {
            
if  (( ! string .IsNullOrEmpty(key))  &&  m_keys.Add(key)  &&  key.Length  >  m_maxLen)
            {
                m_maxLen 
=  key.Length;
            }
        }
        
///   <summary>
        
///  检查是否包含非法字符
        
///   </summary>
        
///   <param name="text"> 输入文本 </param>
        
///   <returns> 找到的第1个非法字符.没有则返回string.Empty </returns>
         public   string  FindOne( string  text)
        {
            
for  ( int  len  =   1 ; len  <=  text.Length; len ++ )
            {
                
int  maxIndex  =  text.Length  -  len;
                
for  ( int  index  =   0 ; index  <=  maxIndex; index ++ )
                {
                    
string  key  =  text.Substring(index, len);
                    
if  (m_keys.Contains(key))
                    {
                        
return  key;
                    }
                }
            }
            
return   string .Empty;
        }
    }

这个方式有个缺点.就是使用 String.Substring会创建大量临时对象.

即便是使用了最大长度进行分割字符串的控件.在需要过滤的字符串较长时,还是不见得高效.

 

Trie,又称单词查找树键树,是一种形结构,是一种哈希树的变种。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。

我们先来看一个Trie结构的例子

高效的关键字过滤及查找算法(Trie KO Hash)

在这个Trie结构中,保存了A、to、tea、ted、ten、i、in、inn这8个字符串

首先看我们如何实现这个结构:

  public   class  TrieFilter
    {
        
private  Char m_key;
        
private  Dictionary < Char, TrieFilter >  m_values;
        //根节点,不包含字符,即m_key='\0';
        
public  TrieFilter()
        {
            m_values 
=   new  Dictionary < Char, TrieFilter > ();
        }
        //用于创建子节点
        TrieFilter(Char key)
        {
            m_key 
=  key;
            m_values 
=   new  Dictionary < Char, TrieFilter > ();
        }
        
///   <summary>
        
///  添加关键字
        
///   </summary>
        
///   <param name="key"></param>
         public   void  AddKey( string  key)
        {
            
if  ( string .IsNullOrEmpty(key))
            {
                
return ;
            }
            TrieFilter node 
=   this ;
            
foreach  (var c  in  key)
            {
                TrieFilter subnode;
                
if  ( ! node.m_values.TryGetValue(c,  out  subnode))
                {
                    subnode 
=   new  TrieFilter(c);
                    node.m_values.Add(c, subnode);
                }
                node 
=  subnode;
            }
            //最后的结点,表示单词结束,用‘\0’表示,指向一个空对象
            node.m_values[
' \0 ' =   null ;
        }
    }
}

 这样。一个C#的Trie结构就表示完成。。

下面我们来看看如何实现关键字查找

 

  ///   <summary>
        
///  检查是否包含非法字符
        
///   </summary>
        
///   <param name="text"> 输入文本 </param>
        
///   <returns> 找到的第1个非法字符.没有则返回string.Empty </returns>
         public   string  FindOne( string  text)
        {
            
for  ( int  i  =   0 ; i  <  text.Length; i ++ )
            {
                TrieFilter node;
                
if  (m_values.TryGetValue(text[i],  out  node))
                {
                    
for  ( int  j  =  i  +   1 ; j  <  text.Length; j ++ )
                    {
                        
if  (node.m_values.TryGetValue(text[j],  out  node))
                        {
                            
if  (node.m_values.ContainsKey( ' \0 ' ))
                            {
                                
return  text.Substring(i, (j  +   1   -  i));
                            }
                        }
                        
else
                        {
                            
break ;
                        }
                    }
                }
            }
            
return   string .Empty;
        }

是不是很简单呢?

把由蛮力匹配转变成基于Trie的匹配方案会极大的节省执行时间,这个比较对较长字符串进行匹配时更加明显。

对于长度在20左右的字符串。Tire的匹配速度比Hash的方式快了近10倍。

下面附上完整的代码下载

里面的BadWord.txt包括了7000多个敏感关键字。 

完整的代码及性能对比测试下载:http://files.cnblogs.com/yeerh/FilterTest.rar

 

希望本文能对你有所帮助。欢迎拍砖。