一、简介

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    }}

字典树实现_反怼面试官系列之 字典树相关推荐

  1. 重复订单号校验_吊打面试官系列重复消费、顺序消费、分布式事务

    你知道的越多,你不知道的越多 前言 消息队列在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在消息队列的使用和原理方面对小伙伴们进行360°的刁难. 作为一个在互联网公司面一次拿一次Of ...

  2. redis 亿级查询速度_吊打面试官系列:Redis 性能优化的 13 条军规大全

    我的官方群点击此处. 1.缩短键值对的存储长度 键值对的长度是和性能成反比的,比如我们来做一组写入数据的性能测试,执行结果如下: 从以上数据可以看出,在 key 不变的情况下,value 值越大操作效 ...

  3. mysql 左连接b表的一条数据_阿里java架构师教你怎么用mysql怒怼面试官

    转载地址: 阿里java架构教你怎么用mysql怒怼面试官​www.jianshu.com 说一下mysql比较宏观的面试,具体咋写sql的这里就不过多举例了.后面我还会给出一个关于mysql面试优化 ...

  4. 面试官问你B树和B 树,就把这篇文章丢给他

    原文链接:面试官问你B树和B 树,就把这篇文章丢给他 在看这篇文章之前,我们回顾一下前面的几篇关于MySQL的文章,应该对你读下面的文章有所帮助. InnoDB与MyISAM等存储引擎对比 面试官问你 ...

  5. redis反杀面试官之10问

    简言 1. 笔者近两年来一直使用redis,也对redis有过仔细的研究,不敢说精通,熟悉至少是有的 2. redis越来越火,网上相应的文章,总结,面试问题也有很多,但大多是应付简单面试用的,如果面 ...

  6. 数据分析菜鸟怒怼面试官却被打脸,只会SQL也敢来面试?

    作为一个冲浪老司机,混迹各大社区打嘴炮,是我为数不多的生活习惯之一.今天下班前的例行摸鱼时间,正当我激情满满地划着手机屏幕,一条动态却让我停了下来. 哈,巧了,我就是楼主怼的那种面试官,怼面试官就算了 ...

  7. “约见”面试官系列之常见面试题之第六十九篇之document.ready和onload的区别(建议收藏)

    document.ready和onload的区别为:加载程度bai不同du.执行次数不同.执行速度zhi不同. 一.加载程度不同 1.document.ready:在DOM加载完成dao后就可以可以对 ...

  8. “约见”面试官系列之常见面试题第四十一篇之VUE生命周期(建议收藏)

    详解Vue Lifecycle 先来看看VUE官网对VUE生命周期的介绍 Vue实例有一个完整的生命周期,也就是从开始创建.初始化数据.编译模板.挂载Dom.渲染→更新→渲染.销毁等一系列过程,我们称 ...

  9. “约见”面试官系列之常见面试题第二十一篇之函数防抖和节流(建议收藏)

    目录 前言 概念 函数防抖(debounce) 函数节流(throttle) 常见应用场景 函数防抖的应用场景 函数节流的应用场景 实现原理 函数防抖(debounce) 函数节流(throttle) ...

最新文章

  1. linux 删除含有关键词的文件_误删除Linux系统文件了?这个方法教你解决
  2. 使用idea 时出现classnotfound
  3. 打包的时候不把配置文件加进去_webpack区分developement和production打包
  4. 如何防止android app被误删除,如何避免手机清理缓存时误删了重要文件【注意事项】...
  5. Spring Cloud Alibaba基础教程:使用Sentinel实现接口限流
  6. CPU寻址过程方框图
  7. mysql的英文字母_MySQL中查询的有关英文字母大小写问题的分析
  8. linux系统上手工建库步骤,Linux下Oracle手工建库过程
  9. 接收二进制数据_详解前端websocket原理之数据传输协议
  10. linux命名管道进程间通信,《Linux 进程间通信》命名管道:FIFO
  11. android init.rc 添加指令三部曲
  12. HCIA-虚拟化与网络存储技术
  13. 最简单的方法来压缩图片(无需下载工具)
  14. keil格式化代码方法
  15. python新闻文本爬虫_python学习_新闻联播文字版爬虫(V 1.0版)
  16. c#运用——简体字转繁体字
  17. mac搜索不到wifi wtg_Mac连不上WiFi怎么办?Mac连不上无线网络解决办法
  18. 关于使用git clone时提示Permission denied (publickey)的解决方法
  19. openlayers 绘制动态迁徙线、曲线
  20. linux buffer io error,Linux Buffer I/O error on device dm-4, logical block,dm-4logical

热门文章

  1. CSS keylogger:攻击与防御
  2. bootstrap-table操作之“删除”
  3. Codeforces Round #441 Div. 2题解
  4. RedHat6.5 搭建glusterfs全过程
  5. LINUX设备驱动程序的注意事项(两)建设和执行模块
  6. 《Linux内核设计与实现》读书笔记(三)- Linux的进程
  7. VMware workstation 7.1 安装错误提示1021解决方法
  8. 进临界区(关全局中断)是否会影响数据的接收?
  9. 批处理(.bat)无限循环,定时,固定时间间隔
  10. STM32向量表详细分析