【问题标题】:Are there O(1) random access data structures that don't rely on contiguous storage?是否存在不依赖连续存储的 O(1) 随机访问数据结构?
【发布时间】:2010-10-02 03:08:11
【问题描述】:

经典的 O(1) 随机访问数据结构是数组。但是数组依赖于所使用的编程语言来支持有保证的连续内存分配(因为数组依赖于能够获取基的简单偏移量来查找任何元素)。

这意味着语言必须具有关于内存是否连续的语义,而不是将其作为实现细节。因此,可能需要一个具有 O(1) 随机访问但不依赖于连续存储的数据结构。

有这种事吗?

【问题讨论】:

  • 很酷的问题。像其他人一样,我立即想到了哈希表,然后,当然,意识到这只是一个桶数组。嗯。
  • 我之所以想到它是因为我对 PL 设计很感兴趣,并且正在考虑某些语言如何具有非常精确和明显的空间语义(例如 C)而其他语言如何没有(我在想Haskell 和空间泄漏——但我不知道 Haskell 是否没有指定或者只是不直观/难)

标签: algorithm arrays memory data-structures complexity-theory


【解决方案1】:

哈希表?

编辑: 数组是O(1) 查找,因为a[i] 只是*(a+i) 的语法糖。换句话说,要获得O(1),您需要一个指向每个元素的直接指针或一个易于计算的指针(以及您要查找的内存是为您的程序提供的良好感觉)。在没有指向每个元素的指针的情况下,如果没有连续的内存,就不可能有一个易于计算的指针(并且知道内存是为您保留的)。

当然,有一个 Hashtable 实现是合理的(如果很糟糕),其中每个查找的内存地址只是 *(a + hash(i)) 不是在数组中完成的,即如果你有这种控制,则在指定的内存位置动态创建.. 关键是最有效的实现将是一个底层数组,但当然可以在其他地方进行点击以进行 WTF 实现,它仍然可以让您进行恒定时间查找。

编辑2: 我的观点是,数组依赖于连续内存,因为它是语法糖,但 Hashtable 选择数组是因为它是最好的实现方法,而不是因为它是必需。当然,我一定是阅读 DailyWTF 太多了,因为我正在想象重载 C++ 的数组索引运算符,以便以同样的方式在没有连续内存的情况下也这样做..

【讨论】:

  • 大多数实现仍然使用连续存储。
  • 对,因为它更容易,但没有理由必须是连续的,对吧?
  • 你能实现一个没有数组的 O(1) 随机访问哈希表吗?换句话说,对我来说,哈希表似乎是一种决定将元素放入数组中的位置的方法。
  • 有趣的编辑,我没有想过为了能够在指定的内存位置创建条目而牺牲连续存储。
  • 作为 DailyWTF 的狂热读者,我只知道给猫剥皮的方法不止一种,即使猫不需要剥皮。但是,在指定位置创建条目的问题是知道您是否分配了该特定内存位置..
【解决方案2】:

分布式哈希映射具有这样的属性。好吧,实际上,不完全是,基本上哈希函数会告诉您要查看的存储桶,在那里您可能需要依赖传统的哈希映射。它并不能完全满足您的要求,因为包含存储区域/节点的列表(在分布式场景中)通常是哈希映射(本质上使其成为哈希表的哈希表),尽管您可以使用其他一些算法,例如如果存储区域的数量已知。

编辑:
忘了一点花絮,您可能希望对不同的级别使用不同的哈希函数,否则您最终会在每个存储区域内得到很多相似的哈希值。

【讨论】:

    【解决方案3】:

    除了哈希表,您还可以有一个两级数组数组:

    • 将前 10,000 个元素存储在第一个子数组中
    • 将下一个 10,000 个元素存储在下一个子数组中

    【讨论】:

    • 这就是他们在硬件中做虚拟内存的方式,本质上。
    • 你可以称之为基数树(好吧,页表确实是基数树)。,即使不是那么灵活:-D
    【解决方案4】:

    trie 怎么样,其中键的长度限制为某个常数 K(例如,4 个字节,因此您可以使用 32 位整数作为索引)。然后查找时间将是 O(K),即 O(1) 与非连续内存。对我来说似乎很合理。

    回想一下我们的复杂度类,不要忘记每个 big-O 都有一个常数因子,即 O(n) + C,这种方法肯定会比实际数组有一个大得多的 C。

    编辑:实际上,现在我考虑一下,它是 O(K*A),其中 A 是“字母”的大小。每个节点必须有一个最多包含 A 个子节点的列表,该列表必须是一个链表,以保持实现不连续。但是 A 仍然是常数,所以它仍然是 O(1)。

    【讨论】:

    • 听起来不错。不过,在使用较大的数组时,您可能会遇到内存使用问题。
    • 完全。毕竟它真的是“O(1) + C”。与真正的数组相比,我猜 C 在这里要大一些 :) 再说一次,它似乎适用于稀疏数组。
    【解决方案5】:

    当然,您在这里谈论的不是连续的内存存储,而是索引包含数据结构的能力。通常在内部将动态数组或列表实现为指针数组,其中包含内存中其他位置每个元素的实际内容。这样做的原因有很多——尤其是它使每个条目的大小都不同。正如其他人所指出的,大多数哈希表实现也依赖于索引。我想不出一种方法来实现不依赖于索引的 O(1) 算法,但这至少意味着索引的连续内存。

    【讨论】:

      【解决方案6】:

      可以不为整个数据分配内存块,而只为数据片段的引用数组分配内存块。这会显着增加必要的连续内存长度。

      另一种选择,如果元素可以用键标识,并且这些键可以唯一地映射到可用的内存位置,则可以不将所有对象连续放置,在它们之间留出空格。这需要控制内存分配,因此当您必须为第一优先对象使用该内存位置时,您仍然可以分配空闲内存并将第二优先对象重新定位到其他地方。不过,它们在子维度中仍然是连续的。

      我能说出一个常见的数据结构来回答您的问题吗?没有。

      【讨论】:

      • “增加”是指减少?顺便提一句。 c++ std::deque 可以通过按照您描述的方式分配小的不连续的连续内存块来实现。
      【解决方案7】:

      实际上,对于小数据集使用连续存储不是问题,对于大数据集 O(log(n)) 和 O(1) 一样好;常数因素更为重要。

      事实上,对于非常大的数据集,O(root3(n)) 随机访问是您在 3 维物理宇宙中可以获得的最佳选择。

      编辑: 假设 log10 和 O(log(n)) 算法在一百万个元素处的速度是 O(1) 算法的两倍,那么它们将需要一万亿个元素才能变成偶数,而 O(1) 算法需要五亿个元素速度提高一倍——甚至比地球上最大的数据库还要快。

      所有当前和可预见的存储技术都需要一定的物理空间(我们称之为 v)来存储数据的每个元素。在 3 维宇宙中,这意味着对于 n 个元素,至少一些元素与进行查找的位置之间存在 root3(n*v*3/4/pi) 的最小距离,因为这是体积为 n*v 的球体。然后,光速给出了对这些元素的访问时间的 root3(n*v*3/4/pi)/c 的物理下限 - 这就是 O(root3(n)),不管是什么花哨的算法你用。

      【讨论】:

      • 为什么对于大型数据集 O(log(n)) 也一样好?因为它和常数之间的差异会随着时间的推移而减小?另外,你能解释一下 O(root3(n)) 部分吗?我猜这与实际电路布线的物理特性有关吗?链接?
      • 如果每个位占用恒定空间,则卷缩放为 n**3。不过,我认为值得注意的是,如果你尝试将其放大到足够大,你最终会得到一个黑洞。 :-)
      【解决方案8】:

      除了其他人注意到的明显嵌套结构到有限深度之外,我不知道具有您描述的属性的数据结构。我同意其他人的观点,即通过精心设计的对数数据结构,您可以拥有非连续内存,并且可以快速访问适合主内存的任何数据。

      我知道一个有趣且密切相关的数据结构:

      • Cedar ropes 是不可变字符串,提供对数而不是恒定时间访问,但它们确实提供了恒定时间连接操作和字符的有效插入。该论文受版权保护,但有Wikipedia explanation

      这种数据结构足够高效,您可以使用它来表示一个大文件的全部内容,并且实现非常聪明,可以在磁盘上保留位,除非您需要它们。

      【讨论】:

        【解决方案9】:

        有点好奇:hash trie 通过在内存中交错不发生冲突的 trie 节点的键数组来节省空间。也就是说,例如,如果节点 1 具有键 A、B、D 而节点 2 具有键 C、X、Y、Z,那么您可以同时为两个节点使用相同的连续存储。它被推广到不同的偏移量和任意数量的节点; Knuth 在Literate Programming 的最常用词程序中使用了这个。

        因此,这使得 O(1) 可以访问任何给定节点的密钥,而无需为其保留连续存储,尽管对所有节点共同使用连续存储。

        【讨论】:

          【解决方案10】:

          因此,可能需要一个具有 O(1) 随机访问的数据结构,但 不依赖于连续存储。

          有这种事吗?

          不,没有。证明草图:

          如果您对连续块大小有限制,那么显然您必须使用间接访问数据项。具有有限块大小的固定间接深度只能得到一个固定大小的图(尽管它的大小随深度呈指数增长),因此随着数据集的增长,间接深度将增长(仅对数,但不是 O(1) )。

          【讨论】:

          • OP 从未提到数据结构必须能够无限增长
          • @Jason - O() 表示法必须根据定义考虑无限增长。对于固定的数据大小,一切都是 O(1)。
          【解决方案11】:

          一些伪 O(1) 答案-

          VList 是 O(1) 访问(平均而言),并且不需要整个数据是连续的,尽管它确实需要在小块中连续存储。其他基于数值表示的数据结构也是摊销 O(1)。

          数字表示应用与 radix sort 相同的“作弊”,产生 O(k) 访问结构 - 如果索引有另一个上限,例如它是 64 位 int,则为二进制树,其中每个级别对应于索引中的一个位需要一个恒定的时间。当然,对于可以与结构一起使用的任何 N,常数 k 都大于 lnN,因此它不太可能是性能改进(如果 k 仅比 lnN 大一点并且实现基数排序更好地利用了平台)。

          如果你使用堆实现中常见的二叉树表示,你最终会回到一个数组。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2011-06-17
            • 2014-04-12
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多