从n个排序数组中查找第k个最小数字

时间:2012-01-06 04:25:05

标签: arrays algorithm data-structures merge

所以,你有n个排序的数组(不一定长度相等),你要返回组合数组中的第k个最小元素(即合并所有n个排序数组形成的组合数组)

我现在已经尝试了它和它的其他变种已经有一段时间了,直到现在我只觉得有两个相等长度的数组,两个都是有序的,一个必须返回这两个的中位数。 这具有对数时间复杂度。

在此之后,我尝试将其概括为在两个排序的数组中找到第k个最小的。 Here是关于SO的问题。 即使在这里,给出的解决方案对我来说并不明显。但即使我以某种方式设法说服自己这个解决方案,我仍然很好奇如何解决绝对一般情况(这是我的问题)

有人可以解释我一步一步的解决方案(我认为应该再采用对数时间,即O(log(n 1 )+ log(n 2 ) ... + log(n N )其中n 1 ,n 2 ... n N 是n个数组的长度)从更具体的情况开始,然后转移到更一般的情况?

我知道类似的问题对于更具体的案例都存在于互联网上,但我没有找到令人信服的明确答案。

Here 是关于SO的问题(及其答案)的链接,它处理5个排序的数组并找到组合数组的中位数。答案对我来说太复杂了,无法概括它。

对于更具体的案例(正如我在帖子中提到的),即使是干净的方法也是受欢迎的。

PS:你认为这可以进一步推广到未排序的数组吗?

PPS:这不是一个家庭作业问题,我只是准备面试。

10 个答案:

答案 0 :(得分:12)

这并没有概括链接,但确实解决了问题:

  1. 浏览所有数组,如果有任何长度> k,截断到长度k(这很愚蠢,但我们以后会用k弄乱,所以无论如何都要这样做)
  2. 确定剩余的最大数组A.如果不止一个,请选择一个。
  3. 选择最大阵列A的中间元素M.
  4. 对其余数组使用二进制搜索以查找相同的元素(或最大元素< = M)。
  5. 基于各种元素的索引,计算元素的总数< = M和> M.这应该给你两个数字:L,数字< = M和G,数字>中号
  6. 如果k < L,截断你找到的分裂点上的所有数组,并在较小的数组上进行迭代(使用下半部分)。
  7. 如果k> L,截断你找到的分裂点处的所有数组,并在较小的数组上进行迭代(使用上半部分,并搜索元素(k-L)。
  8. 当你到达每个数组只有一个元素(或0)的点时,用这些数据创建一个大小为n的新数组,排序并选择第k个元素。

    因为你总是保证删除一个数组的至少一半,所以在 N 迭代中,你将摆脱一半的元素。这意味着有 N log k 迭代。每次迭代都是 N log k 的顺序(由于二进制搜索),所以整个事情是 N ^ 2(log k)^ 2 当然,这都是最坏的情况,基于这样的假设,你只能摆脱一半的最大阵列,而不是其他阵列。在实践中,我认为典型的性能会比最坏的情况好一些。

答案 1 :(得分:2)

不能在O(n)时间内完成。 Proof Sketch 如果确实如此,则必须完全不看至少一个数组。显然,一个数组可以任意改变kth元素的值。

我有一个相对简单的O(n*log(n)*log(m)),其中m是最长数组的长度。我确信它可能会稍微快一些,但速度要快得多。

考虑一个简单的情况,你有n个数组,每个数组的长度为1.显然,这与查找长度为k的未排序列表中的n元素是同构的。可以在O(n)中找到它,请参阅Median of Medians algorithm, originally by Blum, Floyd, Pratt, Rivest and Tarjan,并且不能(渐近地)更快的算法。

现在的问题是如何将其扩展为更长的排序数组。这是算法:找出每个数组的中位数。对元组(median,length of array/2)列表进行排序,并按中位数对其进行排序。通过保持长度的总和,直到达到大于k的总和。你现在有一对中位数,这样你就知道第k个元素就在它们之间。现在对于每个中位数,我们知道kth是否大于或小于它,因此我们可以丢弃每个数组的一半。重复。一旦数组都是一个元素长(或更少),我们使用选择算法。

实现这一点将揭示额外的复杂性和边缘条件,但没有任何东西会增加渐近的复杂性。每一步

  1. 查找中位数或数组,每个O(1),所以O(n)总计
  2. 对中位数O(n log n)
  3. 进行排序
  4. 浏览排序列表O(n)
  5. 分别对O(1)数组O(n)进行切片,O(n) + O(n log n) + O(n) + O(n) = O(n log n)总计
  6. log m。并且,我们必须执行此操作,直到最长的数组长度为1,这将需要O(n*log(n)*log(m))步骤,总共O(m)


    你问这是否可以归结为未排序数组的情况。可悲的是,答案是否定的。考虑我们只有一个数组的情况,那么最佳算法必须至少与每个元素进行一次比较,总共O(m)。如果n个未排序数组的解决方案更快,那么我们可以通过将单个数组拆分为n个部分来实现选择。由于我们刚刚证明选择是{{1}},我们被困住了。

答案 2 :(得分:1)

您可以查看我最近对相关问题here的回答。相同的想法可以推广到多个数组而不是2.在每次迭代中,如果k小于所有数组的中间索引之和,则可以拒绝具有最大中间元素的数组的后半部分。或者,如果k大于所有数组的中间索引之和,则可以拒绝具有最小中间元素的数组的前半部分,调整k。继续这样做,直到你有一个数组减少到0的长度。答案是最后一个数组的第k个元素,它没有被剥离为0个元素。

运行时分析:

在每次迭代中,你摆脱了一个数组的一半。但要确定哪个阵列将减少,您需要花时间线性数组。假设每个数组的长度相同,运行时间为c c log(n),其中c是数组的数量,n是每个数组的长度。

答案 3 :(得分:0)

如果k不是那么大,我们可以保持优先级最小队列。然后循环排序数组的每个头以获得最小元素和队列。当队列的大小是k时。我们得到最小的k。

也许我们可以将n排序的数组视为存储桶,然后尝试使用存储桶排序方法。

答案 4 :(得分:0)

这可以被认为是合并排序的后半部分。我们可以简单地将所有排序列表合并到一个列表中......但只保留合并列表中的k个元素从合并到合并。这具有仅使用O(k)空间的优点,但是比合并排序的O(n log n)复杂度稍微好一些。也就是说,它实际上应该比合并排序稍快一些。从最终组合列表中选择第k个最小值是O(1)。这种复杂性并不是那么糟糕。

答案 5 :(得分:0)

存在一个在O(N log k)时间内解决问题的概括,请参阅question here

答案 6 :(得分:0)

旧问题,但没有一个答案足够好。因此,我将使用滑动窗口技术发布解决方案:

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))

答案 7 :(得分:0)

可以通过在每个数组中进行二分搜索来完成,同时计算较小元素的数量。

我使用 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

答案 8 :(得分:0)

这就是代码。 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;
    }

答案 9 :(得分:-1)

请找到下面的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);
        }
    }