问题描述:在一组字符串中,找到所有具有某个字符串前缀字符串,比如application、apple、eyes、cats等。如果要匹配的字符串是app,则符合匹配条件的有application、apple。
思路:首先采用快排将所有字符串进行字典序排序,这样具有同种前缀的所有字符串都会排在一块,如果给定一个要匹配的前缀字符串,我们只要找到具有这一字符串前缀的首个字符串下标和末个字符串下标即可,两个下标之间所有的字符串都会满足匹配要求。
下面我们的问题是如何找到首个下标和末个下标。字符串已经字典序排好序,我们只要在排好序中二分查找“app”这个前缀字符串,当然不一定存在“app”字符串,但是会找到首个满足“app”前缀匹配的字符串,查找的条件是:
1.当前比较字符串大于或等于匹配字符串“app”;
2.当前字符串满足前缀匹配;
3.前一个字符串小于匹配字符串“app”。
这样就能保证当前比较的字符串是满足匹配的首个字符串,但是也要处理特殊情况,比如,如果当前比较字符串下标为0,就不能取前一个字符串,或下标为0时还是小于匹配字符串。
同样,查找末个满足匹配字符串的下标时,只要在首个下标之后找即可,而且后面的字符串肯定大于匹配字符串“app”,同样用二分法查找,查找的条件是:
1.只判断字符串是否满足前缀匹配;
2.如果当前查找字符串能够满足前缀匹配,且后一个字符串不满足前缀匹配,那么当前字符串下标即为末个下标。
二分法代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void swap(char **str1,char **str2){
char *temp = *str1;
*str1 = *str2;
*str2 = temp;
}
//字典排序
//这里实现一个可以按照按照前len个字符比较字符串
//当len=-1时,比较全部字符,len>0比较前len个字符
//str1>str2 返回1;str1 < str2 返回-1; str1 == str2 返回 0
int strcomp(char* str1, char*str2,int len)
{
while(*str1 && *str2 && *str1==*str2 && len != 0)
{
str1++;
str2++;
len--;
}
return *str1-*str2;
}
//快速排序
//先将无序的字符串数组按照字典序排序
int partition(char** set, int start_index, int end_index){
if(set == NULL)
return -1;
char* temp = set[end_index];
int low = start_index;
int high = start_index+1;
for(;high<end_index;high++){
if(strcomp(set[high], temp,-1) < 0){
low++;
swap(&set[low],&set[high]);
}
}
low++;
swap(&set[low], &set[high]);
return low;
}
void quick_sort(char** set, int start_index, int end_index){
if(start_index>= end_index)
return;
int mid = partition(set, start_index, end_index);
quick_sort(set, start_index, mid-1);
quick_sort(set, mid+1, end_index);
}
//二分法找出符合前缀匹配的首个字符串索引
//比较方式:全字符比较
//如果当前中间字符串小于要匹配字符串,继续在中间字符串后面查找
//如果当前中间字符串大于或等于匹配字符串,此时在判断中间字符串前一个字符串是否小于匹配字符串,如果小于,则当前字符串就是匹配成功的首个字符串
//不小于,继续在中间字符串前面查找
int first_binary_search(char** set, int low, int high, char *search_str)
{
while(low <= high)
{
int middle = (low + high)/2;
if(strcomp(set[middle], search_str,-1) < 0)
{
low = middle + 1;
}
else if(strcomp(set[middle],search_str,-1) >= 0)
{
if(strcomp(set[middle-1], search_str,-1) < 0)
return middle;
else
high = middle - 1;
}
}
//没找到
return -1;
}
//二分法找出符合前缀匹配的最后一个字符串索引
//比较方式:前n个字符比较,即判断查找字符串是否具有前缀匹配
//如果当前中间字符串等于要匹配的字符串,此时在判断中间字符串后一个字符串是否大于匹配字符串,如果大于,则当前字符串就是匹配成功的最后一个字符串
//不大于,继续在中间字符串后面查找
//如果当前中间字符串大于匹配字符串,继续在中间字符串前面查找
int end_binary_search(char** set, int low,int high, char *search_str)
{
int cmp_len = strlen(search_str)-1;
while(low <= high)
{
int middle = (low + high)/2;
if(strcomp(set[middle], search_str,cmp_len) ==0 )
{
if(strcomp(set[middle+1], search_str,cmp_len)> 0)
return middle;
else
low = middle + 1;
}
else if(strcomp(set[middle],search_str,cmp_len) > 0)
{
high = middle - 1;
}
}
//没找到
return -1;
}
int main(){
char *set[] = {"application","apple","apply","eyes","attation"};
//先将字符串数组字典排序
quick_sort(set,0,4);
for(int i = 0;i<5;i++)
{
printf("%s ", set[i]);
}
printf("\n");
char pre[] = "ab";
//计算得到符合前缀匹配的首个字符串下标和最后一个字符串下标
int first_index = first_binary_search(set,0,4,pre);
int end_index = end_binary_search(set,first_index,4,pre);
if(first_index == -1 || end_index == -1)
printf("未找到匹配的字符串");
else
{
for(int i = first_index;i<= end_index;i++)
printf("%s ", set[i]);
}
}
当然,随着字符串数量的增多(1000000个),和动态的插入和删除,上述排序查找的方法就有些效率问题了,这时我们可以采用Trie树组织存储所有的字符串,在网上查了一下资料,研究了一下Trie树,觉得Tire树对字符串匹配会有更好的性能,能比较好的支持插入删除,且查找和匹配的性能也很好。
1.Trie树
Trie树,又称单词查找树、字典树,是一种树形结构,是一种哈希树的变种,是一种用于快速检索的多叉树结构。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
Trie树也有它的缺点,Trie树的内存消耗非常大.当然,或许用左儿子右兄弟的方法建树的话,可能会好点。
2.三个基本属性
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
3.说明
- 和二叉查找树不同,在trie树中,每个结点上并非存储一个元素。
- trie树把要查找的关键词看作一个字符序列。并根据构成关键词字符的先后顺序构造用于检索的树结构。
- 在trie树上进行检索类似于查阅英语词典。
- 一棵m度的trie树或者为空,或者由m棵m度的trie树构成。
4.插入过程
对于一个单词,从根开始,沿着单词的各个字母所对应的树中的节点分支向下走,直到单词遍历完,将最后的节点标记为红色,表示该单词已插入trie树。
5.查找过程
- 从根结点开始一次搜索;
- 取得要查找关键词的第一个字母,并根据该字母选择对应的子树并转到该子树继续进行检索;
- 在相应的子树上,取得要查找关键词的第二个字母,并进一步选择对应的子树进行检索。
- 迭代过程……
- 在某个结点处,关键词的所有字母已被取出,则读取附在该结点上的信息,即完成查找。其他操作类似处理。
即从根开始按照单词的字母顺序向下遍历trie树,一旦发现某个节点标记不存在或者单词遍历完成而最后的节点未标记为红色,则表示该单词不存在,若最后的节点标记为红色,表示该单词存在。如下图中:trie树中存在的就是abc、d、da、dda四个单词。在实际的问题中可以将标记颜色的标志位改为数量count等其他符合题目要求的变量。
采用Trie树,实现上述的字符串前缀匹配算法
代码实现:
#include <iostream>
#include <string.h>
using namespace std;
static const int branchNum = 26; //声明常量
static const int Max_Word_Len = 40; //声明常量
static int i;
static int pos = 0;
char worddump[Max_Word_Len+1];
struct Trie_node
{
//记录此处是否构成一个串。
bool isStr;
//指向各个子树的指针,下标0-25代表26字符
Trie_node *next[branchNum];
Trie_node():isStr(false)
{
for(int i = 0; i<branchNum; i++)
next[i] =NULL;
}
};
class Trie
{
public:
Trie();
void insert(const char* word);
void search(const char* word);
int traverse(Trie_node *result,int i);
void deleteTrie(Trie_node *root);
private:
Trie_node* root;
};
Trie::Trie()
{
root = new Trie_node();
}
void Trie::insert(const char *word)
{
Trie_node *location = root;
while(*word)
{
if(location->next[*word-'a'] == NULL)
{
Trie_node *tmp = new Trie_node();
location->next[*word-'a'] = tmp;
}
location = location->next[*word-'a'];
word++;
}
//到达尾部,即为一个字符串
location->isStr = true;
}
int Trie::traverse(Trie_node *result,int char_i)
{
if (result == NULL)
return 0;
if (result->isStr)
{
worddump[pos]='a'+char_i;
worddump[pos+1]='\0';
printf("%s\n", worddump);
}
else if(char_i>=0)
{
worddump[pos]='a'+char_i;
}
for (int i=0; i<branchNum; ++i)
{
pos++;
traverse(result->next[i],i);
pos--;
}
return 0;
}
void Trie::search(const char *word)
{
Trie_node *location = root;
const char *ptr = word;
while(*ptr && location)
{
location = location->next[*ptr-'a'];
ptr++;
}
if(location != NULL && !(*ptr))
{
ptr = word;
int pre_len = strlen(ptr);
while(*ptr)
worddump[pos++] = *ptr++;
pos--;
traverse(location,-1);
}
else
{
printf("no vaild word\n");
}
}
void Trie::deleteTrie(Trie_node *root)
{
for(i = 0; i < branchNum; i++)
{
if(root->next[i] != NULL)
{
deleteTrie(root->next[i]);
}
}
delete root;
}
int main()
{
Trie t;
char *set[] = {"application","apple","apply","eyes","attation"};
for(int i=0;i<5;i++)
t.insert(set[i]);
char pre[] = "app";
t.search(pre);
return 0;
}