山脉数组的峰顶索引 图解题解
这道题到底在问什么
- 输入
- arr = [0,3,6,9,12,15,18,21,17,13,9,5,1]
- 输出
- 7(值 21 是山顶)
最优解:为什么这么做
一句话答案:LeetCode 852 山脉数组:在先升后降的数组里求峰顶下标,用二分——单峰有保证,比中点和右邻谁大就知在上坡还是下坡,上坡收右、下坡收左逼向峰顶,时间 O(log n)、空间 O(1)。
山脉数组的峰顶,题目让你返回什么
给一个山脉数组 arr:它先严格递增爬到某个最高点,再严格递减下来,像一座只有一个尖的山。题目保证有且只有一个峰,要你返回峰顶元素的下标(不是峰顶的值)。题面 arr=[0,3,6,9,12,15,18,21,17,13,9,5,1],21 排在第 7 格,所以返回 7。
一格格扫过去找拐点,慢在哪
从左往右一格格扫,找第一个 arr[i] 大于 arr[i+1] 的位置,那个 i 就是峰顶。可这要把整座山从山脚走到山顶,数组有 n 个数,峰若靠右就得比满将近 n 次,时间 O(n)。题目给的严格单峰、两侧都单调(左边只升、右边只降)的结构,被一格格扫白白浪费了。
单峰撑腰,比一下邻居就知道往哪走
靠的就是『先升后降、只有一个峰』这个保证。站在任意一格 mid,拿它和右邻居 arr[mid+1] 比:arr[mid] 小于右邻说明还在往上爬,峰顶必在它右边,左半连 mid 一起扔;arr[mid] 大于右邻说明已在下坡,峰顶在 mid 左边或就是它。一次比较就砍掉一半,正因为单峰,被丢的那半绝不藏更高的点。
上坡推左界、下坡收右界,循环为什么写 l 小于 r
用两个下标 l、r 圈住还没排除的范围,起初 l=0、r=n-1。每轮取中点 mid=(l+r)//2,和右邻 arr[mid+1] 比:上坡(arr[mid] 小于 arr[mid+1])就让 l 跳到 mid+1,丢掉含 mid 的左半;下坡就让 r 收到 mid——是 mid 本身、不是 mid-1,因为 mid 自己可能就是峰顶,减一会把它一起丢。
循环条件写 while l 小于 r,收到 l==r 就停,范围只剩一格、它就是峰顶。这里有条不变量(每轮都成立的保证):峰顶始终落在 [l,r] 内,每轮要么 l 前进、要么 r 后退,只缩不漏。
[0,3,6,…,21,…,1] 逼到第 7 格
起点 l=0、r=12。第一轮 mid=(0+12)//2=6,arr[6]=18 小于 arr[7]=21 是上坡,l 跳到 7。第二轮 l=7、r=12,mid=9,arr[9]=13 大于 arr[10]=9 是下坡,r 收到 9。第三轮 l=7、r=9,mid=8,arr[8]=17 大于 arr[9]=13 还是下坡,r 收到 8。第四轮 l=7、r=8,mid=7,arr[7]=21 大于 arr[8]=17 下坡,r 收到 7。此时 l=r=7,循环停,返回 7,即峰顶 21 的位置。
下坡那步减一,峰就被顺手丢了
最容易栽的是下坡那支写成 r=mid-1:mid 自己可能就是峰顶,减一把它排除,收敛的位置就偏左一格、错过真正的峰。循环条件也别写成 l 不大于 r——用 r=mid 收缩时一旦 l==r,mid 仍等于 l、区间缩不动,就成死循环。还有个隐蔽点:本题没有 target,别拿 arr[mid] 和某个目标值比,方向全由相邻两格谁大谁小定。
复杂度上,每轮砍掉一半,n 个数最多比约 log₂n 次,时间 O(log n);只用 l、r、mid 三个下标,空间 O(1)。
▶ 动画逐步走查(共 26 步)——想跟着动画一帧帧对照就展开
- 3记住一句话:上坡就往右收(l = mid+1),下坡就往左收(r = mid)。每次砍掉一半,直到 l == r。
- 4第一遍:13 个数的山脉。l 指最左、r 指最右。只要 l 还没和 r 撞上,就一直对半收紧区间。
- 5当前范围 [0, 12] 的正中间是下标 6。接下来把它和右邻下标 7 比一比,判断在上坡还是下坡。
- 6arr[6]=18 比右邻 arr[7]=21 小,说明这里还在往上爬。山顶一定在 6 右边,左边界要推到 mid+1。
- 7把左半(含中点 6)整段灰掉排除,l 跳到 7。一次砍掉一半,范围缩到 [7, 12]。
- 8当前范围 [7, 12] 的正中间是下标 9。接下来把它和右邻下标 10 比一比,判断在上坡还是下坡。
- 9arr[9]=13 比右邻 arr[10]=9 大,说明已经开始下坡。山顶在 9 左边或正好就是它,右边界收到 mid。
- 10把中点右边那段灰掉排除,r 收到 9(中点 9 自己可能就是峰顶,留下)。范围缩到 [7, 9]。
- 11当前范围 [7, 9] 的正中间是下标 8。接下来把它和右邻下标 9 比一比,判断在上坡还是下坡。
- 12arr[8]=17 比右邻 arr[9]=13 大,说明已经开始下坡。山顶在 8 左边或正好就是它,右边界收到 mid。
- 13把中点右边那段灰掉排除,r 收到 8(中点 8 自己可能就是峰顶,留下)。范围缩到 [7, 8]。
- 14当前范围 [7, 8] 的正中间是下标 7。接下来把它和右邻下标 8 比一比,判断在上坡还是下坡。
- 15arr[7]=21 比右邻 arr[8]=17 大,说明已经开始下坡。山顶在 7 左边或正好就是它,右边界收到 mid。
- 16把中点右边那段灰掉排除,r 收到 7(中点 7 自己可能就是峰顶,留下)。范围缩到 [7, 7]。
- 17l 和 r 撞在下标 7,区间只剩一格。这一格 arr[7]=21 就是山顶,返回 7。
- 18第二遍:换一座更矮的山。l 指最左、r 指最右。只要 l 还没和 r 撞上,就一直对半收紧区间。
- 19当前范围 [0, 10] 的正中间是下标 5。接下来把它和右邻下标 6 比一比,判断在上坡还是下坡。
- 20arr[5]=24 比右邻 arr[6]=20 大,说明已经开始下坡。山顶在 5 左边或正好就是它,右边界收到 mid。
- 21把中点右边那段灰掉排除,r 收到 5(中点 5 自己可能就是峰顶,留下)。范围缩到 [0, 5]。
- 22当前范围 [0, 5] 的正中间是下标 2。接下来把它和右邻下标 3 比一比,判断在上坡还是下坡。
- 23arr[2]=8 比右邻 arr[3]=13 小,说明这里还在往上爬。山顶一定在 2 右边,左边界要推到 mid+1。
- 24把左半(含中点 2)整段灰掉排除,l 跳到 3。一次砍掉一半,范围缩到 [3, 5]。
- 25当前范围 [3, 5] 的正中间是下标 4。接下来把它和右邻下标 5 比一比,判断在上坡还是下坡。
- 26arr[4]=19 比右邻 arr[5]=24 小,说明这里还在往上爬。山顶一定在 4 右边,左边界要推到 mid+1。
- 27把左半(含中点 4)整段灰掉排除,l 跳到 5。一次砍掉一半,范围缩到 [5, 5]。
- 28l 和 r 撞在下标 5,区间只剩一格。这一格 arr[5]=24 就是山顶,返回 5。
⚠️ 容易写错的地方
✗ 错:下坡时写成 r = mid - 1
✓ 对:下坡时 r = mid
中点自己可能就是峰顶,减一会把它一起丢掉,导致漏掉真正的峰
✗ 错:循环条件写 l <= r
✓ 对:循环条件 l < r
用 r = mid 时若写 l <= r,l==r 那轮 mid 仍等于 l,区间不再缩小会死循环
✗ 错:拿 arr[mid] 和 target 比(套普通二分模板)
✓ 对:拿 arr[mid] 和 arr[mid+1] 比
本题没有 target,方向由「相邻两格谁大」即上坡还是下坡决定
完整代码(Python / C++ / Java)
Python
def peakIndexInMountainArray(arr):
l, r = 0, len(arr) - 1
while l < r: # 区间收成一点就停
mid = (l + r) // 2
if arr[mid] < arr[mid + 1]: # 上坡
l = mid + 1 # 峰顶在右,丢左半
else: # 下坡或正是峰顶
r = mid # 峰顶在左或就是它
return l # l == r 即峰顶C++
int peakIndexInMountainArray(vector<int>& arr){
int l = 0, r = arr.size() - 1;
while (l < r) {
int mid = (l + r) / 2;
if (arr[mid] < arr[mid + 1]) l = mid + 1;
else r = mid;
}
return l;
}Java
public int peakIndexInMountainArray(int[] arr) {
int l = 0, r = arr.length - 1;
while (l < r) {
int mid = (l + r) / 2;
if (arr[mid] < arr[mid + 1]) l = mid + 1;
else r = mid;
}
return l;
}复杂度
时间
O(log n)
每轮把搜索区间砍掉一半,最多比 log₂n 次
空间
O(1)
只用 l、r、mid 三个下标变量,不开额外数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 山脉数组的峰顶索引 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么比较 arr[mid] 和 arr[mid+1] 不会下标越界?+
因为进循环时区间至少还有两格。循环条件是 l 小于 r,此刻 l 和 r 之间至少隔着一格,mid=(l+r)//2 向下取整必然严格小于 r,于是 mid+1 不大于 r,永远落在数组范围内,读 arr[mid+1] 不会越界。反过来,正是靠 l 小于 r 这个条件兜住了右邻居的合法性,比较和循环条件是绑在一起的。
这题和 LC162 寻找峰值几乎一样,差在哪?+
骨架完全相同——都是比 mid 和右邻居、上坡把 l 推到 mid+1、下坡把 r 收到 mid。差别在 LC162 不保证单峰,数组里可能有多个局部峰、两端当作负无穷处理;但『往更高的那侧邻居走,一定能撞到某个峰』这条性质照样成立,所以同一套二分能找到其中任意一个峰。本题多了严格单峰的保证,找到的必然是唯一那个峰,逻辑上更省心,代码一字不用改。
能不能直接 O(n) 线性扫,为什么二分更好?+
能,扫一遍找第一个 arr[i] 大于 arr[i+1] 就是峰顶,但那是 O(n)。题目给的严格山脉带来了二段性(整段能被一个判断切成峰左、峰右两截):站在任意一格,比一下相邻两格就知道自己在峰的哪一侧,于是能用二分每轮砍掉一半,做到 O(log n)。正因为有这条能把范围一分为二的判定,才配得上二分。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 山脉数组的峰顶索引 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。