(重述问题:给定 N 个项目,找出其中最大的 m 个。)
一个简单的解决方案是priority queue。将所有 N 个项目送入队列,然后从列表中弹出顶部 m 个项目。喂入 N 个项目将是 O(N log m)。每个单独的 pop 操作是 O(log m),因此删除顶部 n 项将是 O(m log m)。
就地算法应该相对简单。我们是 N 个元素的数组。数组中的每个位置都有编号,编号介于 1 和 N(含)之间。对于数组中的每个位置,取其位置并除以 2(必要时向下舍入),并将该位置定义为其 父位置。除了位置 1 之外,每个位置都将有一个父位置。大多数职位(不是全部)都会有两个孩子。例如:
node position: 1 2 3 4 5 6 7 8 9 ...
parent: - 1 1 2 2 3 3 4 4 ...
我们希望交换节点,直到每个节点的值小于(或等于)其父节点。这将保证最大值位于位置 1。将数组重新排序以具有这种形式非常容易。只需从位置 1 到 N 依次遍历节点,并在其上调用该函数一次:
void fixup_position(int x) {
if(x==1)
return;
int parent_position = (x/2) ; // rounding-down where necessary
if (data[x] > data[parent_position]) {
swap(data[x], data[parent_position]);
check_position(parent_position); // note this recursive call
}
}
for(x = 1; x <= N; ++x) {
fixup_position(x);
}
(是的,我正在计算位置为 1 的数组,而不是 0!实际实现时必须考虑到这一点。但这更容易理解优先级队列的逻辑。)
递归调用的平均次数(因此swaps)是一个常数(如果我没记错的话,是2)。所以这会很快,即使是大型数据集。
值得花一点时间来理解为什么这是正确的。就在调用fixup_position(x) 之前,直到(但不包括x)的每个位置都处于“正确”状态。 “正确”是指它们没有完全排序,但每个节点都小于其父节点。引入了一个新值(在位置 x),并将在队列中“冒泡”。您可能会担心这会使其他职位及其父子关系无效,但不会。一次只有一个节点处于无效状态,并且会一直冒泡到它应有的位置。
这是将您的数组重新排列到优先级队列中的 O(N) 步骤。
删除顶部 n 项。经过上面的方法,很明显最大的数字会在位置1,但是第二大,第三大,等等呢?我们所做的是从位置 1 一次弹出一个值,然后重新排列数据,以便将下一个最大值移到位置 1。这比 fixup_position 稍微复杂一些。
for(int y = 1; y <= m; ++y) {
print the number in position 1 .... it's the next biggest number
data[1] = -10000000000000; // a number smaller than all your data
fixup_the_other_way(1); // yes, this is '1', not 'y' !
}
fixup_the_other_way 在哪里:
void fixup_the_other_way(int x) {
int child1 = 2*x;
int child2 = 2*x+1;
if(child1 > N) // doesn't have any children, we're done here
return;
if(child2 > N) { // has one child, at position[child1]
swap(data[x], data[child1]);
fixup_the_other_way(child1);
return;
}
// otherwise, two children, we must identify the biggest child
int position_of_largest_child = (data[child1]>data[child2]) ? child1 : child2;
swap(data[x], data[position_of_largest_child]);
fixup_the_other_way(position_of_largest_child);
return;
}
这意味着我们打印出最大的剩余项目,然后用一个非常小的数字替换它,并强制它“冒泡”到我们数据结构的底部。