这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解这个题目了, 所以就将整个过程写下来了。
这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐.
https://space.bilibili.com/8888480?spm_id_from=333.999.0.0
顺便提供一下测试链接:
https://leetcode.cn/problems/find-peak-element/
1 . 寻找 num
最基本的问题:在有序数组中, 判断 num
存不存在. (注意一定是在有序数组中进行寻找).
二分搜索的原理解释:
- 在一个有序数组中, 寻找一个数据.
- 设置一个左边界:
l = 0;(因为数组起始位置是“0”)
, 一个右边界r = arr.length - 1(因为数组最右边是数组的长度 - 1)
; - 然后开始进行循环寻找
num
, 循环条件是当左边界 <= 右边界
的时候. - 然后直接搜索中间的数字, 设置为
m = (l + r) / 2
;- 若是
arr[m] == num
, 说明这个数组中存在和num
相等的值, 此时返回true
. - 若是
arr[m] > num
, 说明这个数组右边的所有数字(包括arr[m]
)都比num
大, (这是有序数组)然后将r(右边界)调整为:r = m - 1
, 因为arr[m]
这个数字本身都比num
大. - 若是
arr[m] < num
, 说明这个数组左边的所有数字(包括arr[m]
) 都比num
小, (这是有序数组) 然后将l(左边界)调整为:l = m + 1
, 因为arr[m]
这个数字本身都比num
小.
- 若是
- 最后若是已经跳出
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
的最左位置.
步骤说明 (解释):
- 设置一个左边界:
l = 0;(因为数组起始位置是“0”)
, 一个右边界r = arr.length - 1(因为数组最右边是数组的长度 - 1)
; - 然后设置一个
int ans = -1
, 初始值设置为-1
的原因是:若是在这个有序数组中, 没有一个数字要比num
大, 那就直接返回-1
, 毕竟一个数组中不可能有一个< 0
的下标(索引). - 然后开始进行循环, 循环条件是
当左边界 <= 右边界
的时候. - 然后直接搜索中间的数字, 设置为
m = (l + r) / 2
;- 若是
arr[m] >= num
, 那就更新ans
的值(ans = m
), 因为此时已经有>= num
的值了, 但是我们不知道这个m
是不是最左位置, 所以还是要继续向左二分. 然后更新r(右边界)的值:r = m - 1
; - 若是
arr[m] < num
, 那就不更新ans
的值, 因为现在还没有>= num
的值, 但是我们不知道右边有没有>= num
的值, 所以还要更新l(左边界的值) l = m + 1
; 继续二分.
- 若是
- 最后, 若是有数字
>= 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
的最右位置
步骤说明 (解释):
- 设置一个左边界:
l = 0;(因为数组起始位置是“0”)
, 一个右边界r = arr.length - 1(因为数组最右边是数组的长度 - 1)
; - 然后设置一个
int ans = -1
, 初始值设置为-1
的原因是:若是在这个有序数组中, 没有一个数字要比num
小, 那就直接返回-1
, 毕竟一个数组中不可能有一个< 0
的数字. - 然后开始进行循环, 循环条件是
当左边界 <= 右边界
的时候. - 然后直接搜索中间的数字, 设置为
m = (l + r) / 2
;- 若是
arr[m] > num
, 那就不更新ans
的值, 因为现在还没有<= num
的值, 但是我们不知道右边有没有<= num
的值, 所以还要更新l(左边界的值) l = m + 1
; 继续二分. - 若是
arr[m] <= num
, 那就更新ans
的值, (ans = m
), 因为此时已经有<= num
的值了, 但是我们不知道这个m
是不是最右侧位置, 所以还是要继续二分, 然后更新r (右边界) 的值:r = m - 1
;
- 若是
- 最后, 若是有数字
<= 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 代码实例与解释
- 若是
arr.length == 1
, 就说明直接返回下标 0
就行了, 毕竟只有一个数字, 可定要大于两边的负无穷. - 因为题目中已经假设了
nums[-1]和nums[n]
都已经是无穷小, 所以最开始要先判断nums[1]和nums[n - 1]
, 只要arr[0] > arr[1]或者arr[n - 1] > arr[n - 2]
就说明可以直接返回下标为:0 和 n - 1
. 若是无法满足就继续执行代码. - 还是用
二分
的方式做, 因为在第二步的时候已经知道索引 0 和 n - 1
都不能返回, 所以l可以设置为:1, r可以设置为:n - 2, m = 0, ans = -1
.- 进入到这一步说明一定有
arr[1] > arr[0], 而且arr[n - 2] > arr[n - 1]
- 此时的情况, 无论怎么做, 在
l ~ r
之间一定至少有一个峰值. 因为左边边界是增长的, 右边边界是下降的, 图片在最下面, 可以看一下, 无论怎么画中间的线条, 一定至少有一个峰值.
- 进入到这一步说明一定有
- 进入
while循环
, 终止条件还是l == r, 或者 l > r
然后中点m = (l+r)/2
, - 若是
arr[m - 1] > arr[m]
, 说明这里(m - 1 和 m
)开始下降了, 毕竟左边是上升的, 说明左边一定是有一个峰值的, 继续进入循环, 但不是说右边一定没有, 这个后面说. - 若是
arr[m + 1] > arr[m]
, 说明这里(m + 1 和 m
)开始上升了, 毕竟右边是下降的, 说明右边一定是有一个峰值的, 继续进入循环, 但不是说左边一定没有, 这个后面说. - 若是
arr[m] > arr[m + 1] && arr[m] > arr[m - 1]
, 所以说明这个m
一定是峰值, 就将ans 赋值为 m
直接返回ans
就行了, - 特殊情况:出现
{1, 12, 9, 7, 8, 17, 3}
, 中点“7
”的左侧和右侧同时比“中点
”大, 会怎么样?- 这种情况, 自己调试一下就行了, 我调试的结果是在执行到 “
if (arr[m - 1] > arr[m])
”这一步的时候, 就已经符合这个条件判断的情况了, 所以直接继续向着左侧二分了. - 所以说:右边不一定没有, 只是先进行判断的是左边, 直接去左边找了.
- 这种情况, 自己调试一下就行了, 我调试的结果是在执行到 “
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; // 最后返回一个峰值.
}