二分查找
基本内容
介绍
「二分查找 binary search」是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮减少一半搜索范围,直至找到目标元素或搜索区间为空为止。
Q&A
1 | Q:给定一个长度为 𝑛 的数组 nums ,元素按从小到大的顺序排列,数组不包含重复元素。请查找 |
1 | /* 二分查找(双闭区间)(推荐) */ |
优点与局限性
优点
- 二分查找的时间效率高。在大数据量下,对数阶的时间复杂度具有显著优势。例如,当数据大小𝑛 = 220时,线性查找需要 220 = 1048576 轮循环,而二分查找仅需 log2 220 = 20 轮循环。
- 二分查找无须额外空间。相较于需要借助额外空间的搜索算法(例如哈希查找),二分查找更加节省空间。
局限性
- 二分查找仅适用于有序数据。若输入数据无序,为了使用二分查找而专门进行排序,得不偿失。因为排序算法的时间复杂度通常为 𝑂(𝑛 log 𝑛) ,比线性查找和二分查找都更高。对于频繁插入元素的场景,为保持数组有序性,需要将元素插入到特定位置,时间复杂度为 𝑂(𝑛) ,也是非常昂贵的。
- 二分查找仅适用于数组。二分查找需要跳跃式(非连续地)访问元素,而在链表中执行跳跃式访问的效率较低,因此不适合应用在链表或基于链表实现的数据结构。
- 小数据量下,线性查找性能更佳。在线性查找中,每轮只需要 1 次判断操作;而在二分查找中,需要 1次加法、1 次除法、1 ~ 3 次判断操作、1 次加法(减法),共 4 ~ 6 个单元操作;因此,当数据量 𝑛 较小时,线性查找反而比二分查找更快。
二分查找插入点
二分查找不仅可用于搜索目标元素,还具有许多变种问题,比如搜索目标元素的插入位置。
无重复元素的情况
Q&A(1)
1 | 给定一个长度为 𝑛 的有序数组 nums 和一个元素 target ,数组不存在重复元素。现将 target |
[!NOTE] 当数组中包含 target 时,插入点的索引是否是该元素的索引?
将 target 插入到相等元素的左边,这意味着新插入的 target 替换了原来 target 的位置。也就是说,当数组包含 target 时,插入点的索引就是该target 的索引。
[!NOTE] 当数组中不存在 target 时,插入点是哪个元素的索引?
进一步思考二分查找过程:当 nums[m] < target 时 𝑖 移动,这意味着指针 𝑖 在向大于等于 target 的元素靠近。同理,指针 𝑗 始终在向小于等于 target 的元素靠近。因此二分结束时一定有:𝑖 指向首个大于 target 的元素,𝑗 指向首个小于 target 的元素。易得当数组不包含target 时,插入索引为 𝑖 。
1 | /* 二分查找插入点(无重复元素)索引返回不插入 */ |
Q&A(2)
1 | 在上一题的基础上,规定数组可能包含重复元素,其余不变。 |
假设数组中存在多个 target ,则普通二分查找只能返回其中一个 target 的索引,而无法确定该元素的左边和右边还有多少target。
方法一
题目要求将目标元素插入到最左边,所以我们需要查找数组中最左一个 target 的索引。通过以下步骤实现
- 执行二分查找,得到任意一个 target 的索引,记为 𝑘 。
- 从索引 𝑘 开始,向左进行线性遍历,当找到最左边的 target 时返回。
此方法虽然可用,但其包含线性查找,因此时间复杂度为 𝑂(𝑛) 。当数组中存在很多重复的 target 时,该方法效率很低。
方法二
拓展二分查找代码,整体流程保持不变,每轮先计算中点索引 𝑚 ,再判断 target 和
nums[m] 大小关系,分为以下几种情况。
- 当 nums[m] < target 或 nums[m] > target 时,说明还没有找到 target ,因此采用普通二分查找的缩小区间操作,从而使指针 𝑖 和 𝑗 向 target 靠近。
- 当 nums[m] == target 时,说明小于 target 的元素在区间 [𝑖, 𝑚 − 1] 中,因此采用 𝑗 = 𝑚 − 1 来缩小区间,从而使指针 𝑗 向小于 target 的元素靠近。
循环完成后,𝑖 指向最左边的 target ,𝑗 指向首个小于 target 的元素,因此索引 𝑖 就是插入点。
1 | /* 二分查找插入点(存在重复元素) */ |
二分查找边界
Q&A(左边界)
1 | 给定一个长度为 𝑛 的有序数组 nums ,数组可能包含重复元素。请返回数组中最左一个元素 |
回忆二分查找插入点的方法,搜索完成后 𝑖 指向最左一个 target ,因此查找插入点本质上是在查找最左一个target 的索引。
考虑通过查找插入点的函数实现查找左边界。请注意,数组中可能不包含 target ,这种情况可能导致以下两种结果。
- 插入点的索引 𝑖 越界。
- 元素 nums[i] 与 target 不相等。
当遇到以上两种情况时,直接返回−1即可。
1 | /* 二分查找最左一个 target */ |
右边界
复用查找左边界
实际上,我们可以利用查找最左元素的函数来查找最右元素,具体方法为:将查找最右一个 target 转化为查找最左一个 target + 1。
查找完成后,指针 𝑖 指向最左一个 target + 1(如果存在),而 𝑗 指向最右一个 target ,因
此返回 𝑗 即可。
请注意,返回的插入点是 𝑖 ,因此需要将其减 1 ,从而获得 𝑗 。
1 | /* 二分查找最右一个 target */ |
转化为查找元素
我们知道,当数组不包含 target 时,最终 𝑖 和 𝑗 会分别指向首个大于、小于 target 的元素。
因此,如图所示,我们可以构造一个数组中不存在的元素,用于查找左右边界。
- 查找最左一个 target :可以转化为查找 target - 0.5 ,并返回指针 𝑖 。
- 查找最右一个 target :可以转化为查找 target + 0.5 ,并返回指针 𝑗 。
注意以下两点。 - 给定数组不包含小数,这意味着我们无须关心如何处理相等的情况。
- 因为该方法引入了小数,所以需要将函数中的变量 target 改为浮点数类型。