Loading

刷题复习(四)二分搜索

代码框架

int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;

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

另外提前说明一下,计算 mid 时需要防止溢出,代码中 left + (right - left) / 2 就和 (left + right) / 2 的结果相同,但是有效防止了 leftright 太大,直接相加导致溢出的情况。

一、寻找一个数(基本的二分搜索)

class Solution {
    // 标准的二分搜索框架,搜索目标元素的索引,若不存在则返回 -1
    public int search(int[] nums, int target) {
        int left = 0;
        // 注意
        int right = nums.length - 1;

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

核心思路: [left, right] 两端都闭的区间。这个区间其实就是每次进行搜索的区间

如果 target 不存在,搜索左侧边界的二分搜索返回的索引是大于 target 的最小索引

此算法有什么缺陷?

答:至此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。

比如说给你有序数组 nums = [1,2,2,2,3]target 为 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。

这样的需求很常见,你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了

二、寻找左侧边界的二分搜索

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

为什么是 left = mid + 1right = mid

为什么 left = mid + 1right = mid ?和之前的算法不一样?

答:这个很好解释,因为我们的「搜索区间」是 [left, right) 左闭右开,所以当 nums[mid] 被检测之后,下一步应该去 mid 的左侧或者右侧区间搜索,即 [left, mid)[mid + 1, right)

为什么该算法能够搜索左侧边界?

答:关键在于对于 nums[mid] == target 这种情况的处理:

    if (nums[mid] == target)
        right = mid;

可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。

左闭右闭写法

int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    // 搜索区间为 [left, right]
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            // 搜索区间变为 [mid+1, right]
            left = mid + 1;
        } else if (nums[mid] > target) {
            // 搜索区间变为 [left, mid-1]
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 收缩右侧边界
            right = mid - 1;
        }
    }
    // 判断 target 是否存在于 nums 中
    // 如果越界,target 肯定不存在,返回 -1
    if (left < 0 || left >= nums.length) {
        return -1;
    }
    // 判断一下 nums[left] 是不是 target
    return nums[left] == target ? left : -1;
}

三、寻找右侧边界的二分查找

类似寻找左侧边界的算法,这里也会提供两种写法,还是先写常见的左闭右开的写法,只有两处和搜索左侧边界不同:

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

target 不存在时,right_bound 返回值的含义

直接说结论,和前面讲的 left_bound 相反:如果 target 不存在,搜索右侧边界的二分搜索返回的索引是小于 target 的最大索引

int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    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 if (nums[mid] == target) {
            // 这里改成收缩左侧边界即可
            left = mid + 1;
        }
    }
    // 最后改成返回 left - 1
    if (left - 1 < 0 || left - 1 >= nums.length) {
        return -1;
    }
    return nums[left - 1] == target ? (left - 1) : -1;
}

875. 爱吃香蕉的珂珂 - 力扣(LeetCode)

珂珂喜欢吃香蕉。这里有 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

// 完整可运行版本
class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        int left = 1;
        int right = 1_000_000_000;

        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (f(piles, mid) == h) {
                // 搜索左侧边界,则需要收缩右侧边界
                right = mid - 1;
            } else if (f(piles, mid) < h) {
                // 需要让 f(x) 的返回值大一些
                right = mid - 1;
            } else if (f(piles, mid) > h) {
                // 需要让 f(x) 的返回值小一些
                left = mid + 1;
            }
        }

        return left;
    }

    long f(int[] piles, int x) {
        long hour = 0;
        for (int i = 0; i < piles.length; i++) {
            hour += piles[i] / x;
            if (piles[i] % x > 0) {
                hour++;
            }
        }
        return hour;
    }
}

难点是如何抽象成二分搜索

image-20250615172050965

还有坑点在int会出现数字越界的case,要用long

image-20250615173543906

1011. 在 D 天内送达包裹的能力 - 力扣(LeetCode)

传送带上的包裹必须在 days 天内从一个港口运送到另一个港口。

传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量(weights)的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。

返回能在 days 天内将传送带上的所有包裹送达的船的最低运载能力。

 

示例 1:

输入:weights = [1,2,3,4,5,6,7,8,9,10], days = 5
输出:15
解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 5
第 2 天:6, 7
第 3 天:8
第 4 天:9
第 5 天:10

请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。 
class Solution {
    public int shipWithinDays(int[] weights, int days) {
        int left = 0;
        // 注意,right 是开区间,所以额外加一
        int right = 1;
        for (int w : weights) {
            left = Math.max(left, w);
            right += w;
        }
        while (left < right) {
            int mid = left + (right - left) / 2;
            int cap = timeByLoad(weights, mid);
            if (cap > days) {
                left = mid + 1;
            } else if (cap < days) {
                right = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }

    public int timeByLoad(int[] weights, int load) {
        int days = 0;
        for (int i = 0; i < weights.length; ) {
            int cap = load;
            while (i < weights.length) {
                if (cap < weights[i])
                    break;
                else
                    cap -= weights[i];
                i++;
            }
            days++;
        }
        return days;
    }
}

重点注意需要对left和right进行剪纸不然会判超时.

image-20250615182154070

posted @ 2025-09-15 20:57  kopoo  阅读(11)  评论(0)    收藏  举报