leetcode上的peak finding问题汇总

题目

一共三道,每道题之间互相有联系

https://leetcode-cn.com/problems/peak-index-in-a-mountain-array/

https://leetcode-cn.com/problems/find-peak-element/

https://leetcode-cn.com/problems/search-a-2d-matrix/

852. 山脉数组的峰顶索引

方法1 暴力

读题,发现直接遍历即可

class Solution {
    public int peakIndexInMountainArray(int[] A) {
        for(int i = 1; i<A.length-1; i++){
            if(A[i]>A[i+1])
                return i;
        }
        return -1;
    }
}

方法2 二分

二分查找的思想,山脉数组总是一个先上升后下降的趋势,那么按照二分的思想,每次取得mid进行判断

  1. mid就是山顶,返回mid
  2. mid处于局部上升的区间,返回右边区间的二分
  3. mid处于局部下降的区间,返回左边区间的二分

由此可以写出代码

class Solution {
   public int peakIndexInMountainArray(int[] A) {
        int left = 0, right = A.length - 1;
        while (left<=right) {
            int mid = (right + left) / 2;
            if (A[mid] > A[mid + 1] && A[mid] > A[mid - 1]) {
                return mid;
            } else if (A[mid] < A[mid + 1]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return -1;
    }
}

但是上面的二分并没有很好的利用山脉数组的性质,写起来很复杂,下面有改进的二分

方法3 改进的二分

观察暴力法,我们发现整个山脉数组一定是先上升后下降的趋势,那么这个数组里面的点一共有三种情况

  1. 当前点是峰值
  2. 当前点处于一个局部上升区域,即A[i]<A[i+1]
  3. 当前点处于一个局部下降区域,即A[i]>A[i+1]且A[i-1]>A[i]。需要注意的是,如果只有A[i]>A[i+1],这个时候有可能是峰顶

继续分析,如果我们想要使用二分,可以这样写

class Solution {
   public int peakIndexInMountainArray(int[] A) {
        int left = 0, right = A.length - 1;
		// 二分到只有一个元素的时候退出,不再比较
        while (left<right) {
        	int mid=(right+left)/2;
        	//	可能是局部下降区域,也可能是peak,判断左边区域,包括当前元素,以免错过峰顶
        	if(A[mid]>A[mid+1])
        		right=mid;
        	else if(A[mid]<A[mid+1])	// 是局部上升区域,无需考虑mid点,判断右边区域
        		left=mid+1;
        }
        return left;
    }
}

可以手动推算二分的过程,发现上面的二分总是在往峰值方向逼近的,最后得出的结果也是正确的


162. 寻找峰值

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

方法1 暴力

自然而然就想到的做法

class Solution {
    public int findPeakElement(int[] a) {
    	// 特殊判断
        if (a.length == 0)
            return -1;
        if (a.length == 1)
            return 0;

        // 判断是否在边界
        if (a[0] > a[1])
            return 0;
        else if (a[a.length - 1] > a[a.length - 1 - 1])
            return a.length - 1;
        
        // 不在边界就一定在中间
        for (int i = 1; i < a.length - 1; i++) {
            if (a[i] > a[i - 1] && a[i] > a[i + 1])
                return i;
        }
        
        return -1;
    }
}

方法2 二分

需要推导一下,如果一个数组是distinct的,那么这个数组至少会有一个峰值,且如果峰值的下标i不属于 { i ∣ 0 < i < A . l e n g h t } \{i| 0<i<A.lenght\} {i0<i<A.lenght},那么峰值一定会出现在数组A的起始或者末尾
基于上述的结论,二分法在distinct属性的数组里面一定能找到峰值
不过我们写代码的时候要额外判断一下

class Solution {
    public int findPeakElement(int[] nums) {
        if (nums == null || nums.length == 0)
            return -1;
        if (nums.length == 1) {
            return 0;
        }
        if (nums.length == 2) {
            return nums[0] > nums[1] ? 0 : 1;
        }
        int lo = 0, hi = nums.length - 1;
        int mi = lo + (hi - lo) / 2;
        // 没有碰到边界情况
        while (mi != 0 && mi != nums.length - 1) {
            if (nums[mi] > nums[mi + 1] && nums[mi] > nums[mi - 1]) {
                return mi;
            } else {
                if (nums[mi + 1] > nums[mi - 1])
                    lo = mi + 1;
                else
                    hi = mi - 1;
            }
            mi = lo + (hi - lo) / 2;
        }
        // 边界情况特殊判断
        if (mi == 0) {
            return nums[mi] > nums[mi + 1] ? mi : mi + 1;
        } else {
            return nums[mi] > nums[mi - 1] ? mi : mi - 1;
        }
    }
}

方法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]一定是峰值

由上我们可以得出更加简洁的暴力法

class Solution {
    public int findPeakElement(int[] nums) {
        int i=0;
        for(;i<nums.length-1;i++){
            if(nums[i]>nums[i+1]){
                return i;
            }
        }
        return i;
    }
}

方法4 改进的二分

我们再继续讨论一下二分法能否用在这个问题上,对于nums,我们随机取得一个下标mid, m i d ⊂ { i ∣ 0 < i < n u m s . l e n g t h − 1 } mid\subset\{i| 0<i<nums.length-1\} mid{i0<i<nums.length1},当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]
  2. 如果nums[mid]<nums[mid+1],那么nums[mid]必然不是峰顶,而numd[mid+1]是峰顶的可能性存在,所以我们需要往右边二分,二分的区间是[mid+1,right]
  3. 上面分别是往左和往右二分的情况,现在我们需要终止二分的条件,思考极端情况,当数组元素只有一个的时候,那么这个元素一定是峰值,所以我们可以得出,当二分的left和right相等的时候,此时直接返回left即可,这个元素一定是峰值

观察1,2两个步骤,可能会有疑问,如果往左或者往右二分的时候,峰值不存在怎么办?回到这篇文章的1D finding推论,当多次二分达到边界之后,此时边界一定会是峰值,所以可以这样二分

由此可以写出代码

class Solution {
    public int findPeakElement(int[] nums) {
        int lo=0,hi=nums.length-1;
        while(lo<hi){
            int mid = (hi+lo)/2;
            if(nums[mid]>nums[mid+1]){
                hi=mid;
            }else{
                lo=mid+1;
            }
        }
        return lo;
    }
}

其实这个解法和第一题的二分解法是很像的

这个二分第一次接触真的感觉很玄学,可以自己试着推导一下为什么二分递归达到边界的时候,边界一定是峰值

如果对二分不熟悉的同学,比如说什么时候用开区间,什么时候用闭区间,边界值的取值,我一般做法就是推导一下过程,没有特意去记开区间还是闭区间


74. 搜索二维矩阵

这道题不是我们说的2D-finding问题(找到矩阵中的某个元素,它大于四周的元素),但是思想挺相近的,所以也写一下
贴一下题目吧
在这里插入图片描述

方法1 暴力

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

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        for(int i=0;i<matrix.length;i++){
            for(int j=0;j<matrix[i].length;j++){
                if(matrix[i][j]==target)
                    return true;
            }
        }
        return false;
    }
}

方法2 二分

一个二维矩阵,总是可以展开成一维数组来存储,满足题目中条件的矩阵展开成一维数组后就是一个升序序列,求一个升序序列中是否存在目标值,当然可以用二分法来做,明确可以用二分之后,也有很多种做法,这里先写用整个矩阵来二分的做法

取得矩阵的元素个数m*n个,按照lo=0,hi=m*n-1的区间来二分,取得mid之后,要做的是把一维数组中的mid转化成矩阵里面的i,j,细节在代码上。时间复杂度是 O ( l o g ( m n ) ) O(log(mn)) O(log(mn))

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if(matrix==null || matrix.length==0||(matrix.length==1&&matrix[0].length==0))
            return false;
        // m是行,n是列
        int m=matrix.length,n=matrix[0].length;
        int lo=0,hi=m*n-1;
        int i=0,j=0;
        while(lo<=hi){
            int mid=(hi+lo)/2;
            i=mid/n;
            j=mid%n;
            if(matrix[i][j]==target)
                return true;
            else if(matrix[i][j]>target){
                hi=mid-1;
            }else{
                lo=mid+1;
            }
        }
        return false;
    }
}

上面是用整个矩阵做二分,注意到这个矩阵里面,每一行都是一个升序序列,那么我们可以确定target在矩阵中大概位于哪一行,然后再在那个区间判断,这样不需要对整个矩阵做二分了

改进后的二分解法,时间复杂度是 O ( m + l o g ( n ) ) O(m+log(n)) O(m+log(n))

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if(matrix==null || matrix.length==0||(matrix.length==1&&matrix[0].length==0))
            return false;
        int flag=0;
        // 确定target可能位于哪一行
        for(int i=0;i<matrix.length;i++){
            if(matrix[i][0]>target){
                //如果起点大于target,那么target不会出现在这一行及以后
                flag=i-1;    
                break;
            }else if(matrix[i][0]<target){
                flag=i;    //如果起点小于target,那么有可能存在这一行中
            }else{
                return true;
            }
        }
        if(flag<0) return false;
        return helper(matrix[flag],target);

    }  
    public boolean helper(int A[],int target){
        int lo=0,hi=A.length-1;
        while(lo<=hi){
            int mid=(hi+lo)/2;
            if(A[mid]==target)
                return true;
            else if(A[mid]>target){
                hi=mid-1;
            }else{
                lo=mid+1;
            }
        }
        return false;
    }
}

注意到 矩阵的第一列一定也是一个升序数组,所以我们对第一个遍历过程改成二分

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if(matrix==null || matrix.length==0||(matrix.length==1&&matrix[0].length==0))
            return false;
        // 确定target可能位于哪一行
        int flag=find_index(matrix,target)-1;
        if(flag<0) return false;
        return helper(matrix[flag],target);
    }  

    public int find_index(int[][] matrix, int target){
        // upper_bound 返回第一个大于target的数 注意传入的区间是[0,matrix.length]
        int lo=0,hi=matrix.length;
        while(lo<hi){
            int mid=(hi+lo)/2;
            // 如果起始元素大于target,hi不动,让lo逼近hi
            if(matrix[mid][0]>target){
                hi=mid;
            }else{
                // 如果起始元素小于或等于target,那么一定不是要找的数,让lo=mid+1往后找
                lo=mid+1;
            }
        }
        return lo;
    }

    public boolean helper(int A[],int target){
        int lo=0,hi=A.length-1;
        while(lo<=hi){
            int mid=(hi+lo)/2;
            if(A[mid]==target)
                return true;
            else if(A[mid]>target){
                hi=mid-1;
            }else{
                lo=mid+1;
            }
        }
        return false;
    }
}

写得很复杂,没有原来的方法简洁,但是时间复杂度变为 O ( l o g ( m + n ) ) O(log(m+n)) O(log(m+n))

方法3 利用数组特性

这个数组的特点很明显,要想办法利用一下

每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。

这个矩阵虽然只说了行的特性,但是按照这个性质,每一列的元素也是递增的,而最小值分布在矩阵左上角,最大值分布在矩阵的右下角,很直观就想到从左上角出发按照下面的步骤遍历

  1. 判断当前[x,y]与target的值
  2. 如果[x,y]大于target,那么x–
  3. 如果[x,y]小于target,那么y++
  4. 如果[x,y]==target,那么返回true
  5. 如果遍历过程中x,y出界,那么就返回false

但是实际操作过程中,这样会漏掉数字

matrix = [
  [1,   3,  5,  7],
  [10, 11, 16, 20],
  [23, 30, 34, 50]
]
target = 16

如上,会一直y++导致出界,所以从左上角出发不合适
然后试一下从右下角50出发,发现也不合适
试一下从左下角23出发,发现可以找到,那么上述步骤变为
x=matrix.length-1,y=0

  1. 判断当前[x,y]与target的值
  2. 如果[x,y]大于target,那么x–
  3. 如果[x,y]小于target,那么y++
  4. 如果[x,y]==target,那么返回true
  5. 如果遍历过程中x,y出界,那么就返回false

时间复杂度是 O ( m + n ) O(m+n) O(m+n) 其实不如上面的二分

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if(matrix==null || matrix.length==0||(matrix.length==1&&matrix[0].length==0))
            return false;
        int x=matrix.length-1,y=0;
        while(x>=0&&y<matrix[0].length){
            if(matrix[x][y]>target)
                x--;
            else if(matrix[x][y]<target)
                y++;
            else
                return true;
        }    
        return false;
    }  
}

同时上面的解法在《剑指offer》里面也有介绍,不过我觉得没有上面的二分的做法更巧妙

总结

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值