关键字近似搜索(c#以及lua实现版)

浏览器带有智能提示功能,如图:

今天就利用算法实现这个功能,并集成在cocos2d游戏中。

一、实现原理

我们可以构造一个问题集,搜集所有可能的问题,并利用关键词去匹配这些问题。显示匹配出的结果,作为提示。

字符串近似匹配算法很多,这里直接用成熟的动态规划(DP)+编辑距离(LevenshTein Distanc)的算法,来实现匹配。先上C#源码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace BestString
{
    public static class SearchHelper
    {
        public static string[] Search(string param, string[] datas)
        {
            if (string.IsNullOrWhiteSpace(param))
                return new string[0];
 
            string[] words = param.Split(new char[] { ' ', ' ' }, StringSplitOptions.RemoveEmptyEntries);
 
            foreach (string word in words)
            {
                int maxDist = (word.Length - 1) / 2;
 
                var q = from str in datas
                        where word.Length <= str.Length
                            && Enumerable.Range(0, maxDist + 1)
                            .Any(dist =>
                            {
                                return Enumerable.Range(0, Math.Max(str.Length - word.Length - dist + 1, 0))
                                    .Any(f =>
                                    {
                                        return Distance(word, str.Substring(f, word.Length + dist)) <= maxDist;
                                    });
                            })
                        orderby str
                        select str;
                datas = q.ToArray();
            }
 
            return datas;
        }
 
        static int Distance(string str1, string str2)
        {
            int n = str1.Length;
            int m = str2.Length;
            int[,] C = new int[n + 1, m + 1];
            int i, j, x, y, z;
            for (i = 0; i <= n; i++)
                C[i, 0] = i;
            for (i = 1; i <= m; i++)
                C[0, i] = i;
            for (i = 0; i < n; i++)
                for (j = 0; j < m; j++)
                {
                    x = C[i, j + 1] + 1;
                    y = C[i + 1, j] + 1;
                    if (str1[i] == str2[j])
                        z = C[i, j];
                    else
                        z = C[i, j] + 1;
                    C[i + 1, j + 1] = Math.Min(Math.Min(x, y), z);
                }
            return C[n, m];
        }
    }
}

其中,Distance函数用来计算编辑距离,而Search函数中,把备选问题拆分成一个个与关键字等长的子字符串,并计算自字符串与关键字的编辑距离。遍历所有的问题,以及遍历单个备选问题所有的子字符串来匹配关键字。返回所有满足搜索条件的备选问题。这里需要注意一点,关键字长度必须小于问题长度,否则无法将备选问题拆分成与关键字等长的子字符串。当然,解决这个问题有个很简单的办法:当关键字比备选问题长时,关键字与备选问题互换位置即可。

由于需要将这个功能集成到cocos2d游戏中,游戏采用lua脚本开发,所以给出lua版本:

cc.exports.SearchTool = class("SearchTool")

-- getStrUnicodeList UTF-8转unicode
-- @param 源字符串
-- @return Table unicode编码数组
-- =======================================
local function getStrUnicodeList(convertStr)
   
    if type(convertStr)~="string" then
		return convertStr
    end

	local bit = require("bit")
	local unicodeList = {}
    local i=1
    local num1=string.byte(convertStr,i)
	while num1~=nil do
		local tempVar1,tempVar2
        if num1 >= 0x00 and num1 <= 0x7f then
            tempVar1=num1
            tempVar2=0
        elseif bit.band(num1,0xe0)== 0xc0 then
            local t1 = 0
            local t2 = 0
            t1 = bit.band(num1,bit.rshift(0xff,3))
            i=i+1
            num1=string.byte(convertStr,i)
            t2 = bit.band(num1,bit.rshift(0xff,2))
            tempVar1=bit.bor(t2,bit.lshift(bit.band(t1,bit.rshift(0xff,6)),6))
            tempVar2=bit.rshift(t1,2)
        elseif bit.band(num1,0xf0)== 0xe0 then
            local t1 = 0
            local t2 = 0
            local t3 = 0
            t1 = bit.band(num1,bit.rshift(0xff,3))
            i=i+1
            num1=string.byte(convertStr,i)
            t2 = bit.band(num1,bit.rshift(0xff,2))
            i=i+1
            num1=string.byte(convertStr,i)
            t3 = bit.band(num1,bit.rshift(0xff,2))
            tempVar1=bit.bor(bit.lshift(bit.band(t2,bit.rshift(0xff,6)),6),t3)
            tempVar2=bit.bor(bit.lshift(t1,4),bit.rshift(t2,2))
        end

		table.insert(unicodeList, string.format("\\u%02x%02x",tempVar2,tempVar1))
        i=i+1
        num1=string.byte(convertStr,i)
    end
    return unicodeList
end

-- subTable 取Table中,从bIdx下标开始的cnt个元素构成的子集
-- @param 源Table
-- @param 起始下标
-- @param 子集个数
-- @return Table Table子集
-- =======================================
local function subTable(table, bIdx, cnt)
	local t = {}
	local idx = 1
	for i=bIdx, bIdx + cnt - 1 do
		t[idx] = table[i]
		idx = idx + 1
	end
	return t
end

-- distance 计算两个字符串之间的编辑距离
-- @param 源字符串1
-- @param 源字符串2
-- @return int 编辑距离
-- =======================================
local function distance(str1, str2)
	local n = #str1
	local m = #str2
	local C = {}
	
	for i=0,n do
		C[i] = i
	end
	for i=0,m do
		C[i*(m+1)] = i
	end

	for i=0,n-1 do
		for j=0,m-1 do
			local x = C[i + (j+1)*(m+1)] + 1
			local y = C[i + 1 + j*(m+1)] + 1
			local z
			if str1[i] == str2[j] then
				z = C[i + j*(m+1)]
			else
				z = C[i + j*(m+1)] +1
			end
			C[i+1 + (j+1)*(m+1)] = math.min(math.min(x,y), z)
		end
	end

	return C[n + m*(m+1)]
end

-- Search 单个关键字搜索
-- @param 关键字
-- @param 被搜索的字符串集
-- @return Table 符合搜索的字符串集
-- =======================================
function SearchTool:Search(keyWord, questionSet)
	if keyWord == nil then
		return
	end

	keyWord = getStrUnicodeList(keyWord)
	-- 因为unicode都是以\u开头,单个字会产生误差,所以加空格补成两个字
	if #keyWord == 1 then
		keyWord[2] = " "
	end

	local result = {}
	for _, str in pairs(questionSet) do
		local isFinded = false -- 是否满足匹配

		local str_ = getStrUnicodeList(str)
		local keyWord_ = keyWord

		-- 关键字长度大于问题长度,则用问题去搜索关键字(保证用短的去匹配长的)
		if #str_ < #keyWord_ then
			local tmp = keyWord_
			keyWord_ = str_
			str_ = tmp
		end

		local maxDist = math.floor((#keyWord - 1) / 2)

		for i = 0, maxDist do
			
			for j = 0, math.max(#str_ - #keyWord_ - i + 1, 1) - 1 do
				if distance(keyWord_, subTable(str_, j + 1, #keyWord_ + i)) <= maxDist then
					isFinded = true
					table.insert(result, str)
					break
				end
			end

			if isFinded then
				break
			end

		end
	end
	return result

end

-- SearchWithCompositeKeys 多个关键字搜索,结果取交集
-- @param 关键字集
-- @param 被搜索的字符串集
-- @return Table 符合搜索的字符串集
-- =======================================
function SearchTool:SearchWithCompositeKeys(keyWordSet, questionSet)

	local result = questionSet
	
	for k,v in pairs(keyWordSet) do
		result = self:Search(v, result)
	end

	return result
end

在C#代码的基础上,lua版本的代码对功能进行了扩展。lua代码需要注意的问题有:

1、编码问题:为了支持多种语言,需要采用utf-8编码。但是lua里操作需要使用unicode编码。所以涉及到一个转码问题。这部分这篇文章已说明。

2、关键字长度问题:这个问题在上边提到过,关键字超长时,用问题去匹配关键字就行。

3、多关键字组合问题:取所有关键字搜索结果的交集。

二、cocos2d游戏实现

这里主要实现:当输入框文字变化时,自动刷新搜索结果。

新建一个TextField控件(取名inputBox),并绑定函数:

local function onTxtChanged(sender, eventType)
        if eventType == TEXTFIELD_EVENT_INSERT_TEXT or eventType == TEXTFIELD_EVENT_DELETE_BACKWARD then
            local splitChar = ' '
            local key = StringTool:stringSplit(self.inputBox:getString(), splitChar)
            if key[#key] == "" then
                table.remove(key,#key)
            end  

            if #key == 0 then
            	self:refreshQuestionList(nil)
            else   
            	self.listState = listOnShow.SearchList
            	self:seekChildByName("pSystemQuestions"):setVisible(false)
           	self:showSearchItem()
           	local result = SearchTool:SearchWithCompositeKeys(key, self.questionSet)
           	self:refreshQuestionList(result)
           end   
        end
    end
    inputBox:addEventListenerTextField(onTxtChanged)
每当输入框文字变化时,重新获取输入框中的文字,并使用空格作为划分符进行关键词的划分。这里refreshQuestionList是自己实现的刷新问题列表(ListView)的方法,传入要显示的条目信息即可。这里调用的stringSplit方法大家可以自己实现,也可以参考:

-- 参数:待分割的字符串,分割字符
-- 返回:子串表.(含有空串)
function StringTool:stringSplit(str, splitChar)
	local sub_str_tab = {};
	
	while (true) do
		local pos = string.find(str, splitChar);
		if (not pos) then
			local size_t = table.getn(sub_str_tab)
			table.insert(sub_str_tab,size_t+1,str);
			break;
		end
		
		local sub_str = string.sub(str, 1, pos - 1);
		local size_t = table.getn(sub_str_tab)
		table.insert(sub_str_tab,size_t+1,sub_str);
		local t = string.len(str);
		str = string.sub(str, pos + 1, t);
	end
	return sub_str_tab;
end

另外,刷新界面的方法就不展示了,掌握基本的Cocos2d客户端知识就能自己实现一套展示列表了。

最后,上效果图:



输入测试两个字,自动搜索问题库,列出相关问题。ok,手工!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值