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
,把x
与kmax
比较:如果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<...<kmax
(kmax
设为最大堆中的最大元素)
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