leetcode 上的 peak finding 问题汇总

date
Nov 17, 2022
slug
peak-finding-leetcode
status
Published
tags
算法
summary
巧妙利用二分解题
type
Post

题目

一共三道,每道题之间互相有联系
题目虽然没有保证 distinct,不过我们仍然可以用 divide and conquer 的思想尝试解题

852. 山脉数组的峰顶索引

方法1 暴力

读题,数据范围很小,直接遍历即可
这里有一个技巧,我们不需要真的去判断 A[i - 1], A[i], A[i + 1] 的关系,我们只需要找到第一个A[i] > A[i + 1]i即可,因为这个i必然满足A[i - 1] < A[i]

方法2 二分

二分查找的思想,山脉数组总是一个先上升后下降的趋势,那么按照二分的思想,每次取得mid进行判断
  1. mid就是山顶,返回mid
  1. mid处于局部上升的区间,返回右边区间的二分
  1. mid处于局部下降的区间,返回左边区间的二分
由此可以写出代码:
特别的,由于题目输入保证有解,我们可以初始化边界为 1 和 n - 2

162. 寻找峰值

就是之前提到的1D-finding问题,不过在leetcode上面可以做得更巧妙

方法1 暴力

自然而然就想到的做法

方法2 二分

之前的文章我们分析过了,如果一个数组是distinct的,那么这个数组至少会有一个峰值,且如果峰值的下标i不属于,那么峰值一定会出现在数组A的起始或者末尾,基于上述的结论,二分法在distinct的数组里面一定能找到峰值,我们不断往更大值的方向走就行了。

方法3 改进的暴力

由山脉数组这道题推导得来的结论 其实这种严格不重复的数组不外乎就四种形状
  • 一直上升
  • 一直下降
  • 先上升后下降
  • 先下降再上升
任何更复杂的数组,它的局部一定能找到上述形状,既然局部存在着上述形状,那么一定会存在峰值,所以我们可以分析这四种形状来解决问题
基于这个性质,我提出假设:我们使i=0,然后从第一个元素开始比较,只需要比较第i个元素和第i+1个元素的大小即可
对于假设的证明,我们使用反证法:
对于i属于0到A.length-1,当A[i]>A[i+1]的时候,A[i]一定是峰值。 这是因为如果A[i]不是峰值,那么必定有A[i-1]>A[i],此时如果A[i-1]是峰值就不会遍历到A[i],所以A[i-1]不是峰值,继续往上推导,得出结论A[0]一定大于A[1],否则A[1]是峰值,但是A[1]不是,所以A[0]一定大于A[1],于是A[0]是峰值,但是A[0]必然不是峰值,因为已经遍历到A[i],所以可以得出A[0],A[1]...A[i-1]一定是小于A[i]的,反证法得出A[i]一定是峰值
由上我们可以得出更加简洁的暴力法

方法4 改进的二分

我们再继续讨论一下二分法能否用在这个问题上,对于nums,我们随机取得一个下标mid,,当nums[mid]同时满足nums[mid-1]<nums[mid]<nums[mid+1]的时候,mid对应了一个峰值。
但是经过上述的讨论我们发现,改进的暴力法只需要比较nums[mid]和nums[mid+1]的大小即可,由此我们先尝试写出二分的三个步骤,再验证是否正确。 三个步骤就是:满足条件(二分边界),往左二分,往右二分
取mid
  1. 如果nums[mid]>nums[mid+1],此时需要判断nums[mid-1]nums[mid]的关系,需要往左边二分,由于nums[mid]有可能是峰顶,所以我们往左边二分的区间是[left,mid]
  1. 如果nums[mid]<nums[mid+1],那么nums[mid]必然不是峰顶,而numd[mid+1]是峰顶的可能性存在,所以我们需要往右边二分,二分的区间是[mid+1,right]
  1. 上面分别是往左和往右二分的情况,现在我们需要终止二分的条件,思考极端情况,当数组元素只有一个的时候,那么这个元素一定是峰值,所以我们可以得出,当二分的left和right相等的时候,此时直接返回left即可,这个元素一定是峰值
观察1,2两个步骤,可能会有疑问,如果往左或者往右二分的时候,峰值不存在怎么办?回到这篇文章的1D finding推论,当多次二分达到边界之后,此时边界一定会是峰值,所以这样二分是正确的
由此可以写出代码

74. 搜索二维矩阵

方法1 暴力

首先暴力法很容易想到,时间复杂度$O(MN)$

方法2 二分

一个二维矩阵,总是可以展开成一维数组来存储,满足题目中条件的矩阵展开成一维数组后就是一个升序序列,求一个升序序列中是否存在目标值,当然可以用二分法来做,明确可以用二分之后,也有很多种做法,这里先写用整个矩阵来二分
上面是用整个矩阵做二分,时间复杂度是
注意到这个矩阵里面,每一行都是一个升序序列,那么我们可以确定target在矩阵中大概位于哪一行,然后再在那个区间判断,这样不需要对整个矩阵做二分了
改进后的二分解法,时间复杂度是$O(m+log(n))$
注意到 矩阵的第一列一定也是一个升序数组,所以我们对第一个遍历过程改成二分
写得很复杂,没有原来的方法简洁,但是时间复杂度变为

方法3 利用数组特性

这个数组的特点很明显,要想办法利用一下
每行中的整数从左到右按升序排列。 每行的第一个整数大于前一行的最后一个整数。
比如这个数组,我们逆时针旋转90度,以7为出发点,就能得到一颗类似于二叉搜索树的矩阵,那么我们从根节点出发,利用搜索树的特性就能得到答案了
时间复杂度,上面的解法在《剑指offer》里面也有介绍

总结

上面这三道题就是finding问题,由于数组的特殊性,巧妙的解法都是从A[M]A[M-1],A[M+1]的关系来入手的
 

© hhmy 2019 - 2024