leetcode_162. 寻找峰值_852. 山脉数组的峰顶索引_153. 寻找旋转排序数组中的最小值_33. 搜索旋转排序数组

总结:

  • 162 看似简答,却很不容易想到:

    • 即使开始 l = 0, r = nums.length - 1; while里还是l < r;
      • 因为不是要全部查看所有元素,当nums[l] == nums[r]时,就是答案;
    • 比较判断是 nums[mid] 和 nums[mid+1], 目的是为了判断此时mid处于局部递减区域还是局部递增区域;
      • 若nums[mid] < nums[mid+1] , 处于局部递增,峰值在后面
      • 若nums[mid] > nums[mid+1] , 处于局部递减,峰值在前面
      • 即峰值在nums[l]或nums[l]的右边,在nums[r]或nums[r]的左边;
      • 当 l == r 时,就是峰值,夹击思想,这方面我不行;
    • 边界问题:nums[mid]与nums[mid-1]比较,注意会有越界问题;
  • 比较162和153题,弄懂边界

    • 162题:
    • 当用nums[mid]和nums[mid+1]比较时,int mid = left + (right - left)/2;
    • mid在数组只有两个元素时,指向前一个,所以[mid+1]不会越界;
    • 当用nums[mid]和nums[mid-1]比较时,int mid = left + (right - left + 1)/2;
    • mid在数组只有两个元素时,指向后一个,所以[mid-1]不会越界;
    • 153题:
    • 因为题目既需要判断nums[mid]和nums[mid+1],也需要判断nums[mid]和nums[mid-1],所以int mid = left + (right - left)/2; / left + (right - left + 1)/2; 当数组长度为2时,用哪个都不行;
    • 这里的解决办法是用int mid = left + (right - left)/2; 先判断if(nums[mid] > nums[mid+1]) return nums[mid+1]; 满足直接返回,不会去判断nums[mid]和nums[mid-1],所以不会越界。
    • 那如果不满足呢?即数组长度为2,且是升序;解决办法:在代码之前加上一个升序判断if(nums[nums.length - 1] > nums[0]) return nums[0];
  • 再说一句,int mid = left + (right - left)/2;和int mid = left + (right - left + 1)/2;的区别:

    • 前一个mid,数组长度为双数时,有时指向前一个中位,有时指向后一个中位;为2时,指向前一个中位;
    • 后一个mid, 数组长度为双数时,一直指向后一个中位;

162. 寻找峰值 中等

峰值元素是指其值大于左右相邻值的元素。

给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。

数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞。

示例 1:

输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。
示例 2:

输入: nums = [1,2,1,3,5,6,4]
输出: 1 或 5
解释: 你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。

  • 线性
    • 前提:题目说明 nums[-1] = nums[n] = -∞。
    • 所以一直是升序的序列,最后一个元素可以是峰值。
    • 一直是降序的序列,最开始的元素可以是峰值。
  • 三种情况:
    • 单调递增,nums[i]一直小于nums[i+1], 循环结束,返回最后一个index;
    • 单调递减,nums[i]一直大于nums[i+1],第一个就满足条件,返回第一个index;
    • 先升后降,碰到nums[i] > nums[i+1],就返回index=i
    • 先降后升 , 第一个就满足,返回第一个元素的index

示例 1:

class Solution {
    public int findPeakElement(int[] nums) {

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

        return nums.length - 1;
    }
}
  • 二分法,比较nums[mid] <>? nums[mid+1]

    • 单调递增,则峰值在后面
    • 单调递减,峰值在前面
    • 当l == r时,返回
  • 为什么当l == r时,返回,这一个就是答案峰值?

    • 单调递增,则 l 一直向右移动,找比自己更大的值,直到,l == r;
    • 单调递减,则 r 一直向左移动,找比自己更大的值,直到,r == l;
    • 又因为题目特殊,左右边界是峰值,所有成立;
    • 不是单调递增,或单调递减;题目有说没有重复元素,则肯定存在峰值;
      • 当nums[mid] < nums[mid+1],l 右移到 mid + 1位置;nums[l] 左边元素小于nums[l];
      • 当nums[mid] > nums[mid+1],r 左移到 mid 位置;nums[r] 右边元素小于nums[r] ;
      • 当 l == r时,两边元素均小于nums[l] == nums[r] ,即为峰值
  • 为什么要比较nums[mid]和nums[mid+1], 比较nums[mid]和nums[md-1]不行吗?

    • 都可以,稍微改一次逻辑就可以;
  • 二分法边界问题:

    • 最普通的二分法查找指定元素的边界问题:
    • nums[mid]和指定值比较
      • nums.length 对应 l < r; r = mid +1; l = mid;
      • nums.length - 1 对应 l <= r; r = mid+1; l = mid - 1;
    • 边界问题还要根据具体题目变通,比如这道题的边界:
    • nums[mid] 和nums[mid+1]比较,判断此时的nums[mid]处于局部递增还是局部递减;
      • nums.length - 1 对应 l < r; r = mid;l = mid + 1;
      • 为什么是l < r, 不应该时l <= r吗?
      • 当l < r时推出,此时nums[l]没有进入比较,直接返回,因为就是答案。
      • 普通查找,如果l < r,就会漏掉一个l == r;[]双闭l==r区间是有值的;
      • r = mid;l = mid + 1;就是将l和r位置移动较大的那个元素的位置上;
    • nums[mid] 和nums[mid+1]比较不会越界,因为mid = left + (right-left)/2是向下取整,不会溢出;而如果nums[mid] 和nums[mid-1]会越界:
      • eg: [0,1]中,mid = left + (right-left)/2 = 0 + (1-0)/2 = 0, mid-1=0-1,越界;
      • 因此要加上 mid = left + (right-left + 1)/2; 加上1;
class Solution {
    public int findPeakElement(int[] nums) {

        int left = 0, right = nums.length - 1;
        while(left < right) {
            int mid = left + (right - left) / 2;
            if(nums[mid] < nums[mid + 1]) {
                left = mid + 1;
            }else {
                right = mid;
            }
        }

        return left;

    }
}
public class Solution {

    public int findPeakElement(int[] nums) {
        int len = nums.length;
        int left = 0;
        int right = len - 1;
        // [left, right]
        while (left < right) {
            int mid = left + (right - left + 1) / 2;
            if (nums[mid - 1] < nums[mid]) {
                // 下一轮搜索的区间 [mid, right]
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        // left == right
        return left;
    }
}

852. 山脉数组的峰顶索引 简单

我们把符合下列属性的数组 A 称作山脉:

A.length >= 3
存在 0 < i < A.length - 1 使得A[0] < A[1] < … A[i-1] < A[i] > A[i+1] > … > A[A.length - 1]
给定一个确定为山脉的数组,返回任何满足 A[0] < A[1] < … A[i-1] < A[i] > A[i+1] > … > A[A.length - 1] 的 i 的值。

示例 1:

输入:[0,1,0]
输出:1
示例 2:

输入:[0,2,1,0]
输出:1

  • 可以套用前一个题的解,前提是答案一定在中间,不可能在数组两端;
  • 上一个题解前提是数组两端也是峰值;看 没有这个前提就是错的;
class Solution {
    public int peakIndexInMountainArray(int[] A) {
        
        int l = 0, r = A.length - 1;

        while(l < r) {
            int mid = l + (r-l) / 2;
            if(A[mid] < A[mid+1]) {
                l = mid + 1;
            }else {
                r = mid;
            }
        } 

        return l;
    }
}

153. 寻找旋转排序数组中的最小值

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

请找出其中最小的元素。

你可以假设数组中不存在重复元素。

示例 1:

输入: [3,4,5,1,2]
输出: 1
示例 2:

输入: [4,5,6,7,0,1,2]
输出: 0
在这里插入图片描述

    // 有序数组从现在的nums[0]开始旋转,
    // 分为两个部分:一部分比nums[0]大,一部分比nums[0]小
    // 最小值就是这两部分的拐点,二分法
    // 当nums[mid] > nums[0], 最小值在nums[mid]后面
    // 当nums[mid] < nums[0], 最小值是nums[mid]或者在nums[mid]前面
    // 因为最小值就是拐点,其他地方都是升序,只有拐点是局部降序,前面一个元素大于后面一个元素。
    // 所以当nums[mid-1] > nums[mid] 或者 nums[mid] > nums[mid+1]时,nums[mid]/nums[mid+1]
    // 是最小值
class Solution {
    public int findMin(int[] nums) {

        if(nums.length == 1) return nums[0];

        if(nums[nums.length - 1] > nums[0]) return nums[0];

        int left = 0, right = nums.length - 1;
        while(left <= right) {
            int mid = left + (right - left) / 2;
            
            if(nums[mid] > nums[mid+1]) return nums[mid+1];

            if(nums[mid - 1] > nums[mid]) return nums[mid];

            if(nums[mid] > nums[0]) {
                left = mid + 1;
            }else if(nums[mid] < nums[0]) {
                right = mid - 1;
            }
        }

        return -1;

    }
}

出错:

原因:
int mid = left + (right - left) / 2;
if(nums[mid-1] > nums[mid]) return nums[mid]; // 这里出错
if(nums[mid] > nums[mid+1]) return nums[mid+1];

解决:
调换位置,如果是两个元素的降序,直接直接就返回了,不会去判断nums[mid-1]
if(nums[mid] > nums[mid+1]) return nums[mid+1];

if(nums[mid-1] > nums[mid]) return nums[mid];
在这里插入图片描述

原因:
int mid = left + (right - left) / 2;
if(nums[mid] > nums[mid+1]) return nums[mid+1];
if(nums[mid-1] > nums[mid]) return nums[mid]; // 执行到这里出错

解决办法:
添加
if(nums[nums.length - 1] > nums[0]) return nums[0];
在这里插入图片描述

33. 搜索旋转排序数组

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。

你可以假设数组中不存在重复的元素。

你的算法时间复杂度必须是 O(log n) 级别。

示例 1:

输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
示例 2:

输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1

出错代码:

  • 原因:寻找山峰的前提条件是
class Solution {
    public int search(int[] nums, int target) {
        // 查找指定元素
        // 数组: 先单调递增,突然下降,再单调递增
        
        // 先二分找到这个拐点,前面数组的元素全部大于后面的数组,
        // 先在前面的数组中二分找,找到返回;若没找到,到达right,返回-1;到达left, 在后面一组中找

        // 怎么找拐点,就是找峰值
        if(nums == null || nums.length == 0) return -1;

        int guaidian = fengzhi(nums);

        int res; 
        res = getNum(nums, 0, guaidian, target);
        
        if(res != -1) {
            return res;
        }else {
            res = getNum(nums, guaidian + 1, nums.length - 1, target);
        }

        return res;

    }


    private int fengzhi(int[] nums) {
        int l = 0, r = nums.length - 1;
        while(l < r) {
            int mid = l + (r-l)/2;
            if(nums[mid] < nums[mid+1]) {
                l = mid +1;
            }else if(nums[mid] > nums[mid+1]){
                r = mid;
            }
        }

        return l;
    }

    private int getNum(int[] nums, int l, int r, int target) {
        while(l <= r) {
            int mid = l + (r-l)/2;

            if(nums[mid] < target) {
                l = mid + 1;
            }else if(nums[mid] > target) {
                r = mid - 1;
            }else {
                return mid;
            }
        }

        return -1;
    }
}
  • 错误原因
    • 我用寻找峰值的思路去找到拐点,寻找山峰的前提是峰值一定不在数组收尾,所以代码一定不会返回收尾元素;数组寻找峰值的前提是数组的收尾元素是峰值,返回收尾元素可以;单调递增会返回尾元素,单调递减会返回首元素,只要首元素或尾元素大于相邻元素,收尾元素也是峰值;
    • 但这里不行,如果用那套代码,有可能返回5,也有可能返回3,都是正确答案,但3不符合这题的意思。
    • 所以报错,正解,寻找最小元素;
      在这里插入图片描述

正解,寻找最小值

  • 找到拐点最小值,将数组分为两个有序部分
  • 分别用二分法去寻找
class Solution {
    public int search(int[] nums, int target) {
        // 找到拐点最小值,将数组分为两个有序部分
        // 分别用二分法去寻找
        if(nums == null || nums.length == 0) return -1;

        if(nums[nums.length - 1] > nums[0]) return getNum(nums, 0, nums.length - 1, target);

        int minNumIndex = min(nums);

        if(target == nums[0]) {
            return 0;
        }else if(target > nums[0]){
            return getNum(nums, 0, minNumIndex - 1, target);
        }else {
            return getNum(nums, minNumIndex, nums.length -1, target);
        }
        
    }

    private int min(int[] nums) {
        // 特殊情况[1,2] [2,1]
        // 通过nums[mid]和nums[0]比较,看现在nums[mid]处在哪里,再往哪移动
        // 如果是拐点,立即返回

        if(nums.length == 1) return 0;

        if(nums[nums.length - 1] > nums[0]) return 0;

        int left = 0, right = nums.length - 1;
        while(left <= right) {
            
            int mid = left + (right - left) / 2;
            
            if(nums[mid] > nums[mid+1]) return mid+1;
            if(nums[mid-1] > nums[mid]) return mid;

            if(nums[mid] > nums[0]) {
                left = mid + 1;
            }else if(nums[mid] < nums[0]) {
                right = mid - 1;
            }
        }

        return -1;
    }

    private int getNum(int[] nums, int left, int right,  int target) {
        while(left <= right) {
            int mid = left + (right - left) / 2;
            if(nums[mid] < target) {
                left = mid + 1;
            }else if(nums[mid] > target) {
                right = mid - 1;
            }else {
                return mid;
            }
        }

        return -1;
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值