概述
举个例子,给出一些单词,(and
,as
,at
,cn
,com
),则其字典树如下:
网络异常,图片无法展示
|
前缀树有三个基本性质:
- 1)根结点不包含字符,除根结点外每一个结点都只包含一个字符。
- 2)从根结点到某一结点,路径上经过的字符连接起来,为该结点对应的字符串。
- 3)每个结点的所有子结点包含的字符都不相同。
适用场景
- 1)词频统计:例如,给定一个由 10 万个单词组成的库,现要你判断一个单词是否有在库中出现,若出现,求总共出现多少次或者求第一次出现在哪个位置。(如果我们使用一般的方法,每次查询一个单词都去遍历一遍,那么时间复杂度将为O(n^2),这对于大数据是不能够接受的。假如我们要查找单词student。那我们通过前缀树只需要查找s开头的即可,然后接下来查询t开头的即可,对于大量的数据可以省去不小的时间。)
- 2)前缀匹配:以上图为例,如果想获取所有以
"a"
开头的字符串,那么从图中可以很明显的看到是:(and
,as
,at
)。因此利用这个特性,可以巧妙地实现搜索提示功能,如输入一个网址,可以自动列出可能的选择。当没有完全匹配的搜索结果,可以列出前缀最相似的可能。 - 3)游戏中的敏感词过滤
算法思路
1.设计Trie节点思路
在设计一棵树时,先要设计其节点结构。Trie需要判断当前节点是以前缀还是单词结尾,所以需要一个布尔字段来标记。由于常规情况下只考虑a~z的字母,因此只有26个字母,即最多有26个指向子节点的链接,因此可以用大小为26的数组来表示26个字母,也可以用哈希表键值对来表示。
2.insert方法
insert方法需要从根节点往下搜索。如果已经存在对应节点,沿着指针移动到子节点,继续处理下一个字符;如果不存在对应节点,那么创建一个新的节点,记录在children数组的对应位置,然后沿着指针移动到子节点,继续搜索下一个字符。重复,直到处理完字符串的最后一个字符,然后将当前节点标记为字符串结尾
3.search与startsWith方法
search同样适用DFS深度优先遍历的思想。如果能够到达搜索单词的结尾,并且此时指向的节点的布尔标记是True,则返回正确;如果搜索中途出现节点值不一致或到达单词结尾所指向的节点的布尔标记是False,则返回错误 startsWith和search的区别在于前者只要遍历途中不存在不一致的节点,无需判断布尔标记,就返回正确。
代码实现
public class TrieTree{ public static class TrieNode{ public int pass; public int end; public TrieNode[] nexts; public TrieNode(){ pass = 0; end = 0; nexts = new TrieNode[26]; } } public static 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.pass++; int index = 0; for(int i = 0; i<chs.length; i++){ // 从左往右遍历字符 index = chs[i] - 'a'; // 由字符,对应成走向哪条路 if(node.nexts[index]==null){ node.nexts[index] = new TrieNode(); } node = node.nexts[index]; node.pass++; } node.end++; } public void delete(String word){ if(search(word)!=0){ // 确定树中确实加入过word,才删除 char[] chs = word.toCharArray(); trieNode node = root; node.pass--; int index = 0; for(int i = 0;i<chs.length; i++){ index = chs[i] - 'a'; if(--node.nexts[index].pass == 0){ node.nexts[index] = null; return; } node = node.nexts[index]; } node.end--; } } } } 复制代码