学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构搞懂 FST

目录

数据结构FST简述

FST简述及它的查询过程

从Lucence源码中看FST的结构

查看builder类(package org.apache.lucene.util.fst)

查看BlockTreeTermsWriter类(packageorg.apache.lucene.codecs.blocktree)

FST存储到文件中的存储结构(TermIndex词项索引和TermDictionary词项字典)

FST构建(读取和写入数据)

通过图解一步步搞清楚结点的添加过程。

读取FST的字符串.


首先声明图片数据来源:b站马士兵教育。

luncence源码百度云:

数据结构FST简述

了解一下trie前缀树:复用所有前缀

学习笔记——搞懂FST数据结构

FST简述及它的查询过程

  1. 查询快
  2. 极致压缩空间占用
    特性:
    确定性:在任何给定状态下,对于任何输入,最多只能遍历一个transtion
    非循环:不可能重复遍历同一个状态
    transducer: 转化器有相关的值(payload),final节点会输出一个值
    比起前面的前缀树以及FSA,在存储的时候多了一个value值。
    考虑以下输入字符:

    学习笔记——搞懂FST数据结构

    此时,再输入wl试试?尽管节点3是final节点,但是由于值对不上,所以不会搜索成功的。
























从Lucence源码中看FST的结构

查看builder类(package org.apache.lucene.util.fst)

学习笔记——搞懂FST数据结构

描述边,也就是出度的类是Arc

学习笔记——搞懂FST数据结构


查看BlockTreeTermsWriter类(packageorg.apache.lucene.codecs.blocktree)

PendingEntry—PendingBlock –PendingTerm
通过输入的字符,一般的是Term,满足某种条件则会被聚合成Block

学习笔记——搞懂FST数据结构

通过构建过程来理解:
1.输入abd,abe,此时还都是正常的情况。(abd和abe都是term)

学习笔记——搞懂FST数据结构

2.输入abfi,abfj,abfk,此时可以看到f已经有三个出度k I j了,初步满足聚合成Block的条件

此时还都是term,没有聚合成block,虽然已经满足了条件给出的3个。但是由于输入项还没确定完.所以还不能聚合起来。

学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构

FST存储到文件中的存储结构(TermIndex词项索引和TermDictionary词项字典)

.tip: 存放Term-Index

.tim:存放Term-Dictonary

学习笔记——搞懂FST数据结构

Term-index里面存放的FSTindex存放Block的前缀记录,末尾指向block..这些block具体存放在.tim文件也就是term Dictonary里面。

block具体存放的是啥?

 学习笔记——搞懂FST数据结构

BlockHeader:学习笔记——搞懂FST数据结构

Suffix: 学习笔记——搞懂FST数据结构

states: 学习笔记——搞懂FST数据结构

metaLength: 学习笔记——搞懂FST数据结构

再来看看FST-Index和block更详细的内容

 学习笔记——搞懂FST数据结构

以这样一个词项字典

学习笔记——搞懂FST数据结构

还是这张图

构建FST-Index

学习笔记——搞懂FST数据结构

其中flag是标志位,代表着一个block的开始.

字符’a’以及’c’代表着以’a’开始的block和’c’开始的block

数字12,36,64等代表着在数组中的起始位置.

逐个看看:

学习笔记——搞懂FST数据结构

 学习笔记——搞懂FST数据结构

这一块对应的是这个学习笔记——搞懂FST数据结构a有两个子节点,所以EntryCount为2

学习笔记——搞懂FST数据结构

 学习笔记——搞懂FST数据结构

这一块对应的是这个

学习笔记——搞懂FST数据结构

EntryCount为5,代表五个后缀节点

Suffix里面包含了a,b,c,d,e五个,他们所代表的属性分别是PendingTerm, PendingTerm, PendingBlock,FloorBlock, PendingTerm

学习笔记——搞懂FST数据结构

 学习笔记——搞懂FST数据结构

这一块对应的是这个学习笔记——搞懂FST数据结构

suffix:是a,b,c三个后缀

学习笔记——搞懂FST数据结构

 学习笔记——搞懂FST数据结构

这块对应的是这个最多后缀的d节点:

学习笔记——搞懂FST数据结构

以上是FST-Index以及Block的映射情况。

FST构建(读取和写入数据)

上面的那些都是一些概念定义,对上面的知识点有了大概的认识之后,就可以真正搞懂FST这个牛x的数据结构是怎么读写数据的了。

FST的写数据过程最好是结合它的读的过程去理解,不然会感觉很懵逼。

首先,FST里面包含了两个重要的属性:

Node和Arc,分别代表结点和出度(边)

Node作为节点,其实它在这里主要起到了描述节点状态的作用,描述节点是否已经经过处理。它是一个基类,子类有UncomliedNode和CompliedNode

学习笔记——搞懂FST数据结构

Arc是出度,它里面算是大有文章了。

四个属性:

学习笔记——搞懂FST数据结构

label:存放输入的字符,实际存的是ASCLL码二进制

output:存放输出值,还记得前面说的FST类似于哈希表吗,可以理解为存放k-v中的v值

以上两个属性比较好理解,难点重点是下面两个:

target:存放下标值,如果当前出度指向的字符不是输入值的最后一个时,会存储index值来指向下一个字符的下标。事实上target这个属性就是在flags属性里面的BIT_TARGET_NEXT状态不存在时才会出现的。

flags:

学习笔记——搞懂FST数据结构

这里面用到比特位来存储,主要作用应该是压缩占用空间。利用标志位在读的时候可以解码。想想一个数字可以存放这么多状态, 有点类似于以前学计组时接触过的flag标志位。

flags里面有六个状态,除了BIT_TARGET_NEXT不好理解,其它应该比较简单。这里我也不赘述。

BIT_TARGET_NEXT这个状态一开始我去查百度,大多说的都是TARGET_NEX优化,但是都没有说这个优化是什么。这里我简单理解总结一下就是,当这个状态激活时,我们在读到这个字符的时候不需要发生跳转,只需要继续往下遍历即可。所以当这个状态激活的时候,上面说到的Target属性才会不存在。只有当这个状态没有激活的时候,我们需要在读完这个之后跳转到另一个字符,而这时target才会给出一个index值让我们去跳转。

再看看FST的源码:

这里是它的六个属性flags

学习笔记——搞懂FST数据结构

这是它的构建过程(节选)

学习笔记——搞懂FST数据结构

如果到这个地方还是无法理解,那也没事,后面在写到FST的构建(写)以及读的过程就会好理解很多。

下面再一步步地构建FST,通过观察构建过程和结合读过程搞懂这玩意。

  1. 初始化
new Builder()。初始化BytesStore bytes.写入一个0,用来表示FST的结束。因为读取的时候是反向读取,(先进后出)。
初始化一个长度为10的UnCompliedNode[ ] frontier
 

学习笔记——搞懂FST数据结构

  1. 开始输入数据Builder.add方法

public void add(IntsRef input, T output)

这个方法主要干四件事情:

2.1,计算当前字符串和上一个字符串的公共前缀

2.2,调用freezeTail方法,从尾部一直到公共前缀的节点,将已确定的状态节点冻结

2.3,将当前字符串形成状态节点加入到frontier数组中

2.4,调整每条边的输出值

看看源码:

public void add(IntsRef input, T output) throws IOException { //2.1,计算当前字符串和上一个字符串的公共前缀 if (output.equals(this.NO_OUTPUT)) { output = this.NO_OUTPUT; } assert this.lastInput.length() == 0 || input.compareTo(this.lastInput.get()) >= 0 : "inputs are added out of order lastInput=" + this.lastInput.get() + " vs input=" + input; assert this.validOutput(output); if (input.length == 0) { ++this.frontier[0].inputCount; this.frontier[0].isFinal = true; this.fst.setEmptyOutput(output); } else { int pos1 = 0; int pos2 = input.offset; int pos1Stop = Math.min(this.lastInput.length(), input.length); while(true) { ++this.frontier[pos1].inputCount; if (pos1 >= pos1Stop || this.lastInput.intAt(pos1) != input.ints[pos2]) { int prefixLenPlus1 = pos1 + 1; int idx; if (this.frontier.length < input.length + 1) { Builder.UnCompiledNode 
    
      [] next = (Builder.UnCompiledNode[])ArrayUtil.grow(this.frontier, input.length + 1); for(idx = this.frontier.length; idx < next.length; ++idx) { next[idx] = new Builder.UnCompiledNode(this, idx); } this.frontier = next; } //2.2,调用freezeTail方法,从尾部一直到公共前缀的节点,将已确定的状态节点冻结 this.freezeTail(prefixLenPlus1); //2.3,将当前字符串形成状态节点加入到frontier数组中 for(int idx = prefixLenPlus1; idx <= input.length; ++idx) { this.frontier[idx - 1].addArc(input.ints[input.offset + idx - 1], this.frontier[idx]); ++this.frontier[idx].inputCount; } Builder.UnCompiledNode 
     
       lastNode = this.frontier[input.length]; if (this.lastInput.length() != input.length || prefixLenPlus1 != input.length + 1) { lastNode.isFinal = true; lastNode.output = this.NO_OUTPUT; } //2.4,调整每条边的输出值 for(idx = 1; idx < prefixLenPlus1; ++idx) { Builder.UnCompiledNode 
      
        node = this.frontier[idx]; Builder.UnCompiledNode 
       
         parentNode = this.frontier[idx - 1]; T lastOutput = parentNode.getLastOutput(input.ints[input.offset + idx - 1]); assert this.validOutput(lastOutput); Object commonOutputPrefix; if (lastOutput != this.NO_OUTPUT) { commonOutputPrefix = this.fst.outputs.common(output, lastOutput); assert this.validOutput(commonOutputPrefix); T wordSuffix = this.fst.outputs.subtract(lastOutput, commonOutputPrefix); assert this.validOutput(wordSuffix); parentNode.setLastOutput(input.ints[input.offset + idx - 1], commonOutputPrefix); node.prependOutput(wordSuffix); } else { commonOutputPrefix = this.NO_OUTPUT; } output = this.fst.outputs.subtract(output, commonOutputPrefix); assert this.validOutput(output); } if (this.lastInput.length() == input.length && prefixLenPlus1 == 1 + input.length) { lastNode.output = this.fst.outputs.merge(lastNode.output, output); } else { this.frontier[prefixLenPlus1 - 1].setLastOutput(input.ints[input.offset + prefixLenPlus1 - 1], output); } this.lastInput.copyInts(input); return; } ++pos1; ++pos2; } } } 
        
       
      
    

通过图解一步步搞清楚结点的添加过程。

首先考虑输入这样一组数据构建FST

学习笔记——搞懂FST数据结构

在观察过程之前,要清楚两件事情:

1.这里面的处理主要是往current数组里面填充数据,填充的是arc也就是出度。

2.处理出度的过程涉及到label,output,target,flags的填充,这里侧重点是flags的填充

Step1:ab

学习笔记——搞懂FST数据结构

 学习笔记——搞懂FST数据结构

Step2:abd

学习笔记——搞懂FST数据结构

Step3:abgl

学习笔记——搞懂FST数据结构

这里由于g的顺序在d之后,发生了一些改变。

1.d状态节点被冻结住,发生了freeze Tail操作。

2.重新计算了output值

3.将node g以及node l写入forniter数组(未经处理)

但是要注意的是,此时不会将arc-d写入current数组,正如图中所说,此时尚未确定后面是否还会输入诸如abf之类的字符串。要等到节点b被冻结才会将它的出度写入current数组.

Step4:acd

学习笔记——搞懂FST数据结构

此时由于c的顺序在b之后,所以确定之后不会有ab前缀的输入了,此时又进行了一些操作:

  1. freeze Tail冻结node b,node g,node l
  2. 处理node g,b,将他们的出度加入到current数组

node g有一个出度l.

处理node d的出度l的过程中计算了flags的值,计算过程正如图中所写。

需要注意的是这里写入了 BIT_TARGET_NEXT这个状态。

此时为什么激活呢,联想一下读取的过程,当我们读取到字母g的时候,我们的下一个应该是l,这个时候没有发生跳转而是顺序遍历下来的。所以target值是不存在的。因此这个状态要激活。

学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构

接下来处理node b

node b有两个出度g和d

要对两个出度进行处理,过程基本上和处理node g的过程一样。

这里面出度g 的BIT_TARGET_NEXT状态被激活,再次联想一下读取过程,它只要是顺序读取的,就会被激活。

出度d同上(原作者似乎是计算错误了,d出度也是有激活的)

学习笔记——搞懂FST数据结构

处理完以上三个出度,current数组的情况如下:

学习笔记——搞懂FST数据结构

Step5:msb

学习笔记——搞懂FST数据结构

此时由于m的字典序在a之后,所以又有一系列工作。。

1.处理End Node

2.freeze Tail操作:冻结a,c,d节点

3.处理node-c上面的(arc-d)出度以及node-a上面的(arc-c,arc-b)出度

学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构

这里面由于b出度没有激活BIT_TARGET_NEXT状态,所以多了一个target(index),用作跳转.

Step6:mst

学习笔记——搞懂FST数据结构

此时有msbc上面的node-b节点的arc-c出度需要处理.

学习笔记——搞懂FST数据结构

写入current数组

学习笔记——搞懂FST数据结构

Step7:wl

学习笔记——搞懂FST数据结构

这是最后一组输出了.此时需要将剩下未处理的所有出度处理完写进current数组

学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构

处理node-m节点上的arc-s出度

学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构

再处理node-w上的arc-l出度

学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构

最后处理entry-node(根节点)的三个出度:w,m,a

学习笔记——搞懂FST数据结构

学习笔记——搞懂FST数据结构

这是最终构建成功的FST结构

 学习笔记——搞懂FST数据结构

最终构建完成的current数组

学习笔记——搞懂FST数据结构

以上就是整个FST的构建过程(写),需要明确,以上的写入操作最终是写到.tim文件里

下面是读的过程。

读取FST的字符串.

首先要明确两点

读的是什么? current数组。

读的顺序是? 从后往前。

学习笔记——搞懂FST数据结构

这里演示一下,不过多展开.

我们结合状态的功能来读

 学习笔记——搞懂FST数据结构

首先进来读的第一个字符是a,因为a处于数组的末端。

 学习笔记——搞懂FST数据结构

此时读到它的flag为16,可以解码出它的状态值为:16

 学习笔记——搞懂FST数据结构

16说明它有output值,并且值为2.并且其他状态都未激活;

而TARGET_NEXT优化没有激活,说明需要跳转。跳转的index值为16

它的label是97(ASCLL码对应就是字母’a’)

这就已经成功解读它的四个属性完成解码。再接下来往下读,我们根据a的提示跳转到index16去。

index16代表的字符是b,按照刚刚解读a的方法对它进行解读:

学习笔记——搞懂FST数据结构

flag是41,说明它的状态是32+8+1

 学习笔记——搞懂FST数据结构

 学习笔记——搞懂FST数据结构

 学习笔记——搞懂FST数据结构

32说明它有一个final-output值,并且为7

8说明它是一个终止节点,也就是说读到这就成功取出第一个字符串’ab’

1说明它是当前peding-term的最后一个字符.

而TARGET_NEXT优化没有激活,说明需要跳转。跳转的index值为8.

它的label是98,对应ASCLL码就是字母’b’

如此一来我们就取出第一个字符串’ab’了.

后面字符串的读取过程可以参照此过程进行。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/219293.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月17日 下午10:54
下一篇 2026年3月17日 下午10:54


相关推荐

  • c++中的析构函数

    c++中的析构函数印象中 c 的析构函数是会自动调用 但是今天运行程序的时候 却没有进入到我写的析构函数 让我不禁怀疑 最终到底析构了没有 还是暗自调用了系统的析构 所以来记载一下析构的知识 以下属转载 原文博客地址 https www cnblogs com puyangsky p 5319470 html 一 定义 1 作用 对象消亡时 自动被调用 用来释放对象占用的空间 2 特点 1

    2026年3月16日
    3
  • noip2013提高组_左归丸组方解析

    noip2013提高组_左归丸组方解析题目描述:铺地毯  选择客栈  Mayan游戏 计算系数 聪明的质检员 观光公交day1:铺地毯:只有一个需要注意的地方:给出的g和k不是右下角的坐标,右下角坐标应是(a+g,b+k)倒序判断即可。参考程序:#include#include#definemaxn1100000usingnamespacestd;intx1[maxn],x

    2025年11月24日
    3
  • 安全日志审计系统服务器,日志审计服务器「建议收藏」

    安全日志审计系统服务器,日志审计服务器「建议收藏」日志审计服务器内容精选换一换本地使用远程桌面连接登录Windowsserver2012云服务器,报错:122.112…,服务器频繁掉线,Windows登录进程意外中断。系统资源不足或不可用。服务启动失败。通过VNC方式登录云服务器。单击打开服务管理,选择“管理工具>事件查看器>Windows日志>系统>筛选当前日志”。事件查看器在“事件级别”负载均…

    2022年6月4日
    106
  • 第 3.4 节 MySQL

    第 3.4 节 MySQL

    2021年3月12日
    207
  • mysql数据库报错1062_【1062错误 mysql】

    mysql数据库报错1062_【1062错误 mysql】mysql gt 插 进 helei 文字 值 iii 查询 好 1 行 受影响了吗 0 28 秒 mysql 选择 从 le ID 文本 1 a 2 b 3 抄送 4 e 5 Ff 6 吗 g 7 h 8 ii

    2026年3月26日
    2
  • 哈希和一致性哈希算法

    哈希和一致性哈希算法哈希Hash算法介绍哈希算法也叫散列算法,不过英文单词都是Hash,简单一句话概括,就是可以把任意长度的输入信息通过算法变换成固定长度的输出信息,输出信息也就是哈希值,通常哈希值的格式是16进制或者是10进制,比如下面的使用md5哈希算法的示例md5(“123456”)=>”e10adc3949ba59abbe56e057f20f883e”主要特点:•不可逆从哈希值不能推导出原始数据,所以Hash算法广泛应用在现代密码体系中•无碰撞不同的信息进行哈希后

    2022年7月27日
    7

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号