剑指Offer(专项突破版)刷题笔记 | 第十一章 二分查找

207 阅读5分钟

二分查找的基础知识

在递增排序数组中进行二分查找代码

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

在排序数组中二分查找

关键词:排序数组、查找

Q68:查找插入位置

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

输入: nums = [1,3,5,6], target = 5
输出: 2

示例 2:

输入: nums = [1,3,5,6], target = 2
输出: 1

示例 3:

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

解题思路

注意两种特殊情况

  • target 比所有数字小,返回0
  • target 比所有数字大,返回nums.length
public int searchInsert(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while(left <= right){
        int mid = (left + right) / 2;

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

Q69:山峰数组的顶部

题目(简单):符合下列属性的数组 arr 称为 山峰数组(山脉数组) :

  • arr.length >= 3
  • 存在 i(0 < i < arr.length - 1)使得:
    • arr[0] < arr[1] < ... arr[i-1] < arr[i]
    • arr[i] > arr[i+1] > ... > arr[arr.length - 1] 给定由整数组成的山峰数组 arr ,返回任何满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1] 的下标 i ,即山峰顶部。

示例 1:

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

示例 2:

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

示例 3:

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

示例 4:

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

示例 5:

输入:arr = [24,69,100,99,79,78,67,36,26,19]
输出:2
public int peakIndexInMountainArray(int[] arr) {
//注意左右初始条件
    int left = 1;
    int right = arr.length - 2;

    while(left <= right){
        int mid = (left + right) / 2;
        if(arr[mid] > arr[mid + 1] && arr[mid] > arr[mid - 1]) return mid;
        if(arr[mid] > arr[mid - 1]){
            left = mid + 1;
        }else{
            right = mid - 1;
        }
    }
    return -1;
}

Q70:排序数组中只出现一次的数字

题目(中等):给定一个只包含整数的有序数组 nums ,每个元素都会出现两次,唯有一个数只会出现一次,请找出这个唯一的数字。

示例 1:

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

示例 2:

输入: nums =  [3,3,7,7,10,11,11]
输出: 10

解题思路

  • 先将数组每两个一组
  • 判断第mid组的两个元素是否相等,若不相等
    • mid==0,那么就是第一组不相等的
    • 前一组两元素相等,则当前就是第一组不相等的
  • 否则,第一组不相等的就在前半段
public int singleNonDuplicate(int[] nums) {
    int left = 0;
    int right = nums.length / 2;
    while(left <= right){
        int mid = (left + right) / 2;
        int i = 2 * mid;
        if(i < nums.length - 1 && nums[i] != nums[i + 1]){
            if(mid == 0 || nums[i-2] == nums[i-1]){
                return nums[i];
            }
            right = mid - 1;
        }else{
            left = mid + 1;
        }
    }
    return nums[nums.length - 1];
}

Q71:按权重生成随机数

题目(中等):给定一个正整数数组 w ,其中 w[i] 代表下标 i 的权重(下标从 0 开始),请写一个函数 pickIndex ,它可以随机地获取下标 i,选取下标 i 的概率与 w[i] 成正比。

例如,对于 w = [1, 3],挑选下标 0 的概率为 1 / (1 + 3) = 0.25 (即,25%),而选取下标 1 的概率为 3 / (1 + 3) = 0.75(即,75%)。

也就是说,选取下标 i 的概率为 w[i] / sum(w) 。

示例 1:

输入:
inputs = ["Solution","pickIndex"]
inputs = [[[1]],[]]
输出:
[null,0]
解释:
Solution solution = new Solution([1]);
solution.pickIndex(); // 返回 0,因为数组中只有一个元素,所以唯一的选择是返回下标 0。

示例 2:

输入:
inputs = ["Solution","pickIndex","pickIndex","pickIndex","pickIndex","pickIndex"]
inputs = [[[1,3]],[],[],[],[],[]]
输出:
[null,1,1,1,1,0]
解释:
Solution solution = new Solution([1, 3]);
solution.pickIndex(); // 返回 1,返回下标 1,返回该下标概率为 3/4 。
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 0,返回下标 0,返回该下标概率为 1/4 。

由于这是一个随机问题,允许多个答案,因此下列输出都可以被认为是正确的:
[null,1,1,1,1,0]
[null,1,1,1,1,1]
[null,1,1,1,0,0]
[null,1,1,1,0,1]
[null,1,0,1,0,0]
......

解题思路

创建一个权重数组

class Solution {
    private int[] sum;
    private int total;

    public Solution(int[] w) {
        sum = new int[w.length];
        for(int i = 0;i < w.length;i++){
            total += w[i];
            sum[i] = total;
        }
    }
    
    public int pickIndex() {
        Random random = new Random();
        int p = random.nextInt(total);
        int left = 0;
        int right = sum.length - 1;
        while(left <= right){
            int mid = (left + right) / 2;
            if(sum[mid] > p){
                if(mid == 0 || sum[mid - 1] <= p){
                   return mid; 
                }
                right = mid - 1;    
            }else{
                left = mid + 1;
            }      
        }
        return -1;
    }
}

在数值范围内二分查找

关键两点:

  • 确定解的范围
  • 发现不是中间值之后,选择前半段还是后半段

Q72:求平方根

题目(简单):给定一个非负整数 x ,计算并返回 x 的平方根,即实现 int sqrt(int x) 函数。正数的平方根有两个,只输出其中的正数平方根。如果平方根不是整数,输出只保留整数的部分,小数部分将被舍去。

示例 1:

输入: x = 4
输出: 2

示例 2:

输入: x = 8
输出: 2
解释: 8 的平方根是 2.82842...,由于小数部分将被舍去,所以返回 2
public int mySqrt(int x) {
    int left = 1;//若从0开始会产生分母为0情况
    int right = x;
    while (left <= right){
        int mid = (left + right) / 2;
        if(mid <= x / mid){//用mid*mid<=x会造成溢出
            if((mid+1) > x / (mid+1)){
                return mid;
            }
            left = mid + 1;
        }else{
            right = mid - 1;
        }
    }
    return 0;
}

Q73:狒狒吃香蕉

题目(中等):狒狒喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。狒狒可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉,下一个小时才会开始吃另一堆的香蕉。  狒狒喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。

示例 1:

输入: piles = [3,6,7,11], H = 8
输出: 4

示例 2:

输入: piles = [30,11,23,4,20], H = 5
输出: 30

示例 3:

输入: piles = [30,11,23,4,20], H = 6
输出: 23
public int minEatingSpeed(int[] piles, int h) {
    int max = Integer.MIN_VALUE;
    for (int pile : piles){
        max = Math.max(max,pile);
    }

    int left = 1;
    int right = max;
    while(left <= right){
        int mid = (left + right) / 2;
        int hour = getHour(piles,mid);
        if(hour <= h){
            if(mid == 1 || getHour(piles,mid-1)>h){
                return mid;
            }
            right = mid - 1;
        }else{
            left = mid + 1;
        }
    }
    return -1;
}

private int getHour(int[] piles,int speed){
    int hour = 0;
    for(int pile : piles){
        hour += (pile + speed - 1) / speed;//上取整
    }
    return hour;
}

小结

基本思路:

在查找范围内选取位于中间的数字,判断是否满足要求,若不满足则确定下一轮的查找范围。

应用场合:

  • 用于排序数组中查找某一个数字
  • 在数值范围内实现快速查找
    • 确定查找范围
    • 尝试中间值 ···