字典树
什么是字典树
Trie树(前缀树),即字典树,又称单词查找树或键树,是一种树形结构。
用于高效地存储和查找字符串集合。字典树的每个节点表示一个字符串的前缀,从根节点到叶子节点的路径表示一个完整的字符串,因此字典树可以用于前缀匹配和字符串查找等方面。
每一层的字母是被共享的,这样每一层相同的字符就只会占用一份空间,像上图所示: bat, cat, dad共用第二层共用a, bat和cat共用同一个t。
字典树的特点
- 字符串查询时间复杂度O(L), L为字符串长度
- 空间复杂度较高,需要较多空间复杂维护结构
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
适用环境
- 前缀关键字查询。 用于存储和查询关键词,如搜索引擎、拼音输入法等。
- 自动补全。 从已有数据中自动补全字符串, 如浏览器地址栏自动补全。
- 字符串筛选。 快速判断一个字符串是否在某集合中。
基本操作
- 插入(insert)
- 将一个新的字符串插入到前缀树中
- 过程:根据字符串的每个字符,沿对应路径向下插入节点
- 若路径不存在,则创建新的节点
时间复杂度:O(L),L 表示字符串长度
- 查找(Search)
- 查找一个字符串是否存在于前缀树中
- 过程:根据字符串的每个字符,判断对应的路径是否存在
- 只要有一个字符在路径中不存在,则该字符串不在前缀树中
时间复杂度: O(L), L 表示字符串长度
- 前缀查找(Prefix Search)
- 查找前缀树中所有以某个前缀开始的字符串
- 过程:根据前缀的每个字符,进一步向下搜索对应的路径
- 时间复杂度和普通查找相同,O(P), P 表示前缀长度
手写字典树
完成后的结果如下图所示:
从上到下依次是:
- 根节点
- 静态节点类
- 构造函数
- 插入的方法
- 通过前缀搜索字符串
- 拼接字符串的方法
静态节点类
ini
static class Node{
private char aChar;
boolean isEnd;
Node[] next = new Node[26];
Node(char aChar){
this.aChar = aChar;
}
}
该类是每个节点的具体内容,里面维护:
- char字符aChar
- 标识符isEnd表示是否有单词在此完结
- 维护一个数组,里面只保存26个节点,因为我们存的字符是'a'-'z'
- 构造函数,传一个字符值
字典树构造函数
ini
TrieTree(){
root = new Node(' ');
}
初始化根节点就行了
插入方法
ini
public void insert(String s){
Node temp = root;
if(s.length()==0){
return;
}
char[] c=s.toCharArray();
for(int i=0;i<s.length();i++){
//如果该分支不存在,创建一个新节点
if(temp.next[c[i]-'a']==null){
temp.next[c[i]-'a']=new Node(c[i]);
}
temp=temp.next[c[i]-'a'];
}
temp.isEnd = true;
}
- 遍历传入的字符串,一个一个字符进行读取。
- 进行判断,判断当前节点的next[c[i] - 'a'] ( 该索引不会超过25)是否存在。
- 如果不存在,就创建新节点。
- 将当前节点指向子节点。
- 循环完毕,这时指针指向传入字符串的最后一个字符,将该字符标记为true,表示有以该字符结尾的单词。
根据前缀获得字符串
ini
public List<String> searchPrefix(String prefix) {
Node temp = root;
char[] chars = prefix.toCharArray();
for (char c : chars) {
int idx = c - 'a';
// 匹配为空
if (idx > temp.next.length || idx < 0 || temp.next[idx] == null) {
return Collections.emptyList();
}
temp = temp.next[idx];
}
// 上面循环结束,指向最后一个前缀字符
ArrayList<String> res = new ArrayList<>();
// 模糊匹配:根据前缀的最后一个字符,递归遍历所有的单词
collect(temp, prefix, res, 15);
return res;
}
protected void collect(Node trieNode, String pre, List<String> queue, int resultLimit) {
// 找到单词
if (trieNode.isEnd) {
trieNode.word = pre;
// 保存检索到的单词到 queue
queue.add(pre);
if (queue.size() >= resultLimit) {
return;
}
}
// 递归调用,查找单词
for (int i = 0; i < 26; i++) {
char c = (char) ('a' + i);
if (trieNode.next[i] != null) {
collect(trieNode.next[i], pre + c, queue, resultLimit);
}
}
}
- 首先肯定是要对前缀进行遍历,寻找是否存在以该前缀开头的单词,没有就返回空串。
- 根据前缀开头的每一个字符值来判断当前节点是否存在,不存在就返回空串。
- 循环完毕后,表明存在该前缀开头的单词, 可以进行下一步操作。
- 然后进入拼接函collect函数
- 下面是具体的拼接函数:
- 参数: 当前正在被拼接的节点,当前字符前面的字符(包括自己),最后要返回的列表,返回列表的限定长度。
- 然后判断isEnd,如果为true,则将当前字符串加入到返回列表中
- 然后进行当前节点的子节点遍历,如果不为空,则拼接字符串,此时传入的pre参数为现在它前面的字符串加上自己的字符(pre + c),因为传入的字符串要包括自己。这样进行递归调用,直到所有的子节点都为空。
测试代码
typescript
public static void main(String[] args) {
TrieTree trieTree = new TrieTree();
trieTree.insert("wdnmd");
trieTree.insert("wdhhh");
trieTree.insert("hhh");
List<String> list = trieTree.searchPrefix("wd");
for (String s : list) {
System.out.println(s);
}
}
先插入几个字符串,然后去根据前缀查询打印结果。
输出及分析
wdhhh
wdnmd
可以看到我们成功完成了对前缀"wd"的查询并输出所有字符串。
总结
- 字典树又叫前缀树,它是一种树形的结构
- 它的查询和插入都比较快,时间复杂度为O(L), L为字符串长度.(这里的查询指查找一个确定的)
- 它的内存可能会稍微有点大,因为它需要为可能出现的子节点预留空间,当然你可以使用动态扩容数组ArrayList,我这里使用的是固定长数组,开一个26单位的,但是只能存'a'-'z'。
- 如果要查询以某个前缀开头的所有字符,需要广度遍历,每一层都要找到所有的子节点;时间复杂度为 O(26^L),每一个子节点遍26次,最深为L层,我觉得还是挺慢的。(主要是我使用的是深度优先,可能空间复杂度比较高?问题是我这里只会使用DFS,因为我需要不断保存前缀的字符串)
- 适用于前缀关键字查询,如果仅仅是查询是否存在还是很快的。
- 手写时注意查询所有的字符串时需要使用到递归,其实是一种DFS算法(递归是最先执行最后放入的方法,会维护一个方法栈,所以空间复杂度高)。