我有一个包含130000个元素的数据集,我有两个不同的数据结构,它们是双向链表和哈希表。将数据集元素插入链表时,我使用尾指针将节点放在列表的末尾。将数据集元素插入哈希表时,我会受益于具有探测功能的开放寻址方法。我面对数据集中最后10个元素的110000次碰撞。
但是,两种不同数据结构的插入总运行时间之差等于0.0981秒。
链接列表= 0.028521秒
哈希表= 0.120102秒
指针操作速度慢还是探测方法非常快?
答案 0 :(得分:2)
当你阅读here时,通过尾指针在双链表的末尾插入是O(1)。
在具有开放寻址的哈希表中插入也可以是常量,因为您也可以阅读here。
然而,正确有效地实现具有开放寻址的哈希表非常棘手,因为很多事情都可能出错(探测,加载因子,哈希函数等)。甚至Wikipedia提到了......
我面临最后10个元素的110000次冲突(在哈希表中)
这表明哈希表实现中的某些内容并不好。
这解释了为什么你所做的时间测量,如果它们是正确的,使得双链表比哈希表更快。
答案 1 :(得分:1)
指针操作速度慢还是探测方法非常快?
没有实际的算法,所以答案将是理论上的。
就性能而言,一般的答案是:缓存未命中是昂贵的。 DDR具有60 ns的延迟和3.2 GHz的CPU,最后一级缓存未命中CPU会使CPU停顿60 * 3.2 = ~200个周期。
对于双链表算法,您必须访问尾指针,尾元素和新元素。如果只是在循环中添加元素,那么所有这些访问都很可能在CPU缓存中。
在实际应用程序中,如果在添加之间执行某些操作,则最多可能有3个缓存未命中(尾部指针,尾部元素,新元素)。
对于具有开放寻址的哈希表,情况有点不同。散列函数在散列表中生成随机索引,因此通常,对散列表的第一次访问是高速缓存未命中。在你的情况下,哈希表不是那么大(130K指针),所以它可能适合L3缓存。但是,L3缓存未命中大约是30个周期的CPU停滞。
但接下来会发生什么?您只需将指针放入表中,无需更新tail元素或新元素。所以这里没有缓存丢失。
如果哈希表元素被占用,您只需检查下一个。这种顺序访问很容易被CPU预取器预测,因此所有这些访问通常也不会产生任何缓存未命中:CPU将下一个哈希表预取到L1缓存中。
因此,在实际应用中,哈希表通常只有一个缓存未命中,但由于哈希是不可预测的,哈希表总是会有第一个缓存未命中。
要获得实际答案,您的应用程序中发生了什么,您应该使用工具来分析CPU性能计数器,例如Linux上的perf
或Windows上的VTune
。该工具将显示您的CPU花费的确切时间。
这里还有一个理论上的免责声明。我想,如果你修复你的哈希表(比如说,每桶使用少量元素而不是开放寻址)并有效减少冲突次数,那么性能就可以达到标准。
你应该在哈希表上使用双链表,反之亦然?这取决于你的应用程序。哈希表适用于随机访问,即您可以在O(1)时间内访问任何元素。对于双链表,您必须遍历列表,因此估计值为O(n)。
另一方面,只需在列表末尾添加元素不仅操作更便宜,而且更容易实现。您不关心任何冲突和散列表溢出。
因此,在某些情况下,双链表比哈希表具有巨大的优势,并且应用程序最适合最佳。