浏览器带有智能提示功能,如图:
今天就利用算法实现这个功能,并集成在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,手工!