【问题标题】:What are the lesser known but useful data structures?什么是鲜为人知但有用的数据结构?
【发布时间】:2010-10-04 18:38:24
【问题描述】:

周围有一些数据结构非常有用,但大多数程序员都不知道。它们是哪些?

每个人都知道链表、二叉树和散列,但例如 Skip listsBloom filters 呢?我想了解更多不常见但值得了解的数据结构,因为它们依赖于伟大的想法并丰富了程序员的工具箱。

PS:我也对像Dancing links 这样巧妙地利用通用数据结构的属性的技术感兴趣。

编辑: 请尝试包含链接到更详细地描述数据结构的页面。另外,尝试在为什么数据结构很酷(正如Jonas Kölker 已经指出的那样)上添加几句话。此外,请尝试为每个答案提供一个数据结构。这将允许更好的数据结构仅根据他们的投票浮动到顶部。

【问题讨论】:

标签: language-agnostic data-structures computer-science


【解决方案1】:

Tries,也称为前缀树或crit-bit trees,已经存在了 40 多年,但仍然相对不为人知。在“TRASH - A dynamic LC-trie and hash data structure”中描述了一个非常酷的 Trie 用法,它结合了 Trie 和哈希函数。

【讨论】:

  • 拼写检查器非常常用
  • 突发尝试也是一个有趣的变体,您只使用字符串的前缀作为节点,否则将字符串列表存储在节点中。
  • Perl 5.10 中的正则表达式引擎自动创建尝试。
  • 根据我的经验,尝试非常昂贵,因为指针通常比字符长,这是一种耻辱。它们仅适用于某些数据集。
  • 既然没有人提到 jQuery,任何 SO 问题,无论主题如何,都是完整的...... jQuery 的创建者 John Resig 有一个有趣的数据结构系列文章,他在其中研究了各种 trie 实现其他:ejohn.org/blog/revised-javascript-dictionary-search
【解决方案2】:

Bloom filterm 位的位数组,最初全部设置为 0。

要添加一个项目,您可以通过 k 哈希函数运行它,该函数将为您提供数组中的 k 个索引,然后您将其设置为 1。

要检查一个项目是否在集合中,请计算 k 索引并检查它们是否都设置为 1。

当然,这给出了一些误报的可能性(根据维基百科,它大约是 0.61^(m/n),其中 n 是插入项目的数量)。假阴性是不可能的。

删除一个项目是不可能的,但您可以实现计数布隆过滤器,由整数数组和增量/减量表示。

【讨论】:

  • 您忘记提及它们与字典的使用 :) 您可以将完整的字典压缩到大约 512k 的布隆过滤器中,就像没有值的哈希表一样
  • Google 在 BigTable 的实现中引用了 Bloom 过滤器。
  • @FreshCode 它实际上可以让您廉价地测试集合中元素的不存在,因为您可以获得误报但绝不会出现误报
  • @FreshCode 正如@Tom Savage 所说,它在检查负数时更有用。例如,您可以将其用作快速且小型(就内存使用而言)的拼写检查器。将所有单词添加到其中,然后尝试查找用户输入的单词。如果你得到一个否定,这意味着它拼写错误。然后你可以运行一些更昂贵的检查来找到最接近的匹配并提供更正。
  • @abhin4v:布隆过滤器通常用于大多数请求可能返回“否”的答案(例如这里的情况),这意味着可以检查少量的“是”答案用较慢的精确测试。这仍然会大大减少平均查询响应时间。不知道 Chrome 的安全浏览是否能做到这一点,但这是我的猜测。
【解决方案3】:

Rope: 这是一个允许廉价前置、子串、中间插入和附加的字符串。我真的只用过一次,但没有其他结构就足够了。常规的字符串和数组前置对于我们需要做的事情来说太昂贵了,而且逆转一切都是不可能的。

【讨论】:

【解决方案4】:

Skip lists 非常整洁。

Wikipedia
跳过列表是一种概率数据结构,基于多个并行的排序链表,其效率与二叉搜索树相当(大多数操作的平均时间为 n 次排序)。

它们可以用作平衡树的替代方案(使用概率平衡而不是严格执行平衡)。它们很容易实现并且比红黑树更快。我认为它们应该在每个优秀的程序员工具箱中。

如果您想深入了解跳过列表,请参阅link to a video 麻省理工学院关于它们的算法介绍讲座。

另外,here 是一个 Java 小程序,直观地展示了跳过列表。

【讨论】:

  • Redis 使用跳过列表来实现“Sorted Sets”。
  • 有趣的旁注:如果您在跳过列表中添加了足够多的级别,那么您基本上会得到一个 B-tree。
【解决方案5】:

Spatial Indices,尤其是R-treesKD-trees,可以有效地存储空间数据。它们适用于地理地图坐标数据和 VLSI 布局和路线算法,有时也适用于最近邻搜索。

Bit Arrays 紧凑地存储各个位并允许快速位操作。

【讨论】:

  • 空间索引对于涉及远程力(如重力)的 N 体模拟也很有用。
【解决方案6】:

Zippers - 将结构修改为具有“光标”的自然概念的数据结构的衍生物——当前位置。这些非常有用,因为它们保证索引不会越界——使用,例如在xmonad window manager 中跟踪哪个窗口已聚焦。

神奇的是,您可以通过applying techniques from calculus 将它们导出为原始数据结构的类型!

【讨论】:

  • 这仅在函数式编程中有用(在命令式语言中,您只需保留指针或索引)。另外,我仍然不明白拉链是如何工作的。
  • @Stefan 的重点是您现在不需要保留单独的索引或指针。
【解决方案7】:

这里有几个:

  • 后缀尝试。适用于几乎所有类型的字符串搜索 (http://en.wikipedia.org/wiki/Suffix_trie#Functionality)。另见后缀数组;它们不如后缀树快,但要小得多。

  • 展开树(如上所述)。他们很酷的原因有三个:

    • 它们很小:您只需要像在任何二叉树中一样的左右指针(不需要存储节点颜色或大小信息)
    • 它们(相对而言)非常容易实现
    • 它们为一大堆“测量标准”提供了最佳的摊销复杂性(每个人都知道 log n 查找时间)。见http://en.wikipedia.org/wiki/Splay_tree#Performance_theorems
  • 堆排序的搜索树:您将一堆 (key, prio) 对存储在一棵树中,这样它是一个关于键的搜索树,并且相对于优先级是堆排序的。可以证明这样一棵树具有独特的形状(而且它并不总是完全挤在左边)。使用随机优先级,它会为您提供预期的 O(log n) 搜索时间 IIRC。

  • 利基之一是具有 O(1) 邻居查询的无向平面图的邻接表。这与其说是一种数据结构,不如说是一种组织现有数据结构的特定方式。这样做的方法如下:每个平面图都有一个度数最多为 6 的节点。选择这样一个节点,将其邻居放入其邻居列表中,将其从图中删除,然后递归直到图为空。当给定一对 (u, v) 时,在 v 的邻居列表中查找 u,并在 u 的邻居列表中查找 v。两者的大小最多为 6,所以这是 O(1)。

通过上述算法,如果 u 和 v 是邻居,你不会在 v 的列表中同时拥有 u 和在 u 的列表中的 v。如果您需要,只需将每个节点的缺失邻居添加到该节点的邻居列表中,但存储您需要查看多少邻居列表以便快速查找。

【讨论】:

  • 堆有序搜索树称为treap。您可以使用这些技巧做的一个技巧是更改节点的优先级,将其推送到更容易删除的树的底部。
  • "堆排序的搜索树称为treap。" -- 在我听说过的定义中,IIRC,treap 是具有 random 优先级的堆排序搜索树。您可以根据应用程序选择其他优先级...
  • 后缀 trie 与更酷的后缀 tree 几乎但不完全相同,后者的边缘有字符串而不是单个字母,并且可以建立在线性时间(!)。此外,尽管渐近缓慢,但在实践中,对于许多任务,后缀数组通常比后缀树快得多,因为它们的大小更小,指针间接更少。喜欢 O(1) 平面图查找 BTW!
  • @j_random_hacker: 后缀数组不是渐近的慢。下面是大约 50 行用于构造线性后缀数组的代码:cs.helsinki.fi/u/tpkarkka/publications/icalp03.pdf
  • @Edward Kmett:我其实看过那篇论文,它在后缀数组构造方面是一个相当大的突破。 (虽然已经知道可以通过“通过”后缀树来进行线性时间构造,但这是第一个不可否认的实用“直接”算法。)但是构造之外的一些操作在后缀数组上仍然渐近缓慢,除非 LCA表也​​建好了。这也可以在 O(n) 中完成,但这样做会失去纯后缀数组的大小和位置优势。
【解决方案8】:

我认为标准数据结构(即无锁队列、堆栈和列表)的无锁替代方案被忽视了。
它们变得越来越重要,因为并发性变得更高优先级,并且比使用互斥锁或锁来处理并发读/写更令人钦佩。

这里有一些链接
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's(通常是挑衅性的)博客有一些关于无锁设计和方法的优秀文章

【讨论】:

  • 无锁替代方案在当今多核、非常并行、可扩展性上瘾的世界中非常重要 :-)
  • 嗯,在大多数情况下,破坏者实际上做得更好。
【解决方案9】:

我认为Disjoint Set 非常适合需要将一堆项目划分为不同的集合并查询成员资格的情况。 Union 和 Find 操作的良好实现导致摊销成本实际上是恒定的(如果我没记错我的数据结构类,则与 Ackermnan 的函数相反)。

【讨论】:

  • 这也被称为“union-find 数据结构”。当我第一次在算法课上了解到这种巧妙的数据结构时,我感到敬畏......
  • union-find-delete 扩展也允许固定时间删除。
  • 我为我的地牢生成器使用了一个不相交集,以确保所有房间都可以通过通道到达:)
【解决方案10】:

Fibonacci heaps

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

【讨论】:

  • 这里的这些家伙使它们与其他堆类型相比具有竞争力:cphstl.dk/Presentation/SEA2010/SEA-10.pdf 有一个相关的数据结构称为配对堆,它更容易实现并且提供了相当好的实际性能。然而,理论分析是部分开放的。
  • 根据我使用斐波那契堆的经验,我发现内存分配的昂贵操作使其效率低于以数组为后端的简单二进制堆。
【解决方案11】:

任何有 3D 渲染经验的人都应该熟悉BSP trees。通常,它是通过构建 3D 场景以便在知道相机坐标和方位的情况下进行渲染的方法。

二进制空间分区 (BSP) 是一种 递归细分a的方法 通过超平面将空间划分为凸集。 这种细分产生了一个 用手段表现场景 树形数据结构称为 BSP 树。

换句话说,它是一种 分解错综复杂的形状 多边形到凸集,或更小 多边形完全由 非反射角(角度小于 180°)。更一般的描述 空间划分,见空间 分区。

最初提出这种方法 在 3D 计算机图形中增加 渲染效率。其他 应用包括执行 形状的几何运算 CAD中的(构造立体几何), 机器人和 3D 中的碰撞检测 电脑游戏和其他电脑 涉及处理的应用程序 复杂的空间场景。

【讨论】:

  • ...以及相关的八叉树和kd树。
【解决方案12】:

Huffman trees - 用于压缩。

【讨论】:

  • 虽然很有趣,但这不是“算法介绍”,这里是贪婪算法类型的主题吗?
【解决方案13】:

看看Finger Trees,特别是如果您是previously mentioned 纯函数数据结构的粉丝。它们是持久序列的功能表示,支持在摊销的常数时间内访问末端,以及在较小部分的大小上以时间对数连接和拆分。

根据the original article

我们的函数式 2-3 指树是 Okasaki (1998) 引入的通用设计技术的一个实例,称为隐式递归减速。我们已经注意到,这些树是他的隐式双端队列结构的扩展,用 2-3 个节点替换对,以提供高效连接和拆分所需的灵活性。

可以使用monoid 参数化手指树,并且使用不同的幺半群会导致树的不同行为。这让手指树可以模拟其他数据结构。

【讨论】:

【解决方案14】:

Circular or ring buffer - 用于流媒体等。

【讨论】:

  • 另外,令人作呕的是,以某种方式获得了专利(至少在用于视频时)。 ip.com/patent/USRE36801
  • 基于阅读链接,我不认为数据结构本身是专利,而是基于它的一些发明。我同意这绝对是一个未被充分利用的数据结构。
【解决方案15】:

我很惊讶没有人提到 Merkle 树(即Hash Trees)。

用于许多情况下(P2P 程序、数字签名),当您只有部分文件可供您使用时,您希望验证整个文件的哈希值。

【讨论】:

    【解决方案16】:

    Van Emde-Boas 树

    我认为了解为什么它们很酷会很有用。一般来说,“为什么”这个问题是最重要的;)

    我的回答是,他们为您提供 O(log log n) 个带有 {1..n} 键的字典,与正在使用的键的数量无关。就像重复减半给你 O(log n),重复 sqrting 给你 O(log log n),这就是 vEB 树中发生的情况。

    【讨论】:

    • 从理论上讲,它们很好。然而,在实践中,要从他们身上获得有竞争力的表现是相当困难的。我知道的论文让它们在 32 位密钥 (citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.2.7403) 上运行良好,但该方法不会扩展到超过 34-35 位左右,并且没有实现。
    • 它们很酷的另一个原因是它们是许多无缓存算法的关键构建块。
    【解决方案17】:

    splay trees怎么样?

    另外,我想到了 Chris Okasaki 的purely functional data structures

    【讨论】:

      【解决方案18】:

      哈希表的一个有趣变体称为Cuckoo Hashing。它使用多个散列函数而不是仅使用 1 个来处理散列冲突。通过从主散列指定的位置移除旧对象并将其移动到备用散列函数指定的位置来解决冲突。 Cuckoo Hashing 允许更有效地使用内存空间,因为您只需 3 个哈希函数就可以将负载因子提高多达 91%,并且仍然具有良好的访问时间。

      【讨论】:

      • 检查 hopscotch hashing 声称更快。
      【解决方案19】:

      min-max heapheap 的变体,它实现了双端优先级队列。它通过对堆属性的简单更改来实现这一点:如果偶数(奇数)级别上的每个元素都小于(大于)所有子节点和孙子节点,则称树是最小最大排序的。级别从 1 开始编号。

      http://internet512.chonbuk.ac.kr/datastructure/heap/img/heap8.jpg

      【讨论】:

      【解决方案20】:

      我喜欢Cache Oblivious datastructures。基本思想是在递归较小的块中布置树,以便许多不同大小的缓存将利用方便放入它们的块。这导致在从 RAM 中的 L1 缓存到从磁盘读取的大块数据的所有内容中高效使用缓存,而无需知道任何这些缓存层大小的具体细节。

      【讨论】:

      • 来自该链接的有趣转录:“关键是 van Emde Boas 布局,以 Peter van Emde Boas 于 1977 年构想的 van Emde Boas 树数据结构命名”
      【解决方案21】:

      Left Leaning Red-Black Trees。 Robert Sedgewick 于 2008 年发布的一个显着简化的红黑树实现(大约需要实现的代码行数的一半)。如果您在思考红黑树的实现时遇到过困难,请阅读此变体。

      与安德森树非常相似(如果不相同)。

      【讨论】:

        【解决方案22】:

        工作窃取队列

        用于在多个线程之间平均分配工作的无锁数据结构 Implementation of a work stealing queue in C/C++?

        【讨论】:

          【解决方案23】:

          Bootstrapped skew-binomial heaps Gerth Stølting Brodal 和 Chris Okasaki:

          尽管名称很长,但它们提供了渐近优化的堆操作,即使在函数设置中也是如此。

          • O(1) 大小,联合,插入,最小
          • O(log n)deleteMin

          请注意,联合需要 O(1) 而不是 O(log n) 时间,这与数据结构教科书中通常介绍的更知名的堆不同,例如 leftist heaps。与Fibonacci heaps 不同的是,这些渐近是最坏的情况,而不是摊销,即使持续使用!

          Haskell 中有 multiple implementations

          它们是 Brodal 和 Okasaki 在 Brodal 提出具有相同渐近性的 imperative heap 之后共同推导出的。

          【讨论】:

            【解决方案24】:
            • Kd-Trees,在实时光线跟踪中使用(以及其他)空间数据结构,其缺点是需要裁剪与不同空间相交的三角形。通常 BVH 速度更快,因为它们更轻量级。
            • MX-CIF Quadtrees,通过将常规四叉树与四边形边缘上的二叉树组合来存储边界框而不是任意点集。
            • HAMT,分层哈希映射,由于涉及的常量,访问时间通常超过 O(1) 哈希映射。
            • Inverted Index,在搜索引擎圈子里很有名,因为它用于快速检索与不同搜索词相关的文档。

            其中大部分(如果不是全部)都记录在 NIST Dictionary of Algorithms and Data Structures

            【讨论】:

              【解决方案25】:

              球树。只是因为它们让人们咯咯笑。

              球树是一种数据结构,用于索引度量空间中的点。 Here's an article on building them. 它们通常用于寻找一个点的最近邻居或加速 k-means。

              【讨论】:

              【解决方案26】:

              不是真正的数据结构;更多的是一种优化动态分配数组的方法,但是 Emacs 中使用的 gap buffers 有点酷。

              【讨论】:

              • 我肯定会认为这是一种数据结构。
              • 对于任何感兴趣的人,这正是支持 Swing 文本组件的 Document(例如 PlainDocument)模型的实现方式;在 1.2 之前,我认为文档模型是直接数组,这导致大文档的插入性能很糟糕;当他们搬到 Gap Buffers 后,世界又恢复了正常。
              【解决方案27】:

              芬威克树。它是一种数据结构,用于在两个给定的子索引 i 和 j 之间对向量中所有元素的总和进行计数。简单的解决方案,从一开始就预先计算总和,不允许更新项目(您必须做 O(n) 工作才能跟上)。

              Fenwick Trees 允许您在 O(log n) 内更新和查询,它的工作原理非常酷且简单。 Fenwick 的原始论文中对此进行了很好的解释,可在此处免费获取:

              http://www.cs.ubc.ca/local/reading/proceedings/spe91-95/spe/vol24/issue3/spe884.pdf

              它的父亲,RQM 树也很酷:它允许你保存向量的两个索引之间的最小元素的信息,它也适用于 O(log n) 更新和查询。我喜欢先教授 RQM,然后再教授 Fenwick Tree。

              【讨论】:

              • 恐怕这是duplicate。也许您想添加到之前的答案?
              • 同样相关的是段树,它对于进行各种范围查询很有用。
              【解决方案28】:

              Van Emde-Boas trees。我什至有一个 C++ implementation 它,最多可容纳 2^20 个整数。

              【讨论】:

              【解决方案29】:

              Nested sets 非常适合在关系数据库中表示树并对其运行查询。例如,ActiveRecord(Ruby on Rails 的默认 ORM)带有一个非常简单的nested set plugin,这使得处理树变得很简单。

              【讨论】:

                【解决方案30】:

                它非常特定于域,但half-edge data structure 非常简洁。它提供了一种迭代多边形网格(面边)的方法,这在计算机图形学和计算几何中非常有用。

                【讨论】:

                  猜你喜欢
                  • 2010-11-07
                  • 2011-06-15
                  • 2011-03-05
                  • 2010-11-30
                  • 1970-01-01
                  • 2012-12-08
                  • 2011-05-05
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多