您的位置:首页 > 理论基础 > 数据结构算法

结构的艺术:模糊查询

2016-05-25 10:41 302 查看
系统的学过编程的人应该都知道,有一门基础课:《数据结构与算法》,这门课很重要,但是许多人却不怎么重视,导致后来算法学习频频碰壁。我不会给大家系统的讲数据结构,但是我会给大家讲一些很有趣的结构,下来的学习还是得靠大家自己努力啦。

这次讲的是模糊查询,在模糊查询之前,我们先看一下如何精确查询。

比如我想要在一堆(n个)字符串里查找我的名字‘liuruiyang’,那么我需要和这堆(n个)字符串里的每个字符串作比较,而每次比较时间复杂度都是O(len),所以总的时间复杂度就是O(n*len);

既然精确查询如此,模糊查询也不会差太多,模糊查询,当你查询‘liu’时,会出现所有开头为‘liu’的字符串,既然这样,我依然要跟每个字符串作比较,总的时间复杂度依然是O(n*len);

我们知道,英文字母有26个,不考虑大小写和符号的话,我们使用26个字母就可以表示所有的单词,那么对于一个单词而言,这个单词由字母组成,那么每个单词就由两部分组成:

①:单词的长度;

②:每个位置的字母。

单词的长度很容易,一个计数器就可以搞定了,而每个位置的字母却比较麻烦,因为每个字母都要保存下来,这样会大大增加操作的复杂程度,其实我们可以注意到,一共有26个字母,并且这些字母是连续的,这样我们就可以创建一个长度为26的数组,用这个数组的下标可以和字母形成一一对应的关系,0~25分别对应a~z。



这张图里的树结构,每条边代表一个字母,而一个节点对应的就是该节点的路径,而我们将数组下标和字母组成了一一对应的关系,字母对应的下标是该字母的字符值减去字母a的字符值,例:t对应的下标是’t’-‘a’;

上面的图里,有一点还没有表示清楚,那就是如何判断一个单词是否存在,其实很简单,我们只需要增加一个标记位,对单词的存在与否进行标记就好了。那么来看结构体吧:

typedef struct TrieTree
{
int isStr;    //标记这个单词是否存在
struct TrieTree * next
;//N为26
}Trie;


举个例子吧,比如我已经在树中插入了”liuruiyang”,”liuyufu”,”lijing”这三条记录,由于字母表示边单词本身表示路径,那么可以肯定,这三条记录的公共边一定有”l”,”i”两个字母,既然我们知道它们的公共边,那么就可以轻松的得到它们的最近公共祖先,也就是”li”这个节点,注意:这个节点不一定是单词,它有可能不存在。

既然我们在树中得到了它们的最近公共祖先,那么它们就都可以通过这个公共祖先的子树进行遍历得到。反过来说,开头为”li”的单词,一定是”li”节点的子孙,它们都可以通过遍历”li”的子孙得到。这样,我们查找开头为”li”的节点,就不需要遍历整个树了,只需要遍历”li”节点的子树就好了。

这样,模糊查询就分析完了。下面开始编码实现吧。

当然,我们只有模糊查询算法是不行的,至少,你要能给树里加入单词啊。

首先是增:我们只需要将字符串按字符读出来,每个字符都代表一个边,如果边不存在,则创建这个边,当字符串读完时,将最后的那个节点的标记置为1,表示这个字符串已经存储在树里。

思想很简单,下面看代码吧:

int Insert(char * str, Trie * root)
{
int len = strlen(str);
Trie * temp = root;
for (int i = 0; i < len; i++)
{
int index = str[i]-'a';
if (temp->next[index] == NULL)
{
temp->next[index] = (Trie *)malloc(sizeof(Trie));
temp = temp->next[index];
temp->isStr = 0;
for (int i = 0; i < N; i++)
{
temp->next[i] = NULL;
}
}
else
{
temp = temp->next[index];
}
}
if (temp->isStr == 1)
return 0;
temp->isStr = 1;
return 1;
}


函数返回0时说明这个单词已经在树中,不需要添加,返回1时说明添加成功。

下来是删除:

当然,删除有两种删法:

①:找到字符串对应的节点,直接将该节点对应的标记值改为0,表示该字符串不在树中。

②:找到字符串对应的节点,检查该节点是否有子孙节点,如果有则将该节点对应的标记值改为0,如果没有,就得删除该节点,并且如果该节点的祖先节点对其余节点不产生影响的话,要递归删除该节点的祖先节点。

两种方法各有利弊,第一种的好处就是删除时速度快,并且下次增加时也会更方便,坏处就是大量删除后会产生大量冗余节点,冗余节点会使查询时的效率变低,第二种方法的好处与坏处正好和第一种相反。

当然,字符串特别多时,使用第一种方法删除的话产生的冗余节点与原有的节点相比不值一提,(其实我是为了图方便),所以简单的使用了第一种方法。

int Delete(char * str, Trie * root)
{
int len = strlen(str);
Trie * temp = root;
for (int i = 0; i < len; i++)
{
int index = str[i]-'a';
if (temp->next[index])
temp = temp->next[index];
else
return 0;
}
if (temp->isStr == 0)   return 0;
temp->isStr = 0;
return 1;
}


返回1表示成功删除,返回0表示不存在这个字符串。

下面就是模糊查询了:

按照先前说的思想,模糊查询实际上是首先精确查询目标字符串,得到该节点后,只需要递归遍历它的子树就好了。

int Fuzzys(char * str, Trie * root)
{
int len = strlen(str);
Trie * temp = root;
for (int i = 0; i < len; i++)
{
int index = str[i]-'a';
if (temp->next[index])
temp = temp->next[index];
else
return 0;
}
Trie * newr = temp;
int count = 0;
Visit(str,newr,N,count);
return 0;
}

int Visit(char * str, Trie * root, int n, int count)
{
if (root->isStr == 1)
{
printf("%s",str);
for (int i = 0; i < count; i++)
printf("%c",a[i]+'a');
printf("\n");
}
for (int i = 0; i < n; i++)
{
if (root->next[i] != NULL)
{
a[count] = i;
Visit(str,root->next[i],n,count+1);
}
}
return 0;
}


上面的代码由两个函数组成,Fuzzys()函数就是模糊查询的主函数,通过这个函数找到目标节点,然后Fuzzys()函数里面调用了Visit()函数,用来访问该节点的子孙。在整个过程中使用了一个全局不定长数组记录了节点的路径,输出时使用这个数组里的值进行输出。

我没有上精确查询的代码,其实认真看的同学应该已经发现了,精确查询的代码跟模糊查询应该差不太多。的确,基本没什么差别,只需要在Fuzzys()函数上稍微改动几行就行了。怎么改大家下去改吧,很简单,我就不多说了。

数据结构与算法本来就是一家,是密不可分的。许多算法之所以快,是因为它们使用了合理的数据结构。

我是算法吹,以后会给大家带来更多精彩的算法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息