LeetCode中等难度题目「162. 寻找峰值」,这道题的核心考点是二分查找的灵活运用——题目明确要求时间复杂度O(log n),而很多人第一反应会想到暴力遍历,两种思路的碰撞正好能帮我们吃透二分查找的本质。
先明确题目核心,避免踩坑:
峰值元素定义:值严格大于左右相邻值的元素;
边界条件:默认nums[-1] = nums[n] = -∞(意味着数组首尾元素只要比相邻的一个元素大,就是峰值);
输出要求:数组可能有多个峰值,返回任意一个即可;
关键约束:必须实现O(log n)时间复杂度。
一、先看直观解法:暴力遍历(不满足O(log n),但帮我们理解题意)
拿到题目,最容易想到的就是从头到尾遍历数组,找到一个满足“严格大于左右”的元素。但先看下面这段暴力代码,其实它的思路更巧妙——利用了题目边界条件的隐藏优势。
function findPeakElement_1(nums: number[]): number {
let idx: number = 0;
for (let i = 1; i < nums.length; ++i) {
if (nums[i] > nums[idx]) {
idx = i;
}
}
return idx;
};
暴力解法解析
这段代码并没有逐元素判断“是否大于左右”,而是维护一个当前最大值的索引idx,遍历过程中不断更新idx为更大元素的位置,最终返回idx。为什么这样是对的?
核心逻辑:结合边界条件nums[n] = -∞,数组的最后一个元素如果是当前最大值,那么它一定是峰值(因为它右边是-∞,只要比左边大即可);而遍历过程中,idx始终指向当前最大元素,这个元素的左边没有比它更大的元素,右边如果有更大的就会更新idx,直到遍历结束,idx指向的元素必然满足“比左边大”,且右边要么是更小的元素,要么是-∞,因此一定是峰值。
但注意:这种解法的时间复杂度是O(n),虽然能通过所有测试用例,但不满足题目要求的O(log n),因此只能作为辅助理解的思路,不能作为最终解法。
二、核心解法:二分查找(满足O(log n),吃透核心思路)
二分查找的前提通常是“有序数组”,但这道题的数组是无序的,为什么能用到二分?这正是这道题的巧妙之处——我们不需要找到所有峰值,只需要找到任意一个,而数组的“局部单调性”足够支撑二分。
先看满足要求的二分解法代码:
function findPeakElement(nums: number[]): number {
const n: number = nums.length;
let left = 0;
let right = nums.length - 1;
// 辅助函数:比较两个索引对应的元素大小,处理边界(索引越界返回-∞)
const compare = (idx1: number, idx2: number): boolean => {
const get = (idx: number): number => {
if (idx < 0 || idx >= n) {
return -Infinity;
}
return nums[idx];
}
const num1 = get(idx1);
const num2 = get(idx2);
return num1 < num2;
}
while (left < right) {
// 计算中间索引(避免left+right溢出,等价于Math.floor((left+right)/2))
const mid = Math.floor((left + right) / 2);
// 关键判断:比较mid和mid+1的大小,确定峰值所在区间
if (compare(mid, mid + 1)) {
// 右侧元素更大 → 峰值一定在右侧区间[mid+1, right]
left = mid + 1;
} else {
// 左侧元素更大(或相等)→ 峰值一定在左侧区间[left, mid]
right = mid;
}
}
return left;
};
二分解法核心思路(重中之重)
二分查找的关键是“每次缩小一半查找区间”,这道题的核心是「如何判断峰值在左半区间还是右半区间」,这里的判断逻辑基于一个简单但关键的观察:
对于任意中间索引mid,比较nums[mid]和nums[mid+1]:
-
如果nums[mid] < nums[mid+1]:说明从mid到mid+1是“上升”的。结合边界条件nums[n] = -∞,继续往右侧走,必然会出现一个“下降”的点(否则一直上升到末尾,末尾元素就是峰值),因此峰值一定在[mid+1, right]区间内;
-
如果nums[mid] > nums[mid+1]:说明从mid到mid+1是“下降”的。结合边界条件nums[-1] = -∞,往左侧走,必然会出现一个“上升”的点(否则一直下降到开头,开头元素就是峰值),因此峰值一定在[left, mid]区间内;
-
如果nums[mid] == nums[mid+1]:题目要求“严格大于”,因此mid和mid+1都不可能是峰值,此时归入第二种情况(nums[mid] > nums[mid+1]),缩小到左半区间即可(不影响最终结果,因为我们只需要任意一个峰值)。
细节补充(避坑点)
-
边界处理:辅助函数get(idx)专门处理索引越界的情况,当idx < 0或idx >= n时,返回-∞,完美契合题目给出的边界条件,避免手动判断首尾元素的特殊情况;
-
mid计算:用Math.floor((left + right)/2),避免left+right数值过大导致溢出(虽然在JavaScript/TypeScript中数值溢出概率低,但这是二分查找的规范写法);
-
循环终止条件:left < right,当left == right时,循环终止,此时left(或right)就是峰值的索引——因为每次缩小区间都能保证“区间内一定存在峰值”,最终收敛到唯一的索引就是峰值。
三、两种解法对比&总结
| 解法 | 时间复杂度 | 空间复杂度 | 核心优势 | 适用场景 |
|---|---|---|---|---|
| 暴力遍历(findPeakElement_1) | O(n) | O(1) | 思路简单,代码简洁 | 不要求时间复杂度,快速解题 |
| 二分查找(findPeakElement) | O(log n) | O(1) | 满足题目约束,效率更高 | 题目要求O(log n),面试高频考点 |
四、面试延伸&思考
这道题是面试中常考的“二分查找变种”,重点考察你是否能跳出“二分只能用于有序数组”的思维定式,核心是抓住“局部单调性”和“边界条件带来的必然峰值”。
延伸思考:
-
如果数组中存在多个峰值,二分查找会返回哪一个?—— 取决于二分过程中区间缩小的方向,但无论返回哪一个,都满足题目要求;
-
如果数组是严格递增的,二分查找会返回什么?—— 会返回最后一个元素(因为最后一个元素右边是-∞,是峰值);
-
如果数组是严格递减的,二分查找会返回什么?—— 会返回第一个元素(因为第一个元素左边是-∞,是峰值)。
最后总结:这道题的关键的是理解“二分查找的核心是缩小查找区间”,而不是“有序数组”。只要能找到一个判断条件,每次将区间缩小一半,并且保证目标一定在缩小后的区间内,就可以用二分查找。