一、山峰数组的峰顶索引
题目链接:852. 山脉数组的峰顶索引 - 力扣(LeetCode)
题目描述:
给定一个长度为
n
的整数 山脉 数组arr
,其中的值递增到一个 峰值元素 然后递减。返回峰值元素的下标。
你必须设计并实现时间复杂度为
O(log(n))
的解决方案。示例 1:
输入:arr = [0,1,0] 输出:1示例 2:
输入:arr = [0,2,1,0] 输出:1示例 3:
输入:arr = [0,10,5,2] 输出:1提示:
3 <= arr.length <= 10^5
0 <= arr[i] <= 10^6
- 题目数据 保证
arr
是一个山脉数组
题目分析:
我们只需要在一个 先升序后降序的数组中 找到转折点的下标即可
需要注意的是,在【提示】中,明确说明:该数组的长度是大于等于3的,并且保证了数组arr是一个山脉数组(满足先升序后降序--必有转折点)。所以当数组长度为3时,返回值为1.
所以峰值元素肯定不会出现在数组的两端,而是出现在数组中间。
即返回值区间为:[1,arr.length-2]
题目所满足的条件在后续解题中,有很大的作用
在本题中,数组具有【二段性】,即当数组被分为两个部分后,每个部分内部都有某种特定的性质。在本题中,前半部分满足升序,后半部分满足降序。我们在【手撕二分查找】中的二分查找本质中,说过:当数组具有有序性/二段性时,存在一个分界点时,使得分界点前后元素的性质不同,便可以使用二分查找来解决问题。
解题思路:
在循环中,我们只需要判断mid处元素是否大于mid-1处的元素,然后依此更新左右边界,直到找到峰值元素
1.确认区间形式:【左开右闭】
2.维护区间形式:初始值、循环条件、左右边界
初始值:left=0,right=arr.length-2/arr.length-1(由于峰值元素不出现在数组两端,搜索区间为[1,arr.length-2])。注意:left一定不能为-1,因为当left=-1时,可能会导致mid-1<0越界
循环条件:left<right
左右边界:left=mid,right=mid-1
3.选择mid的计算方式:向上调整:mid=left+(right-left+1)/2
4.返回值:return left
如果我们用left=-1去提交,发现会报一个越界异常,如下:
arr=[3,5,3,2,0]
越界的原因:
- 初始时:left= -1,right=4。mid=2,arr[mid]=3<arr[mid-1]=5。令right=mid-1=1
- 此时:left=-1,right=1。mid=0。由于mid-1=-1<0,所以越界
为了避免越界,要保证mid-1>=0,即mid>=1。
初始化left=0则可以解决这一问题
解题代码:
class Solution {
public static int peakIndexInMountainArray(int[] arr) {
int left=0;
int right=arr.length-2;//或者可以为arr.length-1
while(left<right){
int mid=left+(right-left+1)/2;
if(arr[mid]>arr[mid-1])left=mid;
else right=mid-1;
}
return left;
}
}
二、寻找峰值
1.题目链接:162. 寻找峰值 - 力扣(LeetCode)
2.题目描述:
峰值元素是指其值严格⼤于左右相邻值的元素。给你⼀个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何⼀个峰值 所在位置即可。你可以假设 nums[-1] = nums[n] = -∞ 。你必须实现时间复杂度为 O(log n) 的算法来解决此问题。⽰例 1:输⼊:nums = [1,2,3,1]输出:2解释:3 是峰值元素,你的函数应该返回其索引 2。⽰例 2:输⼊:nums = [1,2,1,3,5,6,4]输出:1 或 5解释:你的函数可以返回索引 1,其峰值元素为 2;或者返回索引 5, 其峰值元素为 6。提⽰:1 <= nums.length <= 1000-2^31 <= nums[i] <= 2^31 - 1对于所有有效的 i 都有 nums[i] != nums[i + 1]
题目分析:
首先解析为什么给出nums[-1]=nums[n]= -∞
这保证了数组一定有峰值。
比如数组是严格递增的,那么nums[n-1]就是峰值。为什么?因为此时nums[n-2]<nums[n-1]>nums[n]。同样地,假如数组是严格递减的,那么nums[0]就是峰值。因为此时nums[-1]<nums[0]>nums[1]
其实简单来说,峰值元素就是一个大于其两侧的元素而已。而nums[-1]和nums[n]为 -∞,则保证了下标为0或者n-1处的元素也可能是峰值元素。
定理:如果i<n-1且nums[i-1]<nums[i],那么在下标[i,n-1]中一定存在至少一个峰值
反证法:假设下标[i,n-1]中没有峰值
·由于i处不是峰值且nums[i-1]<nums[i],所以一定有nums[i]<nums[i+1]成立,否则i就是峰值了。
·由于i+1处不是峰值且nums[i]<nums[i+1],所以一定有nums[i+1]<nums[i+2]成立,否则i+1就是峰值了。
·依此类推,得 nums[i-1]<nums[i]<nums[i+1]<nums[i+2]<........nums[n-1]>nums[n]= -∞
这意味着nums[n-1]是峰值,假设不成立,所以定理成立。
同理可得,如果i<n-1且nums[i-1]>nums[i],那么在[0,i-1]中一定存在至少一个峰值
解题思路:
根据上述分析,我们已知数组中一定存在至少一个峰值。只需要通过比较nums[i-1]和nums[i]得大小关系,从而不断地缩小峰值所在位置的范围,二分找到峰值即可
细节:
- 当数组只有一个元素时,该元素即为峰值。应当返回下标0
- 当n-1处的元素为峰值元素时,每次更新的都是left,当left走到n-1处(right初始值)时,结束循环,返回n-1
我们这里选择【左开右闭】来解决本题
由于当数组只有一个元素时,应该返回下标0。所以这里初始化left=0
解题代码:
class Solution {
public int findPeakElement(int[] nums) {
int left=0;
int right=nums.length-1;//左开右闭 (0,n-1]
while(left<right){
int mid=left+(right-left+1)/2;
if(nums[mid]>nums[mid-1])left=mid;
else right=mid-1;
}
return left;
}
}
三、搜索旋转排序数组中的最小值
1.题目链接:153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
2.题目描述:
已知一个长度为
n
的数组,预先按照升序排列,经由1
到n
次 旋转 后,得到输入数组。例如,原数组nums = [0,1,2,4,5,6,7]
在变化后可能得到:
- 若旋转
4
次,则可以得到[4,5,6,7,0,1,2]
- 若旋转
7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组
[a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组[a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。给你一个元素值 互不相同 的数组
nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。你必须设计一个时间复杂度为
O(log n)
的算法解决此问题。示例 1:
输入:nums = [3,4,5,1,2] 输出:1 解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。示例 2:
输入:nums = [4,5,6,7,0,1,2] 输出:0 解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。示例 3:
输入:nums = [11,13,15,17] 输出:11 解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums
中的所有整数 互不相同nums
原来是一个升序排序的数组,并进行了1
至n
次旋转
题目分析:
其实本题的数组可以看成由 两段升序数组(假设为数组M、N)拼接而成。
如果原数组由N+M拼接而成,那么M+N就是一个升序数组。而M、N分别又是升序的
我们用一张图表示,会更加清晰
其中C点就是我们所要求的点
二分的本质:找到一个判断标准,使得查找区间能够一分为二
通过图像我们发现,[A,B]区间内的点都是严格大于D点的值,C点的值是严格小于D点的值。但是当[C,D]区间只有一个元素的时候,C点的值可能等于D点的值
因此,初始化左右两个指针left,right:
根据mid的落点,我们可以这样划分下一次查询的区间:
·当mid在[A,B]区间的时候,也就是mid位置的值严格大于D点的值,下一次查询区间在[mid+1,right]
·当mid在[C,D]区间的时候,也就是mid位置的值严格小于等于D点的值,下一次查询区间在[left,mid]
当区间长度变成1的时候,就是我们要找的结果
注意:我们这里的D需要额外处理。我们将查询区间内的最后一个元素看作D。
解题代码:
class Solution {
public static int findMin(int[] nums) {
//左闭右开
int left=0;
int right=nums.length-1;
int x=nums[right];//标记一下最后一个位置的值
while(left<right){
int mid=left+(right-left)/2 ;
if(nums[mid]>x)left=mid+1;
else right=mid;
}
return nums[left];
}
}
四、0~n-1中缺失的数字
1.题目链接:LCR 173. 点名 - 力扣(LeetCode)
2.题目描述:
某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组
records
。假定仅有一位同学缺席,请返回他的学号。示例 1:
输入:records = [0,1,2,3,5] 输出:4示例 2:
输入:records = [0, 1, 2, 3, 4, 5, 6, 8] 输出:7提示:
1 <= records.length <= 10000
简单来说:有一个长度为n的升序数组,数组内的元素应该为0~n-1。请找出缺少的那个数字
我们本题使用【左闭右开】来解题
这题其实更加简单了。我们只需要比较下标是否与元素相等即可。
如果相等,表示左侧没有缺少,右侧缺少,那么下一次查询区间为[mid+1,right]
如果不相等,表示左侧缺少,右侧不缺少,那么下一次查询区间为[left,mid]
左右指针相遇处的元素则表示 下标与元素不相等。返回此处下标--表示缺少元素
解题代码:
class Solution {
public static int takeAttendance(int[] records) {
int left=0;
int right=records.length;
//左闭右开
while(left<right){
int mid=left+(right-left)/2;
if(records[mid]==mid)left=mid+1;
else right=mid;
}
return left;
}
}