Skip to content

Commit 124b6a7

Browse files
committed
✨feat: Add 剑指 Offer II 069
1 parent b0cd196 commit 124b6a7

File tree

3 files changed

+224
-0
lines changed

3 files changed

+224
-0
lines changed

Index/三分.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
| 题目 | 题解 | 难度 | 推荐指数 |
22
| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | -------- |
33
| [852. 山脉数组的峰顶索引](https://leetcode-cn.com/problems/peak-index-in-a-mountain-array/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/peak-index-in-a-mountain-array/solution/gong-shui-san-xie-er-fen-san-fen-cha-zhi-5gfv/) | 简单 | 🤩🤩🤩🤩🤩 |
4+
| [剑指 Offer II 069. 山峰数组的顶部](https://leetcode-cn.com/problems/B1IidL/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/B1IidL/solution/gong-shui-san-xie-er-fen-san-fen-ji-zhi-lc8zl/) | 简单 | 🤩🤩🤩🤩🤩 |
45

Index/二分.md

+1
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@
3838
| [1838. 最高频元素的频数](https://leetcode-cn.com/problems/frequency-of-the-most-frequent-element/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/frequency-of-the-most-frequent-element/solution/gong-shui-san-xie-cong-mei-ju-dao-pai-xu-kxnk/) | 中等 | 🤩🤩🤩 |
3939
| [1894. 找到需要补充粉笔的学生编号](https://leetcode-cn.com/problems/find-the-student-that-will-replace-the-chalk/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/find-the-student-that-will-replace-the-chalk/solution/gong-shui-san-xie-yi-ti-shuang-jie-qian-kpqsk/) | 中等 | 🤩🤩🤩🤩 |
4040
| [剑指 Offer 53 - I. 在排序数组中查找数字 I](https://leetcode-cn.com/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/solution/gong-shui-san-xie-liang-chong-er-fen-ton-3epx/) | 简单 | 🤩🤩🤩🤩🤩 |
41+
| [剑指 Offer II 069. 山峰数组的顶部](https://leetcode-cn.com/problems/B1IidL/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/B1IidL/solution/gong-shui-san-xie-er-fen-san-fen-ji-zhi-lc8zl/) | 简单 | 🤩🤩🤩🤩🤩 |
4142

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
### 题目描述
2+
3+
这是 LeetCode 上的 **[剑指 Offer II 069. 山峰数组的顶部](https://leetcode-cn.com/problems/B1IidL/solution/gong-shui-san-xie-er-fen-san-fen-ji-zhi-lc8zl/)** ,难度为 **简单**
4+
5+
Tag : 「二分」、「三分」
6+
7+
符合下列属性的数组 `arr` 称为 山峰数组(山脉数组) :
8+
* `arr.length >= 3`
9+
* 存在 `i``0 < i < arr.length - 1`)使得:
10+
* `arr[0] < arr[1] < ... arr[i-1] < arr[i]`
11+
* `arr[i] > arr[i+1] > ... > arr[arr.length - 1]`
12+
13+
给定由整数组成的山峰数组 arr ,返回任何满足 `arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1]` 的下标 `i` ,即山峰顶部。
14+
15+
示例 1:
16+
```
17+
输入:arr = [0,1,0]
18+
19+
输出:1
20+
```
21+
示例 2:
22+
```
23+
输入:arr = [1,3,5,4,2]
24+
25+
输出:2
26+
```
27+
示例 3:
28+
```
29+
输入:arr = [0,10,5,2]
30+
31+
输出:1
32+
```
33+
示例 4:
34+
```
35+
输入:arr = [3,4,5,1]
36+
37+
输出:2
38+
```
39+
示例 5:
40+
```
41+
输入:arr = [24,69,100,99,79,78,67,36,26,19]
42+
43+
输出:2
44+
```
45+
46+
47+
提示:
48+
* 3 <= arr.length <= $10^4$
49+
* 0 <= arr[i] <= $10^6$
50+
* 题目数据保证 arr 是一个山脉数组
51+
52+
**进阶:很容易想到时间复杂度 O(n) 的解决方案,你可以设计一个 O(log(n)) 的解决方案吗?**
53+
54+
---
55+
56+
### 二分
57+
58+
往常我们使用「二分」进行查值,需要确保序列本身满足「二段性」:当选定一个端点(基准值)后,结合「一段满足 & 另一段不满足」的特性来实现“折半”的查找效果。
59+
60+
但本题求的是峰顶索引值,如果我们选定数组头部或者尾部元素,其实无法根据大小关系“直接”将数组分成两段。
61+
62+
但可以利用题目发现如下性质:**由于 `arr` 数值各不相同,因此峰顶元素左侧必然满足严格单调递增,峰顶元素右侧必然不满足。**
63+
64+
因此 **以峰顶元素为分割点的 `arr` 数组,根据与 前一元素/后一元素 的大小关系,具有二段性:**
65+
66+
* 峰顶元素左侧满足 $arr[i-1] < arr[i]$ 性质,右侧不满足
67+
* 峰顶元素右侧满足 $arr[i] > arr[i+1]$ 性质,左侧不满足
68+
69+
因此我们可以选择任意条件,写出若干「二分」版本。
70+
71+
代码:
72+
```Java
73+
class Solution {
74+
// 根据 arr[i-1] < arr[i] 在 [1,n-1] 范围内找值
75+
// 峰顶元素为符合条件的最靠近中心的元素
76+
public int peakIndexInMountainArray(int[] arr) {
77+
int n = arr.length;
78+
int l = 1, r = n - 1;
79+
while (l < r) {
80+
int mid = l + r + 1 >> 1;
81+
if (arr[mid - 1] < arr[mid]) l = mid;
82+
else r = mid - 1;
83+
}
84+
return r;
85+
}
86+
}
87+
```
88+
```Java
89+
class Solution {
90+
// 根据 arr[i] > arr[i+1] 在 [0,n-2] 范围内找值
91+
// 峰顶元素为符合条件的最靠近中心的元素值
92+
public int peakIndexInMountainArray(int[] arr) {
93+
int n = arr.length;
94+
int l = 0, r = n - 2;
95+
while (l < r) {
96+
int mid = l + r >> 1;
97+
if (arr[mid] > arr[mid + 1]) r = mid;
98+
else l = mid + 1;
99+
}
100+
return r;
101+
}
102+
}
103+
```
104+
```Java
105+
class Solution {
106+
// 根据 arr[i-1] > arr[i] 在 [1,n-1] 范围内找值
107+
// 峰顶元素为符合条件的最靠近中心的元素的前一个值
108+
public int peakIndexInMountainArray(int[] arr) {
109+
int n = arr.length;
110+
int l = 1, r = n - 1;
111+
while (l < r) {
112+
int mid = l + r >> 1;
113+
if (arr[mid - 1] > arr[mid]) r = mid;
114+
else l = mid + 1;
115+
}
116+
return r - 1;
117+
}
118+
}
119+
```
120+
```Java
121+
class Solution {
122+
// 根据 arr[i] < arr[i+1] 在 [0,n-2] 范围内找值
123+
// 峰顶元素为符合条件的最靠近中心的元素的下一个值
124+
public int peakIndexInMountainArray(int[] arr) {
125+
int n = arr.length;
126+
int l = 0, r = n - 2;
127+
while (l < r) {
128+
int mid = l + r + 1 >> 1;
129+
if (arr[mid] < arr[mid + 1]) l = mid;
130+
else r = mid - 1;
131+
}
132+
return r + 1;
133+
}
134+
}
135+
```
136+
* 时间复杂度:$O(\log{n})$
137+
* 空间复杂度:$O(1)$
138+
139+
---
140+
141+
### 三分
142+
143+
事实上,我们还可以利用「三分」来解决这个问题。
144+
145+
顾名思义,**「三分」就是使用两个端点将区间分成三份,然后通过每次否决三分之一的区间来逼近目标值。**
146+
147+
具体的,由于峰顶元素为全局最大值,因此我们可以每次将当前区间分为 $[l, m1]$、$[m1, m2]$ 和 $[m2, r]$ 三段,如果满足 $arr[m1] > arr[m2]$,说明峰顶元素不可能存在与 $[m2, r]$ 中,让 $r = m2 - 1$ 即可。另外一个区间分析同理。
148+
149+
代码:
150+
```Java
151+
class Solution {
152+
public int peakIndexInMountainArray(int[] arr) {
153+
int n = arr.length;
154+
int l = 0, r = n - 1;
155+
while (l < r) {
156+
int m1 = l + (r - l) / 3;
157+
int m2 = r - (r - l) / 3;
158+
if (arr[m1] > arr[m2]) r = m2 - 1;
159+
else l = m1 + 1;
160+
}
161+
return r;
162+
}
163+
}
164+
```
165+
* 时间复杂度:$O(\log{n})$
166+
* 空间复杂度:$O(1)$
167+
168+
---
169+
170+
### 二分 & 三分 & k 分 ?
171+
172+
必须说明一点,「二分」和「三分」在渐进复杂度上都是一样的,都可以通过换底公式转化为可忽略的常数,因此两者的复杂度都是 $O(\log{n})$。
173+
174+
因此选择「二分」还是「三分」取决于要解决的是什么问题:
175+
176+
* 二分通常用来解决单调函数的找 $target$ 问题,但进一步深入我们发现只需要满足「二段性」就能使用「二分」来找分割点;
177+
* 三分则是解决单峰函数极值问题。
178+
179+
**因此一般我们将「通过比较两个端点,每次否决 1/3 区间 来解决单峰最值问题」的做法称为「三分」;而不是简单根据单次循环内将区间分为多少份来判定是否为「三分」。**
180+
181+
随手写了一段反例代码:
182+
```Java
183+
class Solution {
184+
public int peakIndexInMountainArray(int[] arr) {
185+
int left = 0, right = arr.length - 1;
186+
while(left < right) {
187+
int m1 = left + (right - left) / 3;
188+
int m2 = right - (right - left + 2) / 3;
189+
if (arr[m1] > arr[m1 + 1]) {
190+
right = m1;
191+
} else if (arr[m2] < arr[m2 + 1]) {
192+
left = m2 + 1;
193+
} else {
194+
left = m1;
195+
right = m2;
196+
}
197+
}
198+
return left;
199+
}
200+
}
201+
```
202+
203+
这并不是「三分」做法,最多称为「变形二分」。本质还是利用「二段性」来做分割的,只不过同时 check 了两个端点而已。
204+
205+
如果这算「三分」的话,那么我能在一次循环里面划分 $k - 1$ 个端点来实现 $k$ 分?
206+
207+
**显然这是没有意义的,因为按照这种思路写出来的所谓的「四分」、「五分」、「k 分」是需要增加同等数量的分支判断的。这时候单次 `while` 决策就不能算作 $O(1)$ 了,而是需要在 $O(k)$ 的复杂度内决定在哪个分支,就跟上述代码有三个分支进行判断一样。** 因此,这种写法只能算作是「变形二分」。
208+
209+
**综上,只有「二分」和「三分」的概念,不存在所谓的 $k$ 分。** 同时题解中的「三分」部分提供的做法就是标准的「三分」做法。
210+
211+
---
212+
213+
### 最后
214+
215+
这是我们「刷穿 LeetCode」系列文章的第 `No.剑指 Offer II 069` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
216+
217+
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
218+
219+
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode
220+
221+
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。
222+

0 commit comments

Comments
 (0)