二分法专题

二分法思想

70%的题时根据单调性确定使用二分法,而95%的题根据两段性可以确定使用二分法求解·。
所谓两段性,就是一个区间根据某个性质可以二分为红色和绿色两段,而我们的目的就通过二分找到这个分界点t
在这里插入图片描述

如何确定红绿两段?
设定一个性质check(t),t自然满足性质,

  • 若大于t的数都满足性质,则红段为<t,绿段为>=t,待求点为绿段起点使用模板一;
  • 若小于t的数都满足性质,则红段为<=t,绿段为>t,待求点为红段末点使用模板二;

二分的流程

  1. 确定二分的初始边界;
  2. 编写二分的代码框架;
  3. 设计一个性质( check() )划分区间为红绿两段;
  4. 判断一下区间如何更新,如果更新方式为left = mid, right = mid - 1,则在计算mid时加上1向上取整;

二分的两个模板

在这里插入图片描述
在这里插入图片描述

解题技巧

1、设定性质要考虑清楚,一定要严格保证两段性,如x的平方根;
2、数组中寻找某个目标值时,能用模板1就不要用模板2;

x的平方根

题目链接:x的平方根

给你一个非负整数x,计算并返回 x 的平方根 。由于返回类型是整数,结果只保留整数部分,小数部分将被舍去。
注意:不允许使用任何内置指数函数和算符,如 pow(x, 0.5) 或者 x ** 0.5。

题目分析:

一个数的平方根必然在1和其自身之间,因此本题可以看做一个[0,x]的升序整形数组,当设定性质check(t) -> t^2 <= x时,此时区间可划分为[0,sqrt(t)]红色段和(sqrt(t),x]绿色段,使用二分法。

当设定为t^2 >= x时,
当x刚好是一个平方数时(如4),【0,x的平方根下取整-1】不满足条件,【x的平方根下取整,x】满足条件;
当x不是一个平方数时(如5),【0,x的平方根下取整】为不满足条件,【x的平方根下取整+1,x】为满足条件;
因此这个性质不好,不能将区间严格分为两段。

当设定为t^2 > x时,
【0,x的平方根下取整】一定不满足条件,【x的平方根下取整+1,x】一定满足条件
因此这个性质是可以用的。

代码实现

class Solution {
    public int mySqrt(int x) {
        //1确定初始边界
        long left = 0, right = x;
        //2编写二分框架
        while(left < right){
            long mid = left + right + 1 >> 1;
            //3确定性质划分区间为红绿两端 这里选t^2 <= x --> t <= x/t 
            if(mid <= x/mid){ //如果mid满足性质在红色段,分界点在mid右边且mid可能为分界点
                //4确定更新方式 如果为left=mid,right=mid-1;则计算mid时+1
                left = mid;
            }else{   //如果mid在绿色段,分界点在mid左边且mid不可能为分界点
                right = mid - 1;
            }
        }
        //返回left或right都一样,因为跳出while循环时,left=right,且如果target存在,则就是这个索引值
        return (int)right;
    }
}

搜索插入位置

题目链接:搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。

题目分析

解题思路:
题目中提到排序数组,且要求时间复杂度为 O(log n),就可以考虑二分法了
首先本题存在四种情况:
1.数组中不存在target,该元素小于数组第一个元素,即插入索引为0;
2.数组中不存在target,该元素大于数组最后一个元素,即插入索引为nums.length;
3.数组中不存在target,该元素在数组中某个元素之后;
4.数组中存在target,即找到该元素返回索引;
然后确定性质划分区间为两段, check(t)设为 t >= target
在这里插入图片描述
然后确定更新规则。

为什么是 t >= target 而不是 t <= target
举例说明,nums = {1,3,5,6}
如果是t <= target,设target=2,mid首先为1,nums[mid]>target,left=0,right=mid-1=0,直接退出while循环,输出0,错误;
如果是t >= target,left=0,right=mid=1,更新,mid=0,nums[mid]<target,left=1=right,退出while循环,输出1.
可以看出t <= target时,设定为right=mid-1,如果数组中不存在target,就会定位到刚好小于target的位置上去,而如果是t >= target,如果不存在target,就会定位到刚好大于target的位置上去,相比小于,我们更需要大于,因为小于还需要额外处理(考虑边界,考虑是否等于target等),大于可以直接作为返回值。

代码实现

class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        if(nums.length == 0 || nums[right] < target){
            return nums.length;
        }
        while(left < right){
            int mid = left + right >> 1;
            //确定性质划分区间为两段, 选t >= target;
            if(nums[mid] >= target){  //mid在绿段,分界点在mid左边且mid可能为分界点
                //确定更新方式,如果为left = mid; right = mid - 1;
                right = mid;
            }else{   //mid在红段,分界点在mid左边且mid不可能为分界点
                left = mid + 1;
            }

        } 
        //返回left或right都一样,因为跳出while循环时,left=right,且如果target存在,则就是这个索引值 
        return left;
    }
}

在排序数组中查找元素的第一个和最后一个位置**

题目链接: 在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]。
进阶:你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
在这里插入图片描述

题目分析

本题要求查找某一元素的第一个位置和最后一个位置,也就是将区间按两种方式二分,第一种check(t) >= target,二分找到第一个位置,第二种check(t)<= target ,二分找到最后一个位置,如下图:
在这里插入图片描述
因此需要根据两个性质执行两次二分查找。

代码实现

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        if(nums.length == 0){
            return new int[]{-1, -1};
        }
        //第一种二分法,找到元素的第一个位置
        while(left < right){
            int mid = left + right >> 1;
            //确定check划分区间 选用t >= target
            if(nums[mid] >= target){  //mid在绿段
                //确定区间更新方式  如果为left = mid; right = mid -1;计算mid时+1;
                right = mid;
            }else{    //mid在红段
                left = mid + 1;
            }
        }
        //如果二分出来的边界值不等于target,说明target不存在
        if(nums[left] != target){
            return new int[]{-1, -1};
        }
        int start = right;

        //第二种二分法,找到元素的最后一个位置
        left = 0;
        right = nums.length - 1;
        while(left < right){
            int mid = left + right + 1 >> 1;
            //确定check划分区间 选用t <= target
            if(nums[mid] <= target){  
                //确定区间更新方式 如果为left = mid; right = mid + 1;计算mid时+1
                left = mid;
            }else{
                right = mid - 1;
            }
        }
        int end = left;
        return new int[]{start, end};
    }
}

搜索二维矩阵

题目链接
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。
示例 1:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true

在这里插入图片描述

题目分析

给定数组中具有两个特性:每行中的整数从左到右按升序排列,并且每行的第一个整数大于前一行的最后一个整数,实际上就是一个区间,因此可以设定check(t)>=target将该数组进行二分,查找到target。
在这里插入图片描述
另外,本题中存在一个难点,就是如何简单有效地根据mid确定元素的索引,使用如下
matrix[mid/column][mid%column]计算。例如元素6,排次顺序为5(排次从0开始),5/3=1,5%3=2`,即第2行3列。

代码实现

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if(matrix.length == 0 || matrix[0].length == 0){
            return false;
        }

        //martix.length就是二维数组的行数
        //martix[0].length就是二维数组的列数。
        int row = matrix.length, column = matrix[0].length;
        int left = 0, right = row * column - 1;
        while(left < right){
            int mid = left + right >> 1;
            //设定check(t)>=target二分数组
            //martix[mid/column][mid%column]确定mid对应的数组元素
            if(matrix[mid/column][mid%column] >= target){//mid在绿段
            //确定区间更新规则,如果是left=mid;right=mid-1;则计算mid时+1
                right = mid;
            }else{
                left = mid + 1;
            }
        }
        //确定二分找到的元素是否等于target
        if(matrix[right/column][right%column] != target){
            return false;
        }
        return true;
    }
}

//法二
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if(matrix.length == 0 || matrix[0].length == 0){
            return false;
        }

        //martix.length就是二维数组的行数
        //martix[0].length就是二维数组的列数。
        int row = matrix.length, column = matrix[0].length;
        int left = 0, right = row * column - 1;
        while(left < right){
            int mid = left + right + 1 >> 1;
            //设定check(t)<=target二分数组
            //martix[mid/column][mid%column]确定mid对应的数组元素
            if(matrix[mid/column][mid%column] <= target){//mid在绿段
            //确定区间更新规则,如果是left=mid;right=mid-1;则计算mid时+1
                left = mid;
            }else{
                right = mid - 1;
            }
        }
        //确定二分找到的元素是否等于target
        if(matrix[right/column][right%column] != target){
            return false;
        }
        return true;
    }
}

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

题目链接
已知一个长度为 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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

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

题目分析

在这里插入图片描述
一个升序排序的数组,经过旋转,将后面一部分较大的元素换到前面(第二象限 ),前面一部分较小的元素换到后面(第四象限),如图中所示,可以设定一个性质check(t) —> nums[t]<=nums.back(),即上图中第四象限的元素都满足这个条件,而第二象限的元素都不满足,由此可以将数组划分为红绿两段,所二分的得到元素就是数组最小值。
进一步判断,当nums[mid]不满足check(t时),mid在红段,此时只更新left,right不会改变;当nums[mid]满足check(t时),mid必然在绿段;因此区间更新后,right只会不断靠近最小值因此check(t)可以改为nums[t]<=nums[right]

相似题目:剑指 Offer 11. 旋转数组的最小数字 注意这道题和本题的差异,数组元素可能是重复的。

代码实现

class Solution {
    public int findMin(int[] nums) {
        int left = 0, right = nums.length - 1;
        while(left < right){
            int mid = left + right >> 1;
            //设定check(t) -> nums[t] <= nums[nums.length - 1]
            if(nums[mid] <= nums[nums.length - 1]){  //绿段
            //确定更新规则,如果是left=mid;right=mid-1;计算mid时就+1
                right = mid;
            }else{
                left = mid + 1;
            }
        }
        return nums[left];
    }
}
//进一步判断
class Solution {
    public int findMin(int[] nums) {
        int left = 0, right = nums.length - 1;
        while(left < right){
            int mid = left + right >> 1;
            //设定check(t) -> nums[t] <= nums[right]
            if(nums[mid] <= nums[right]){  //绿段
            //确定更新规则,如果是left=mid;right=mid-1;计算mid时就+1
                right = mid;
            }else{
                left = mid + 1;
            }
        }
        return nums[left];
    }
}

搜索旋转排序数组***

题目链接
整数数组 nums 按升序排列,数组中的值互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你旋转后的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

在这里插入图片描述

题目分析

第一步,使用寻找旋转排序数组中的最小值的方法确定最小值的下标;
第二步,判断target<=nums[nums.length - 1]是否成立以确定二分区间:

  • 如果成立则说明target在第四象限,对在第四象限的区间进行二分,由于此时right=left=最小值下标,因此left不变,right更新为nums.length - 1,即再第四象限的区间为[最小值下标,nums.length - 1];
  • 如果不成立则说明target在第二象限,对在第二象限的区间进行二分,由于此时right=left=最小值下标,因此更新为left=0,right-1,即在第二象限的区间为[0,最小值下标 - 1];

第三步,二分所确定的区间,得到结果判断num[left]==target,满足则返回left,否则返回-1;

注意:第三步中判断结果是必须是left,因为极端情况下,整个输入数组单调时,第一步执行结果得到最小值下标为0,第二步执行时如果target不满足target <= nums[nums.length - 1]right--就会使得right=-1,执行第三步时不满足left < right条件,直接判断是否等于target,此时如果是right就会超出数组边界。

记录一个小坑:

//java中不能如下:
if(target <= nums[nums.length - 1])  right = nums.length - 1;
else left = 0, right--;
//只能条件判断后跟单条语句,否则如下写表示无论条件是否成立,right--都会执行
if(target <= nums[nums.length - 1])  right = nums.length - 1;
else left = 0; right--;

代码实现

class Solution {
    public int search(int[] nums, int target) {
        if(nums.length == 0){
            return -1;
        } 
        //找到最小值
        int left = 0, right = nums.length - 1;
        while(left < right){
            int mid = left + right >> 1;
            if(nums[mid] <= nums[nums.length - 1]){
                right = mid;
            }else{
                left = mid + 1;
            }
        }

        //确定二分区间
        if(target <= nums[nums.length - 1]){
            right = nums.length - 1;
        } else{
            left = 0;
            right--;
        }
        
        //二分查找目标值
        while(left < right){
            int mid = left + right + 1 >> 1;
            //设定check(t) -> nums[t] <= target
            if(nums[mid] <= target){
                //区间更新准则,如果left=mid; right=mid-1;计算mid时+1
                left = mid;
            }else{
                right = mid - 1;
            }
        }
		//判断target是否存在
		//注意此处必须是left,因为极端情况下,right=-1,超出数组边界
        if(nums[left] == target){
            return left;
        } 
        return -1;
    }
}

第一个错误的版本

题目链接
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数.

输入:n = 5, bad = 4
输出:4
解释:
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。

在这里插入图片描述

题目分析

在这里插入图片描述
如上图,使用二分法划分区间找到第一个错误的版本即可
注意,由于本题1 <= bad <= n <= 2^31 - 1 计算mid = left + right >> 1可能会溢出变为负数,因此转换一下计算公式为mid = left + (right - left >> 1) ,算术运算符优先级高于左移右移运算符

代码实现

/* The isBadVersion API is defined in the parent class VersionControl.
      boolean isBadVersion(int version); */

public class Solution extends VersionControl {
    public int firstBadVersion(int n) {
        int left = 1, right = n;
        while(left < right){
            int mid = left + (right - left >> 1);
            //check(t) -> isBadVersion(t)返回true则在绿段,返回false则在红段
            if(isBadVersion(mid)){
                right = mid;
            }else{
                left = mid + 1;
            }
        }
        return left;
    }
}

寻找峰值

题目链接
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log n) 的算法来解决此问题

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

提示:
在这里插入图片描述

题目分析

本题属于5%的情况,即不能利用两段性将区间分为红绿两段然后套模板找到目标点。
因为题目中指出:数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
因此要利用这个特点解题。
在这里插入图片描述
首先二分区间得到一个中点m,判断nums[m]与nums[m+1]的关系:
题目指出nums[-1] = nums[n] = -∞,即nums[m+1]>nums[m]或nums[m+1]<nums[m]始终成立的情况下也会存在峰值,索引为0或n-1,因此:

  • 如果nums[m+1]>nums[m],说明[m+1,n-1]区间一定存在一个峰值,因为如果在该区间出现拐点就得到峰值,否则如果一直到边界点n-1都一直是单调上升的话,则边界点n-1就是峰值;
  • 如果nums[m+1]<nums[m],说明[0,m]区间一定存在一个峰值,因为如果在该区间出现拐点就得到峰值,否则如果一直到边界点0都一直是单调下降的话,则边界点0就是峰值

如此就可以断定,左右两边一定有一边存在峰值,这样就可以缩小区间范围到一半;
继续二分区间,一直到left=right的时候就可以得到答案了(left=right说明峰值在区间内部, left=right=边界点说明一直单调,峰值为边界)

问题1:为什么nums[m+1]>nums[m]时,取[m+1,n-1]而不是[m,n-1],而nums[m+1]<nums[m]时,取[0,m]而不是[0,m-1]?
因为当nums[m+1]>nums[m]时,根据峰值的定义,m必然不是峰值点,而m+1可能是峰值点,只要m+1的下一个点比m+1小,m+1就是峰值,因此区间取[m+1,n-1]; 同理,当nums[m+1]<nums[m]时,根据峰值的定义,m+1必然不是峰值点,而m可能是峰值点,只要m的上一个点比m小,m就是峰值,因此区间取[0,m]。

问题2,判断划分区间时存在nums[m+1] ,当mid=n-1时,nums[m+1]不就超出数组边界了吗?
mid=left + right >> 1;这说明mid要等于n-1,必然有right=left,但此时必然退出while循环,因此不会出现超出数组边界的问题

代码实现

class Solution {
    public int findPeakElement(int[] nums) {
        int left = 0, right = nums.length - 1;
        while(left < right){
            int mid = left + right >> 1;
            //当nums[mid] < nums[mid + 1]时,说明[mid+1,n-1]存在峰值
            if(nums[mid] < nums[mid + 1]){
                left = mid + 1;
            }else{
            //当nums[mid] > nums[mid + 1]时,说明[0,mid]存在峰值
                right = mid;
            }
        }
        return right;
    }
}

寻找重复数

题目链接
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,找出 这个重复的数 。

你设计的解决方案必须不修改数组 nums 且只用常量级 O(1) 的额外空间。

输入:nums = [1,3,4,2,2]
输出:2

提示:
1 <= n <= 10^5
nums.length == n + 1
1 <= nums[i] <= n
nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

题目分析

本题属于5%的情况,即不能利用两段性将区间分为红绿两段然后套模板找到目标点。
抽屉原理:
桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是我们所说的“抽屉原理”。 抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。”
本题利用抽屉原理可解出,分析如下:
在这里插入图片描述

总共有n个抽屉,n+1个苹果,
首先设定一个区间,左边界为L,右边界为R,设定一个中点M,左区间[L,M]共有M-L+1个抽屉,右区间[M+1,R]共有R-M个抽屉;

然后遍历数组中的元素,设定小于等于M的苹果元素全部在左区间的抽屉中,大于M的苹果元素在右区间的抽屉中;

不可能同时出现左边的苹果个数<=抽屉个数和右边的苹果个数<=抽屉个数,否则总苹果个数小于n+1个,因此至少有一边的苹果个数>抽屉个数;因此:

  • 如果左边苹果个数>抽屉个数,说明左边有一个抽屉放了两个苹果,即所求的重复数,此时我们可以确定,重复数所在区间为[L,M];
  • 如果右边苹果个数>抽屉个数,说明右边有一个抽屉放了两个苹果,即所求的重复数,此时我们可以确定,重复数所在区间为[M+1,R];

以此不断二分区间,直到找到重复数。

本题主要思想在于,n个抽屉,分为两边,设定小于等于中点M的在左边,大于M的在右边,不修改数组,只是遍历数组元素,根据数组元素与M的大小判断元素在哪边抽屉,最终得到的结果,必然会有一边元素个数大于抽屉个数,从而更新区间(二分)以确定新的M的值;然后再一次遍历数组元素,再一次根据数组元素与新M的大小判断元素在哪边抽屉,然后再次更新区间…不断迭代,直到left=right=M=重复数,退出循环得到结果。

复杂度分析
时间复杂度:O(nlogn),其中 n 为nums 数组的长度。最多需要二分O(logn) 次,又每次判断的时候需要遍历nums 数组求解小于等于 mid 的数的个数,复杂度为O(n) ,因此总时间复杂度为 O(nlogn)。
空间复杂度:O(1),只需要常数空间存放若干变量。

代码实现

class Solution {
    public int findDuplicate(int[] nums) {
    	//确定初始区间为[1,n]
        int left = 1, right = nums.length - 1;
        while(left < right){
            int mid = left + right >> 1;
            int cnt = 0;
            //遍历数组,计算左半边区间的元素个数
            //小于等于mid的在左区间,大于mid的在右区间
            for(int x : nums){
                if(x >= left && x <= mid){
                    cnt++;
                }
            }
            // 更新区间以确定新的M的值
            // 如果左半边区间的元素个数大于抽屉个数mid-left+1,
            // 说明重复的数在左半边区间, 
            if(cnt > mid - left + 1){
                right = mid;
            }else{
                //否则重复的数在右半边区间
                left = mid + 1;
            }
        }
        return right;
    }
}

H 指数 II *****

题目链接
给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数,citations 已经按照 升序排列 。计算并返回该研究者的 h 指数。

h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (n 篇论文中)总共有 h 篇论文分别被引用了至少 h 次。且其余的 n - h 篇论文每篇被引用次数 不超过 h 次。

提示:如果 h 有多种可能的值,h 指数 是其中最大的那个。

请你设计并实现对数时间复杂度的算法解决此问题

输入:citations = [0,1,3,5,6]
输出:3
解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6 次。
由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3 。

n == citations.length
1 <= n <= 10^5
0 <= citations[i] <= 1000
citations 按 升序排列

题目分析

首先明确h的定义,h表示至少存在h个数满足>=h,并且明确h不一定为数组元素;
然后确定h的范围[0,n];

  • h必然小于等于总数n,因为不可能存在n+1个数大于等于n+1,而可能存在n个数大于等于n,比如[6,4,5],h=3,n=3;
  • h必然大于等于0,h=0表示没有论文被引用,也就是总共有0篇论文分别被引用了至少 0 次,比如[0,0,0]中满足至少存在0个数>=0,因此h=0,;

然后设定一个性质划分区间为红绿两段:是否存在h个数满足h的定义条件;因为如果t满足h的定义条件,则在这t个数中挑t-1个数也必然满足h的定义条件,也就是t-1个数必然满足>=t-1(因为至少有t个数满足>=t),因此可以划分区间,满足性质的为红段,不满足性质的为绿段

  • 由于数组是升序排列的,因此满足h指数定义的数组元素必然是数组末尾几个元素;

  • 设定性质check(t) —> citations[ citations.length - t ] >= t ;,表示若 t 为所求的最大的h指数,那么 从数组最后一个元素开始往前数 t 个数(从1开始) 必然是使得t满足h指数条件的数组元素 ;

    • 例如[0,1,3,5,6]中假设t=2,n=5,使得t满足h指数条件(至少存在2个数组元素>=2)的数组元素必然有,倒数第一个元素6,倒数第二个元素5
  • 确定了h指数条件与数组元素的关系后,可以判定,小于t的数也都满足该性质,即[0,t]的元素都是h指数,由此得到红段;

    • 例如[0,1,3,5,6]中假设t=3,n=5;check(t-1=2)->从数组最后一个元素开始往前数 2个数(6,5)都满足>=t-1;check(t-2=1)->从数组最后一个元素开始数 1个数(6)都满足>=t-2;check(t-3=0)->从数组最后一个元素开始往前数 0个数都满足>=t-3;

因此红段为[0,t],绿段为[t+1,n],使用模板2

注意二分初始区间,即t的初始取值范围为[0,citations.length]

本题的主要思想在于,将数组元素与h值关联起来,满足h指数定义的必然满足从数组末尾到第h个数>=h,通过这个性质可以将h的取值区间划分红绿两段,满足h定义的在红段,不满足h定义的在绿段,以此不断二分,最终确定h,重点在于想清楚h指数的定义(至少存在h个数组元素>=h)。
在这里插入图片描述

相似题:274 H指数

代码实现

class Solution {
    public int hIndex(int[] citations) {
    	//注意初始区间,即t的初始取值范围为[0,citations.length]
        int left = 0, right = citations.length;
        while(left < right){
            int mid = left + right + 1 >> 1;
            //设定性质,如果t为所求的最大h指数(至少存在t个数组元素>=t)
            //因为数组元素升序排列,所以数组最后一个元素往前数t个数组元素(从1开始)就是使t满足h指数条件的元素
            //由此可推出t-1,t-2,...,0,都为h指数(即[0,t]),不在这个范围的就不满足,从而分为两段
            //倒数第mid个数(从1开始)就是正数第n-mid+1个数,对应数组下标就是第n-mid个数(从0开始)
            if(citations[citations.length - mid] >= mid){ //在红段
            //确定区间更新规则,如果是left=mid;right-1;则计算mid时+1
                left = mid;
            }else{//在绿段,即不满足h指数条件
                right = mid - 1;
            }
        }
        return left;
    }
}

练习

有效的完全平方数

题目链接
给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。 进阶:不要 使用任何内置的库函数,如 sqrt 。
在这里插入图片描述

class Solution {
    public boolean isPerfectSquare(int num) {
        if(num < 2){
            return true;
        }
        //确定初始区间
        int left = 2, right = num;
        //编写二分框架
        while(left < right){
            int mid = left + right + 1 >> 1;
            //设定性质 t^2 <= num
            if(mid <= num/mid){
                //确定区间更新规则,如果是left=mid;right=mid-1;则计算mid时+1
                left = mid;
            }else{
                right = mid - 1;
            }
        }
        if(right*right == num){
            return true;
        }
        return false;
    }
}

相关题目

  1. 寻找两个正序数组的中位数
  2. 两数相除
  3. 搜索旋转排序数组 II
  4. 寻找旋转排序数组中的最小值 II
  5. 存在重复元素 III
  6. H 指数
  7. 第一个错误的版本
  8. 俄罗斯套娃信封问题
  9. 矩形区域不超过 K 的最大数值和
  10. 猜数字大小
  11. 水位上升的泳池中游泳
  12. 山脉数组的峰顶索引
  13. 基于时间的键值存储
  14. 最大连续1的个数 III
  15. 在 D 天内送达包裹的能力
  16. 尽可能使字符串相等
  17. 绝对差不超过限制的最长连续子数组
  18. 制作 m 束花所需的最少天数
  19. 与数组中元素的最大异或值
  20. 最多可以参加的会议数目 II
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值