以下约束的最佳数据结构?

时间:2009-03-01 20:00:22

标签: performance language-agnostic optimization data-structures

以下是我需要的数据结构的一些约束。看起来没有一个共同的数据结构(我会提到我在下面想到的那些)都很适合这些。任何人都可以提出一个我可能没有想到的问题吗?

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

10 个答案:

答案 0 :(得分:4)

当N很小时,带有键+值作为有效载荷的简单数组或单个链表是非常有效。即使N变大也不是最好的。

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

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

可能难以击败。

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

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

答案 1 :(得分:3)

这个建议假设现代cpus:

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

我认为混合结构可能胜过单一结构。

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

一旦你超越了这个数字,你会想要回到一个具有更好的O(N)行为的结构,我建议尝试一个合适的哈希表开始,因为这可能是合理的16 - 几千范围如果你倾向于查找类似的值,更多时候往往会留在更快的缓存中。

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

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

其他选项包括:

  • 处理所有关键值&lt; 256不同地将它们存储在一个字节->结构对的数组中,从而节省了键上的空间(并且可能允许它们在切换到二级结构时保留在那里)这可能会因为需要解压缩而执行得很糟糕数组即时到本机字长。
  • 使用类似于trie的结构,在键的一次执行一个字节。我怀疑这种复杂性会使它在实践中表现良好

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

答案 2 :(得分:2)

哈希表查找的速度和它一样快:

唯一区别于常规数组查找的是散列的计算和(如果你的散列函数足够好,或者你在插入过程中花了足够的时间来生成一个最佳散列函数,这将使你的插入占用O (N))然后基本上是一个数组查找。

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

由于用于散列表的大多数散列函数都是k * c_1%c_2,因此在相当稀疏和/或最优的散列表中与数组查找的差异包括一个间接,两个乘法,一个减法和一个除法(一个使用cpus功能的高效模运算可能会通过减法和乘法以及数组查找来减少它。

它的速度并不快。

答案 3 :(得分:1)

您可能尝试结合两者的优点:如果密钥很小,请将其放入类似数组的数据结构中,该数据结构不会大于预定义的最大密钥。如果密钥很大,请将其放入哈希表中。

答案 4 :(得分:1)

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

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

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

一个非常简单的哈希函数如何展开聚类键:

左边5位(假设是32位整数。如果是64位则再加一位。)是实际包含数据的位数,其余的只是总和(丢弃进位) )将原始密钥切割成您正在使用的多个位的块并加在一起。

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

答案 5 :(得分:1)

您可以考虑Judy Arrays

  

Judy是一个提供a的C库   最先进的核心技术   实现稀疏动态数组。   Judy数组简单地用a表示   空指针。 Judy阵列消耗   只有当它被填充时才有内存   可以成长,以利用所有   如果需要可用的记忆......朱迪   可以取代许多常见数据   结构,如数组,稀疏   数组,哈希表,B树,二进制   树木,线性列表,跳过列表,其他   排序和搜索算法,以及   计算功能。

答案 6 :(得分:0)

我会考虑使用自平衡二叉树而不是简单链接处理哈希冲突的哈希表。您应该可以通过O(1)摊销查找所有密钥和O(logN)的最坏情况查找。由于您的密钥分配有偏差,因此您可能会遇到索引值较低的冲突,并且树查找将真正得到回报。

答案 7 :(得分:0)

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

老实说,即使是标准的哈希表给你带来如此悲惨的表现,我也感到惊讶。也许你可以分析一下它是什么让它如此慢 - 如果它是哈希函数本身,使用一个简单的,如二次幂模数(例如,键和(N-1),其中N是已知的为2 ^ x)无论如何都会支持以0为中心的分布。如果是追逐单独链的dcache miss,请编写一个实现,将存储桶中每个存储桶中的前四个元素存储起来,这样您至少可以快速获取它们。 N = 1有多慢?

我会在存储桶链中存储指向结构而不是结构本身的指针:如果结构很大,那么走一条链就会有许多缓存未命中。另一方面,您可以在单个缓存行上容纳大约16个键/指针对,并且只有在找到正确的元素时才支付未命中。

答案 8 :(得分:0)

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

使用简单模数散列密钥,这是一个整数,与哈希表的每个实例一起存储

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

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

答案 9 :(得分:0)

我会在这里推荐一个Skip list。如果你进入那个java.util.concurrent包有一个很好的实现。