O(log N)的数据结构查找和更新,考虑小L1缓存

时间:2012-05-11 12:56:44

标签: c++ algorithm complexity-theory

我目前正在开发一个嵌入式设备项目,我遇到了性能问题。分析已经找到了我想要消除的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]时,我必须在k中找到B并将其移至新位置,以维持搜索顺序。由于我知道A[k]的旧值和新值,因此memmove()的旧位置和新位置之间只有B[]的{​​{1}}个k。这是我需要解决的O(N)操作;由于A[k]的旧值和新值基本上是随机的,我平均在 N / 2 N / 3元素上移动。

我使用std::make_heap作为谓词来查看[](int i, int j) { return A[i] < A[j]; }。在这种情况下,我可以轻松地使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),那么仍然可以接受。

3 个答案:

答案 0 :(得分:7)

如果唯一的操作是(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等人的一项研究。已经表明杜鹃散列很多   对于小的,缓存驻留的哈希表而言,比链式哈希更快   现代处理器。肯尼斯罗斯已经展示了bucketized版本   cuckoo散列(使用包含多个存储桶的变体)   key)比传统方法更快也适用于大哈希   表,当空间利用率很高时。的表现   Askitis进一步调查了bucketized cuckoo hash table,   其性能与其他散列方案相比较。

答案 1 :(得分:1)

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

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

答案 2 :(得分:0)

由于你的N有限,你不能std::set<short, cmp, pool_allocator> B使用Boost's pool_allocator吗?