使用Lua实现字典树,进行敏感词匹配

我们在玩游戏的时候,聊天窗口经常会看到有“我**谁”等文字,其中的“**”可能是一个骂人的词语或者是政治敏感词汇,游戏审核部门会要求游戏必须屏蔽敏感词汇,否则不允许上线,所以骂人的词语或句子就被替换成*号了。

游戏项目中是如何处理这样的敏感字匹配的呢?KMP?还是Boyer-Moore算法?其实都不是,基本上这类匹配用的都是字典树的方式, 字典树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种,它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

它有3个基本性质:
1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3. 每个节点的所有子节点包含的字符都不相同。

因为现在大多数项目都是使用Lua版本进行热更新,逻辑都写在lua上,所以使用Lua实现字典树是很有必要的。

核心代码如下:

local WordTree ={}

--树节点创建
function WordTree.CreateNode(self, char, flag, childs)
	local node = {}
	node.char = char or nil		--字符
	node.flag = flag or 0		--是否结束标志,0:继续,1:结尾
	node.childs = childs or {}	--保存子节点
	node.isleaf = true --childs数量为0则是叶子节点
	return node
end

//创建根结点
WordTree.m_RootNode = WordTree.CreateNode('root') 

//更新字典树
function WordTree.UpdateNodes(self, words)
	for i, v in pairs(words) do
		local chars = self:GetCharList(string.lower(v))
		if #chars > 0 then
			self:InsertNode(self.m_RootNode, chars, 1)
		end
	end
end

--插入节点
function WordTree.InsertNode(self, parent, chars, index)
	local node = self:FindNode(parent, chars[index])
	if node == nil then
		node = self:CreateNode(chars[index])
		parent.isleaf = false
		table.insert(parent.childs, node)
	end
	local len = #chars
	if index == len then
		node.flag = 1
	else
		index = index + 1
		if index <= len then
			self:InsertNode(node, chars, index)
		end
	end
end

--节点中查找子节点
function WordTree.FindNode(self, node, char)
	local childs = node.childs
	for i, child in ipairs(childs) do
		if child.char == string.lower(char) then 
			return child
		end
	end

end

function WordTree.GetCharList(self, str)
	local list = {}
	while str do
		local utf8 = string.byte(str,1)
		if utf8 == nil then
			break
		end
		--utf8字符1byte,中文3byte
		if utf8 > 127 then
			local tmp = string.sub(str,1,3)
			table.insert(list,tmp)
			str = string.sub(str,4)
		else
			local tmp = string.sub(str,1,1)
			table.insert(list,tmp)
			str = string.sub(str,2)
		end
	end
	return list
end

--将字符串中敏感字用*替换返回
-- flag == true,替换的 * 的数量 = 敏感词长度;flag == false,默认使用 *** 替换
function WordTree.ReplaceMaskWord(self, str, flag)
	local chars = self:GetCharList(str)
	local index = 1
	local node = self.m_RootNode
	local prenode = nil
	local matchs = {}
	local isReplace = false
	local lastMatchLen = nil
	local totalLen = #chars
	local function replace(chars, list, last, flag)
        local stars = ""
		for i=1, last do
			local v = list[i]
			if flag then
				chars[v] = "*"
				isReplace = true
			else
				if isReplace then
					chars[v] = ""
				else
					chars[v] = "***"
					isReplace = true
				end
			end
		end
	end
	while totalLen >= index do
		prenode = node
		node = self:FindNode(node, chars[index])
		if chars[index] == " " then
			if #matchs then
				table.insert(matchs, index)
				node = prenode
			else
				node = self.m_RootNode
			end
		elseif node == nil then
			index = index - #matchs
			if lastMatchLen then
				replace(chars, matchs, lastMatchLen, flag)
				index = index + (lastMatchLen - 1)
				lastMatchLen = nil
			else
				isReplace = false
			end
			node = self.m_RootNode
			matchs = {}
		elseif node.flag == 1 then
			table.insert(matchs, index)
			if node.isleaf or totalLen == index then
				replace(chars, matchs, #matchs, flag)
				lastMatchLen = nil
				matchs = {}
				node = self.m_RootNode
			else
				lastMatchLen = #matchs
			end
		else
			table.insert(matchs, index)
		end
		index = index + 1
	end
	local str = ''
	for i, v in ipairs(chars) do
		str = str..v
	end
	return str
end

--字符串中是否含有敏感字
function WordTree.IsContainMaskWord(self, str)
	local sCheck = string.gsub(str, " ", "")
	local chars = self:GetCharList(sCheck)
	local index = 1
	local node = self.m_RootNode
	local masks = {}
	while #chars >= index do
		node = self:FindNode(node, chars[index])
		if node == nil then
			index = index - #masks 
			node = self.m_RootNode
			masks = {}
		elseif node.flag == 1 then
			return true
		else
			table.insert(masks,index)
		end
		index = index + 1
	end
	return false
end


return WordTree

假如有一组屏蔽字为:

local data={“二货”,  “傻B”,   “二B”}

首先根据屏蔽字表创建字典树:WordTree.UpdateNodes(data);

然后就可以判断是否存在敏感字,如WordTree .IsContainMaskWord(“你是 二货 “)返回true。

快速排序C#实现

快速排序是7大排序算法中最高效的算法,它在C++中的STL、java sdk 、.Net中都有实现。

它的算法时间复杂度为O(nlogn),它的原理是通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

具体C#代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sort
{
    class Program
    {
       //快速排序
        static void quickSort(int[] arr, int low, int high)
        {
	        if (low < high) {
		        // 找寻基准数据的正确索引
		        int index = getIndex(arr, low, high);

		        // 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
		        quickSort(arr, 0, index - 1);
		        quickSort(arr, index + 1, high);
	        }
        }
       //选取枢轴,也是先选取当中的一个关键字,比如3,然后想办法把它放到一个位置,使它左边的值都比它小,右边的值都比它大,将这样的关键字称为枢轴。
       static int getIndex(int[] arr, int low, int high)
       {
	        // 基准数据
	        int tmp = arr[low];
	        while (low < high) {
		        // 当队尾的元素大于等于基准数据时,向前挪动high指针
		        while (low < high && arr[high] >= tmp) {
			        high--;
		        }
		        // 如果队尾元素小于tmp了,需要将其赋值给low
		        arr[low] = arr[high];
		        // 当队首元素小于等于tmp时,向前挪动low指针
		        while (low < high && arr[low] <= tmp) {
			        low++;
		        }
		        // 当队首元素大于tmp时,需要将其赋值给high
		        arr[high] = arr[low];

	        }
	        // 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
	        // 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
	        arr[low] = tmp;
	        return low; // 返回tmp的正确位置
        }
        static void Main(string[] args)
        {
            int[] arr = { 1, 3, 2, 4, 0, 5, 6 };
            quickSort(arr,0,arr.Length-1);
            for (int i = 0; i < arr.Length; ++i) {
                Console.WriteLine(arr[i]);
            }
            Console.ReadKey();
        }
    }
}

以上代码在VS2017中运行通过。

递归实现斐波那契数列

递归的定义:把一个直接调用自己或通过一系列的调用语句间接的调用自己的函数,叫做递归函数。

斐波那契数列定义如下:
{ 0                        n=0
f(n){ 1                        n=1
{ f(n-1)+f(n-2)    n>1

   public int fbnq(int n)
   {
          if(n < 0)
	     return 0;
	  if (n < 2){
             return n;
	  }
	  else{
	     return fbnq(n - 1) + fbnq(n - 2);
	  }
    }

c#实现KMP算法的优化

KMP算法是有三位前辈提出来的一个模式匹配算法,也就是从一个字符串中查找子串的算法,类似C#里面 indexOf ()函数的功能。

如果利用普通的匹配算法去查找子串,最坏的情况下算法复杂度是O((n-m+1)*m) n代表的主串的长度,m代表的是子串的长度,很显然这个算法比较低效。

所以就有大神们发明了KMP算法,它的原理是比普通的匹配算法节省了比对步骤,如主串m=”abcababca”,子串s=”abcabx”,前5个字符是相同的第6个字符不等,那么可以判断子串s中的第一个字符和主串中的第二个,第三个字符也不相等。KMP算法正是省略这些可以判断步骤达到优化匹配算法的目的。

代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace KMP
{
    class Program
    {
        //计算出子串返回的next数组,当子串不匹配时根据next数组值进行回溯
        static void GetNext(string p, int []next){
            int len = p.Length;
            int i = 0;
            int j = -1;
            next[0] = -1;
            while (i < len)
            {
                if (j == -1 || p[i] == p[j]){
                    ++j;
                    ++i;
                    next[i] = next[j];
                }
                else{
                    j = next[j];//若字符不相同,则j值回溯
                }
            }
        }

        static int Main(string[] args)
        {
            string str = "abccc aabb";
            string sub = "abb";
            int []next = new int[100];
            //计算出子串返回的next数组
            GetNext(sub, next);
            int i = 0;
            int j = 0;
            while (i < str.Length && j < sub.Length) {
                if (j == -1 || str[i] == sub[j] )
                {
                    ++i;
                    ++j;
                }
                else
                    j = next[j];//j回溯到合适的位置
            }
            if (j == sub.Length)
            {
                Console.WriteLine(i - j);
                Console.ReadKey();
                return i - j;
            }
            return 0;
        }
    }
}
  

KMP算法的优化:

后来有人发现上面的KMP算法还是有缺陷的,比如主串m=”aaaabcde” ,子串s=”aaaaax”,由于子串的前面4个字符和主串都是相等的,那么可以用首位next[1]的值去取代与它相等的字符后续的next[j]的值,因此可以对获取next数组的函数进行改良。

static void GetNext(string p, int []next){
            int len = p.Length;
            int i = 0;
            int j = -1;
            next[0] = -1;
            while (i < len)
            {
                if (j == -1 || p[i] == p[j]){
                    j++;
                    i++;
                    if (i < len && p[i] != p[j])
                        next[i] = j;
                    else
                        next[i] = next[j];//改良部分
                }
                else{
                    j = next[j];
                }
            }
        }

反转单链表,递归实现

ListNode * reverseList(ListNode * list) {
if ( list == NULL || list ->next == NULL) //链表为空直接返回,而 list ->next为空是递归基
return list;
ListNode * newHead = reverseList( list ->next); //一直循环到链尾
list ->next->next = list ; //翻转链表的指向
list ->next = NULL; //记得赋值NULL,防止链表错乱
return newHead; //新链表头永远指向的是原链表的链尾
}

反转单向链表

   
 网上有许多代码 看了一下发现他们的好多反转后都缺失了头结点.
 所以自己又写了一个.

//
#include "stdafx.h"
#include "iostream"
using namespace std;
struct ListNode
{
ListNode* next;
int data;
	ListNode()
	{
	next=NULL;
	data=0;
 
	}
};
 
ListNode *ReverseList(ListNode *&list)
{
	ListNode *head = list, *last = NULL;
	list = list->next;
 	while (list){
		head->next = last;
		last = head;
		head = list;
		list = list->next;
	}
	head->next = last;
	return head;
}




//创建链表添加结点;
void CreateNode(ListNode*&head, int data)
{
	ListNode *fist = head;
	while (head != NULL)
	{
		if (head->next == NULL)
		{
			ListNode *tem = new ListNode();
			tem->data = data;
			head->next = tem;
			break;
		}
		head = head->next;
	}
	head = fist;
}
int main(int argc, char* argv[])
{
ListNode *head=new ListNode();
CreateNode(head,1);
CreateNode(head,2);
CreateNode(head,3);
CreateNode(head,4);
CreateNode(head,5);
CreateNode(head,6);
CreateNode(head,7);
ListNode *tem=head;

while (head!=NULL)
{
cout<<head->data<<endl;
head=head->next;
}
ListNode* re=ReverseList(tem);
cout<<"反转链表:"<<endl;
while (re!=NULL)
{
cout<<re->data<<endl;
re=re->next;
}
getchar();
return 0;
}

翻转句子中单词的顺序

     题目描述:翻转句子中单词的顺序,但单词内字符的顺序不变。句子中单词以空格符隔开。
     为简单起见,标点符号和普通字母一样处理。如:"I am a student."翻转成"student. a am I"。
             在上述题目中我看到网上好多答案在最后一个单词是没有经过翻转的,假如句子翻转后最后
     一个单词不是一个字母(也就是原句中的第一个单词不是一个字母),那么就会出现最后一个单词没有翻转的     情况.所以在最后还要进行一次单词翻转.
     如: “My name is fgreen”翻转后应该是”fgreen is name My”
    代码如下:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
void ReverseWord(char *str,int start,int end)
{
	char tem=' ';
	while (start<end)
	{
		 tem=str[start];	
		 str[start]=str[end];
		 str[end]=tem;
		 ++start;
		 --end;
	}
}
void ReverseSentence(char *str)
{ 
	if(str==NULL||*str=='\0')
		return;
	ReverseWord(str,0,strlen(str)-1);
	int i=0;
	int t=i;
	while (true)
	{
         if(str[i]==' ')
		 {   
			 ReverseWord(str,t,i-1); 
			 t=i+1;		
		 }
        ++i;
		if(str[i]=='\0')//最后一个单词也要翻转;
        { ReverseWord(str,t,i-1); 
		  break;
		}
	}
}
int main(int argc, char* argv[])
{
	char str[]="My name is fgreen";
	ReverseSentence(str);
	cout<<str<<endl;
	getchar();
	return 0;
}

 

不使用递归实现斐波那契数列

由于递归的写法效率太过低下,要求用非递归实现。

 

定义斐波那契数列如下:
{ 0                        n=0
{ 1                        n=1
{ f(n-1)+f(n-2)    n>1

// ConsoleApplication4.cpp : 定义控制台应用程序的入口点。
int fbnqsl(int n)
{
int tem = 0, h1 = 0, h2 = 1;
if (n < 0)
return -1;
if (1 == n || 0 == n)
return n;
for (int i = 2; i < n; ++i)
{
tem = h2;
h2 += h1;
h1 = tem;
}
return h1 + h2;
}
int main(int argc, char* argv[])
{
cout << fbnqsl(10);
getchar();
return 0;
}

 

第一个只出现一次的字符

在一个字符串中找到第一个只出现一次的字符。如输入aabbccddvllllsd,则输出v。听说此题目为2006年谷歌笔试.

C++代码如下:

// ConsoleApplication2.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;

char FindFirstShow(char *data)
{
	int s[256]={0};
	char *str=data;
	while(*str!='\0')
	{   
        ++s[*str];
		++str;
	}
  for(char* i=data;*i!='\0';++i)
	  if(s[*i]==1)
	  {
		  return *i;
	  }
	  return NULL;	
}
int main(int argc, char* argv[])
{
	char *str="aabbccddvllllsd";
	cout<<"第一个不重复的字符是"<<FindFirstShow(str)<<endl;
	getchar();
	return 0;
}

 

 

在一个有序数组中查找2个数和为SUM的个数

输入一个递增排序的数组和一个数字sum,在数组中查找两个数,使得它们的和正好是s。
如果有多对数字的和等于sum,输出有多少对数满足。

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "iostream"
using namespace std;
//查找函数;
int  FindSum(int *arr,int length,int sum)
{
	int *end=arr+length-1,*start=arr,number=0;

	while (start!=end)
	{  
		if(*start+*end<sum)
		   ++start;
		else if(*start+*end>sum)
			--end;
		else
		{  
			 ++number;
			 --end;//这里写++start也可以
		}
	}
	return number;
}
int main(int argc, char* argv[])
{

	int a[]={1,2,3,4,5};//测试数组
	cout<<"数组中和为7的有"<<FindSum(a,5,7)<<"对"<<endl;
	getchar();
	return 0;
}