字典树
又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。
典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),
所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符
串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,
查询效率比哈希树高(摘自百度百科)
好了现在描述我现在要解决的问题:
在一个词库里 我要找出以已知串A为前缀的所有单词.
算法一:
我最先想到的算法是这个样子的:
对词库进行遍历,找出前缀相同的串记录下来.
我对上面算法的评价是: 简单粗暴,容易实现,算法复杂度 o(n) 貌似在接受范围
但是如果单词量巨大,1亿个,不够多?100亿,首先存储就是个问题,进行一个简单的
计算,假设平均每个串只有6个字母 可表示的串个数有大约140亿个(认为区分大小写)
(10^11 * 6 * 8)/(8*1024*1024*1024) = 558GB;
所以这个简单粗暴算法的时空复杂度是线性上升的, 故而需要优化之.
下面我给出
算法二:应用字典树进行实现
思想:
利用树的原理如下图所示
这样的时间复杂度可以降到常数级别,搜索变得十分容易,公共前缀的引入,空间复杂度也
随之减小.
废话不多说 下面给出原代码,可能实现起来看着比较复杂,原因是我实现了一些不必要的链表
操作
lib.cpp
#include "lib.h"
#include <iostream>
#include <vector>
#include <cstring>
#define IsLetter(Letter) ((Letter>=65 && Letter <=90) || (Letter>=97 && Letter<=122))
using namespace std;
// 将不使用模板 因而有一定局限性
// 使用单链表作为基本的存储结构,构造效率比较低下
// 当且仅当链表中节点数据唯一
// 定义链表节点
struct LinkList_Node{
char NodeData;
int BIA;
LinkList_Node *pNext;
LinkList_Node *pDown;
};
class TrieTree{
public:
TrieTree(); // 构造函数
~TrieTree();
vector<char*> WordVector;
void TrieTree_Insert(char*,int); // 给字典树插入值
int TrieTree_Find(char*,int,LinkList_Node**); // 指示找到或没有找到
void TrieTree_CompleteWord(char*,int); // 补全单词
void Vector_Clear();
private:
// 关于链表的操作
LinkList_Node* LinkList_InIt(); // 初始化函数
bool LinkList_Insert(LinkList_Node*,char*,int); // 插入函数 在pHead之后插入
LinkList_Node* LinkList_Find(LinkList_Node*,char,int*); // 在链表中寻找变量的索引 从1开始计数 //
LinkList_Node* LinkList_Find(LinkList_Node*,int);
bool LinkList_Clear(LinkList_Node *); // 释放务必调用头指针
bool LinkList_Del(LinkList_Node*,char); // 通过数据内容删除
LinkList_Node* LinkList_Push_Back(LinkList_Node*,char); // 在链表末尾加入值 返回指向其的指针
// 关于字典树的操作
LinkList_Node* PTrieTreeHead; // 树的根节点
bool TrieTree_Clear(); // 释放整棵树
void TrieTree_Clear(LinkList_Node*); // 递归的释放整棵树
void TrieTree_CompleteWord(char*,int,LinkList_Node*);
};
void TrieTree :: Vector_Clear(){
int len = WordVector.size();
int i;
for(i=0;i<len;i++)
delete[] WordVector[i];
WordVector.clear();
}
// 初始化一个头指针 指向头节点
LinkList_Node* TrieTree :: LinkList_InIt(){
LinkList_Node *pHead = new LinkList_Node;
pHead->pNext = NULL;
pHead->pDown = NULL;
return pHead;
}
bool TrieTree :: LinkList_Insert(LinkList_Node *pn,char *Ch_Array,int len){
if(pn == NULL) return false;
LinkList_Node *pt = pn,*pv;
pv = pn->pNext;
try{
for(int i=0;i<len;i++){
pt->pNext = new LinkList_Node;
pt->pNext->pNext = NULL;
pt->pNext->NodeData = Ch_Array[i];
pt = pt->pNext;
}
pt->pNext = pv;
}
catch(char *str){
return false;
}
return true;
}
// 可以选择调用或者不调用index
// index 是相对位置 我们将会检索pHead指针的数据域
LinkList_Node* TrieTree::LinkList_Find(LinkList_Node *pHead,char ch,int *index=NULL){
LinkList_Node *pt = pHead;
int Count = 0;
while(pt){
if(pt->NodeData == ch) {
if(index != NULL) *index = Count;
return pt;
}
Count++;
pt = pt->pNext;
}
if(index != NULL) *index = -1;
return NULL;
}
// 注意 index是相对位置 相对于 pn
LinkList_Node* TrieTree :: LinkList_Find(LinkList_Node *pn,int index){
LinkList_Node *pt = pn;
int Count = 0;
while(pt != NULL && Count <=index){
if(Count == index) return pt;
pt = pt->pNext;
Count++;
}
return NULL;
}
bool TrieTree :: LinkList_Clear(LinkList_Node *pHead){
LinkList_Node *pt = pHead,*pn;
try{
while(pt != NULL){
pn = pt->pNext;
delete pt;
pt = pn;
}
}
catch(char *str){
return false;
}
return true;
}
bool TrieTree :: LinkList_Del(LinkList_Node *pHead,char ch){
if(pHead == NULL) return false;
LinkList_Node *pt = pHead->pNext,*pu = pHead;
while(pt != NULL){
if(pt->NodeData == ch){
pu->pNext = pt->pNext;
delete pt;
return true;
}
pu = pt;
pt = pt->pNext;
}
return false;
}
// 清理这棵树 采用中序遍历的方法
bool TrieTree :: TrieTree_Clear(){
if(PTrieTreeHead == NULL) return false;
TrieTree_Clear(PTrieTreeHead);
return true;
}
void TrieTree :: TrieTree_Clear(LinkList_Node *pt){
if(pt->pDown != NULL) TrieTree_Clear(pt->pDown);
if(pt->pNext != NULL) TrieTree_Clear(pt->pNext);
delete pt;
}
// 初始化一棵字典树
TrieTree :: TrieTree(){
// 初始化根节点
// 指向第一个链表的头节点
PTrieTreeHead = LinkList_InIt();
}
TrieTree :: ~TrieTree(){
TrieTree_Clear();
Vector_Clear();
}
LinkList_Node * TrieTree :: LinkList_Push_Back(LinkList_Node* pt, char Ch){
LinkList_Node *pn;
while(pt){
pn = pt;
pt = pt->pNext;
}
pt = pn->pNext = new LinkList_Node;
pt->pDown = NULL;
pt->pNext = NULL;
pt->NodeData = Ch;
pt->BIA = 0;
return pt;
}
void TrieTree :: TrieTree_Insert(char *Ch,int len){
int i;
LinkList_Node* pt = PTrieTreeHead,*pf;
for(i=0;i<len-1;i++){
// 如果没有找到
pf = LinkList_Find(pt,Ch[i]);
if(pf == NULL){
pt = LinkList_Push_Back(pt,Ch[i])->pDown = LinkList_InIt();
pt->pDown = NULL;
pt->pNext = NULL;
pt->BIA = 0;
}
else{
pt = pf->pDown;
}
}
// 将最后一个单独处理
pf = LinkList_Find(pt,Ch[i]);
if(pf == NULL){
pf = LinkList_Push_Back(pt,Ch[i]);
// 务必new一个新链表 后续插入依赖其实现 原则是 要访问到的pDown 不为空
// 认为每一个元素都有后继 很好的处理了被查询字母长度大于树深度的情况
pf->pDown = LinkList_InIt();
pf->pNext = NULL;
pf->BIA = 0;
}
pf->BIA++;
}
int TrieTree :: TrieTree_Find(char *Word,int len,LinkList_Node** Pn = NULL){
// 根节点
int i;
LinkList_Node* pt = PTrieTreeHead;
for(i=0;i<len-1;i++){
pt = LinkList_Find(pt,Word[i]);
if(pt == NULL) {if(Pn != NULL) *Pn = pt; return 0;}
else {
pt = pt->pDown;
}
}
// 将最后一个单独处理 避免重新查找的情况
pt = LinkList_Find(pt,Word[i]);
if(Pn != NULL) *Pn = pt;
// 不存在有两种情况 1 这点不存在 2 这点是其他点的子集
if(pt == NULL || pt->BIA == 0) return 0;
return pt->BIA;
}
void TrieTree:: TrieTree_CompleteWord(char* Word,int len){
LinkList_Node *pt = PTrieTreeHead,*Pn;
int i;
// 这是一个 new运算符 注意del的使用
char *CompleteWord = new char[100];
TrieTree_Find(Word,len,&Pn);
if(Pn == NULL) return;
for(i=0;i<len;i++)
CompleteWord[i] = Word[i];
//如果此点有词 加入队列
char* AddVector = new char[len+1];
if(Pn->BIA >0){
for(i=0;i<len;i++)
AddVector[i] = CompleteWord[i];
AddVector[i] = '\0';
WordVector.push_back(AddVector);
}
TrieTree_CompleteWord(CompleteWord,i,Pn->pDown);
delete []CompleteWord;
}
void TrieTree :: TrieTree_CompleteWord(char* CompleteWord,int len,LinkList_Node*pt){
int i;
int AddNum = 0;
if(IsLetter(pt->NodeData) ){
CompleteWord[len] = pt->NodeData;
/*
只为报复社会
其实可以等价于
AddNum++; // 先自增表示此行添加单词
len += AddNum; // 表示已添加到补全单词数组中
*/
len += ++AddNum;
char* AddVector = new char[len+1];
if( pt->BIA !=0){
for(i=0;i<len;i++)
AddVector[i] = CompleteWord[i];
AddVector[i] = '\0';
WordVector.push_back(AddVector);
}
}
if(pt->pDown != NULL)
TrieTree_CompleteWord(CompleteWord,len,pt->pDown);
if(pt->pNext != NULL){
TrieTree_CompleteWord(CompleteWord,len-AddNum,pt->pNext);
}
}
/*书写入口函数*/
TrieTree t;
int Iterator = 0;
void InsertStr(char* Str,int len){
t.TrieTree_Insert(Str,len);
}
void CompleteWord(char* Str,int len){
t.Vector_Clear();
t.TrieTree_CompleteWord(Str,len);
}
char *ReturnStr(){
t.WordVector;
if(Iterator < t.WordVector.size())
return t.WordVector[Iterator++];
else
return NULL;
}
lib.h
#ifndef LIB_H
#define LIB_H
extern "C" char __declspec(dllexport) *ReturnStr();
extern "C" void __declspec(dllexport) InsertStr(char*,int);
extern "C" void __declspec(dllexport) CompleteWord(char*,int);
#endif
dlltest.cpp
#include <stdio.h>
#include <windows.h>
#include <iostream>
#include <time.h>
char Word[26] = {'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'};
typedef char*(*lpReturnStr)(); //定义函数指针类型
typedef void(*lpInsertStr)(char*,int);
typedef void(*lpCompleteWord)(char*,int);
void test(lpInsertStr InsertStr,lpCompleteWord CompleteWord,lpReturnStr ReturnStr){
int a,b,c,d,e,f;
char cmd[10];
for(a=0;a<26;a++)
for(b=0;b<26;b++)
for(c=0;c<26;c++)
for(d=0;d<26;d++)
{
char array[6] ={Word[a],Word[b],Word[c],Word[d],'\0'};
InsertStr(array,4);
}
printf("创建完毕");
scanf("%s",cmd);
int m = clock();
CompleteWord(cmd,2);
int n = clock();
char* pt;
pt = ReturnStr();
while(pt != NULL){
printf("%s %d",pt,n-m);
pt = ReturnStr();
}
}
int main(int argc, char *argv[])
{
HINSTANCE hDll; //DLL句柄
lpReturnStr ReturnStr; //函数指针
lpInsertStr InsertStr;
lpCompleteWord CompleteWord;
hDll = LoadLibrary("E:\\MyDll\\dllTest\\Debug\\dllTest.dll");
if (hDll != NULL)
{
ReturnStr = (lpReturnStr)GetProcAddress(hDll, "ReturnStr");
InsertStr = (lpInsertStr)GetProcAddress(hDll,"InsertStr");
CompleteWord = (lpCompleteWord)GetProcAddress(hDll,"CompleteWord");
test(InsertStr,CompleteWord,ReturnStr);
FreeLibrary(hDll);
}
return 0;
}
实验得知查询可以在常数时间完成
版权声明:本文为rongdongzhu1原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。