Skip to content

004 median of two sorted arrays

4. Median of Two Sorted Arrays

题目: https://leetcode.com/problems/median-of-two-sorted-arrays/

难度:

Hard

一看到的时候,觉得跟CLRS书上的一道习题类似 求X[1....n] Y[1....n] 的 median

习题 9.3-8

Let X[1..n] and Y [1..n] be two arrays, each containing n numbers already in sorted order. Give an O(lg n)-time algorithn to find the median of all 2n elements in arrays X and Y .

The median can be obtained recursively as follows. Pick the median of the sorted array A. This is just O(1) time as median is the n/2th element in the sorted array. Now compare the median of A, call is a∗ with median of B, b∗. We have two cases.

  • a∗ < b∗ : In this case, the elements in B[n/2 ···n] are also greater than a . So the median cannot lie in either A[1 · · · n/2 ] or B[n/2 · · · n]. So we can just throw these away and recursively

  • a∗ > b∗ : In this case, we can still throw away B[1··· n/2] and also A[ n/ · · · n] and solve a smaller subproblem recursively.

In either case, our subproblem size reduces by a factor of half and we spend only constant time to compare the medians of A and B. So the recurrence relation would be T (n) = T (n/2) + O(1) which has a solution T (n) = O(log n).

divide and conquer

  • 如果X[n/2] == Y[n/2],则找到,return
  • 如果X[n/2] < Y[n/2],找X[n/2+1….n]和Y[1,2…n/2]之间
  • 否则找X[1..n/2]和Y[n/2…n]

但是实际上不同,这里需要考虑的问题更多:

  • 两个数组长度不一样
  • 并不是只找一个median,如果median有两个,需要算平均

思路

把它转化成经典的findKth问题

参考: http://chaoren.is-programmer.com/posts/42890.html

首先转成求A和B数组中第k小的数的问题, 然后用k/2在A和B中分别找。

比如k = 6, 分别看A和B中的第3个数, 已知 A1 < A2 < A3 < A4 < A5... 和 B1 < B2 < B3 < B4 < B5..., 如果A3 <= B3, 那么第6小的数肯定不会是A1, A2, A3, 因为最多有两个数小于A1, 三个数小于A2, 四个数小于A3。 关键点是从 k/2 开始来找。

B3至少大于5个数, 所以第6小的数有可能是B1 (A1 < A2 < A3 < A4 < A5 < B1), 有可能是B2 (A1 < A2 < A3 < B1 < A4 < B2), 有可能是B3 (A1 < A2 < A3 < B1 < B2 < B3)。那就可以排除掉A1, A2, A3, 转成求A4, A5, ... B1, B2, B3, ...这些数中第3小的数的问题, k就被减半了。每次都假设A的元素个数少, pa = min(k/2, lenA)的结果可能导致k == 1或A空, 这两种情况都是终止条件。

发问,为什么要从k/2开始寻找,依旧k = 6, 我可以比较A1 和 B5的关系么,可以这样做,但是明显的问题出现在如果A1 > B5,那么这个第6小的数应该存在于B6和A1中。

如果A1 < B5,这个时间可能性就很多了,比如A1 < A2 < A3 < A4 < B1 < B2,各种可能,无法排除元素,所以还是要从k/2开始寻找。

这个跟习题算法的区别是每次扔的东西明显少一些,但是k也在不断变小。下面的代码的时间复杂度是O(lg(m+n))

class Solution(object):
    def findMedianSortedArrays(self, nums1, nums2):
        """
        :type nums1: List[int]
        :type nums2: List[int]
        :rtype: float
        """
        n = len(nums1) + len(nums2)
        if n % 2 == 1:
            return self.findKth(nums1, nums2, n / 2 + 1)
        else:
            smaller = self.findKth(nums1, nums2, n / 2)
            bigger = self.findKth(nums1, nums2, n / 2 + 1)
            return (smaller + bigger) / 2.0


    def findKth(self, A, B, k):
        if len(A) == 0:
            return B[k-1]
        if len(B) == 0:
            return A[k-1]
        if k == 1 :
            return min(A[0],B[0])


        a = A[ k / 2 - 1 ] if len(A) >= k / 2 else None
        b = B[ k / 2 - 1 ] if len(B) >= k / 2 else None

        if b is None or (a is not None and a < b):
            return self.findKth(A[k/2:], B, k - k/2)
        return self.findKth(A, B[k/2:],k - k/2)  #这里要注意:因为 k/2 不一定 等于 (k - k/2), 


#python3里面要用向下取整函数才可以AC,否则报错,TypeError: list indices must be integers or slices, not float

from math import floor
class Solution:
    def findMedianSortedArrays(self, nums1, nums2):
        """
        :type nums1: List[int]
        :type nums2: List[int]
        :rtype: float
        """
        n = len(nums1) + len(nums2)
        if n % 2 == 1:
            return self.findKth(nums1, nums2, floor(n/2)+1)
        else:
            smaller = self.findKth(nums1, nums2, floor(n/2))
            bigger = self.findKth(nums1, nums2, floor(n/2)+1)
            return (smaller + bigger) / 2.0
    def findKth(self, A, B, k):

        if len(A) == 0:
            return B[k-1]
        if len(B) == 0:
            return A[k-1]
        if k == 1:
            return min(A[0], B[0])
        a = A[floor(k/2)-1] if len(A) >= k/2 else None
        b = B[floor(k/2)-1] if len(B) >= k/2 else None
        if b is None or (a is not None and a < b):
            return self.findKth(A[floor(k/2):], B, k - floor(k/2))
        else:
            return self.findKth(A, B[floor(k/2):], k - floor(k/2))

这个findKth的算法单独抽出来也是题目。

寻找最小的k个数

题目描述

输入n个整数,输出其中最小的k个。 分析与解法

解法一

要求一个序列中最小的k个数,按照惯有的思维方式,则是先对这个序列从小到大排序,然后输出前面的最小的k个数。 至于选取什么的排序方法,我想你可能会第一时间想到快速排序(我们知道,快速排序平均所费时间为n*logn),然后再遍历序列中前k个元素输出即可。因此,总的时间复杂度:O(n * log n)+O(k)=O(n * log n)

解法二

咱们再进一步想想,题目没有要求最小的k个数有序,也没要求最后n-k个数有序。既然如此,就没有必要对所有元素进行排序。这时,咱们想到了用选择或交换排序,即: 1. 遍历n个数,把最先遍历到的k个数存入到大小为k的数组中,假设它们即是最小的k个数; 2. 对这k个数,利用选择或交换排序找到这k个元素中的最大值kmax(找最大值需要遍历这k个数,时间复杂度为O(k)); 3. 继续遍历剩余n-k个数。假设每一次遍历到的新的元素的值为x,把xkmax比较:如果x < kmax ,用x替换kmax,并回到第二步重新找出k个元素的数组中最大元素kmax‘;如果x >= kmax,则继续遍历不更新数组。 每次遍历,更新或不更新数组的所用的时间为O(k)O(0)。故整趟下来,时间复杂度为n*O(k)=O(n*k)

解法三

更好的办法是维护容量为k的最大堆,原理跟解法二的方法相似: 1. 用容量为k的最大堆存储最先遍历到的k个数,同样假设它们即是最小的k个数; 2. 堆中元素是有序的,令k1<k2<...<kmaxkmax设为最大堆中的最大元素) 3. 遍历剩余n-k个数。假设每一次遍历到的新的元素的值为x,把x与堆顶元素kmax比较:如果x < kmax,用x替换kmax,然后更新堆(用时logk);否则不更新堆。 这样下来,总的时间复杂度:O(k+(n-k)logk)=O(nlogk)。此方法得益于堆中进行查找和更新的时间复杂度均为:O(logk)(若使用解法二:在数组中找出最大元素,时间复杂度:O(k))。 解法四

在《数据结构与算法分析--c语言描述》一书,第7章第7.7.6节中,阐述了一种在平均情况下,时间复杂度为O(N)的快速选择算法。如下述文字: - 选取S中一个元素作为枢纽元v,将集合S-{v}分割成S1和S2,就像快速排序那样 - 如果k <= |S1|,那么第k个最小元素必然在S1中。在这种情况下,返回QuickSelect(S1, k)。 - 如果k = 1 + |S1|,那么枢纽元素就是第k个最小元素,即找到,直接返回它。 - 否则,这第k个最小元素就在S2中,即S2中的第(k - |S1| - 1)个最小元素,我们递归调用并返回QuickSelect(S2, k - |S1| - 1)。此算法的平均运行时间为O(n)。

示例代码如下:

//QuickSelect 将第k小的元素放在 a[k-1]  
void QuickSelect( int a[], int k, int left, int right )
{
    int i, j;
    int pivot;

    if( left + cutoff <= right )
    {
        pivot = median3( a, left, right );
        //取三数中值作为枢纽元,可以很大程度上避免最坏情况
        i = left; j = right - 1;
        for( ; ; )
        {
            while( a[ ++i ] < pivot ){ }
            while( a[ --j ] > pivot ){ }
            if( i < j )
                swap( &a[ i ], &a[ j ] );
            else
                break;
        }
        //重置枢纽元
        swap( &a[ i ], &a[ right - 1 ] );  

        if( k <= i )
            QuickSelect( a, k, left, i - 1 );
        else if( k > i + 1 )
            QuickSelect( a, k, i + 1, right );
    }
    else  
        InsertSort( a + left, right - left + 1 );
}

这个快速选择SELECT算法,类似快速排序的划分方法。N个数存储在数组S中,再从数组中选取“中位数的中位数”作为枢纽元X,把数组划分为Sa和Sb俩部分,Sa<=X<=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较小的k个元素,否则返回Sa中所有元素+Sb中小的k-|Sa|个元素,这种解法在平均情况下能做到O(n)的复杂度。 更进一步,《算法导论》第9章第9.3节介绍了一个最坏情况下亦为O(n)时间的SELECT算法,有兴趣的读者可以参看。

给定两个已经排序好的数组,求第k大的,算法有O(m+n).类似merge sort的原理。否则利用的就是之上提到的,利用已经有序的原理,然后每次丢。

之所以这里还有一个丢弃条件是b is None 丢A的一部分,是因为B的数组长度是有限的,这个时候很明显丢A的k/2是不影响的,因为无论B[-1]是如何大或者小,因为整个B的长度没有达到k/2小,所以丢掉的这部分最大的A[k/2-1]也不可能是第k个,因为即使整个B都比A[k/2-1]小,拼起来也不能使A[k/2-1]第k大,所以可以放心丢弃。

这里是两个sorted list/array findKth,想到了类似的题目,如果给一个n个linked list,findKth,能想到的办法也只能是用heap吧,类似merge k sorted lists.

再写一个O(m+n)类似merge sort的也可以AC的代码

class Solution(object):
    def findMedianSortedArrays(self, nums1, nums2):
        """
        :type nums1: List[int]
        :type nums2: List[int]
        :rtype: float
        """
        def findKth(A, pa, B, pb, k):
            res = 0
            m = 0
            while pa < len(A) and pb < len(B) and m < k:
                if A[pa] < B[pb]:
                    res = A[pa]
                    m += 1
                    pa += 1
                else:
                    res = B[pb]
                    m += 1
                    pb += 1

            while pa < len(A) and m < k:
                res = A[pa]
                pa += 1
                m += 1


            while pb < len(B) and m < k:
                res = B[pb]
                pb += 1
                m += 1
            return res

        n = len(nums1) + len(nums2)
        if n % 2 == 1:
            return findKth(nums1,0, nums2,0, n / 2 + 1)
        else:
            smaller = findKth(nums1,0, nums2,0, n / 2)
            bigger = findKth(nums1,0, nums2,0, n / 2 + 1)
            return (smaller + bigger) / 2.0


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组