热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【算法】字典树

一、算法理解参考:字典树(前缀树)_越看越喜欢啊-CSDN博客_字典树字典树,叫前缀树可能更好理解。Trie又被称为前缀树、字典树,所以当然是一棵树。上面这棵Trie树包含的字符串

一、算法理解

参考:字典树(前缀树)_越看越喜欢啊-CSDN博客_字典树

字典树,叫前缀树可能更好理解。

image

Trie又被称为前缀树、字典树,所以当然是一棵树。

上面这棵Trie树包含的字符串集合是{in, inn, int, tea, ten, to}。每个节点的编号是我们为了描述方便加上去的。树中的每一条边上都标识有一个字符。这些字符可以是任意一个字符集中的字符。比如对于都是小写字母的字符串,字符集就是’a’-‘z’;对于都是数字的字符串,字符集就是’0’-‘9’;对于二进制字符串,字符集就是0和1。

比如上图中3号节点对应的路径0123上的字符串是inn,8号节点对应的路径0568上的字符串是ten。终结点与集合中的字符串是一一对应的。

【原理】:

下面我们来讲一下对于给定的字符串集合{W1, W2, W3, … WN},如何创建对应的Trie树。

其实上Trie树的创建是从只有根节点开始,通过依次将W1, W2, W3, … WN插入Trie中实现的。所以关键就是之前提到的Trie的插入操作

具体来说,Trie一般支持两个操作:



  1. Trie.insert(W):第一个操作是插入操作,就是将一个 字符串W 加入到集合中。

  2. Trie.search(S):第二个操作是查询操作,就是查询一个 字符串S 是不是在集合中。


1、插入操作

(1)假设我们要插入字符串”in”。

我们一开始位于根,也就是0号节点,我们用P=0表示。我们先看P是不是有一条标识着i的连向子节点的。没有这条边,于是我们就新建一个节点,也就是1号节点,然后把1号节点设置为P也就是0号节点的子节点,并且将边标识为i。最后我们移动到1号节点,也就是令P=1

image

这样我们就把”in”的i字符插入到Trie中了。

然后我们再插入字符n,也是先找P也就是1号节点有没有标记为n的边。还是没有,于是再新建一个节点2,设置为P也就是1号节点的子节点,并且把边标识为n。最后再移动到P=2。这样我们就把n也插入了。由于n是”in”的最后一个字符,所以我们还需要将P=2这个节点标记为终结点

image

(2)现在我们再插入字符串”inn”

过程也是一样的,从P=0开始找标识为i的边,这次找到1号节点。于是我们就不用创建新节点了,直接移动到1号节点,也就是令P=1。再插入字符n,也是有2号节点存在,所以移动到2号节点,P=2。最后再插入字符n这时P没有标识为n的边了,所以新建3号节点作为2号节点的子节点,边标识为n,同时将3号节点标记为终结点:

image

(3)将后面的字符串int tea ten to都插入之后,就得到了我们一开始给出的Trie:

image

【总结】:

插入操作伪码如下:

image


2、查询操作

下面我们再讲一下如何查询Trie树中是不是包含字符串S,也就是之前提到的查找操作。

查找其实比较简单。我们只要从根节点开始,沿着标识着S[1] -> S[2] -> S[3] … -> S[S.len]的边移动,如果最后成功到达一个终结点,就说明S在Trie树中;如果最后无路可走,或者到达一个不是终结点的节点,就说明S不在Trie树中。

image

如果是查找”te”,就会从0开始经过5最后到达6。但是6不是终结点,所以te没在Trie树中。如果查找的是”too”,就会从0开始经过5和9,然后发现之后无路可走:9号节点没有标记为o的边连出去。所以too也不在Trie中。

查找操作伪码:

image


3、Trie树的代码实现参考

数组方式实现

要写代码实现一个Trie首先就要确定如何存储一个Trie结构。这里用一个二维数组来存储:

int trie[MAX_NODE]/[CHARSET];

int k;



  • 其中MAX_NODE是trie中最大能存储的节点数目

  • CHARSET是字符集的大小

  • k是当前trie中包含有多少个节点。

  • Trie[i]/[j]的值是0表示trie树中i号节点,并有一条连出去的边,边上的字符是字符集中第j个字符(从0开始);

  • trie[i]/[j]的值是正整数x,表示trie树中i号节点,有一条连出去的边,边上的字符是字符集中第j个字符,并且这条边的终点是x号节点

PS:Tire树需要保存的值:

1)边的起节点

2)边的终节点

3)边的字符。

如上采用数组方式,非0的[i]/[j],其值表示边连接的下一个节点,i表示当前节点,j表示边的字符。

#include
#include
#include
using namespace std;
const int MAX_NODE = 1000000 + 10;
const int CHARSET = 26;
int trie[MAX_NODE][CHARSET] = {0};
int color[MAX_NODE] = {0}; //记录是不是树的叶子节点
int k = 1;
void insert(char *w){
int len = strlen(w);
int p = 0;
for(int i=0; i int c = w[i] - 'a';
if(!trie[p][c]){
trie[p][c] = k;
k++;
}
p = trie[p][c];
}
color[p] = 1;
}
int search(char *s){
int len = strlen(s);
int p = 0;
for(int i=0; i int c = s[i] - 'a';
if(!trie[p][c]) return 0;
p = trie[p][c];
}
return color[p] == 1;
}
int main(){
int t,q;
char s[20];
scanf("%d%d", &t,&q);
while(t--){
scanf("%s", s);
insert(s);
}
while(q--){
scanf("%s", s);
if(search(s)) printf("YES\n");
else printf("NO\n");
}
return 0;
}

二、适用场景

三、注意事项

【个人理解】



  1. 当然Java中也可以采用这种方式(个人理解)。节点通过Node { Map; xxxx}表示:key表示字符,value表示重点Node索引, xxx表示节点需要记录的其它属性(如:节点所在树深度)。如果所有节点需要遍历,可以考虑同时使用ArrayList记录节点全集。

  2. 如果需要计算叶子节点相关,则最好每个Node中保存一个Count,记录当前Node的边数。(参考单词压缩编码的例子)。Node节点,包含Count和Node[],[]索引表示边、值表示下个Node。


【字典树关键操作】

1)Insert(String),单词插入树

2)search(String),判断单词是否存在指定单词。(需到叶子节点)

3)searchWith(String),判断字典树中是否有单词是指定前缀


四、使用案例

1)单词压缩编码(leetCode)

给定义一组单词words,按照如下规则进行编码,编码后结果由:助记字符串 s 和 下标数组 indices 组成。输出最小 助记字符串 长度。

如:

1)单词组是words = ["time", "me", "bell"]

2)编码后结果为s = "time#bell#" 和 indices = [0, 2, 5]

如上:单词组words和编码结果的关系如下:

words[0] = "time" ,s 开始于 indices[0] = 0 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"

words[1] = "me" ,s 开始于 indices[1] = 2 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"

words[2] = "bell" ,s 开始于 indices[2] = 5 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"

【思路一】存储后缀

如果单词 X 是 Y 的后缀,那么单词 X 就不需要考虑了,因为编码 Y 的时候就同时将 X 编码了。例如,如果 words 中同时有 "me" 和 "time",我们就可以在不改变答案的情况下不考虑 "me"。

如果单词 Y 不在任何别的单词 X 的后缀中出现,那么 Y 一定是编码字符串的一部分。

因此,目标就是保留所有不是其他单词后缀的单词,最后的结果就是这些单词长度加一的总和,因为每个单词编码后后面还需要跟一个 # 符号。

代码参考:

class Solution {
/*
*
* 方式一:逐个单词遍历其后缀节点,删除为其后缀的单词
*/
public int compressWords(String[] words) {
if (words == null || words.length == 0)
{
return 0;
}
ArrayList wordsList = new ArrayList<>(Arrays.asList(words));
wordsList.sort((i, j) -> j.length() - i.length());
for (String word: words) {
for (int i=1; i wordsList.remove(word.substring(i));
}
}
int sum = 0;
for (String word2: wordsList) {
sum += (word2.length() + 1);
}
return sum;
}
}

【思路二】字典树

如方法一所说,目标就是保留所有不是其他单词后缀的单词

去找到是否不同的单词具有相同的后缀,我们可以将其反序之后插入字典树中。例如,我们有 "time" 和 "me",可以将 "emit" 和 "em" 插入字典树中。

然后,字典树的叶子节点(没有孩子的节点)就代表没有后缀的单词,统计叶子节点代表的单词长度加一的和即为我们要的答案。

代码参考:

字典树结构:
class Solution {
public int minimumLengthEncoding(String[] words) {
TrieNode trie = new TrieNode();//根节点

Map nodes = new HashMap();
for (int i = 0; i String word = words[i];
TrieNode cur = trie;
for (int j = word.length() - 1; j >= 0; --j) {
cur = cur.get(word.charAt(j));
}
nodes.put(cur, i); //记录单词索引---单词终结节点
}
int ans = 0;
for (TrieNode node: nodes.keySet()) {
if (node.count == 0) { //终结节点 的单词长度之和
ans += words[nodes.get(node)].length() + 1;
}
}
return ans;
}
}
//节点的边对应的下个节点,数组下标和'a'的差值表示边
class TrieNode {
TrieNode[] children;
int count;
TrieNode() {
children = new TrieNode[26];
count = 0;
}
public TrieNode get(char c) {
if (children[c - 'a'] == null) {
children[c - 'a'] = new TrieNode();
count++;
}
return children[c - 'a'];
}
}

字典树结构2:
class Solution {
// 定义节点结构,通过字符关联下一个节点。
class Node {
HashMap relatiOnMap= new HashMap();
Integer deep;
}
/*
* 方式二,采用字典树方式
*
*/
public int compressWords2(String[] words) {
if (words.length == 0) {
return 0;
}
// 创建根节点
ArrayList nodeList = new ArrayList();
nodeList.add(new Node());
//排序,字典树中先插入最长的单词
ArrayList wordsList = new ArrayList<>(Arrays.asList(words));
wordsList.sort((i, j) -> j.length() - i.length());
for(String word: words) {
if(!search(nodeList, word)) {
insert(nodeList, word);
}
}
int sum = 0;
for (Node node: nodeList) {
if (node.relationMap.size() == 0) {
//单词长度 + '#'
sum += node.deep + 1;
}
}
return sum;
}
private void insert(ArrayList nodeList, String word) {
if (word == null || word.length() == 0 || nodeList.size() == 0) {
return;
}
Node curNode = nodeList.get(0);
for (int i= word.length()-1; i>=0; i--) {
Character charac = word.charAt(i);
HashMap nextMap = curNode.relationMap;
if(nextMap.get(charac) == null) {
Node tmpNode = new Node();
tmpNode.deep = word.length() - i;
nextMap.put(charac, tmpNode);
nodeList.add(tmpNode);
curNode = tmpNode;
}
else {
curNode = nextMap.get(charac);
}
}
return;
}
private boolean search(ArrayList nodeList, String word) {
if (word == null || word.length() == 0 || nodeList.size() == 0) {
return false;
}
Node curNode = nodeList.get(0);
for (int i = word.length() - 1; i >= 0; i--) {
HashMap nextMap = curNode.relationMap;
if (nextMap.get(word.charAt(i)) == null) {
return false;
} else {
curNode = nextMap.get(word.charAt(i));
}
}
return true;
}
}


推荐阅读
  • 题目:图像处理(HDU1828,计算周长并集,利用线段树与离散化技术进行扫描) ... [详细]
  • 本文探讨了基于点集估算图像区域的Alpha形状算法在Python中的应用。通过改进传统的Delaunay三角剖分方法,该算法能够生成更加灵活和精确的形状轮廓,避免了单纯使用Delaunay三角剖分时可能出现的过大三角形问题。这种“模糊Delaunay三角剖分”技术不仅提高了形状的准确性,还增强了对复杂图像区域的适应能力。 ... [详细]
  • React项目基础教程第五课:深入解析组件间通信机制 ... [详细]
  • 探索偶数次幂二项式系数的求和方法及其数学意义 ... [详细]
  • 在处理遗留数据库的映射时,反向工程是一个重要的初始步骤。由于实体模式已经在数据库系统中存在,Hibernate 提供了自动化工具来简化这一过程,帮助开发人员快速生成持久化类和映射文件。通过反向工程,可以显著提高开发效率并减少手动配置的错误。此外,该工具还支持对现有数据库结构进行分析,自动生成符合 Hibernate 规范的配置文件,从而加速项目的启动和开发周期。 ... [详细]
  • 利用 Spring BeanUtils 实现 JavaBean 的深度克隆与属性复制 ... [详细]
  • Java 模式原型在游戏服务器架构中的应用与优化 ... [详细]
  • 在2021-2022 ACM集训队月度编程挑战赛第二轮中,题目“最大值与最小值的选择”要求参赛者处理一个包含n个元素的数组,并给定一个整数k。任务是通过选择特定的子数组,计算并返回这些子数组的最大值和最小值之间的差值。该问题考验了选手对数组操作和优化算法的理解与应用能力。 ... [详细]
  • 本文深入探讨了二叉树路径和问题的算法优化方法。具体而言,给定一棵二叉树,需要找出所有从根节点到叶节点的路径,其中各节点值的总和等于指定的目标值。通过详细分析和优化,提出了一种高效的解决方案,并通过多个样例验证了其有效性和性能。 ... [详细]
  • HDU1176:免费馅饼问题的动态规划解法分析
    题目“免费馅饼”通过动态规划方法进行了解析。该问题的时间限制为 Java 2000ms 和其他语言 1000ms,内存限制为 Java 65536K 和其他语言 32768K。本文详细探讨了如何利用动态规划算法高效求解此问题,并对算法的时间复杂度和空间复杂度进行了深入分析。此外,还提供了具体的实现步骤和代码示例,帮助读者更好地理解和应用这一方法。 ... [详细]
  • 在过去,我曾使用过自建MySQL服务器中的MyISAM和InnoDB存储引擎(也曾尝试过Memory引擎)。今年初,我开始转向阿里云的关系型数据库服务,并深入研究了其高效的压缩存储引擎TokuDB。TokuDB在数据压缩和处理大规模数据集方面表现出色,显著提升了存储效率和查询性能。通过实际应用,我发现TokuDB不仅能够有效减少存储成本,还能显著提高数据处理速度,特别适用于高并发和大数据量的场景。 ... [详细]
  • Eclipse JFace Text框架中IDocument接口的getNumberOfLines方法详解与编程实例 ... [详细]
  • Java Web开发中的JSP:三大指令、九大隐式对象与动作标签详解
    在Java Web开发中,JSP(Java Server Pages)是一种重要的技术,用于构建动态网页。本文详细介绍了JSP的三大指令、九大隐式对象以及动作标签。三大指令包括页面指令、包含指令和标签库指令,它们分别用于设置页面属性、引入其他文件和定义自定义标签。九大隐式对象则涵盖了请求、响应、会话、应用上下文等关键组件,为开发者提供了便捷的操作接口。动作标签则通过预定义的动作来简化页面逻辑,提高开发效率。这些内容对于理解和掌握JSP技术具有重要意义。 ... [详细]
  • 在C#中开发多线程应用程序变得高效且简便,与之前使用VB时的复杂性和局限性形成鲜明对比。C#不仅提供了丰富的多线程编程模型,还简化了线程管理、同步和通信等关键任务,使得开发者能够更加轻松地构建高性能的应用程序。此外,C#的异步编程特性进一步增强了多线程应用的开发效率和可维护性。 ... [详细]
  • Java集合框架特性详解与开发实践笔记
    Java集合框架特性详解与开发实践笔记 ... [详细]
author-avatar
清晨竹林9_877
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有