Leetcode算法笔记(一)——分治法
前言
分治法在于将一个复杂的问题分解为其的几个子问题,减小计算复杂度,常常需要用到递归的思想,在这其中的关键是把整个过程形式化定义出来,找到状态转移,边界条件
以Leetcode中的Hard题Median of Two Sorted Arrays为例来加深印象
1. 题目复述:
Given two sorted arrays nums1
and nums2
of size m
and n
respectively, return the median of the two sorted arrays.
The overall run time complexity should be O(log (m+n))
.
2. 思路分析
虽然这里我们求解的是中位数,但是我们可以考虑求解合并数组中的第k个数的方法。然后我们根据数组总长度的奇偶性来求解中位数
于是问题便转化为了求合并数组中的第k个数,鉴于此,我们有两种思路。
2.1 思路一:以数值分割k
通过分解尝试寻找第k个数,每次对k进行二分操作。
我们设s1p,s1r,s2p,s2r 分别为数组1、数组2 的左边界和右边界
初始情况下我们对s1p=0, s1r=nums1.size()-1, s2p=0, s2r=nums2.size() , k 进行搜索
递归终点:
- 若s1p==s1r+1或s2p==s2r+1 即数组1或者数组2 已经超出界限,代表已搜索完毕,那么直接返回数组2或数组1的第k个数即可
- 若k==1,代表找当前区间内的nums1和nums2的第一个数,也就是返回min(nums1[s1p],nums2[s2p])
核心操作:
- 我们每次取nums1和nums2的第k/2个数进行比较,记此时nums1的下标为nows1p=s1p+k/2-1,此时nums2的下标为nows2p=s2p+k/2-1,接下来我们判断两个数的大小
- 如果nums1[nows1p]==nums2[nows2p] 说明两数相同,且两数左边一共有k-2个数,那么两数中的后者就是第k个数,又由于两数相同,任意返回一个即可
- 如果nums1[nows1p]<nums2[nows2p],对于nums1[nows1p]来说,由于其最多为第k-1个数,那么其左边包括自己都可以抛弃 即下次递归时 s1p=nows1p+1 k=k-(nows1p-s1p+1) 这里k 减掉的是 舍弃掉的数的个数
- 如果nums1[nows1p]>nums2[nows2p],同理对nums2作上述操作 s2p=nows2p+1 k=k-(nows2p-s2p+1)
上述便完成了整个递归过程中的形式化定义,包括了递归的终点和递归条件的判断,但是需要我们注意几个问题
第一是 nows1p和nows2p由于每次会增加,所以可能会越界,所以我们需要加一个限制条件,比如nows1p=min(s1p+k/2-1,s1r)
第二是 由于我们对nows1p和nows2p加了限制条件,所以当nums1[nows1p]==nums2[nows2p]时,可能这两个数并不是第k个数,所以我们只能将其加入大于或小于的条件中继续判断,任意放在一边即可,因为总有一个不会被舍弃
第三是我们需要非常清晰地知道每个地方的边界到底是什么,某个地方到底是第几个数,中间到底舍弃掉了多少数,不能说这个地方加个1那个地方减个1,正好通过就行了,我们一定要真正搞清楚弄明白每个地方为什么是这样,这样才能自己解决问题!
附代码如下:
递归版:
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int size1 = nums1.size();
int size2 = nums2.size();
int k = (size1 + size2) / 2;
if ((size1 + size2) % 2 == 1)
return findKthNumber(nums1, nums2, 0, size1 - 1, 0, size2 - 1, k+1);
else
return (findKthNumber(nums1, nums2, 0, size1 - 1, 0, size2 - 1, k) + findKthNumber(nums1, nums2, 0, size1 - 1, 0, size2 - 1, k + 1)) * 0.5;
}
int findKthNumber(vector<int>nums1,vector<int>nums2,int s1p,int s1r,int s2p,int s2r,int k)
{
if(s1p==s1r+1)
return nums2[s2p+k-1];
if(s2p==s2r+1)
return nums1[s1p+k-1];
if(k==1)
return min(nums1[s1p],nums2[s2p]);
int nowsp1=min(s1p+k/2-1,s1r);
int nowsp2=min(s2p+k/2-1,s2r);
int a1=nums1[nowsp1];
int b1=nums2[nowsp2];
if(a1<=b1)
return findKthNumber(nums1,nums2,nowsp1+1,s1r,s2p,s2r,k-(nowsp1-s1p+1));
else
return findKthNumber(nums1,nums2,s1p,s1r,nowsp2+1,s2r,k-(nowsp2-s2p+1));
}
};
迭代版:
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int size1 = nums1.size();
int size2 = nums2.size();
int k = (size1 + size2) / 2;
if ((size1 + size2) % 2 == 1)
return findKthNumber(nums1, nums2, 0, size1 - 1, 0, size2 - 1, k+1);
else
return (findKthNumber(nums1, nums2, 0, size1 - 1, 0, size2 - 1, k) + findKthNumber(nums1, nums2, 0, size1 - 1, 0, size2 - 1, k + 1)) * 0.5;
}
int findKthNumber(vector<int>nums1,vector<int>nums2,int s1p,int s1r,int s2p,int s2r,int k)
{
while(1)//直到找到为止
{
if(s1p==s1r+1)
return nums2[s2p+k-1];
if(s2p==s2r+1)
return nums1[s1p+k-1];
if(k==1)
return min(nums1[s1p],nums2[s2p]);
int nowsp1=min(s1p+k/2-1,s1r);
int nowsp2=min(s2p+k/2-1,s2r);
int a1=nums1[nowsp1];
int b1=nums2[nowsp2];
k=(a1<=b1)?(k-(nowsp1-s1p+1)):(k-(nowsp2-s2p+1));
(a1<=b1)?s1p=nowsp1+1:s2p=nowsp2+1;
}
}
};
2.2 思路二:以数的个数分割k
我们将nums1和nums2均分为两半,寻找其各自的中位数
这里我们还是记nums1、nums2的左右边界分别为s1p,s1r,s2p,s2r
mid1=(s1r-s1p+1)/2,mid2=(s2r-s2p+1)/2 ,mid1和mid2代表nums1和nums2元素个数的一半
nows1p=s1p+mid1,nows2p=s2p+mid2
a1=nums1[nows1p] b1=nums2[nows2p]
这样我们知道在a1左边的数的个数是mid1个,而在b1左边的数是mid2个
我们先记a1<b1,如果b1<a1,则将a1,b1倒过来即可
递归操作:
- 如果k<mid1+mid2+2 说明此时b1的下标肯定是大于k(因为其下标至少为mid1+mid2+2),那么我们就可以抛弃掉b1的右边包括b1的数,即第二个数组的范围变为s2p,nows2p-1
- 如果k>mid1+mid2+2 说明此时a1的下标肯定是小于k(因为其下标最多为mid1+mid2+1),那么我们就可以抛弃a1的左边包括a1的数,即第一个数组的范围变为nows1p+1,s1r,且我们要知道这里到底抛弃了多少个数,容易计算得出为 (nows1p+1-s1p) 则k需要减去抛弃掉的数
- 如果k==mid1+mid2+2 此时并不一定结果是b1,因为还需要考虑a1的右边,所以此情况应该归为情况二
递归终点:同思路一
注意:我们一定要搞明白每种情况应该是什么样的,并且一定要小心边界的问题,在这里我们由于每次都有下标的变化,所以才使得递归一直在进行。而在一开始,我个人由于没用分清楚每种情况,所以导致有时候递归一直在一个地方打转,自己也搞不清楚哪里出了错误,但是静下心来仔细分析后,才真正确定了边界位于何处,终于把每种情况处理妥当。
附代码:
递归版:
int findKthNumber(vector<int>nums1,vector<int>nums2,int s1p,int s1r,int s2p,int s2r,int k)
{
if(s1p==s1r+1)
return nums2[s2p+k-1];
if(s2p==s2r+1)
return nums1[s1p+k-1];
if(k==1)
return min(nums1[s1p],nums2[s2p]);
int mid1=(s1r-s1p+1)/2;
int mid2=(s2r-s2p+1)/2;
int nowsp1=s1p+mid1;
int nowsp2=s2p+mid2;
int a1=nums1[nowsp1];
int b1=nums2[nowsp2];
if(a1<=b1)
{
if(k<mid1+mid2+2)
return findKthNumber(nums1,nums2,s1p,s1r,s2p,nowsp2-1,k);
else
return findKthNumber(nums1,nums2,nowsp1+1,s1r,s2p,s2r,k-(nowsp1-s1p+1));
}
else
{
if(k<mid1+mid2+2)
return findKthNumber(nums1,nums2,s1p,nowsp1-1,s2p,s2r,k);
else
return findKthNumber(nums1,nums2,s1p,s1r,nowsp2+1,s2r,k-(nowsp2-s2p+1));
}
}
迭代版:
int findKthNumber(vector<int>nums1,vector<int>nums2,int s1p,int s1r,int s2p,int s2r,int k)
{
while(1)//直到找到为止
{
if(s1p==s1r+1)
return nums2[s2p+k-1];
if(s2p==s2r+1)
return nums1[s1p+k-1];
if(k==1)
return min(nums1[s1p],nums2[s2p]);
int mid1=(s1r-s1p+1)/2;
int mid2=(s2r-s2p+1)/2;
int nowsp1=s1p+mid1;
int nowsp2=s2p+mid2;
int a1=nums1[nowsp1];
int b1=nums2[nowsp2];
if(a1<=b1)
{
if(k<mid1+mid2+2)
{
s2r=nowsp2-1;
}
else
{
k=k-(nowsp1-s1p+1);
s1p=nowsp1+1;
}
}
else
{
if(k<mid1+mid2+2)
{
s1r=nowsp1-1;
}
else
{
k=k-(nowsp2-s2p+1);
s2p=nowsp2+1;
}
}
}
}
3. 总结
通过练习,发现自己的抽象思维能力还是比较差,特别是在对奇偶性的判断,对边界的判断上无从下手,自我感觉最重要的就是静下心来分析问题,一个问题总是能被分解成诸多小问题,不太可能会交叉,也不太可能会有一部分没有考虑到,只要耐心细致地全局考虑,最后的结果一定是连在一起的,继续努力!