首页 > 编程语言 >科普文:算法和数据结构系列【高效的字符串检索结构:字典树Trie树原理、应用及其java示例代码解读】

科普文:算法和数据结构系列【高效的字符串检索结构:字典树Trie树原理、应用及其java示例代码解读】

时间:2025-01-18 13:57:48浏览次数:3  
标签:current ch java 示例 Trie 单词 哈希 节点

概叙

科普文:算法和数据结构系列【算法和数据结构概叙】-CSDN博客

科普文:算法和数据结构系列【非线性数据结构:树Tree和堆Heap的原理、应用、以及java实现】-CSDN博客

科普文:算法和数据结构系列【树:4叉树、N叉树】_动态维护四叉树-CSDN博客

科普文:算法和数据结构系列【二叉树总结-上篇:满二叉树、完全二叉树、大顶堆/小顶堆、二叉搜索树、自平衡二叉树、红黑树总结】-CSDN博客

科普文:算法和数据结构系列【二叉树总结-下篇:满二叉树、完全二叉树、大顶堆/小顶堆、二叉搜索树、自平衡二叉树、红黑树总结】-CSDN博客

科普文:算法和数据结构系列【数据库喜欢的数据结构:B-树、B+树原理、应用、以及java实现示例】-CSDN博客

科普文:算法和数据结构系列【B+树java示例代码解读】-CSDN博客

科普文:算法和数据结构系列【压缩和通信利器:哈夫曼树(Huffman Tree)java示例代码解读】-CSDN博客

今天我们再来看一种树型结构:字典树又名前缀树、单词查找树、Trie树,是哈希树的变种,是一种存储大量字符串的树形数据结构,相比于HashMap存储,在存储单词(和语种无关,任意语言都可以)的场景上,节省了大量的内存空间。 

下面是一颗结构非常简单的Trie树,每个节点都有26(字符的个数,也可以更多)个孩子,每一条路径都代表了一个字符串,而这个字符串可以是英文单词、或是其他专用的字符串。

典型应用是用于统计,排序和保存大量的字符串串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。

它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树树高。他们对比如下:

一、原理

  • 字典树(Trie树)‌:是一种树形结构,每个节点代表一个字符,从根节点到叶子节点的路径表示一个字符串。利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较‌。

  • 哈希树‌:基于哈希函数实现,通过哈希函数将关键字映射到存储位置,实现快速存取。理想情况下具有O(1)的查找时间复杂度,但存在哈希冲突的问题‌。

二、优缺点

  • 字典树(Trie树)‌:‌

    • 优点‌:查询效率高,特别适用于大量字符串的查找和前缀匹配场景;利用公共前缀减少查询时间‌。‌

    • 缺点‌:空间复杂度较高,因为每个节点可能包含多个子节点(通常等于字符集大小),是一个以空间换时间的算法‌。

  • 哈希树‌:‌

    • 优点‌:在理想情况下具有极高的查找效率,时间复杂度接近O(1)‌。‌

    • 缺点‌:存在哈希冲突,需要解决冲突以保证数据的正确存取;不支持动态查询,即必须等待用户输入完整的字符串后才能进行哈希查询‌。

三、适用场景

  • 字典树(Trie树)‌:适用于需要前缀匹配、自动完成功能或大量字符串查找的场景,如搜索引擎、文本编辑器中的自动补全等‌。

  • 哈希树‌:适用于需要快速精确匹配的场景,且对存储空间要求较为敏感的情况(但注意这里对比的是哈希树,而哈希表可能更为常用且符合这一描述)。然而,由于哈希树的具体应用场景不如哈希表广泛,且存在哈希冲突等问题,因此在实际应用中可能较少直接使用哈希树‌。

四、空间复杂度与时间复杂度

  • 字典树(Trie树)‌:

    • 空间复杂度‌:有公共前缀的字符串只需要存储一次,但每个节点可能包含多个子节点,因此空间复杂度较高‌。
    • 时间复杂度‌:插入和查询字符串的复杂度都是O(m),其中m是待插入或查询字符串的长度‌。
  • 哈希树‌:

    • 空间复杂度‌:取决于哈希函数和哈希表的大小,以及解决哈希冲突的方法。
    • 时间复杂度‌:理想情况下为O(1),但存在哈希冲突时可能增加查找时间。

五、注意事项

  • 字典树(Trie树)‌:实现时需要注意字符集的大小,以及如何处理不同字符的存储和比较。在进行前缀匹配时,可以利用公共前缀信息减少计算量。

  • 哈希树‌:需要选择合适的哈希函数以减少哈希冲突。需要设计合理的解决哈希冲突的方法,如链地址法或开放地址法等。

字典树(Trie)

字典树(Trie树)‌:是一种树形结构,每个节点代表一个字符,从根节点到叶子节点的路径表示一个字符串。利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较‌。的

‌原理‌:

Trie树利用字符串之间的公共前缀来减少不必要的字符串比较,从而提高查询效率。每个节点代表一个字符,从根节点到某一节点的路径上经过的字符连接起来,就是该节点对应的字符串。

‌优缺点‌:

  • 优点:查询效率高,最大限度地减少无谓的字符串比较;支持前缀匹配,适合用于搜索引擎的词频统计等场景。
  • 缺点:空间复杂度可能较高,特别是当存储大量字符串时。
    • 空间消耗:对于不共享前缀的字符串集合,Trie 可能消耗大量内存。
    • 不适合动态数据:如果需要频繁地插入和删除字符串,维护 Trie 的代价可能较高。

复杂度‌:

  • ‌时间复杂度‌:插入、查找和前缀匹配操作的时间复杂度通常为O(L),其中L是字符串的长度。
  • ‌空间复杂度‌:取决于存储的字符串数量和长度。

‌应用场景‌:

‌应用场景‌:搜索引擎的词频统计、自动补全、拼写检查等。

1)搜素引擎
2)区块链:trie树的进阶版,Merkle Patricia Tree,他能够高效、安全地验证大型数据结构中的数据,我从别的地方摘抄了下摘要:

一种经过改良的、融合了默克尔树和前缀树两种树结构优点的数据结构,以太坊中,MPT是一个非常重要的数据结构,在以太坊中,帐户的交易信息、状态以及相应的状态变更,还有相关的交易信息等都使用MPT来进行管理,其是整个数据存储的重要一环。交易树,收据树,状态树都是采用的MPT结构。
3)IP路由,倒排索引,这个感兴趣的可以去了解下,我也不太了解,这是听说过可以
4)分词,常见的分词库,或多或少会用到字典树,或者其它类似的存储字符串的树形数据结构(比如"双数组trie树")。原因就是因为它能提供良好的前缀查询(一些分词算法需要大量调用该方法)。python有一个很著名的分词库叫做jieba,里面就用到了字典树(虽说由于jieba源码里字典树实现得不够优雅,后来被替代了)。这个库有java版本叫做jieba-analysis,但是已经很久不更新了,而且分词结果和python版本的不一致!


‌注意事项‌:实现时需要考虑内存使用,避免浪费空间。
优化方向 节省空间:  压缩字典树,但是维护成本更高;  三分搜索树
压缩前缀树:合并只有一个子节点的节点,可以减少内存使用。
Ternary Search Tree:每个节点有三个子节点,可以在某些情况下提供更好的平衡。
使用哈希表:在每个节点使用哈希表而不是数组来存储子节点,可以处理更大的字符集。

Java代码实现

/**
 * Trie4LowerChar结构特点
 * 根节点不包含字符,除根节点外每个节点都只包含一个字符
 * 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
 * 每个节点的所有子节点包含的字符都不相同
 *
 * @author zhouxx
 * @create 2025-01-17 13:23
 */
public class Trie4LowerChar {
    private TrieNode4LowerChar root;

    public Trie4LowerChar() {
        root = new TrieNode4LowerChar();
    }

    // 插入单词
    public void insert(String word) {
        word = word.toLowerCase();
        if(search(word)){
            return;
        }
        TrieNode4LowerChar current = root;
        for (char ch : word.toCharArray()) {
            if (!current.containsKey(ch)) {
                current.setChild(ch);
            }
            current = current.getChild(ch);
        }
        // todo 同拼写,不同读音的单词这里有问题。 eg:Live:直播/实况,居住。
        current.setEndOfWord(true);
        System.out.println(word+" insert TrieNode : "+current);
        System.out.println(word+" insert TrieNode/root : "+root);
    }

    // 搜索单词
    public boolean search(String word) {
        word = word.toLowerCase();
        TrieNode4LowerChar node = searchNode(word);
        return node != null && node.isEndOfWord();
    }

    // 检查是否有以给定前缀开头的单词
    public boolean startsWith(String prefix) {
        prefix = prefix.toLowerCase();
        return searchNode(prefix) != null;
    }

    private TrieNode4LowerChar searchNode(String word) {
        word = word.toLowerCase();
        TrieNode4LowerChar current = root;
        for (char ch : word.toCharArray()) {
            if (!current.containsKey(ch)) {
                return null;
            }
            current = current.getChild(ch);
        }
        return current;
    }
    //删除
    public void delete(String word) {
        word = word.toLowerCase();
        if(!search(word)){
            return;
        }
        TrieNode4LowerChar current = root;
        for (char ch : word.toCharArray()) {
            current = current.getChild(ch);
        }
        // todo 同拼写,不同读音的单词这里有问题。 eg:Live:直播/实况,居住。
        current.setEndOfWord(false);
        //System.out.println(word+" delete TrieNode : "+current);
        System.out.println(word+" delete TrieNode/root : "+root);
    }


}
class TrieNode4LowerChar {
    private TrieNode4LowerChar[] children;
    private boolean EndOfWord;

    TrieNode4LowerChar() {
        children = new TrieNode4LowerChar[26]; // 假设只包含小写字母
        EndOfWord = false;
    }

    public boolean containsKey(char ch) {
        return children[ch - 'a'] != null;
    }

    public TrieNode4LowerChar getChild(char ch) {
        return children[ch - 'a'];
    }

    public void setChild(char ch) {
        setChild(ch,new TrieNode4LowerChar());
    }

    public void setChild(char ch, TrieNode4LowerChar node) {
        children[ch - 'a'] = node;
    }

    public boolean isEndOfWord() {
        return EndOfWord;
    }

    public void setEndOfWord(boolean isEndOfWord) {
        this.EndOfWord = isEndOfWord;
    }

    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder("{");
        stringBuilder.append("isEndOfWord=").append(EndOfWord).append(",");
        stringBuilder.append("children=").append(children == null || children.length == 0 ? "[]" : printChildren());
        stringBuilder.append("}");
        return stringBuilder.toString();
        //return value.toString();
    }
    public String printChildren(){
        StringBuilder stringBuilder=new StringBuilder("{");
        for (int i = 0; i < children.length; i++) {
            if (children[i] == null) {
                //stringBuilder.append(i).append(",");
            } else {
                stringBuilder.append("[").append(i).append("]").append(children[i]).append(",");
            }
        }
        return stringBuilder.append("}").toString().replaceAll(",}","}");
    }
}

字典树的节点定义类:TrieNode4LowerChar 类

 

专门用于存储小写字母的 Trie 树节点。

类成员变量

  • private TrieNode4LowerChar[] children;:一个 TrieNode4LowerChar 类型的数组,用于存储当前节点的子节点。数组的大小是 26,对应 26 个小写英文字母。
  • private boolean EndOfWord;:一个布尔值,用于标记当前节点是否是一个单词的结束。

构造方法

  • TrieNode4LowerChar():构造方法初始化 children 数组为大小为 26 的 TrieNode4LowerChar 类型数组,并将所有元素初始化为 null。同时,将 EndOfWord 设置为 false

方法

  • containsKey(char ch):检查当前节点的子节点中是否存在对应字符 ch 的节点。通过计算 ch - 'a' 得到字符在 children 数组中的索引,然后判断该索引位置的元素是否为 null
  • getChild(char ch):返回对应字符 ch 的子节点。如果子节点不存在,则返回 null
  • setChild(char ch) 和 setChild(char ch, TrieNode4LowerChar node):这两个方法用于设置对应字符 ch 的子节点。第一个方法创建一个新的 TrieNode4LowerChar 节点并设置,而第二个方法则使用提供的节点进行设置。
  • isEndOfWord() 和 setEndOfWord(boolean isEndOfWord):这两个方法用于检查和设置 EndOfWord 标志。
  • toString():重写 toString 方法,用于返回当前节点的字符串表示形式。包括 isEndOfWord 标志和子节点信息。
  • printChildren():一个辅助方法,用于生成 children 数组的字符串表示形式。它遍历 children 数组,将非 null 的子节点转换为字符串,并将它们连接起来。

注意事项

  • 这个类假设只处理小写字母,因此 children 数组的大小被硬编码为 26。
  • containsKeygetChild 和 setChild 方法都依赖于字符 'a' 的 ASCII 值来计算数组索引,因此只能处理小写字母。
  • toString 和 printChildren 方法提供了节点的可视化表示,有助于调试和测试。

字典树的主类:Trie4LowerChar 类

* 根节点不包含字符,除根节点外每个节点都只包含一个字符
* 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
* 每个节点的所有子节点包含的字符都不相同

类成员变量

  • private TrieNode4LowerChar root;:一个 TrieNode4LowerChar 类型的变量,代表 Trie 树的根节点。

构造方法

  • Trie4LowerChar():构造方法初始化 Trie 树的根节点。它创建了一个新的 TrieNode4LowerChar 对象,并将其赋值给 root 变量。

插入方法:insert

用于向 Trie 树中插入一个单词。

TrieNode4LowerChar current = root;
for (char ch : word.toCharArray()) {
    if (!current.containsKey(ch)) {
        current.setChild(ch);
    }
    current = current.getChild(ch);
}

初始化一个 TrieNode4LowerChar 类型的变量 current,并将其设置为 Trie 树的根节点 root。然后,遍历单词的每个字符 ch

  • 如果当前节点 current 不包含字符 ch 的子节点,则调用 setChild(ch) 方法创建一个新的子节点。
  • 无论是否创建了新子节点,都将 current 更新为字符 ch 对应的子节点。

遍历完单词的所有字符后,将当前节点(即单词的最后一个字符对应的节点)标记为单词的结束节点。

搜索方法/前缀方法:search/startsWith

搜索单词方法 search

这个方法用于检查 Trie 树中是否存在给定的单词。它首先将单词转换为小写,然后调用 searchNode 方法(该方法在代码段中未给出,但我们可以假设它存在并用于查找单词对应的节点)来查找单词的最后一个字符对应的节点。如果找到了该节点,并且该节点被标记为单词的结束,则返回 true,否则返回 false

检查前缀方法 startsWith

这个方法用于检查 Trie 树中是否存在以给定前缀开头的单词。它首先将前缀转换为小写,然后调用 searchNode 方法来查找前缀的最后一个字符对应的节点。如果找到了该节点,则返回 true,否则返回 false

注意事项和潜在问题

  1. searchNode 方法‌:代码中多次调用了 searchNode 方法,但这个方法的具体实现没有给出。这个方法是实现搜索和前缀检查的关键,需要确保它能够正确地遍历 Trie 树并找到对应的节点。

  2. 大小写处理‌:所有方法都将输入的单词或前缀转换为小写字母,这意味着 Trie 树将不区分大小写。这是设计上的选择,但需要确保所有与 Trie 树交互的操作都遵循这一规则。

  3. 同音异义词问题‌:注释中提到了同音异义词可能导致的问题,但在这些方法的实现中并没有处理这个问题。如果需要区分同音异义词,可能需要额外的数据结构或信息。

  4. 性能考虑‌:插入、搜索和前缀检查的时间复杂度都与单词或前缀的长度成正比,即 O(m),其中 m 是单词或前缀的长度。在大多数情况下,这是可以接受的,但对于非常长的单词或前缀,可能需要考虑性能优化。

  5. 调试信息‌:insert 方法中包含了打印调试信息的代码,这在实际使用中可能不是必需的。在生产环境中,应该移除这些打印语句或使用日志记录器来记录必要的信息。

删除方法:delete

用于从 Trie 树中删除一个单词。逻辑同insert,只是最后将单词的结束改成false。(简单粗暴)

current.setEndOfWord(false);‌取消单词结束标记‌:遍历完单词的所有字符后,将当前节点(即单词的最后一个字符对应的节点)的单词结束标记设置为 false,表示该节点不再是一个单词的结束。

注意事项和潜在问题

  • search 方法‌:代码中调用了 search 方法来检查单词是否存在,但这个方法的具体实现没有给出。确保 search 方法能够正确地遍历 Trie 树并检查单词是否存在。

  • 大小写处理‌:代码将输入的单词转换为小写字母,这意味着 Trie 树将不区分大小写。如果这是期望的行为,那么确保所有与 Trie 树交互的操作都遵循这一规则。

  • 同音异义词问题‌(todo 注释):

    // todo 同拼写,不同读音的单词这里有问题。 eg:Live:直播/实况,居住。

    这个注释指出了同音异义词(即拼写相同但读音或意义不同的单词)可能导致的问题。在标准的 Trie 树实现中,同音异义词会被视为相同的单词,因为它们具有相同的拼写。要处理这种情况,可能需要额外的数据结构或信息来区分不同的读音或意义。然而,这个删除方法并没有处理这个问题,它只是简单地删除了拼写相同的单词,不考虑读音或意义的差异。

  • 删除后的节点处理‌:这个方法只是取消了单词结束标记,并没有处理可能存在的孤立节点(即没有其他单词经过的节点)。在某些情况下,可能需要进一步清理这些孤立节点以节省空间。但这通常是一个更复杂的问题,需要额外的逻辑来处理。

  • 性能考虑‌:删除操作的时间复杂度与单词的长度成正比,即 O(m),其中 m 是单词的长度。在大多数情况下,这是可以接受的,但对于非常长的单词,可能需要考虑性能优化。

总的来说,这个方法实现了从 Trie 树中删除单词的基本功能,并考虑了大小写处理和潜在的同音异义词问题(尽管后者没有在这段代码中解决)。要确保这个方法的正确性,需要确保 search 方法的正确实现,并考虑如何处理同音异义词和孤立节点等特殊情况。在实际应用中,可能还需要进一步优化和完善这个方法。

测试结果

        Trie4LowerChar trie = new Trie4LowerChar();
        trie.insert("Hello");
        trie.insert("World");
        System.out.println("'Hello' search in trie:"+trie.search("Hello"));   // 返回 true
        System.out.println("'He' search in trie:"+trie.search("He"));     // 返回 false
        System.out.println("'He' startsWith in trie:"+trie.startsWith("He")); // 返回 true
        trie.insert("He");
        System.out.println("'He' search in trie:"+trie.search("He"));      // 返回 true
        trie.delete("He");
        System.out.println("'He' search in trie:"+trie.search("He"));      // 返回 false
        trie.insert("Hell");
        System.out.println("'He' search in trie:"+trie.search("He"));      // 返回 true

标签:current,ch,java,示例,Trie,单词,哈希,节点
From: https://blog.csdn.net/Rookie_CEO/article/details/145224059

相关文章

  • NB!一款基于java开发的漏洞检测工具,集合了泛微、用友、大华、海康、致远、红帆、万户
    1、工具介绍基于https://github.com/yhy0/ExpDemo-JavaFX上添加poc2、工具下载链接:工具下载:工具下载3、新增检测漏洞用友NC-Cloud系统接口getStaffInfo存在SQL注入漏洞用友U8-CloudReleaseRepMngAction存在SQL注入漏洞复现(CNVD-2024-33023)用友U8-CRM系统getDeptName......
  • Java使用sql查询mongodb
    概述MongoDB是一种NoSQL数据库,它使用文档存储数据,与传统的关系型数据库不同。尽管MongoDB不使用SQL进行查询,但有时在熟悉SQL语法的团队中,能够使用SQL查询MongoDB可以大大简化开发工作。本文将详细介绍如何在Java中使用SQL查询MongoDB。工具与依赖要实现这一......
  • JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、复杂API请
    目录JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、复杂API请求、DOM操作、搜索和过滤等,array.map()的使用详解(附实际应用代码)一、什么时候该使用Array.map(),与forEach()的区别是什么?1、什么时候该用Array.map()2、Array.map()与Array.forEach()的......
  • 1055 集体照(java)
    拍集体照时队形很重要,这里对给定的N个人K排的队形设计排队规则如下:每排人数为N/K(向下取整),多出来的人全部站在最后一排;后排所有人的个子都不比前排任何人矮;每排中最高者站中间(中间位置为m/2+1,其中m为该排人数,除法向下取整);每排其他人以中间人为轴,按身高非增序,先......
  • 2025.1.18 JavaScript基础
    1、变量的定义var变量名例如:<html> <body> <scripttype="text/javascript"> functionzhaoling(){ n=Number(document.form1.txt1.value); if(n!=parseInt(n/1)||n<1||n>100) { alert("请输入一个1-100的整数"); ......
  • springboot+vue+java大学生健康管理系统 867i9
    目录系统实现截图开发核心技术介绍技术栈核心代码部分展示操作手册视频演示/源码获取系统实现截图开发核心技术介绍springboot+Element-UI+vue本系统采用MVVM模式,开发框架使用SpringBoot框架,开发工具使用IDEA,VisualStudioCode,Web服务器使用Tomcat,数据库服......
  • Java虚拟机(JVM)深入解析
    Java虚拟机(JVM)是Java程序运行的核心环境,它负责将Java字节码转换为机器码并执行。本文将深入解析JVM的运行时数据区、类加载机制以及执行引擎,帮助读者更好地理解JVM的工作原理。一、运行时数据区(RuntimeDataArea)运行时数据区是JVM在执行Java程序时分配的内存区域,主要包括以......
  • JAVA多线程
           一多线程基础知识相关概念进程(Process):进程是程序的基本执行实体。进程是操作系统分配资源的基本单位。每个进程都有自己的内存空间、代码段、数据段等。进程之间相互独立,一个进程的崩溃不会影响其他进程。进程是程序的基本执行实体。线程(Thread): ......
  • Java集合小结
    、这一节先快速回顾所学集合知识(抓要点,不深追底层代码),下一节复习集合的八股文狠狠学java,猛猛赚他一笔!一集合体系图集合分为单列集合和双列集合,先来看集合体系图二单列集合2.1List之三种遍历方式 iterator迭代器遍历(idea快捷键itit)Listlist=newArrayList();It......
  • JAVA:根据经纬度获取夏令时以及偏移(免费)
    注:国内根据经纬度来获取夏令时区以及时区偏移量的api的服务有百度和谷歌,但是谷歌的获取夏令时和时区的api在国内服务其上部署时访问不了的(看过有在服务器上安装代理的,但是操作有点复杂。好吧,其实是我看着步骤太多,感觉太麻烦所以直接pass了)。所以目前在我获取到的信息中,只有百......