概念
二分查找(Binary Search)是一种效率较高的查找方式,它的时间复杂度为O(logn)
。
二分查找要求数组按关键字有序排列,并设置两个指针left
和right
分别指向数组的开头和末尾,每次查找left
和right
的中间项[mid]
。
设数组非递减排列:
- 若目标值
target
等于中间项[mid]
,则找到元素; - 若目标值
target
小于中间项[mid]
,则前往当前范围的左半部分查找,将right
置为mid-1
; - 若目标值
target
大于中间项[mid]
,则前往当前范围的右半部分查找,将left
置为mid+1
; - 若指针
left
跑到了right
的右边,代表数组已查询完毕,且数组中没有目标值。由此可得循环的结束条件为left>right
。
要点
-
请牢记二分查找算法的时间复杂度为O(log2n)
-
二分查找要求线性表必须以顺序结构存储,并且表中元素按关键字有序排列。
-
二分查找一般设置2个指针,一个指针left指向数组开头,另一个指针right指向数组末尾。查找循环的有效条件为
left<=right
。/* C++ */ // n为数组长度 for(int left=0, right=n-1;left<=right;) {...}
-
每次循环需要对
left
和right
中间一位进行判断。以C++为例,由于除法运算符/会将整数除法结果自动舍去小数点,因此每次的目标为left
和right
的中间位(right-left
结果为偶数)或中间偏左一位(right-left
结果为奇数)。中间位
mid
的计算有两种方式:/* C++ */ // n为数组长度 // int left = 0; // int right = n - 1; /* 式1 */ int mid = (right + left) >> 1; 或 /* 式2 */ int mid = ((right - left) >> 1) + left;
将式1转化为数学公式如下:
m i d = l e f t 2 + r i g h t 2 mid=\frac{left}{2}+\frac{right}{2} mid=2left+2right
式1非常易懂,将两数相加除以2即为他们的中位数。但是这个写法已经这么简单了,为什么还会出现式2呢?原因是1式会产生数值溢出。我们粗暴一点,假设当前
left
和right
皆为INT_MAX
,即7FFFFFFFH
,则两数相加就超出了int能表示的范围上限,最终mid的值就不正确了。将式2转换为数学公式如下:
m i d = r i g h t 2 − l e f t 2 + l e f t mid=\frac{right}{2}-\frac{left}{2}+left mid=2right−2left+left可以看到只是进行了一次简单的改变,但式2是不会出现数值溢出问题的。
当然1,在Python等语言中基本上是不会出现这种问题的,可以不用考虑。不过以这些语言为主力语言的小伙伴了解一下这个问题还是有益的。
当然2,你要这么写也没人拦你(也确实能出正确结果:
/* C++ */ int mid = (long long(left) + long long(right)) >> 1;
但万一
left
和right
本身就是双长整型的最大值呢?即使mid
定义为long long最终也会产生数值溢出的。所以无论怎样还是要用到式2的。 -
left
和right
在什么时候变化可能会根据实际情况改变,这里仅记录最基础的改变时机。/* C++ */ for (int left = 0, right = n - 1; left <= right;) { int mid = ...; if (target == nums[mid]) return mid; else if (target < nums[mid]) right = mid - 1; else left = mid + 1; }
-
二分查找不仅可以用于查找确切值,还能查找到比某个数大,或比某个数小的临界位置。
(这里指的当然不是找到那个数target然后左边就比它小,右边就比它大的这种显而易见的问题。)
在基础的算法结构上修改一些细节即可达到此目的。 参考案例[278. 第一个错误的版本]和以下案例:
/* C++ */ // 查找数组nums(升序排列)中第一个大于target的值。 int left = 0, right = nums.size() - 1; int sub = nums.size(); while (left <= right) { int mid = ((right - left) >> 1) + left; if (target < nums[mid]) { sub = mid; right = mid - 1; } else { left = mid + 1; } } // 此时sub为数组nums中第一个比target的值的下标
-
若
target<nums[mid]
,则代表中间项大于target
,第一个比target
大的值要么在该项之前,要么就是它自身。由于没有变量能够,因此需要一个额外的变量sub存放当时的mid
,并且置right
为mid-1
。 -
若
target>=nums[mid]
,则代表中间项小于target
,第一个比target
大的值必定在该项之后,因此我们将left
置为mid+1
。
上述代码在循环结束后,sub为数组
nums
中第一个比target
的值的下标。 -