Leetcode算法笔记(一)——分治法

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. 总结

通过练习,发现自己的抽象思维能力还是比较差,特别是在对奇偶性的判断,对边界的判断上无从下手,自我感觉最重要的就是静下心来分析问题,一个问题总是能被分解成诸多小问题,不太可能会交叉,也不太可能会有一部分没有考虑到,只要耐心细致地全局考虑,最后的结果一定是连在一起的,继续努力!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值