前缀树是什么 前缀树的使用场景

前缀树的概述

前缀树又名字典树,单词查找树,Trie树,是一种多路树形结构,是哈希树的变种,和hash效率有一拼,是一种用于快速检索的多叉树结构。

典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。

Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

Trie树也有它的缺点,Trie树的内存消耗非常大。

性质:不同字符串的相同前缀只保存一份。

操作:查找,插入,删除。

举个栗子:给出一组单词,inn, int, at, age, adv,ant, adv 我们可以得到下面的Trie:在这里插入图片描述

从上面可以发现一些Trie树的特性:

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

2)从根节点到某一节点的路径上的字符连接起来,就是该节点对应的字符串。

3)每个节点的所有子节点包含的字符都不相同。

4)每条边对应一个字母。每个节点对应一项前缀。叶节点对应最长前缀,即单词本身。

单词inn与单词int有共同的前缀“in”, 因此他们共享左边的一条分支,root->i->in。同理,ate, age, adv, 和ant共享前缀"a",所以他们共享从根节点到节点"a"的边。

查询操纵非常简单。比如要查找int,顺着路径i -> in -> int就找到了。

搭建Trie的基本算法也很简单,无非是逐一把每则单词的每个字母插入Trie。插入前先看前缀是否存在。如果存在,就共享,否则创建对应的节点和边。比如要插入单词add,就有下面几步:

考察前缀"a",发现边a已经存在。于是顺着边a走到节点a。

考察剩下的字符串"dd"的前缀"d",发现从节点a出发,已经有边d存在。于是顺着边d走到节点ad

考察最后一个字符"d",这下从节点ad出发没有边d了,于是创建节点ad的子节点add,并把边ad->add标记为d。

前缀树的应用场景

(1)字符串的快速检索

字典树的查询时间复杂度是O(logL),L是字符串的长度。所以效率还是比较高的。字典树的效率比hash表高。

hash表:

通过hash函数把所有的单词分别hash成key值,查询的时候直接通过hash函数即可,都知道hash表的效率是非常高的为O(1),当然这是对于如果我们hash函数选取的好,计算量少,且冲突少,那单词查询速度肯定是非常快的。那如果hash函数的计算量相对大呢,且冲突律高呢?这些都是要考虑的因素。

还有就是hash表不支持动态查询,什么叫动态查询,当我们要查询单词apple时,hash表必须等待用户把单词apple输入完毕才能hash查询。当你输入到appl时肯定不可能hash吧。

字典树(tries树):

对于单词查询这种,还是用字典树比较好,但也是有前提的,空间大小允许,字典树的空间相比较hash还是比较浪费的,毕竟hash可以用bit数组。

(2)字符串排序

从上图我们很容易看出单词是排序的,先遍历字母序在前面。

减少了没必要的公共子串。

(3)最长公共前缀

inn和int的最长公共前缀是in,遍历字典树到字母n时,此时这些单词的公共前缀是in。

(4)自动匹配前缀显示后缀

我们使用辞典或者是搜索引擎的时候,输入appl,后面会自动显示一堆前缀是appl的东东吧。

那么有可能是通过字典树实现的,前面也说了字典树可以找到公共前缀,我们只需要把剩余的后缀遍历显示出来即可。

前缀树的java实现

节点

import java.util.Arrays;


public class TreeNode{
    //经过这个节点的字符串的个数(以这个节点为前缀的字符串的个数)
    public int path;
    //以这个节点结束的字符串的个数(有多少个字符串有这条路径的char组成)
    public int end;
    //对应着小写的a-z的26个字母(如果要更多可以使用hashmap<char,Node>
    public TreeNode[] next;

    public TreeNode(){
        path = 0;
        end = 0;
        next = new TreeNode[26];
    }

    @Override
    public String toString() {
        return "TreeNode{" +
                "path=" + path +
                ", end=" + end +
                ", next=" + Arrays.toString(next) +
                '}';
    }
}

前缀树(增加,查询字符串数量,查询前缀数量)

package datastructure.tree.trietree;
 
public class TrieTree {
	
	public TreeNode root;
	
	public TrieTree(){
		root=new TreeNode();
	}
	
 
	/**在前缀树中插入字符串
	 * 这种++的方法,导致,一个node,有多少个end,就有多少个相同的字符串
	 * 一个node,有多少个path,就有多少个字符串经过(root的path代表有多少个字符串)(字符串末尾的node的path也会++)
	 * @param string 被插入的字符串(以前插入过的也可以插入)
	 */
	public void insertString(String string){
		if(string==null||string.length()==0){
			return;
		}
		int length=string.length();
		TreeNode nowNode=root;
		for(int i=0;i<length;i++){
			char now=string.charAt(i);
			int index=now-'a';
			//index为字符now所处的位置
			if(nowNode.next[index]==null){
				nowNode.next[index]=new TreeNode();
			}
			//先对当前node的path++,再转移到下一个node
			nowNode.path++;
			nowNode=nowNode.next[index];
			
		}
		//在最后的node,path和end++
		nowNode.path++;
		nowNode.end++;				
	}
	
 
	/**返回这个前缀树总共插入了多少个字符串
	 * @return
	 */
	public int size(){
		return root.path;
	}
	
	
	/**前缀树查询总共插入这个字符串多少次,如果没插入过,则返回0
	 * @param string
	 * @return
	 */
	public int getStringNum(String string){
		if(string==null||string.length()==0){
			return 0;
		}
		int length=string.length();
		TreeNode nowNode=root;
		for(int i=0;i<length;i++){
			char now=string.charAt(i);
			int index=now-'a';
			//如果没有这个节点,说明不存在,直接返回0
			if(nowNode.next[index]==null){
				return 0;
			}
			nowNode=nowNode.next[index];
		}		
		//此时nowNode已经处于最后一个节点
		return nowNode.end; 
	}
 
	/**前缀树查询以这个字符串为前缀的字符串总共多少个(包括以他为结尾的)
	 * @param string 前缀
	 * @return
	 */
	public int getPrefixNum(String string){
		if(string==null||string.length()==0){
			return 0;
		}
		int length=string.length();
		TreeNode nowNode=root;
		for(int i=0;i<length;i++){
			char now=string.charAt(i);
			int index=now-'a';
			//如果没有这个节点,说明前缀不存在,直接返回0
			if(nowNode.next[index]==null){
				return 0;
			}
			nowNode=nowNode.next[index];
		}		
		//此时nowNode已经处于前缀的最后一个节点
		return nowNode.path; 
	}
		
}

测试:

package datastructure.tree.trietree;
 
public class Main {
 
	public static void main(String[] args) {
		TrieTree tree=new TrieTree();
		tree.insertString("aa");
		tree.insertString("aa");
		tree.insertString("ab");
		tree.insertString("ba");
		//System.out.println(tree.root);
		//System.out.println(tree.size());
		//System.out.println(tree.getStringNum("aa"));
		//System.out.println(tree.getStringNum("ab"));
		//System.out.println(tree.getStringNum("ac"));
		System.out.println(tree.getPrefixNum("a"));
		System.out.println(tree.getPrefixNum("b"));
		System.out.println(tree.getPrefixNum("c"));
	}
 
}

几百本常用电子书免费领取:https://github.com/XiangLinPro/IT_book

在这里插入图片描述
Life is not to surpass
others, but to surpass ourselves.
生命不是要超越别人,而是要超越自己。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值