我有几个数字范围。这些范围不重叠 - 因为它们不重叠,逻辑结果是任何时候任何数字都不能成为多个范围的一部分。每个范围都是连续的(单个范围内没有孔,因此范围8到16实际上包含8到16之间的所有数字),但两个范围之间可能存在空洞(例如范围从64开始到128,下一个范围从256开始并转到384),因此某些数字可能根本不属于任何范围(在此示例中,数字129到255不属于任何范围)。
我得到一个号码,需要知道该号码属于哪个范围......如果它属于任何范围。否则我需要知道它不属于任何范围。当然速度很重要;我不能简单地检查所有范围是O(n),因为可能有数千个范围。
一个简单的解决方案是将所有数字保存在已排序的数组中并对其运行二进制搜索。这至少会给我O(log n)。当然二进制搜索必须稍微修改,因为它必须始终检查范围的最小和最大数量。如果要查找的数字介于两者之间,我们找到了正确的范围,否则我们必须搜索当前范围之下或之上的范围。如果最后只剩下一个范围且数字不在该范围内,则该数字根本不在范围内,我们可以返回“未找到”结果。
范围也可以在某种树形结构中链接在一起。这基本上就像是带有二分搜索的排序列表。优点是修改树比修改数组(添加/删除范围)更快,但不像我们浪费一些额外的时间来保持树平衡,树可能会在一段时间内变得非常不平衡,这将导致搜索速度比排序数组上的二进制搜索慢得多。
有人可以争论哪种解决方案更好或更差,因为在实践中,搜索和修改操作的数量几乎是平衡的(每秒执行的搜索数量和添加/删除操作数量相同)。
对于这类问题,是否存在比排序列表或树更好的数据结构?也许在最好的情况下可能比O(log n)更好,在最坏的情况下可能比O(log n)更好?
以下可能有用的一些其他信息如下:所有范围始终以2的幂的倍数开始和结束。它们总是以相同的2的幂开始和结束(例如,它们都以4的倍数或8的倍数或16的倍数开始/结束,依此类推)。在运行时,2的功率不能改变。在添加第一个范围之前,必须设置2的幂,并且所有已添加的范围必须以此值的倍数开始/结束,直到应用程序终止。我认为这可以用于优化,就好像它们都是从例如...的倍数开始。 8,我可以忽略所有比较操作的前3位,其他位将告诉我范围是否有。
我读到了关于树和范围的树。这些是问题的最佳解决方案吗?有没有更好的解决方案?问题听起来类似于malloc实现必须做的事情(例如,每个freed内存块属于一系列可用内存,malloc实现必须找到哪一个),那么通常如何解决这个问题?
答案 0 :(得分:21)
在运行各种基准测试后,我得出的结论是,只有树状结构可以在这里工作。排序列表当然显示了良好的查找性能 - O(log n) - 但它显示了可怕的更新性能(与树相比,插入和删除速度慢了10倍!)。
平衡二叉树也具有O(log n)查找性能,但更新速度要快得多,也是O(log n),而排序列表更像O(n)更新(O(log) n)找到insert或要删除的元素的位置,但是最多必须在列表中移动n个元素,这是O(n))。
我实现了一个AVL树,一个红黑树,一个Treap,一个AA树和B树的各种变体(B表示拜耳树,而不是二进制)。结果:拜耳树几乎从未获胜。他们的查找很好,但是他们的更新性能很差(因为在B-Tree的每个节点中你都有一个排序列表!)。拜耳树只有在读取/写入节点是非常慢的操作(例如,直接从/从硬盘读取或写入节点)的情况下才是优越的 - 因为B树必须读取/写入比其他任何节点少得多的节点树,所以在这种情况下它会赢。如果我们在记忆中拥有树,它就没有机会对抗其他树木,对那里的所有B树粉丝都很抱歉。
Treap最容易实现(不到其他平衡树所需的代码行的一半,只是非平衡树所需代码的两倍),并且显示了查找和更新的良好平均性能......但我们可以做得更好。
AA-Tree显示出惊人的良好查找性能 - 我不知道为什么。它们有时会击败所有其他树(不是很远,但仍然不够重合)......并且移除性能还可以,但是除非我太愚蠢而无法正确实现它们,插入性能非常糟糕(它执行每个插入物上的树旋转比任何其他树更多 - 甚至B树也具有更快的插入性能。
这给我们留下了两个经典,AVL和RB-Tree。它们都非常相似,但经过数小时的基准测试后,有一件事是清楚的:AVL Trees肯定比RB-Trees有更好的查找性能。差异并不大,但在所有基准测试的2/3中,他们将赢得查找测试。在所有AVL树比RB-Trees更严格平衡之后,并不太令人惊讶,因此在大多数情况下它们更接近最佳二叉树。我们这里并没有谈论一个巨大的差异,它始终是一场紧密的竞赛。
另一方面,RB Trees几乎在所有测试运行中都击败了AVL Trees以进行插入,而这种情况并非如此紧密。和以前一样,这是预期的。与AVL树相比,不太严格平衡的RB树在插入上执行的树旋转要少得多。
删除节点怎么样?这似乎很大程度上取决于节点的数量。对于小节点数(一切都不到五十万),RB树再次拥有AVL树;差异甚至比插入更大。相当出乎意料的是,一旦节点数量增长超过一百万个节点,AVL树似乎就会赶上并且RB树的差异会缩小,直到它们或多或少地同样快速。但这可能是系统的影响。它可能与进程的内存使用或CPU缓存等有关。对RB树具有比AVL树更负面影响的东西,因此AVL树可以赶上。对于查找没有观察到相同的效果(AVL通常更快,无论有多少节点)和插入(RB通常更快,无论有多少节点)。
<强>结论:强>
我认为我能得到的最快的是使用RB-Trees时,因为查找次数只会略高于插入和删除次数,无论AVL查找速度有多快,整体性能都会受到更糟糕的插入影响/删除性能。
也就是说,除非这里的任何人都想出一个更好的数据结构,它将拥有RB树的大时间; - )
答案 1 :(得分:8)
创建排序列表并按下边距/开始排序。这是最容易实现的,并且足够快,除非你有数百万个范围(甚至可能那么)。
查找范围时,请找到start <= position
的范围。您可以在此处使用二进制搜索,因为列表已排序。如果position <= end
,则该数字在范围内。
由于任何范围的结束都保证小于下一个范围的开始,因此在找到可能包含位置的范围之前,您无需关心结束。
当您获得交叉点或者您拥有大量范围以及构建结构并经常查询时,所有其他数据结构都会变得有趣。
答案 2 :(得分:7)
在每个节点上都有范围的平衡排序树似乎就是答案。 我不能证明它是最优的,但如果我是你,我就不会再看了。
答案 3 :(得分:1)
如果总的数字范围很小,并且你有足够的内存,你可以创建一个包含所有数字的巨大表格。
例如,如果您有一百万个数字,则可以创建一个引用范围对象的表。
答案 4 :(得分:0)
作为O(log n)平衡二叉搜索树(BST)的替代方案,您可以考虑构建按位(压缩)trie。即您要存储的数字位的前缀树。
这为您提供了O(w) - 搜索,插入和删除性能;其中w =位数(例如32或64减去你的范围所基于的2的幂)。
并不是说它会表现得更好或更差,但它似乎是一个真正的替代品,因为它与BST不同,但仍具有良好的理论性能,并允许像BST一样的前任查询。