【算法】二分查找(下)

目录

一、山峰数组的峰顶索引

二、寻找峰值

三、搜索旋转排序数组中的最小值

四、0~n-1中缺失的数字


一、山峰数组的峰顶索引

题目链接:852. 山脉数组的峰顶索引 - 力扣(LeetCode)

题目描述:

给定一个长度为 n 的整数 山脉 数组 arr ,其中的值递增到一个 峰值元素 然后递减。

返回峰值元素的下标。

你必须设计并实现时间复杂度为 O(log(n)) 的解决方案。

示例 1:

输入:arr = [0,1,0]
输出:1

示例 2:

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

示例 3:

输入:arr = [0,10,5,2]
输出:1

提示:

  • 3 <= arr.length <= 10^5
  • 0 <= arr[i] <= 10^6
  • 题目数据 保证 arr 是一个山脉数组

题目分析:

我们只需要在一个 先升序后降序的数组中 找到转折点的下标即可

需要注意的是,在【提示】中,明确说明:该数组的长度是大于等于3的,并且保证了数组arr是一个山脉数组(满足先升序后降序--必有转折点)。所以当数组长度为3时,返回值为1.

所以峰值元素肯定不会出现在数组的两端,而是出现在数组中间。

即返回值区间为:[1,arr.length-2]

题目所满足的条件在后续解题中,有很大的作用

在本题中,数组具有【二段性】,即当数组被分为两个部分后,每个部分内部都有某种特定的性质。在本题中,前半部分满足升序,后半部分满足降序。我们在【手撕二分查找】中的二分查找本质中,说过:当数组具有有序性/二段性时,存在一个分界点时,使得分界点前后元素的性质不同,便可以使用二分查找来解决问题。

解题思路:

在循环中,我们只需要判断mid处元素是否大于mid-1处的元素,然后依此更新左右边界,直到找到峰值元素

1.确认区间形式:【左开右闭】

2.维护区间形式:初始值、循环条件、左右边界

初始值left=0,right=arr.length-2/arr.length-1(由于峰值元素不出现在数组两端,搜索区间为[1,arr.length-2])。注意:left一定不能为-1,因为当left=-1时,可能会导致mid-1<0越界

循环条件:left<right

左右边界:left=mid,right=mid-1

3.选择mid的计算方式:向上调整:mid=left+(right-left+1)/2

4.返回值:return left

如果我们用left=-1去提交,发现会报一个越界异常,如下:

arr=[3,5,3,2,0]

越界的原因:

  1. 初始时:left= -1,right=4。mid=2,arr[mid]=3<arr[mid-1]=5。令right=mid-1=1
  2. 此时:left=-1,right=1。mid=0。由于mid-1=-1<0,所以越界

为了避免越界,要保证mid-1>=0,即mid>=1。

初始化left=0则可以解决这一问题

解题代码:

class Solution {
    public static int peakIndexInMountainArray(int[] arr) {
        int left=0;
        int right=arr.length-2;//或者可以为arr.length-1
        while(left<right){
            int mid=left+(right-left+1)/2;
            if(arr[mid]>arr[mid-1])left=mid;
            else right=mid-1;
        }
        return left;
    }
}

二、寻找峰值

1.题目链接162. 寻找峰值 - 力扣(LeetCode)

2.题目描述:

峰值元素是指其值严格⼤于左右相邻值的元素。
给你⼀个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返
回 任何⼀个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log 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。
提⽰:
1 <= nums.length <= 1000
-2^31 <= nums[i] <= 2^31 - 1
对于所有有效的 i 都有 nums[i] != nums[i + 1]

题目分析:

首先解析为什么给出nums[-1]=nums[n]= -∞

这保证了数组一定有峰值。

比如数组是严格递增的,那么nums[n-1]就是峰值。为什么?因为此时nums[n-2]<nums[n-1]>nums[n]。同样地,假如数组是严格递减的,那么nums[0]就是峰值。因为此时nums[-1]<nums[0]>nums[1]

其实简单来说,峰值元素就是一个大于其两侧的元素而已。而nums[-1]和nums[n]为 -∞,则保证了下标为0或者n-1处的元素也可能是峰值元素。

定理:如果i<n-1且nums[i-1]<nums[i],那么在下标[i,n-1]中一定存在至少一个峰值

反证法:假设下标[i,n-1]中没有峰值

·由于i处不是峰值且nums[i-1]<nums[i],所以一定有nums[i]<nums[i+1]成立,否则i就是峰值了。

·由于i+1处不是峰值且nums[i]<nums[i+1],所以一定有nums[i+1]<nums[i+2]成立,否则i+1就是峰值了。

·依此类推,得 nums[i-1]<nums[i]<nums[i+1]<nums[i+2]<........nums[n-1]>nums[n]= -∞

这意味着nums[n-1]是峰值,假设不成立,所以定理成立。

同理可得,如果i<n-1且nums[i-1]>nums[i],那么在[0,i-1]中一定存在至少一个峰值

解题思路:

根据上述分析,我们已知数组中一定存在至少一个峰值。只需要通过比较nums[i-1]和nums[i]得大小关系,从而不断地缩小峰值所在位置的范围,二分找到峰值即可

细节:

  1. 当数组只有一个元素时,该元素即为峰值。应当返回下标0
  2. 当n-1处的元素为峰值元素时,每次更新的都是left,当left走到n-1处(right初始值)时,结束循环,返回n-1

我们这里选择【左开右闭】来解决本题

由于当数组只有一个元素时,应该返回下标0。所以这里初始化left=0

解题代码:

class Solution {
    public int findPeakElement(int[] nums) {
        int left=0;
        int right=nums.length-1;//左开右闭 (0,n-1]
        while(left<right){
            int mid=left+(right-left+1)/2;
            if(nums[mid]>nums[mid-1])left=mid;
            else right=mid-1;
        }
        return left;
    }
}

三、搜索旋转排序数组中的最小值

1.题目链接153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)

2.题目描述:

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例 2:

输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

示例 3:

输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。

提示:

  • n == nums.length
  • 1 <= n <= 5000
  • -5000 <= nums[i] <= 5000
  • nums 中的所有整数 互不相同
  • nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转

题目分析:

其实本题的数组可以看成由 两段升序数组(假设为数组M、N)拼接而成。

如果原数组由N+M拼接而成,那么M+N就是一个升序数组。而M、N分别又是升序的

我们用一张图表示,会更加清晰

其中C点就是我们所要求的点

二分的本质:找到一个判断标准,使得查找区间能够一分为二

通过图像我们发现,[A,B]区间内的点都是严格大于D点的值,C点的值是严格小于D点的值。但是当[C,D]区间只有一个元素的时候,C点的值可能等于D点的值

因此,初始化左右两个指针left,right:

根据mid的落点,我们可以这样划分下一次查询的区间:

·当mid在[A,B]区间的时候,也就是mid位置的值严格大于D点的值,下一次查询区间在[mid+1,right]

·当mid在[C,D]区间的时候,也就是mid位置的值严格小于等于D点的值,下一次查询区间在[left,mid]

当区间长度变成1的时候,就是我们要找的结果

注意:我们这里的D需要额外处理。我们将查询区间内的最后一个元素看作D。

解题代码:

class Solution {
        public static int findMin(int[] nums) {
        //左闭右开
        int left=0;
        int right=nums.length-1;
        int x=nums[right];//标记一下最后一个位置的值
        while(left<right){
            int mid=left+(right-left)/2 ;
            if(nums[mid]>x)left=mid+1;
            else right=mid;
        }
        return nums[left];
    }
}

四、0~n-1中缺失的数字

1.题目链接LCR 173. 点名 - 力扣(LeetCode)

2.题目描述:

某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组 records。假定仅有一位同学缺席,请返回他的学号。

示例 1:

输入:records = [0,1,2,3,5]
输出:4

示例 2:

输入:records = [0, 1, 2, 3, 4, 5, 6, 8]
输出:7

提示:

1 <= records.length <= 10000

简单来说:有一个长度为n的升序数组,数组内的元素应该为0~n-1。请找出缺少的那个数字

我们本题使用【左闭右开】来解题

这题其实更加简单了。我们只需要比较下标是否与元素相等即可。

如果相等,表示左侧没有缺少,右侧缺少,那么下一次查询区间为[mid+1,right]

如果不相等,表示左侧缺少,右侧不缺少,那么下一次查询区间为[left,mid]

左右指针相遇处的元素则表示 下标与元素不相等。返回此处下标--表示缺少元素

解题代码:

class Solution {
        public static int takeAttendance(int[] records) {
        int left=0;
        int right=records.length;
        //左闭右开
        while(left<right){
            int mid=left+(right-left)/2;
            if(records[mid]==mid)left=mid+1;
            else right=mid;
        }
        return left;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值