`
frank-liu
  • 浏览: 1667453 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Trie树的分析和理解

 
阅读更多

简介

  在使用一些搜索引擎去搜一些东西的时候,我们经常会碰到一个有意思的事情。有时候我们在搜索框输入一部分内容的时候,会发现搜索框会显示一个下拉的列表,里面有一些以前面输入的内容为开头的一系列搜索字段。比如当输入search的时候,搜索框会显示如下的内容:

  如图所示,这里显示一个比较神奇的东西,网站居然可以给我们有一个自动补全的提示,这样可以省略了一些手动的输入。 那么,是什么可以支持这种自动补全的功能呢?它为什么能够这么快呢?

 

Trie树

  其实能够达到上述效果的后面是因为一个数据结构,就是Trie树,它也称为字典树。在上述示例中,当我们输入一部分字符串的时候,它能够根据我们输入的部分迅速的找到一些匹配的串。当然,除了上述的这个特点,它还有很多其他的特点。我们会详细的一一分析。

  Trie树的典型结构如下图:

 

  从最上面的那个节点开始,它是一个起始的根节点。而通过这个根节点它有若干个子节点。而从根节点到它的某些个子节点上有一个对应的值。比如说the,它表示从根节点到t节点,再到h节点,然后到e节点,在这个节点上有对应的值。 我们同时也看到,在一些节点上并没有对应的值,比如se, th等。这些节点虽然有对应的字符,但是它没有对应的值。

  从上述的讨论中可以看到,Trie树其实是比较适合存储有很多相同前缀的字符串的。比如前面的字符串she就是shell的前缀。这样它的存储得到充分的利用。

 

Trie树结构

  基于上述的讨论,我们可以构思一下Trie树的结构。因为从最开始的根节点起,理论上它的起始字符可以是任意的一个字符。那么它的可选项是比较大的。而在设定了第一个字符之后,它后续的字符也是面临同样的情况。也就是说,如果一级的可选字符为n个,那么第k级的所有可选范围就达到了n^k这么多。因此,在每个节点,我们都需要定义一个长度为n的数组,表示我们所有可能的选项。如果n和k比较大的话,它消耗的空间将是非常大的。

  在前面的描述里我们还提到了一些对应的节点有详细的取值,而有的没有。这样,我们就可以定义一个如下类型的节点结构:

 

private static class Node {
    private Object val;
    private Node[] next = new Node[R];
}

  在上述的定义里,R表示我们选择的字符集数量。可以根据需要来定义。 

  我们定义的一个初步的Trie树类如下:

 

public class TrieST<T> {
    private static int R = 256;
    private Node root;
    private int n;

    private static class Node {
        private Object val;
        private Node[] next = new Node[R];
    }

    public TrieST() {}
}

  这里我们假定字符集取值是0-256之间。 这正好代表了ASCII码的取值范围。

 

搜索

  既然有了这么一个定义,我们来看查找一个元素的过程。以下图为例:

 

  假设我们需要搜索字符串by,那么需要从根节点开始,一个字符一个字符的对应下去。比如说对应第一个字符的时候,如下图:

  这表示我们已经找到了第一个对应的字符了。接着我们需要继续找下一个字符y:

  这样我们就找到了对应的字符串by以及它对应的值4。当然,这里描述的是一种比较理想的情况。还存在着一些其他的情况。比如说对应的字符串在树里找不到的情况。比如说我们要搜索字符串bee。当我们找到第一个字符b之后,再找e发现没有匹配的字符了。也就是说对应的节点位置是null。那么,这代表了一种查找失败的情况。

  还有一种查找失败的情况,比如我们查找字符串sell,在上图中确实可以找到sell这个串,可是这个串里对应的值为空。因此它也表示查找失败。

  因此,综合来说,对元素的查找可以是一个递归的过程。每次判断当前字符串的位置以及树里头当前的节点。如果当前节点为null,则返回空。如果和当前位置的字符有匹配,则继续下一步,一直到字符串的最后一个元素能找到匹配的节点并有对应的值。这样,我们可以得到搜索的详细实现如下:

 

public T get(String key) {
    Node x = get(root, key, 0);
    if(x == null) return null;
    return (T)x.val;
}

private Node get(Node x, String key, int d) {
    if(x == null) return null;
    if(d == key.length()) return x;
    char c = key.charAt(d);
    return get(x.next[c], key, d + 1);
}

  get方法的时间复杂度相对比较简单,取决于当前匹配字符串的长度。 

 

添加元素

  添加元素的过程其实也不复杂。也是根据字符串的位置从头一步步的往树里遍历。当碰到的当前节点为空的话,则创建一个新的节点。当从头到尾一直遍历到字符串末尾的时候,根据当前位置节点的情况来处理。如果当前节点为空,则创建一个新节点,并设置给定的值。而如果这个节点已经存在了,则更新这个节点的值为给定的值。

  从实现的角度来说,也可以使用递归的方法。只是因为每次递归的时候需要将当前节点返回并赋值给上一个节点的某个元素。这种手法在递归的方法里应用比较多,也比较巧妙。详细的实现如下:

 

public void put(String key, T val) {
    root = put(root, key, val, 0);
}

private Node put(Node x, String key, T val, int d) {
    if(x == null) x = new Node();
    if(d == key.length()) {
        if(x.val == null)
            n++;
        x.val = val;
        return x;
    }
    char c = key.charAt(d);
    x.next[c] = put(x.next[c], key, val, d + 1);
    return x;
}

  

前缀匹配搜索

    Trie树里的一个最典型的应用就是给定一个字符串,查找处所有以这个字符串为前缀的所有串。这就和最开始我们提到的示例那样。如果要实现这部分功能,我们需要首先找到那个以给定字符串作为前缀的节点。当找到这个节点之后,再把它里面所有的子节点都列出来。

  因此它的详细过程分为两个步骤,一个是首先查找这个前缀对应的节点。然后就是收集节点下面所有合法的子节点。而收集所有子节点的过程也是一个递归的过程。每次遍历当前节点的时候,先遍历它所有的next数组,如果有后续的节点,则继续递归。如果有合法的节点,则将它加入到一个队列里。这部分的代码详细实现如下:

 

public Iterable<String> keysWithPrefix(String prefix) {
    Queue<String> q = new LinkedList<String>();
    Node x = get(root, prefix, 0);
    collect(x, prefix, q);
    return q;
}

private void collect(Node x, String pre, Queue<String> q) {
    if(x == null) return;
    if(x.val != null)
        q.add(pre);
    for(char c = 0; c < R; c++)
        collect(x.next[c], pre + c, q);
}

public Iterable<String> keys() {
    return keysWithPrefix("");
}

 

前缀串的模糊匹配

  和前面的特性稍微有点不一样,假设我们需要匹配的串里包含有一些通配符。比如说'.'符号的时候,我们需要将所有的子节点都作为匹配当前节点的一个选项,然后再将这些元素继续向后匹配。详细的代码实现如下:

 

public Iterable<String> keysThatMatch(String pat) {
    Queue<String> q = new LinkedList<String>();
    collect(root, "", pat, q);
    return q;
}

public void collect(Node x, String pre, String pat, Queue<String> q) {
    int d = pre.length();
    if(x == null) return;
    if(d == pat.length() && x.val != null) q.add(pre);
    if(d == pat.length) return;

    char next = pat.charAt(d);
    for(char c = 0; c < R; c++)
        if(next == '.' || next == c)
            collect(x.next[c], pre + c, pat, q);
}

  

最长前缀

  Trie树里还有一种比较常见的应用就是给定一个字符串,求和它匹配的最长前缀。对于这个过程,我们可以这么来看。给定一个字符串来说,它最小的可能是没有字符串和它匹配。也就是说它的第一个元素在树里都找不到匹配的。最长的可能就是它在树里找到一个完整的匹配。那么,要找到这个完整的匹配,我们需要去从树的根节点开始,每次去和串匹配。当中间匹配到某个部分的时候,我们就设置当前匹配的长度为某个值length。如果碰到节点为空或者匹配到串的末尾了,直接返回这个length的长度。这是一种递归的思路。

  还有一种循环遍历的思路,就是从根节点开始进行遍历比较,当碰到节点为空或者到达串末尾的时候退出。它们的详细实现如下:

 

public String longestPrefixOf(String s) {
    int length = search(root, s, 0, 0);
    return s.substring(0, length);
}

private int search(Node x, String s, int d, int length) {
    if(x == null) return length;
    if(x.val != null) length = d;
    if(d == s.length()) return length;
    char c = s.charAt(d);
    return search(x.next[c], s, d + 1, length);
}

private int search(Node x, String s) {
    int i = 0, length = 0;
    while(x != null && i < s.length()) {
        char c = s.charAt(i);
        x = x.next[c];
        if(x != null && x.val != null)
            length = i;
        i++;
    }

    return length;
}

 

删除元素

    删除元素的过程相对来说比较复杂。首先需要像前面搜索的过程那样找到需要删除的节点。然后将该节点的值设置为空。当然,事情并不是这么简单。我们不能将节点设置为空就完事了。还要看它的子节点和父节点的情况。当它还有非空的子节点的时候,可以直接返回。如果它所有的子节点都为空的时候,我们也需要删除当前的节点。但是这样也可能是的它的父节点也面临同样的情况,我们因此也需要进一步的将它的父节点依次删除。

  所以这里在实现的时候要判断,当已经到达当前字符串的结尾时,需要设置当前节点的值为空。当然,判断当前节点是否为空也是一个重要的判断条件,如果当前节点为空,则直接返回null。在没有到达当前节点的情况,则需要递归的去调用方法删除子节点里的元素。这是一个递归的实现,递归方法的每一层都是返回当前递归层所访问的节点。也有可能是null。还有一个很重要的部分就是当递归结束后要开始回溯的时候,我们要根据当前的情况来决定返回的值。

  假如当前节点的值为非空,则直接返回这个节点。如果它的所有子节点里有非空的元素,也直接返回这个节点。只有它所有子节点都为空的时候,返回null。这样就在回溯的时候实现了每一级都是空的时候删除这个节点。详细的代码实现如下:

 

public void delete(String key) {
    root = delete(root, key, 0);
}

private Node delete(Node x, String key, int d) {
    if(x == null) return null;
    if(d == key.length())
        x.val = null;
    else {
        char c = key.charAt(d);
        x.next[c] = delete(x.next[c], key, d + 1);
    }

    if(x.val != null) return x;
    for(char c = 0; c < R; c++)
        if(x.next[c] != null) return x;
    return null;
}

  这部分的代码实现比较简短,但是运用的思路还是比较灵活,值得仔细体会。 

 

总结

    Trie树是一种比较独特的数据结构。它对于字符串的搜索有比较高的效率。尤其在字符的取值范围比较有限而且长度并不大的情况下表现非常理想。大多数情况下,它的查找和插入元素的复杂度只是和给定串的长度有关。当然,因为它要考虑到每一个节点的所有可能取值。在元素取值范围比较大而且串比较长的时候它的空间消耗会非常大,这样就会变得不适用。在某些情况下,另外一个数据结构Tenary Search Tree会更加合适一些。关于Tenary search tree的讨论,我们会在后面的文章里涉及。

  另外,从树的节点个数角度来考虑,也可以将Trie树当做一个k叉树,只是在很多情况下,它的多部分节点都是空的。

 

参考材料

Algorithms

  • 大小: 20.1 KB
  • 大小: 35 KB
  • 大小: 47 KB
  • 大小: 46.4 KB
  • 大小: 46 KB
分享到:
评论

相关推荐

    用Trie树实现词频统计和单词查询

    一个简单的C语言程序:用Trie树实现词频统计和单词查询

    Python实现Trie树

    用Python实现Trie树的应用,并可以对英汉词典进行导入和检索、添加和删除,最终可以将导入的英汉词典保存到本地磁盘。内附两个.py文件,分别是tree.py和d_gui.py,tree.py是类和方法,d_gui.py是图形界面;一个.txt...

    trie树的实现(C)

    trie.c中定义了trie树的操作函数; trie.h为相应的头文件; test.c用于测试相关的函数。 在trie.c中,关于查找定义了两个函数,一个是find(),一个是search(),二者的区别是,前者仅判断一个字符串是否在树中出现,...

    C++/C Trie树算法

    用C实现的数据结构Trie树算法 实验的函数的trie树的插入 搜索和删除

    基于双数组Trie_树中文分词研究

    对双数纽Trie 树(Double-Array Trie)分词算法进行了优化:在采用... 分词系统,并与其他几种分词方法进行对比,结果表明,优化后的双数纽Trie 树插入速度和 空间利用率得到了很大提高,且分词查询效率也得到了提高.

    Trie 树实现的源码

    Trie 树实现的源码,用C++编写实现,做自然语言处理的朋友可以参考一下

    Trie树入门,很容易上手

    很容易上手的Trie树入门,特别适合于acm初学者

    从trie树谈到后缀树

    网上大神的总结,从trie树谈到后缀树,常用的字符串匹配算法

    Acm Trie树

    这是一个ACM算法,Trie树,他能很好的解决字符问题

    基于Trie树模仿谷歌百度搜索框提示

    基于Trie树模仿谷歌百度搜索框提示。写的比较简单、代码漏洞之处欢迎指正。

    Trie树 linux32 SDK V3.0

    2、Trie树SDK中的API支持以下功能 1)插入节点 2)精确删除节点 3)正向模糊匹配 4)反向模糊匹配 5)精确查询节点 6)获取头(尾)节点 7)删除头(尾)节点 8)排序 9)支持多级树 10)支持强大的查询节点功能 ...

    ACM Trie树 模板 字典树

    ACM Trie树 模板,字典树模板,数据结构

    基本Trie树的实现

    Trie是一种树型数据结构,用于存储字符串,可以实现字符串的快速查找。Trie的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。...适用范围:统计和排序大量的字符串

    trie树模板,acm竞赛

    trie树模板,acm竞赛,可以进行适当的修改就可以解决问题,在进行字符串处理的时候尤其能用到。

    双数组Trie树算法优化及其应用研究.

    Double Array Trie是TRIE树的一种变形,它是在保证TRIE树检索速度的前提下,提高空间利用率而提出的一种数据结构,本质上是一个确定有限自动机(deterministic finite automaton,简称DFA)。 所谓的DFA就是一个能实现...

    论文研究-划分位无冲突哈希在trie树分组中的研究.pdf

    通过基于划分位构建无冲突哈希函数,实现对片上存储器有效的控制,攻击特征平均分配到trie树每层的多个组中。该结构可以在同一个芯片中实现流水并行地执行,获得比较大的吞吐量。理论及实验表明该方法在片上存储器一...

    HASH(Trie)-.rar_HashTree.h_TRIE_hash树_trie树_字典树

    hash trie树 字典树,完整的sdk开发包 具有说明文档

    双数组 DoubleArray Trie树的数组实现 双数组字典

    Trie树是搜索树的一种,来自英文单词"Retrieval"的简写,可以建立有效的数据检索组织结构,是中文匹配分词算法中词典的一种常见实现。它本质上是一个确定的有限状态自动机(DFA),每个节点代表自动机的一个状态。在...

    IT笔试面试--Trie树前缀树常考题目及解析

    IT笔试面试--Trie树前缀树常考题目及解析,包含了Trie树的常考题目,以及详细的解析

    实现trie树的C/C++模板

    建立trie树,并进行相关操作,包括 insert:插入一个字符串,重复插入无效 remove:删除指定的字符串,如果不存在,则不进行操作 find:判断是否有指定的字符串

Global site tag (gtag.js) - Google Analytics