有哪些不太为人知但很有用的数据结构?

793

有一些数据结构对于大多数程序员来说很有用,但是却不为人所知。它们都是哪些呢?

每个人都知道链表、二叉树和哈希表,但是例如跳表布隆过滤器呢?我想了解更多不太常见但值得知道的数据结构,因为它们依赖于巨大的思想,并且丰富了程序员的工具箱。

PS:我还对像舞蹈链这样聪明地利用常见数据结构属性的技术感兴趣。

编辑: 请尝试在描述数据结构的页面中包含链接。此外,请尽量添加一些关于为什么某个数据结构很酷的词语(正如Jonas Kölker 已经指出的)。此外,请每次只提供一个数据结构的答案。这将使更好的数据结构根据它们的投票结果浮现在最上面。


16
http://zh.wikipedia.org/wiki/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%88%97%E8%A1%A8 + http://zh.wikipedia.org/wiki/%E4%BA%8C%E5%86%B3%E5%AE%9A%E5%9B%BE - Fanatic23
83个回答

270

12
常被拼写检查器所使用。 - Steven A. Lowe
根据我的经验,尝试(tries)通常非常昂贵,因为指针通常比字符长得多,这很遗憾。它们只适用于某些数据集。 - Joe
@Joe:这些都是天真的trie实现中存在的问题。在实践中,您通常具有比单个字符更高的分支因子,并且通常会压缩树中的行,以便将字符序列(如音节)存储在单个节点中。 - J D
18
既然无论主题如何,没有哪个SO问题是完整的,没有人提到jQuery......John Resig是jQuery的创造者,他有一系列有趣的数据结构帖子,在其中他研究了各种trie实现等:http://ejohn.org/blog/revised-javascript-dictionary-search/ - Oskar Austegard
这些是一些针对 tries 数据结构的真正有趣的应用场景。 http://ejohn.org/blog/javascript-trie-performance-analysis/ http://ejohn.org/blog/revised-javascript-dictionary-search/ http://stevehanov.ca/blog/index.php?id=120 - Pete Brumm
显示剩余3条评论

231

布隆过滤器:由m位比特位组成的位数组,最开始所有的比特位都为0。

添加一个元素时,需要经过k个散列函数计算,得出k个在数组中对应的索引,并将它们设置为1。

检查一个元素是否在集合中时,同样需要计算k个索引并检查它们是否都被设置为1。

当然,这会导致一定的误报概率(根据维基百科的说法,误报概率约为0.61^(m/n),其中n为插入的元素数量)。但不会发生漏报。

删除元素是不可能的,但可以使用计数型布隆过滤器,用整数数组表示并进行增量/减量操作。


20
你忘了提到布隆过滤器与字典的使用 :) 像哈希表一样,你可以将一个完整的字典压缩成大约512k的布隆过滤器,但不包括值。 - Chris S
8
谷歌在实现BigTable中引用了Bloom过滤器的使用。 - Brian Gianforcaro
16
它实际上让你以低廉的价格测试集合中元素的缺失,因为你可能会得到错误的肯定结果,但永远不会得到否定结果。 - Tom Savage
26
正如@Tom Savage所说,当检查否定情况时,这种方法更加有用。例如,你可以将它用作快速、占用内存小的(指内存使用量小)拼写检查器。将所有单词添加到其中,然后尝试查找用户输入的单词。如果返回否定结果,则表示该单词拼写错误。接着,你可以运行一些更昂贵的检查来查找最接近的匹配,并提供更正建议。 - lacop
5
布隆过滤器通常用于大部分请求都可能返回“否”的情况下(如此处所示),这意味着可以使用较慢的精确测试来检查少量“是”答案。这仍然会显著降低平均查询响应时间。我不知道Chrome的安全浏览是否使用了这种方法,但这只是我的猜测。 - j_random_hacker
显示剩余6条评论

139

Rope是一种字符串数据结构,它使得在字符串的前部添加、截取子串、中间插入和追加操作变得更加便宜。我只使用过一次,但没有其他数据结构可以胜任。常规字符串和数组的前置操作对我们需要完成的任务来说太昂贵了,而将所有内容反转则不可行。


15
SGI STL(1998年版本)中有一个实现:http://www.sgi.com/tech/stl/Rope.html - quark
2
我最近为Java写了类似于这个的东西,但不知道它叫什么——性能非常出色:http://code.google.com/p/mikeralib/source/browse/trunk/Mikera/src/mikera/persistent/Text.java - mikera
绳索(Rope)是相当罕见的:https://dev59.com/YnI-5IYBdhLWcg3wbHq- - Will
在Good Math, Bad Math上有一篇关于绳索的好文章:http://scienceblogs.com/goodmath/2009/01/ropes_twining_together_strings.php - Kuroki Kaze
6
Mikera的链接已经失效了,这里是最新链接 - aptwebapps

126

跳表是一种很棒的数据结构。

维基百科
跳表是一种基于多个平行排序链表的概率型数据结构,其效率可媲美二叉搜索树(大多数操作的平均时间为对数级别)。

它们可以作为平衡树的替代品(使用概率平衡而不是严格平衡),易于实现且比如红黑树等其他数据结构更快。我认为每个优秀的程序员都应该掌握这项技能。

如果你想深入了解跳表,这里有麻省理工学院算法导论讲座关于跳表的视频链接。

此外,这里有一个Java applet,可以通过可视化演示跳表的操作:链接


2
Redis使用跳表来实现“有序集合”。 - antirez
有趣的一点是:如果你在跳表中添加足够多的层级,实际上就得到了一棵B树。 - Riyad Kalla

91

空间索引,特别是R树KD树,可以高效地存储空间数据。它们适用于地理地图坐标数据、VLSI布线算法,有时也可用于最近邻搜索。

位数组紧凑地存储单个位,并允许快速位操作。


6
空间索引对于涉及长程力(如重力)的N体模拟也非常有用。 - Justin Peel

86
Zippers - 是一种数据结构的派生体,可以修改数据结构以便拥有自然的“光标” -- 当前位置。 这些非常有用,因为它们确保索引不会超出边界 -- 例如在xmonad窗口管理器中用于跟踪哪个窗口处于焦点状态。

令人惊奇的是,您可以通过应用微积分技术到原始数据结构的类型上来推导它们!


2
这仅在函数式编程中有用(在命令式语言中,只需保留指针或索引)。说实话,我还不太明白如何真正使用Zippers。 - Stefan Monov
4
重点是现在你不需要保留单独的索引或指针。 - Don Stewart

69

这里有几个:

  • 后缀树。对于几乎所有类型的字符串搜索都很有用(http://en.wikipedia.org/wiki/Suffix_trie#Functionality)。也可以看一下后缀数组;它们不如后缀树快,但要小得多。

  • Splay树(如上所述)。它们酷的原因有三个:

    • 它们很小:你只需要像在任何二叉树中一样使用左右指针(无需存储节点颜色或大小信息)
    • 它们(相对而言)非常容易实现
    • 它们为“测量标准”提供了最优摊销复杂度(每次查找时间为log n),请参见http://en.wikipedia.org/wiki/Splay_tree#Performance_theorems
  • 堆排序搜索树:您可以在树中存储一组(键、优先级)对,使其成为关于键的搜索树,并且与优先级有序。可以证明这样的树具有唯一的形状(并且它并不总是完全向左上方填充)。使用随机优先级,它给出了预期的O(log n)查找时间。

  • 一个小众的数据结构是具有O(1)邻居查询的无向平面图的邻接表。这不是一种数据结构,而是组织现有数据结构的一种特定方式。这里是如何做到的:每个平面图都有一个度数不超过6的节点。选择这样的节点,将其邻居放入其邻居列表中,从图中删除它,并递归直到图为空。当给出一对(u,v)时,在v的邻居列表中查找u,在u的邻居列表中查找v。两者大小均不超过6,因此复杂度为O(1)。

根据上述算法,如果u和v是邻居,则u不会出现在v的列表中,v也不会出现在u的列表中。如果需要这个功能,只需将每个节点缺少的邻居添加到该节点的邻居列表中,但要存储需要查找多少邻居列表以实现快速查找。

堆有序搜索树被称为Treap。你可以用它们的一个技巧是改变节点的优先级,将其推到树的底部,这样更容易删除。 - paperhorse
1
堆有序搜索树被称为Treap。在我听到的定义中,如果我没记错的话,Treap是具有随机优先级的堆有序搜索树。根据应用程序,您可以选择其他优先级... - Jonas Kölker
2
一个后缀trie几乎但并不完全相同于更酷的后缀tree,它在其边缘上具有字符串而不是单个字母,并且可以在线性时间内构建(!)。此外,尽管渐近速度较慢,但在实践中,由于其较小的大小和较少的指针间接引用,后缀数组对于许多任务通常比后缀树快得多。顺便说一句,我喜欢O(1)平面图查找! - j_random_hacker
@j_random_hacker:后缀数组不会渐进地变慢。这里有大约50行代码用于线性后缀数组构建:http://www.cs.helsinki.fi/u/tpkarkka/publications/icalp03.pdf - Edward Kmett
1
@Edward Kmett:事实上,我已经阅读了那篇论文,它在后缀数组构建方面取得了重大突破。(尽管通过后缀树“间接”地进行线性时间构建已经是可能的,但这是第一个无可争议的实用“直接”算法。)但是,在构建之外的一些操作仍然在后缀数组上渐进地变慢,除非还建立了LCA表。这也可以在O(n)内完成,但这样做会失去纯后缀数组的大小和局部性优势。 - j_random_hacker
@j_random_hacker:好的,说得对。如果没有上下文,我就不清楚你指的是哪种渐近性了。 - Edward Kmett

65

我认为锁定数据结构的无锁替代方案,例如无锁队列、栈和列表,被忽视了很多。
随着并发性变得更加重要,它们变得越来越相关,并且比使用互斥锁或锁来处理并发读/写要更可贵。

以下是一些链接:
http://www.cl.cam.ac.uk/research/srg/netos/lock-free/
http://www.research.ibm.com/people/m/michael/podc-1996.pdf [链接到PDF]
http://www.boyet.com/Articles/LockfreeStack.html

Mike Acton的(常常具有挑衅性)博客中有一些关于无锁设计和方法的优秀文章。


在当今这个多核、高度并行且追求可扩展性的世界中,无锁替代方案非常重要 :-) - earino
嗯,在大多数情况下,破坏者确实做得更好。 - deadalnix

55

我认为不相交集合在需要将一堆项目分成不同集合并查询成员身份的情况下非常方便。好的Union和Find操作实现会导致摊销成本基本保持不变(如果我还记得我的数据结构课程正确,这是Ackerman函数的倒数)。


8
这也被称为“并查集数据结构”。当我在算法课上第一次学习这种巧妙的数据结构时,我感到非常惊叹。 - BlueRaja - Danny Pflughoeft
并查集删除扩展还允许常数时间的删除操作。 - Peaker
4
我在我的地牢生成器中使用了不相交集合,以确保所有房间都可以通过通道到达 :) - goldenratio

52

斐波那契堆

它们被用于一些最快的已知算法(在渐近意义下)来解决许多图相关问题,例如最短路径问题。Dijkstra算法在使用标准二叉堆时的时间复杂度为O(E log V);使用斐波那契堆可以将时间复杂度提高到O(E + V log V),这对于密集图来说是一个巨大的加速。不幸的是,它们具有很高的常数因子,通常使它们在实践中不切实际。


这些人在这里让它们与其他堆种类相比具有竞争力:http://www.cphstl.dk/Presentation/SEA2010/SEA-10.pdf还有一种相关的数据结构叫做配对堆,它更容易实现,并且提供了相当不错的实际性能。然而,理论分析部分尚未解决。 - Manuel
从我使用斐波那契堆的经验来看,我发现昂贵的内存分配操作使其比由数组支持的简单二叉堆效率低下。 - jutky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接