首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >什么是前缀树--打开了我的新思路

什么是前缀树--打开了我的新思路

作者头像
用户7656790
发布2020-08-13 15:07:17
1.6K0
发布2020-08-13 15:07:17
举报

今天继续来讲面试,已经出了将近十个美团java一面真题系列文章了,今天来讲一讲前缀树,相信大多数小伙伴对这个前缀树是很陌生的,有些甚至都没有听说过“前缀树”这个词,说实话我也是看面经才知道这个词的

,我们根据面经来进行补短板,查漏补缺。下面开始今天的干货内容吧,走起

1. 前缀树的概述

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

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

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

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

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

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

举个栗子:给出一组单词,inn, int, at, age, adv,ant, adv 我们可以得到下面的Trie:

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

如要查找int,顺着路径i -> in -> int就找到了。

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

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

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

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

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

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

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

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

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

2. 前缀树的应用场景

(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的东东吧。

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

3. 前缀树的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) +
                '}';
    }
}

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

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;
  }
    
}

测试:

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"));
  }
 
}

输出:

END

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 五角钱的程序员 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 前缀树的概述
  • 2. 前缀树的应用场景
  • 3. 前缀树的java实现
    • 节点
    • 前缀树(增加,查询字符串数量,查询前缀数量)
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档