Leetcode——二分查找的运用

154 阅读5分钟

在Leetcode题库里,属于该类型的题有很多.本文选用了其中两道简单难度和一道中等难度的来辅助理解二分查找算法。

二分查找算法分析

二分查找(Binary Search)也叫作折半查找。二分查找有两个要求,一个是数列有序,另一个是数列使用顺序存储结构(比如数组)

二分查找的实现原理非常简单,首先要有一个有序的列表。如所操作的列表无序则先进行排序。

常用的二分查找的使用场景有查找指定元素、寻找左侧边界、寻找右侧边界。

简单题

Leetcode_704. 二分查找

题目

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9 输出: 4 解释: 9 出现在 nums 中并且下标为 4 示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2 输出: -1 解释: 2 不存在 nums 中因此返回 -1

解题思路

想解出本题的方法有很多,从暴力遍历解决到查找算法的运用都可以解出本题,我放在了针对二分查找的运用的练习,所以我想到的即为使用二分查找解决。

对于查找指定元素:以升序为例,比较一个元素与数列中的中间位置的元素的大小,如果比中间位置的元素大,则继续在后半部分的数列中进行二分查找;如果比中间位置的元素小,则在数列的前半部分进行比较;如果相等,则找到了元素的位置。每次比较的数列长度都会是之前数列的一半,直到找到相等元素的位置或者最终没有找到要找的元素。

代码实现

public int search(int[] nums, int target) {
        //取数组长度
        int n = nums.length;
        //设置边界,left为左边界,设为0;right为右边界,取n-1,因n为数组长度,边界所设则为下标。
        int left = 0, right = n - 1;
        
        //取<=而不是<,因为初始化right值为最后元素的索引,相当于是闭区间内查找。当找到目标或者查找区间为空时停止
        while (left <= right) {
            //设置中间坐标,left+(right-left)/2 就和(left+right)/2的结果相同,但有效防了止left和right直接相加太大而导致mid溢出。
            int mid = left + (right - left) / 2;
            //查找到目标,直接返回下标。
            if (nums[mid] == target)
                return mid;
            //mid已经搜索过,维持搜索区间为闭区间而采用mid加减1来将搜索过的mid排除。
            else if (nums[mid] < target)
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid - 1;
        }
        //数组不存在目标值,返回-1.
        return -1;
    }

Leetcode_35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1:

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

示例 2:

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

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

解题思路

这道题就是考察搜索左侧边界的二分算法的细节理解,当目标元素 target 不存在数组 nums 中时,搜索左侧边界的二分搜索的返回值是 target 应该插入在 nums 中的索引位置。本题数组中不存在重复元素,所以对于考察边界的搜索,在我看来没有那么明显,但是也可以代入对边界搜索的初步认识。

代码

public int searchInsert(int[] nums, int target) {
        if (nums.length == 0) return -1;
        int left = 0;
        //采取了左闭右开的搜索区间
        int right = nums.length; 

        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                //缩小上界,由于本题不存在重复元素,所以也可以直接return mid;
                right = mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid; 
            }
        }
        //循环结束时 left和right相等,返回两者都可。
        return left;
    }

中等题

1011. 在 D 天内送达包裹的能力

题目描述

传送带上的包裹必须在 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) 是不允许的。

示例 2:

输入:weights = [3,2,2,4,1,4], days = 3

输出:6

解释: 船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:

第 1 天:3, 2 第 2 天:2, 4 第 3 天:1, 4

解题思路

本题可转变为二分查找的左边界查找,因运输天数和运输能力成反比,所在我看来题所求的即为在搜索出船的载重符合days天内运输所有包裹的载重区间中的左边界值。

对于左边界而言,由于我们不能拆分一个包裹,因此船的运载能力不能小于所有包裹中最重的那个的重量,即左边界为数组中元素的最大值。

对于右边界而言,船的运载能力也不会大于所有包裹的重量之和,即右边界为数组中元素的和。

解题代码

public int shipWithinDays(int[] weights, int days) {
        int left = 0;
        int right = 1;

        //左右边界的设值
        for (int w : weights) {
            left = Math.max(left, w);
            right += w;
        }

        //二分查找
        while (left < right) {
            //设置当前载重
            int mid = left + (right - left) / 2;
            //当前载重下,所需天数小于等于题给天数时,压低上界
            if (f(weights, mid) <= days) {
                right = mid;
            } else {
                //当前载重下,所需天数大于题给天数时,提高载重
                left = mid + 1;
            }
        }
        //结束循环时,right和left相等。
        return right;
    }

    // 定义:当运载能力为 x 时,需要 f(x) 天运完所有货物
    // f(x) 随着 x 的增加单调递减
    int f(int[] weights, int x) {
        int days = 0;
        for (int i = 0; i < weights.length; ) {
            // 尽可能多装货物
            int cap = x;
            while (i < weights.length) {
                if (cap < weights[i]) break;
                else cap -= weights[i];
                i++;
            }
            days++;
        }
        return days;
    }

小结

对于二分查找的运用理解并不是很难,主要是对于细节的设置和边界查找时拓展的运用需要通过练习来取得熟悉运用的能力。

欢迎一起讨论,共同学习!