【问题标题】:Best Data Structure for The Following Constraints?以下约束的最佳数据结构?
【发布时间】:2009-03-01 20:00:22
【问题描述】:

以下是我需要的数据结构的一些限制条件。似乎没有一个常见的数据结构(我将在下面提到我想到的那些)适合所有这些。谁能推荐一个我可能没有想到的?

  1. 我需要能够通过无符号整数键执行查找。
  2. 要存储的项目是用户定义的结构。
  3. 这些索引将是稀疏的,通常非常稀疏。常规数组已淘汰。
  4. 每个索引的频率分布不均匀,小索引比大索引更频繁。
  5. N 通常很小,可能不会大于 5 或​​ 10,但我不想过分依赖它,因为它有时可能会大得多。
  6. 常数项很重要。当 N 很小时,我需要非常快速的查找。我已经尝试过通用哈希表,根据经验,它们太慢了,即使 N=1,也意味着没有冲突,这可能是因为涉及的间接数量。不过,我愿意接受有关利用上述其他约束的专用哈希表的建议。
  7. 只要检索时间快,插入时间很重要。即使是 O(N) 的插入时间也足够了。
  8. 空间效率并不是非常重要,但重要的是不要只使用常规数组。

【问题讨论】:

  • 像非常具体的约束一样,提供了一个有趣且可能非常有用的答案。您使用的语言是否假定边界检查可能会有所作为。如果您使用某些具有非原始结构的 .Net 方法,则不会内联哪些颜色的某些东西

标签: performance language-agnostic optimization data-structures


【解决方案1】:

当 N 较小时,使用键 + 值作为有效负载的简单数组或单链表非常高效。就算N变大也不是最好的。

您获得 O(N) 查找时间,这意味着查找需要 k * N 时间。 O(1) 查找需要一个常数K 时间。因此,N < K/k 的 O(N) 性能更好。这里k 非常小,因此您可以获得有趣的 N 值。请记住,大 O 表示法仅描述 large Ns 的行为,而不是您所追求的。对于小桌子

void *lookup(int key_to_lookup)
{
  int n = 0;
  while (table_key[n] != key_to_lookup)
    n++;
  return table_data[n];
}

可能很难被击败。

对您的哈希表、平衡树和简单数组/链表进行基准测试,看看它们在 N 的哪个值开始变得更好。然后你就会知道哪个更适合你。

我差点忘了:将经常访问的键放在数组的开头。鉴于您的描述,这意味着保持排序。

【讨论】:

    【解决方案2】:

    此建议假设现代 CPU 具有:

    • 快速缓存
    • 与时钟速度相比,内存延迟要慢得多。
    • 合理的分支预测(在最新的桌面/服务器处理器中确实令人惊叹)

    我建议混合结构可能胜过单一结构。

    使用简单的基于数组的键值对,具有如上所述的 O(N) 访问,但常数因子非常低,缓存行为非常好。这个初始结构应该很小(可能不大于 16 和可能 8 个值)以避免超出单个高速缓存行。遗憾的是,您需要自己调整一个参数。

    一旦超过了这个数字,你会想要回退到具有更好 O(N) 行为的结构,我建议尝试一个像样的哈希表开始,因为这在 16 到几千范围内可能是合理的如果您倾向于更频繁地查找相似的值,则倾向于留在更快的缓存中。

    如果您还 删除 以及插入,则必须注意不要在两种状态之间来回颠簸。要求计数缩小到“升级”到二级结构的截止值的一半应该可以防止这种情况发生,但请记住,任何确定性的交叉行为都容易受到最坏情况行为输入的影响。
    如果您试图防御恶意输入数据,这可能是一个问题。如果是这样,在决策中使用随机因素可以防止它发生。不过你可能并不关心这个,因为你没有提到它。

    如果您愿意,可以尝试对初始主数组进行排序,允许进行 O(log(N)) 的二进制搜索,但代价是更复杂的搜索代码。我认为简单的数组遍历实际上会击败它,但是您可能希望针对不同的 N 值对此进行基准测试,它可能允许您更长时间地坚持使用主数组,但我认为这是大小的函数缓存行大小大于 O(N) 行为。

    其他选项包括:

    • 以不同的方式处理所有 -> struct 数组对节省键空间(并可能在您切换到二级结构时允许它们保留在那里)这可能会表现不佳需要将数组即时解包到本机字长。
    • 使用类似 trie 的结构,每次执行一个字节的键。我怀疑它的复杂性会让它在实践中表现良好

    我将再次重复 kmkaplan 的非常好的建议。对它进行基准测试,彻底避免微基准测试。在这种分析中,实数可能与理论有惊人的不同......

    【讨论】:

      【解决方案3】:

      哈希表查找的速度几乎可以达到:

      唯一区别于常规数组查找的是哈希的计算和(如果您的哈希函数足够好,或者您在插入期间花费足够的时间来生成最佳哈希函数,这将使您的插入花费 O (N)) 然后本质上是一个数组查找。

      基本上因为它可能会发生(除非您使用最佳哈希函数),您必须重新哈希或遵循一个非常小的链表。

      由于用于哈希表的大多数哈希函数都是 k*c_1 % c_2,因此在相当稀疏和/或最佳哈希表中查找数组的区别在于一个间接、两个乘法、一个减法和一个除法(一个使用 cpus 功能的高效模实现可能会通过减法和乘法来减少它)和数组查找。

      它根本不会比这更快。

      【讨论】:

      • 对不起,但他特别提到了常数因子和哈希表实际上可能非常慢,因为它们需要检查哈希和相等性,如果哈希很差,会严重降低,不允许使用特定于域的知识,like 存在于此处,并且可能具有较差的缓存行为
      • "如果散列很差,会严重降级,不允许使用特定领域的知识" 如果散列在你知道大量使用一小部分无符号整数的情况下很差 - 那么你可能选择了错误的哈希?
      • 为特定的数据分布设计一个好的散列函数很难(我们去购物吧)正常的雪崩卡方测试并不是一个很好的指标,因为分布域非常紧凑。
      • 哦,具有合理负载因子的哈希表总是会浪费一些空间。在小型集合中,这可能是填充一个或两个缓存行之间的区别。这可能会使性能数据不堪重负...
      【解决方案4】:

      您可能会尝试将两全其美的优点结合起来:如果键很小,则将其放入一个不会增长到大于预定义最大键的类似数组的数据结构中。如果 key 很大,把它放到 hashtable 中。

      【讨论】:

        【解决方案5】:

        对于所描述的问题,我能看到的唯一解释是哈希函数太复杂了。我倾向于采用两阶段方法:

        1) 对于小键,一个简单的指针数组。没有哈希或任何东西。

        2) 对于大于您分配的表大小的键:

        如何使用一个非常简单的散列函数来分散聚集的键:

        左序 5 位(我假设是 32 位整数。如果是 64 位,则再添加一位。)是实际包含数据的位数,其余的只是总和(丢弃进位) 的原始密钥切成块,无论您为此目的使用多少位,然后加在一起。

        请注意,有效位的数量可以部分预先计算——构建一个 64k 的高位值表。如果高位字不为零,则将其用作表的索引并加16,否则将低位字用作索引。对于 64 位整数,您显然必须使用 4 步而不是 2 步。

        【讨论】:

          【解决方案6】:

          你可以考虑Judy Arrays:

          Judy 是一个 C 库,它提供了一个 最先进的核心技术, 实现一个稀疏动态数组。 Judy 数组简单地用 a 声明 空指针。一个 Judy 数组消耗 仅当它被填充时内存,然而 可以成长以利用所有 如果需要,可用内存......朱迪 可以替换很多常用数据 结构,例如数组,稀疏 数组、哈希表、B-树、二进制 树,线性列表,跳过列表,其他 排序和搜索算法,以及 计数函数。

          【讨论】:

          • 请注意,这些都相当依赖于某些语言功能才能获得完整的实用程序。尤其是指针(空的 judy 数组是一个空指针)。
          • 另外,我还没有发现任何最近对 x86 cpu 的严重性能分析,最初的调整是在 HP cpu 上完成的,缓存实现有所不同。较早的分析:nothings.org/computer/judy
          • 顺便说一句,我同意 Judy 数组的工作效果可能不如宣传的那么好,但它们确实(据称)满足了许多要求,所以如果没有其他方法可行,可能值得一试。
          【解决方案7】:

          我会考虑使用自平衡二叉树而不是简单的链接来处理哈希冲突的哈希表。您应该能够对所有键进行 O(1) 分期查找和 O(logN) 的最坏情况查找。由于您的键分布是倾斜的,因此您很可能会与索引值较低的冲突,并且树查找将在那里真正得到回报。

          【讨论】:

          • 为什么会这样?至少使用 TRS1 哈希图您可以指定您的哈希函数?
          • 由于它们是整数键,我假设是一个简单的哈希函数(如模)。使用更复杂的哈希函数可以解决这个问题。
          • 我的假设也是基于他所说的哈希表经验。
          • 如果他使用的分布是低值支配一个非常简单的函数,如 k % (mapsize*2) 实际上应该表现得很好?这实际上会将哈希图更改为数组索引,其中高键值弹出到标准范围内。
          【解决方案8】:

          如果您的 N 通常很小,您可以尝试使用二次探测而不是单独链接的开放寻址哈希。如果您遇到罕见的 N 情况会溢出它,您需要从 32 的初始大小重新分配到更大的宽度。如果您可以让整个结构适合几个缓存行,线性探测或布谷鸟哈希将为您提供良好的性能。

          老实说,我很惊讶即使是标准哈希表也会给您带来如此糟糕的性能。也许您可以对其进行剖析以查看导致它如此缓慢的原因-如果它是哈希函数本身,请使用一个简单的函数,例如二的幂模(例如,已知 N 的 key & (N-1)为 2^x),这将有利于以 0 为中心的分布。如果是追逐单独链的 dcache 未命中,请编写一个实现,将每个存储桶中的前四个元素存储在存储桶本身中,以便您至少快速获取它们。 N=1 有多慢?

          我会将指向结构的指针而不是结构本身存储在存储桶链中:如果结构很大,那么遍历它们的链将有很多缓存未命中。另一方面,您可以在单个缓存行上放置大约 16 个键/指针对,并且仅在找到正确元素时才为未命中付费。

          【讨论】:

            【解决方案9】:

            这是散列函数的一般概念。你说插入可能很昂贵。

            哈希键,它是一个整数,具有简单的模数,与哈希表的每个实例一起存储

            如果插入会导致冲突,请通过计算合理范围内每个模数可能发生的冲突数来重新优化哈希表,例如,通过某个常数倍数计算映射中的元素数.

            显然,如果您最小化分配,您的插入实际上会变得非常昂贵,大约为 O(n^2),但是您可能能够通过单个整数除法和单个指针间接实现查找,而且您知道,因为您在插入时计算它,最坏情况的查找将是什么。

            【讨论】:

              【解决方案10】:

              我会在这里推荐Skip list。如果你喜欢的话,java.util.concurrent 包有一个很好的实现。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2017-09-26
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-04-02
                • 1970-01-01
                相关资源
                最近更新 更多