赞
踩
Trie 树也称为字典树、单词查找树,最大的特点就是共享字符串的公共前缀来达到节省空间的目的。
例如,字符串 “abc” 和 “abd” 构成的 trie 树如下:
字典树的根节点不存任何数据,每整个分支代表一个完整的字符串。像 abc 和 abd 有公共前缀 ab,所以我们可以共享节点 ab。
如果再插入 abf,则变成这样:
如果我再插入 bc,则是这样(bc 和其他三个字符串没有公共前缀)
那如果再插入 “ab” 这个字符串呢?
每个分支的内部可能也含有完整的字符串,所以我们可以对于那些是某个字符串结尾的节点做一个标记,例如 abc,abd,abf 都包含了字符串 ab,所以我们可以在节点 b 这里做一个标记。如下(我用红色作为标记):
字典树最大的特点就是利用了字符串的公共前缀,像我们有时候在百度、谷歌输入某个关键字的时候,它会给我们列举出很多相关的信息。
例:一段字符串 “abcdefghi" ,以及三个敏感词 “de” ,“bca” ,“bcf” 。
先把三个敏感词建立一颗字典树,如下:
接着可以采用三个指针来遍历。
首先指针 p1 指向 root ,指针 p2 和 p3 指向字符串第一个字符。
然后从字符串的 a 开始,检测有没有以 a 作为前缀的敏感词,直接判断 p1 的孩子节点中是否有 a 这个节点就可以了,显然这里没有。
接着把指针 p2 和 p3 向右移动一格。
然后从字符串 b 开始查找,看看是否有以 b 作为前缀的字符串,p1 的孩子节点中有 b,这时,我们把 p1 指向节点 b ,p2 向右移动一格,不过,p3 不动。
判断 p1 的孩子节点中是否存在 p2 指向的字符 c ,显然有。我们把 p1 指向节点 c ,p2 向右移动一格,p3 不动。
判断 p1 的孩子节点中是否存在 p2 指向的字符 d ,这里没有。这意味着,不存在以字符 b 作为前缀的敏感词。这时我们把 p2 和 p3 都移向字符 c ,p1 还是还原到最开始指向 root 。
和前面的步骤一样,判断有没以 c 作为前缀的字符串,显然这里没有,所以把 p2 和 p3 移到字符 d 。
然后从字符串 d 开始查找,看看是否有以 d 作为前缀的字符串,p1 的孩子节点中有 d ,这时,我们把 p1 指向节点 b ,p2 向右移动一格,不过,p3 和刚才一样不动。
判断 p1 的孩子节点中是否存在 p2 指向的字符 e ,显然有。我们把 p1 指向节点 e ,并且,这里 e 是最后一个节点了,查找结束,所以存在敏感词 de ,即 p3 和 p2 这个区间指向的就是敏感词了,把 p2 和 p3 指向的区间那些字符替换成 * 。并且把 p2 和 p3 移向字符 f 。如下:
接着还是重复同样的步骤,直到 p3 指向最后一个字符。
复杂度分析
如果敏感词的长度为 m,则每个敏感词的查找时间复杂度是 O(m),字符串的长度为 n,我们需要遍历 n 遍,所以敏感词查找这个过程的时间复杂度是 O(n * m)。如果有 t 个敏感词的话,构建字典树的时间复杂度是 O(t * m)。
这里我说明一下,在实际的应用中,构建字典树的时间复杂度我觉得可以忽略,因为字典树我们可以在一开始就构建了,以后可以无数次重复利用。
字典树可以采用 HashMap 来实现,因为一个节点的字节点个数未知,采用 HashMap 可以动态拓展,而且可以在 O(1) 复杂度内判断某个子节点是否存在。
public class TrieNode { // 是否敏感词的结尾 private boolean isEnd = false; // 下一层节点 Map<Character, TrieNode> subNodes = new HashMap<>(); /** * 添加下一个敏感词节点 */ public void addSubNode(Character c, TrieNode node) { subNodes.put(c, node); } /** * 获取下一个敏感词节点 */ public TrieNode getSubNode(Character c) { return subNodes.get(c); } /** * 设置敏感词标识 */ public void setEnd(boolean end) { this.isEnd = end; } /** * 判断是否为敏感词的结尾 */ public boolean getIsEnd() { return this.isEnd; } }
public class TireTree { // 敏感词替换字符 private static final String DEFAULT_REPLACEMENT = "*"; // 根节点 private final TrieNode rootNode; public TireTree() { rootNode = new TrieNode(); } /** * 识别特殊符号 */ private boolean isSymbol(Character c) { int ic = (int) c; // 0x2E80-0x9FFF 东亚文字范围 return !CharUtils.isAsciiAlphanumeric(c) && (ic < 0x2E80 || ic > 0x9FFF); } /** * 将敏感词添加到字典树中 */ public void addWord(String text) { if (text == null || text.trim().equals("")) return; TrieNode curNode = rootNode; int length = text.length(); // 遍历每个字 for (int i = 0; i < length; ++i) { Character c = text.charAt(i); // 过滤特殊字符 if (isSymbol(c)) continue; TrieNode nextNode = curNode.getSubNode(c); // 第一次添加的节点 if (nextNode == null) { nextNode = new TrieNode(); curNode.addSubNode(c, nextNode); } curNode = nextNode; // 设置敏感词标识 if (i == length - 1) curNode.setEnd(true); } } /** * 过滤敏感词 */ public String filter(String text) { if (text == null || text.trim().equals("")) return text; StringBuilder result = new StringBuilder(); TrieNode curNode = rootNode; int begin = 0; // 回滚位置 int position = 0; // 当前位置 while (position < text.length()) { Character c = text.charAt(position); // 过滤空格等 if (isSymbol(c)) { if (curNode == rootNode) { result.append(c); ++begin; } ++position; continue; } curNode = curNode.getSubNode(c); // 当前位置的匹配结束 if (curNode == null) { // 以begin开始的字符串不存在敏感词 result.append(text.charAt(begin)); // 跳到下一个字符开始测试 position = begin + 1; begin = position; // 回到树初始节点,重新匹配 curNode = rootNode; } else if (curNode.getIsEnd()) { // 发现敏感词,从begin到position的位置用replacement替换掉 result.append(DEFAULT_REPLACEMENT); position = position + 1; begin = position; // 回到树初始节点,重新匹配 curNode = rootNode; } else { ++position; } } result.append(text.substring(begin)); return result.toString(); } }
class Test {
public static void main(String[] args) {
TireTree tree = new TireTree();
// 添加敏感词
tree.addWord("de");
tree.addWord("bca");
tree.addWord("bcf");
// 过滤敏感词
String res = tree.filter("abcdefghi");
System.out.println(res); // abc*fghi
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。