add by zhj: 在学习Lucene的存储结构时,看到其使用了FST,这篇文章写的不错。

trie,FSA,FST都是用来解决有限状态机的存储,trie是树,它进一步演化为FSA和FST,这两者是图

该文的原标题是“使用自动机来索引1,600,000,000个键”,我改了一下,原标题其实是

说这三类数据结构的使用场景。

原文:https://steflerjiang.github.io/2017/03/18/%E4%BD%BF%E7%94%A8Automata%E6%9D%A5%E7%B4%A2%E5%BC%951-600-000-000%E4%B8%AA%E9%94%AE/

本文翻译自Index 1,600,000,000 Keys with Automata and Rust,所以标题也直译过来。

有限状态机(FSM, finite state machine)可以用来紧密地存储有序集合和有序键值对,并且可以实现快速搜索。本文中,我会表明怎样用FSM来作为数据结构存储这样的数据。

FSM作为数据结构

FSM是一个状态的集合和状态转移的集合。一个起始状态,0个或多个结束状态。一个FSM在同一时间只有一个状态。

FSM非常常见,并且可以用来描述一系列过程。比如我家猫咪Cauchy一天的日常生活:

里面有一些”asleep”或者”eating”的状态,一些转移”food is served”, “something moved”。这里没有结束状态,如果结束了,那真是太恐怖了!

FSM近似的表达了现实中的情况。Cauchy不可能同时吃饭和睡觉,这跟FSM中同一时刻只有一个状态是一样的。并且,从一个状态转移到另一个状态需要外部环境的一个输出。需要睡觉,可能是因为”吃饱了”, “累了”等等。不管他睡得多死,”听到外面的声音”,它总会醒过来。

有序集合

有序集合里的键按照字典序排序。典型的应用是二叉查找树和B树。无序集合,典型应用就是哈希表。这里,我们先描述一个确定无环有限状态接收器(deterministic acyclic finite state acceptor),即FSA。

一个FSA需要满足以下条件:

  • 确定性的。给定已给输入,最多只能转移到一个状态。
  • 无环的。不能反序遍历。
  • 接收器。FSA可以接收一系列特定的输入。

那么,怎么用这些特性来表示一个集合呢。诀窍在于,key作为FSA的状态转移。这样,给定一个输入key,我们可以知道这个key这个key是否在FSA中。

假设一个集合,只有一个key”jul”。FSA就像下面这样:

这时候如果问FSA,是否包含”jul”。处理顺序如下:

  • 给定j,FSA状态从0变为1.
  • 给定u,FSA状态从1变为2.
  • 给定l,FSA状态从2变为3.

输入结束,这时候判断一下FSA是否处在final状态(图中用双圈表示),表明jul确实在set中。

这时候如果问FSA,是否包含”jun”。处理顺序如下:

  • 给定j,FSA状态从0变为1.
  • 给定u,FSA状态从1变为2.
  • 给定l,FSA不动,处理结束。

FSA不动,因为状态2只接收’l’的转移,但是当前输入为’n’。因此处理结束,也表明集合中不包含”jun”。

这时候如果问FSA,是否包含”ju”。处理顺序如下:

  • 给定j,FSA状态从0变为1.
  • 给定u,FSA状态从1变为2.

判断一下,此时是否处于final状态。

值得注意的是,判断一个key是否存在,受限于key的长度,而不是set的大小。

下面把key”mar”添加到FSA中去,这时候FSA的表现如下:

FSA变得稍微复杂一点,状态0可以有两个转移。如果起始输入mar,它会先转移到1状态。

还有一个需要注意的是,状态3被jul和mar两个key共享。即,状态3可以由l和r转移过来。这种共享的方式,可以用更少的空间保存更多的信息。

如果再加入jun,FSA表现如下:

看到变化了么?只有一点点变化。FSA看起来和之前的几乎没什么区别。唯一变化的地方在状态5多了一个转移。FSA其实没有新增状态节点,因为jun和jul共享了前缀ju

下面展示一个更复杂的FSA,包含三个key,october,november,december。

因为有相同的后缀ber,在FSA中只需要编码一次就行了。两个key有更大的相同的后缀,ember。

在介绍FST之前,我们先看看,如何来遍历FSA中所有的key呢?

为了阐述这个过程,还用一个之前的一个简单的图,有三个key,jul,jun和mar。

遍历方式如下:

  • 初始化状态0
  • 移动到状态4,把j添加到key中
  • 移动到状态5,把u添加到key中
  • 移动到状态3,把l添加到key中,输出jul
  • 返回状态5,把key中的l抛弃掉
  • 移动到状态3,把n添加到key中,输出jun
  • 返回状态5,把key中的n抛弃掉
  • 返回状态4,把key中的u抛弃掉
  • 返回状态0,把key中的j抛弃掉
  • 移动到状态1,把m添加到key中
  • 移动到状态2,把a添加到key中
  • 移动到状态3,把r添加到key中,输出mar

这个算法直接应用一个栈存储访问过的状态,和一个栈存储相应的转移。时间复杂度为O(n),空间复杂度O(k),k是set中最长的键的大小。

有序map

和有序集合类似,只是多了一个输出。有序map常用在二叉查找树和b树,无序map就是hashtbale。这里我们介绍一个deterministic acyclic finite state transducer,确定无环有限状态转移器,FST。

FST满足以下特性:

  • 确定性。
  • 无环。
  • 一个转移器。给定一系列输入,会输出一个值。当且仅当输入会达到FST的final状态。

FST和FSA很像,但是对于一个key,FSA只回答了”yer or no”,FST不仅回答”yes or no”,还好返回和这个key相关的一个值。

在有序集合中,只需要把key保存在转移时。但是在这里,还需返回与key对于的value。

一种方法是,在每次转移的时候添加一些值。当输入序列在状态之间转移的时候,输出序列也在慢慢增加。

还是看一个简单的例子吧。map中只包含一个数据jul,对应的value为7:

这和上面的集合差不多,只是在第一个转移状态j之后多了一个相关联的输出7.另外的转移u和l对应的输出都是0,所以图中就不显示了.

如果要判断,FST中是否存在key”jul”,并且需要对应的返回值,处理过程如下:

  • 初始化value为0
  • 给定输入j,FST从状态0转移到1,value+7
  • 给定输入u,FST从状态1转移到2,value+0
  • 给定输入l,FST从状态2转移到3,value+0

输入结束,状态3为final状态,因此key存在,value为7

下面把k-v,”mar 3”添加到FST中

在起始节点,多了一个新的转移m,对应输出为3.如果我们查询jul,那么应该和上面是一样的处理过程。

继续,当添加一个有相同前缀的key,会发生什么呢?
添加key jun,value 6

在状态5和状态3之间添加了一个转移n。但是还有另外两个变化

  1. 0->4转移j输入对应输出从7变成了6.
  2. 5->3转移l输入对应输出从0变成了1.

这个变化之后们可以正确查询jun和jul,并且返回正确的值。

这种key的属性确保了,即使共享前缀,对于每一个key,然后只有一个唯一的路径可以贯穿整个machine。因此,每个key也有唯一的value。我们要做的就是怎么把这些输出放在转移中去。

其实不仅可以共享前缀,还可以共享后缀。对于两个key tuesday和thursday,分别对于输出3和5.

这两个key有相同的前缀t,相同的后缀sday,按照图里的方式可以保证输出的正确性。

这里在描述输出的时候,其实有一点局限,如果输出不是整形。确实,在FST里用做输出的类型必须满足以下特性:

  • 加法
  • 减法
  • 取前缀(对于整数来说就是min)

构建

Trie树构建

trie树,前缀树。和FSA的区别在于,FSA可以共享前缀和后缀。对于键mon,tues,thus来说,FSA如下:

而trie树只共享前缀,如下:

构建trie树很直接的,对于一个给定的输入,只需要去看看有没有相同的前缀。直到找出相同的前缀,余下的直接转移到一个final状态就可以了。

FSA构建

FSA和trie的区别在于,共享后缀。因此一个FSA的空间会比trie少很多,但是构建起来却更复杂,因此我们如果按照key的字典序插入的话,会好很多,还是用图片来说明。

对于三个key,mon,tues和thurs。按照字典序,插入顺序mon,thurs和tues。先插入mon:

下面插入thurs:

插入thurs的时候,会导致之前的mon被冻结。当FSA中一部分被冻结的时候,我们知道,它以后再也不会被更改了。因为按照字典序排序的,后面的key肯定都是大于等于thurs的。因此不会和mon有相同前缀的key插入了。蓝色的state代表被冻结住,以后不会被更改但是可以被复用。

虚线的状态表示thurs还没有被真正加入到FSA中去,下面插入tues:

在这一步里,我们可以确定hurs会被冻住。因为将会不会有和它有相同前缀的词插入进来了。因为thurs和mon可以有相同的final state了。

这里状态4仍然是虚线,因为还不能确定t开头的key还有没有了。如果下面插入zon:

看到,这时状态4已经被冻住了,因为不会在有t开头的key出现了,另外thurs和tues有一个共同的后缀s,因此状态7和状态9被合并了。

最后,在结束操作以后,把FSA的最后一部分冻住,一个完整的没有重复的结构如下:

因为mon和zon有相同的后缀,因此它们除了第一个状态转移不一样,剩下的可以重复利用。

FST构建

下面快速说一下FST构建,插入键值对 mon-2,tues-3,thurs-5.

直接上图,插入mon-2

对于第一步,我们也可以这样分配输出的值

其实这样也是可以的,但是在算法上来说,把输出放在靠近初始状态的地方,代码写起来更简单。

插入thurs-5

插入tues-3

在把状态0-4之间的输出从5变为3的之后,需要把4之后所有的输出全部加2,除了新添加的key,这样就可以保持输出的平衡。

下面插入一个tye-99

最后的完全形态

trie、FSA、FST(转)的更多相关文章

  1. 洛谷P4180【Beijing2010组队】次小生成树Tree

    题目描述: 小C最近学了很多最小生成树的算法,Prim算法.Kurskal算法.消圈算法等等.正当小C洋洋得意之时,小P又来泼小C冷水了.小P说,让小C求出一个无向图的次小生成树,而且这个次小生成树还 ...

  2. lucene底层数据结构——FST,针对field使用列存储,delta encode压缩doc ids数组,LZ4压缩算法

    参考: http://www.slideshare.net/lucenerevolution/what-is-inaluceneagrandfinal http://www.slideshare.ne ...

  3. lucene字典实现原理——FST

    转自:http://www.cnblogs.com/LBSer/p/4119841.html 1 lucene字典 使用lucene进行查询不可避免都会使用到其提供的字典功能,即根据给定的term找到 ...

  4. POJ 2001 Shortest Prefixes 【 trie树(别名字典树)】

    Shortest Prefixes Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 15574   Accepted: 671 ...

  5. 蓝书2.3 Trie字典树

    T1 IMMEDIATE DECODABILITY poj 1056 题目大意: 一些数字串 求是否存在一个串是另一个串的前缀 思路: 对于所有串经过的点权+1 如果一个点的end被访问过或经过一个被 ...

  6. 基于trie树做一个ac自动机

    基于trie树做一个ac自动机 #!/usr/bin/python # -*- coding: utf-8 -*- class Node: def __init__(self): self.value ...

  7. 基于trie树的具有联想功能的文本编辑器

    之前的软件设计与开发实践课程中,自己构思的大作业题目.做的具有核心功能,但是还欠缺边边角角的小功能和持久化数据结构,先放出来,有机会一点点改.github:https://github.com/chu ...

  8. [LeetCode] Implement Trie (Prefix Tree) 实现字典树(前缀树)

    Implement a trie with insert, search, and startsWith methods. Note:You may assume that all inputs ar ...

  9. hihocoder-1014 Trie树

    hihocoder 1014 : Trie树 link: https://hihocoder.com/problemset/problem/1014 题意: 实现Trie树,实现对单词的快速统计. # ...

随机推荐

  1. vue+element 根据内容计算单元格的宽度

    需求是这样的,之前我也写过,就是前端渲染的表格数据是动态渲染表格的行和列, 那么就必然出现了一个问题,当列超过一定的个数的时候,会出现横向滚动条, 那么怎么让表格整体看起来自然协调一些呢,老大要求表格 ...

  2. maven 学习---Maven安装配置

    想要安装 Apache Maven 在Windows 系统上, 只需要下载 Maven 的 zip 文件,并将其解压到你想安装的目录,并配置 Windows 环境变量. 所需工具 : JDK 1.8 ...

  3. 触发器TRIGGER 自增IDENTITY 聚集索引CLUSTERED

    在触发器的“触发”过程中,有两个临时表inserted和deleted发生了作用.这两个特殊的临时表inserted和deleted,仅仅在触发器运行时存在,它们在某一特定时间和某一特定表相关. CR ...

  4. 交叉编译用于生成aarch64指令的GCC (9.2)

    参考 Building GCC as a cross compiler for Raspberry Pi How to Build a GCC Cross-Compiler 环境 PC: ubuntu ...

  5. Redis开启远程访问及密码

    一.开启远程访问 1.开放端口 firewall-cmd --zone=public --add-port=6379 firewall-cmd --zone=public --add-port=637 ...

  6. python参数传递

    1.形式参数:在定义函数时,函数名后面括号中的参数为“形式参数”,也称形参 2.实际参数:在调用一个函数时,函数名后面括号种的参数为“实际参数”,也就是将函数的调用者提供给函数的参数称为实际参数,也称 ...

  7. SpringBoot中使用Maven插件,上传docker镜像

    开启docker远程端口 我上一篇里面写了,这里暴露的路径: 18.16.202.95:2375 简单构建 配置pom.xml文件 在properties中增加一行指定远程主机的位置 <prop ...

  8. 【java异常】Building workspace has encountered a problem. Error

    可能是workspace设置错误,检查一下 或者把项目重新下一下,或者重新maven导入

  9. 总结敏捷开发之Scrum

    敏捷开发的概念 敏捷开发是一种以人为核心,迭代,循序渐进的开发方法. 为什么说是以人为核心?传统的瀑布模型是以文档驱动的,但是在敏捷中,只写少量的文档,注重的是人与人之间面对面的交流. 什么是迭代?迭 ...

  10. es4x 调用其他三方jar 包

    es4x 使用了graalvm 作为运行时环境,所以即拥有vertx 的强大,又拥有了与java 代码便捷的通信能力 以下是一个简单的测试,同时也简单说明下es4x 的es4x-launcher.ja ...