【题目描述】
给定一个长度为n的数组nums,请你找到峰值并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个所在位置即可。
1.峰值元素是指其值严格大于左右相邻值的元素。严格大于即不能有等于
2.假设 nums[-1] = nums[n] = −∞−∞
3.对于所有有效的 i 都有 nums[i] != nums[i + 1]
4.你可以使用O(logN)的时间复杂度实现此问题吗?
【解题】
对于题目描述的要求4,使用O(logN)的时间复杂度去完成,很容易想到二分查找。而要使用二分查找的思想,就必须能找到一个条件可以使区间分成两个部分。
比如力扣上的从升序数组中查找target的下标。每次把区间平分,并用下标为mid的数组值与target比较,如果相等则直接返回,如果不相等则把该区间分为了两部分,一部分是小于mid下标值的,另一部分是大于mid下标值的,再根据mid与target的大小关系,重新划分新的区域,并以相同的步骤寻找目标值的下标。
这题也是如此,我们可以通过是否一定存在峰值去划分区域。注意这里是用的是“一定”。
首先还是用二分的方法选定中间下标mid,此时判断mid 和 mid + 1的大小关系,如果nums[mid] > nums[mid + 1],那说明mid的左区域一定存在峰值,峰值可能就是mid,也可能还要再往左查找。而右区域我们无法保证一定存在峰值,因为有区域可能一直递减。
这样说可能会有点抽象,那么举一个具体的例子,如数组[1,5,4,2,1], 第一次的mid的值为4,通过与mid+1(值为2)比较,发现mid大于mid + 1,由此判定mid的左区域(也包括mid,因为mid也可能是峰值)一定存在峰值。那么右区域呢?从数组中可以直观的看到4后面都是递减的,并没有峰值,所以右区域是可能不会存在峰值的。
而从上述例子中,可以得出如何重新划分区域。当nums[mid] > nums[mid + 1]时,我们就让右边界right更新到mid,一定不能是mid-1的位置,因为mid也可能是峰值。如果nums[mid] < nums[mid + 1]更新左边界left 为 mid + 1;
直到区域缩小为一个值的时候,也就是left = right的时候,此时该值即为峰值。可能还是会有疑问为什么收缩到一个值的时候就确定这个值为峰值了呢?可以再回想一下我们的收缩条件,什么时候left会更新?当nums[mid] < nums[mid + 1]时,更新到mid +1,也就是说left的左边一个值一定是比left小的。什么时候更新right呢?当nums[mid] > nums[mid + 1]时,我们就让右边界right更新到mid,也就是说right右边的值也是比right小的。当left和right相等时,左右两边又都比该值小,这不就是要找的峰值吗。
【代码】
class Solution {
public int findPeakElement(int[] nums) {
int left = 0, right = nums.length - 1;
while(left < right){
int mid = left + (right - left) / 2;
if(nums[mid] > nums[mid + 1]) right = mid;
else left = mid + 1;
}
return left;
}
}