算法篇-二分搜索-基础到实战


title: 算法篇-二分搜索
date: 2023-4-10 20:12:57
categories:

  • 算法
    tags:
  • Java
  • 计算机基础
  • 数据结构
  • 算法
  • 二分搜索

参考:

  • 个人网站:www.huangrd.top
  • 力扣windliang:https://leetcode.cn/problems/search-a-2d-matrix-ii/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-5-4/
  • 力扣Krahets:https://leetcode.cn/leetbook/read/illustration-of-algorithm/5dj09d/
  • 代码随想录:https://programmercarl.com/

二分搜索

基础概述

Binary Search

对于已经有序的数组,使用二分搜索加快搜索速度

时间复杂度O(log N)

  1. 有一个有序表为 1,5,8,11,19,22,31,35,40,45,48,49,50 当二分查找值为 48 的结点时,查找成功需要比较的次数

  2. 使用二分法在序列 1,4,6,7,15,33,39,50,64,78,75,81,89,96 中查找元素 81 时,需要经过?次比较

  3. 在拥有128个元素的数组中二分查找一个数,需要比较的次数最多不超过多少次

对于前两个题目,记得一个简要判断口诀:奇数二分取中间,偶数二分取中间靠左。对于后一道题目,需要知道公式:

n = log2 N = log_{10}N/log_{10}2

其中 n 为查找次数,N 为元素个数

二分法的两种写法:

  • 左闭右闭[left,right]
  • 左闭右开[left,right)

左闭右闭

target 是在一个在左闭右闭的区间里,[left, right] 。

对于左闭右闭的写法:

  • while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1

二分法

/**
 * 左闭右闭
 */
public int binarySearch1(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;//区间[left,right]要有意义
    while (left <= right) {//注意点1
        int mid = left + ((right - left) / 2);//防止溢出
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else return mid;
    }
    return -1;
}

左闭右开

定义 target 是在一个在左闭右开的区间里,也就是[left, right)。

对于左闭右开的写法:

  • while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
  • if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]

在这里插入图片描述

/**
 * 左闭右开
 * */
public int binarySearch2(int[] nums, int target) {
    int left = 0;
    int right = nums.length;//区间[left,right)
    while (left < right) {
        int mid = left + ((right - left) / 2);
        if (nums[mid] < target) {//左闭
            left = mid + 1;
        } else if (nums[mid] > target) {//右开
            right = mid;
        } else return mid;
    }
    return -1;
}

二分法查找左右边界

public static int binarySearch(int[] nums, int target) {
    int left = 0, 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 {
            right = mid - 1;
        }
    }
    return -1;
}

public static int binarySearchFindLeft(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 {
            // 不返回,收缩右边界,找到左侧边界 合并 > 情况
            right = mid;
        }
    }
    return left != nums.length && nums[left] == target ? left : -1;
}

public static int binarySearchFindRight(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 {
            right = mid;
        }
    }
    return left != 0 && nums[left - 1] == target ? left - 1 : -1;
}

public static void main(String[] args) {
    int[] nums = {1, 5, 8, 11, 19, 19, 19, 22, 31, 35, 40, 45, 48, 49, 50};
    System.out.println(binarySearchFindLeft(nums, 19));
    System.out.println(binarySearch(nums, 19));
    System.out.println(binarySearchFindRight(nums, 19));
}

力扣

LC69. x 的平方根

给你一个非负整数 x ,计算并返回 x算术平方根

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

**注意:**不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5

利用二分法的思路,在0~x-1中搜索出他的平方根,排除特例。

public int mySqrt(int x) {
    /**
     * 二分查找
     * [left,right)
     * */
    //因为定义的区间为左闭右开,对于x,当x < 2的时候,x才会取0 or x 即 0 1
    if (x < 2) return x;
    int left = 1;
    int right = x;
    while (left < right) {
        int mid = left + ((right - left) / 2);
        if (mid > x / mid) {
            //mid > x / mid防止溢出
            right = mid;
        } else if (mid < x / mid) {
            left = mid + 1;
        } else return mid;
    }
    return right - 1;
}

面试题目要求:求某正整数的平方根,要求误差小于0.01

//二分法进行处理,注意变量类型
 public static double sqrt(int target){
        double n=1e-2;            //在这里可根据精度要求进行调整
        double l=0,r=target;      //这里若初始化r=target/2的话,则处理较小正整数会出错,如1,2
        while(l<=r){              //二分查找
            double mid=(l+r)/2;
            //这里用除法,而不用mid*mid与target比较,是防止mid过大时,mid*mid产生溢出问题
            if(target/mid<mid){   
                r=mid-n;
            }else{
                l=mid+n;
            }
        }
        return r;
    }
}

LC367. 有效的完全平方数

给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false

进阶:不要 使用任何内置的库函数,如 sqrt

防止越界,用long表示。

/**
* 二分法
* [left,right]左闭右闭
* */
public boolean isPerfectSquare(int num) {
    int left = 0;
    int right = num;
    while (left <= right) {
        int mid = left + ((right - left) / 2);
        long square = (long) mid * mid;
        if (square > num) {
            right = mid - 1;
        } else if (square < num) {
            left = mid + 1;
        } else{
            return true;
        }
    }
    return false;
}

LC1802. 有界数组中指定下标处的最大值

给你三个正整数 nindexmaxSum 。你需要构造一个同时满足下述所有条件的数组 nums(下标 从 0 开始 计数):

  • nums.length == n
  • nums[i]正整数 ,其中 0 <= i < n
  • abs(nums[i] - nums[i+1]) <= 1 ,其中 0 <= i < n-1
  • nums 中所有元素之和不超过 maxSum
  • nums[index] 的值被 最大化

返回你所构造的数组中的 nums[index]

注意:abs(x) 等于 x 的前提是 x >= 0 ;否则,abs(x) 等于 -x

示例 1:

输入:n = 4, index = 2,  maxSum = 6
输出:2
解释:数组 [1,1,2,1] 和 [1,2,2,1] 满足所有条件。不存在其他在指定下标处具有更大值的有效数组。

示例 2:

输入:n = 6, index = 1,  maxSum = 10
输出:3

提示:

  • 1 <= n <= maxSum <= 109
  • 0 <= index < n

Related Topics

贪心

二分查找

👍 81

👎 0

根据题目描述,如果我们确定了 nums[index] 的值为 x,此时我们可以找到一个最小的数组总和。也就是说,在 index 左侧的数组元素从 x-1 每次递减 1,如果减到 1 后还有剩余元素,那么剩余的元素都为 1;同样的,在 index 及右侧的数组元素从 x 也是每次递减 1,如果减到 1 后还有剩余元素,那么剩余的元素也都为 1。

这样我们就可以计算出数组的总和,如果总和小于等于 maxSum,那么此时的 x 是合法的。随着 x 的增大,数组的总和也会增大,因此我们可以使用二分查找的方法,找到一个最大的且符合条件的 x。

为了方便计算数组左侧、右侧的元素之和,我们定义一个函数 sum(x, cnt),表示一共有 cnt 个元素,且最大值为 x 的数组的总和。函数 sum(x, cnt) 可以分为两种情况:

计算[0,idx] 和 [idx, n - 1]区间所有元素的和
设cnt为区间的元素个数,即idx + 1 或 n - idx
1. cnt >= x, 那么会有多余的数全部放置为1: cnt - mid, 剩下的数为1,2,...x, 和为(x+1)*x/2。  
总和 = cnt - x + (x+1)*x/2
2. cnt < x, 那么放置的数为 x - cnt + 1, x - cnt, ...., x即[x-cnt+1, x],
总和 = (2x-cnt+1) * cnt / 2
  • 如果 x ≥ cnt,那么数组的总和为 [(x+x−cnt+1)×cnt]/2

  • 如果 x < cnt,那么数组的总和为 [(x+1)×x]/2+cnt−x

对于满足条件的最大的 x,x+1,x+2,… 一定不满足条件,而 1,2,…,x 这些数都满足条件,我们需要寻找最大的x,由于 n=10^9, 直接循环肯定不行,我们使用二分查找来寻找满足条件的最大x。

接下来,定义二分的左边界 left = 1,右边界 right = maxSum,然后二分查找 nums[index] 的值 mid,如果 sum(mid−1,index) + sum(mid,n−index) ≤ maxSum,那么此时的 mid 是合法的,我们可以将 left 更新为 mid,否则我们将 right 更新为 mid - 1。

最后将 left 作为答案返回即可。

class Solution {
    public int maxValue(int n, int index, int maxSum) {
        int left = 1, right = maxSum;
        while (left < right) {
            // 当用到的是r=mid是,就是mid=l+r>>1,当用到的是l=mid时,就是mid=l+r+1>>1,这是别人总结出来的经验,这样就避免了出现各种各样的边界错误。
            int mid = (left + right + 1) >>> 1;
            if (sum(mid - 1, index) + sum(mid, n - index) <= maxSum) {
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        return left;
    }

    private long sum(long x, int cnt) {
        return x >= cnt ? (x + x - cnt + 1) * cnt / 2 : (x + 1) * x / 2 + cnt - x;
    }
}

复杂度分析

  • 时间复杂度 O*(logN)*
  • 空间复杂度 O(1)。其中 N = maxSum

LC33. 搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k0 <= 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

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

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

示例 2:

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

示例 3:

输入:nums = [1], target = 0
输出:-1

提示:

  • 1 <= nums.length <= 5000
  • -104 <= nums[i] <= 104
  • nums 中的每个值都 独一无二
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • -104 <= target <= 104

Related Topics

数组

二分查找

👍 2442

👎 0

解题思路

先根据 nums[mid] 与 nums[left] 的关系判断 mid 是在左段还是右段,接下来再判断 target 是在 mid 的左边还是右边,从而来调整左右边界 left 和 right。

class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) { // [,]左闭右闭
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            }
            // 先根据 nums[mid] 与 nums[left] 的关系判断 mid 是在左段还是右段
            if (nums[mid] >= nums[left]) {
                // 再判断 target 是在 mid 的左边还是右边,从而调整左右边界 left 和 right
                if (target < nums[mid] && target >= nums[left]) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            } else {
                if (target > nums[mid] && target <= nums[right]) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }
        return -1;
    }
}

复杂度分析

  • 时间复杂度O(log N)
  • 空间复杂度O(1)

LC240. 搜索二维矩阵 II

编写一个高效的算法来搜索 *m* x *n* 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

  • 每行的元素从左到右升序排列。
  • 每列的元素从上到下升序排列。

示例 1:

在这里插入图片描述

输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true

示例 2:

在这里插入图片描述

输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20
输出:false

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= n, m <= 300
  • -109 <= matrix[i][j] <= 109
  • 每行的所有元素从左到右升序排列
  • 每列的所有元素从上到下升序排列
  • -109 <= target <= 109

Related Topics

数组

二分查找

分治

矩阵

👍 1251

👎 0

方法一 逐行二分

逐行进行二分搜索,根据有序性:

  • 当某一行第一个元素 > target,则没找到 target, 结束。
  • 当某一行最后一个元素 < target,则改行没有 target, 下一行继续。
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }

        for (int i = 0; i < matrix.length; i++) {
            // 1. 第一个元素大于tar
            if (matrix[i][0] > target) {
                return false;
            }
            // 2. 最后一个元素小于tar
            if (matrix[i][matrix[i].length - 1] < target) {
                continue;
            }
            if (binarySearch(matrix[i], target) != -1) {
                return true;
            }
        }
        return false;
    }

    private int binarySearch(int[] matrix, int target) {
        int left = 0, right = matrix.length - 1;
        while (left <= right) {
            int mid = left + right >> 1;
            if (matrix[mid] == target) {
                return mid;
            } else if (matrix[mid] > target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return -1;
    }
}

复杂度分析

  • 时间复杂度 O(M * log N)
  • 空间复杂度 O(1)

方法二 二分查找数

从右上角出发开始遍历,会发现每次都是向左数字会变小,向下数字会变大,有点和 二分查找树 相似。二分查找树的话,是向左数字变小,向右数字变大。

所以我们可以把 target 和当前值比较。

  • 如果 target 的值大于当前值,那么就向下走。
  • 如果 target 的值小于当前值,那么就向左走。
  • 如果相等的话,直接返回 true

也可以换个角度思考:

如果 target 的值小于当前值,也就意味着当前值所在的列肯定不会存在 target 了,可以把当前列去掉,从新的右上角的值开始遍历。

同理,如果 target 的值大于当前值,也就意味着当前值所在的行肯定不会存在 target 了,可以把当前行去掉,从新的右上角的值开始遍历。

[1,   4,  7, 11, 15],
[2,   5,  8, 12, 19],
[3,   6,  9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]

如果 target  = 9,如果我们从 15 开始遍历, cur = 15
    
target < 15, 去掉当前列, cur = 11
[1,   4,  7, 11],
[2,   5,  8, 12],
[3,   6,  9, 16],
[10, 13, 14, 17],
[18, 21, 23, 26]    
    
target < 11, 去掉当前列, cur = 7  
[1,   4,  7],
[2,   5,  8],
[3,   6,  9],
[10, 13, 14],
[18, 21, 23]     

target > 7, 去掉当前行, cur = 8   
[2,   5,  8],
[3,   6,  9],
[10, 13, 14],
[18, 21, 23]       

target > 8, 去掉当前行, cur = 9, 遍历结束    
[3,   6,  9],
[10, 13, 14],
[18, 21, 23]   
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        int row = 0, col = matrix[0].length - 1;
        while (row < matrix.length && col >= 0) {
            if (target == matrix[row][col]) {
                return true;
            } else if (target < matrix[row][col]) {
                col--;
            } else {
                row++;
            }
        }
        return false;
    }
}

复杂度分析

  • 时间复杂度:O(M + N)
  • 空间复杂度:O(1)

方法三 二维二分

找到矩阵的中心,然后和目标值比较看能不能丢弃一些元素。

如下图,中心位置是 9
[1,   4,  7, 11, 15],
[2,   5,  8, 12, 19],
[3,   6, /9/,16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]

通过中心位置, 我们可以把原矩形分成四个矩形, 左上, 右上, 左下, 右下
[1,   4,  7         [11, 15  
 2,   5,  8          12, 19  
 3,   6, /9/]        16, 22] 
 
[10, 13, 14         [17, 24
18, 21, 23]          26, 30]

如果 target = 10,
此时中心值小于目标值,左上角矩形中所有的数都小于目标值,我们可以丢弃左上角的矩形,继续从剩下三个矩形中寻找

如果 target = 5,
此时中心值大于目标值,右下角矩形中所有的数都大于目标值,那么我们可以丢弃右下角的矩形,继续从剩下三个矩形中寻找 

在这里插入图片描述

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

    private boolean searchMatrixHelper(int[][] matrix, int x1, int y1, int x2, int y2, int xMax, int yMax, int target) {
        //只需要判断左上角坐标即可
        if (x1 > xMax || y1 > yMax) {
            return false;
        }

        // x表示列,y表示行
        if (x1 == x2 && y1 == y2) {
            return target == matrix[y1][x1];
        }

        int m1 = (x1 + x2) >>> 1;
        int m2 = (y1 + y2) >>> 1;
        if (matrix[m2][m1] == target) {
            return true;
        }

        if (matrix[m2][m1] < target) {
                    // 右上矩阵
            return searchMatrixHelper(matrix, m1 + 1, y1, x2, m2, x2, y2, target) ||
                    // 左下矩阵
                    searchMatrixHelper(matrix, x1, m2 + 1, m1, y2, x2, y2, target) ||
                    // 右下矩阵
                    searchMatrixHelper(matrix, m1 + 1, m2 + 1, x2, y2, x2, y2, target);

        } else {
                    // 右上矩阵
            return searchMatrixHelper(matrix, m1 + 1, y1, x2, m2, x2, y2, target) ||
                    // 左下矩阵
                    searchMatrixHelper(matrix, x1, m2 + 1, m1, y2, x2, y2, target) ||
                    // 左上矩阵
                    searchMatrixHelper(matrix, x1, y1, m1, m2, x2, y2, target);
        }
    }
}

复杂度分析

  • 时间复杂度:O(n(log_43)),大约n^0.8.

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

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

输入:nums = [], target = 0
输出:[-1,-1]

提示:

  • 0 <= nums.length <= 105
  • -109 <= nums[i] <= 109
  • nums 是一个非递减数组
  • -109 <= target <= 109

Related Topics

数组

二分查找

👍 2255

👎 0

先查找左边界,再从左边界向后二分查找右边界。

class Solution {
    public int[] searchRange(int[] nums, int target) {
        // 先找最左边位置
        int left = binarySearchLeft(nums, target);
        if (left == -1) {
            return new int[]{-1, -1};
        }
        // 再找最右边的位置
        int right = binarySearchRight(nums, left, target);
        return new int[]{left, right};
    }

    private int binarySearchRight(int[] nums, int left, int target) {
        int right = nums.length;
        while (left < right) {
            int mid = left + ((right - left) >>> 1);
            if (nums[mid] <= target) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return left != 0 && nums[left - 1] == target ? left - 1 : -1;
    }

    private int binarySearchLeft(int[] nums, int target) {
        int left = 0, right = nums.length;
        while (left < right) {
            int mid = left + ((right - left) >>> 1);
            if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return left != nums.length && nums[left] == target ? left : -1;
    }
}

复杂度分析

  • 时间复杂度:O(log N)
  • 空间复杂度:O(1)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值