2021-01-29

本文深入探讨了二分查找算法的三种常见模板,包括其时间复杂度、应用场景和实例解析。从基础的有序数组查找,到处理旋转排序数组和寻找峰值问题,详细阐述了如何根据题目需求选择合适的二分查找变体。同时强调了理解题目条件和构建正确模板对于解决问题的重要性。
摘要由CSDN通过智能技术生成

二分法

复杂度:O(N*logN)

模板一

int binarySearch(int[] nums, int target){
  if(nums == null || nums.length == 0)
    return -1;

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

  // End Condition: left > right
  return -1;
}

1.1区分:

  • 初始条件:left = 0, right = length-1
  • 终止:left > right
  • 向左查找:right = mid-1
  • 向右查找:left = mid+1
  • 查找条件可以在不与元素的两侧进行比较的情况下确定(或使用它周围的特定元素)。

1.2例题

69. x 的平方根

简述:手写sqrt

int mySqrt(int x){
    if( x == 0 || x == 1)
        return x;
    int left = 0 , right = x;
    int ans = 0;
    while( left <= right )
    {
        int mid = (left+right)/2;
        if( (long long)mid *mid <= x )
        {
            ans = mid;
            left = mid + 1;
        }
        else
        {
            right = mid - 1;
        }
    }
    return ans;
}
374. 猜数字大小

简述:猜数字

注意 : 原题的guess函数太过SB,原题乱tm翻译,一直wa

看了评论才发现

/** 
 * Forward declaration of guess API.
 * @param  num   your guess
 * @return 	     -1 if num is lower than the guess number
 *			      1 if num is higher than the guess number
 *               otherwise return 0
 * int guess(int num);
 */
int guessNumber(int n){
    int left = 1 , right = n;
    int mid = (left + right)/2;
    while( left <= right )
    {
        int mid = left + (right-left)/2;
        if( guess(mid) == -1)
        {
            right = mid - 1;
        }
        else if( guess(mid) == 1)
        {
            left = mid + 1;
        }
        else if( guess(mid) == 0)
        {
            return mid;
        }
    }
    return -1;
}
33. 搜索旋转排序数组
思路:

该数组为旋转数组,就一定旋转,后面会有不旋转的(tmd)

我们的二分一般情况下喜欢有序数组(当然也有表面无序,实则有规则,但整体来说有序)

数字条件:不重复

我们要找有序的一侧,例如:

4,5,6,7,0,1,2

我们 [4-7] 和 [0-2]为有序,所以

mid = left + (right-left)/2;
//nums[mid] = 7;
if( nums[0] > nums[mid] )	//mid 左侧有序,单调递增
{
	....
}
// nums[0] < nums[mid] , 所以[0-mid]数组走过下坡路
else //右侧有序,
{
	...
}

这是一个点

然后我们在判断,我们的target是否包在我们的有序区间

1、在,就在这个区间缩小范围二分

2、不在,换区间

int search(int* nums, int numsSize, int target){
    if( numsSize == 1) return nums[0] == target ? 0 : -1;
    if( numsSize == 0)return -1;

    int left = 0 , right = numsSize - 1;
    int mid;
    while(left <= right)
    {
        mid = left + (right-left)/2;
        if(nums[mid] == target)
        {
            return mid;
        }
        
        if( nums[0] <= nums[mid] ) //左侧有序
        {
            if( nums[left] <= target && nums[mid] > target ) //有序的区间内
            {
                right = mid-1;
            }
            else //不在这个区间,换
            {
                left = mid + 1;
            }
        }
        else //右侧有序
        {
            if( nums[mid] < target && nums[numsSize-1] >= target) //有序的区间内
            {
                left = mid + 1;
            }
            else //不在这个区间,换
            {
                right = mid - 1;
            }
        }
    }
    return -1;
}

模板二

int binarySearch(int[] nums, int target){
  if(nums == null || nums.length == 0)
    return -1;

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

  // Post-processing:
  // End Condition: left == right
  if(left != nums.length && nums[left] == target) return left;
  return -1;
}
属性
  • 查找条件需要访问元素的直接右邻居。
  • 保证查找空间在每一步中至少有 2 个元素。
  • 当你剩下 1 个元素时,退出。也就是 left 和 right 指向同一个
区别
  • 初始条件:left = 0, right = length
  • 终止:left == right
  • 向左查找:right = mid
  • 向右查找:left = mid+1

2.1例题

278. 第一个错误的版本
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);

int firstBadVersion(int n) {
    int left = 0 , right = n;
    while( left < right )
    {
        int mid = left + (right-left)/2;
        if( isBadVersion(mid) ) //true;
        {
            right = mid;
        }
        else
        {
            left = mid + 1;
        }
    }
    return left;
}//啊这.....

//挺简单的没啥说的

162. 寻找峰值
思路:

1、对第一个元素和最后一个元素,特判一下

条件:

num[i] != num[i+1]

我们不妨假设下特殊情况

1 2 3 4 7 5 6 8 9	//ans = 7
1 2 3 1 4 5 7 8 	//ans = 3 , ans = 8 
Q : 为什么二分查找大的那一半一定会有峰值呢 ?(关键)

首先已知 nums[mid+1]>nums[mid] ,

那么mid+2只有两种可能:

1、是大于mid+1

2、是小于mid+1

小于mid+1的情况,那么mid+1就是峰值

大于mid+1的情况,继续向右推。

如果一直到数组的末尾都是大于的,那么可以肯定最后一个元素是峰值,因为nums[numsSize] = 负无穷

妥了。

int findPeakElement(int* nums, int numsSize ){
    int l = 0, r = numsSize - 1;
        while (l < r) {
            int mid = (l + r) / 2;
            if (nums[mid] > nums[mid + 1])
                r = mid;
            else
                l = mid + 1;
        }
        return l;
    }
153. 寻找旋转排序数组中的最小值
思路:

它来了它来了,上面说可能不旋转的

wa全是单调数据

思路都差不多,千篇一律

不过,我们分为旋转和不旋转来讨论,用:

nums[0] < nums[numsSize-1]
nums[0] > nums[numsSize-1]

来判断单增还是单减

其他的和上面旋转的思路差不多

int findMin(int* nums, int numsSize){
    if( numsSize == 1 )return nums[0];
    int left = 0 , right = numsSize - 1;
    while( left < right )
    {
        int mid = left + (right-left)/2;
        //旋转
        if( nums[0] < nums[mid] && nums[0] > nums[numsSize-1] )//左侧有序
        {
            left = mid + 1;
        }
        //旋转
        else if( nums[mid] < nums[numsSize-1] && nums[0] > nums[numsSize-1] )
        {
            right = mid;
        }
        //单增
        else if( nums[0] < nums[numsSize-1] )
        {
            right = mid;
        }
        //单减
        else
        {
            left = mid + 1; 
        }
    }
    return nums[left];
}

模板三

不大常用

int binarySearch(int[] nums, int target) {
    if (nums == null || nums.length == 0)
        return -1;

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

    // Post-processing:
    // End Condition: left + 1 == right
    if(nums[left] == target) return left;
    if(nums[right] == target) return right;
    return -1;
}
区别
  • 初始条件:left = 0, right = length-1
  • 终止:left + 1 == right
  • 向左查找:right = mid
  • 向右查找:left = mid

例题

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

先找到相等的位置,只要找到我们就好说了,直接暴力法

二分法查找我们需要的元素,但凡找到一个

向左向右用循环,来扩散

最后写入答案

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* searchRange(int* nums, int numsSize, int target, int* returnSize){
    int *ans = (int *)malloc(sizeof(int)*2);
    ans[0] = -1;
    ans[1] = -1;
    int left = 0 , right = numsSize-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 
        {
            left = mid;
            right = mid;
            while( left >= 0 && nums[left] == target )
            {
                left--;
            }
            while( right < numsSize && nums[right] == target )
            {
                right++;
            }
                ans[0] = left + 1;
                ans[1] = right - 1;
            break;
        }
    } 
    *returnSize = 2;
    return ans;
}
658. 找到 K 个最接近的元素

思路:

这次的二分不是下标二分了,而是值二分,因为我们的值有序

  while( right - left >= k )
    {
        if( (arr[left] + arr[right]) >= x*2 )
        {
            right--;
        }
        else
        {
            left++;
        }
    }

我们的答案长度得是K啊

然后数组值二分

下标不能移动太狠了

然后对x的值特判一下,是不是在数组外,在数组外,直接就写入

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* findClosestElements(int* arr, int arrSize, int k, int x, int* returnSize){
    int *ans = (int *)malloc(sizeof(int)*arrSize);
    *returnSize = k;
    if( x < arr[0] )
    {
        for(int i = 0 ; i < k ; i++)
        {
            ans[i] = arr[i];
        }
        return ans;
    }
    if( x > arr[arrSize-1] )
    {
        int index = 0;
        for(int i = arrSize - k; i < arrSize ; i++ )
        {
            ans[index++] = arr[i];  
        }
        return ans;
    }

    int left = 0 , right = arrSize-1;
    while( right - left >= k )
    {
        if( (arr[left] + arr[right]) >= x*2 )
        {
            right--;
        }
        else
        {
            left++;
        }
    }
    
   return &arr[left];
}
162. 寻找峰值
int findPeakElement(int* nums, int numsSize ){
    int l = 0, r = numsSize - 1;
        while (l < r) {
            int mid = (l + r) / 2;
            if (nums[mid] > nums[mid + 1])
                r = mid;
            else
                l = mid + 1;
        }
        return l;
    }

总结

模板很重要,是做题的第一思路

不过题和题差别还是需要对要求和一些条件的理解,所以模板只是帮你快速分析以及构建框架

具体的内容,要以题目来拟定

img

模板 #1 (left <= right)

二分查找的最基础和最基本的形式。
查找条件可以在不与元素的两侧进行比较的情况下确定(或使用它周围的特定元素)。
不需要后处理,因为每一步中,你都在检查是否找到了元素。如果到达末尾,则知道未找到该元素。

模板 #2 (left < right)

一种实现二分查找的高级方法。
查找条件需要访问元素的直接右邻居。
使用元素的右邻居来确定是否满足条件,并决定是向左还是向右。
保证查找空间在每一步中至少有 2 个元素。
需要进行后处理。 当你剩下 1 个元素时,循环 / 递归结束。 需要评估剩余元素是否符合条件。

模板 #3 (left + 1 < right)

实现二分查找的另一种方法。
搜索条件需要访问元素的直接左右邻居。
使用元素的邻居来确定它是向右还是向左。
保证查找空间在每个步骤中至少有 3 个元素。
需要进行后处理。 当剩下 2 个元素时,循环 / 递归结束。 需要评估其余元素

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值