2020-07-23[日三省吾身] 二分搜索主题刷题过程思考记录

前排感谢labuladong大佬的模板,大多数分析是摘录其公众号文章!强推

疑惑:二分法和双指针法的应用场景异同

二分法是双指针法的一种情况,双指针法分为两类:快慢指针和左右指针
快慢指针主要解决的是链表中的问题,例如判断链表中是否包含环
左右指针主要解决数组或字符串中的问题,例如二分查找(是否需要排序呢?)

二分法中的左右指针在数组中实际是指连个索引值,一般初始化为left=0;right=nums.length()-1

二分法中的注意点:

1.整型溢出bug??

2.mid加一减一的情况

3.while中判断用<=还是<

分三种场景:寻找一个数、寻找左边界、寻找右边界

基本框架:

int binarySearch(int[] nums, int target) 
{
    int left = 0, right = ...;
    while(...) 
    {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) 
        {
            ...
        } 
        else if (nums[mid] < target)
        {
            left = ...
        } 
        else if (nums[mid] > target) 
        {
            right = ...
        }
    }
    return ...;
}

分析⼆分查找的⼀个技巧是:不要出现 else,⽽是把所有情况⽤ else if 写清
楚,这样可以清楚地展现所有细节。

上述中$...$所在处为需要注意的细节;

还要注意防止mid计算溢出,left + (right - left) / 2(right + left) / 2更安全,但前者有效避免溢出

搜索区间定义

  • 左闭右闭:left = 0; right = nums.length -1;
  • 左闭右开/ 左开右闭(情况基本一致,下述统一前者):left = 0; right = nums.length;

while中是否要加等号

取决于搜索的区间是左闭右闭left<=right,还是左闭右开left

——错误会导致越界,超出nums.length

  • 思考方式(搜索区间):停止while循环有两种情况,此处以左闭右闭为例
  • 一种为找到目标值if(nums[mid] == target) return mid;
  • 另一种为剩余的搜索集为空,left==right+1->[right+1,right]停止条件符合搜索区间为空$([3,2])$,但是left==right->[right,right]停止条件不符合搜索区间为空$([2,2])$,同理如果左闭右开则left==right符合停止条件,即while判断不需要加等号

mid中加一减一,依旧是依据搜索区间

  • 思考方式:
  • 如果是左闭右闭的区间情况:left = mid+1; right=mid-1;则下次的搜索区间会变成[left,mid-1]或者[mid+1,right];
  • 如果是左闭右开的区间情况:left = mid+1; right=mid;则下次的搜索区间会变成[left,mid)或者[mid+1,right);
  • 如果是左开右闭的情况:left = mid; right=mid-1;则下次的搜索区间会变成(left,mid-1]或者(mid,right]

返回值

四种情况:

  • 固定值return mid;
  • 左侧边界return left;//不断收缩右侧边界;
  • 右侧边界return right-1;/return left-1//此时left==right,不断收缩左侧边界,由于收缩条件的特殊性mid = left-1;/left = mid+1;,搜索结束时nums[left-1]才可能是目标值;
  • -1(说明没有搜索到,越界情况判断)
//搜索固定值,对应两种情况
while(left<=right) --> return -1;
while(left return nums[left] == target ? left : -1;
//搜索左侧边界
if (left >= nums.length || nums[left] != target)
    return -1;//左侧越界
return left;
//搜索右侧边界
if (right < 0 || nums[right] != target)
    return -1;//右侧越界
return right;

总结

int binary_search(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while(left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if(nums[mid] == target) {
            // 直接返回
            return mid;
        }
    }
    // 直接返回
    return -1;
}

int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定左侧边界
            right = mid - 1;
        }
    }
    // 最后要检查 left 越界的情况
    if (left >= nums.length || nums[left] != target)
        return -1;
    return left;
}

int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定右侧边界
            left = mid + 1;
        }
    }
    // 最后要检查 right 越界的情况
    if (right < 0 || nums[right] != target)
        return -1;
    return right;
}

刷题分割线


目前掌握的是左右指针的方法,两分法还没有成熟运用。

后续做完的题要回头看一下如何运用两分法,并且判断是否可以提高效率

还可以尝试动态规划问题,用KMP的算法思想,判断几者间是否有效率的差异

[167] 两数之和 II - 输入有序数组

class Solution {
public:
    vector twoSum(vector& numbers, int target) {
        int left = 0;
        int right = numbers.size()-1;
       // vector index;
        while(left<=right)
        {
           // int mid = left+(right-left)/2;//索引的中位数(奇数为中心,偶数为右侧)
            if(numbers[left]+numbers[right]==target){//终止条件
                return vector{++left,++right};//索引从1开始
            }
            else if (numbers[left]+numbers[right]>target){//右边界左移
                right--;
            }
             else if (numbers[left]+numbers[right]{};
    }
};

[34] 在排序数组中查找元素的第一个和最后一个位置

class Solution {
public:
    vector searchRange(vector& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right)//结束条件,左闭右闭
        {
            if (nums[left]target)//再缩小右界
            {
                right--;
            }
            else if (nums[left] == target&&nums[right] == target)//找到左界和右界
            {
                return vector{left, right};
            }
        }
        return vector{-1, -1};//未查找到
    }
};

这个题目一开始没有注意if-else if的逻辑,都用if导致其中的判断溢出了
刚开始理解的话还是用if-else if确定逻辑比较好

[50] Pow(x, n)

class Solution {
public:
    double myPow(double x, int n) {
        if(n==0) return 1;
        if(n<0) return _pow(1/x,n);
        else return _pow(x,-n);//都用负数处理,防止溢出        
    }
private:
double _pow(double x, int n)
{
    if(n==0) return 1;//base case
    int count = -1;
    double num = x;
    while(n-count<=count)
    {
        num*=num;
        count+=count;
    }
    return num*_pow(x,n-count);
}
};

这道题借鉴的是29两数相除的思路,重点在于负数处理的思路,以及递归的思路,题解在下也列出供比对参照。
如果一个一个x乘以n次肯定会超出时间限制,所以考虑一下方法。
主要是将x以幂级数次(count次)相乘,当count逼近次数n时就回归正常幂级数计算,即剩下(n-count)个x相乘。

[29] 两数相除

注意!!由于-INT_MIN=INT_MIN,因此应该转变思路,把所有正值变为负值就不存在溢出的问题了。
用负数思考一定要注意dividend <= divisor 解才会大于0

class Solution {
public:
    int divide(int dividend, int divisor) {
        if(divisor==0) return 0;
        if(divisor==1) return dividend;
        if(divisor==-1) return (dividend==INT_MIN)?INT_MAX:-dividend;//溢出判断
        //记录符号
        int sign=0;
        if((dividend < 0 && divisor>0)||(dividend > 0 && divisor < 0)) sign=-1;
        int _a = dividend < 0?dividend:-dividend;
        int _b = divisor < 0?divisor:-divisor;
        //皆为负数,则不用考虑溢出
        int res = div(_a,_b);
        if(res==INT_MIN && sign==-1) return INT_MAX;
        return sign==-1? (-res) : res;
    }
private:
    int div(int dividend, int divisor) //递归法,用幂级数2^0+2^1+...逼近,余下部分再计算是除数的几倍
    {
        if(dividend>divisor) return 0;//负数!注意符号
        int num =1;
        int minus = divisor;
        while(minus>=dividend-minus){//负数!注意符号
            minus+=minus;
            num+=num;
        }
        return num+div(dividend-minus,divisor);
    }
};

[74] 搜索二维矩阵

该问题有以下几种思路:

  1. 两次二分法,但是要注意边界条件;并且while后面的判断要注意
if (matrix.size()==0||matrix[0].size()==0) return false;
        //[[]]这种情况下matrix.size()==1,matrix[0].size()==0;
        //[]这种情况也要考虑,matrix[0].size()会溢出,此时matrix.size()==0;

网上看到的一种写法:

if (matrix==null||matrix.length()==0||matrix[0]==null||matrix[0].length()==0) return false;

网上看到的还有一种写法:

if (matrix.empty()||matrix[0].empty()) return false;
  1. 两维变一维(映射或者整除取模)
int mid = start + (end - start)/2;
int e = matrix[mid/n][mid%n];//n=matrix[0].length
  1. 缩小领域法:转换思路从右上角开始找或者左下角开始找,每次比较可以排除一行/一列,时间复杂度为$O_{(m+n)}$

第一种两次两分,主要是在第一次判断存在于哪一行时;经过验证当top==bottom==mid时,如果target存在于当前行,则matrix[mid][0] < target;top = mid + 1>bottom;满足停止条件,并且此时bottom为所在行。 总结下来while后面的判断主要还是依据搜索集是否为左闭右闭,而if判断则是根据不同的需求具体分析。

class Solution {
public:
bool searchMatrix(vector>& matrix, int target) {
        if (matrix.size() == 0 || matrix[0].size() == 0) return false;
        //[[]]这种情况下matrix.size()==1,matrix[0].size()==0;
        //[]这种情况也要考虑,matrix[0].size()会溢出,此时matrix.size()==0;
        if (targetmatrix[matrix.size() - 1][matrix[0].size() - 1]) return false;//界外
        int top = 0;
        int bottom = matrix.size() - 1;
        while (top<=bottom)//先判断存在于哪一行,两分法加速
        {
            int mid = (top + bottom) / 2;
            if (matrix[mid][0] < target)//上界收缩
            {
                top = mid + 1;
            }
            else if (matrix[mid][0] > target)//下界收缩
            {
                bottom = mid-1;
            }
            else if (matrix[mid][0] == target)//位于某行首位,则直接返回
            {
                return true;
            }
        }//结束时top==bottom,bottom即target存在的那一行
        int left = 0;
        int right = matrix[0].size() - 1;
        while (left <= right)
        {
            int mid = (left + right) / 2;
            if (matrix[bottom][mid]target)//右界收缩
            {
                right = mid - 1;
            }
            else if (matrix[bottom][mid] == target)//位于某行首位,则直接返回
            {
                return true;
            }
        }
        return false;
    }
};

第二种映射成一维数组,并没有两次两分速度快。

    // treat the matrix as an array, just taking care of indices
    // [0..n*m]
    // (row, col) -> row*n + col
    // i -> [i/n][i%n]
class Solution {
public:
bool searchMatrix(vector>& matrix, int target) {
        if (matrix.size() == 0 || matrix[0].size() == 0) return false;
        //[[]]这种情况下matrix.size()==1,matrix[0].size()==0;
        //[]这种情况也要考虑,matrix[0].size()会溢出,此时matrix.size()==0;
        if (targetmatrix[matrix.size() - 1][matrix[0].size() - 1]) return false;//界外
        int left = 0;
        int right = matrix.size()*matrix[0].size() - 1;
        //映射到一维数组考虑
        while (left <= right)
        {
            int mid = (left + right) / 2;
            if (matrix[mid/matrix[0].size()][mid%matrix[0].size()]target)//右界收缩
            {
                right = mid - 1;
            }
            else if (matrix[mid/matrix[0].size()][mid%matrix[0].size()] == target)//存在
            {
                return true;
            }
        }
        return false;
    }
};

第三种从左下角或者右上角开始检索更加快,需要注意的是判断停止的条件

class Solution {
public:
bool searchMatrix(vector>& matrix, int target) {
        if (matrix.size() == 0 || matrix[0].size() == 0) return false;
        //[[]]这种情况下matrix.size()==1,matrix[0].size()==0;
        //[]这种情况也要考虑,matrix[0].size()会溢出,此时matrix.size()==0;
        if (targetmatrix[matrix.size() - 1][matrix[0].size() - 1]) return false;//界外——有这句话能快一些
        int m = matrix.size()-1;
        int n = 0;
        //从左下角开始检索
        while (m>=0&&n=0针对只有一行的情况,否则会有问题
        {
            if (matrix[m][n]>target)//上移一行
            {
                m--;
            }
            else if (matrix[m][n]

[81] 搜索旋转排序数组 II

较简单,可以后续思考一下不用sort的处理方式

class Solution {
public:
    bool search(vector& nums, int target) {
        if(nums.empty()) return false;
        //1.sort
        sort(nums.begin(),nums.end());
        //2.binary-search
        int left = 0;
        int right = nums.size()-1;
        while(left<=right)
        {
            int mid = left+(right-left)/2;
            if(nums[mid]target){
                right = mid-1;
            }
            else if(nums[mid]==target){
                return true;
            }
        }
        return false;        
    }
};

[4] 寻找两个正序数组的中位数

直接用STL函数就很简单了,这道题可以跟[88]合并两个有序数组结合来看,STL部分完全就是合并数组的思路,可以用递归的方式。

class Solution {
public:
    double findMedianSortedArrays(vector& nums1, vector& nums2) {
        if(!nums2.empty()) copy(nums2.begin(),nums2.end(),back_inserter(nums1));//拼接
        if(nums1.empty()) return -1;//无输入
        sort(nums1.begin(),nums1.end());//排序
        int mid = (nums1.size()-1)/2;
        if(nums1.size()%2==0) return (double)(nums1[mid]+nums1[mid+1])/2;
        else return nums1[mid];  
    }
};

[88] 合并两个有序数组

这道题一开始理解错了题中你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。 的含义,实际上nums1中最后n位已经用0补齐了,所以可以用双指针一头一尾的方法不需要开辟额外的内存空间即可实现。
先放一个STL模板版本:

class Solution {
public:
    void merge(vector& nums1, int m, vector& nums2, int n) {
        if(!nums1.empty()) nums1.erase(nums1.begin()+m,nums1.end());
        if(!nums2.empty()) copy(nums2.begin(),nums2.begin()+n,back_inserter(nums1));
        if(!nums1.empty()) sort(nums1.begin(),nums1.end());
    }
};

双指针版本:
这个版本的重点在于最后一句!一定要注意边界条件!

class Solution {
public:
    void merge(vector& nums1, int m, vector& nums2, int n) {
        int i = m--+--n;//--在后,先运算后赋值,--在前,先赋值后运算,i=m+n-1,但是m,n作为索引值都需要减一
        while(m>=0&&n>=0)
        {
            nums1[i--] = nums1[m]

当然还有一种经典的谁大换谁算法,没有上面一种速度快

class Solution {
public:
    void merge(vector& nums1, int m, vector& nums2, int n) {
        int end = nums1.size()-1;
        m--;
        n--;
        while(n>=0){
            while(m>=0&&nums2[n]

这几种的内存利用率都不高,速度是2>3>1

[153][154] 寻找旋转排序数组中的最小值

这两道题都有两种方案:

  • 二分法,最小的点即图中的min点,是数组中第一个小于nums[end]的数字。不能考虑是第一个小于nums[start]的数字是因为当数组严格递增时就不成立了。
          *
      *
start
----------------------
                 end
              *
         min

因此二分法有以下两种情况:(相当于找左侧边界,判断条件应该是左闭右开-因为不是纯升序的)

  1. mid可能定位在左边,即nums[mid]>nums[end],我们需要在mid右边搜索变化点,此时我们需要将start的位置调整到mid处
  2. mid可能定位在右边,即nums[mid],我们需要在mid左侧搜索变化点,此时我们需要将end的位置调整到mid处
  • 先排序后取首

先是用排序取首,主要用于比较效率——运算快但耗内存

class Solution {
public:
    int findMin(vector& nums) {
        if(!nums.empty()) 
        {
            sort(nums.begin(),nums.end());
            return nums[0];
        }
        return 0;
    }
};

二分法:貌似效率没差别

class Solution {
public:
    int findMin(vector& nums) {
        int left = 0;
        int right = nums.size()-1;
        while(leftnums[right]){
                left = mid+1;//缩小左界
            }
        }
        return nums[left];
    }
};

[154] 的差别主要是要考虑nums[mid]==nums[end]的情况,由于数组中可能有重复数据,因而无法判断变化点具体在左侧还是有右侧。

我们采用 right = right - 1 解决此问题,证明:
此操作不会使数组越界:因为迭代条件保证了 right > left >= 0
此操作不会使最小值丢失:假设 nums[right]nums[right] 是最小值,有两种情况:

nums[right]nums[right] 是唯一最小值:那就不可能满足判断条件 nums[mid] == nums[right],因为mid < right(left != right 且 mid = (left + right) // 2向下取整);

nums[right]nums[right] 不是唯一最小值,由于mid < rightnums[mid] == nums[right],即还有最小值存在于 [left, right - 1][left,right−1] 区间,因此不会丢失最小值。

class Solution {
public:
    int findMin(vector& nums) {
        int left = 0;
        int right = nums.size()-1;
        while(leftnums[right]){
                left = mid+1;//缩小左界
            }
            else//相等时不确定在左侧还是右侧,但是mid一定是在右边界左侧,右边界可忽略
            {
                right--;
            }
        }
        return nums[left];
    }
};

效率看下来并无太大差异,仍旧是速度快但耗内存

[162] 寻找峰值

在题目描述中出现了 nums[-1] = nums[n] = -∞,这就代表着只要数组中存在一个元素比相邻元素大,那么沿着它一定可以找到一个峰值

二分法思路很简单,根据左右指针计算中间位置 m,并比较 m 与 m+1 的值,如果 m 较大,则左侧存在峰值,r = m,如果 m + 1 较大,则右侧存在峰值,l = m + 1

class Solution {
public:
    int findPeakElement(vector& nums) {
        int left =0;
        int right = nums.size()-1;
        if(nums.size()==1) return 0;//
        //if(nums.size()==2) return nums[left]>nums[right]?left:right;//
        while(leftnums[mid+1]) //峰值在左侧
            {
                right = mid;
            }
            else if(nums[mid]

[315] 计算右侧小于当前元素的个数

一开始比较傻的解法,超时了。思路就是将输入数组与索引号一起排序,从最大值往前统计索引值比当前值大的个数,即为当前值右侧小于当前值的个数。再按照之前的索引值输出。思路比较简单,但是最后一个用例超时了。

class Solution {
public:
    vector countSmaller(vector& nums) {
        vector> nums_sort;
        vector res;
        for (int i = 0; i temp{ nums[i] ,i};
            nums_sort.push_back(temp);
        }
        sort(nums_sort.begin(), nums_sort.end());//搜索表(从小到大)
        //从后往前计算
        for (int n = nums.size() - 1; n >=0;n--)
        {
            int num = 0;
            int i = n - 1;
            while ( i >=0)
            {
                if (nums_sort[i--][1]>nums_sort[n][1])  num++;
            }
            nums_sort[n][0] = num;
        }
        sort(nums_sort.begin(), nums_sort.end(),[](const vector &a, const vector &b) {return a[1] < b[1]; });//根据第二列排序(升序)
        //提取第一列
        for (int i = 0; i

于是思路不变,参考归并排序结合二分法查找简化了一下。需要注意的是一开始使用二分查找的时候按照查找target的方法来报错了,因为可能会由于重复数定位到了右侧的索引值,所以改成了查找左侧边界。
具体思路:表中第一行指针ptr1逐一往右,表中第二行用二分法查找对应*ptr1的位置,索引值(重复数则左侧的索引值)即为*ptr1元素右侧小于当前元素的个数num,将*ptr1的值改写为num,并在表中第二行删除*ptr1的值(左侧元素完全不用考虑),重复上述操作直至第一行遍历完成,即可返回第一行。

ptr1 5 2 6 1
ptr2 1 2 5 6
class Solution {
public:
    vector countSmaller(vector& nums) {
        vector nums_sort = nums;
        sort(nums_sort.begin(), nums_sort.end());//搜索表(从小到大)
        for (int i = 0; inums[i])
                {
                    right = mid - 1;
                }
            }
            nums[i] = left;
            nums_sort.erase(nums_sort.begin() + left);
        }
        return  nums;
    }
};

看官方还有很多其他解法,没有看明白等后面再研究吧。。。

待续。。。

你可能感兴趣的:(2020-07-23[日三省吾身] 二分搜索主题刷题过程思考记录)