一.题目链接:https://leetcode.com/problems/median-of-two-sorted-arrays
二.题目大意:
给定两个排序过的数组,求出两个数组的中位数,要求时间复杂度为O(log(m+n)).
三.题解:
这是我在LeetCode上做的第一道hard级别的题目,没想到难度系数达到了5!被虐哭了。。。言归正传,这道题目的解题思路看起来并不难,难点在于它限定了时间复杂度,而且是对数级别的复杂度(说明优化程度已经相当高了)。这里,我给出三种解决方案,当然前两种时间复杂度并不满足要求(仅供参考思路);只有第三种方案符合题目要求:
方法1:
看到这种题目,首先想到的就是对两个数组进行合并,合并成一个有序的数组,然后根据数组元素个数的奇偶性求中位数即可。此处的排序方法有多种,最适合该题目的排序算法应该是归并排序。对于归并排序(默认升序),只需定义2个指针pa和pb分别指向数组a(长度为m)和数组b(长度为n),如果数组a的当前元素小,则*pa赋值给新数组,并且pa++;否则,*pb赋值给新数组,并且pb++;这么遍历指导遍历完某个数组,剩下的部分直接拼接到新数组末尾。最后找中位数。这么做的话时间复杂度为O(m+n),空间复杂度为O(1)。
方法2:
我们想要的仅仅是数组的中位数,而排序操作实际上消耗的代价太大,而我们没必要消耗如此大的代价。所以,此处不必对数组进行合并与排序,但还是借鉴方法1的思想,定义一个计数器cnt,每当*pa<*pb时,cnt++,pa++;每当*pa>=*pb,cnt++,pb++;当cnt==k时们就能找到相当于合并后数组的第k大的数了(此处包括本文所指的第k大的数指的是在数组中位置排序为k的数,而不是按大小来算的)。而中位数就是去中间大的那个数。这么做的话,时间复杂度也是O(m+n),空间复杂度O(1)。但是平均时间复杂度肯定是比方法1要小的。所以,方法2效率更好一些;遇到类似题目时,应该优先考虑方法2。
方法3:
前两种方法,都不能满足题目的要求。到底怎样才能找到指数级别的方法呢?首先我们来求一个更一般化的问题:求有序数组中第k个元素。而求有序数组的中位数实际就是这个问题的一个特例。具体一点说,假设数组的长度为n,当n为奇数时,即求第(n/2+1)个元素,当n为偶数时,即求第(n/2+1)和第(n/2)个元素,然后取他们的均值。下面我们从两个角度来分析:
1.如何搜索两个有序数组中第k个元素呢,这里又有个技巧。假设数组都是从小到大排列,对于第一个数组中前p个元素和第二个数组中前q个元素,我们想要的最终结果是:(1)p+q等于k,(2)第一个数组中第p个元素(即A[p-1])和第二个数组中第q-1个元素(即B[q-2])都小于等于总数组第k个元素(或者第一个数组中第p-1个元素(即A[p-2])和第二个数组中列q个元素(即B[q-q]))。因为总数组中,必然有k-1个元素小于等于第k个元素。这样第p个元素或者第q个元素就是我们要找的第k个元素。
2.如果我们每次都能够删除一个一定在k位置之前的元素,那么我们需要进行k次。但是如果我们每次都删除一半呢?(即p和q都是k/2级别的)要达到log级别,我们应该充分利用数组的有序性,也就是类似于二分查找。
结合角度1和角度2,我们可以通过二分法将问题规模缩小,假设p=k/2,则q=k-p=k/2,且p+q=k。这样A[0]~A[p-1]共有k/2个元素,如果第一个数组第p个元素小于第二个数组第q个元素(即A[k/2-1] < B[k/2-1]),我们不确定第二个数组中第q个元素是大了还是小了(即B[k/2-1]的大小不确定),但第一个数组的前p个元素肯定都小于目标(反证法可证),所以我们将第一个序列前p个元素全部抛弃(A[0]~A[k/2 -1]全部抛弃),形成一个较短的新数组(从A[k/2]开始)。然后,用新数组替代原先的第一个数组,再找其中的第k-p个元素(因为我们已经排除了p个元素,k需要更新为k-p,相当于总数组的第k个位置变成了总数组第k-p个位置(总数组变小了)),依次递归。同理,如果第一个数组中的第p个元素大于第二个数组中的第q个元素,我们则抛弃第二个数组的前q个元素(B[0]~B[k/2 -1]全部抛弃)。递归的终止条件有如下几种:
(1)较短数组中的所有元素都被抛弃,则返回较长数组中的第k个元素(在数组中下标是k-1)。
(2)第一个数组中的第p个元素等于二数组中第q个元素(即A[k/2-1] == B[k/2 - 1]),此时第一个数组中的第p个元素和二数组中第q个元素就是总数组中的第k个元素。(此时返回A[k/2-1]或者B[k/2-1]都可以)
(3)k == 1时,此时k不能再继续更新了,返回min(A[0],B[0])。(即本次传参时的数组A、B的起始位置)、
此外,还要注意:每次递归不仅要更新数组起始位置(起始位置之前的元素被抛弃),也要更新k的大小(扣除被抛弃的元素)!代码如下:
class Solution {
public:
double findMedianSortedArrays(const vector<int>& A, const vector<int>& B) {
const int m = A.size();
const int n = B.size();
int total = m + n;
if (total & 1)//判断数组长度是否为奇数
return find_kth(A.begin(), m, B.begin(), n, total / 2 + 1);
else
return (find_kth(A.begin(), m, B.begin(), n, total / 2)+ find_kth(A.begin(), m, B.begin(), n, total / 2 + 1)) / 2.0;
}
private:
static int find_kth(std::vector<int>::const_iterator A, int m,std::vector<int>::const_iterator B, int n, int k) {
//始终保证m小于等于n
if (m > n)
return find_kth(B, n, A, m, k);//此处一定不要忘记加return
if (m == 0)
return *(B + k - 1);
if (k == 1)
return min(*A, *B);
//相当于把k分成p和q两部分
int ia = min(k / 2, m), ib = k - ia;
if (*(A + ia - 1) < *(B + ib - 1))
return find_kth(A + ia, m - ia, B, n, k - ia);
else if (*(A + ia - 1) > *(B + ib - 1))
return find_kth(A, m, B + ib, n - ib, k - ib);
else
return A[ia - 1];
}
};
该方法的时间复杂度为O(log(m+n)),空间复杂度为O(log(m+n))(递归过程中每次调用都为O(1),总共为O(log(m+n))),符合题目要求。
注意:
1.当数组A的长度小于k/2时,此时选择p=m;那么此时B的长度一定是大于k/2的,并且舍去的一定是B的k/2个元素。此处利用反证法证明:
假设第K位置的元素在B的前k/2中,例如位置在索引i(i <= k/2-1)那么A必然拥有前k中的k -(i+1)个元素,而i <= k/2-1,则 i+1 <= k/2 , k - (i+1) >= k/2与条件:A的元素不够k/2矛盾,所以假设不成立,得证。
所以此时舍去的必然是B的k/2个元素,下一步迭代的过程中k也会变小所以对于更新后的k,A的长度还是小于k/2的话,则会继续删除B的k/2个元素。
2.总的来说,删除元素的过程主要包括三种类型:
(1).A[k/2-1] < B[k/2-1],此时删除B的前k/2个元素。
(2)B[k/2-1] < B[k/2-1],此时删除A的前k/2个元素。
(3)A的元素个数小于k/2的话,此时删除B的前k/2个元素。 (不过这一过程并没有在程序中显试的表现出来,而是隐含在程序处理的过程中,此处说明这一步只是为了更好的理解本算法;实际上
int ia = min(k / 2, m)
这行代码就是这一点的体现。)
3.函数find_kth中的参数m、n和k,都表示的是数字的位置,而不是数组的下标;实际数组的下标要再减去1。
=====================================================附录====================================================================
1.A[k/2-1]<B[k/2-1]时,总数组第k位置的数,会有三种情况:
(1)在A[k/2]~A[m-1]这一部分中。例如:
A={2,3,5}
B={1,7,8}
找到第4位置上的数,此时为5。
(2)在B[0]~B[k/2-1]这一部分。例如:
A={1,2,3,8}
B={4,5,7}
找到第4位置上的数,此时为4。
(3)就是B[k/2-1]。例如:
A={1,2,5}
B={3,4,7}
找到第4位置上的数,此时为4。
2.当A[k/2-1]>B[k/2`-1]时,也一样。
3.为什么当A[k/2-1] == B[k/2-1]时,A[k/2-1]或B[k/2-1]就是第k位置上的数?
证明:
对于A[0]~A[k/2-2],它们全部都小于等于A[k/2-1],所以它们一定是在总数组中第k位置之前。同理,对于B[0]~B[k/2-2],它们全部都小于等于B[k/2-1],所以它们也一定是在总数组中第k位置之前。这样就有(k/2-1)+(k/2-1) = k-2个已确定的数在k位置之前。又由于A[k/2-1] == B[k/2-1],而A[k/2-1]或者B[k/2-1]是大于这k-2个数中的最小的数,故第k位置的数一定是A[k/2-1]或者B[k/2-1]。