我被要求设计一个名为clever-list的数据结构,它保存带有真实密钥号的项目,并提供下一步操作:
插入(x) - 在列表中插入新元素。应该在O(log n)。
删除min / max - 删除并返回列表中的min / max元素。应该在O(log n)时间内。
转换 - 更改删除min / max的返回对象(如果是min然后是max,则相反)。应该在O(1)。
随机样本(k) - 从列表中返回随机选择的k个元素(k大于0且小于n)。应该是O(min(k log k,n +(n-k)log(n-k)))。
关于结构的假设: 数据结构在任何阶段都不会容纳3n多个元素。 我们不能假设n = O(1)。 我们可以使用Random()方法在O(1)时间内在[0,1)和预成型之间返回实数。
我设法使用min-max精细堆来实现前三种方法。但是,在这个时间限制内,我对随机样本(k)方法没有任何线索。我能找到的只是" Reservoir sampling",它在O(n)时间内运行。
有什么建议吗?
答案 0 :(得分:2)
您可以使用数组中实现的最小 - 最大堆来完成所有这些操作,包括随机抽样。
对于随机抽样,选择0到n之间的随机数。这是您要删除的项目的索引。复制该项目,然后将该索引处的项目替换为数组中的最后一项,并减少计数。现在,要么将项目冒泡,要么根据需要对其进行筛选。
如果它处于最低级别并且该项目小于其父级,则将其冒泡。如果它比它最小的孩子大,那就把它筛下来。如果它处于最高级别,则反转逻辑。
随机抽样是O(k log n)。也就是说,您将从n个项目堆中删除k个项目。它与k调用delete-min
的复杂性相同。
如果您不必从列表中删除项目,则可以通过从阵列中选择k
索引,在O(k)中进行天真的随机抽样。但是,有可能重复。为避免重复,您可以这样做:
当您随机选择一个项目时,将其与数组中的最后一项交换,并将计数减少1.当您选择了所有项目时,它们位于该项目的最后k
个位置。阵列。这显然是O(k)操作。您可以复制要由函数返回的项目。然后,将count设置回原始值并调用MakeHeap
函数,该函数可以从O(n)中的任意数组构建堆。所以你的操作是O(k + n)。
MakeHeap
功能非常简单:
for (int i = count/2; i >= 0; --i)
{
SiftDown(i);
}
另一种选择是,在进行交换时,将交换操作保存在堆栈上。也就是说,保存from和to索引。要放回项目,只需按相反顺序运行交换(即从堆栈弹出,交换项目,然后继续直到堆栈为空)。这是选择的O(k),O(k)用于放回,O(k)用于堆栈的额外空间。
当然,另一种方法是按照我的建议进行删除,一旦完成所有删除操作,就会将项目重新插入堆中。那是要移除的O(k log n)和要添加的O(k log n)。
顺便说一句,你可以通过使用哈希表来保存随机选择的索引,在O(k)最佳情况下进行随机抽样。您只需生成随机索引并将其添加到哈希表(不会接受重复项),直到哈希表包含k
项。该方法的问题在于,至少在理论上,算法可能无法终止。
答案 1 :(得分:1)
如果将数字存储在数组中,并使用自平衡二叉树来维护它们的排序索引,那么您可以使用给定的时间复杂度执行所有操作。在树的节点中,您需要指向数字数组的指针,并且在数组中,您需要一个指针返回到该数字所属的树的节点。
在0(n)范围内创建一组k个唯一整数,可以在O(k)时间内完成,假设(未初始化的)内存可以在O(1)时间内分配。
首先,假设您有办法知道内存是否未初始化。然后,你可以有一个大小为n的未初始化数组,并执行Fisher-Yates shuffle的常规k步骤,除非每次访问数组的元素(例如,索引i),如果它未初始化,然后你可以将它初始化为值i。这避免了初始化整个数组,这允许在O(k)时间而不是O(n)时间内完成shuffle。
其次,显然一般不可能知道内存是否未初始化,但是你可以使用的技巧(以增加内存量的两倍为代价)让你实现未初始化内存中的稀疏数组。它在Russ Cox的博客中有详细描述:http://research.swtch.com/sparse
这为您提供了一种随机选择k数的O(k)方式。如果k很大(即:>> n / 2),你可以选择(nk)数而不是k数,但你仍然需要将未选择的数字返回给用户,这总是为O (k)如果你把它们复制出来,那么选择速度越快你就什么也得不到。
如果您不介意访问内部数据结构,更简单的方法是在底层阵列上执行Fisher-Yates shuffle的k或nk步骤(取决于k< n / 2,并小心更新树中相应的节点以保持其值),然后返回[0..k-1]或[k..n-1]。在这种情况下,返回值仅在数据结构上的下一个操作之前有效。该方法是O(min(k,n-k))。