从 n 个排序数组中找到第 k 个最小的数
Posted
技术标签:
【中文标题】从 n 个排序数组中找到第 k 个最小的数【英文标题】:Finding kth smallest number from n sorted arrays 【发布时间】:2012-02-03 22:01:07 【问题描述】:所以,你有n个排序数组(不一定等长),你要返回组合数组中的第k个最小元素(即合并所有n个排序数组形成的组合数组)
我一直在尝试它和它的其他变体已经有一段时间了,直到现在我才觉得有两个长度相等的数组,都是排序的,一个必须返回这两个数组的中位数。 这具有对数时间复杂度。
在此之后,我尝试将其概括为在两个排序数组中找到第 k 个最小的。 Here 是关于 SO 的问题。 即使在这里,给出的解决方案对我来说也不是很明显。但即使我设法说服自己接受这个解决方案,我仍然对如何解决绝对一般情况感到好奇(这是我的问题)
有人可以向我解释一步一步的解决方案吗(我认为这又应该花费对数时间,即 O( log(n1) + log(n2) ... + log(nN) 其中 n1, n2...nN 是n 个数组的长度)从更具体的情况开始,然后转到更一般的情况?
我知道互联网上到处都有针对更具体案例的类似问题,但我还没有找到令人信服和明确的答案。
Here 是一个关于 SO 的问题(及其答案)的链接,它处理 5 个排序数组并找到组合数组的中位数。答案太复杂了,我无法一概而论。
欢迎使用更具体的案例(正如我在帖子中提到的)的干净方法。
PS:您认为这可以进一步推广到未排序数组的情况吗?
PPS:这不是作业问题,我只是在准备面试。
【问题讨论】:
我认为这个问题更适合 StackProgramming。无论如何我无法回答你的问题:) 对数时间是什么意思?我们有两个参数,n 和 k。我认为你不能比 O(n) 更快,因为你必须至少查看每个数组一次。 对数意味着类似于 O( lg(n1) + lg(n2) + lg(n3)...) 其中 n1, n2, n3.. 是数组 n1, n2, n3 的长度...n 【参考方案1】:这并没有概括链接,但确实解决了问题:
-
遍历所有数组,如果有长度 > k,则截断为长度 k(这很愚蠢,但我们稍后会弄乱 k,所以无论如何都要这样做)
确定剩余的最大数组 A。如果超过一个,则选择一个。
选择最大数组 A 的中间元素 M。
对剩余的数组使用二分法查找相同的元素(或最大的元素
根据各种元素的索引,计算元素的总数 M。这应该给你两个数字:L,数字 M
如果 k
如果 k > L,则截断您找到的分割点处的所有数组并迭代较小的数组(使用上半部分,并搜索元素 (k-L)。
当您达到每个数组只有一个元素(或 0)的程度时,使用这些数据创建一个大小为 n 的新数组,排序并选择第 k 个元素。
因为您始终保证至少删除一个数组的一半,所以在 N 次迭代中,您将删除一半的元素。这意味着有 N log k 次迭代。每次迭代的顺序是 N log k (由于二分搜索),所以整个事情是 N^2 (log k)^2 就是这样,当然,最坏的情况,基于您只删除最大数组的一半,而不是其他数组的假设。在实践中,我想典型的性能会比最坏的情况好很多。
【讨论】:
你不认为它可以通过简单的最小堆在 N^2LogN 中解决。取一个大小为 N 的堆并将最小元素放入 N 个数组中,然后弹出一个并检查数组。插入同一数组中的下一个元素。从堆中获得第 K 个元素后,继续执行此操作。 geeksforgeeks.org/… 这个解决方案表明它可以在 O(N + kLogN) 时间内完成。 如果我错了,请纠正我。但我对这条线很困惑That means there are N log k iterations
。为了向我自己或任何像我一样感到困惑的人解释这一点,这是因为你在每次 N 次迭代中去掉了一半的元素(即 N*K),所以要减少到 N 个元素(每次 1 个或 0 个元素)数组),你需要log((N * K) / N) = log(K)
次=>总共是N * log(K)
【参考方案2】:
它不能在少于O(n)
的时间内完成。 Proof Sketch 如果是这样,它就必须完全不查看至少一个数组。显然,一个数组可以任意改变kth
元素的值。
我有一个相对简单的O(n*log(n)*log(m))
,其中m
是最长数组的长度。我确信可以稍微快一点,但不会快很多。
考虑一个简单的情况,你有一个长度为 1 的 n
数组。显然,这与在长度为 n
的未排序列表中找到 k
th 元素是同构的。可以在O(n)
中找到它,参见Median of Medians algorithm, originally by Blum, Floyd, Pratt, Rivest and Tarjan,并且没有(渐近地)更快的算法是可能的。
现在的问题是如何将其扩展为更长的排序数组。这是算法:找到每个数组的中值。对元组列表(median,length of array/2)
进行排序并按中值排序。遍历保持长度的总和,直到总和大于 k。您现在有一对中位数,这样您就知道第 k 个元素在它们之间。现在对于每个中位数,我们知道第 k 个是大于还是小于它,所以我们可以丢弃每个数组的一半。重复。一旦数组都是一个元素长(或更少),我们使用选择算法。
实现这一点将揭示额外的复杂性和边缘条件,但不会增加渐近复杂性。每一步
-
找到中位数或数组,
O(1)
每个,所以O(n)
总计
对中位数进行排序O(n log n)
遍历排序列表O(n)
对数组进行切片O(1)
,因此,O(n)
总计
那是O(n) + O(n log n) + O(n) + O(n) = O(n log n)
。而且,我们必须执行此操作,直到最长的数组长度为 1,这将采取 log m
步骤,总共 O(n*log(n)*log(m))
您问这是否可以推广到未排序数组的情况。可悲的是,答案是否定的。考虑我们只有一个数组的情况,那么最好的算法必须与每个元素至少比较一次,总共O(m)
。如果对 n 个未排序的数组有更快的解决方案,那么我们可以通过将单个数组拆分为 n 个部分来实现选择。因为我们刚刚证明了选择是O(m)
,所以我们被卡住了。
【讨论】:
这只是 k = n/2 的特定情况,即找到中位数等同于找到整体中的第 n/2 个最小值。这个特殊问题可以通过找到每个数组的中位数来解决(这是 O(1),因为数组是排序的。然后,在 O(n) 时间内找到这 n 个中位数的最小值和最大值。现在组合中位数将位于在最小和最大中位数之间,所以我们可以摆脱其他元素。这本质上是 O(log(maxM)) 但我不确定。在你的情况下,对中位数进行排序会在我们需要的时候稍微增加一点复杂性不过,这将是最小和最大 +1 的努力 是的,从找到中位数到找到第 k 个并不难。仅选择最小值和最大值的问题在于,除非我遗漏了某些东西,否则您对要丢弃的值没有限制。排序步骤可让您丢弃一半的值。尽管如此,还是很高兴能找到O(n*log m)
请注意,我的解决方案与您链接的解决方案具有相同的渐近行为。因为在那种情况下n = 5
它被视为大哦计算的常量。
知道中位数是bw min和max,你当然可以抛出数组的下半部分对应最小中值,上半部分对应最大中值。
那么这个方案怎么能很容易地扩展到寻找第k个?【参考方案3】:
您可以查看我最近对相关问题here 的回答。相同的想法可以推广到多个数组而不是 2。在每次迭代中,如果 k 小于所有数组的中间索引的总和,您可以拒绝具有最大中间元素的数组的后半部分。或者,如果 k 大于所有数组的中间索引之和,则可以拒绝具有最小中间元素的数组的前半部分,调整 k。继续这样做,直到除了一个数组之外,所有数组的长度都减少到 0。答案是最后一个数组的第 k 个元素,它没有被剥离为 0 个元素。
运行时分析:
您在每次迭代中摆脱了一个数组的一半。但是要确定要减少哪个数组,您需要花费与数组数量成线性关系的时间。假设每个数组的长度相同,则运行时间为 cclog(n),其中 c 是数组的数量,n 是每个数组的长度。
【讨论】:
【参考方案4】:存在一个在 O(N log k) 时间内解决问题的泛化,请参阅question here。
【讨论】:
【参考方案5】:老问题,但没有一个答案足够好。所以我使用滑动窗口技术和堆来发布解决方案:
class Node
int elementIndex;
int arrayIndex;
public Node(int elementIndex, int arrayIndex)
super();
this.elementIndex = elementIndex;
this.arrayIndex = arrayIndex;
public class KthSmallestInMSortedArrays
public int findKthSmallest(List<Integer[]> lists, int k)
int ans = 0;
PriorityQueue<Node> pq = new PriorityQueue<>((a, b) ->
return lists.get(a.arrayIndex)[a.elementIndex] -
lists.get(b.arrayIndex)[b.elementIndex];
);
for (int i = 0; i < lists.size(); i++)
Integer[] arr = lists.get(i);
if (arr != null)
Node n = new Node(0, i);
pq.add(n);
int count = 0;
while (!pq.isEmpty())
Node curr = pq.poll();
ans = lists.get(curr.arrayIndex)[curr.elementIndex];
if (++count == k)
break;
curr.elementIndex++;
pq.offer(curr);
return ans;
这里我们需要访问的最大元素数是O(K)
,并且有M
数组。所以有效时间复杂度为O(K*log(M))
。
【讨论】:
我认为上面的代码有一个错误。在递增和提供之前需要一个 if,如下所示: if (curr.elementIndex 【参考方案6】:这就是代码。 O(k*log(m))
public int findKSmallest(int[][] A, int k)
PriorityQueue<int[]> queue = new PriorityQueue<>(Comparator.comparingInt(x -> A[x[0]][x[1]]));
for (int i = 0; i < A.length; i++)
queue.offer(new int[] i, 0 );
int ans = 0;
while (!queue.isEmpty() && --k >= 0)
int[] el = queue.poll();
ans = A[el[0]][el[1]];
if (el[1] < A[el[0]].length - 1)
el[1]++;
queue.offer(el);
return ans;
【讨论】:
【参考方案7】:如果 k 不是那么大,我们可以维护一个优先级最小队列。然后循环排序数组的每个头部以获得最小的元素和入队。当队列大小为 k 时。我们得到前k个最小的。
也许我们可以把第n个排序的数组看成桶,然后试试桶排序的方法。
【讨论】:
复杂度是多少?除了使用 O(k) 空间之外,您还将执行至少 K 个入队,即 O(k log(k))。所以,是的,如果 K 很大,我们就有问题了。我同意当您需要所有 k 个最小数字时,这可能是一个很好的解决方案,但在这种情况下,我只需要第 k 个最小的数字。【参考方案8】:这可以被认为是归并排序的后半部分。我们可以简单地将所有排序的列表合并到一个列表中……但只保留合并列表中的 k 个元素从合并到合并。这具有仅使用 O(k) 空间的优点,但比归并排序的 O(n log n) 复杂度稍好一些。也就是说,它在实践中应该比归并排序稍微快一点。从最终组合列表中选择第 k 个最小的为 O(1)。这种复杂性还不错。
【讨论】:
是的,这是解决问题的简单方法之一,不幸的是它还不够优化。【参考方案9】:可以通过在每个数组中进行二分查找,同时计算较小元素的数量。
我使用bisect_left
和bisect_right
使其也适用于非唯一数字,
from bisect import bisect_left
from bisect import bisect_right
def kthOfPiles(givenPiles, k, count):
'''
Perform binary search for kth element in multiple sorted list
parameters
==========
givenPiles are list of sorted list
count is the total number of
k is the target index in range [0..count-1]
'''
begins = [0 for pile in givenPiles]
ends = [len(pile) for pile in givenPiles]
#print('finding k=', k, 'count=', count)
for pileidx,pivotpile in enumerate(givenPiles):
while begins[pileidx] < ends[pileidx]:
mid = (begins[pileidx]+ends[pileidx])>>1
midval = pivotpile[mid]
smaller_count = 0
smaller_right_count = 0
for pile in givenPiles:
smaller_count += bisect_left(pile,midval)
smaller_right_count += bisect_right(pile,midval)
#print('check midval', midval,smaller_count,k,smaller_right_count)
if smaller_count <= k and k < smaller_right_count:
return midval
elif smaller_count > k:
ends[pileidx] = mid
else:
begins[pileidx] = mid+1
return -1
【讨论】:
【参考方案10】:请找到下面的 C# 代码来查找两个排序数组的并集中的第 k 个最小元素。时间复杂度:O(logk)
public int findKthElement(int k, int[] array1, int start1, int end1, int[] array2, int start2, int end2)
// if (k>m+n) exception
if (k == 0)
return Math.Min(array1[start1], array2[start2]);
if (start1 == end1)
return array2[k];
if (start2 == end2)
return array1[k];
int mid = k / 2;
int sub1 = Math.Min(mid, end1 - start1);
int sub2 = Math.Min(mid, end2 - start2);
if (array1[start1 + sub1] < array2[start2 + sub2])
return findKthElement(k - mid, array1, start1 + sub1, end1, array2, start2, end2);
else
return findKthElement(k - mid, array1, start1, end1, array2, start2 + sub2, end2);
【讨论】:
以上是关于从 n 个排序数组中找到第 k 个最小的数的主要内容,如果未能解决你的问题,请参考以下文章
用JAVA语言编译:数组中包含n个整数,从其中找出k个最小的数,写出你能想到的最快的方法!!!
在大小为 N 的数组的每 k 个元素中查找最小和第二小的元素