算法导论:Trie字典树

1、 概述

Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树。

Trie一词来自retrieve,发音为/tri:/ “tree”,也有人读为/tra?/ “try”。

Trie树可以利用字符串的公共前缀来节约存储空间。如下图所示,该trie树用10个节点保存了6个字符串pool、prize、preview、prepare、produce、progress

在该trie树中,字符串preview,prepare公共前缀是“pre”,因此可以只存储一份“pre”以节省空间。当然,如果系统中存在大量字符串且这些字符串基本没有公共前缀,则相应的trie树将非常消耗内存,这也是trie树的一个缺点。

Trie树的基本性质可以归纳为:

(1)根结点不包含字符,除根节点意外每个结点只包含一个字符。

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

(3)每个结点的所有子结点包含的字符串不相同。

注意:每个结点可以有没有或者一个或者多个字结点,叶子结点没有子结点

2、数据结构表示

/**
 * 定义字典树的数据结构
 * c 当前节点值
 * isLeaf 是否是叶子结点
 * children 孩子,key是孩子结点值,value是孩子结点的下一个字典树
 *
 */
class TrieNode {
    char c;
    boolean isLeaf;
    HashMap<Character,TrieNode> children = new HashMap<Character,TrieNode>();
    public TrieNode(char c){
        this.c = c;
    }
    public TrieNode() {}
}

3、Trie字典树查找

利用children HashMap,key是结点值,value是子结点

    /**
     * 查找 返回最后的字符
     * @param word
     * @return
     */
    public TrieNode searchNode(String word){
        HashMap<Character,TrieNode> children = root.children;
        TrieNode t = null;// 临时结点
        // 遍历字符串word的每个字符
        for(int i = 0;i<word.length();i++){
            char c = word.charAt(i);
            if(!children.containsKey(c)){
                return null;
            }else{
                t = children.get(c);
                children = t.children;
            }
        }// 返回最后一个结点,若空word不存在,若非空,并且是叶子结点则存在
        return t;
    }

对返回的t进行判断, 返回最后一个结点,若空word不存在,若非空,并且是叶子结点则存在

    /**
     * 查找是否存在 word字符
     * @param word
     * @return
     */
    public boolean search(String word){
        TrieNode t= searchNode(word);
        // t.c 是字符串word最后一个结点值
        return t!=null && t.isLeaf;
    }

查找前缀字符

    /**
     * 查找前缀字符
     * @param prefix
     * @return
     */
    public boolean startsWith(String prefix) {
        return searchNode(prefix) != null;
    }

4、Trie字典树的插入

    /**
     * 插入
     * @param word
     */
    public void insert(String word){
        HashMap<Character,TrieNode> childern = root.children;
        for(int i=0;i< word.length();i++){
            char c = word.charAt(i);
            TrieNode t = null;
            // 孩子结点中包括了 c 字符
            if(childern.containsKey(c)){
                t = childern.get(c);// c字符所在结点的 子字典树,进入下一个字典树
            }else{
                t = new TrieNode(c);// 不存在的时候新建结点
                childern.put(c,t);// key value
            }
            childern = t.children;// 字典树的 孩子结点
            if(i == word.length() - 1){ // 插入是叶子结点的时候
                t.isLeaf = true;
            }
        }
    }

再说明下children

key是父结点对应的子结点的值,value是key对于的子结点

在非根结点时候,childrenA的key就是children的下一层结点值

5、Trie字典树的删除

这个时间复杂度比较多

首先判断是否可以删除

定义parent child两个结点

当parent的孩子> 1个 通过child的孩子有且只有一个的时候,将parent对于的这个孩子child删除

这里利用上面的searchNode,从下向上对word的子串进行判断

但是这里有个问题,举个例子好说:我们需要删除“pool”,但是“poo”在字典树也是合法的字符,我这样的程序也将“poo”进行了删除,如果定义一个计数器,比较好,当计数器为0的时候进行删除

    /**
     * 删除
     * 删除的是word这个字符串,首先要找到word和其他字符串的公共前缀,从最后一个公共前缀开始删除,结点
     * 值为 null
     * @param word
     */
    public void delete(String word){
        boolean flag = search(word);
        if(flag==false){
            System.out.println("不存在该字符串:"+word);
            return;
        }
        HashMap<Character,TrieNode> childern = root.children;
        TrieNode child = null;// 定义子结点
        for(int i=word.length();i>=1 ;i--){
            String subWord = word.substring(0,i);
            TrieNode parent  = searchNode(subWord);
            if(child!=null &&
                    parent.children.size() !=1 &&
                    child.children.size()==1){
//                删除孩子结点
                parent.children.remove(child.c);
                System.out.println("删除成功:"+word);
                return;
            }else{
                child = parent;
            }
        }
    }

6、字典树的层次输出

利用二叉树层次遍历的思想

定义ArrayList或者队列,每次讲该层结点放到队列中,并删除父结点

    public  void printTrie(){
        HashMap<Character,TrieNode> children = null;
        int size = 0;
        ArrayList<TrieNode> tree = new ArrayList<TrieNode>();
        tree.add(root);
        while(tree.size()!=0){
            size = tree.size();

            for(int i =0;i<size;i++){
                children = tree.get(0).children;

                tree.remove(0);
                // 把children中 value的值都放入到 tree中
                if(children == null)
                    break;
                for(TrieNode tN:children.values()){
                    tree.add(tN);
                    System.out.print(tN.c +"\t");
                }
                System.out.print("\\");
            }
            System.out.println();

        }

    }

}

上面程序对该层的元素进行了输出,

以“\” 为分割部分的字符表示其父结点在:左上最近的结点

但是当某层出现单个字符的时候不能够很好的表示

同时,输出的时候发现每一层的最后一个结点是空,表示不理解

上面例子输出结果:

p    r    o    e    o    i    \o    v    p    \g    d    \z    \l    i    \a    \r    \u    \e    \e    \r    \e    \c    \w    \e    \s    \e    \\s    \\

最后s字符判断父结点出错,因为结点左侧过多的空的时候,只输出一个“\”,但是为什么输出一个“\”,我不理解,不知道哪里处理错误

全部程序

package twotree;

import java.util.ArrayList;
import java.util.HashMap;
/**
 * 定义字典树的数据结构
 * c 当前节点值
 * isLeaf 是否是叶子结点
 * children 孩子,key是孩子结点值,value是孩子结点的下一个字典树
 *
 */
class TrieNode {
    char c;
    boolean isLeaf;
    HashMap<Character,TrieNode> children = new HashMap<Character,TrieNode>();
    public TrieNode(char c){
        this.c = c;
    }
    public TrieNode() {}
}
class Trie{
    private TrieNode root = null;
    public Trie(){
        root = new TrieNode();
    }
    /**
     * 删除
     * 删除的是word这个字符串,首先要找到word和其他字符串的公共前缀,从最后一个公共前缀开始删除,结点
     * 值为 null
     * @param word
     */
    public void delete(String word){
        boolean flag = search(word);
        if(flag==false){
            System.out.println("不存在该字符串:"+word);
            return;
        }
        HashMap<Character,TrieNode> childern = root.children;
        TrieNode child = null;// 定义子结点
        for(int i=word.length();i>=1 ;i--){
            String subWord = word.substring(0,i);
            TrieNode parent  = searchNode(subWord);
            if(child!=null &&
                    parent.children.size() !=1 &&
                    child.children.size()==1){
//                删除孩子结点
                parent.children.remove(child.c);
                System.out.println("删除成功:"+word);
                return;
            }else{
                child = parent;
            }
        }
    }
    /**
     * 插入
     * @param word
     */
    public void insert(String word){
        HashMap<Character,TrieNode> childern = root.children;
        for(int i=0;i< word.length();i++){
            char c = word.charAt(i);
            TrieNode t = null;
            // 孩子结点中包括了 c 字符
            if(childern.containsKey(c)){
                t = childern.get(c);// c字符所在结点的 子字典树
            }else{
                t = new TrieNode(c);// 不存在的时候新建结点
                childern.put(c,t);// key value
            }
            childern = t.children;// 字典树的 孩子结点
            if(i == word.length() - 1){ // 插入是叶子结点的时候
                t.isLeaf = true;
            }
        }
    }
    /**
     * 查找 返回最后的字符
     * @param word
     * @return
     */
    public TrieNode searchNode(String word){
        HashMap<Character,TrieNode> children = root.children;
        TrieNode t = null;// 临时结点
        // 遍历字符串word的每个字符
        for(int i = 0;i<word.length();i++){
            char c = word.charAt(i);
            if(!children.containsKey(c)){
                return null;
            }else{
                t = children.get(c);
                children = t.children;
            }
        }
        // 返回最后一个结点,若空word不存在,若非空,并且是叶子结点则存在
        return t;
    }
    /**
     * 查找是否存在 word字符
     * @param word
     * @return
     */
    public boolean search(String word){
        TrieNode t= searchNode(word);
        // t.c 是字符串word最后一个结点值
        return t!=null && t.isLeaf;
    }
    /**
     * 查找前缀字符
     * @param prefix
     * @return
     */
    public boolean startsWith(String prefix) {
        return searchNode(prefix) != null;
    }
    public  void printTrie(){
        HashMap<Character,TrieNode> children = null;
        int size = 0;
        ArrayList<TrieNode> tree = new ArrayList<TrieNode>();
        tree.add(root);
        while(tree.size()!=0){
            size = tree.size();

            for(int i =0;i<size;i++){
                children = tree.get(0).children;

                tree.remove(0);
                // 把children中 value的值都放入到 tree中
                if(children == null)
                    break;
                for(TrieNode tN:children.values()){
                    tree.add(tN);
                    System.out.print(tN.c +"\t");
                }
                System.out.print("\\");
            }
            System.out.println();

        }

    }

}
public class TrieTest {

    public static void main(String[] args){
        Trie trie = new Trie();
        // pool、prize、preview、prepare、produce、progress
        System.out.println("\t insert");
        trie.insert("pool");
        trie.insert("prize");
        trie.insert("preview");
        trie.insert("prepare");
        trie.insert("produce");
        //trie.insert("produces");
        trie.insert("progress");
        System.out.println("\t printTrie");
        trie.printTrie();
        System.out.println("\t search");
        System.out.println(trie.search("pool"));

        System.out.println(trie.search("produce"));

        System.out.println(trie.search("pop"));
        System.out.println("\t searchNode");
        System.out.println(trie.searchNode("pop") == null);
        System.out.println(trie.searchNode("pop"));

        System.out.println(trie.searchNode("produce") == null);
        System.out.println(trie.searchNode("produce").c);

        System.out.println("\t startsWith");
        System.out.println(trie.startsWith("pre"));
        System.out.println(trie.startsWith("hello"));
        System.out.println(trie.startsWith("pop"));

        System.out.println("\t delete");
        trie.delete("pool");

        System.out.println("\t printTrie");
        trie.printTrie();

        trie.delete("produce");

        System.out.println("\t printTrie");
        trie.printTrie();
    }
}

输出结果

     insert
     printTrie
p    r    o    e    o    i    \o    v    p    \g    d    \z    \l    i    \a    \r    \u    \e    \e    \r    \e    \c    \w    \e    \s    \e    \\s    \     search
true
true
false
     searchNode
true
null
false
e
     startsWith
true
false
false
     delete
删除成功:pool
     printTrie
p    r    e    o    i    v    p    \g    d    \z    i    \a    \r    \u    \e    e    \r    \e    \c    \w    \e    \s    \e    \\s    \删除成功:produce
     printTrie
p    r    e    o    i    v    p    \g    \z    i    \a    \r    \e    e    \r    \e    \w    \e    \s    \\s    \

7、 Trie树的应用

Trie是一种非常简单高效的数据结构,但有大量的应用实例。

(1) 字符串检索

事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。

举例:

@  给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。

@  给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。

(2)字符串最长公共前缀

Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。

举例:

@ 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少?

解决方案:首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线(Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。

而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:

1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;

2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;

(3)排序

Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。

举例:

@ 给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。

(4) 作为其他数据结构和算法的辅助结构

如后缀树,AC自动机等

5、 Trie树复杂度分析

(1) 插入、查找的时间复杂度均为O(N),其中N为字符串长度。

(2) 空间复杂度是26^n级别的,非常庞大(可采用双数组实现改善)。

6、 总结

Trie树是一种非常重要的数据结构,它在信息检索,字符串匹配等领域有广泛的应用,同时,它也是很多算法和复杂数据结构的基础,如后缀树,AC自动机等。

参考1:http://dongxicheng.org/structure/trietree/

参考2:http://linux.thai.net/~thep/datrie/datrie.html#Ref_Fredkin1960

时间: 2024-10-05 12:54:21

算法导论:Trie字典树的相关文章

DFA和trie字典树实现敏感词过滤(python和c语言)

现在做的项目都是用python开发,需要用做关键词检查,过滤关键词,之前用c语言做过这样的事情,用字典树,蛮高效的,内存小,检查快. 到了python上,第一想法是在pip上找一个基于c语言的python字典树模块,可惜没找到合适的,如果我会用c写python模块的话,我就自己写一个了,可惜我还不具备这个能力, 只能用python写了,性能差一点就差点吧,内存多一点也无所谓了. 用搜索引擎看CSDN上的网友的用python实现的DFA,再参照自己以前用c语言写过的字典树,有些不大对,就自己写了一

Trie 字典树 删除操作

字典树的删除操作: 1 没找到直接返回 2 找到叶子节点的时候,叶子节点的count标志清零,代表不是叶子节点了 3 如果当前节点没有其他孩子节点的时候,可以删除这个节点 判断是否需是叶子节点,就检查叶子节点的count标志就可以了. 判断是否有其他孩子节点就需要循环26个节点了,如果都为空,那么就没有其他孩子节点了. #include <stdio.h> #include <stdlib.h> #include <iostream> #include <vect

Trie字典树 动态内存

Trie字典树 1 #include "stdio.h" 2 #include "iostream" 3 #include "malloc.h" 4 #include "string.h" 5 6 using namespace std; 7 8 #define MAX_SIZE 26 9 10 typedef struct Trie{ 11 char val; 12 bool isword; 13 struct Trie*

萌新笔记——C++里创建 Trie字典树(中文词典)(插入、查找、遍历、导入、导出)(上)

写了一个词典,用到了Trie字典树. 写这个词典的目的,一个是为了压缩一些数据,另一个是为了尝试搜索提示,就像在谷歌搜索的时候,打出某个关键字,会提示一串可能要搜索的东西. 首先放上最终的结果: input: 1 编程入门 2 编程软件 3 编程学习 4 编程学习网站 output: 1 char : 件 2 word : 编程软件 3 char : 习 4 word : 编程学习 5 char : 网 6 word : 编程学习网 7 char : 门 8 word : 编程入门 其实这里不应

trie/字典树几题

以后有好多次看到这地方就过去了, 未亲自实践过. 不过今晚看了一下,感觉trie的思想还算是比较基础的. 感觉这一个链接讲的不错:http://www.cnblogs.com/BeyondAnyTime/archive/2012/07/16/2592838.html 顺便水了几道题. 具体列表可见:http://vjudge.net/contest/view.action?cid=47036#overview A:水题啦. 稍微理解下trie就能写出来了. 附个自己写的代码,还是用LRJ书上的非

【oiClass1502】查单词(Trie字典树)

题目描述 全国英语四级考试就这样如期来了,可是小y依然没有做好充分准备.为了能够大学毕业,可怜的小y决定作弊.小y费尽心机,在考试的时候夹带了一本字典进考场,但是现在的问题是,考试的时候可能有很多的单词要查,小y能不能来得及呢? 输入 第一行一个整数N,表示字典中一共有多少个单词(N<=10000).接下来每两行表示一个单词,其中:第一行是一个长度<=100的字符串,表示这个单词,全部小写字母,单词不会重复.第二行是一个整数,表示这个单词在字典中的页码.接下来是一个整数M,表示要查的单词数(M

LeetCode 208.实现Trie(字典树) - JavaScript

??Blog :<LeetCode 208.实现Trie(字典树) - JavaScript> 实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作. Trie trie = new Trie(); trie.insert("apple"); trie.search("apple"); // 返回 true trie.search("app"); // 返回 false trie.

Trie(字典树)解析及其在编程竞赛中的典型应用举例

摘要: 本文主要讲解了Trie的基本思想和原理,实现了几种常见的Trie构造方法,着重讲解Trie在编程竞赛中的一些典型应用. 什么是Trie? 如何构建一个Trie? Trie在编程竞赛中的典型应用有些? 例题解析 什么是Trie? 术语取自retrieval中(检索,收回,挽回)的trie,读作"try",也叫做前缀树或者字典树,是一种有序的树形数据结构.我们常用字典树来保存字符串集合(但不仅限于字符串),如下图就是一个字典树. 它保存的字符集合是{to,te,tea,ted,te

Trie 字典树

1.UVa 1401 Remember the Word 题意:给出n个字符串集合,问其有多少种组合方式形成目标字符串. 思路:对n个字符串集合建立Trie树,保存每个结点的字符串的顺序编号.然后对这棵树查找目标字符串每一个后缀的前缀字符串,累加. 1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #include<iostream> 5 #include<vector>