LeetCode 剑指 Offer II 二分查找 专题总结

  • 博客主页:⭐️这是一只小逸白的博客鸭~⭐️
  • 欢迎 关注❤️点赞收藏⭐️评论
  • 小逸白正在备战实习,经常更新面试题LeetCode题解,欢迎志同道合的朋友互相交流~
  • 若有问题请指正,记得关注哦,感谢~

往期文章 :

  • LeetCode 剑指 Offer II 链表 专题总结
  • LeetCode 剑指 Offer II 哈希表 专题总结
  • LeetCode 剑指 Offer II 栈 专题总结
  • LeetCode 剑指 Offer II 队列 专题总结
  • LeetCode 剑指 Offer II 树(上) 专题总结
  • LeetCode 剑指 Offer II 树(下) 专题总结
  • LeetCode 剑指 Offer II 堆 专题总结
  • LeetCode 剑指 Offer II 前缀树(上) 专题总结
  • LeetCode 剑指 Offer II 前缀树(下) 专题总结

目录

  • 068. 查找插入位置
  • 069. 山峰数组的顶部
  • 070. 排序数组中只出现一次的数字
  • 071. 按权重生成随机数
  • 072. 求平方根
  • 073. 狒狒吃香蕉

068. 查找插入位置

题目:

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

示例:

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

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums 为无重复元素的升序排列数组
  • -104 <= target <= 104

思路:

要达到O(log n)的复杂度,只能用二分算法
target 插入的位置 ans应该满足 nums[ans - 1] < target <= nums[ans]
套用二分模板,在 target <= nums[ans] 的时候记录ans

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1;
        // ans记录位置,初始值为nums.size()是为了防止 target大于数组内所有值
        int ans = nums.size();
        while(left <= right) {
            int mid = ((right - left) >> 1) + left;
            // nums[ans - 1] < target <= nums[ans],所以小于等于且ans记录位置
            if(target <= nums[mid]) {
                ans = mid;
                right = mid - 1;
            }else {
                left = mid + 1;
            }
        }
        return ans;
    }
};

069. 山峰数组的顶部

题目:

符合下列属性的数组 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 ,即山峰顶部。

示例:

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

提示:

  • 3 <= arr.length <= 104
  • 0 <= arr[i] <= 106
  • 题目数据保证 arr 是一个山脉数组

思路:

二分查找
arr[mid] < arr[mid + 1] 时:表示此时在答案左边
arr[mid] >= arr[mid + 1]时:表示此事在答案右边且可能是答案,用ans记录

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int left = 0, right = arr.size() - 1;
        int ans = 0;
        while(left <= right) {
            int mid = ((right - left) >> 1) + left;
            // 数组最小长度为3,不用担心越界问题
            if(arr[mid] < arr[mid + 1]) {
                left = mid + 1;
            }else {
                // arr[mid] > arr[mid + 1] 时记录
                ans = mid;
                right = mid - 1;
            }
        }
        return ans;
    }
};

070. 排序数组中只出现一次的数字

题目:

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

示例:

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

提示:

  • 1 <= nums.length <= 105
  • 0 <= nums[i] <= 105

进阶: 采用的方案可以在 O(log n) 时间复杂度和 O(1) 空间复杂度中运行吗?

思路:

通过观察可以知道答案必然在数组下标为偶数的位置上,而且这个数组是升序的,可以使用二分查找
所以我们在查找时只需考虑偶数位,奇数跳过,知道这个即可用二分实现 O(log n) 复杂度

  • nums[mid] == nums[mid + 1] 代表这个不是答案且两个数一样,都跳过 left = mid + 2;
  • nums[mid] < nums[mid + 1] 代表这个数在答案右边也有可能是答案,ans保存
class Solution {
public:

    // 注释的奇数,偶数说的是数组下标
    // 思路:答案必定在偶数位上,因为相同的两个数都相邻,所以可以根据数组下标以及相邻数大小判断答案在左边还是在右边
    int singleNonDuplicate(vector<int>& nums) {
        // 这样就不用担心数组越界问题了
        if(nums.size() == 1) 
            return nums[0];
        int left = 0, right = nums.size() - 1;
        int ans = 0;
        while(left <= right) {
            int mid = ((right - left) >> 1) + left;
            // 答案一定在数组下标偶数位上,所以只考虑偶数位
            if(mid & 1 == 1) mid--;
            if(nums[mid] == nums[mid + 1]) {
                // 如果这个数不是答案,相当于他右边也不是,直接跳过
                left = mid + 2;
            }else {
                ans = mid;
                right = mid - 1;
            }
        }
        return nums[ans];
    }
};

071. 按权重生成随机数

题目:

给定一个正整数数组 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) 。

示例:

输入:
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]

诸若此类。

提示:

  • 1 <= w.length <= 10000
  • 1 <= w[i] <= 10^5
  • pickIndex 将被调用不超过 10000

思路:

前缀和 + 二分查找
以权重数组 [1, 2, 3, 4] 为例,标 0 的概率为 10% ,下标 1 、2 、 3 的概率为 20% 、30% 和40%。先按等概率生成 1 ~ 10,则每个数字的概率的都为 10%。如果生成 1 则选择下标 0,概率为 10%;如果生成 2 或 3 则选择下标 1,概率为 20%;如果生成 4、5 或 6 则选择下标 2,概率为 30%;如果生成 7、8、9 或 10 则选择下标 3,概率为 40%。

用前缀和数组pre存储,因为pre递增,所以二分查找pre数组
答案满足:pre[mid - 1] < rand() <= pre[mid] , (mid == 0)直接返回,防止越界

class Solution {
public:
    vector<int> pre;
    Solution(vector<int>& w) {
        int sum = 0;
        for(int i : w) {
            sum += i;
            pre.push_back(sum);
        }
    }
    
    int pickIndex() {
        int left = 0, right = pre.size() - 1;
        // 取随机数
        int target = rand() % pre.back() + 1;
        while(left <= right) {
            int mid = ((right - left) >> 1) + left;
            if(target <= pre[mid] ) {
                if(mid == 0 || target > pre[mid - 1])
                    return mid;
                right = mid - 1;
            }else {
                left = mid + 1;
            }
        }
        return -1;
    }
};

072. 求平方根

题目:

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

示例:

输入: x = 8
输出: 2
解释: 8 的平方根是 2.82842…,由于小数部分将被舍去,所以返回 2

提示:

  • 0 <= x <= 231 - 1

思路:

二分查找
mid * mid <= x,则答案在右边或本身,ans记录
mid * mid > x,则答案在左边
mid * mid <= x 换成mid <= x / mid防止乘积过大,同时过滤0,防止除数为0

class Solution {
public:
    int mySqrt(int x) {
        // 防止除数为0
        if(x == 0) 
            return 0;
        int left = 1, right = x;
        int ans = 0;
        while(left <= right) {
            int mid = ((right - left) >> 1) + left;
            // 将原本的 mid * mid <= x 可能越界,所以换格式
            if(mid <= x / mid) {
                ans = mid;
                left = mid + 1;
            }else {
                right = mid - 1;
            }
        }
        return ans;
    }
};

073. 狒狒吃香蕉

题目:

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

示例:

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

提示:

  • 1 <= piles.length <= 10^4
  • piles.length <= H <= 10^9
  • 1 <= piles[i] <= 10^9

思路:

速度取值[1, piles.max],因为速度至少为1,超过数组最大值也只能吃数组最大值那么多
然后对区间二分查找,check(piles, mid)函数计算mid速度所用时间

  • check(piles, mid) <= h 代表此速度能够完成,即右边不用考虑
    • check(piles, mid - 1) > h 代表速度减一就无法完成,即找到最小速度
  • check(piles, mid) > h 代表此速度无法完成,左边不用考虑,考虑右边
class Solution {
public:
    int minEatingSpeed(vector<int>& piles, int h) {
        int left = 1, right = 0;
        for(int& i : piles) {
            right = max(right, i);
        }
        // 二分查找[1,piles.max]
        while(left <= right) {
            int mid = ((right - left) >> 1) + left;
            // 如果当前速度已经满足 <= h 内吃完
            if(check(piles, mid) <= h) {
                // 速度减一不满足 <= h 内吃完,则得到速度最小值
                if(mid == 1 || check(piles, mid - 1) > h) {
                    return mid;
                }
                // 答案只可能在左边
                right = mid - 1;
            }else {
                // 否则答案在右边
                left = mid + 1;
            }
        }
        return -1;
    }
    // 计算mid速度吃完的时间
    int check(vector<int>& piles, int mid) {
        int time = 0;
        for(int& i : piles) {
            time += i / mid;
            time += ((i % mid) > 0);
        }
        return time;
    }
};

你可能感兴趣的:(LeetCode,leetcode,算法,二分查找,c++)