Trie树进阶:Double-Array Trie原理及状态转移过程详解

2024-06-16 09:18

本文主要是介绍Trie树进阶:Double-Array Trie原理及状态转移过程详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013761665/article/details/49281865

前言:

  Trie树本身就是一个很迷人的数据结构,何况是其改进的方案。

  在本博客中我会从DAT(Double-Array Tire)的原理开始,并结合其源代码对DAT的状态转移过程进行解析。如果因此你能从我的博客中有所收获或启发,It's my pleasure.


本文链接:http://blog.csdn.net/lemon_tree12138/article/details/49281865 -- Q-WHai 
                                                                 --转载请注明出处


特别说明:

0.关于Trie树的构建及使用,请移步:http://blog.csdn.net/lemon_tree12138/article/details/49177509

1.本文参考:

(0)双数组Trie树(DoubleArrayTrie)Java实现-码农场

(1)基于双数组Trie树算法的字典改进和实现 - 期刊论文 - 道客巴巴


图形展示及说明:

0.朴素Trie树示意图:

  

  从上图中可以看到,这样的树结构是非常稀疏的。造成了资源的巨大浪费。


1.DAT节点示意图:

  

  这里"NULL"代表结束。


DAT原理说明:

0.简介:

  在学习DAT(Double-Array Trie)之前,如果你对Tire树的了解还是处在一个模糊的状态,那么我想你现在可以移步到本人的另一篇博客《数据结构:字典树的基本使用》,在对Trie树有一个基本的了解之后,再来学习本文的内容应该会更加轻松自如(如果你对Trie树已经有了或浅或深的了解,那么可以直接看下面的内容了)。

  DAT的本质是一个有限自动机(因为博主在学习DAT之前对自动机的相关内容也是一知半解,在学习DAT的过程,难免有一些痛苦。博主也紧追一下这方面的知识,也会在后面的博客中写一些相关的博文).我们要构建一些状态,用于状态的自动转移。顾名思义,在DAT中用的就是双数组:base数组和check数组。双数组的分工是:base负责记录状态,用于状态转移;check负责检查各个字符串是否是从同一个状态转移而来,当check[i]为负值时,表示此状态为字符串的结束。

  你可能问一个这样的问题:那么base数组和check数组是怎么来进行状态转移呢?

  请看下面关于DAT双数组的计算过程。


1.DAT中双数组的计算过程:

假定有字符串状态s,当前字符串状态为t,假定t加了一个字符c就等于状态tc,加了一个字符x等于状态tx,那么有:
base[t] + c.code = base[tc]
base[t] + x.code = base[tx]
check[tc] = check[tx]

上面的几个等式就是状态base和它的转移方程。


Double-Array Trie源码解析:

0.特别说明:

  DAT中的节点信息如下:

  1. private static class Node {
  2. int code;
  3. int depth;
  4. int left;
  5. int right;
  6. }
  code: 代表节点字符的编码。如:'a'.code = 97

  depth: 代表节点所在树的深度。root.depth = 0

  left: 代表节点的子节点在字典中范围的左边界

  rigth: 代表节点的子节点在字典中范围的右边界


1.DAT的创建

  和Trie树一样,DAT的创建只是创建Root的过程。如下:

  1. public int build(List<String> _key) {
  2. key = _key;
  3. ...
  4. resize(65536 * 32);
  5. ...
  6. Node root_node = new Node();
  7. root_node.left = 0;
  8. root_node.right = keySize;
  9. root_node.depth = 0;
  10. ...
  11. return error_;
  12. }


2.为节点parent生成子节点

  在生成子节点的过程中,如果碰到parent='B',而'B'又是某一个key的结尾。该如何办呢?

  比如:比如若以"一举"中的'举'字符为parent,那么parent.depth = 2,"一举".length = 2.

  遇到这种情况,我们就需要对其进行过滤操作,过程如下:

  1. String tmp = key.get(i);
  2. int currCode = 0;
  3. if (tmp.length() != parent.depth) {
  4. currCode = (int) tmp.charAt(parent.depth) + 1;
  5. }
完整过程:

  1. private int fetch(Node parent, List<Node> siblings) {
  2. ...
  3. int prevCode = 0;
  4. for (int i = parent.left; i < parent.right; i++) {
  5. if (key.get(i).length() < parent.depth) {
  6. continue;
  7. }
  8. String tmp = key.get(i);
  9. int currCode = 0;
  10. if (tmp.length() != parent.depth) {
  11. currCode = (int) tmp.charAt(parent.depth) + 1;
  12. }
  13. ...
  14. if (currCode != prevCode || siblings.size() == 0) {
  15. Node tmp_node = new Node();
  16. tmp_node.depth = parent.depth + 1;
  17. tmp_node.code = currCode;
  18. tmp_node.left = i;
  19. if (siblings.size() != 0) {
  20. siblings.get(siblings.size() - 1).right = i;
  21. }
  22. siblings.add(tmp_node);
  23. }
  24. prevCode = currCode;
  25. }
  26. if (siblings.size() != 0) {
  27. siblings.get(siblings.size() - 1).right = parent.right;
  28. }
  29. return siblings.size();
  30. }

3.向Trie树中插入子节点

  在DAT的创建过程中,insert是关键部分。

  在insert操作里,我们使用了递归的思路来解决问题。为什么要利用递归呢?因为在我们状态转移的过程中,父节点的base值需要依赖子返回的begin值,因为在DAT中,code[null] = 0,所以也可以认为是依赖于子节点的check值,如此反复,层层嵌套。关于这一点在下面的结构图展示中更容易体现。

(0)check的合法性检查

  之前我们说check数组是为了检查各个字符串是否是从同一个状态转移而来,但是,要如何检查呢?看下面这段代码:

  1. outer: while (true) {
  2. position++;
  3. if (check[position] != 0) {
  4. continue;
  5. } else if (first == 0) {
  6. ...
  7. }
  8. begin = position - siblings.get(0).code; // 当前位置离第一个兄弟节点的距离
  9. ...
  10. for (int i = 1; i < siblings.size(); i++) {
  11. if (check[begin + siblings.get(i).code] != 0) {
  12. continue outer;
  13. }
  14. }
  15. break;
  16. }
  这里的position即在数组中的下标。可以看到这是一个循环遍历的过程,我们在一个合适的位置开始,逐步地尝试check值是否合法,找到第一个合法的begin值即可。
  而check[i]合法的条件就是check[i]是否为0。如果check[i]不为0,则说明此位置已经被别的状态占领了,需要更换到下一个位置。

(1)计算所有子节点的check值

  1. for (int i = 0; i < siblings.size(); i++) {
  2. check[begin + siblings.get(i).code] = begin;
  3. }

(2)计算所有子节点的base值

  1. private int insert(List<Node> siblings) {
  2. ...
  3. for (int i = 0; i < siblings.size(); i++) {
  4. List<Node> new_siblings = new ArrayList<Node>();
  5. if (fetch(siblings.get(i), new_siblings) == 0) {
  6. base[begin + siblings.get(i).code] = (value != null) ? (-value[siblings.get(i).left] - 1) : (-siblings.get(i).left - 1);
  7. ...
  8. } else {
  9. int h = insert(new_siblings);
  10. base[begin + siblings.get(i).code] = h;
  11. }
  12. }
  13. return begin;
  14. }
在这一步中,大家可以很明显地看到,这是一个递归的过程。我们需要获得子节点的begin值。


采用递归之后,我们的DAT节点的状态转移过程

(3)整体的insert过程:

  1. private int insert(List<Node> siblings) {
  2. ...
  3. // check的合法性检查
  4. ...
  5. // 计算所有子节点的check值
  6. // 计算所有子节点的base值
  7. ...
  8. }

DAT中双数组的状态转移过程


4.前缀查询

  现在假设待查找字符串T="走廊里的壁画",我们需要在之前的字典中查找所有是T前缀的字符串。我们要怎么做呢?

  其实在上面的双数组状态转移过程图中,我们可以很清楚地找到一条满足条件的路径.如下:

  

关键代码如下:

  1. public List<Integer> commonPrefixSearch(String key, int pos, int len, int nodePos) {
  2. ...
  3. int b = base[nodePos];
  4. ...
  5. for (int i = pos; i < len; i++) {
  6. p = b;
  7. n = base[p];
  8. if (b == check[p] && n < 0) {
  9. result.add(-n - 1);
  10. }
  11. p = b + (int) (keyChars[i]) + 1;
  12. if (b == check[p]) {
  13. b = base[p];
  14. } else {
  15. return result;
  16. }
  17. }
  18. p = b;
  19. n = base[p];
  20. if (b == check[p] && n < 0) {
  21. result.add(-n - 1);
  22. }
  23. return result;
  24. }

5.关键词智能提示:

  在上面“前缀查询”的例子中,我们的匹配字符串中比较长,在还没到字符串的最后一位就遇到状态停止标志。而如果匹配字符串比较短,我就还可以做一些其他的事情了,比如常见的搜索引擎中关键词智能提示。

  过程就是在上一步的基础上,把终止循环的条件修改为直到遇到一个状态停步标志.这样我们就可以在遍历整条路径。

  这个功能,在源码中没有涉及。而本文的目的是在于解释DAT的原理和其状态转移的过程。所以,这里就暂不贴代码了。不过,在后期的《搜索引擎:对用户输入有误的关键词进行纠错处理》博客中应该会有所涉及。感兴趣的朋友,可以关注下。


实现源码下载:

http://download.csdn.net/detail/u013761665/9201933

这篇关于Trie树进阶:Double-Array Trie原理及状态转移过程详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1066053

相关文章

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

作业提交过程之HDFSMapReduce

作业提交全过程详解 (1)作业提交 第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。 第2步:Client向RM申请一个作业id。 第3步:RM给Client返回该job资源的提交路径和作业id。 第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。 第5步:Client提交完资源后,向RM申请运行MrAp

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

hdu1565(状态压缩)

本人第一道ac的状态压缩dp,这题的数据非常水,很容易过 题意:在n*n的矩阵中选数字使得不存在任意两个数字相邻,求最大值 解题思路: 一、因为在1<<20中有很多状态是无效的,所以第一步是选择有效状态,存到cnt[]数组中 二、dp[i][j]表示到第i行的状态cnt[j]所能得到的最大值,状态转移方程dp[i][j] = max(dp[i][j],dp[i-1][k]) ,其中k满足c

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和