前言
Leetcode刷到150道了,各种题型都已经练习了一遍,没有必要再去刷数量了!分类总结解题方法,完善知识体系已经是刻不容缓了。尤其是在看了《暗时间》之后,深有感触。总结、反思自己的思维过程也许是最重要的。对每道题进行深加工,抽象出一般的概念,得到一般的解题策略。这个过程才是最重要的,是沉淀思想的绝好途径。
先简单摘一些常用的解题方法,以后每碰到难题的时候,都要想一下用这些方法是否可以解决:
时刻不忘未知量
时刻要想到自己的问题是什么,要求什么。用特例启发思考
构造一个合适的实例,可能会发现一般的规律。反过来推导
设立未知数,从结论出发,向已知条件靠扰。试错
- 调整题目的条件
去掉一个条件,观察区别,再放上那个条件,感觉到题目的内在结构上的某种约束,进而得到答案。 - 求解一个类似的题目
为了优化脑中的知识结构,我们在记忆掌握和分析问题的时候都应该尽量抽象地去看待,这样才能建立知识的本质联系。 - 列出所有可能与题目有关的定理或性质
比如这道题目,可以列出这样的性质:中位数是数组中最中间的数。如果元素总数为奇数,它左边所有元素的个数和右边所有元素的个数相等;如果为偶数,则将所有元素平分成两左右两部分,两部分元素个数相等, 中位数为最中间两者的均值。 - 考察反面,考察其他所有情况
- 将问题泛化
这道题应该要进行泛化,比如如果要求两个排序元素里的第K大元素怎么求?如果是n个排序数组呢?
题目分析1
Leetcode-CPP_p14
可以从结论来推导方法:题目要求用
log(m+n)
的复杂度,而中位数的序号为
i=m+n2
,要想达到要求的复杂度,则每次查找都应该使
i
减半,即要用到二分搜索。那怎样才能用到二分搜索呢?这一步还不是那么明显,答案是将原问题泛化,寻找两个排序数组的第
假设
A,B
两个数组的元素个数都大于
k/2
,那么将
A,B
的第
k
个元素,也就是
可以得到:
对于情形
(1)
,
A[0]⋯A[k2−1]
一定是排在
B[k2−1]
之前,因此
A[0]⋯A[k2−1]
绝对不会是第
k
小的数,可以在下一轮寻找中去掉,因此下一轮比较将变成:
这里我们不能排除
B
中的元素,是因为我们仅仅知道
因为我们是要寻找第永远不要忘了我们的目的是什么——走得太远,不要忘了当初是为什么出发!
而我们已经排除了
k2
个数,因此下一步是寻找第
k2
(这里的第
k2
是指包括当前元素的元素个数)小的数,因而又可以排除一半的数,即
k4
。
情形 (2) 的分析类似;
而情形 (3) 就更简单了,直接可以得到要找的数就是 A[k2−1] 。因为一定可以得到下面的排列:
A[0]⋯A[k2−2]⋯B[0]⋯B[k2−2] A[k2−1] B[k2−1]
虽然我们不知道 A⋃B 中的前 k−2 个数的具体顺序,但是最后两个数一定是 A[k2−1],B[k2−1] ,而最后一个数正是我们要找的第 k 小的数。
算法的正确性
如何证明算法的正确性呢?
每次递归都会排除一半的元素或者排除掉整个数组,即当
代码
int getKth(int a[], int m, int b[], int n, int k)
{
if (m > n)
return getKth(b, n, a, m, k);
if (0 == m)
return b[k-1];
if (1 == k)
return min(a[0], b[0]);
int i = min((k+1)/2, m);
/*if (a[i-1] < b[i-1])
return getKth(a+i, m-i, b, n, k-i);
else if (a[i-1] > b[i-1])
return getKth(a, m, b+i, n-i, k-i);*/
int j = k-i;
if (a[i-1] < b[j-1])
return getKth(a+i, m-i, b, n, k-i);
else if (a[i-1] > b[j-1])
return getKth(a, m, b+i, n-i, k-j);
else
return a[i-1];
}
double findMedianSortedArrays(int* nums1, int nums1Size, int* nums2, int nums2Size)
{
int total = nums1Size+nums2Size;
if (total & 1) //odd
return getKth(nums1, nums1Size, nums2, nums2Size, total/2+1);
else //even
return (getKth(nums1, nums1Size, nums2, nums2Size, total/2+1) + getKth(nums1, nums1Size, nums2, nums2Size, total/2))/2.0;
}
代码中注释的地方是有问题的,如果只是比较 A[i−1] 和 B[i−1] ,那么无论 i 是等于
k2 还是等于 k+12 ,最后都是不能直接用后面三种情况来处理的。所以我们还需要一个变量 j=k−i 来保证目前我们比较的元素个数为 k 。还是那句话,不要忘了最初的目的是什么。所以这里的关键在于选出
k 个数,比较每个一维数组的最后一个元素的大小。对于 kn 大于一维数组的长度 m 的情形,就会越界,这时只能取m 个元素了,那另外一个数组就必须取 k−m 个元素了,对于 n==2 时,显然 k−m 对于第2个数组是不越界的。但对 n>2 的情形,则情况会复杂很多。
下面是用vector加上迭代器的代码:
int getKthOfVectors(vector<int>& nums1, vector<int>::iterator it1, vector<int>& nums2, vector<int>::iterator it2, int k)
{
int sz1 = nums1.end()-it1;
int sz2 = nums2.end()-it2;
if (sz1 > sz2)
return getKthOfVectors(nums2, it2, nums1, it1, k);
if (0 == sz1)
return *(it2+k-1);
if (1 == k)
return min(*it1, *it2);
int i = min((k+1)/2, sz1);
int j = k-i;
if (*(it1+i-1) < *(it2+j-1))
{
it1 += i;
return getKthOfVectors(nums1, it1, nums2, it2, k-i);
}
else if (*(it1+i-1) > *(it2+j-1))
{
it2 += j;
return getKthOfVectors(nums1, it1, nums2, it2, k-j);
}
else
return *(it1+i-1);
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2)
{
int total = nums1.size()+nums2.size();
if (total & 1) //odd
return getKthOfVectors(nums1, nums1.begin(), nums2, nums2.begin(), (total+1)/2);
else //even
return (getKthOfVectors(nums1, nums1.begin(), nums2, nums2.begin(), total/2+1)+getKthOfVectors(nums1, nums1.begin(), nums2, nums2.begin(), total/2))/2.0;
}
效率
假定
n
是要找的第
T(n)=T(n/2)+O(1)
,由主定理
⇒
T(n)=logn
。
题目分析2
根据discuss里分享的解答,还可以利用中位数的这一性质:中位数两边的元素个数相等(或相差1)
。列出这一性质并不难,难就难在怎么根据这一性质继续往下走。
当左右两部分的元素个数相等或者相差1时,而且 A[i]>B[j−1],B[j]>A[j−1] ,那么中位数就不难找出来了。因此我们只要找出 i
由此,可列方程
⇒
⇒
j=m+n+12−i (将m+n为奇数和偶数统一起来)
因此我们只要在
0
~
算法的正确性
每次查找,要么找到
代码
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2)
{
int sz1 = nums1.size();
int sz2 = nums2.size();
if (sz1 > sz2)
return findMedianSortedArrays(nums2, nums1);
int imin = 0;
int imax = sz1;
int i, j;
while (imin <= imax)
{
i = (imin+imax)/2;
j = (sz1+sz2+1)/2 - i;
if (i > 0 && j < sz2 && nums2[j] < nums1[i-1])
imax = i-1;
else if (j > 0 && i < sz1 && nums1[i] < nums2[j-1])
imin = i+1;
else
break;
}
int num1;
if (0 == i)
num1 = nums2[j-1];
else if (0 == j)
num1 = nums1[i-1];
else
num1 = max(nums1[i-1], nums2[j-1]);
if ((sz1+sz2) & 1) //odd
return num1;
int num2 = min(nums1[i], nums2[j]);
return (num1+num2)/2.0;
}
注意:代码最后返回时用到除法,除数要用2.0,否则返回的是int类型转换到double,结果错误。
效率
二分查找的效率当然是 log2min(m,n) 。
推广
如果是在
n
个已排序的数组,寻找第
根据思路1,我们可以比较每个数组的第
kn
个数,如果全部相等,则找到第
k
小的数;否则,可以排除
上面所说的是理想情况下,实际写代码的时候要考虑的东西稍复杂一些,当数组的元素个数小于 kn 时,明显就会越界。再有首先得保证,所有数组的元素总数一定大于 k 的。
因此,可以推广为找出二维vector中的第
接口为:
double findMedianSortedArrays(vector<vector<int>> &nums, int k)
效率又该怎么计算呢?
这里
n
和主定理
对应,我们用
n
表示输入的规模,即
复杂度与 n 其实没有关系,只与
b 有关,因此 T(n)=O(1) 。
(2)
如果是在
b
个已排序的数组,寻找第
每次递归后 n 都会变成原来的
1b ⇒ T(n)=T(n/b)+O(1) ,由主定理 ⇒ T(n)=logn 。
最坏情况下,每次只能排除一个数,那么时间复杂度就会降为 O(n)
结语
如果
n
维数组的每维的长度相等,还比较好办。如果每维的长度不等,对于最好情况下的每次递归后规模变成原来的
不管怎么说,第一篇博客,加油!!!