【数据结构基础】树 - 前缀树(Trie Tree)

news2024/11/16 21:35:28
Trie,又称字典树、单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

什么是前缀树

在计算机科学中,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

Trie 这个术语来自于 retrieval。根据词源学,trie 的发明者 Edward Fredkin 把它读作/ˈtriː/ “tree”。但是,其他作者把它读作/ˈtraɪ/ “try”。trie 中的键通常是字符串,但也可以是其它的结构。trie 的算法可以很容易地修改为处理其它结构的有序序列,比如一串数字或者形状的排列。比如,bitwise trie 中的键是一串位元,可以用于表示整数或者内存地址。trie 树常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。

上图是一棵Trie树,表示了关键字集合{“a”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”} 。从上图可以归纳出Trie树的基本性质:

  • 根节点不包含字符,除根节点外的每一个子节点都包含一个字符。

  • 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。

  • 每个节点的所有子节点包含的字符互不相同。

  • 从第一字符开始有连续重复的字符只占用一个节点,比如上面的to,和ten,中重复的单词t只占用了一个节点。

前缀树的实现

重点在于节点数据结构,重要的插入和查找方法,以及递归和非递归两种形式。

节点数据结构定义

Node节点中使用map较为高效,用于映射到下一个节点:

public class Trie {
 
  private class Node{

      public boolean isWord; // 是否是某个单词的结束
      
      public TreeMap<Character, Node> next; //到下一个节点的映射

      public Node(boolean isWord){
          this.isWord = isWord;
          //初始化字典树
          next = new TreeMap<>();
      }

      public Node(){
          this(false);
      }
  }
  
  //根节点
  private Node root;
  //Trie单词个数
  private int size;

  public Trie(){
      root = new Node();
      size = 0;
  }

  // 获得Trie中存储的单词数量
  public int getSize(){
      return size;
  }

}

插入方法

  • 非递归方式

向Trie中添加一个新的单词word: 将单词拆分成一个个字符c,然后从根节点开始往下添加

public void add(String word){

    Node cur = root;
    //循环判断新的cur节点是否包含下一个字符到下一个节点的映射
    for(int i = 0 ; i < word.length() ; i ++){
        //将c当成一个节点插入Trie中
        char c = word.charAt(i);
        //判断cur.next是不是已经指向我们要找的c字符相应的节点
        if(cur.next.get(c) == null){
            //新建节点
            cur.next.put(c, new Node());
        }
        //否则,就直接走到该节点位置即可
        cur = cur.next.get(c);
    }
    //判断该单词并不表示任何一个单词的结尾
    if(!cur.isWord){
        //确定cur是新的单词
        cur.isWord = true;
        size ++;
    }
}
  • 递归方式

/**
  * 向Trie中添加一个新的单词word(递归写法接口)
  *
  * @param word
  */
public void recursionAdd(String word) {
    Node cur = root;
    add(root, word, 0);
}

/**
  * 递归写法调用方法实现递归添加
  *
  * @param node 传入要进行添加的节点
  * @param word 传入要进行添加的单词
  */
public void add(Node node, String word, int index) {
    // 确定终止条件,这个终止条件在没加index这个参数时,很难确定
    // 此时一个单词已经遍历完成了,如果这个结束节点没有标记为单词,就标记为单词
    if (!node.isWord && index == word.length()) {
        node.isWord = true;
        size++;
    }

    if (word.length() > index) {
        char addLetter = word.charAt(index);
        // 判断trie的下个节点组中是否有查询的字符,如果没有,就添加
        if (node.next.get(addLetter) == null) {
            node.next.put(addLetter, new Node());
        }
        // 基于已经存在的字符进行下个字符的递归查询
        add(node.next.get(addLetter), word, index + 1);
    }
}

查询单词方法

  • 非递归方式

/**
  * 查询单词word是否在Trie中(非递归写法)
  *
  * @param word
  * @return
  */
public boolean contains(String word) {
    Node cur = root;
    for (int i = 0; i < word.length(); i++) {
        char c = word.charAt(i);
        if (cur.next.get(c) == null) {
            return false;
        } else {
            cur = cur.next.get(c);
        }
    }
    return cur.isWord;
}
  • 递归方式

/**
  * 查询单词word中是否在Trie中接口(递归写法)
  *
  * @param word
  * @return
  */
public boolean recursionContains(String word) {
    Node cur = root;
    return contains(root, word, 0);
}

/**
  * 查询word中是否在Trie中递归写法
  *
  * @param node
  * @param word
  * @param index
  * @return
  */
private boolean contains(Node node, String word, int index) {
    if (index == word.length()) {
        return node.isWord;
    }
    char c = word.charAt(index);
    if (node.next.get(c) == null) {
        return false;
    } else {
        return contains(node.next.get(c), word, index + 1);
    }
}

查询前缀方法

  • 非递归方式

/**
  * 查询是否在Trie中有单词一prefix为前缀
  *
  * @param prefix
  * @return
  */
public boolean isPrefix(String prefix) {
    Node cur = root;
    for (int i = 0; i < prefix.length(); i++) {
        char c = prefix.charAt(i);
        if (cur.next.get(c) == null) {
            return false;
        }
        cur = cur.next.get(c);
    }
    return true;
}
  • 递归方式

/**
  * 查询是否在Trie中有单词一prefix为前缀(递归调用)
  *
  * @param prefix
  * @return
  */
public boolean recursionIsPrefix(String prefix) {
    Node node = root;
    return recursionIsPrefix(root, prefix, 0);
}

/**
  * 查询是否在Trie中有单词一prefix为前缀(递归实现)
  *
  * @return
  */
public boolean recursionIsPrefix(Node root, String prefix, int index) {
    if (prefix.length() == index) {
        return true;
    }
    char c = prefix.charAt(index);
    if (root.next.get(c) == null) {
        return false;
    } else {
        return recursionIsPrefix(root.next.get(c), prefix, ++index);
    }
}

前缀树的拓展

再深入理解下前缀树。

前缀树的复杂度

设平均查询的query词长n, 白名单m条记录,平均长度k,

简单单词查询:一个query,需要遍历每一个白名单,调用query是否contains方法,contains方法遍历前词,找到头元素一致,再遍历判断尾序列,contains的复杂度是O(n),整体复杂度是O(mn)

前缀树查询: 一个query,将这个query从头到尾遍历,每个元素在前缀树中判断,操作都是取下一个节点和判断是否是end,时间复杂度是O(1),整体时间复杂度是O(n)

前缀树有哪些应用

这个比较简单,就简单列下:

  • 前缀匹配

  • 字符串检索, 比如 敏感词过滤,黑白名单等

  • 词频统计

  • 字符串排序

前缀树的压缩:基数树

在计算机科学中,基数树,或称压缩前缀树,是一种更节省空间的 Trie(前缀树)。对于基数树的每个节点,如果该节点是确定的子树的话,就和父节点合并。基数树可用来构建关联数组。 用于 IP 路由。 信息检索中用于文本文档的倒排索引。

基数树可看做是以二进制位串为关键字的 trie 树,是一种多叉树形结构,同时又类似多层索引表,每个中间节点包含指向多个子节点的指针数组,叶子节点包含指向实际的对象的指针(由于对象不具备树节点结构,因此将其父节点看做叶节点)。基数树也被设计成多道树,以提高磁盘交互性能。同时,基数树也是按照字典序来组织叶节点的,这种特点使之适合持久化改造,加上它的多道特点,灵活性较强,适合作为区块链的基础数据结构,构建持久性区块时较好地映射各类数据集合上。基数树支持插入、删除、查找操作。查找包括完全匹配、前缀匹配、前驱查找、后继查找。所有这些操作都是 O(k)复杂度,其中 k 是所有字符串中最大的长度。

双数组Trie树(DoubleArrayTrie)

双数组Trie树(DoubleArrayTrie)是一种空间复杂度低的Trie树,应用于字符区间大的语言(如中文、日文等)分词领域

双数组Trie (Double-Array Trie)结构由日本人JUN-ICHI AOE于1989年提出的,是Trie结构的压缩形式,仅用两个线性数组来表示Trie树,该结构有效结合了数字搜索树(Digital Search Tree)检索时间高效的特点和链式表示的Trie空间结构紧凑的特点。双数组Trie的本质是一个确定有限状态自动机(DFA),每个节点代表自动机的一个状态,根据变量不同,进行状态转移,当到达结束状态或无法转移时,完成一次查询操作。在双数组所有键中包含的字符之间的联系都是通过简单的数学加法运算表示,不仅提高了检索速度,而且省去了链式结构中使用的大量指针,节省了存储空间。——《基于双数组Trie树算法的字典改进和实现》

具体可以看这篇文章

参考文章

  • https://blog.csdn.net/v_july_v/article/details/6897097

  • https://www.cnblogs.com/bonelee/p/8830825.html

  • https://blog.csdn.net/forever_dreams/article/details/81009580

  • https://www.jianshu.com/p/b9b8bf82fcd5

  • https://bestqiang.blog.csdn.net/article/details/89103524

  • https://java-sword.blog.csdn.net/article/details/89373156

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/195719.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Zabbix灾难备份多种方式分享(建议收藏)

感谢本文译者田川 ! 欢迎更多资深用户翻译原厂博文&#xff08;https://blog.zabbix.com/&#xff09;&#xff01; 田川 | 宏时数据技术工程师 Zabbix 5.0中文手册官方译者 2017-2018年Zabbix中国峰会演讲嘉宾 8年监控领域实施和管理工作经验 ►在这篇博文中&#xff…

mysql关系型数据库免安装包下载以及安装教程

对于大部分技算计技术相关的初学者而言&#xff0c;mysql关系型数据库无可厚非是最适合初学者学习使用的&#xff0c;但是对于安装mysql数据库来说可能就不是特别明确&#xff0c;到底如何安装。互联网上比较多都是官网的.msi安装包&#xff0c;自带mysql客户端和一些其他关联程…

2023年“华数杯”国际大学生数学建模A题赛题发布

MCM问题一&#xff1a;雅鲁藏布江综合发展规划背景雅鲁藏布江是中国最长的高原河流&#xff0c;也是世界上最高的高原河流之一。它起源 于喜马拉雅山脉的北部山麓&#xff0c;从西向东流&#xff0c;然后转向南部&#xff0c;流向印度。中国的雅鲁 藏布江全长2057公里&#xff…

诺依集成mybatis换成mybatis-plus(完美解决)

总结分析&#xff1a;问题1&#xff1a;mybatis与mybatis-plus能否共存&#xff1f;经过将近一天的搜索发现&#xff0c;mybatis与mybatis-plus的sql工厂不通&#xff0c;mybatis是SqlSessionFactoryBean&#xff0c;而mybatis-plus是MybatisSqlSessionFactoryBean&#xff0c;…

python下载油管、B站视频的方法

这是2023年的第一篇博客。但绝不是最后一篇。 今天的博客记录篇娱乐向。 今夜想让wh听我听的歌。 利用python的you-get实现听歌自由。&#xff08;虽然有音乐会员&#xff09; FFmpeg的下载与安装。 FFmpeg的下载地址 选择对应型号的操作系统。 本次演示采用windows操作系统…

ChatGPT的调用API被提前发现了?

前言 近日&#xff0c;有消息称ChatGPT的API已经被提前发现。作为一名技术爱好者&#xff0c;我决定亲自试试看。经过几次尝试&#xff0c;我发现这确实是真的&#xff01;&#xff08;不过OpenAI确实动作很快&#xff0c;如今已经修补了大部分的模型&#xff09;。 1. 如何调…

使用 4EVERLAND 将您的 Damus 配置文件存储到 IPFS/Arweave

真正控制您的社交网络&#xff01; 挑战Twitter的去中心化社交应用Damus已登陆App Store。它是第一个使用名为Nostr的开放式去中心化社交网络协议的移动应用程序&#xff0c;该协议由 Twitter 联合创始人 Jack Dorsey 资助&#xff0c;该协议基于加密密钥对。 Damus 的出现是…

001. SQL慢查询排查(字段类型不一致)

目录一&#xff1a;背景二&#xff1a;排查过程2.1: SQL慢查询定位2.2: Python层面分析将String翻译成Int类型的原因2.3: Python进行SQL执行时间检测出现的问题三&#xff1a;总结一&#xff1a;背景 新的业务上线后&#xff0c; 合作部门发现我们引擎执行完&#xff0c; 回调…

从用户到专家-Zabbix培训【优惠】通道开启

Q&#xff1a;我从2.0开始使用Zabbix&#xff0c;还用得着参加培训&#xff1f; A&#xff1a;相信我&#xff0c;多得是你不知道的事&#xff01; Zabbix与时俱进&#xff0c;6.0新增了很多功能&#xff0c;如何快速系统掌握&#xff1f; 培训内容是Zabbix原厂设计的系统课…

自学软件测试从哪里开始?给还在迷茫的人一条出路

这两天和朋友谈到软件测试的发展&#xff0c;其实软件测试已经在不知不觉中发生了非常大的改变&#xff0c;前几年的软件测试行业还是一个风口&#xff0c;随着不断地转行人员以及毕业的大学生疯狂地涌入软件测试行业&#xff0c;目前软件测试行业“缺口”已经基本饱和。当然&a…

JVM的垃圾回收机制GC

GC回收区域GC主要针对堆区回收&#xff0c;回收是以对象为单位。方法区的类对象加载后不太需要回收&#xff1b;栈区的释放时机确定&#xff0c;不必回收&#xff1b;程序计数器是固定内存地址&#xff0c;不必回收。找出垃圾的方法引用计数法&#xff08;jvm未采取&#xff09…

SAP 编号范围及BUFFER缓冲

一 前言 编号范围对象(NUMBER RANGE)是SAP ERP 软件中的一个重要概念. 主要用来获取流水号. 在标准功能及自开发功能中大量使用.系统中的几乎所有对象的号码都是通过编号范围对象获取的. 二 编号范围对象的创建 事务代码SNRO 用于创建编号范围对象 三 编号范围对象的配置…

几个关键字(final、static、权限修饰符、super、this、instanceof)

Java知识点总结&#xff1a;想看的可以从这里进入 目录5、关键字5.1、final5.2、static5.3、 权限修饰符5.4、 super、this5.5、instanceof5、关键字 5.1、final 1、final&#xff1a;最终的。 修饰局部变量时赋值后不可改&#xff0c;修饰成员变量时必须赋初值且不可改&#x…

基于AST的babel库实现js反混淆还原基础案例荟萃

基本概念 AST简介 AST全称Abstract Syntax Tree&#xff0c;即抽象语法树&#xff0c;简称语法树&#xff08;Syntax tree&#xff09;&#xff0c;树上的每个节点都表示源代码中的一种结构。 JavaScript 领域常用的 AST 解析库有 babel、esprima、espree 和 acorn 等&#…

【2023】Prometheus-先搭出来玩玩

找两台还没回收的服务器&#xff08;虚拟机&#xff09;&#xff0c;使用centos7.*系统 一台作为监控端&#xff0c;一台作为被监控端。目录1.快速部署Prometheus服务2.快速部署被监控端加入监控端3.使用grafana作为UI展示4.导入node_exporter模板监控node节点1.快速部署Promet…

【nodejs】脚手架从零开始搭建JBD

&#x1f6ce;️脚手架编写 脚手架框架&#xff1a; bin www.js src contant.js create.js main.js package-lock.json package.json &#x1f6e0;️插件安装 devDependencies & dependencies 脚本名称脚本作用commander读取版本&#xff0c;设定选项&#xff…

混合人机协同制造系统设计与控制中的运营管理问题:一项调查

S. Ehsan Hashemi-Petroodi , Simon Thevenin , Sergey Kovalev , Alexandre Dolgui 小于翻译摘要&#xff1a;能够执行多种任务的制造系统需要不同类型的资源。使用机器人的全自动系统具有高速、准确、不知疲倦和力量&#xff0c;但它们很昂贵。另一方面&#xff0c;人类工作者…

【Redis】快速入门使用

文章目录Redis初识NosqlRedis安装依赖库上传安装包并解压启动Redis桌面客户端Redis常见命令Redis通用命令String类型String的常见命令Key结构Hash类型List类型Set类型SortedSet类型Redis的Java客户端Jedis客户端快速入门连接池SpringDataRedis客户端快速入门自定义序列化String…

无法通过SSH远程登录Linux实例时的排查指引-阿里云国际

本文介绍在使用阿里云国际版云服务器实例时&#xff0c;通过SSH远程登录Linux系统的ECS实例时&#xff0c;连接失败&#xff0c;无法正常登录Linux实例的排查指引&#xff1a; SSH登录失败时没有明确的报错信息 处理流程 如果没有收到系统返回的报错信息&#xff0c;请根据以下…

多级缓存架构 | 黑马Redis高级篇

目录 一、多级缓存介绍 1、传统缓存的问题 2、多级缓存方案 二、JVM进程缓存 1、初始Caffeine 缓存分类 Caffeine入门 2、实现进程缓存 三、Lua脚本 1、初始Lua 2、Lua语法 数据类型 变量 循环 ​编辑 函数 条件控制 四、OpenResty 1、初始OpenResty 2、Open…