典型的题目有:Leetcode 162. 寻找峰值 和 Leetcode 852. 山脉数组的峰顶索引,都是要找出数组中峰值元素的索引。峰值元素的取值比它两边的元素要大,就像山峰的峰顶一样,因此这种数组也被称为山脉数组。
题目要求使用代码的时间复杂度是 O(log n),我们首先想到用二分查找法。可是,数组的元素有起伏变化,还能用二分查找吗?仔细观察会发现,其实峰值的两侧都是有规律地变化:单调递增或递减。Leetcode 852 的数组中只有一个峰值元素。而 Leetcode 162 的数组可能有多个峰值元素,可以把元素的取值变化想象成相邻的多个小山坡,或者波浪型,两个峰值之间的元素也分段符合单调递增或递减规律,因此可以采用相同的思路。
用二分查找解决这类问题的关键点:不是寻找一个确定值 target,而是寻找一个有峰值特征的元素,也就是这个元素比两边元素的值要大。只要修改二分查找中对 middle 元素的判断条件如下:
细节:数组首尾元素。这个方法应用于数组首尾元素,例如 middle = length(nums) - 1 时,nums[middle +1] 超出索引范围,会报错。因此,搜索区间初始为 [ 1, length -2 ]。而判断是否为峰值元素时,区间要有至少 3 个元素: arr[mid-1] < arr[mid] < arr[mid+1],因此,应用二分查找时数组长度不小于 3。
这里以 Leetcode 852 为例,其代码为:
class Solution:
def peakIndexInMountainArray(self, arr: List[int]) -> int:
low, high = 1, len(arr) - 2
while low <= high:
mid = low + (high - low) // 2
if arr[mid] > arr[mid - 1] and arr[mid] > arr[mid + 1]:
return mid
elif arr[mid] < arr[mid + 1]:
low = mid + 1
elif arr[mid] < arr[mid - 1]:
high = mid - 1
return low
Leetcode 162 给定的数组长度不小于 1,需要在上述代码之前加上对于数组长度小于 3 时的处理。此外,这道题假设 nums[-1] = nums[n] = -∞
,数组首尾元素也有可能是峰值元素,需要额外考虑。具体代码为:
# 数组长度为 1,直接返回该元素
if len(nums) == 1:
return 0
# 查看数组首尾元素是否是峰值元素
if nums[0] > nums[1]:
return 0
if nums[-1] > nums[-2]:
return len(nums) - 1
Leetcode 1095. 山脉数组中查找目标值 这道题基于 Leetcode 852 ,但更难一些,还要在数组中寻找一个目标值。因为这个数组是山脉数组,如果目标值出现了两次,题目要求返回较小的索引值。
解题的关键在于要先找到峰值元素,以该元素为界把整个数组划分成递增数组和递减数组。 接下来就容易了,只要先在峰值元素的左区间、再在右区间应用二分查找寻找目标值。先查找左区间、再查找右区间是为了返回符合条件的较小索引。总体要做 3 次二分查找。
当我理解了这道题的解题思路之后,就用 Leetcode 852 的方法寻找峰值。但是代码运行报错:调用 MountainArray.get(k)
函数超过了 100 次限制。我看了这个解答,发现如果用 while low < high,判断条件会简化,因此调用 MountainArray.get(k)
函数次数也会减少。相应的代码及注释如下:
length = mountain_arr.length()
low, high = 1, length - 2
while low < high:
mid = low + (high - low) // 2
# 这种情况下与之前的处理相同
if mountain_arr.get(mid) < mountain_arr.get(mid + 1):
low = mid + 1
# 变化在这里:arr[mid]>arr[mid+1]时
# 把 arr[mid]>arr[mid-1] 和 arr[mid]
# 此时有边界应包含 mid(考虑到arr[mid]>arr[mid-1]时mid符合条件)
else:
high = mid
# 搜索区间只剩一个元素,就是峰值元素
peak = low
注:我用此方法再做一遍 Leetcode 852 ,发现它比前面使用的 while low <= high 方法更快,可能是简化了判断条件带来的提升。
回到本题Leetcode 1095,找到峰值之后,后面就用基本二分查找法先在峰值的左区间 [ 0, peak ],后在峰值的右区间 [ peak+1, length-1 ]寻找就好了。这一步比较简单,就不列出具体代码了。需要注意的是峰值的右区间是单调递减数组,与我们经常遇到的递增数组在左右区间选择上不同。比如,mid 元素值小于目标值时,mid 元素左边的元素比它大,因此应该到左区间继续寻找。 同理,mid 元素值大于目标值时,则应该到右区间继续寻找。
之前分析的两种情况都是在山脉数组中寻找某个元素,主要用到二分查找法。而接下来的这道题 Leetcode 941. 有效的山脉数组 是要判断一个数组是不是有效的山脉数组,即满足:(1)数组长度不小于 3。(2)存在一个元素(峰值元素),该元素左边元素单调递增,该元素右边元素单调递减。
设想一下,如果两人爬的是同一座山,分别从左右两侧的山底开始走,那么最终会在山顶相遇。这个思路也可以用在这道题的解答中,为此我们需要用到双指针。相应的代码及注释如下:
class Solution:
def validMountainArray(self, arr: List[int]) -> bool:
length = len(arr)
# 判断条件(1):山脉数组的长度不小于3
if length < 3:
return False
# 左、右指针分别指向数组的首尾元素
left, right= 0, length - 1
# 左指针往山顶方向移动:
# 山顶元素左右两边都有元素,因此其索引在[1,length-2]范围
# 如果左指针右边相邻的元素值更大,依次增大索引,直到不满足条件
while left + 1 < length - 1 and arr[left + 1] > arr[left]:
left += 1
# 右指针往山顶方向移动:
# 如果右指针左边相邻的元素值更大,依次减小索引,直到不满足条件
while right - 1 > 0 and arr[right - 1] > arr[right]:
right -= 1
# 此时左右指针都指向了某个峰值元素,也就是爬到了某个山顶
# 判断条件(2):山脉数组只有一个山顶,左右指针指向的是同一个山顶吗?
if left == right:
return True
else:
return False
本文对您有帮助的话,请点赞支持一下吧,谢谢!
关注我 宁萌Julie,互相学习,多多交流呀!