刷题的时候遇到了利用前缀树来处理字符串包含前缀问题的解法,然后就补充一下前缀树的理解和实现,正好Leetcode208题也是前缀树的实现,作为练手。
原理
“字典树又称前缀树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。”
三个基本性质
- 根节点不包含字符,除根节点外每个节点只包含一个字符。
- 从根节点到某个节点,路径上所有的字符连接起来,就是这个节点所对应的字符串。
- 每个节点的子节点所包含的字符都不同。
字典树是一种树形结构,优点是利用字符串的公共前缀来节约存储空间。
字典树如图1所示:
图1
在字典树上搜索添加过的单词的步骤如下:
1、从根结点开始搜索
2、取得要查找单词的第一个字母,并根据该字母选择对应的字符路径向下继续搜索。
3、字符路径指向的第二层节点上,根据第二个字母选择对应的字符路径向下继续搜索。
4、一直向下搜索,如果单词搜索完后,找到的最后一个节点是一个终止节点,比如图1中的实心节点,说明字典树中含有这个单词,
如果找到的最后一个节点不是一个终止节点,说明单词不是字典树中添加过的单词。如果单词没搜索完,但是已经没有后续的节点了,
也说明单词不是字典树中添加过的单词。
举个例子:单词leet在Trie中的查找路径如下图所示,第一次查找首先在当前节点的26个子空间中找下标对应"l"的节点是否存在(ASCII码与"a"相减得到下标),不存在直接返回查找失败;若存在,则将Node设置为"l",依次迭代,在查找完leet四个字符后,Node停留在了"t",此时需要判断是否为终止节点,否则为另一个单词的前缀。
每个键在 trie 中表示为从根到内部节点或叶的路径。我们用第一个键字符从根开始。检查当前节点中与键字符对应的链接。有两种情况:
存在链接。我们移动到该链接后面路径中的下一个节点,并继续搜索下一个键字符。
不存在链接。若已无键字符,且当前结点标记为 isEnd,则返回 true。否则有两种可能,均返回 false :
还有键字符剩余,但无法跟随 Trie 树的键路径,找不到键。
没有键字符剩余,但当前结点没有标记为 isEnd。也就是说,待查找键只是Trie树中另一个键的前缀。
题目:字典树(前缀树)的实现:
字典树又称为前缀树或Trie树,是处理字符串常见的数据结构。假设组成所有单词的字符串是“a”~“z”,请实现字典树结构,并包含以下四个主要功能。
void insert(String word): 添加word,可以重复添加。
void delete(String word):删除word,如果word添加过多次,仅删除一个。
boolean search(String word):查询word是否在字典树中。
int prefixNumber(String pre):返回以字符串pre为前缀的单词数量。
leetcode 208 问题描述:
实现一个 Trie (前缀树),包含 insert
, search
, 和 startsWith
这三个操作。
示例:
Trie trie = new Trie(); trie.insert("apple"); trie.search("apple"); // 返回 true trie.search("app"); // 返回 false trie.startsWith("app"); // 返回 true trie.insert("app"); trie.search("app"); // 返回 true
说明:
- 你可以假设所有的输入都是由小写字母
a-z
构成的。 - 保证所有输入均为非空字符串。
实现
这是一个前缀树的节点,path用于记录通过该节点的路径有多少条,insert时增加1条,delete时减少1条;end用于记录当前节点是否为终止节点,不为0即为终止节点。TrieNode数组用于索引下一个节点的元素,因为题中指定了输入单词为小写字母a-z,故创建长度为26的数组。
1 class TrieNode { 2 int path; 3 int end; 4 TrieNode[] map; 5 6 TrieNode(){ 7 path = 0; 8 end = 0; 9 //针对字母a-z,所以初始化分配长度为26的空间 10 map = new TrieNode[26]; 11 } 12 }
下面是Trie树操作实现,基本流程都是先判断输入单词是否为空,然后将其转化为char数组,依次遍历char数组,根据与'a'的ASCII码差值,得到该字符的下标,然后将该节点赋值给当前节点,递归地去寻找下一个节点。
public class Trie { private TrieNode root; public Trie(){ root = new TrieNode(); } public void insert(String word){ if(word==null){ return; } char[] chs = word.toCharArray(); TrieNode node = root; node.path++; int index = 0; for (int i=0;i<chs.length;i++){ index = chs[i]-'a'; if(node.map[index]==null){ node.map[index] = new TrieNode(); } node = node.map[index]; node.path++; } //表示该字符串的最后一个节点为重点,用于search处的判断 node.end++; } //删除word,如果word添加过多次,仅删除一个。 public void delete(String word){ if(search(word)){ char[] chs = word.toCharArray(); TrieNode node = root; int index = 0; node.path--; for(int i=0;i<chs.length;i++){ index = chs[i]-'a'; node.map[index].path--; node = node.map[index]; } node.end--; } } public boolean search(String word){ if(word == null){ return false; } char[] chs = word.toCharArray(); TrieNode node = root; int index = 0; for(int i=0;i<chs.length;i++){ index = chs[i]-'a'; if(node.map[index]==null){ return false; }else{ node = node.map[index]; } } // if(node.end!=0){ // return true; // }else { // return false; // } return node.end!=0; } /**返回以字符串pre为前缀的单词数量*/ public int prefixNumber(String pre){ if(pre==null){ return 0; } char[] chs = pre.toCharArray(); TrieNode node = root; int index = 0; for(int i=0;i<chs.length;i++){ index = chs[i]-'a'; if(node.map[index]==null){ return 0; } node = node.map[index]; } return node.path; } /** 判断树中是否存在以此字符串为前缀的单词 */ public boolean startsWith(String prefix) { if(prefix==null){ return false; } char[] chs = prefix.toCharArray(); TrieNode node =root; int index = 0; for(int i=0;i<chs.length;i++){ index = chs[i]-'a'; if(node.map[index]==null){ return false; } node = node.map[index]; }
// return true; return node.path!=0; } }
参考:
[1] https://www.cnblogs.com/hengzhezou/p/11046886.html 字典树(Trie树)的实现
[2] https://leetcode-cn.com/problems/implement-trie-prefix-tree/solution/shi-xian-trie-qian-zhui-shu-by-leetcode/ leetcode 208 官方题解