006 二分搜索

这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解这个题目了, 所以就将整个过程写下来了。

这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐.
https://space.bilibili.com/8888480?spm_id_from=333.999.0.0

顺便提供一下测试链接:
https://leetcode.cn/problems/find-peak-element/

在这里插入图片描述

1 . 寻找 num

最基本的问题:在有序数组中, 判断 num 存不存在. (注意一定是在有序数组中进行寻找).

二分搜索的原理解释:

  1. 在一个有序数组中, 寻找一个数据.
  2. 设置一个左边界:l = 0;(因为数组起始位置是“0”), 一个右边界 r = arr.length - 1(因为数组最右边是数组的长度 - 1);
  3. 然后开始进行循环寻找 num, 循环条件是 当左边界 <= 右边界 的时候.
  4. 然后直接搜索中间的数字, 设置为 m = (l + r) / 2;
    1. 若是 arr[m] == num, 说明这个数组中存在和 num 相等的值, 此时返回 true.
    2. 若是 arr[m] > num, 说明这个数组右边的所有数字(包括 arr[m])都比 num 大, (这是有序数组)然后将 r(右边界)调整为:r = m - 1, 因为 arr[m] 这个数字本身都比 num 大.
    3. 若是 arr[m] < num, 说明这个数组左边的所有数字(包括 arr[m]) 都比 num 小, (这是有序数组) 然后将 l(左边界)调整为:l = m + 1, 因为 arr[m] 这个数字本身都比 num 小.
  5. 最后若是已经跳出 while循环了, 都没有找到和 num 相等的值, 直接就返回 false, 说明这个数组中没有和 num 相等的值.

1.1 代码实例

// 一定要保证数组有序.
public static boolean exist(int[] arr, int num) {  
    if (arr == null || arr.length == 0) {  
       return false;  
    }  
    int l = 0, r = arr.length - 1, m = 0;  
    while (l <= r) {  
       m = (l + r) / 2;  
       if (arr[m] == num) {  
          return true;  
       } else if (arr[m] > num) {  
          r = m - 1;  
       } else {  
          l = m + 1;  
       }  
    }  
    return false;  
}

1.2 一个小细节:l + ((r - l) >> 1)

被注释过的这两种写法是一个很有意思(很装逼)的写法

我们首先假设这样一种情况, 有一个很长很长的数组, 数组长度有 30个亿,
int 类型是有一定限制的, 最多是:2的31次方 - 1(21亿多一点), 所以若是写成:m = (l + r) / 2; 假设 l 是 15亿, r 是 16亿; 此时 (l + r)就是31亿, 就超出了 int 类型的范围, 所以对应的存储在 int类型 m 有可能是一个 负数, 或者出现什么问题.

所以对应的解决方案:m = l + ((r - l) >> 1); m = l + (r - l) / 2; 这两种写法是很好的, 上面的例子放到这两个式子中就没有任何问题了, l = 15亿, r - l = 1亿, 所以l + (r - l) / 2 肯定是不会超出 int类型 对应的范围的(这个方法是一个比较安全的写法), (但是其实这个是没有必要的, 毕竟题目中是不会出现这么大的数组的).

m = (l + r) / 2;  
// m = l + (r - l) / 2;  
// m = l + ((r - l) >> 1);

2. 在有序数组中寻找 >= num 的最左位置

基本问题:给你一个数字 num, 在这个有序数组中寻找 >= num 的最左位置.

步骤说明 (解释):

  1. 设置一个左边界:l = 0;(因为数组起始位置是“0”), 一个右边界 r = arr.length - 1(因为数组最右边是数组的长度 - 1);
  2. 然后设置一个 int ans = -1, 初始值设置为 -1 的原因是:若是在这个有序数组中, 没有一个数字要比 num 大, 那就直接返回 -1, 毕竟一个数组中不可能有一个 < 0 的下标(索引).
  3. 然后开始进行循环, 循环条件是 当左边界 <= 右边界 的时候.
  4. 然后直接搜索中间的数字, 设置为 m = (l + r) / 2;
    1. 若是 arr[m] >= num, 那就更新 ans 的值(ans = m), 因为此时已经有 >= num 的值了, 但是我们不知道这个 m 是不是最左位置, 所以还是要继续向左二分. 然后更新 r(右边界)的值:r = m - 1;
    2. 若是 arr[m] < num, 那就不更新 ans 的值, 因为现在还没有 >= num 的值, 但是我们不知道右边有没有 >= num 的值, 所以还要更新 l(左边界的值) l = m + 1; 继续二分.
  5. 最后, 若是有数字 >= num , 那 ans 就会返回对应的 m, 若是没有 ans 就返回 -1 (表示整个数组中没有一个数字 >= num).

2.1 代码实例

public static int findLeft(int[] arr, int num) {  
    int l = 0, r = arr.length - 1, m = 0;  
    int ans = -1;  
    while (l <= r) {  
       // m = (l + r) / 2;  
       // m = l + (r - l) / 2;       m = l + ((r - l) >> 1);  
       if (arr[m] >= num) {  
          ans = m;  
          r = m - 1;  
       } else {  
          l = m + 1;  
       }  
    }  
    return ans;  
}

3. 在有序数组中寻找 <= num 的最右位置

步骤说明 (解释):

  1. 设置一个左边界:l = 0;(因为数组起始位置是“0”), 一个右边界 r = arr.length - 1(因为数组最右边是数组的长度 - 1);
  2. 然后设置一个 int ans = -1, 初始值设置为 -1 的原因是:若是在这个有序数组中, 没有一个数字要比 num 小, 那就直接返回 -1, 毕竟一个数组中不可能有一个 < 0 的数字.
  3. 然后开始进行循环, 循环条件是 当左边界 <= 右边界 的时候.
  4. 然后直接搜索中间的数字, 设置为 m = (l + r) / 2;
    1. 若是 arr[m] > num, 那就不更新 ans 的值, 因为现在还没有 <= num 的值, 但是我们不知道右边有没有 <= num 的值, 所以还要更新 l(左边界的值) l = m + 1; 继续二分.
    2. 若是 arr[m] <= num, 那就更新 ans 的值, (ans = m), 因为此时已经有 <= num 的值了, 但是我们不知道这个 m 是不是最右侧位置, 所以还是要继续二分, 然后更新r (右边界) 的值:r = m - 1;
  5. 最后, 若是有数字 <= num , 那 ans 就会返回对应的 m, 若是没有 ans 就返回 -1 (表示整个数组中没有一个数字 <= num).

3.1 代码实例

    public static int findRight(int[] arr, int num) {
        int l = 0, r = arr.length - 1, m = 0;
        int ans = -1;
        while (l <= r) {
            m = (l + r) / 2;
            if (arr[m] > num) {
                r = m - 1;
            } else {
                ans = m;
                l = m + 1;
            }
        }
        return ans;
    }

4. 二分搜索不一定发生在有序数组上 (寻找峰值问题)

题目描述:
在这里插入图片描述

4.1 代码实例与解释

  1. 若是 arr.length == 1, 就说明直接返回 下标 0 就行了, 毕竟只有一个数字, 可定要大于两边的负无穷.
  2. 因为题目中已经假设了 nums[-1]和nums[n] 都已经是无穷小, 所以最开始要先判断 nums[1]和nums[n - 1], 只要 arr[0] > arr[1]或者arr[n - 1] > arr[n - 2] 就说明可以直接返回下标为: 0 和 n - 1. 若是无法满足就继续执行代码.
  3. 还是用 二分 的方式做, 因为在第二步的时候已经知道 索引 0 和 n - 1 都不能返回, 所以 l可以设置为:1, r可以设置为:n - 2, m = 0, ans = -1.
    1. 进入到这一步说明一定有 arr[1] > arr[0], 而且arr[n - 2] > arr[n - 1]
    2. 此时的情况, 无论怎么做, 在 l ~ r 之间一定至少有一个峰值. 因为左边边界是增长的, 右边边界是下降的, 图片在最下面, 可以看一下, 无论怎么画中间的线条, 一定至少有一个峰值.
  4. 进入 while循环, 终止条件还是 l == r, 或者 l > r 然后中点 m = (l+r)/2,
  5. 若是 arr[m - 1] > arr[m], 说明这里(m - 1 和 m)开始下降了, 毕竟左边是上升的, 说明左边一定是有一个峰值的, 继续进入循环, 但不是说右边一定没有, 这个后面说.
  6. 若是 arr[m + 1] > arr[m], 说明这里(m + 1 和 m)开始上升了, 毕竟右边是下降的, 说明右边一定是有一个峰值的, 继续进入循环, 但不是说左边一定没有, 这个后面说.
  7. 若是 arr[m] > arr[m + 1] && arr[m] > arr[m - 1], 所以说明这个 m 一定是峰值, 就将 ans 赋值为 m 直接返回 ans 就行了,
  8. 特殊情况:出现 {1, 12, 9, 7, 8, 17, 3}, 中点“7”的左侧和右侧同时比“中点”大, 会怎么样?
    1. 这种情况, 自己调试一下就行了, 我调试的结果是在执行到 “if (arr[m - 1] > arr[m])”这一步的时候, 就已经符合这个条件判断的情况了, 所以直接继续向着左侧二分了.
    2. 所以说:右边不一定没有, 只是先进行判断的是左边, 直接去左边找了.
public static int findPeakElement(int[] arr) {  
    int n = arr.length;  
    if (arr.length == 1) {  
        return 0;             // 若是数组的长度为“1”, 那就说明这个数字一定是“峰值”  
    }                         // 因为题目的条件就是数组越界坐标的数字是“无穷小”的.  
    if (arr[0] > arr[1]) {  
        return 0;              // 先对“下标为0”的位置进行判断, 若是arr[0] > arr[1],  
    }                          // 那就说明“arr[0]”一定是一个“峰值”.  
    if (arr[n - 1] > arr[n - 2]) {  
        return n - 1;          // 然后对“下标为n - 1”的位置进行判断, 若是arr[n - 1] > arr[n - 2]  
    }                          // 那就说明“arr[n - 1]”一定是一个峰值.  
    int l = 1, r = n - 2, m = 0, ans = -1;  // 既然“arr[0]和arr[n - 1]”都不是峰值, 那就说明峰值一定在“1 ~ n - 2”之间.  
    while (l <= r) {             // 进入循环, 循环条件还是左边界小于右边界.  
        m = (l + r) / 2;         // 还是将“m”设置为“中点”.  
        if (arr[m - 1] > arr[m]) {        // 若是“中点”左边的数字比“中点”的数字大,  
            r = m - 1;                    // 则说明:左边一定有“峰值”  因为说明此时已经开始下降了, 左边是上升的
        } else if (arr[m] < arr[m + 1]) {  // 若是“中点”右边的数字比“中点”的数字大,  
            l = m + 1;                     // 则说明:右边一定有“峰值”.  因为说明此时已经开始上升了, 右边是下降的
        } else {  
            ans = m;// 最后, 只有一种情况, 就是当“arr[m]”的值同时比“中点”左边的数字和“中点”右边的数字同时都大.  
            break;  // 此时跳出循环.  
        }  
        // 若是你问出现{1, 12, 9, 7, 8, 17, 3}, 中点“7”的左侧和右侧同时比“中点”大, 会怎么样  
        // 这种情况, 自己调试一下就行了, 我调试的结果是在执行到“if (arr[m - 1] > arr[m])”这一步的时候,  
        // 就已经符合这个条件判断的情况了, 所以直接继续向着左侧二分了.  
    }  
    return ans;   // 最后返回一个峰值.  
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值