问题引入
给定一个数组arr,其中 arr[i] != arr[i+1],找到唯一峰值元素并返回其索引
首先,定位到题干中的关键字:峰值元素
峰值元素是指 其值大于左右相邻值 的元素,是数组的一个转折点。
法一:循环遍历
根据峰值元素的特性,可以直接遍历找到符合条件的峰值元素:
思想:
遍历数组,如果当前元素大于它相邻的元素,则输出它的索引。
若当前数组只有一个元素,则输出0,若当前元素为首尾元素,则只需要大于其另一侧的元素即可。
//js
let arr = [3,4,5,6,7,1,2];
let index = 0;
if (arr.length !== 1) {
// 遍历判断第二个元素~倒数第二个元素
for (let i = 1; i < arr.length-1; i++) {
if (arr[i-1] < arr[i] && arr[i] > arr[i+1]) {
index = i;
break;
}
}
// 如果上述未找到峰值,则峰值为最后一个元素(整个数组全部升序排列)
if (index === 0) index = arr.length-1;
}
console.log(index);
上述代码的时间复杂度为O(n),在执行数据量小的数组当然没问题,但是如果我们要找一个数据量大的数组呢?
法二:二分查找
这个时候我们就应该想到二分法来进行优化,最后的时间复杂度可变为O(logn)
实现二分查找方法之前,我们先来了解一个常识:
在当一个人爬山,先上山再下山,目的地为山最高的点(山峰)
若我们知道 i+1 和 i 值的大小关系 ,就可推断这个人是在山峰的左边还是右边
-
当i+1值 > i值,人在山峰的左边(上坡路)
-
当 i+1值 < i值,人在山峰的右边(下坡路)
这道题的核心思想就是如此。
二分实现:
设置两个指针,left指向第一个元素,right指向最后一个元素,mid指向中间元素。
如果mid-1指向的元素小于mid指向的元素 => mid之后可能存在最大值,left = mid;
如果mid指向的元素大于mid+1指向的元素 => mid之前存在最大值,right=mid。
直到mid指向的元素大于mid+1和mid-1指向的元素时,停止.
图解
- left=0,right = 6,middle = (0+6)/ 2=3
arr[middle-1]=5 < arr[middle]=6
=> middle左边都是升序值,所以峰值必定在middle右边,将左边界位置设为middle+1=4
此时截取右边的子数组:
- left=4,right = 6,middle = (4+6)/ 2=5
arr[middle-1]=7 < arr[middle]=1
=> 说明峰值在middle左边,将右边界位置设为middle=5
截取左边子数组:
- left=4,right = 5,middle = (4+5)/ 2=4
arr[middle]满足大于相邻元素的值,说明此时已找到峰值7,索引为4
function half() {
let left = 0;
let right = arr.length;
// 如果数组长度为1,则峰值索引为0
if (right === 1) {
return 0;
}
while (left < right) {
let middle = parseInt((left+right)/2);
// 如果middle大于相邻元素,返回middle,表示已找到
if (arr[middle] > arr[middle+1] && arr[middle] > arr[middle-1]) {
return middle;
}
if (arr[middle] < arr[middle-1]) {
// 若middle<middle-1值,说明峰值在middle左边,将middle设为右边界
right = middle;
} else {
// 若middle>middle-1值,说明峰值在middle右边,将middle+1设为左边界
left = middle+1;
}
}
return left;
}