热门标签 | 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;
}
}


推荐阅读
  • 本文详细介绍了GetModuleFileName函数的用法,该函数可以用于获取当前模块所在的路径,方便进行文件操作和读取配置信息。文章通过示例代码和详细的解释,帮助读者理解和使用该函数。同时,还提供了相关的API函数声明和说明。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 电话号码的字母组合解题思路和代码示例
    本文介绍了力扣题目《电话号码的字母组合》的解题思路和代码示例。通过使用哈希表和递归求解的方法,可以将给定的电话号码转换为对应的字母组合。详细的解题思路和代码示例可以帮助读者更好地理解和实现该题目。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
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社区 版权所有