trie 和 radix trie 数据结构有啥区别?
Posted
技术标签:
【中文标题】trie 和 radix trie 数据结构有啥区别?【英文标题】:What is the difference between trie and radix trie data structures?trie 和 radix trie 数据结构有什么区别? 【发布时间】:2013-01-20 09:58:11 【问题描述】:trie 和 radix trie 数据结构是一样的吗?
如果它们不一样,那么 radix trie (AKA Patricia trie) 是什么意思?
【问题讨论】:
只有我觉得标签是radix-tree
而不是radix-trie
有点烦人吗?此外,还有很多问题都被它标记了。
@errantlinguist Wikipedia 将 radix trie
文章命名为 Radix tree
。此外,术语“基数树”在文献中被广泛使用。如果任何调用尝试“前缀树”对我来说更有意义。毕竟都是树数据结构。
另外:“radix trie (AKA Patricia trie) 的含义是什么?” 这假设 radix 树和 PATRICIA 树是一回事,但它们不是(例如见this answer)。 PATRICIA 树是您通过运行 PATRICIA 算法 获得的树(仅供参考,PATRICIA 是首字母缩写词,代表“检索字母数字编码信息的实用算法”)。生成的树可以理解为带有radix = 2
的基数树,这意味着您通过一次查找输入字符串的log2(radix)=1
位来遍历树。
【参考方案1】:
基数树是 trie 的压缩版本。在 trie 中,您在每条边上写一个字母,而在 PATRICIA 树(或基数树)中,您存储整个单词。
现在,假设您有 hello
、hat
和 have
这三个词。要将它们存储在 trie 中,它看起来像:
e - l - l - o
/
h - a - t
\
v - e
你需要九个节点。我已经将字母放在节点中,但实际上它们标记了边缘。
在基数树中,您将拥有:
*
/
(ello)
/
* - h - * -(a) - * - (t) - *
\
(ve)
\
*
你只需要五个节点。上图中的节点是星号。
因此,总体而言,基数树占用内存,但更难实现。否则两者的用例几乎相同。
【讨论】:
谢谢...你能给我提供一个学习trie DS的好资源吗...那会很有帮助... 我相信我第一次实现 Trie 时使用的唯一东西是 wikipedia article。我并不是说它是完美的,但它已经足够好了。 我可以说在 TRIE 中搜索比 Radix 树快吗?因为在 TRIE 中,如果要搜索下一个字符,则需要查看当前节点的子数组中的第 i 个索引,但在基数树中,您需要按顺序搜索所有子节点。查看实现code.google.com/p/radixtree/source/browse/trunk/RadixTree/src/… 实际上,在基数树中,您不能有多个以相同字母开头的边,因此您可以使用相同的常量索引。 @Trying Algorithmically Radix 比 TRIE 快,这就是为什么它值得进行压缩。更少的节点加载和更少的空间通常更好。也就是说,实施质量可能会有所不同。【参考方案2】:我的问题是 Trie 数据结构和 Radix Trie 是不是一回事?
简而言之,没有。 Radix Trie 类别描述了 Trie 的特定类别,但这并不意味着所有尝试都是基尝试。
如果它们[n't]相同,那么 Radix trie(又名 Patricia Trie)是什么意思?
我假设你的意思是在你的问题中写 不是,因此我更正了。
同样,PATRICIA 表示特定类型的基数树,但并非所有基数树都是 PATRICIA 树。
什么是尝试?
“Trie”描述了一种适合用作关联数组的树数据结构,其中分支或边对应于键的部分。 parts 的定义在这里相当模糊,因为尝试的不同实现使用不同的位长来对应边。例如,二叉树的每个节点有两条边,对应于 0 或 1,而 16 路特里树的每个节点有十六条边,对应于 4 位(或十六进制数字:0x0 到 0xf)。
从 Wikipedia 检索到的这张图似乎描绘了一个带有(至少)键 'A'、'to'、'tea'、'ted'、'ten'、'i'、'in' 和'客栈'插入:
如果此树要存储键“t”或“te”的项目,则需要在每个节点上提供额外信息(图中的数字)以区分空节点和具有实际值的节点。
什么是基数?
“Radix trie”似乎描述了一种压缩公共前缀部分的 trie 形式,正如 Ivaylo Strandjev 在他的回答中所描述的那样。考虑一个 256 路的 trie,它使用以下静态分配索引键“smile”、“smiled”、“smiles”和“smiling”:
root['s']['m']['i']['l']['e']['\0'] = smile_item;
root['s']['m']['i']['l']['e']['d']['\0'] = smiled_item;
root['s']['m']['i']['l']['e']['s']['\0'] = smiles_item;
root['s']['m']['i']['l']['i']['n']['g']['\0'] = smiling_item;
每个下标访问一个内部节点。这意味着要检索smile_item
,您必须访问七个节点。八个节点访问对应smiled_item
和smiles_item
,九个对应smiling_item
。对于这四个项目,总共有十四个节点。但是,它们都有前四个字节(对应于前四个节点)。通过压缩这四个字节来创建一个对应于['s']['m']['i']['l']
的root
,四个节点访问已经被优化掉了。这意味着更少的内存和更少的节点访问,这是一个很好的指示。可以递归地应用优化以减少访问不必要的后缀字节的需要。最终,您只需要比较 trie 索引位置的搜索键和索引键之间的差异。这是一个基数。
root = smil_dummy;
root['e'] = smile_item;
root['e']['d'] = smiled_item;
root['e']['s'] = smiles_item;
root['i'] = smiling_item;
要检索项目,每个节点都需要一个位置。使用搜索关键字“smiles”和root.position
4,我们访问root["smiles"[4]]
,恰好是root['e']
。我们将其存储在一个名为current
的变量中。 current.position
是5,就是"smiled"
和"smiles"
的区别所在的位置,所以下次访问会是root["smiles"[5]]
。这将我们带到smiles_item
,以及我们字符串的结尾。我们的搜索已终止,项目已被检索到,只有 3 个节点访问,而不是 8 个。
什么是 PATRICIA trie?
PATRICIA trie 是基数尝试的一种变体,其中应该只存在用于包含 n
项的 n
节点。在上面我们粗略演示的 radix trie 伪代码中,总共有五个节点:root
(这是一个空节点;它不包含实际值)、root['e']
、root['e']['d']
、root['e']['s']
和 root['i']
。在 PATRICIA trie 中应该只有四个。由于 PATRICIA 是一种二进制算法,因此让我们以二进制形式查看这些前缀可能有何不同。
smile: 0111 0011 0110 1101 0110 1001 0110 1100 0110 0101 0000 0000 0000 0000
smiled: 0111 0011 0110 1101 0110 1001 0110 1100 0110 0101 0110 0100 0000 0000
smiles: 0111 0011 0110 1101 0110 1001 0110 1100 0110 0101 0111 0011 0000 0000
smiling: 0111 0011 0110 1101 0110 1001 0110 1100 0110 1001 0110 1110 0110 0111 ...
让我们考虑按上面显示的顺序添加节点。 smile_item
是这棵树的根。不同之处(粗体显示以便更容易发现)位于"smile"
的最后一个字节,位于第 36 位。到目前为止,我们所有的节点都有相同的前缀。 smiled_node
属于 smile_node[0]
。 "smiled"
和 "smiles"
之间的区别出现在第 43 位,其中"smiles"
有一个“1”位,所以smiled_node[1]
是smiles_node
。
而不是使用NULL
作为分支和/或额外的内部信息来表示搜索何时终止,分支链接回up树的某处,因此当要测试的偏移量减少而不是增加。这是一个这样的树的简单图(虽然 PATRICIA 确实更像是一个循环图,而不是一棵树,正如你将看到的),它包含在下面提到的 Sedgewick 的书中:
一个更复杂的涉及变长密钥的 PATRICIA 算法是可能的,尽管 PATRICIA 的一些技术特性在此过程中丢失了(即任何节点都包含与它之前的节点相同的前缀):
通过这样的分支,有很多好处:每个节点都包含一个值。这包括根。结果,代码的长度和复杂性变得更短,实际上可能更快一些。遵循至少一个分支和最多k
分支(其中k
是搜索关键字中的位数)来定位项目。这些节点很小,因为它们每个只存储两个分支,这使得它们非常适合缓存局部性优化。这些属性使 PATRICIA 成为我迄今为止最喜欢的算法...
我将在此处缩短此描述,以减少即将发生的关节炎的严重程度,但如果您想了解更多关于 PATRICIA 的信息,可以查阅诸如“计算机编程艺术,第 3 卷”之类的书籍由 Donald Knuth 撰写,或 Sedgewick 撰写的任何“your-favourite-language 中的算法,第 1-4 部分”。
【讨论】:
你能帮我理解“基数”这个词的意义吗!我理解我们如何以自然的方式尝试通过允许多个符号/边缘合并为一个边缘来将 TRIE 转换为紧凑的 TRIE。但是,我无法辨别为什么不能将未压缩的 TRIE(简称 TRIE)称为 Radix TRIE。 @ Seb - 非常感谢您对 Radix Tree 上的 ***.com/questions/40087385/… 帖子的反馈。谢谢。 @BuckCherry 我很乐意这样做,但请注意,由于我的电脑被盗,我无法做出充分的回应。 > 这张从 Wikipedia 检索的图表似乎描绘了一个带有(至少)键 'A'、'to'、'tea'、'ted'、'ten' 和 'inn' 的树' 插入:不要挑剔但实际上这里的数字是值,因此还有“i”和“in” @Ervadac ? 请再检查一次【参考方案3】:TRIE: 我们可以有一个搜索方案,而不是将整个搜索关键字与所有现有关键字(例如哈希方案)进行比较,我们还可以比较搜索关键字的每个字符。按照这个想法,我们可以构建一个结构(如下所示),它具有三个现有的键——“dad”、“dab”和“cab em>”。
[root]
...// | \\...
| \
c d
| \
[*] [*]
...//|\. ./|\\... Fig-I
a a
/ /
[*] [*]
...//|\.. ../|\\...
/ / \
B b d
/ / \
[] [] []
(cab) (dab) (dad)
这本质上是一个 M-ary 树,内部节点表示为 [ * ] 和叶节点表示为 [ ]。 这种结构称为trie。每个节点的分支决策可以保持等于字母表中唯一符号的数量,比如 R。对于小写英文字母 a-z,R=26;对于扩展的 ASCII 字母,R=256 和二进制数字/字符串 R=2。
紧凑型 TRIE: 通常,trie 中的节点使用 size=R 的数组,因此当每个节点的边数较少时会导致内存浪费。为了规避内存问题,提出了各种建议。基于这些变化,trie 也被命名为“compact trie”和“compressed trie”。虽然一致的命名法很少见,但紧凑 trie 的最常见版本是通过在节点具有单条边时对所有边进行分组来形成的。使用这个概念,上面(图一)trie 键为“dad”、“dab”和“cab”可以采用以下形式。
[root]
...// | \\...
| \
cab da
| \
[ ] [*] Fig-II
./|\\...
| \
b d
| \
[] []
请注意,“c”、“a”和“b”中的每一个都是其对应父节点的唯一边,因此,它们被合并为单个边“cab”。类似地,“d”和“a”被合并为标记为“da”的单条边。
基数特里:radix 一词在数学中表示数字系统的基础,它本质上表示在该系统中表示任何数字所需的唯一符号的数量。例如,十进制系统是基数十,二进制系统是基数二。使用类似的概念,当我们对通过底层表示系统的唯一符号数量来表征数据结构或算法感兴趣时,我们用术语“基数”来标记这个概念。例如,某些排序算法的“基数排序”。在同一条逻辑中,trie 的所有变体,其特征(例如深度、内存需求、搜索未命中/命中运行时等)都取决于底层的基数字母表,我们可以称它们为基数“trie's”。例如,当使用字母 az 时,未压缩和压缩的 trie,我们可以将其称为基数 26 trie 。任何仅使用两个符号(传统上是“0”和“1”)的 trie 都可以称为基数 2 trie。然而,不知何故,许多文献将术语“Radix Trie”限制为仅用于压缩的 trie。
PATRICIA Tree/Trie 的前奏: 有趣的是,即使作为键的字符串也可以使用二进制字母表示。如果我们假设 ASCII 编码,那么键“dad”可以通过按顺序写入每个字符的二进制表示以二进制形式写入,例如“011001000110000101100100”,通过顺序编写 'd'、'a' 和 'd' 的二进制形式。 使用这个概念,可以形成一个 trie(带有基数二)。下面我们使用一个简化的假设来描述这个概念,即字母“a”、“b”、“c”和“d”来自较小的字母表而不是 ASCII。
图 III 的注意事项: 如前所述,为了便于描述,我们假设一个只有 4 个字母 a,b,c,d 的字母表,它们对应的二进制表示分别为“00”、“01”、“10”和“11”。这样,我们的字符串键“dad”、“dab”和“cab”分别变为“110011”、“110001”和“100001”。对此的尝试将如下图 III 所示(位从左到右读取,就像字符串从左到右读取一样)。
[root]
\1
\
[*]
0/ \1
/ \
[*] [*]
0/ /
/ /0
[*] [*]
0/ /
/ /0
[*] [*]
0/ 0/ \1 Fig-III
/ / \
[*] [*] [*]
\1 \1 \1
\ \ \
[] [] []
(cab) (dab) (dad)
PATRICIA 特里/树: 如果我们使用单边压缩来压缩上述二进制 trie(图 III),它的节点会比上面显示的要少得多,但节点仍然会多于3,它包含的键的数量。 Donald R. Morrison(在 1968 年)发现了一种使用二进制 trie 描述 N 个键的创新方法,仅使用N 个节点,他将这个数据结构命名为 PATRICIA。他的 trie 结构基本上摆脱了单边(单向分支);在这样做的过程中,他还摆脱了两种节点的概念——内部节点(不描述任何键)和叶节点(描述键)。与上面解释的压缩逻辑不同,他的 trie 使用不同的概念,其中每个节点都包含一个指示,即要跳过多少位密钥来做出分支决策。他的 PATRICIA trie 的另一个特点是它不存储键 - 这意味着这种数据结构不适合回答诸如 列出与给定前缀匹配的所有键之类的问题,但是很好用于查找 trie 中是否存在密钥。尽管如此,从那时起,Patricia Tree 或 Patricia Trie 一词已被用于许多不同但相似的含义,例如,表示紧凑的 trie [NIST],或表示具有基数 2 的基数树 [如微妙的WIKI中的方式]等等。
Trie 可能不是 Radix Trie:Ternary Search Trie(又名 Ternary Search Tree)通常缩写为 TST 是一种数据结构(由 J. Bentley 和 R. Sedgewick 提出),看起来非常类似于具有三向分支的 trie。对于这样的树,每个节点都有一个特征字母“x”,因此分支决策是由一个键的字符是否小于、等于或大于“x”来驱动的。由于这个固定的 3 路分支特性,它为 trie 提供了一种节省内存的替代方案,尤其是当 R(基数)非常大时,例如 Unicode 字母表。有趣的是,与 (R-way) trie 不同,TST 的特性不受 R 的影响。例如,TST 的搜索未命中是 ln( N) 相对于 R-way Trie 的 logR(N)。 TST 的内存需求,不像 R-way trie 是 NOT R 的函数。所以我们应该小心地将 TST 称为 radix-trie。我个人认为我们不应该称它为 radix-trie,因为(据我所知)它的特征都不受其底层字母的基数 R 的影响。
【讨论】:
作为根据 Morrison、Sedgewick 和 Knuth 实施 PATRICIA 的人,我可以告诉你你在这里描述的算法(我也试图在我的回答中描述)仍然非常合适 用于回答诸如列出与给定前缀匹配的所有键之类的问题。附言很高兴看到其他人回答:另一个问题:)我喜欢这个解释。 Re“将不适合回答诸如列出与给定前缀匹配的所有键之类的问题”,认真的吗? @Pacerier 当然!经典 PATRICIA 存储一个整数,您可以将其用作数组的索引。将字符串放入数组中。将字符串的基于 0 的数组索引放入 trie 中。使搜索 & 比较 & 位提取函数对对应于整数而不是整数的字符串进行操作,并且如果您的插入函数基于其他函数(应该如此,因为那里有很多重复的逻辑)并且您一切都会好起来的。您也可以使用uintptr_t
作为您的整数,因为通常预期(尽管不是必需的)该类型存在。
您说“许多文献限制使用术语“Radix Trie”仅用于压缩的 trie。”。实际上,除了***,我找不到任何其他参考资料。你找到其他人了吗?
@ wds - 你可能是对的,因为我真的不记得我写这篇文章时提到的资源是什么。一个快速的谷歌搜索让我得到像mathcs.emory.edu/~cheung/Courses/323/Syllabus/Text/trie02.html 或tutorialsdiary.com/radix-trie-patricia-trie-or-compressed-trie 这样的链接,它们基本上指向或(很可能)源自维基/受维基影响。如果我找到任何其他可靠/学术资源,我会在这里发布。【参考方案4】:
在尝试中,大多数节点不存储密钥,而只是在密钥和扩展它的节点之间的路径上的跳跃。这些跃点中的大多数都是必要的,但是当我们存储长词时,它们往往会产生长链的内部节点,每个节点只有一个子节点。这是尝试需要太多空间的主要原因,有时甚至超过 BST。
基数尝试(又名基数树,又名帕特里夏树)基于我们可以以某种方式压缩路径的想法,例如在“中间 t 节点”之后,我们可以在一个节点中包含“hem”或“idote”在一个节点中。
这是比较 trie 与 radix trie 的图表:
原始的 trie 有 9 个节点和 8 条边,如果我们假设一条边有 9 个字节,每个节点有 4 个字节的开销,这意味着
9 * 4 + 8 * 9 = 108 bytes.
右边的压缩树有 6 个节点和 5 条边,但在这种情况下,每条边都带有一个字符串,而不仅仅是一个字符;但是,我们可以通过以下方式简化操作 分别考虑边缘引用和字符串标签。这样一来,我们仍然 每条边计算 9 个字节(因为我们会在 边成本),但我们可以将字符串长度的总和作为第三项添加到最后 表达;所需的总字节数由下式给出
6 * 4 + 5 * 9 + 8 * 1 = 77 bytes.
对于这个简单的 trie,压缩后的版本需要减少 30% 记忆。
【讨论】:
以上是关于trie 和 radix trie 数据结构有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章