从 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 的未排序列表中找到 kth 元素是同构的。可以在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_leftbisect_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 个元素中查找最小和第二小的元素

k个最小的数

如何在未排序数组的情况下找到未排序数组中的第k个最小整数?

使用来自 2 个排序数组的想法在 m 个排序数组中找到第 n 个最小值

找到排序矩阵中从小到大第K个数字