算法系列 二分查找

二分查找

适用于有序数组,思想比较简单,每次折半搜索,时间复杂度为O(logN)。具体实现参考 Search.java ,特别需要注意二分查找的边界问题

标准写法:

class Solution {
    public int binarySearch(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) { // 退出循环时left=right+1区间内没有任何一个元素,所以要跳出循环
            int mid = left + (right - left)/2;  // 防止溢出           
            if (nums[mid] > target) {
                right = mid - 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                return mid;	// 如果target存在,则一定会进入本分支
            }
        }
        return left;	// target不存在,返回插入位置,也可返回-1表示未找到
    }
}
复制代码

注意

  • 上述算法正确运行的前提是不存在重复元素,如果存在重复元素,则无法保证最终返回的是重复元素中的哪一个元素的下标。如果nums中不存在目标元素,则返回的是target应该插入的位置。

  • 为何是while(left <= right)

    • 算法初始化时right = nums.length - 1,搜索下标的区间的应该是左闭右闭的[left, right],因为可能存在更新left = mid + 1 = right的情况(eg:在[0, 2]中找2,此时mid = right = left = 1没有被搜索就直接认为元素不存在了),所以不能写成while(left < right)
    • 循环退出条件为left = right+1,此时可以保证在未找到元素时返回目标元素的正确插入位置(如果目标元素比原始数组中的任何元素都大的话)。
  • 如果写成while(left != right),当数组长度为1时,直接无法找到又有效下标(如果是直接返回-1的话);当数组中没有目标元素时,程序陷入死循环。

  • 为何是mid = left + (right - left)/2

    • 如果写成 mid = (left + right)/2,左右边界相加可能超过Integer.MAX_VALUE导致越界。而(first + last) / 2 = (2 * first + last - first) / 2 = first + length / 2, 其中length = last - first为区间长度。此时不可能存在越界的情况。

left从0起始,mid是向下取整,left只在mid遇到 确定 小于目标数时才前进一步,left一直在朝着第一个目标数的位置在逼近。永远记住left是对的!!!

而right的收缩往往是大胆的(如写法二部分),所以right不可取。

写法二

寻找左侧边界的二分查找

记住 标准写法最后的原则!此时寻找最左侧的目标值将变得很简单:

class Solution {
    public int binarySearchFirst(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) { 
            int mid = left + (right - left)/2;         
            if (nums[mid] >= target) {	// 取等号的时候不能直接返回,因为左边可能还有相同的数字
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return left;	// 如果target不存在则left表示插入位置
        /*
        if (left != nums.length && nums[left] == target) {
        	return left;
        }
        return -1;	// 表示未找到
        */
    }
}
复制代码
寻找右侧边界的二分查找

和寻找左侧边界解法类似,只是最终需要返回right而不是left(因为此时左侧的前进是激进的,右侧是保守的)!如果目标元素不存在,返回的下标没有意义!

class Solution {
    public int binarySearchLast(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 {
                right = mid - 1;
            }
        }

        return right;
    }
}
复制代码

相关问题

  • 基于值的二分查找:参考leetcode.problem378 有序矩阵中第K小的元素

  • while循环条件不一定是

    left <= right
    复制代码
    • leetcode.problem540 有序数组中的单一元素
    • leetcode.problem278 有序矩阵中第K小的元素
  • 局部有序,旋转数组相关的题目

    • leetcode.problem33 寻找旋转数组中的元素 I
  • 同时寻找左右边界

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

待补充。。。

总结

while 中到底是 left < right 还是 left <= right (假定left=0,right=n-1分别为左右两端元素的下标值)?

  • left <= right的循环退出条件为left = right + 1, 此时适用于原始数组中午target值时可以返回target的正确插入位置(return left)
  • left < right的循环退出条件为left = right, 此时适用于不允许数组越界访问的情形(因为循环体可能可能存在mid+1可能越界的情况), 此时return left/right均可。

本文正在参加「金石计划 . 瓜分6万现金大奖」

分类:
后端