【问题标题】:Data structure for O(log N) find and update, considering small L1 cacheO(log N) 查找和更新的数据结构,考虑到小型 L1 缓存
【发布时间】:2012-05-11 12:56:43
【问题描述】:

我目前正在处理一个遇到性能问题的嵌入式设备项目。分析找到了一个我想消除的 O(N) 操作。

我基本上有两个数组int A[N]short B[N]A 中的条目是唯一的,并按外部约束排序。最常见的操作是检查特定值a 是否出现在A[] 中。不太常见但仍然常见的是对A[] 元素的更改。新值与之前的值无关。

由于最常见的操作是查找,这就是B[] 的用武之地。它是A[] 中索引的排序数组,因此A[B[i]] < A[B[j]] 当且仅当i<j。这意味着我可以使用二进制搜索在A 中找到值。

当然,当我更新A[k]时,我必须在B中找到k并将其移动到一个新的位置,以保持搜索顺序。因为我知道A[k] 的旧值和新值,所以这只是B[] 的子集memmove()k 的旧位置和新位置之间的一个memmove()。这是我需要修复的 O(N) 操作;因为A[k] 的新旧值基本上是随机的,所以我平均移动 N/2 N/3 个元素。

我使用[](int i, int j) { return A[i] < A[j]; } 作为谓词查看了std::make_heap。在这种情况下,我可以轻松地将B[0] 指向A 的最小元素,并且更新B 现在是一个廉价的O(log N) 重新平衡操作。但是,我通常不需要 A 的最小值,我需要查找是否存在任何给定值。现在在B 中进行 O(N log N) 搜索。 (我的 N 个元素中有一半位于堆深度 log N,四分之一位于 (log N)-1 等),这与直接在 A 中的愚蠢 O(N) 搜索相比没有任何改进。

考虑到std::set 有 O(log N) 的插入和查找,我想说应该可以在此处获得相同的更新和查找性能。但是我该怎么做呢? B 需要另外订购吗?不同的类型?

B 当前是short [N],因为AB 加起来大约是我的CPU 缓存大小,而我的主内存要慢很多。从 6*N 到 8*N 字节不是很好,但如果我的查找和更新都达到 O(log N) 仍然可以接受。

【问题讨论】:

  • 确切地说:移动2个随机元素平均需要移动N/3个元素,而不是N/2个(第一个元素之前和第二个之后的元素不需要移动)。不过,这是一个非常有趣的问题!
  • 也许我不明白,但我认为您可以保持数组排序并进行二进制搜索。当您需要排序时,这将花费 nlogn 并在您需要搜索时记录 n。
  • N 大概有多大?另外,为什么包含搜索 O(N log N) 而不仅仅是 O(N)?在堆中搜索也只是 O(N)(你甚至不必按堆顺序)
  • @memo:这就是为什么我说A的顺序是外部强加的。我使用B 创建了我可以控制的第二个订单。但是,即使我可以重新排序 A 而不是 B,它仍然是一个 O(N) 操作(在 A[old]A[new] 之间向上或向下移动一个位置)。我的目标是 O(log N)。

标签: c++ algorithm complexity-theory


【解决方案1】:

如果唯一的操作是 (1) 检查值 'a' 是否属于 A 和 (2) 更新 A 中的值,为什么不使用 哈希表 代替排序数组 B?特别是如果 A 的大小没有增长或缩小并且值只会改变,这将是一个更好的解决方案。哈希表不需要比数组更多的内存。 (或者,不应该将 B 更改为堆,而应将其更改为二叉搜索树,这可能是自平衡的,例如 splay 树或红黑树。但是,树需要 额外的内存,因为左右指针。)

将内存使用量从 6N 增加到 8N 字节的实用解决方案是瞄准 50% 填充的哈希表,即使用由 2N 个短裤数组组成的哈希表。我建议实施 Cuckoo Hashing 机制(参见http://en.wikipedia.org/wiki/Cuckoo_hashing)。进一步阅读这篇文章,你会发现通过使用更多的散列函数,你可以获得超过 50% 的负载因子(即将内存消耗从 8N 推向 7N)。 "仅使用三个哈希函数将负载增加到 91%。"

来自维基百科:

Zukowski 等人的一项研究。已经表明杜鹃散列是很多 小型缓存驻留哈希表比链式哈希更快 现代处理器。 Kenneth Ross 展示了桶化版本的 杜鹃散列(使用包含多个存储桶的变体 key) 对于大哈希也比传统方法更快 表,当空间利用率很高时。的表现 Askitis 进一步研究了桶化的杜鹃哈希表, 与其他散列方案相比,它的性能。

【讨论】:

  • 但是为了使用哈希表获得良好的性能,它(哈希表)的大小需要为元素数量的~*2-4(负载平衡)......否则 -冲突会太频繁,搜索和更新都会衰减到O(n)
  • 是的,无论哈希表是如何实现的,哈希表都比简单数组占用更多的内存...问题:您是否有关于查询 'a 存在的频率的统计信息' 它存在以及不存在的频率 --- 哪一种情况更常见,或者它们都一样常见?
  • @antti.huima:N=1000 我看到 60-90% 的存在。取决于所使用的数据集和准确的用户查询,但 2:1 是该比率的合理近似值。
  • 嗯,好吧,那么为任何一种情况优化算法都没有意义
  • 事实证明,实际的实现有一个微妙的边缘情况;如果两个哈希函数返回相同的值,则您必须检测一个长度为 1 的循环。我们很幸运在审查中发现了这个错误;在我们的测试集上,这并没有发生。此外,对于高负载表,我们发现“Hopscotch Hashing”作为替代方案。
【解决方案2】:

std::set 通常使用二叉搜索树提供O(log(n))的插入和删除。不幸的是,对于大多数基于指针的实现来说,这使用了 3*N 空间。假设字大小的数据,1 表示数据,2 表示每个节点上左右子节点的指针。

如果您有一些常数 N 并且可以保证 ceil(log2(N)) 小于字长的一半,您可以使用每个 2*N 大小的固定长度的树节点数组。使用1表示数据,1表示两个子节点的索引,存储为单词的上、下半部分。这是否会让您以某种方式使用自平衡二叉搜索树取决于您的 N 和字长。对于 16 位系统,您只能得到 N = 256,但对于 32,它是 65k。

【讨论】:

  • 我确实可以忍受 N
【解决方案3】:

既然你的N有限,你不能用std::set<short, cmp, pool_allocator> BBoost's pool_allocator吗?

【讨论】:

  • 至少对于 libstdc++ 4.6.3,我发现 std::set 至少分配了 20*N 个字节。
  • 同意 Vaughn,std::set 将分配至少两个指针。我可以自己滚动,使用 Adam 的想法,但是使用 short 代替指针构建红黑树听起来很难。
猜你喜欢
  • 1970-01-01
  • 2015-06-04
  • 1970-01-01
  • 2011-08-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-09
  • 1970-01-01
相关资源
最近更新 更多