数组 | 704. 二分查找

参考:

1. 二分查找详解 - labuladong - 博客园

2. 七种不同要求的二分法(c++) - 知乎

目录

题目:704.二分查找(力扣)

零.描述

1. 最常用的二分查找场景

2. 二分法框架 

区别:左闭右闭 or 左闭右开

3. 左闭右闭 [left,right]

3. 左闭右开 [left,right)

一、七种情况(原文见参考2)

总结:

1、查找数组中等于key的数字

2、查找数组中第一个等于key的数字

3、查找数组中最后一个等于key的数字

4、查找数组中第一个大于等于key的数字

5、查找数组中最后一个小于等于key的数字

6、查找数组中第一个大于key的数字

7、查找数组中最后一个小于key的数字


题目:704.二分查找(力扣

零.描述

思路不难,难于细节

1. 最常用的二分查找场景

寻找一个数、寻找左侧边界、寻找右侧边界。
 

细节:while (不等号) 是否应该带等号; mid 是否应该加一

2. 二分法框架 

int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;                       //注意1: right
    while(...) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) { ...} 
        else if (nums[mid] < target) { left = ...}   //注意2: 下一次left取值
        else if (nums[mid] > target) { right = ...}
   } 
    return ...;
}

区别:左闭右闭 or 左闭右开

3. 左闭右闭 [left,right]

关键:当left ==right 区间左闭右闭,仍然有效 ,因此需要while( <= )

举例:{1,3,4 }寻找4,如果缺少等于,就找不到

L=0,R=2,M=1 ,nums[M]=3<4

L=2,R=2,缺少等于,不进入while循环,错误输出-1

//搜索一个数,如果存在,返回其索引,否则返回 -1。
int Search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;                         // 注意[l,r]
    while(left <= right) {                               // 注意while(l<=r)  "<="
    // 当left==right,区间[left, right]依然有效,所以用 <=

        int mid = left + (right - left) / 2;
        if(nums[mid] == target)     return mid;
        else if (nums[mid] < target)    left = mid + 1;  // 注意
                                        // target 在右区间,所以[middle + 1, right]

        else if (nums[mid] > target)    right = mid - 1; // 注意
                                        // target 在左区间,所以[left, middle - 1]
    } 
    return -1;
}

// 同理
int Search(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size() - 1; 
    while (left <= right) { 
    // 当left==right,区间[left, right]依然有效,所以用 <=

        int middle = left + ((right - left) / 2);
        if (nums[middle] > target) {
            right = middle - 1;          // target 在左区间,所以[left, middle - 1]

        } else if (nums[middle] < target) {
            left = middle + 1;           // target 在右区间,所以[middle + 1, right]

        } else{
            return middle;               // nums[middle] == target
        }                                // 数组中找到目标值,直接返回下标
    }
        // 未找到目标值
        return -1;
}

3. 左闭右开 [left,right)

关键: 因为left == right的时候,在[left, right)是无效的空间,所以使用 <

举例:{1,3,4 }寻找4

L=0,R=3,M=1 ,nums[M]=3<4

L=2,R=3,M=2,nums[M]=4 正确输出

int search(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size();         // 定义target在左闭右开的区间里,即:[left, right)
    while (left < right) { 
    // 因为left == right的时候,在[left, right)是无效的空间

        int middle = left + ((right - left) >> 1);
        if (nums[middle] > target) {
            right = middle;          // target 在左区间,在[left, middle)中

        } else if (nums[middle] < target) {
            left = middle + 1;       // target 在右区间,在[middle + 1, right)中

        } else {                     // nums[middle] == target
            return middle;           // 数组中找到目标值,直接返回下标
        }
     }
     // 未找到目标值
     return -1;
}

一、七种情况(原文见参考2)

总结:

  1. 查找数组中等于key的数字
    基础模板,常用[left,right]
  2. 查找数组中第一个等于key的数字
    区别:去arr[mid]== key分支,改arr[mid]>key 为 >=
               加判断​ ​​​​​​if(L < arr.size() && arr[L] == key)
               输出L
  3. 查找数组中最后一个等于key的数字
    区别:去arr[mid]== key分支,改arr[mid]<key 为 <=
               加判断​ ​​​​​​if(R >= 0 && arr[R] == key)
               输出R
  4. 查找数组中第一个大于等于key的数字
    区别:去arr[mid]== key分支,改arr[mid]>key 为 >=
               加判断​ ​​​​​​if(L < arr.size())
               输出L
  5. 查找数组中最后一个小于等于key的数字
    区别:去arr[mid]== key分支,改arr[mid]<key 为 <=
               不要加判断​ ​​​​​​if(R >= 0)不要return -1
               输出R
  6. 查找数组中第一个大于key的数字
    区别:去arr[mid]== key分支,依旧arr[mid]>key 
               不要加判断​ ​​​​​​if(L < arr.size()) 不要return -1
               输出L
  7. 查找数组中最后一个小于key的数字
    区别:去arr[mid]== key分支,依旧arr[mid]<key 
               不要加判断​ ​​​​​​if(R >= 0)不要return -1
               输出R

1、查找数组中等于key的数字

如上文所述。


2、查找数组中第一个等于key的数字

问题:数组中可能有重复的key,要找的是第一个key的位置
分析:找到一个等于key的数,依然还要继续寻找它的前面有没有等于key的数。

  • 修改之前的arr[mid]>key修改为arr[mid]>=key,并且去掉arr[mid]== key这条分支,
    这样就算遇到了等于key的答案,依然不会返回
  • 程序继续往前寻找是否有其它等于key的值
  • 最后跳出循环之后,如果L满足L<arr.size()&&arr[L]==key,返回L的值作为结果,
  • 否则输出-1,代表没有找到。

代码:

/**查找第一个与key相等的元素的下标, 如果不存在返回-1 */
int erfenfirstEqual(vector<int> arr, int key){
    int L = 0, R = arr.size() - 1;          //在[L,R]查找第一个>=key的
    int mid;

    while( L <= R){
    // 采用[left,right]

        mid = L + ((R - L) >> 1);

        if(arr[mid] >= key)//第一次key出现在[L,mid]中
            R = mid - 1;   //下一次搜索范围[L,mid-1]
                           //意义:想使得arr[R+1]==key,即,R是key索引之前的倒数第一个数
        else
            L = mid + 1;   //下一次搜索范围[mid+1,R]
                           //意义:上面固定了arr[R+1]==key,再使得L不断递增,使得L=R+1跳出
    } 

    //此时,L>R
    if(L < arr.size() && arr[L] == key) //
        return L;

    return -1;
}

举例:

  • 找到一个等于key的数,并且它的前面没有等于key的数了
    1,2,3,5,6,6,6,6,6,6(key=6)【正确解为4】
    L=0,R=9,M=4,arr[M]=key 但是去掉arr[mid]== key分支,因此继续循环
    L=0,R=4-1=3,M=1,arr[M]<key   L=mid+1
    L=2,R=3,M=2,arr[M]<key   L=mid+1
    L=3,R=3,M=3,arr[M]<key   L=mid+1
    L=4,跳出循环,判断arr[L]==key 
  • 找到一个等于key的数,但是它的前面还有等于key的数。
    1,2,3,5,6,6,6,6,6,6,6(key=6)【正确解为4】
    L=0,R=10,M=5,arr[M]=key 但是去掉arr[mid]== key分支,因此继续循环
    L=0,R=5-1=4,M=2,arr[M]<key   L=mid+1
    L=3,R=4,M=3,arr[M]<key   L=mid+1
    L=4,R=4,M=4,arr[M]=key   但是去掉arr[mid]== key分支,因此继续循环
    L=4,R=4-1=3,M=3,arr[M]<key   L=mid+1
    L=M+1=4, 此时因为L>R,跳出循环
  • 数组中不存在key,那么输出条件的arr[L] == key就不会满足,
    输出的依然是-1;

记忆:

        修改之前的arr[mid]>key修改为arr[mid]>=key,并且去掉arr[mid]== key这条分支
        想法在于向使得R为最后一个小于key的数,然后不断增加L,使得L>R,跳出while

3、查找数组中最后一个等于key的数字

问题:数组中可能有重复的key,要找的是最后一个key的位置
分析:找到一个等于key的数,依然还要继续寻找它的后面有没有等于key的数。

  • 修改之前的arr[mid]>=key修改为arr[mid]<=key
  • 将R=mid-1改成了L=mid+1,表示向后移动

代码:

/**查找第一个大于等于key的元素的下标*/
int erfenlasteuual(vector<int> arr,int key){
    int L = 0, R = arr.size() - 1;
    int mid;

    while( L <= R){
        mid = L + ((R - L) >> 1);

        if(arr[mid] <= key)      //注意:区别1
            L = mid + 1;
        else
            R = mid - 1;
        } 

    if(R >= 0 && arr[R] == key)  //注意:区别2
        return R;

    return -1;
}

记忆:
        跟找第一个等于key类似,区别在于:先if的是arr[mid]<=key
        输出R


  •  

4、查找数组中第一个大于等于key的数字

问题:返回第一个等于key的数,如果没有等于key的数,返回第一个大于key的数,
           如果没有大于key的数,那么返回-1

分析:

  • 在第二种情况的代码下修改,修改if(L < arr.size()&& arr[L]== key) 为if(L <arr.size()),
    因为如果没有等于key的数,那么就直接返回L就可以
  • L有几种情况:
  • 第一个是L没有超过数组的范围,那么返回的就是正确的结果,
    如果L>=arr.size(),那么说明整个数组都小于key,直接返回-1.
    如果数组中有key的情况下,那么最后按照寻找第一个key的思想,会返回正确的答案
  • 没有key的情况
    1、所有元素大于key​​​​​​​:
          L一直不会变化的,因为不会运行到L=mid+1这一条分支,最后返回0,答案正确
    2、所有元素小于key
          R一直不会变化的,因为不会运行到R=mid-1,最后一次循环肯定就是当L等于R的时候
          依然还是走了L=mid+1着一个分支,此时的mid为arr.size()-1,
          因为R是不会变的,R最开始是arr.size()-1,最后就有L=R+1=mid+1=arr.size().
          返回 arr.size().答案正确.
    3、不存在key,但是数组元素有比key大的,有比key小的
          R可能有两种指向:
                指向最后一个小于key的数
                指向第一个大于key的数
          (因为二分法的性质决定的),不可能再往前或者往后指了,但是最后跳出循环的条件必        然是L等于R,那么如果指向小于key的数,跳出循环前做L=mid+1,或者指向大于key,
      跳出循环前做R=mid-1,结果都是正确的。

代码:

/*在[L,R]查找第一个>=key的数字*/
int erfenfirstlargeEqual(vector<int> arr,int key){
    int L = 0, R = arr.size() - 1;      
    int mid;


    while( L <= R){
        mid = L + ((R - L) >> 1);
        if(arr[mid] >= key)
            R = mid - 1;
        else
            L = mid + 1;
    } 

    if(L < arr.size())       // 与寻找第一个等于key问题 区别if条件 不需要 arr[L] == key
        return L;

    return -1;
}

记忆:
        跟找第一个等于key类似,区别在于:最后输出判断时,if条件不需要arr[L]==key


5、查找数组中最后一个小于等于key的数字

因为R只有等于-1这一种情况超出数组范围,而能造成这种情况的前提就是数组中确实没有小于等于key的数字,所以直接输出R就行了
 

代码:

int erfenlastSmallEqual(vector<int> arr,int key){
    int L = 0, R = arr.size() - 1;
    int mid;

    while( L <= R){
        mid = L + ((R - L) >> 1);
        if(arr[mid] <= key)
            L = mid + 1;
        else
            R = mid - 1;
    } 
    //if(R >= 0 && arr[R] == key)  //注意:区别于找第最后一个等于key的值
    return R;
    //也不需要return -1
}


6、查找数组中第一个大于key的数字

问题:

和第二种和第四种情况的不同在于:if(arr[mid] >= key)改成了if(arr[mid] > key),因为不是要寻找等key的;而是要寻找大于key的,
但是因为是寻找第一个大于的,所以依然找到后不能直接返回,和普通的二分法有点像,但是没有等于key跳出这条分支,最后依然是返回L

分析:

  • 全部小于key,那么R不会变,直到最后都不会变,
    所以最后返回的L=mid+1= R+1= arr.size()-1+1 = arr.size()
  • 全部大于key,那么L不会变,最后返回L,L=0
  • 大于的,有小于的
    因为在等于key的时候是选择做L=mid-1,所以R最小只能指向最后一个等于key或者最后一个小于key的值(无key的情况下),那么最后的结果是返回L=R+1,依然是返回了第一个大于key的数

代码:

/**查找第一个大于key的元素的下标 */
int erfenfirstLarge(vector<int> arr,int key){
    int L = 0,R = arr.size() - 1;
    int mid;
    while(L <= R){
        mid = L + ((R - L) >> 1);

        if(arr[mid] > key)
            R = mid - 1;
        else
            L = mid + 1;
    } 
    return L;               //区别在此,不需要判断
}

​​​​​​​
7、查找数组中最后一个小于key的数字


int erfenlastSmall(vector<int> arr,int key){
    int L = 0, R = arr.size() - 1;
    int mid;

    while(L <= R){
        mid = L + ((R - L) >> 1);
        if(arr[mid] < key)
            L = mid + 1;
        else
            R = mid - 1;
    }

    return R;
}

猜你喜欢

转载自blog.csdn.net/weifengomg/article/details/128210184