同步到开源项目Github传送门: Easy-Programming及微信公众号:CVBear
项目内含Leetcode五杀刷题指南-致力于通过5个问题带你入门掌握算法套路
漫话算法[二分查找|算法模板]一首诗解决5道Leetcode题!
From all CV to No CV !我的梦想是编程不再CV!
相信你和我一样在解决二分查找的时候,被“边界”以及二分查找诸多细节折磨过痛苦过,什么时候该前进什么时候该退,半个小时还是没有解决一个问题最后"进退两难"!没关系,通过本文我相信你将学会做出正确的"进退选择"并且我将手把手带你解决5道算法题。
你是否有疑问别人的写法中为什么会出现这样或者那样的写法,而别人仅仅只告诉你,这是一种技巧!其实并没有这么神秘,只是别人花时间去细细的调试了里面的细节而已,看完这篇文章相信你心中的疑惑都会解开!
使用这首诗歌你至少能完成本文5道题目而且不仅仅于此!
LeetCode 题目 |
---|
34. 在排序数组中查找元素的第一个和最后一个位置 |
35. 搜索插入位置 |
69. x 的平方根 |
278. 第一个错误的版本 |
852. 山脉数组的峰顶索引 |
… |
还有很多问题你也可以解决! |
使用[减治]思想裁剪原问题中[无效区间],将原问题变为规模更小的子问题配合边界的[收缩]从减小[有效子区间],从而获取答案!这也是通过本文希望你掌握的而不是去死记所谓的算法模板!
如果你了解归并排序的话一定知道他的步骤:分解、求解、合并,而二分查找实际上每次分解都会淘汰掉另一个子问题,并不需要子问题合并再得出原问题的解,这种就属于单子问题也就是下图中提到的,Anany V. Levitin
提出的减治!
本文将反复提到的关键词
[无效区间]:不在的区间
[有效子区间]:解在的区间
[收缩]:收缩边界减小[有效子区间]范围
[裁剪]:排除掉解一定不存在的位置
[左中位]:0 1 2 3(left mid) 4 5 6 7
即7/2=3,向下取整的中间位置
[右中位]:0 1 2 3 4(rigtht mid) 5 6 7
即(7+1)/2=4,向上取整的中间位置
左边(left为左边界) | 小于 | mid中值 | 小于 | 右边(right为右边界) |
---|---|---|---|---|
left~(mid-1) | < | mid | < | (mid+1)~right |
logn
):n为数组长度从三国的角度看二分查找中区域的划分
[裁剪]掉左右找到孩子[mid]
待搜索区间划分为三块:各部分独立,谁都不属于谁
// [进退三则]
if (target > a[mid]) l = mid + 1; // [大中则进] [mid+1,r] [裁剪]
else if (target < a[mid]) r = mid - 1; // [小中则退] [l,mid-1] [裁剪]
else return mid; // [等中则返] [mid]解区间
舍不得孩子(mid)套不出狼
待搜索区间划分为两块:
// 伪代码只是一种思想,具体编码要考虑不同的细节,通常这种写法需要考虑[中位]向下取整导致取不到[右边界]的解导致死循环的问题
// 隐藏技巧: 取右中位 int mid = l + (r - l + 1) /2;
if (m及右侧属于[无效区间]吗?) {
right = mid - 1;// [裁剪]掉[无效区间]
} else {
left = mid; // 边界[收缩]
}
舍不得孩子(mid)套不出狼
将待搜索区间划分为两块:假设mid属于left均为[无效区间],让出mid给东部然后right进行边界[收缩]得出解!
// 伪代码只是一种思想,具体编码要考虑不同的细节
if (m及左侧属于[无效区间]吗?) {
left = mid + 1; // [裁剪]掉[无效区间]
} else {
right = mid; // 边界[收缩]
}
记住双指针终止位置这些细节,他们将是你以后解决问题的关键!熟悉基本写法而不要被“模板”学会思想是关键,写法可以千变万化
温馨提示:基本写法如果会的话可以跳过,这是为了帮助你了解
目的
查询target所在的索引位置,并不能解决含有重复数字时取左右边界位置的情况
样例
这种写法出自《算法4》,将其中变量lo和hi替换为left和right而来,书中的变量定义其实更规范,但left和right更容易让大家理解!
基本步骤
[定左右]:确定左右边界
[定区间]:确定查询区间即条件while(查询区间)
,左右闭区间[left,right]
双指针终止位置(目标值不在样例中的情况):双指针不重合坚持的原则[left,right=left-1]!因为终止条件是(left > right)
7
(超出右边界):[left=n,right=n-1] 且mid=n-1(n为数组长度)-1
(超出左边界):[left=0,right=-1] 且mid=0隐藏技巧:适当修改后如果目标值在数组中则返回对应的位置而如果不在时则left
总会落在它应该插入的位置!
[取中值]:获取中值(后面不再赘述)
int mid = (left + right)/2;
容易整型溢出,/2
并不用改为位运算>>1
因此也不必装x!,编译器已经帮你优化了int mid = left + (right - left)/2;
避免整型溢出,出自《算法4》,当然还是无法避免就把int修改为long即可int mid = (low + high) >>> 1;
出自JDK Arrays
源码注意点:/是向下取整如 0 1 2 ->3<- 4 5 6 7
重复数字取不到右边界,还有避免整型溢出的写法后面不再赘述!
[进退三则]:进退就是确定下一轮查询的[有效区间]的边界
[无功而返]:没有查询到元素返回-1
public int indexOf(int[] a, int target) {
// 1.[定左右]
int left = 0;
int right = a.length -1;
// 2.[定区间]
while (left <= right) {
// [left,right]
// 3.[取中值]
int mid = left + (right-left)/2;
// 4.[进退三则]
if (target > a[mid]) left = mid + 1; // [大中则进] [mid+1,right]
else if (target < a[mid]) right = mid - 1; // [小中则退] [left,mid-1]
else return mid; // [等中则返] [mid]且l=r=mid 解区间
}
// 5.[无功而返]
return -1;
}
只是为了让你了解这种写法中引发的修改细节!理解即可
修改说明
while
循环基本步骤
[定左右]:确定左右边界
[定区间]:确定查询区间即条件while(查询区间)
,左闭右开区间[left,right)
双指针终止位置1(目标值在样例中的情况):双指针不重合并返回mid
双指针终止位置2(目标值不在样例中的情况):双指针重合坚持的原则[left,right=left)
7
(超出右边界):[left =n,right=n) 且mid=n-1(n为数组长度),双指针重合在最右侧后再执行一步[大中则进]后产生无效区间[7,7)终止-1
(超出左边界):[left =0,right=0]且mid=0,双指针重合在数组最左侧后产生无效区间[0,0)后终止隐藏技巧:left和right指针可以游走于[0,n]n为数组长度的范围,且只要将等于的情况按需修改就能保证终止时指针重合
[取中值]:获取中值
[进退三则]:进退就是确定下一轮查询的左右[区间]的边界
[无功而返]:没有查询到元素返回-1
public int indexOf(int[] a, int target) {
// 1.[定左右]
int left = 0;
int right = a.length;// 修改①
// 2.[定区间]
while (left < right) {
// [l,r)
// 3.[取中值]
int mid = left + (right-left)/2;
// 4.[进退三则]
if (target > a[mid]) left = mid + 1; // [大中则进] [mid+1,r)
else if (target < a[mid]) right = mid; // [小中取中] [l,mid) 修改②
else return mid; // [等中则返] [mid]且l+1=r & mid=l
}
// 5.[无功而返]
return -1;
}
C总:赛赛!你能给我查询出第一个出现的2吗
后面会教你写这种框架的思路,而并不需要你去背所谓的“算法框架”,现在只要有印象就好啦!
查询target所在的最左索引位置或target应该插入的第一个位置
[定左右]:确定左右边界
[定区间]:确定查询区间即条件while(查询区间)
,左右闭区间[left,right]
双指针终止位置1(目标值在样例中的情况):坚持的原则**[left,right=left-1]**
双指针终止位置2(目标值不在样例中的情况):坚持的原则**[left,right=left-1]**
8
(超出右边界):[left=n,right=n-1]且mid=n-1(n为数组长度),双指针重合在最右侧后再执行一步[大中则进]后终止-1
(超出左边界):[left=0,right=-1]且mid=0,双指针重合在数组最左侧后再执行一步[小中则退]后终止总结!!!:这种写法可以让left指针的范围在[0,数组长度]区间内而right指针在[-1,数组长度-1],且终止时right总是在left左侧
[取中值]:获取中值
[进退三则]:大中则进、小中则退、等中则退,当然也可以将后两步合并在一起正是本文后半部分要带你学会的核心思想
[检越]:检查left指针是否越界,若越界则“矫正”
[返边界]:查询左边界则“返左”,left总会在终止时落在你想要的位置!
public int indexOfLeft(int[] nums, int target) {
// 1.[定左右]
int left = 0;
int right = nums.length-1;
// 2.[定区间]
while (left <= right) {
//[l,r]
// 3.[取中值]
int mid = left + (right - left)/2;
// 4.[进退三则]
if (target > nums[mid]) left = mid + 1; // [mid+1,r] 大中则进
else if (target < nums[mid]) right = mid - 1; // [l,mid-1] 小中则退
else right = mid -1; // [r=mid-1] 等中则退
}
// 5.[检越]
if (left >= nums.length || nums[left] != target) return -1;
// 6.[返边界]
return left;
}
基于基本写法的修改,查询区间修改为[left,right)
目的
查询target所在的最左索引位置或插入位置
基本步骤
[定左右]:确定左右边界
①处修改为数组长度,扩大范围为寻找超出右边界的插入位置创造条件
[定区间]:确定查询区间即条件while(查询区间)
,左右闭区间[left,right)
修改说明
双指针终止位置1(目标值在样例中的情况):坚持的原则**[left,right=left]**
双指针终止位置2(目标值不在样例中的情况):坚持的原则**[left,right=left]**
8
(超出右边界):[left=n,right=n]且mid=n-1(n为数组长度),[大中则进]后双指针重合[8,8)退出-1
(超出左边界):[left=0,right=0]且mid=0,右指针向左缩进[小则取中]后双指针重合在数组最左侧[0,0)总结!!!:这种写法可以让left指针和right指针的范围都在**[0,数组长度]区间内移动且终止时最终位置重合**
[取中值]:获取中值
[进退三则]:大中则进、小中则中、等中则中,当然也可以将后两步合并在一起,因为后两步都是为了将right向左[缩进]\
[检越]:最后一个元素还不是那说明没有相等的了
[返边界]:查询左边界则“返左”便于你记忆但实际上双指针重合随意返回
public int indexOfLeft(int[] nums, int target) {
// 1.[定左右]
int left = 0;
int right = nums.length;// ①修改自: nums.length-1
// 2.[查区间]
while (left < right) {
// [l,r) ②修改自: l <= r
// 3.[取中值]
int mid = left + (right - left)/2;
// 4.[进退三则]
if (target > nums[mid]) left = mid + 1; // [mid+1,r) 大中则进
else if (target < nums[mid]) right = mid; // [l,mid) 小中则中
else right = mid; // [r=mid] 等中则中 注意[r=mid-1]错误
}
// 5.[检越]
if (left >= nums.length || nums[left] != target) return -1;
// 6.[返边界]
return left;// right也可因为重合了
}
想吐槽了吧!毫无重点!写法太多晕了吧!或许你和我一样看见有这么多种写发放已经晕了!没关系你已经有了这些写法的基本印象接下来我来告诉你怎样快速写出这些写法!
下图是以左开右闭[)版本说明,其实双闭区间[]写法只是最后指针位置不重合以及有一些细节,因此不用纠结一定要使用哪一种!理解思想才是关键
mid+1
后mid在最左侧,通过边界[收缩]减小问题方可找到答案!写法1:[裁剪]掉解不就得到解右边界+1的位置[左边界]了吗?那最后-1不就得到解了吗?
/**
* 修改自[查询左边界]版本
* 左闭右开版[l,r)
*/
private static int indexOfRight(int[] nums, int target) {
// 1.[定左右]
int left = 0;
int right = nums.length;
// 2.[检越] 根据实际情况检越即可不是死记
if (target > nums[right-1] || target < nums[left]) return -1;
// 3.[定区间]
while (left < right) {
// [0,r)双指针可以在[0,n]移动
// 4.[取中值]
int mid = left + (right - left) /2;
// 5.[裁剪与收缩]
if (nums[mid] <= target) {
// 修改① < 修改为了 <=
left = mid + 1;// [裁剪]后 [mid+1,hi)
} else {
right = mid; // 边界[收缩]至[l,mid)实际移动范围[l,mid]
}
}
// 6.[返边界] 方便记忆查询哪边返回哪边
return right-1;// 修改② left-1也行因为[双指针重合]
}
写法2:是不是发现写法1[返边界]的写法return right-1;
很难受呢!
通过循环条件 while (left <= right)
的终止条件的特点right会终止在左侧来改变它!
揭秘:不理解你再回头看看※写法1的图例更好的理解[裁剪]与边界[收缩]两种思想
/**
* 修改自写法1
* 闭区间版[l,r]
*
*/
private static int indexOfRight(int[] nums, int target) {
// 1.[定左右]
int left = 0;
int right = nums.length - 1;// 修改①
// 3.[定区间]
while (left <= right) {
// [0,r] 修改② 双指针技巧 left→逃跑 逃跑<-right
// 4.[取中值]
int mid = left + (right - left) / 2;
// 5.[裁剪与收缩]
if (nums[mid] <= target) {
// 修改③ < 修改为了 <=
left = mid + 1; // [裁剪]后 [mid+1,right]
} else {
right = mid - 1; // 边界[收缩]至[left,mid-1]
}
}
// 2.[检越] right左逃 left右逃
if (right < 0 || left > nums.length-1) return -1; // 修改⑤
// 6.[返边界] 方便记忆查询哪边返回哪边
return right;// 修改④
}
到这里不知道你是否已经发现!教叮当写的这种框架其实就是利用了[统一东部]的思想来解决的,也就是从左向右裁剪,从右向左压缩,那么你思考一下能不能利用[统一西部]的思想来解决呢!
下面写法会产生什么问题呢?
我将通过例题为你讲解!
LeetCode
五杀实战!一个思想解决多个问题通过解决题目能更好的理解算法思想和享受支配算法的乐趣!
说明:在题解中我将left写为l,right写为r
掌握点:取(右)中位的技巧
给定一个按照升序排列的整数数组 nums
,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
private int indexOfRight(int[] nums, int target) {
int l = 0;
int r = nums.length - 1;
while (l < r) {
// 3.[取中值]
int mid = l + (r - l) /2;// 错误的获取中值
if (nums[mid] > target) {
r = mid - 1;
} else {
l = mid;
}
}
return l;
}
private int indexOfRight(int[] nums, int target) {
// 1.[定左右]
int l = 0;
int r = nums.length - 1;// [右边界] 照顾只有一个数的情况
// 本题不用检查最后一个元素是否为target因为先查询了左边界保证数字存在了,单独使用一定检查
// 2.[定区间]
while (l < r) {
// [0,r)双指针可以在[0,n]移动
// 3.[取中值]
int mid = l + (r - l + 1) /2;
// 4.[裁剪与收缩]
if (nums[mid] > target) {
r = mid - 1;// [裁剪]后 [mid+1,hi)
} else {
l = mid; // 收缩[l,mid)实际移动范围[l,mid]
}
}
return l;// r 终止时[双指针重合]
}
private int indexOfLeft(int[] nums, int target) {
// 1.[定左右]
int l = 0;
int r = nums.length;
// 2.[定区间]
while (l < r) {
// [0,r)双指针可以在[0,n]移动
// 3.[取中值]
int mid = l + (r - l) /2;
// 4.[裁剪与收缩]
if (nums[mid] < target) {
l = mid + 1;// [裁剪]后 [mid+1,r)
} else {
r = mid; // 收缩[l,mid)实际移动范围[l,mid]
}
}
return l;// r 终止时[双指针重合]
}
// searchRange函数与Solution1相同
public int[] searchRange(int[] nums, int target) {
// 1.定义结果集
int [] ans = {
-1, -1};
// 2.空判
if (nums == null || nums.length == 0) return ans;
// 3.[查询左边界]
int leftIndex = indexOfLeft(nums, target);
// 3.[检越]避免不在的情况和越界情况,因为left总是往mid+1的方向->
if (leftIndex == nums.length || target != nums[leftIndex]) {
return ans;
}
// 4.查询target的下一个元素的[左边界]索引-1即可
int rightIndex = indexOfRight(nums, target);
// int rightIndex = indexOfLeft(nums, target+1)-1; //写法②
// 5.设置
ans[0] = leftIndex;
ans[1] = rightIndex;
return ans;
}
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
[大中则进]:[裁剪]不是解的部分
[小中则退]:mid-1会导致比如查询2[2,2)产生无效区间就查不到元素
[等中则中]:为什么不是mid-1同①
public int searchInsert(int[] nums, int target) {
// 0.特判
if(nums.length == 0) return 0;
// 1.[定左右]
int l = 0;
int r = nums.length;
// 2.[定区间]
while (l < r) {
//[lo,hi)
int mid = l + (r - l)/2;
if (target > nums[mid]) l = mid + 1; // [mid+1,hi) [大中则进]
else if (target < nums[mid]) r = mid; // [lo,mid) [小中则中]
else r = mid; // [mid] [等中则中]
}
// 3.[返回边界]
return l;
}
// 统一东部
public int searchInsert(int[] nums, int target) {
// 1.[定左右]
int l = 0;
int r = nums.length;
// 2.[定区间]
while (l < r) {
//[l,r)
int mid = l + (r - l)/2;
if (nums[mid] < target) l = mid + 1; // [裁剪] 更新区间[mid+1,r]
else r = mid; // 边界[收缩] 更新区间[l,mid]不是[l,mid)
}
// 3.[返回边界]
return l;
}
简介:此题要找的是[严格平方]根向的下取整
实现 int sqrt(int x)
函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842...,
由于返回类型是整数,小数部分将被舍去。
目的:基本写法查询的是相同的数字或者应该插入的第一个位置也就是查询[左边界]
说明:此写法中(left)获取的是不重复的有序数组中某个数字特殊位置有[插入位置]和[相等位置]
0 1 2 3
中1的后一个位置,但我们要向下取整因此left-1就得到了答案1,且终止时候right总是在left左侧所以也可以返回right突破点:寻找[严格平方根]的位置
0 1 2 3 4 5
,而left-1才是本题的答案具体分析
/**
* l: left r: right
* @param x 目标值
* @return 返回x的平方根
*/
public static int mySqrt(int x) {
// 1.[定左右],范围: [0,x]
long l = 0;
long r = x;
// 2.[定区间]
while (l <= r) {
// [l,r]
// 3.[取中值]
long mid = l + (r - l)/2;
long midVal = mid * mid;
// 4.[进退三则]
if (x > midVal) l = mid + 1; //[大中则进] 下一轮查询区间 [mid+1,r]①
else if (x < midVal) r = mid - 1; //[小中则退] 下一轮查询区间 [l,mid-1]②
else return (int)mid; //[等中则返] 获取值的区间 [mid]③
}
// 5.[返边界]
return (int)(l-1);// r
}
说明:这种写法可以用于含有重复数字的数组中[查询右边界]的问题!如0 2 2 3
返回2出现的第最后一个位置2
突破点:mid2>x一定不是解需要[裁剪]而另一面则是[有效区间]需要[收缩]
技巧:①和②属于减治策略,③则是while的终止条件的技巧
※注意点1说明:这样
例子:查询在0 2 2
中2的第一个位置
left = mid + 1
使得left始终在解的位置“mid”!/**
* l: left r: right
* @param x 目标值
* @return 返回x的平方根
*/
public static int mySqrt(int x) {
// 1.[定左右],范围: [0,x]
long l = 0;
long r = x;
// 2.[定区间]
while (l <= r) {
// [l,r]
// 3.[取中值]
long mid = l + (r - l)/2;
long midVal = mid * mid;
// 4.[裁剪与收缩]
if (midVal > x) r = mid - 1;// 从右向左[裁剪]掉一定不是解的部分,更新有效区间为[l,mid-1]
else l = mid + 1;// 从左向右[收缩]解存在的[有效区间]更新有效区间为[mid+1,r]
// ※注意点1 发现了吗[收缩]少了[mid]
}
// 5.[返边界]
return (int)(r);// l-1
}
技巧:
l < r
这样做能保证查询过程中双指针不重合/**
* l: left r: right
* @param x 目标值
* @return 返回x的平方根
*/
public int mySqrt(int x) {
// 1.[定左右],范围: [0,x]
long l = 0;
long r = x;
// 2.[定区间]
while (l < r) {
//技巧① [l,r) 虽然是左闭右开但双指针仍然可以在[l,r]内移动
// 3.[取中值] 使用整型避免溢出
long mid = l + (r - l + 1)/2;
long midVal = mid * mid;
// 4.[裁剪与收缩]
// 如 5: 0 1 2 3 4 5
if (midVal > x) {
r = mid - 1;// [裁剪]一定不是解的部分即[无效区间],更新有效区间为[l,mid-1]
} else {
// 错误写法mid-1显然[mid-1,r]就漏掉了[mid]
l = mid;// [收缩]解存在的[有效区间]更新有效区间为[mid,r]注意是左右双闭区间
}
}
// 5.[返边界]: 返回有效区间的边界l
return (int)(l);// 实际上双指针重合了因此也可以返回l或则r
}
隐藏技巧:选取右中值
public int mySqrt(int x) {
// 1.[定左右],范围: [0,x)
long l = 0, r = x;
// 2.确定[查询区间]
while (l < r) {
// [l,r)
// 3.[取(右)中值]
long mid = l + (r - l + 1)/2;
long midVal = mid * mid;
// 4.[进退三则]
if(x > midVal) l = mid; // 大中则中
else if (x < midVal) r = mid - 1; // 小中则退
else l = mid; // 等中则中
}
return (int)l;
}
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version)
接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API
的次数。
示例
给定 n = 5,并且 version = 4 是第一个错误的版本。
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
分析
很明显这是个查询[左边界]的问题
false false false ->true<- true true
相信你要是认真看到这里的话你已经会写了
l <= r
允许双指针重合,统一东部思想:这个题目和之前的题目的不同点并没有nums[mid]==target
的情况!
public int firstBadVersion(int n) {
// 1.[定左右],范围: [0,n]
int l = 0;
int r = n;
// 2.[定区间]查询左边界
while (l <= r) {
// 3.[取中值]
int mid = l + (r-l)/2;
// 4.[裁剪与收缩]
if (!isBadVersion(mid)) {
l = mid + 1;// [裁剪]掉无效区间[l,mid]后更新有效区间区间为[mid+1,r]
} else {
r = mid - 1;// [收缩]将右边界,下一轮区间[l,mid-1]不用担心错过[mid]因为最后l会+1刚好落在解的位置
}
}
// 5.[返边界]
return l;// 不能返回r
}
l < r
[l,r)统一东部思想
public int firstBadVersion(int n) {
// 1.[定左右],范围: [0,n]
int l = 0;
int r = n;
// 2.[定区间]查询左边界
while (l < r) {
// [l,n)
// 3.[取中值]
int mid = l + (r-l)/2;
// 4.[裁剪与收缩]
if (!isBadVersion(mid)) {
l = mid + 1;// [裁剪]掉无效区间[l,mid]后更新有效区间区间为[mid+1,r]
} else {
r = mid;// [收缩]将右边界,下一轮区间[l,mid)实际包含了mid[l,mid]
}
}
// 5.[返边界]
return l;// r 或者 l都可双指针重合
}
我们把符合下列属性的数组 A 称作山脉:
A.length >= 3
存在 0 < i < A.length - 1 使得A[0] < A[1] < ... A[i-1] < A[i] > A[i+1] > ... > A[A.length - 1]
给定一个确定为山脉的数组,返回任何满足 A[0] < A[1] < ... A[i-1] < A[i] > A[i+1] > ... > A[A.length - 1] 的 i 的值。
示例 1:
输入:[0,1,0]
输出:1
示例 2:
输入:[0,2,1,0]
输出:1
掌握点:灵活运用基本的写法
public int peakIndexInMountainArray(int[] A) {
// 1.[定左右]
int l = 0;
int r = A.length-1;
// 2.[定区间]
while (l <= r) {
// [l,r]
// 3.[取中值]
int mid = l + (r-l)/2;
// 4.[进退三则]
if (A[mid+1] > A[mid]) {
// 上坡
l = mid + 1; // [爬坡]
} else if (A[mid-1] > A[mid]){
// 下坡
r = mid - 1; // [返回坡顶]
} else {
return mid;
}
}
// 5.[无功而返]
return -1;
}
代码已经上到我的开源项目将持续更新各种算法和基础知识
Github传送们