【数据结构:前缀树Trie】
目录
- 前言
- 前缀树介绍和应用
- 一、前缀树的定义
- 前缀树的问题和思考
- 前缀树的映射思想
- 前缀树三大性质
- 二.前缀树节点结构
- 三. 前缀树接口介绍和实现
- 四个·接口API
- 1. `insert(String word)`
- 2. `search(String word)`
- 3. `startsWith(String pre)`
- 4. `delete(String word)`
- API实现
- 1. 查询操作`search`
- 2. 插入操作`insert`
- 查询前缀`startsWith`
- 删除函数`delete`
- **四. Java代码完整实现**
- **五、前缀树类模板的两道题**
- **`前缀树I`**
- **前缀树II**
- **C++完整实现(deleteNode管理内存)**
- 总结
前言
本篇介绍数据结构前缀树的理论部分。
代码实现采用动态和类实现。
编程语言:Java/C++, 前面的理论部分主要以Java语言为主。
前缀树介绍和应用
前缀树(Trie Tree),读做Trie[traɪ]
。
它是一种典型的多叉树结构。字符集只包括小写字母a
~z
, 那么它就是一棵26叉树。
应用:比如自动补全功能(根据已有的前缀内容自动弹出后续可能的内容),拼写纠错,词频统计等等功能。
比如一个比较经典的问题, 给定两个字符串数组arr1
,arr2
。
arr2
中某个字符串比如abc
是否出现在arr1
中?- 接着1的问题, 如果该字符串出现,那么该字符串出现几次?
- 第3个问题, 该字符串能作为
arr1
字符串前缀串吗?若可以,那么出现过几次?比如abc
作为arr1
字符串数组abcd
,abc
,abcdf
(假设它们是arr1
数组的字符串)字符串的前缀串。 arr2
哪些字符串是作为arr1
中某些字符串出现最频繁的前缀串, 返回这些字符串和最大出现次数。
学习完下面的前缀树结构, 就能实现
一、前缀树的定义
前缀树的问题和思考
如何理解前缀树这个结构?
前缀树是如何存储字符串信息?
前缀树的节点和边要维护哪些信息?
它的核心API和实现?
前缀树的映射思想
以下假设字符串内部存储的小写字母a
~z
, 关于更多字符类型, 包括所有ASCII码和Unicode码等实现方式采用哈希表。
前缀树是一种有序树形数据结构。
理解前缀树的关键是前缀树的路径存储字符串信息。
一条边存储字符串的某个字符。
回顾路径的概念: 从根节点出发到某个叶子节点经过的边序列就是一条简单路径。
比如,参考下图。 从根节点沿着t这条边然后再走o边, 这样得到一条序列to
.。
这条路径存储了字符串to
的信息。
这反映了字符串to
存在, 因为前缀树存在一条存储的路径。同理,我们可以从根节点出发沿着t->e->a
这条边到达叶子节点。这说明tea
字符串也存在。
下面我们对前缀树的边考虑。
边至少要存储3个信息: 边的起点,边的终点,边附带的字符。
好像应该这么设计边比较合适。
事实上, 前缀树不需要对边单独定义。
前缀树只定义了节点Node
,没有定义Edge
。
为什么呢?
不妨考虑节点如何定义。 当前节点如何知道下一步去往哪些节点呢?字符信息!
比如从根节点出发, 查询字符串to
是否出现过, 先从根节点找到并前往t
字符的节点, 然后再从该节点找到前往o
字符的节点。
如何知道通往对应的字符的节点呢? 用一个数组或者哈希表存储字符->节点
的信息。
你可能大概知道如何定义了, 请看下面的java代码。
当前节点+字符信息->下一个节点
, 理解这个就明白为什么边不需要定义了, 因为节点的这种关联关系更加方便。
class TrieNode{TrieNode[] next;TrieNode(){//'a'->0//'b'->1//...//'z'->25next = new TrieNode[26];}
}
前缀树用到了映射的思想, 节点内部定义了一个关联数组代替边的功能来查找当前节点去往指定字符的下一个节点。
事实上, 一个字符串对应前缀树的某一条特定路径。字符串是作为前缀树查询的完整的键, 某个节点只是单独存储键的一个字符。
前缀树三大性质
根节点不包含字符,除根节点外每一个节点都只包含一个字符。
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
每个节点的所有子节点包含的字符都不相同。
二.前缀树节点结构
前面篇幅提过前缀树不需要定义边, 只需要节点带关联数组或者哈希表就可以替代边的功能。
下面对节点完善定义。
前面提出的问题中, 我们需要从前缀树获取某个字符串出现的次数信息, 以及字符串作为前缀的次数。
还是这张图
。
比如字符串ten
出现了12次。 如何从前缀树中获取呢?
还是那句话,字符串对应前缀树的路径。从根节点出发,沿着t
到达节点1·t, 后e
走到节点2·te, 最后n
走到节点3·ten。
最后一个节点存储end
信息, end
表示从根节点出发到该节点结尾的字符串个数。
新增一个字符串ten
就沿着路径t->te->ten
,对末尾的节点3·ten的end++
。
同理,若要查询ten
字符串出现的次数,只需要从根节点出发走到3·ten读取end
信息即可。
因此,一个节点存储end信息记录字符串出现的次数
te
是作为tea
,ted
,ten
这三种字符串的前缀串。反映在前缀树中就是这条子路径,被多少条路径覆盖了。对每个节点加入pass
属性,记录当前该节点的字符串数量。表示从根节点沿着某条路径到当前节点的字符串作为前缀串的次数。
如下就是前缀树节点的完整定义:
//Java内部类
public static class TrieNode{//记录经过该节点的字符串数量int pass;//记录以该节点结尾的字符串数量int end;//指向子节点的指针数组TrieNode[] next;//HashMap<Character,TrieNode> next 哈希表充当路public TrieNode(){pass = 0;end = 0;//next['a'-'a']->null; 当前节点不存在走向字符a的路。//next['a'-'a']->不是null,是某一个TrieNode。 存在走向字符a的路。//next['z'-'a']:next[25] 有没有走向字符z的路。next = new TrieNode[26];//26个小写字母}}
三. 前缀树接口介绍和实现
下面将详细介绍前缀树的四个主要接口函数及其参数功能:
四个·接口API
public void insert(String word);
public int search(String word);
public int startsWith(String pre);
public void delete(String word);
1. insert(String word)
-
功能:将一个字符串
word
插入到前缀树中。若word
已经存在于前缀树中,则相应的计数器end
增加。 -
参数:
word
:需要插入到前缀树中的字符串。这里由小写字母组成,可以根据需求扩展支持其他字符集。
2. search(String word)
-
功能:查询字符串
word
在前缀树中出现的次数。 -
参数:
word
:需要查询的字符串。
3. startsWith(String pre)
-
功能:查询以字符串
pre
作为前缀的所有字符串的数量。 -
参数:
pre
:需要查询的前缀字符串。
-
返回值:
int
:表示以pre
为前缀的字符串数量。若没有任何字符串以pre
为前缀,返回0
。
4. delete(String word)
-
功能:从前缀树中删除一个字符串
word
。如果word
在前缀树中存在多次,则减少其出现次数;如果只存在一次,则完全删除word
,并释放不再需要的节点。 -
参数:
word
:需要删除的字符串。
-
返回值:
int
:表示word
在前缀树中出现的次数。如果word
不存在,返回0
。
API实现
1. 查询操作search
理解查询操作,那么实现API将毫无难度, 前缀树API之间代码极其相似。
查找核心是找到字符串最后一个字符对应前缀树中节点的end
信息。
- 算法流程:
- 步骤:
- 从根节点开始遍历。定义
cur=root
,cur:循环变量。 - 对于字符串
word
中的每一个字符,计算其在子节点数组中的索引。 - 检查当前节点的对应子节点是否存在:
- 如果不存在,说明
word
不在前缀树中,返回0
。 - 如果存在,移动到该子节点。
- 如果不存在,说明
- 遍历完所有字符后,返回最后一个节点的
end
计数,表示word
出现的次数。
- Java代码实现:
/**** @param word 在前缀树中查询word字符串出现了几次* @return word在前缀树中出现的次数。*/public int search(String word){if(word==null) return 0;char[] s = word.toCharArray();TrieNode cur = root;for(int i=0,index;i<s.length;i++){index = s[i]-'a';if(cur.next[index]==null){return 0;}cur = cur.next[index];}return cur.end;}
2. 插入操作insert
插入操作对于循环的处理与查找相同。
但需注意这里是新增字符串, 每经过一个节点都要对其pass
进行自增。
还需注意字符串可能是初次添加进前缀树, 对于不存在的路,要创建新节点。
-
详细说明:
- 步骤:
- 从根节点开始遍历。定义
cur=root
,cur:循环变量。 - 对于字符串
word
中的每一个字符,计算其在子节点数组中的索引(例如,'a'
对应索引0
,'b'
对应索引1
,以此类推)。 - 检查当前节点的对应子节点是否存在:
- 如果不存在,则创建一个新的子节点。
- 如果存在,直接移动到该子节点。
- 每访问一个子节点,增加该节点的
pass
计数,表示有一个字符串经过该节点。 - 最后一个字符对应的节点,增加其
end
计数,表示有一个字符串以该节点结尾。
- 从根节点开始遍历。定义
- 步骤:
-
Java代码实现:
public void insert(String word){if(word==null) return ;char[] s = word.toCharArray();TrieNode cur = root;cur.pass++;for(int i=0,index;i<s.length;i++){index = s[i] - 'a';if(cur.next[index]==null){cur.next[index] = new TrieNode();}cur = cur.next[index];cur.pass++;}cur.end++;}
- 举例
//假设封装好一个前缀树类TrieTree TrieTree trie = new TrieTree(); trie.insert("apple"); trie.insert("app");
以下是创建路和不创建路只增加pass
,end
的例子。
- 插入
"apple"
时,会依次创建节点a
->p
->p
->l
->e
,并相应增加pass
和end
计数。 - 插入
"app"
时,节点a
、p
、p
已存在,只需增加相应的pass
和end
计数。
查询前缀startsWith
查询前缀串与查询字符串逻辑完全相同,唯一区别在于查询前缀最后读取pass
信息, 并非end
信息。
-
详细说明:
- 步骤:
- 从根节点开始遍历。定义
cur=root
,cur:循环变量。 - 对于前缀
pre
中的每一个字符,计算其在子节点数组中的索引。 - 检查当前节点的对应子节点是否存在:
- 若不存在,则说明没有字符串以
pre
为前缀,返回0
。 - 若存在,移动到该子节点。
- 若不存在,则说明没有字符串以
- 遍历完所有字符后,返回最后一个节点的
pass
计数,表示有多少个字符串经过该节点,即以pre
为前缀的字符串数量。
- 从根节点开始遍历。定义
- 步骤:
-
Java代码实现
/*** @apiNote 查询前缀树中以pre为前缀的字符串数目。* @param pre 前缀串* @return 以pre前缀的字符串数目*/public int startsWith(String pre){if(pre==null) return 0;char[] s = pre.toCharArray();TrieNode cur = root;for(int i=0,index;i<s.length;i++){index = s[i]-'a';if(cur.next[index]==null){return 0;}cur = cur.next[index];}return cur.pass;}
- 示例:
int prefixCount = trie.startsWith("ap"); // 返回 2 ("apple" 和 "app") prefixCount = trie.startsWith("app"); // 返回 2 ("apple" 和 "app") prefixCount = trie.startsWith("ban"); // 返回 0
删除函数delete
删除函数实现的注意事项。
先做查询! 字符串存在则执行后面的删除。
根据存在的字符串出现的个数, 又分两种情况。
- 沿途减少计数
pass
,最末尾字符减少end
. - 在1的基础上,出现
pass
为0的情况, 意味着当前节点开始后续的节点都无效了。需要释放(C++手动管理内存,Java直接置空交给JVM处理)。
C++代码会解释如何手动释放不必要节点, 参考下文C++实现。
-
详细说明:
- 步骤:
- 查询存在性:首先调用
search(word)
方法检查word
是否存在于前缀树中。若不存在,直接返回,删除无效! - 遍历减少计数:
- 根节点开始,依次遍历
word
中的每一个字符。 - 对于每一个字符,计算其在子节点数组中的索引。
- 移动到对应的子节点,并减少该节点的
pass
计数。
- 根节点开始,依次遍历
- 减少
end
计数:在最后一个字符对应的节点,减少其end
计数,表示word
出现次数减少。 - 删除pass为0及以后的节点:
- 从后往前遍历
word
中的字符,检查每个节点的pass
计数。 - 如果某个节点的
pass
计数降为0
,表示没有其他字符串经过该节点,可以删除该节点并将其父节点对应的指针设为null
(Java)。 - 继续向前检查,直到遇到
pass
不为0
的节点为止。
- 从后往前遍历
- 查询存在性:首先调用
- 步骤:
-
Java代码实现:
/**** @param word 前缀树中删除的字符串word* 如果字符串word不存在什么也不做* 若字符串word不止一个, 那么减少一次词频。* 若删除导致路径不存在了,那么释放节点。*/public void delete(String word){if(search(word)==0) return;//删除的word在前缀树中不存在。TrieNode cur = root;cur.pass--;char[] s = word.toCharArray();for(int i=0,index=0;i<s.length;i++){index = s[i] - 'a';if(--cur.next[index].pass==0){cur.next[index] = null;//JVM会处理return ;}cur = cur.next[index];}cur.end--;}
-
示例:
trie.insert("apple"); trie.insert("apple"); trie.insert("app");trie.delete("apple"); int count = trie.search("apple"); // 返回 1trie.delete("apple"); count = trie.search("apple"); // 返回 0trie.delete("app"); count = trie.search("app"); // 返回 0
- 在上述示例中,
"apple"
被插入了两次,删除一次后,"apple"
的出现次数减少为1
。 - 再次删除
"apple"
后,"apple"
不再存在于前缀树中,节点被删除。 - 删除
"app"
后,"app"
也被完全移除。
- 在上述示例中,
四. Java代码完整实现
节点类和前缀树类是两个内部类,自行改写。
package Code01_TrieTree;public class Code01_TrieTree {public static class TrieNode{//记录经过该节点的字符串数量int pass;//记录以该节点结尾的字符串数量int end;//指向子节点的指针数组TrieNode[] next;//HashMap<Character,TrieNode> next 哈希表充当路public TrieNode(){pass = 0;end = 0;//next['a'-'a']->null; 当前节点不存在走向字符a的路。//next['a'-'a']->不是null,是某一个TrieNode。 存在走向字符a的路。//next['z'-'a']:next[25] 有没有走向字符z的路。next = new TrieNode[26];//26个小写字母}}public static class Trie{private TrieNode root;public Trie(){root = new TrieNode();}public void insert(String word){if(word==null) return ;char[] s = word.toCharArray();TrieNode cur = root;cur.pass++;for(int i=0,index;i<s.length;i++){index = s[i] - 'a';if(cur.next[index]==null){cur.next[index] = new TrieNode();}cur = cur.next[index];cur.pass++;}cur.end++;}/**** @param word 在前缀树中查询word字符串出现了几次* @return word在前缀树中出现的次数。*/public int search(String word){if(word==null) return 0;char[] s = word.toCharArray();TrieNode cur = root;for(int i=0,index;i<s.length;i++){index = s[i]-'a';if(cur.next[index]==null){return 0;}cur = cur.next[index];}return cur.end;}/*** @apiNote 查询前缀树中以pre为前缀的字符串数目。* @param pre 前缀串* @return 以pre前缀的字符串数目*/public int startsWith(String pre){if(pre==null) return 0;char[] s = pre.toCharArray();TrieNode cur = root;for(int i=0,index;i<s.length;i++){index = s[i]-'a';if(cur.next[index]==null){return 0;}cur = cur.next[index];}return cur.pass;}/*** @param word 前缀树中删除的字符串word* 如果字符串word不存在什么也不做* 若字符串word不止一个, 那么减少一次词频。* 若删除导致路径不存在了,那么释放节点。*/public void delete(String word) {if (search(word) == 0) return;//删除的word在前缀树中不存在。TrieNode cur = root;cur.pass--;char[] s = word.toCharArray();for (int i = 0, index = 0; i < s.length; i++) {index = s[i] - 'a';if (--cur.next[index].pass == 0) {cur.next[index] = null;//JVM会处理return;}cur = cur.next[index];}cur.end--;}}
}
五、前缀树类模板的两道题
其它的测试链接要用静态数组的方式实现, 将在算法篇的前缀树说明。
以下是来自leetcode的两道前缀树类模板的题型
测试链接:
前缀树I
前缀树II
前缀树I
直接改写一下部分方法的返回类型和返回值即可。
class Trie {public static class TrieNode {int pass;int end;TrieNode[] next;// HashMap<Character,TrieNode> next 哈希表充当路public TrieNode() {pass = 0;end = 0;// next['a'-'a']->null; 当前节点不存在走向字符a的路。// next['a'-'a']->不是null,是某一个TrieNode。 存在走向字符a的路。// next['z'-'a']:next[25] 有没有走向字符z的路。next = new TrieNode[26];// 26个小写字母}}private TrieNode root;public Trie() {root = new TrieNode();}public void insert(String word) {if (word == null)return;char[] s = word.toCharArray();TrieNode cur = root;cur.pass++;for (int i = 0, index; i < s.length; i++) {index = s[i] - 'a';if (cur.next[index] == null) {cur.next[index] = new TrieNode();}cur = cur.next[index];cur.pass++;}cur.end++;}/**** @param word 在前缀树中查询word字符串出现了几次* @return word在前缀树中出现的次数。*/public boolean search(String word) {if (word == null)return false;char[] s = word.toCharArray();TrieNode cur = root;for (int i = 0, index; i < s.length; i++) {index = s[i] - 'a';if (cur.next[index] == null) {return false;}cur = cur.next[index];}return cur.end!=0;}/*** @apiNote 查询前缀树中以pre为前缀的字符串数目。* @param pre 前缀串* @return 以pre前缀的字符串数目*/public boolean startsWith(String pre) {if (pre == null)return false;char[] s = pre.toCharArray();TrieNode cur = root;for (int i = 0, index; i < s.length; i++) {index = s[i] - 'a';if (cur.next[index] == null) {return false;}cur = cur.next[index];}return true;}
}
前缀树II
改个方法名, 提交一下。 本题是会员题。
提供题目所给的示例一,自行对照。非会员的朋友们可以参考一下。
输入
[“Trie”, “insert”, “insert”, “countWordsEqualTo”, “countWordsStartingWith”, “erase”, “countWordsEqualTo”, “countWordsStartingWith”, “erase”, “countWordsStartingWith”]
[[], [“apple”], [“apple”], [“apple”], [“app”], [“apple”], [“apple”], [“app”], [“apple”], [“app”]]
输出
[null, null, null, 2, 2, null, 1, 1, null, 0]
解释
Trie trie = new Trie();
trie.insert(“apple”); // 插入 “apple”。
trie.insert(“apple”); // 插入另一个 “apple”。
trie.countWordsEqualTo(“apple”); // 有两个 “apple” 实例,所以返回 2。
trie.countWordsStartingWith(“app”); // “app” 是 “apple” 的前缀,所以返回 2。
trie.erase(“apple”); // 移除一个 “apple”。
trie.countWordsEqualTo(“apple”); // 现在只有一个 “apple” 实例,所以返回 1。
trie.countWordsStartingWith(“app”); // 返回 1
trie.erase(“apple”); // 移除 “apple”。现在前缀树是空的。
trie.countWordsStartingWith(“app”); // 返回 0
class Trie {class TrieNode {public int pass;public int end;public TrieNode[] nexts;public TrieNode() {pass = 0;end = 0;nexts = new TrieNode[26];}}private TrieNode root;public Trie() {root = new TrieNode();}public void insert(String word) {TrieNode node = root;node.pass++;for (int i = 0, path; i < word.length(); i++) { // 从左往右遍历字符path = word.charAt(i) - 'a'; // 由字符,对应成走向哪条路if (node.nexts[path] == null) {node.nexts[path] = new TrieNode();}node = node.nexts[path];node.pass++;}node.end++;}// 如果之前word插入过前缀树,那么此时删掉一次// 如果之前word没有插入过前缀树,那么什么也不做public void erase(String word) {if (countWordsEqualTo(word) > 0) {TrieNode node = root;node.pass--;for (int i = 0, path; i < word.length(); i++) {path = word.charAt(i) - 'a';if (--node.nexts[path].pass == 0) {node.nexts[path] = null;return;}node = node.nexts[path];}node.end--;}}// 查询前缀树里,word单词出现了几次public int countWordsEqualTo(String word) {TrieNode node = root;for (int i = 0, path; i < word.length(); i++) {path = word.charAt(i) - 'a';if (node.nexts[path] == null) {return 0;}node = node.nexts[path];}return node.end;}// 查询前缀树里,有多少单词以pre做前缀public int countWordsStartingWith(String pre) {TrieNode node = root;for (int i = 0, path; i < pre.length(); i++) {path = pre.charAt(i) - 'a';if (node.nexts[path] == null) {return 0;}node = node.nexts[path];}return node.pass;}}
C++完整实现(deleteNode管理内存)
C++earse
函数需要释放无效的节点。
思路:
用vector收集路径的所有节点的地址, 然后遍历vector删除所有pass==0
的节点。
// 删除一个单词void erase(const string& word) {if (countWordsEqualTo(word) == 0) return; // 单词不存在,无法删除Node* node = root;node->pass--; // 根节点的pass减少// 记录遍历路径上的节点,用于可能的回溯删除vector<Node*> path;path.push_back(node);for (char ch : word) {int index = ch - 'a';Node* nextNode = node->nexts[index];nextNode->pass--;path.push_back(nextNode);node = nextNode;}node->end--; // 单词结尾计数减少// 从后向前检查是否需要删除节点for (int i = word.size(); i >= 1; --i) {Node* cur = path[i];if (cur->pass == 0) {// 当前节点的pass为0,删除该节点Node* p = path[i - 1];int index = word[i - 1] - 'a';delete p->nexts[index];p->nexts[index] = nullptr;} else {// 当前节点的pass不为0,其他单词仍在使用该节点break;}}}
完整代码
class Trie {
private:// TrieNode 类,表示 Trie 中的每个节点class TrieNode {public:int pass; // 经过该节点的单词数int end; // 以该节点为结尾的单词数vector<TrieNode*> nexts; // 子节点,大小为 26,对应 'a' 到 'z'TrieNode() : pass(0), end(0), nexts(26, nullptr) {}// 析构函数,递归删除子节点~TrieNode() {for (int i = 0; i < 26; ++i) {if (nexts[i] != nullptr) {delete nexts[i];nexts[i] = nullptr;}}}};typedef TrieNode Node;TrieNode* root; // Trie 的根节点public:// 构造函数,初始化 TrieTrie() {root = new TrieNode();}// 析构函数~Trie() {delete root;}// 插入一个单词void insert(const string& word) {Node* node = root;node->pass++; // 根节点的 pass 加 1for (char c : word) {int path = c - 'a'; // 计算字符对应的索引(0 到 25)if (node->nexts[path] == nullptr) {node->nexts[path] = new TrieNode(); // 如果该路径没有节点,则创建新节点}node = node->nexts[path];node->pass++; // 经过该节点的单词数加 1}node->end++; // 单词的结束节点,end 加 1}// 删除一个单词void erase(const string& word) {if (countWordsEqualTo(word) == 0) return; // 单词不存在,无法删除Node* node = root;node->pass--; // 根节点的pass减少// 记录遍历路径上的节点,用于可能的回溯删除vector<Node*> path;path.push_back(node);for (char ch : word) {int index = ch - 'a';Node* nextNode = node->nexts[index];nextNode->pass--;path.push_back(nextNode);node = nextNode;}node->end--; // 单词结尾计数减少// 从后向前检查是否需要删除节点for (int i = word.size(); i >= 1; --i) {Node* cur = path[i];if (cur->pass == 0) {// 当前节点的pass为0,删除该节点Node* p = path[i - 1];int index = word[i - 1] - 'a';delete p->nexts[index];p->nexts[index] = nullptr;} else {// 当前节点的pass不为0,其他单词仍在使用该节点break;}}}// 查询前缀树中,单词出现的次数int countWordsEqualTo(const string& word) {Node* node = root;for(int i=0,path;i<word.size();i++){path=word[i]-'a';if(node->nexts[path]==nullptr){return 0;}node = node->nexts[path];}return node->end;}// 查询前缀树中,有多少单词以指定前缀开始int countWordsStartingWith(const string& prefix) {Node* node = root;for (char c : prefix) {int path = c - 'a'; // 计算字符对应的索引if (node->nexts[path] == nullptr) {return 0; // 如果没有该前缀的路径,返回 0}node = node->nexts[path];}return node->pass; // 返回以该前缀开始的单词数量}
};
总结
前缀树是一个学习起来非常容易的树。
理论很简单, 明白节点的定义的。API实现想明白查询操作的写法,也是一通百通。
前缀树的数据结构和类模板实现, 到此结束。
有需要补充的内容,笔者会更新此篇内容的。
后续会更新算法篇的前缀树。
相关文章:
【数据结构:前缀树Trie】
目录 前言前缀树介绍和应用一、前缀树的定义前缀树的问题和思考前缀树的映射思想前缀树三大性质 二.前缀树节点结构三. 前缀树接口介绍和实现四个接口API1. insert(String word)2. search(String word)3. startsWith(String pre)4. delete(String word) API实现1. 查询操作sear…...
如何让QPS提升20倍
一、什么是QPS QPS,全称Queries Per Second,即每秒查询率,是用于衡量信息检索系统(例如搜索引擎或数据库)或请求-响应系统(如Web服务器)每秒能够处理的请求数或查询次数的一个性能指标。以下是…...
时间复杂度简介
定义 时间复杂度是用来衡量算法运行时间随着输入规模增长而增长的量级。简单来说,它描述了算法执行时间与数据规模之间的关系。我们通常用大O符号( O O O)来表示时间复杂度。例如,对于一个简单的加法运算,它的执行时间…...
记一次sealos部署k8s集群之delete了第一台master如何恢复
记一次sealos部署k8s集群之delete了第一台master如何恢复 一、背景描述 使用sealos部署了一套K8S集群 master信息:172.27.100.1、172.27.100.2、172.27.100.3 node信息:172.27.100.4、172.27.100.5 sealos安装在172.27.100.1节点,根目录下/root/.sealos/文件还在! [root…...
【json】
JSON JSON是一种轻量级的,按照指定的格式去组织和封装数据的数据交互格式。 本质上是一个带有特定格式的字符串(py打印json时认定为str类型) 在各个编程语言中流通的数据格式,负责不同编程语言中的数据传递和交互,类似于计算机普通话 python与json关系及相互转换…...
TypeScript语言的并发编程
TypeScript语言的并发编程 引言 随着现代应用程序的复杂性不断增加,性能和用户体验的重要性显得尤为突出。在这种背景下,并发编程应运而生,成为提升应用程序效率的重要手段。在JavaScript及其超集TypeScript中,尽管语言本身是单…...
左值引用(Lvalue Reference)和右值引用(Rvalue Reference)详解
左值引用(Lvalue Reference)和右值引用(Rvalue Reference)详解 文章目录 左值引用(Lvalue Reference)和右值引用(Rvalue Reference)详解1. 什么是左值和右值?左值&#x…...
音视频入门基础:RTP专题(1)——RTP官方文档下载
一、引言 实时传输协议(Real-time Transport Protocol,简写RTP)是一个网络传输协议,由IETF的多媒体传输工作小组1996年在《RFC 1889》中公布的。 RTP作为因特网标准在《RFC 3550》有详细说明。而《RFC 3551》详细描述了使用最小…...
【Flutter】使用ScrollController配合EasyRefresh实现列表预加载:在还未滑动到底部时加载下一页数据
需求/背景 在我们的业务场景中,列表的加载使用easy_refresh组件: https://pub.dev/packages/easy_refresh 大概效果是往上滑动到一定的offset会触发一个上滑加载,可以触发一些网络请求拉取列表后面的数据来展示。 这种模式一般在一页翻完…...
js实现md5加密
要在JavaScript中实现MD5加密并截取特定位置的字符,你可以使用像crypto-js这样的库。首先,你需要确保你的项目中包含了crypto-js库。如果你是在浏览器环境中,可以通过CDN引入;如果是在Node.js环境中,可以通过npm安装。…...
[java基础-集合篇]LinkedList源码粗析
LinkedList 的数据结构 实现List、Deque 接口,基于 双向链表实现的列表。与基于数组的 ArrayList 不同,基于链表的LinkedList 允许在列表的任何位置快速地插入和删除元素。 Java中LinkedList实现了Deque,它提供了 add, offer, remove, poll, …...
【Rust自学】11.1. 编写和运行测试
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(・ω・) 11.1.1. 什么是测试 在Rust里一个测试就是一个函数,它被用于验证非测试代码的功能是否和预期一致。 在一个测试的函数体里通…...
移动端屏幕分辨率rem,less
谷歌模拟器:能直接看到移动端效果 屏幕分辨率 右键电脑桌面 ,点击显示设置 PC端是逻辑分辨率,移动端代码也是参考逻辑分辨率 网页端宽度和逻辑分辨率尺寸相同 手机屏幕尺寸不同,网页宽度均为 100% 所以就需要添加视口标签&#x…...
rk3568 , buildroot , qt ,使用sqlite, 动态库, 静态库
问题说明: 客户反馈 ,buildroot 系统 ,使用qt 使用sqlite ,有报错,无法使用sqlite. 测试情况说明: 我自己测试,发现, buildroot 自己默认就是 使能了 sqlite 的。 是否解决说明&…...
web-app uniapp监测屏幕大小的变化对数组一行展示数据作相应处理
web-app uniapp监测屏幕大小的变化对数组一行展示数据作相应处理 1.uni.getSystemInfoSync().screenWidth; 获取屏幕宽度 2.uni.onWindowResize() 实时监测屏幕宽度变化 3.根据宽度的大小拿到每行要展示的数量itemsPerRow 4.为了确保样式能够根据 items…...
Airflow:TimeSensor感知时间条件
在数据管道工作流中,任务可能需要在特定的时间执行,或者在继续之前等待一定的时间。为了满足这些需求,Apache Airflow提供了TimeSensor,这是一种内置Sensor,可以监控当前时间,并在达到指定时间时触发后续任…...
使用Python和Neo4j驱动程序来实现小规模数据的CSV导入
要将CSV数据导入到Neo4j数据库中,你可以使用Neo4j提供的工具,比如neo4j-admin import命令(适用于大规模数据导入),或者使用Python的Neo4j驱动程序通过Cypher查询逐行插入数据(适用于小规模数据导入…...
网络安全测评技术与标准
网络安全测评概况 网络安全测评是网络信息系统和IT技术产品的安全质量保障。本节主要阐述网络安全测评的概念,给出网络安全测评的发展状况。 18.1.1 网络安全测评概念 网络安全测评是指参照一定的标准规范要求,通过一系列的技术和管理方法,获…...
某漫画网站JS逆向反混淆流程分析
文章目录 1. 写在前面1. 接口分析2. 反混淆分析 【🏠作者主页】:吴秋霖 【💼作者介绍】:擅长爬虫与JS加密逆向分析!Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Pyth…...
如何获取文件的MIME类型
文章目录 1. 概念介绍2. 方法与类型2.1 使用方法2.2 常见类型3. 示例代码4. 内容总结我们在上一章回中介绍了"如何加载本地图片"相关的内容,本章回中将介绍如何获取文件类型.闲话休提,让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章回中提到的文件类型是指MI…...
Three.js 基础概念:构建3D世界的核心要素
文章目录 前言一、场景(Scene)二、相机(Camera)三、渲染器(Renderer)四、物体(Object)五、材质(Material)六、几何体(Geometry)七、光…...
Linux web服务器
Linux 作为 Web 服务器操作系统 安装 Web 服务器软件(以 Apache 为例) 步骤一:更新系统软件包列表 在 CentOS 系统中,使用命令 yum update -y 这个命令会连接到 CentOS 的软件包仓库,检查所有已安装软件包是否有更…...
Linux 下信号的保存和处理
信号的几个状态 信号抵达: 当接收到的信号被处理时, 此时就成为信号的抵达信号的未决: 从信号的产生到信号抵达这个时间段之间, 称为信号未决信号阻塞: 当进程设置了某个信号为阻塞后, 这个进程就不会在接收到这个信号信号忽略: 将信号设置为忽略后, 接收到这个信号, 对这个信…...
宝塔安装教程,bt怎么安装 linux
Centos安装脚本 yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh 37a09b35 Ubuntu/Deepin安装脚本 wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo b…...
java通过ocr实现识别pdf中的文字
需求:识别pdf文件中的中文 根据github项目mymonstercat 改造,先将pdf文件转为png文件存于临时文件夹,然后通过RapidOcr转为文字,最后删除临时文件夹 1、引入依赖 <dependency><groupId>org.apache.pdfbox</groupId><artifactId&g…...
基于SpringBoot的养老院管理系统
作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏:…...
阿里云发现后门webshell,怎么处理,怎么解决?
当收到如下阿里云通知邮件时,大部分管理员都会心里一惊吧!出现Webshell,大概是网站被入侵了。 尊敬的 xxxaliyun.com: 云盾云安全中心检测到您的服务器:47.108.x.xx(xx机)出现了紧急安全事件…...
韩顺平老师Linux学习笔记【持续更新...】
1、课程内容 1.1、课程大纲 1.2、Linux使用在哪些地方 Linux运维工程师Linux嵌入式工程师Linux下开发项目:JavaEE、大数据、Python、PHP、C/C、Go 1.3、Linux的应用领域 个人桌面领域服务器领域(最强领域)嵌入式领域 2、Linux入门 2.1、…...
Cognitive architecture 又是个什么东东?
自Langchain: https://blog.langchain.dev/what-is-a-cognitive-architecture/ https://en.wikipedia.org/wiki/Cognitive_architecture 定义 A cognitive architecture refers to both a theory about the structure of the human mind and to a computational…...
【css】浏览器强制设置元素状态(hover|focus……)
直接上步骤: 打开浏览器控制台 → 找到样式选项 → 找到:hov选项 → 点击:hov选项,会展开【设置元素状态】。 只要选中就会展示出自己写在css里面的该种状态下的样式了。...
leetcode热题100——NO.160相交链表——JAVA
一、题目描述 题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。题目数据 保证 整个链式结构中不存在环。 注意,函数返回结果后,链表必…...
基于Media+Unity的手部位姿三维位姿估计
使用mediapipe Unity 手部位姿三维位姿估计 参考文章 基于Mediapipe的姿势识别并同步到Unity人体模型中 MediapipeUnity3d实现虚拟手_unity mediapipe-CSDN博客 需求 我的需求就是快速、准确的跟踪手部位姿并实现一个三维显示。 主要思路 搭建mdeiapipe系统,…...
cJson——序列化格式json和protobuf对比
cJson——序列化格式json和protobuf对比 1. 更小的消息体积2. 更快的序列化与反序列化速度3. 类型安全4. 向后和向前兼容性5. 更低的带宽消耗6. 高效的编码方式7. 易于跨语言支持8. 支持复杂的数据结构9. 更好的支持大型数据交换总结 Protocol Buffers (Protobuf) 和 JSON 都是…...
STM32F1学习——ADC模数转换器
一、ADC模数转换器 ADC的全称 Analog-Digital Converter 模拟-数字转换器,他可以用来将引脚上连续变换的模拟电压转换为内存中存储的数字变量。 ADC有两个重要指标,分辨率和频率。 STM32的ADC是 12位 逐次逼近型,1us转换时间,也就…...
2025-1-10-sklearn学习(36、37) 数据集转换-无监督降维+随机投影 沙上并禽池上暝。云破月来花弄影。
文章目录 sklearn学习(36、37) 数据集转换-无监督降维随机投影sklearn学习(36) 数据集转换-无监督降维36.1 PCA: 主成份分析36.2 随机投影36.3 特征聚集 sklearn学习(37) 数据集转换-随机投影37.1 Johnson-Lindenstrauss 辅助定理37.2 高斯随机投影37.3 稀疏随机矩阵 sklearn学…...
Linux之线程池与单例模式
目录 线程池 线程池代码 单例模式 饿汉模式单例模式 懒汉模式单例模式 在前几期,我们已经学习了多线程的创建和控制,学习了多线程中的同步和互斥,学习了多线程中的条件变量和信号量,基于此我们实现了基于阻塞队列和基于环形队…...
LabVIEW调用不定长数组 DLL数组
在使用 LabVIEW 调用 DLL 库函数时,如果函数中的结构体包含不定长数组,直接通过 调用库函数节点(Call Library Function Node) 调用通常会遇到问题。这是因为 LabVIEW 需要与 DLL 中的数据结构完全匹配,而包含不定长数…...
计算机的错误计算(二百零七)
摘要 利用两个数学大模型计算 arccot(0.125664e2)的值,结果保留16位有效数字。 实验表明,它们的输出中分别仅含有3位和1位正确数字。 例1. 计算 arccot(0.125664e2)的值,结果保留16位有效数字。 下面是与一个数学解题器的对话。 以上为与…...
基于 GEE 利用 RF 回归模型实现空间降尺度
目录 1 前言 2 完整代码 3 运行结果 1 前言 本篇讲述在GEE上基于回归模型降尺度,也就是要复现以下论文,该论文发表在J-Star期刊上。 “Ebrahimy H, Aghighi H, Azadbakht M, et al. Downscaling MODIS land surface temperature product using an a…...
Linux 系统下磁盘相关指令:df、du、fdisk、lsblk
文章目录 I df、du、fdisk、lsblk指令df命令用于显示文件系统的磁盘空间使用情况du命令用于估算目录或文件的磁盘空间使用情况fdisk命令用于对磁盘进行分区操作lsblk指令查看设备信息II 应用du估算目录或文件的磁盘空间使用情况lsblk查看服务器上查看硬盘个数III 知识扩展磁盘阵…...
在线或离线llama.cpp安装和模型启动
该版本安装时间是2025-01-10,因为不同版本可能安装上会有所不同,下面也会讲到。 先说下问题——按照官方文档找不到执行命令llama-cli或./llama-cli 先附上llama.cpp的github地址:https://github.com/ggerganov/llama.cpp,build…...
(Arxiv-2023)LORA-FA:针对大型语言模型微调的内存高效低秩自适应
LORA-FA:针对大型语言模型微调的内存高效低秩自适应 paper是香港浸会大学发表在Arxiv 2023的工作 paper title:LORA-FA: MEMORY-EFFICIENT LOW-RANK ADAPTATION FOR LARGE LANGUAGE MODELS FINE-TUNING ABSTRACT 低秩自适应 (LoRA) 方法可以大大减少微调…...
Ubuntu | 系统软件安装系列指导说明
文章目录 Ubuntu 系统软件安装系列指导说明工具系列1. Docker 与 Docker-Compose部署与安装 环境系列1. Golang部署与安装 数据库系列1. PostgreSQL17.2源码部署与安装 Ubuntu 系统软件安装系列指导说明 工具系列 1. Docker 与 Docker-Compose部署与安装 链接 环境系列 1…...
攻防靶场(32):两个爆破技巧 Funbox 7 EasyEnum
目录 攻击路径一 1. 侦查 1.1 收集目标网络信息:IP地址 1.2 主动扫描:扫描IP地址段 1.3 主动扫描:字典扫描 2. 初始访问 2.1 有效帐号:本地账户 3. 权限提升 3.1 滥用特权控制机制:Sudo和Sudo缓存 4. 凭据访问 4.1 凭据…...
Vue3初学之插槽(slot)使用
在 Vue 3 中,插槽(Slots)是一种强大的内容分发机制,允许你在组件中定义可替换的内容区域,从而使组件更加通用和灵活。以下是 Vue 3 中插槽的几种常见用法: 默认插槽 默认插槽是最基本的插槽类型࿰…...
从 0 开始上手 Solana 智能合约
Solana CLI 基础知识 Solana CLI 是一个命令行界面工具,提供了一系列用于与 Solana Cluster 交互的命令。 我们将介绍一些最常见的命令,但你始终可以通过运行 solana --help 查看所有可能的 Solana CLI 命令列表。 Solana CLI 配置 Solana CLI 存储了…...
USB基础 -- USB 控制传输(Control Transfer)的重传机制
USB 控制传输(Control Transfer)的重传机制 1. 控制传输的事务结构 控制传输分为三个阶段,每个阶段都有自己的事务,并可能触发重传机制: 设置阶段(Setup Stage):主机发送 8 字节的…...
创建基本的 Electron 应用项目的详细步骤
创建一个基本的 Electron 应用项目的详细步骤。我们将从安装 Node.js 开始,然后创建项目文件夹并初始化 Electron 项目。 1. 安装 Node.js 首先,确保你已经安装了 Node.js 和 npm。你可以在终端中运行以下命令来检查是否已经安装: node -v…...
《异步编程之美》— 全栈修仙《Java 8 CompletableFuture 对比 ES6 Promise 以及Spring @Async》
哈喽,大家好!在平常开发过程中会遇到许多意想不到的坑,本篇文章就记录在开发过程中遇到一些常见的问题,看了许多博主的异步编程,我只能说一言难尽。本文详细的讲解了异步编程之美,是不可多得的好文…...
Android 修改SVG属性并显示图片(AndroidSvg)
引入依赖: dependencies {implementation com.caverock:androidsvg-aar:1.4 }核心代码: import com.caverock.androidsvg.SVG import org.w3c.dom.Document import java.io.StringWriter import javax.xml.transform.OutputKeys import javax.xml.tran…...