字典树原理与应用

简介: 一、概念 字典树又称单词查找树,Trie树,是一种树形结构,是哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计、搜索联想等。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较。 二、特点 根结点不包含字符

字典树原理与应用


一、概念


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


二、特点


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


三、常见操作


查找、插入和删除(很少用到)。


四、实现


代码如下,这段代码实现了字典树的insert以及根据前缀匹配所有字符串的方法。


public class Trie {
    private TrieNode root;
    public Trie(){
        root = new TrieNode();
    }
    public void insert(String str){
        if(str==null||str.length()==0){
            return;
        }
        TrieNode node = root;
        char[] allChars = str.toCharArray();
        for(int i=0; i<allChars.length; i++){
            Character character = new Character(allChars[i]);
            if (!node.children.containsKey(character)){
                node.children.put(character, new TrieNode(character));
            }
            node = node.children.get(character);
        }
        node.isEnd = true;
    }
    public List<String> matchPrefix(String prefix){
        List<String> result = new ArrayList<String>();
        if(prefix==null||prefix.length()==0){
            return result;
        }
        char[] allChars = prefix.toCharArray();
        TrieNode node = root;
        for(int i=0; i<allChars.length; i++){
            Character character = new Character(allChars[i]);
            if(!node.children.containsKey(character)){
                return result;
            }else{
                node = node.children.get(character);
            }
        }
        preTraverse(node, prefix, result);
        return result;
    }
    private void preTraverse(TrieNode node, String prefix, List<String> result){
        if(!node.children.isEmpty()){
            for (Map.Entry<Character, TrieNode> entry: node.children.entrySet()){
                if (entry.getValue().isEnd){
                    result.add(prefix+entry.getKey().toString());
                }
                preTraverse(entry.getValue(), prefix+entry.getKey().toString(), result);
            }
        }
    }
    private class TrieNode {
        private Map<Character, TrieNode> children;
        private boolean isEnd;
        private Character character;
        TrieNode(){
            children = new HashMap<Character, TrieNode>();
            isEnd = false;
        }
        TrieNode(Character character){
            children = new HashMap<Character, TrieNode>();
            isEnd = false;
            this.character = character;
        }
    }
}

 

我们通过一段代码测试一下:


public class MainApp {
    public static void main(String[] argv){
        ArrayList<String> strs = new ArrayList<String>();
        strs.add("我爱学习");
        strs.add("我爱学");
        strs.add("我爱学JAVA");
        strs.add("我不爱学习");
        strs.add("小明也爱学习");
        strs.add("我爱Python");
        Trie trie = new Trie();
        for(String s: strs){
            trie.insert(s);
        }
        String prefix = "我爱";
        List<String> res = trie.matchPrefix(prefix);
        System.out.println(res);
    }
}


执行后输出:[我爱Python, 我爱学, 我爱学习, 我爱学JAVA]


五、应用


给定一个单词列表,我们将这个列表编码成一个索引字符串 S 与一个索引列表 A。

例如,如果这个列表是 ["time", "me", "bell"],我们就可以将其表示为 S = "time#bell#" 和 indexes = [0, 2, 5]。


对于每一个索引,我们可以通过从字符串 S 中索引的位置开始读取字符串,直到 "#" 结束,来恢复我们之前的单词列表。


那么成功对给定单词列表进行编码的最小字符串长度是多少呢?


示例:


输入: words = ["time", "me", "bell"]


输出: 10


说明: S = "time#bell#" , indexes = [0, 2, 5]


通过分析,我们可以看出,这道题目标就是保留所有不是其他单词后缀的单词,最后的结果就是这些单词长度加一的总和(因为每个单词编码后后面还需要跟一个 # 符号)。很明显这里需要使用字典表。代码如下:


class Solution {
    public int minimumLengthEncoding(String[] words) {
        Trie trie = new Trie();
        HashMap<TrieNode, Integer> nodes = new HashMap<TrieNode,Integer>();
        for(int k=0; k<words.length; k++){
            String s = words[k];
            TrieNode node = trie.insert(s);
            if(node!=null){
                nodes.put(node, k);    
            }
        }
        int res = 0;
        for(TrieNode n: nodes.keySet()){
            if(n.children.isEmpty()){
                res += words[nodes.get(n)].length()+1;
            }
        }
        return res;
    }
}
class Trie {
    TrieNode root;
    Trie(){
        root = new TrieNode();
    }
    TrieNode insert(String str){
        if(str==null||str.length()==0){
            return null;
        }
        TrieNode node = root;
        char[] allChars = str.toCharArray();
        for(int i=allChars.length-1; i>=0; i--){
            Character character = new Character(allChars[i]);
            if (!node.children.containsKey(character)){
                node.children.put(character, new TrieNode(character));
            }
            node = node.children.get(character);
        }
        node.isEnd = true;
        return node;
    }
}
class TrieNode {
    Map<Character, TrieNode> children;
    boolean isEnd;
    Character character;
    TrieNode(){
        children = new HashMap<Character, TrieNode>();
        isEnd = false;
    }
    TrieNode(Character character){
        children = new HashMap<Character, TrieNode>();
        isEnd = false;
        this.character = character;
    }
}


这里为了方便最后的字符串统计,所以对本文前面提供的字典表代码略作修改,但是整体思路是一样的。


分类: 数据结构与算法

相关文章
|
6月前
|
算法
数据结构中的KMP算法及其改进算法
KMP算法通过引入部分匹配表,有效避免了重复计算,从而将字符串匹配的时间复杂度降低到O(m+n)。通过进一步优化next数组,KMP算法的效率得到了进一步提升。对于大规模字符串匹配问题,KMP算法及其改进算法提供了高效的解决方案,是计算机科学领域的经典算法之一。
105 3
|
3月前
|
存储 C语言
数据结构基础详解(C语言): 树与二叉树的应用_哈夫曼树与哈夫曼曼编码_并查集_二叉排序树_平衡二叉树
本文详细介绍了树与二叉树的应用,涵盖哈夫曼树与哈夫曼编码、并查集以及二叉排序树等内容。首先讲解了哈夫曼树的构造方法及其在数据压缩中的应用;接着介绍了并查集的基本概念、存储结构及优化方法;随后探讨了二叉排序树的定义、查找、插入和删除操作;最后阐述了平衡二叉树的概念及其在保证树平衡状态下的插入和删除操作。通过本文,读者可以全面了解树与二叉树在实际问题中的应用技巧和优化策略。
|
6月前
|
算法
数据结构和算法学习记录——小习题-二叉树的遍历&二叉搜索树
数据结构和算法学习记录——小习题-二叉树的遍历&二叉搜索树
41 0
|
7月前
|
存储 算法 NoSQL
数据结构期末复习(4)串 树和二叉树
数据结构期末复习(4)串 树和二叉树
78 0
|
存储 算法
数据结构练习题——树和二叉树(算法设计题)
以二叉链表作为二叉树的存储结构,编写以下算法: (1)统计二叉树的叶结点个数。 [题目分析]如果二叉树为空,返回0,如果二叉树不为空且左右子树为空,返回1,如果二叉树不为空,且左右子树不同时为空,返回左子树中叶子节点个数加上右子树中叶子节点个数。 [算法描述]
329 0
|
算法
代码随想录算法训练营第二十二天 | LeetCode 669. 修剪二叉搜索树、108. 将有序数组转换为二叉搜索树、538. 把二叉搜索树转换为累加树
代码随想录算法训练营第二十二天 | LeetCode 669. 修剪二叉搜索树、108. 将有序数组转换为二叉搜索树、538. 把二叉搜索树转换为累加树
54 0
剑指offer(C++)-JZ26:树的子结构(数据结构-树)
剑指offer(C++)-JZ26:树的子结构(数据结构-树)
|
存储 Java
Java数据结构之第十五章、Trie(前缀树/单词查找树)
1.前缀树的概念:前缀树又叫字典树或单词查找树(高效的存储和查找字符串集合的数据结构)。2.3.存储形式:存储的字符串可能:全是 小写字母 或全是 大写字母 或全是 数字 或全是 0和1。它是一棵,每个代表一个,从。字典树的根节点不包含字符,每个子节点代表一个字符,从根节点到任意一个节点所经过的路径上的字符连接起来即为该节点所代表的字符串。每个节点可以存储一个或多个字符串,通常使用一个标志来标记一个节点代表的字符串是否存在。当需要在一组字符串中查找某个字符串时,可以利用字典树来实现高效的查找操作。
81 0
|
存储 人工智能
【数据结构和算法思想】递归思想
在程序中可以调用函数来完成任务,为了完成相同的任务可以调用同一个函数。如果在函数中调用函数本身,那么改函数就被称为递归函数。递归函数的调用是按层,不是次,有 N 层就同时调用(打开)了 N 个函数,不是 N 次。 无限递归(递而不归、死递归),栈溢出(函数的调用有时间和空间的开销,一个程序中同时调用的函数个数是有限的)。• 递归函数的调用有时间和空间的开销,而且递归的次数受到堆栈大小的限制。 • 循环没有函数调用和返回中的参数传递和返回值的额外开销,更快。 如何在递归和循环之间选择? 一般情况下,当循环方法比较容易实现时,应该避免使用递归。当很难简历一个循环方法时,递归可能是一个很好的选择

热门文章

最新文章