本文主要分享算法设计思路,其中代码均原创,供大家参考和学习
开始学习数据结构的时候,很多像顺序表、二叉树、邻接链表的结构学会了完全不知道该在什么地方使用。到学习算法一段时间,做了些题目后,发现自己还是需要阅读一些博客和以前写过的数据结构去“启发式”的引导自己解决问题。其实我们一开始在学习数据结构的时候就应该去思考每个数据结构可以应用的场景,这样学习数据结构才有意思,才能“玩转数据结构”。
通讯录的拼音搜索也是我们常用的场景,“常用”、“不起眼”、但“有意思”。本项目也是我在大二数据结构课程设计的选题,分享一下我的思想给大家。
提示:问题描述很重要,大家以后做算法或者开发也一定要关注问题描述!!!
我们作为个人每天都使用通讯录(包括微信、qq中的通讯录),但考虑一个比较大的场景,许多大型企业也拥有企业的通讯录,可能有上千甚至上万人的名录,那么我们就希望做设计一个快速检索算法,能够通过输入部分的关键字的首拼字母快速的过滤出你想要的联系人。
存储名字的每个字对应的音节采用“字符串”类型的顺序表“StringList”存储元素。一个顺序表结构存处一个名字的读音组合,比如“guanyunchang”可以存储为一个顺序表结构,“guan”存入item[0],“yun”存入item[1],“chang”存入item[2]。可以通过item[0][0]访问到‘g’。
其实这里也可以采用字符串类型数组,但是毕竟学习了数据结构,想练习一下顺序表的使用,并且自己写的顺序表在使用上更加灵活。
//顺序表
struct StringList {
string* item;
int capacity;
int length;
//构造函数
StringList(int c=10) {
capacity = c;
length = 0;
item = new string[capacity];
}
//添加元素
void append(string s) {
if (length == capacity) {
capacity += 10;
item = new string[capacity];
}
item[length++] = s;
}
//返回指定位置index的元素
string getItem(int index) {
return item[index];
}
//显示全部元素
void show() {
for (int i = 0; i < length; i++) {
cout << item[i] << " ";
}
cout << endl;
}
};
拼音库可以用哈希表结构存储,这里用二维数组存储。
string hashs[Max_size][Max_size];//拼音库hashs表
C++处理拼音库(不如Java和C#方便),我采用的拼音表我提前处理过,我在这里给出资源链接。(大家下载保存为txt文件,设置ANSI编码格式)
拼音库文件下载-csdn
读取拼音表的方式(我的方式,大家可以参考,避免大家踩坑!)
//初始化hashs拼音库表
void initHashMap() {
string str;
ifstream inf;
//保存为ANSI编码格式
inf.open("yinjie.txt");
while (getline(inf, str)) {
//cout << str << endl;
int a1 = str[0];
int a0 = str[1];
a1 = -a1;
a0 = -a0;
string str1;
str1 = str.substr(2, str.length() - 2);
string str3 = str1;
hashs[a1][a0] = str3;
}
inf.close();
}
通讯录表可以爬取网上的名字数据(保证不犯法的前提下),或者自己写一个名字生成函数,准备好姓名的常用字表,按照一定的规则生成。在这为了测试简单直接写字符串数组存储。
string peopleTable[6];//联系人表
//初始化通讯录表
void initPeopleTable() {
peopleTable[0] = "关云长";
peopleTable[1] = "丁云长";
peopleTable[2] = "丁乐乐";
peopleTable[3] = "张三";
peopleTable[4] = "李四";
peopleTable[5] = "迪丽热巴";
}
在正式进入递归去匹配之前,需要提前做一些工作,那么我们不如就把提前做的工作单独写,核心的递归回溯的算法单独抽象出去写,这样也提高的算法的复用性,并且也方便算法修改和重构。
提前做:
该函数类型为bool,被调用返回得到匹配还是未匹配的结果。
该函数的流程图如下:
代码:
(这里未给出处理多音,多音则是在该处理函数中增加音节的排列组合,实现方式很多:可以每次从每个字的音节中截断取一个处理啊生成组合,调用核心算法匹配;也可以提前取生成每个名字的多个音节组合;)
提示:推荐大家处理一个名字的多音组合时产生一个就匹配一个,这样会减少很多的排列组合的代价,当然也可以把名字中对应位置的常用音节在拼音库表中做调整,这样也可以大幅提高搜索匹配的时间。
//不完全拼音模糊匹配(不处理多音字)
bool PinyinMatch(string search, string name) {
StringList pinyinItem;
for (int i = 0; i < name.length(); i += 2) {
string s = getYinJie(name.substr(i, 2));//从拼音表中获得音节字符串
//cout << s.length() << endl;//验证一下存进去的字符串长度,是否截断
int pos = 0;
for (int j = 0; j < s.length(); j++) {
//判断是否含有空格,这里不考虑多音字,直接在第一个空格处截断取第一个读音
if (s[j] == ' ') {
pos = j; break; }
}
if (pos == 0) pinyinItem.append(s);//不含空格,直接存入pinyinItem[]
else {
string yinjie = s.substr(0, pos);//含空格,截断以后存入pinyinItem[]
pinyinItem.append(yinjie);
}
}
pinyinItem.show();//输出音节表测试
//调用匹配核心算法
if (Match(search, 0, pinyinItem, 0, 0) == true) return true;//如果匹配算法通过,认为该条记录匹配通过,返回true
return false;//匹配未通过返回false
//屏蔽核心算法测试pyItem,直接return true
//return true;
}
首先了解我的Match函数的基础思想。该匹配函数需要传入输入的待匹配的字符串,以及名字转拼音音节后的存储的顺序表。然后根据递归回溯原理进行匹配。
首先看一下原理:
以输入“guanyucha”匹配“关云长”为例,“关云长”在工具函数pinyinMatch中得到pyItem为“guan”“yun”“chang”,然后首先进行“guanyucha”的首字母‘g’与“guan”比较相等后,递归,’u’,’a’,’n’分别继续匹配“guan”,该层匹配结束后,继续匹配’y’与“yun”的首字母匹配,匹配到’c’无法继续在“yun”中匹配,进入pyItem的下一个元素“chang”匹配之后的元素,匹配到’a’结束匹配,返回true标志匹配成功。
提示:核心核心核心!!!
每次传入Match函数中的参数有:
递归回溯图:
递归回溯部分:
//匹配算法(递归回溯)
bool Match(string search,int searchIndex,StringList pyItem,int yinjieIndex,int yinjieStart) {
//这个if是规定第一个首字母必须匹配的一种匹配方法,例如yc是匹配不到gyc的
//if (searchIndex == 0)//如果在输入的search[]的第一个字母位置
// //第一个必须匹配&&如果只有一个字符,算匹配成功,否则从第二个字符开始比较,进入下一层递归
// return search[0] == pyItem.getItem(0)[0] && (search.length() == 1 || Match(search, 1, pyItem, 0, 1));
//这里处理如果guan yun chang,yc可以匹配,c不能匹配
if (searchIndex == 0) {
if (yinjieIndex > pyItem.length - 1) return false;
startFloorIndex++;
return search[0] == pyItem.getItem(yinjieIndex)[0] ? Match(search, 1, pyItem, yinjieIndex, 1) : Match(search, 0, pyItem, yinjieIndex + 1, 0);
}
//判断不越界&&判断匹配
if (pyItem.getItem(yinjieIndex).length() > yinjieStart && search[searchIndex] == pyItem.getItem(yinjieIndex)[yinjieStart])
//如果这是最后一个字符,检查之前的声母是否依次出现
//如果不是最后一个字符,同一个字的拼音连续匹配
return searchIndex == search.length() - 1 ? keyMatch(search, pyItem, yinjieIndex) : Match(search, searchIndex + 1, pyItem, yinjieIndex, yinjieStart + 1);
//判断不越界&&不能拼音连续匹配的情况下,看看下一个字的拼音的首字母能否匹配
else if (pyItem.length > yinjieIndex + 1 && search[searchIndex] == pyItem.getItem(yinjieIndex + 1)[0])
//如果这是最后一个字符,检查之前的声母是否依次出现
//如果不是最后一个字符,去判断下一个字拼音的第二个字母
return searchIndex == search.length() - 1 ? keyMatch(search, pyItem, yinjieIndex) : Match(search, searchIndex + 1, pyItem, yinjieIndex + 1, 1);
else if (pyItem.length > yinjieIndex + 1)//回退
for (int i = 1; i < searchIndex; i++)
if (Match(search, searchIndex - i, pyItem, yinjieIndex + 1, 0)) return true;
return false;
}
提示:代码中的keyMatch函数后面会解释,先说要注意的两个点!
递归回溯算法设计——处理几种不匹配返回false的情况:
匹配算法的完善——不能“截断匹配”:
例:关云长—用“gy”,“yc”可搜索,用“gc”不可搜索。
(实现原理与首字母的匹配原理一致,判断此记录的每个汉字的音节的首字母是否在搜索拼音中出现过,且按照顺序出现,用搜索字符控制判断次数。)
提示:看不懂递归回溯图的,我再解释一下,看懂的跳过。
辅助匹配部分(检验作用):
//辅助变量:用于取到在辅助函数中第几层开始判断每个音节首字母顺序
int startFloorIndex = -1;
//辅助匹配函数,确保pyItem.getItem(n)[0]都按照顺序出现
//防止guan yu chang匹配gc(跳过了yu达到匹配的这种情况)
bool keyMatch(string search,StringList pyItem,int yinjieIndex) {
int lastIndex = 0;
int i;//i代表开始的层数
for (i = startFloorIndex; i < yinjieIndex + startFloorIndex; i++) {
lastIndex = IndexOf(search, pyItem.getItem(i)[0], lastIndex);
if (lastIndex == -1) return false;
lastIndex++;
}
return true;
}
本来是没有这部分的,因为本篇博客也只想阐述算法的设计思想。但是我对然不能分享我整个项目的源码给大家,但是可以给大家演示和说明一下我实现了核心的算法后,最终的效果,也希望大家能有启发,或者可以开发出可视化的界面达到更好的效果。
运行开始,在指定位置生成通讯录(随机10000条)。
以“长乐宁”为例,输入“cln”,得到“长乐宁”。
输入“zhanglening”,得到“长乐宁”。
输入“zn”“zhangn”,无法得到“长乐宁”。
输入“ln”,得到“长乐宁”。
我的代码还有许多可改进的地方,希望大家多多批评指正!
希望本篇博客能让大家有所收获。
本算法最终是团队成果的呈现,在此特别感谢铁锅炖乔治couple,马总,yyj-么,韦哥的大力支持。代码改变着世界,我们创造代码,也是在创造世界,让我们在代码的世界里风雨同舟,一往无前!