算法总结第4弹,今天来总结下字典树(Trie树),Trie树算是我学的第一个高级数据结构了吧,还是比较简单的。
Trie树
Trie树,又称字典树,是字典的一种存储方式,字典中的每一个单词在Trie树种体现为从根节点出发的路径,路径中每条边代表一个字母,将边连接起来便形成了对应的单词,如图,就是一颗Trie树,其中存储了ab,ac,bc,c,cd五个单词(其中加粗节点表示单词结尾节点)。
一:Trie树的基本概念
Trie树是由链接的节点所组成的数据结构,这些链接可能为空,也可能指向其他节点,每个节点只有一个指向它的节点,称之为父节点,(只有根节点例外),每个节点都有X条链接,其中X代表字符集的大小。每一个链接代表一个字符,指向下一个节点。对于每一个节点,从根节点到该节点的路径中包含的链接串就是该节点所表示的字符串,也就是字典树中的一个单词。在一般的应用中,我们常常在Trie树的节点中保存一个bool值flag表示该节点是否是一个单词的结尾。也就是说,我们从根节点按照对应的链接走到一个节点,发现该节点的flag值为真,那么表示当前的字符串就是字典树中保存的单词。(如下图,加粗节点的flag就为1,表示该节点是一个单词的结尾)因为从根节点到一个节点的路径是唯一的,所以Trie树种每一个节点唯一地表示一个字符串(根节点表示空串)。下面是Trie树节点的代码实现:
const int maxn=100010;//节点总数的最大值 const int knum=26;//字符集的大小 struct Trie { struct Node { int next[knum];//next[i]表示该节点通过第i条边连接的节点编号 int flag;//标记 void INIT()//初始化节点 { memset(next,0,sizeof(next)); flag=0; } }node[maxn]; int tot;//当前Trie树中节点的数量 }T; void TRIE_INIT(Trie &T)//初始化Trie树和根节点 { T.tot=0;//将节点总数清0,0号点为根节点 T.node[0].INIT();//初始化根节点 }Trie树的节点数量和Trie树所存单词的总长度有关,设Trie树中所存单词总长度为n,则其空间复杂度为O(n*m),其中m为每一个节点所占空间,m的值取决于字符集knum的大小和标记的数量,对于不同的问题有不同的空间复杂度。
二:Trie树的基本操作
Trie树有两个基本操作,插入操作和查询操作,其中插入操作一般用来更新Trie树,
向Trie树中插入一个新的单词,查询操作一般是查询某个特定单词是否在Trie树中存在。
1. 插入操作
现在来介绍Trie树的插入操作,首先举个例子。假设插入字符串abc,Trie树现在是一颗空树,只有根节点存在,从根节点出发,依次按字符更新Trie树,第一个字符为a,因为目前根节点next数组均为空,不存在表示a的边,所以我们要新建一个节点,使得根节点通过a边连接到新建节点,同时我们进入新节点,判断下一个字符边是否存在,如果存在就直接运动到下一个节点,否则继续新建一个节点,直到到达最后一个字符c。插入字符串abc的过程如下图所示。
插入完字符串abc后,为了表示这个字符串为Trie树中的一个单词,我们还需要将插入操作中最后一个节点的flag设为1,表示它是单词abc的结尾。
现在如果要继续插入单词,如abd,那么同样要从根节点开始插入,由于根节点已经存在a边,所以不需要新建节点,直接移动到a节点即可,同理移动到ab节点,这时插入字符d,由于ab节点没有d边,所以要新建节点。最后还要标记新建立的节点为单词的结尾。插入abd的过程如下图所示。
插入单词的代码实现如下。
//向Trie树中插入单词str void INSERT(Trie &T,char *str) { int p=0;//p表示当前节点的标号,0表示根节点的标号 int len=strlen(str); //len表示插入单词的长度 for(int i=0;i<len;i++) { int index=str[i]-'a';//index是下一个字符的编号,这里默认第一个字符为a if(T.node[p].next[index]==0) { T.node[++T.tot].INIT();//新建一个节点 T.node[p].next[index]=T.tot;//将当前节点的index边连向新建立节点 } p=T.node[p].next[index];//进入下一个节点 } T.node[p].flag=1;//将该节点标记为单词的结尾 }
插入一个单词的时间复杂度为O(n),其中n为插入单词的长度。
插入多个单词和插入一个单词的思路是一致的,事实上我们可以将第一次插入看成是在一颗包含空串的Trie树上插入一个单词,所以插入多个单词就是多次在当前Trie树的基础上调用insert函数。
2. 查询操作
查询操作和插入类似,也是从根节点开始,不过查找不能新建节点,如果查找的过程中遇到空边表示查找的单词不在Trie树中,另外,如果查找完成,没有遇到空边,也不能说明查找单词一定存在,这时还要判断最后一个节点的flag标记是否为1,如果不为1,则依然表示查找的单词不在Trie树中(因为它是Trie树中一个单词的前缀),否则表示单词存在。
//在Trie树中查找单词str,若存在返回true,否则返回false bool SEARCH(Trie &T,char *str) { int p=0;//p表示当前节点的标号,0表示根节点的标号 int len=strlen(str);//len表示插入单词的长度 for(int i=0;i<len;i++) { int index=str[i]-'a';//index是下一个字符的编号,这里默认第一个字符为a if(T.node[p].next[index]==0) return false; p=T.node[p].next[index]; } if(T.node[p].flag) return true; return false; }
同插入操作,查询一个单词的时间复杂度为O(n),其中n为查询单词的长度。
以上就是Trie树的基本操作,在实际应用中,常常会根据题目需要在节点中维护多个信息,下面举几个例子来说明Trie树的应用。