字典树 - 核心思想及其代码实现

一、简介

Trie 树也称为字典树、单词查找树,最大的特点就是共享字符串的公共前缀来达到节省空间的目的。

例如,字符串 “abc” 和 “abd” 构成的 trie 树如下:

在这里插入图片描述

字典树的根节点不存任何数据,每整个分支代表一个完整的字符串。像 abc 和 abd 有公共前缀 ab,所以我们可以共享节点 ab。

如果再插入 abf,则变成这样:

在这里插入图片描述
如果我再插入 bc,则是这样(bc 和其他三个字符串没有公共前缀)

在这里插入图片描述
那如果再插入 “ab” 这个字符串呢?

每个分支的内部可能也含有完整的字符串,所以我们可以对于那些是某个字符串结尾的节点做一个标记,例如 abc,abd,abf 都包含了字符串 ab,所以我们可以在节点 b 这里做一个标记。如下(我用红色作为标记):

在这里插入图片描述

二、应用

2.1 提前列出搜索信息

字典树最大的特点就是利用了字符串的公共前缀,像我们有时候在百度、谷歌输入某个关键字的时候,它会给我们列举出很多相关的信息。

在这里插入图片描述

2.2 敏感词过滤

例:一段字符串 “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) 复杂度内判断某个子节点是否存在。

三、敏感词过滤代码实现

3.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;
    }

}
3.2 构建字典树
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();
    }
    
}
3.3 测试
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
    }

}

一个值得尝试的 AI 赚钱小项目

  • 4
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
字典树Java实现可以使用递归来构建。首先需要定义一个TrieNode类,其中包含一个Map用于存储子节点,以及一个布尔值表示当前节点是否是一个单词的结尾。然后,定义一个Trie类来维护根节点。在Trie类中实现insert、search和startsWith方法。 下面是一个简单的字典树Java代码实现示例: ```java class TrieNode { Map<Character, TrieNode> children; boolean isEndOfWord; public TrieNode() { children = new HashMap<>(); isEndOfWord = false; } } class Trie { private TrieNode root; public Trie() { root = new TrieNode(); } public void insert(String word) { TrieNode current = root; for (int i = 0; i < word.length(); i++) { char ch = word.charAt(i); TrieNode node = current.children.get(ch); if (node == null) { node = new TrieNode(); current.children.put(ch, node); } current = node; } current.isEndOfWord = true; } public boolean search(String word) { TrieNode current = root; for (int i = 0; i < word.length(); i++) { char ch = word.charAt(i); TrieNode node = current.children.get(ch); if (node == null) { return false; } current = node; } return current.isEndOfWord; } public boolean startsWith(String prefix) { TrieNode current = root; for (int i = 0; i < prefix.length(); i++) { char ch = prefix.charAt(i); TrieNode node = current.children.get(ch); if (node == null) { return false; } current = node; } return true; } } ``` 这是一个简单的Trie字典树Java代码实现,其中包括了插入、查询和查询前缀三个常用操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值