,### 通讯录T9搜索算法
引言:
本人是做车机的,由于项目需要,手机与车机通过蓝牙连接后,需要在车机上实现T9键盘的通讯录搜索功能,算法实现参考了网上的一些想法,但是代码每一行都是自己写的,没有参考过其他人的代码,本人深信自己实现比看懂别人的代码来得快。
算法原理:
T9键盘相信大家都有用过,每个数字键上有0个,3个或4个字母,最常规的想法是将所输入的数字所对应的所有组合罗列出来,再进行搜索,稍微想想就知道这样实现代码太过复杂,而且算法复杂度也高,随着输入数字的增加组合结果是爆炸性增长的。正面突破不行那就反过来呗,把名字转成T9键盘对应的数字,再与输入的数字去匹配,是不是就简单多了?举个例子:“博客”两个字的拼音是:boke,转成T9对应的数字是2653,然后你刚好输入了2653那不就匹配上了,具体的匹配规则当然是跟需求相关的了,不过总的来说大同小异。
另外,算法中的汉字转拼音支持多音字哦!算法还能拿到名字中哪一部分匹配上了,用于实现高亮显示的需求。
本算法实现的匹配规则:
- 每个字首字母匹配或拼音全匹配
- 最后一个字的首字母匹配或开头部分匹配
- 需要连续匹配,但是不必从第一个字开始匹配
上面的需求看起来有点抽象,举个例子就比较容易理解了:
比如:“博博CSDN客”
对应的拼音数字分别是:bo bo csdn ke => 26 26 2736 53 - bo => 2
- ke => 5
- csdn ke => 25
- bo csdn ke => 225
- bo bo csdn ke => 2225
- bo bo csdn ke => 22253
- bo bo csdn ke => 26225
- bo bo csdn ke => 262253
- bo bo csdn ke => 22625
- bo bo csdn ke => 226253
- bo bo csdn ke => 2227365
- bo bo csdn ke => 22273653
- bo bo csdn ke => 262625
- bo bo csdn ke => 2626253
- bo bo csdn ke => 2626273653
等等,还有很多匹配的情况没有全部列出,上面提到支持多音字,那么名字的所有拼音组合都会去匹配一遍,比如“成都重庆”中“都”和“重”是多音字,所有可能的拼音组合有四种:
- cheng du chong qing
- cheng du zhong qing
- cheng dou chong qing
- cheng dou zhong qing
搜索的时候输入的数字会去把上面四种情况都按照上面提到的规则匹配一遍
基本数据结构
算法只实现了一个数据结构,如下:
struct PBCodeUnit
{
string oriStr; //原始字符串
string pyStr; //对应的拼音
bool isChinese; //是否是汉字
}
一个PBCodeUnit对应一个汉字或一串英文,比如“博”对应的结构是{“博”,“bo”, true}
“csdn”对应的结构是{“csdn”,“csdn”, false}
算法实现
算法实现中关键算法有以下几个(代码实现见文后的github链接):
//这个算法将给定的字符串分解成上面提到的所有可能拼音的PBCodeUnit的组合,算法有两步:
//1,将给定字符串中每个字对应的PBCodeUnit放入一个vector,因为可能是多音字,然后再将所有字的vector放入一个vector中。
//比如“重长”分解成[[chong zhong], [chang zhang]];
//2,将上面得到的vector通过另一个算法将其转化为所有可能拼音的组合
//[[chong zhong], [chang zhang]]转化成[[chong chang], [chong zhang], [zhong chang], [zhong zhang]]
const std::vector<std::vector<tPBCodeUnit>> ConvertString2CodeUnits(const std::string& aString);
把名字分解成上面到的结构以后,接下来就是搜索的算法实现了,搜索算法很简单,核心代码也就几十行,很容易看懂。
//index是表示从哪个字开始搜索,因为有可能前面的几个字没匹配上,需要从后面字继续搜索
//cus是名字的一组可能拼音,比如:“成都重庆”=>cus = [cheng du chong qing]
//key是T9键盘输入的用于搜索的数字
//hilight用来得到名字中哪一部分匹配了key,用来高亮显示
bool T9Search::SearchImpl(int index, const std::vector<PBCodeUnit>& cus, const std::string& key, std::string &hilight)
{
std::string searchkey = key;
bool hasMatched = false;
for (size_t i = index; i < cus.size(); i++)
{
PBCodeUnit cu = cus[i];
std::string numstr = Converter::ConvertString2NumberString(cu.pyStr);
if (numstr[0] == searchkey[0])
{
hasMatched = true;
if (cu.isChinese)//是否是中文
{
hilight += cu.oriStr;
if (searchkey.size() <= numstr.size() && searchkey == numstr.substr(0, searchkey.size()))
{
return true;
}
if (numstr == searchkey.substr(0, numstr.size()))
{
searchkey.erase(0, numstr.size());
}
else
{
searchkey.erase(0, 1);
}
}
else
{
if (searchkey.size() <= numstr.size() && searchkey == numstr.substr(0, searchkey.size()))
{
hilight += cu.oriStr.substr(0, searchkey.size());
return true;
}
if (numstr == searchkey.substr(0, numstr.size()))
{
searchkey.erase(0, numstr.size());
hilight += cu.oriStr;
}
}
}
else
{
//有可能第一个匹配上了,第二个字没匹配上,所以需要从第二个开始重新搜索
//比如“博博客”,输入25的话,第一个“博”匹配了2,第二个“博”匹配不上5,会导致匹配失败
if (hasMatched)
{
hilight = "";
return SearchImpl(i, cus, key, hilight);
}
}
}
//key匹配完了就认为匹配成功了
if (searchkey.size() > 0)
{
return false;
}
return true;
}
上面的搜索实现参数不好理解,封装一下就好理解了:
bool Search(const std::string& name, const std::string& key, std::string& hilight)
{
std::vector<std::vector<PBCodeUnit>> cusOfAll = Converter::ConvertString2CodeUnits(name);
for (size_t i = 0; i < cusOfAll.size(); i++)
{
if (SearchImpl(0, cusOfAll[i], key, hilight))
{
return true;
}
}
return false;
}
这样,T9搜索算法大部分就算完成了,但是这样的实现还有个小bug,对于某些名字首字母搜索的时候可能会失败,比如我的通讯录里面有个名字是“lu ting yong”,首字母搜索的时候输入589会匹配失败,因为第一个字的u和第二个字的t是同一个数字键上面,lu一次性就匹配了58两个数字,剩下一个9去匹配t肯定就失败了,目前我的解决办法是专门做一个首字母搜索先搜一遍,代码几乎跟上面的代码一样,可参见github,这样以后总体上就没有明显的bug了,如果有更好的办法欢迎讨论。
注:这个算法是基于中文UTF8编码的,如果是其它编码可以先转成UTF8或者就绕道吧。。。
如果有其它疑问欢迎留言,有问题轻拍哈