二分查找算法模板总结

147 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的的第12天

概述

要求

  1. 必须采用顺序存储结构
  2. 必须按关键字大小有序排列(后有补充说明)

复杂度

时间复杂度:O(logn)O(\log n)

本质

二分查找本质是把数组看作左右两部分,然后找到两部分的边界,因此数组有序不是必要条件。只要能把数组分成两个部分,那么就可以使用二分法。

基本原则

  1. 每次都要缩减搜索范围
  2. 每次缩减不能排除潜在答案(若题目规定有多个可行答案,则每次缩减不能排除所有潜在答案)

基本模板1

来源

二分查找为什么总是写错?

一般流程

满足isBlue(m)函数的情况会导致l移动。因此最终l所在的位置即为最后一个满足isBlue(m)的数组元素的位置或-1,最终r在的位置即为第一个不满足isBlue(m)的数组元素的位置或N

后处理一般是边界处理,即检查返回的lr是否越界(即lr不移动),一般有以下三种情况:

  1. 数组元素只有一个。
  2. 不存在“等于”的数字。
  3. 符合条件的数字恰好在端点处,如:在[2, 5]中寻找≤ 5的数字,最终l = 1r = 2r不移动。

伪代码

l = -1, r = N  # l是左边界,r是右边界。l = 数组第一个元素的前一个位置,r = 数组最后一个元素的后一个位置
while l +1 != r
	m = l + (r - l) // 2  # 防止溢出,如果直接用m = (l + r) / 2可能会溢出
	if isBlue(m)
		l = m
	else
		r = m
checkCondition()  # 一些条件处理,如边界检测等
return l or r

时间复杂度:O(logn)O(\log n)

细节问题

  1. lr指代什么 l表示蓝色区域最后一个元素的下标,r表示红色区域第一个元素的下标。
  2. 为什么l的初始值为-1而不是0r的初始值为N而不是N-1? 当数组全为红色区域或蓝色区域时,lr的定义违背了第一条。
  3. m是否始终处于[0,N)[0, N)以内? 当l=lmin=1,r=rmin=1l=l_{min}=-1, r=r_{min}=1时,m=mmin=0m=m_{min}=0 注意:由于m可以为0,说明数组至少含有一个元素,因此rminr_{min}只可以为1,而非0(参考第二条)。另外,若r=0r=0,则程序将会立马退出while循环 当l=lmax=N2,r=rmax=Nl=l_{max}=N-2, r=r_{max}=N时,m=mmax=N1m=m_{max}=N-1 注意:为什么lmaxl_{max}不等于N-1?因为当lmax=N2l_{max}=N-2时将会进入while的最后一次循环,此时通过计算得到l的新值l = m = N - 1。之后就由于不满足l + 1 != r无法再进入下一次while循环。所以能进入循环的lmaxl_{max}只能为N - 2
  4. 更新指针时,能不能写成l = m + 1r = m - 1? 对于这种模板来说,不能。若m刚好指向蓝色区域的右边界,那么l = m + 1会导致l进入红色区域。同理,若m刚好指向红色区域的左边界,那么r = m - 1会导致r进入蓝色区域。
  5. 会不会进入死循环? 当l + 1 = r时,会退出循环。 当l + 2 = r时,m将指向lr正中间,此时要么l = m,要么r = m,之后将会进入第一种情况。 其他情况,最后都会进入上述两种情况。因此,程序不会陷入死循环。
  6. isBlue(m)问题 isBlue(m)只能是判断第m个元素是否属于蓝色区域,不能判断其是否属于红色区域。

部分常见题型

另外,若要找值为target的元素,则需令isBlue(m)条件为<= target,然后判断第l个元素是否为target,并且注意边界情况,最后返回l(方法不唯一)。

基本模板2

来源

二分查找细节详解,顺便赋诗一首

框架

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 ...;
}

其中...标记的部分是可能出现细节问题的部分。

搜索区间为左闭右闭型。

技巧

  1. 不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节
  2. 使用left + (right - left) / 2防止溢出

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

代码(左闭右闭)

int binarySearch(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;
}

细节

  1. 什么是左闭右闭区间?什么是左闭右开区间? 左闭右闭区间:leftright指向的都是搜索区间内的元素,即搜索范围是[left, right]。当搜索区间为空时,left = right + 1,此时搜索区间为[right + 1, right],显然不包含任何元素,因此while循环条件是left <= rightleft + 1 != right,即退出循环时left = right + 1 左闭右开区间:left指向搜索区间的起始位置(第一个元素),right指向搜索区间末端的后一个元素,即搜索范围是[left, right)。当搜索区间为空时,left = right,此时搜索区间为[left, left),显然不包含任何元素,因此while循环条件是left < rightleft != right,即退出循环时left = right
  2. 如何判断搜索区间的类型? 规定mid = left + (right - left) / 2 则当while的循环条件是left <= rightleft + 1 != right时,搜索区间是左闭右闭型,且right的初值一般为nums.size() - 1,且right = mid - 1。 当while的循环条件是left < rightleft != right时,搜索区间是左闭右开型,且right的初值一般为nums.size(),且right = mid。 此处不考虑左开右闭型。
  3. 为什么while循环的条件中是<=,而不是<? 因为该模板是左闭右闭型。
  4. 为什么left = mid + 1right = mid - 1?而有的代码是right = mid或者left = mid? 因为该模板是左闭右闭型。当发现mid不是要找的target后,自然下一个要搜索的区间是[left, mid - 1][mid + 1, right],因为mid已经搜索过,应该从搜索区间中去除。 若是左闭右开区间,则下一个搜索区间就是[left, mid)[mid + 1, right),因此应该使用left = mid + 1right = mid。 而模板1之所以同时使用left = midright = mid,是因为思考的角度不同:模板1把数组分为蓝红两个部分,leftright分别指向蓝红区域的边界。数组中任何一个元素只有两种可能:要么是蓝色,要么是红色。所以如果nums[mid]是蓝色,则left = mid,反之亦然。
  5. 该模板的缺陷 只能找到target,但不能找到target第一次、最后一次出现的位置,也不能找到第一个大于或小于target的元素的位置。