1. 前文
本文和大家一起聊聊字典树,从字典二字可知,于功能而言,字典树是类似于英汉字典
的一棵信息树。字典树有 2
大特点:
- 有容乃大。能存储大量的数据信息。
- 提供有基于关键字的查询、检索机制。
常用字典树
存储字符串(单词)信息,使用字典树
能方便实现字符串的存储、查询、统计、排序……一系列操作。
2. 字典树特点
字典树
是树结构的典型应用,如下图所示,为一棵字典树。字典树
的叶节点起标志性作用,标记字符串的结束,类似于C++
字符串的结束符号\0
。
通过对结构的观察,可以大致了解字典树的几个特点:
- 字典树不是
二叉树
,字典树的每一个节点可以有多个子节点,除根节点外每个节点存储一个字符
信息(常用于字符信息存储,但不仅限于字符信息)。 - 顺着根节点向子节点连接,可以找到一个
字符串
信息。正因为这个特性,字典树
如其名,可存储大量的单词。如从根节点开始,找到它的第一个子节点a
,然后找到a
的子节点c
,再顺着c
找到e
。这样就能得到子符串ace
。 - 具有
公共前缀
的字符串不需要重复存储,公用共同的祖先,如ace
和act
的公共祖先节点是ac
。这也是字典树的一大特点,相比较其它的存储方案,具有高度的空间利用率。
3. 字典树的物理存储
字典树常用的API
有插入
和查询
,其它可根据应用场景的需要进行扩展。
字典树的物理实现可以使用矩阵
和链式
存储 2
种方案,本文分别探讨这 2
种方案的实现过程。
3.1 链式存储
为了简化操作,实现过程中使用了STL
的vector
容器存储任一节点的子节点,当然,也可以使用带有头节点的链表。
3.1.1 结构类型
节点类型: 存储字典树的节点信息。
字典类: 维护字典的常用API
,此处先提供基本的API
,后面根据需要再扩展。
3.1.2 常规 API
3.1.2.1 insert
函数
功能描述: 提供添加字符串(单词)的功能,是构建字典树的第一重要环节。
**实现流程:**现以添加abc
字符串为例,讲解添加函数的实现过程。
- 首先构建根节点。根节点不需要存储具体的数据信息。
- 分解字符串
abc
,先读入字符a
,以根节点为当前节点,查询当前节点是否存在值为a
的子节点。
- 因字典树刚创建,此子节点不存在,则为节结点新建值为
a
的子节点,并且当前节点指针
指向新建的节点。
-
继续从字符串中分割出字符
b
,检查当前节点是否存在值为b
的子节点。没有则为当前节点创建值为b
的新节点,重设当前节点的指针为新建节点。
-
同理,分割出
c
,因当前节点不存在值为c
的子节点,为当前节点构建新节点。且重设当前节点的指针。
- 当字符串分割完毕后,最后添加值为
#
的叶节点作为结束标志符。
- 在现有字典树上继续添加
abd
字符串(单词)时,当前节点需要重置为根结点。分割abd
字符串时,因为值为a
和b
的节点已经存在,则仅让当前节点向下滑动。
- 分割出
d
字符,因当前节点不存在值为d
的子节点,新建值为d
的子节点。字符串分割完毕,为d
节点添加结束子节点。如下图所示。
编码实现:
实现辅助函数findChild
:
实现insert
函数:
测试插入函数:
输出结果:
3.1.2.2 search
函数
功能描述: 查询给定的字符串(单词)是否存在于字典树中。
实现流程: 查询和插入流程相似。如果检查到存在与分割出来的字符值相等的子节点便继续向下查询,否则,认为查询失败。
不要求一定的是完整字符串(单词),仅是前缀也可以。
如下图所示,虽然字典树中不存在ab
字符串(单词),因为存在ab
前缀,也认为是存在的。
编码实现:
测试查询:
输出结果:
3.1.2.3 getAllWords
函数
功能描述: 返回字典树上的所有字符串(单词)。
实现流程: 对于整棵树的搜索常用的方案有深度
和广度
搜索。针对此需求使用深度
搜索便能查询出树上的所有单词。
编码实现:
实现辅助函数 showAllWords
:显示字典树上的所有单词。
实现getAllWords
函数:
测试代码:
输出结果:
3.2 矩阵存储
使用矩阵
存储树节点之间的关系也是一种常见方案。
基本存储思想:
-
对每一个节点进行编号。
-
以父节点的编号为矩阵的行号,子节点的编号为列号,行与列对应的单元格中存储节点的关系描述(或值、权重)。
在存储字典树信息时,对上述的存储方案可以稍加改进一下。
-
如先确定根节点的编号为
0
,子节点a
在矩阵中的列号由其对应的ASCII
码决定,当然,会对其范围缩小。对应单元格中存储字符出现的顺序编号,并以此编号为此结点的唯一标志符号(类似于主键)。
这个编号与字符添加顺序有关,与字符本身无关。如下图所示:
- 字符
b
是字符a
的子节点。添加过程如下图所示。
- 继续完成其它节点关系的存储。
上述存储的优点:
- 可以把矩阵的列数限制在
26
之内。 - 查询任一节点的子节点时,可以根据字符本身所携带信息找到其存储位置。如果
b
节点下还有字符w
的子节点,便能轻易知道其存储位置是[2]['w'-'a']
并能获取w
节点的编号。
编码实现:
与前文的链式存储相毕较,仅是改变了存储方式,逻辑是没有发生任何变化。
输出结果:
使用数组存储的优点:
- 在查询所有单词时,会自动对其按字典进行排序。
- 实现起来较直观,易理解。
4. 字典树的应用
至此,想必对字典树有了较好的理解,下文再提供 2
个案例 ,深入体会字典树的神奇之处。
4.1 自动补全
所谓自动补全:指当用户输入单词前缀,则会显示所有与此前缀有关联的单词。此功能在关键字搜索应用中经常可以看到。
如果字典树中存在单词集["cat","caton","cater","this"]
,当用户输入cat
时,则自动显示所有以cat
为前缀的单词:caton
、cater
。
实现原理:
- 在字典树中查找前缀是否存在。
- 如果存在,以此前缀最后一个字符的节点为当前节点进行深度搜索。
编码实现:
在前文的矩阵实现方案中添加如下函数。
输出结果:
4.2 求 2 个字符串的最长公共前缀
所谓字符串的公共前缀指字符串前面相同的部分。如caton
和cater
的公共前缀是cat
。与自动补全功能是相逆的操作。
基本思想:
- 使用字典树存储所有字符串。
- 两个字符串的最长公共前缀的长度即他们所在的节点的公共祖先个数,于是,问题就转化为求公共祖先问题。
在树中求解节点的公共祖先问题可以有多种方案,本文侧重于字典树的理解,仅提供下面的穷举算法。其它方案可以自行查阅相关书籍。
测试:
输出结果:
5. 总结
本文介绍了字典树的逻辑结构,并且使用链式和矩阵2
种方案实现了字典的物理存储。并且通过 2
个具有代表性的案例让大家更深入理解字典树的实际应用。