1.引言
Trie树是一种快速查找树,关于Trie树的特性和优点,这里就不再罗嗦了,请参照这篇文章:Trie树实现词典查找算法。在此文中,Trie树的实现用的是二十六叉树,理解起来直观易懂,算法也简单明了,但是有一个致命的弱点,就是浪费空间。特别是在数据规模比较小的情况下,数据稀疏问题表现的特别严重。因此需要进行改进。二十六叉树浪费空间的原因是有好多被分配的空间上实际上没有数据,但是Trie树的节点结构是在创建一个节点时就默认分配了指向其二十六个孩子的指针,即便是该节点仅有一个孩子,它也会有这二十六个指针,因此会造成极大的空间浪费。
“任何森林都可以转换为二叉树”。将多叉树转成二叉树非常容易,简单说就是“左儿子,右兄弟”。就是将一个节点的第一个儿子放在左儿子的位置,下一个儿子,即左儿子的第一个兄弟,放在左儿子的右儿子位置上,再下一个兄弟接着放在右儿子的右儿子的位置。看下面的图,就更清楚了。左边是多叉树,右边是转换后的二叉树。
下面将Trie树的算法改造为用二叉树来实现,同样实现如下的功能:
随机产生10W个字符串,字符串的长度在10~30之间,以这10W个数为单词构建词典,词典用二叉树实现的Trie树进行存储。(1)实现词典的检索功能;(2)将这10W个词拼成一个大的字符串A,实现一个多模匹配算法,寻找词典中的词在字符串A中出现了多少次(很明显,最少为一次)。
2.时间复杂度分析
下面的算法的运行结果如下,从中可以看出,在长字符串中搜索字符串,即多模式匹配的时间耗费很大,时间约为35分钟,其时间复杂度为mn(m为长字符串的长度,n为单词的最大长度),还需要进一步改进。
(1)建立Trie树用时(纳秒):441412
(2)查找字符串用时(纳秒):4
(3)在长字符串中搜索短字符串用时(纳秒):2090213036
3.源代码
好了,下面上算法:
#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <sstream>
#include <sys/time.h>
using namespace std;
//---------------Trie树的定义及实现部分----------------
//节点的定义
class Node{
public:
char index; //节点所表示的字母
int flag; //节点是否有单词出现的标记
int count; //单词出现的次数
string word; //节点表示的字符串
Node* left; //左孩子
Node* right; //右孩子
public:
Node() : index('a'), word(""), flag(0), count(0){
left = NULL;
right = NULL;
}
Node(char c) : index(c), word(""), flag(0), count(0){
//memset(left, NULL, sizeof(Node*));
//memset(right, NULL, sizeof(Node*));
left = NULL;
right = NULL;
}
};
//Trie树的操作定义
class Trie{
private:
Node* pRoot;
private:
void print(Node* node); //按字典续打印整棵树
void destory(Node* node); //销毁Trie树
public:
Trie();
~Trie();
void insert(string str); //插入字符串
bool search(string, int &count);//搜索字符串
void search_2(string); //特殊的搜索字符串
void printAll(); //打印节点内容
};
Trie::Trie(){
pRoot = new Node();
}
Trie::~Trie(){
}
//插入新的字符串
void Trie::insert(string str){
//如果字母为'a',那么在左孩子中找,否则去左孩子的又孩子中找
Node* cur = pRoot; //当前节点
Node* pre = pRoot; //前驱节点
Node* pLev = pRoot;
pre = pRoot;
pLev = pRoot -> left;
for(int i = 0; i < str.size(); i++){
cur = pLev;
if(cur == NULL){
//若首节点不存在,则创建首节点
cur = new Node(str[i]);
pre -> left = cur;
pLev = cur -> left;
pre = cur;
}else if(cur != NULL && cur -> index > str[i]){
//若首节点存在,且首节点大于目标字符,重建首节点
Node *p = new Node(str[i]);
p -> right = cur;
pre -> left = p;
pLev = p -> left;
pre = p;
continue;
}else{
//首节点存在,且首节点小于等于目标字符
while(cur != NULL && cur -> index < str[i]){
pre = cur;
cur = cur -> right;
}
if(cur != NULL && cur -> index == str[i]){
//找到了目标节点
pLev = cur -> left;
pre = cur;
}else{
//创建目标节点
Node *p = new Node(str[i]);
p -> right = cur;
pre -> right = p;
pLev = p -> left;
pre = p;
}
}
}
if(pre -> flag == 1){
pre -> count++;
return;
}else{
pre -> flag = 1;
pre -> count++;
pre -> word = str;
}
}
//搜索字符串
bool Trie::search(string str, int &count){
Node *cur = pRoot, *pLev = pRoot -> left;
for(int i = 0; i < str.size(); i++){
cur = pLev;
if(cur != NULL){
while(cur != NULL && cur -> index < str[i]){
cur = cur -> right;
}
if(cur != NULL && cur -> index == str[i]){
pLev = cur -> left;
}else{
return false;
}
}else{
return false;
}
}
if(cur -> flag == 1){
count = cur -> count;
return true;
}else{
return false;
}
}
//打印节点内容
void Trie::print(Node *pRoot){
if(pRoot != NULL){
if(pRoot -> word != ""){
cout << pRoot -> count << "\t" << pRoot -> word << endl;
}
print(pRoot -> right);
print(pRoot -> left);
}else{
return;
}
}
//打印整棵树
void Trie::printAll(){
print(pRoot);
}
//特殊的搜索字符串(特殊用法)
void Trie::search_2(string str){
Node *cur = pRoot, *pLev = pRoot -> left;
for(int i = 0; i < str.size(); i++){
cur = pLev;
if(cur != NULL){
while(cur != NULL && cur -> index < str[i]){
cur = cur -> right;
}
if(cur != NULL && cur -> index == str[i]){
pLev = cur -> left;
if(cur -> flag == 1){
cur -> count++;
}
}else{
return;
}
}else{
return;
}
}
}
//----------------------测试部分----------------------------
//获取当前时间(纳秒)
long getCurrentTime(){
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec*1000*1000 + tv.tv_usec;
}
//主函数
int main(){
//1.创建对象,打开文件
Trie trie;
string str;
ifstream fin;
fin.open("dict.txt");
if(!fin){
cout << "文件打开失败!" << endl;
return -1;
}
//2.建立Trie树
long time_1 = getCurrentTime();
while(getline(fin, str, '\n')){
trie.insert(str);
}
long time_2 = getCurrentTime();
fin.close();
//3.验证Trie树的正确性
fin.open("dict.txt");
while(getline(fin, str, '\n')){
int count = -1;
bool isFind = trie.search(str, count);
if(!isFind){
cout << "未找到: " << str << endl;
}else{
//cout << "已找到:" << str << endl;
}
}
fin.close();
//4.普通查询
int count = -1;
str = "pfpqefqdbjbmywz";
long time_3 = getCurrentTime();
bool isFind = trie.search(str, count);
long time_4 = getCurrentTime();
if(isFind){
cout << "已找到: " << str << " 存在次数:" << count << endl;
}else{
cout << "未找到: " << str << endl;
}
//5.将短字符串组合为长字符串
string long_str;
ostringstream string_out;
fin.open("dict.txt");
while(getline(fin, str, '\n')){
string_out << str;
}
long_str = string_out.str();
//6.在长字符串中搜索字典中的字符串
long time_5 = getCurrentTime();
cout << long_str.size() << endl;
for(int i = 0; i < long_str.size()-30; i++){
string str_in = long_str.substr(i, i+30);
trie.search_2(str_in);
}
long time_6 = getCurrentTime();
//5.打印字典中的所有词以及出现次数
trie.printAll();
//8.打印各项任务时间
cout << "建立Trie树用时(纳秒):" << time_2 - time_1 << endl;
cout << "查找字符串用时(纳秒):" << time_4 - time_3 << endl;
cout << "在长字符串中搜索短字符串用时(纳秒):" << time_6 - time_5 << endl;
}