一、二分查找概述
二分查找(Binary Search
)是一种高效的查找算法,适用于有序数组或列表。(但其实只要满足二段性,就可以使用二分法,本篇博客后面博主会持续更新一些题,来破除一下人们对“只有有序才能二分”的误解。)
二分通过反复将查找范围分为两半,并根据目标值与中间元素的大小关系来确定下一步查找的方向,从而快速定位目标值的位置。
二、二分法代码实现
三、二分法习题合集
1.LeetCode 35 搜索插入位置
- 解法
public static int searchInsert(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 if (nums[mid] < target) {
left = mid + 1; // 如果中间值小于目标值,缩小搜索范围至右半部分
} else {
return mid; // 找到目标值,直接返回索引
}
}
// 循环结束时,left 指向应该插入的位置
return left;
}
- 这里博主解释一下为什么最后返回left(debug走一下流程或者在草稿纸上画一画其实就很容易看出来啦~)。
函数 searchInsert
的目标是在给定的有序数组 nums
中查找目标值 target
的插入位置(如果目标值不存在于数组中)。
如果数组中存在目标值,则返回目标值的索引;如果不存在,则返回应该插入的位置索引,使得插入后数组依然保持有序。
插入位置保持有序性:
- 返回
left
而不是right
是因为当循环结束时,left
恰好指向比target
大的第一个元素的位置,或者数组的末尾位置(如果target
大于数组中的所有元素),这正是目标值应该插入的位置,可以保持数组的有序性。
2.LeetCode 69 x的平方
- 解法
public static int mySqrt(int x) {
if (x == 0 || x == 1) return x;
int left = 0, right = x;
int ans = 0;
while (left <= right) {
int mid = left + (right - left) / 2;
//防止越界~因为 mid*mid 数值过大 int可能会越界 或者 强转一下也可——(long)mid*mid
if (mid < x / mid) { //如果这个整数的平方 严格小于 输入整数,那么这个整数 可能 是我们要找的那个数(重点理解这句话)。
ans = mid;//所以我们更新答案
left = mid + 1;
} else if (mid > x / mid) { //如果这个整数的平方 严格大于 输入整数,那么这个整数 肯定不是 我们要找的那个数;
right = mid - 1;
} else { //如果这个整数的平方 恰好等于 输入整数,那么我们就找到了这个整数;
return mid;
}
}
return ans;
}
3.LeetCode 367 有效的完全平方数
- 解法
public static boolean isPerfectSquare(int num) {
int left = 0, right = num;
// 使用二分查找来确定是否为完全平方数
while (left <= right) {
int mid = left + (right - left) / 2; // 计算中间值,避免整数溢出问题
if ((long) mid * mid < num) {
left = mid + 1; // 如果 mid 的平方小于 num,说明目标值在右半部分,缩小搜索范围至右半部分
} else if ((long) mid * mid > num) {
right = mid - 1; // 如果 mid 的平方大于 num,说明目标值在左半部分,缩小搜索范围至左半部分
} else {
return true; // 如果 mid 的平方等于 num,直接返回 true,表示找到完全平方数
}
}
// 循环结束时,未找到完全平方数,返回 false
return false;
}
4.LeetCode 34 在排序数组查找元素的第一个和最后一个位置
- 解法
本题拆分成两个函数,分别处理较好,不过这个对处理二分的熟练度要求还挺高,比如说left<=right 还是left<right以及左右指针什么时候该怎么移动都有讲究,一个小细节不对的话就得不到正确的答案。
大概的二分的模版大家都知道,区别就在于具体问题的边界问题,是需要自己思考的。
建议有电脑的小伙伴可以debug走一下流程 ,可以看出来左右指针怎么移动的,慢慢调试;或者在纸上画一画,看一看自己写的二分是怎么个流程~
public static int[] searchRange(int[] nums, int target) {
int[] ans = {-1, -1};
// 特殊情况处理:数组为空,或者目标值不在数组范围内
if (nums.length == 0 || nums[0] > target || nums[nums.length - 1] < target) return ans;
// 查找第一次出现的位置
int first = findFirst(nums, target);
if (first == -1) return ans; // 如果找不到目标值,返回初始的{-1, -1}
ans[0] = first;
// 查找最后一次出现的位置
ans[1] = findLast(nums, target);
return ans;
}
// 找到元素第一次出现的位置
public static int findFirst(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 if (nums[mid] >= target) {
right = mid - 1; // 目标值在左半部分或者当前位置就是目标值
}
}
// 当退出循环时,left 指向第一个大于等于目标值的位置
return nums[left] == target ? left : -1; // 如果找到目标值,返回该位置;否则返回 -1
}
// 找到元素最后一次出现的位置
public static int findLast(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 if (nums[mid] > target) {
right = mid - 1; // 目标值在左半部分
}
}
// 当退出循环时,right 指向最后一个小于等于目标值的位置
return right; // 直接返回 right,即最后一次出现的位置
}
5.LeetCode 33 搜索旋转排序数组
- 解法
题目要求我们设计一个时间复杂度为O(logN)的算法,很容易就想到二分法。
但是本题整个数组并不是完全有序的,而是被“旋转”拆分成了两个部分。
如果我们能找到那个旋转点的话,在两个有序的部分进行二分查找就非常轻松了。
那么如果直接遍历数组去找旋转点的话,时间复杂度还是会上升到O(N),不符合题目要求。
我们能不能也用二分去寻找这个旋转点呢? 答案是可以的。
eg 4 5 6 7 1 2 3——旋转点在 1 处
我们以arr[0],也就是4为基准,用mid 去跟 arr[0]比较,如果mid>arr[0],说明旋转点在mid右边,如果mid<arr[0],那么可能当前就是旋转点,或者旋转点在右边。
这也算是满足二段性的一个例子了——二分法并不是一定要有序的时候才能用,满足二段性时,也可以使用。
//查找旋转点
public static int findRotationPointIndex(int[] arr) {
// 如果数组长度为2,直接返回较小元素的索引
if (arr.length == 2) return arr[0] < arr[1] ? 0 : 1;
// 初始化左右边界
int left = 0;
int right = arr.length - 1;
// 二分查找旋转点
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] >= arr[0]) {
left = mid + 1; // mid处于前半段递增序列,旋转点在右半段
} else {
right = mid; // mid处于后半段递增序列或是旋转点
}
}
return left; // left指向旋转点的索引
}
查找到旋转点之后,我们再在两端进行二分查找就比较容易了。
public int search(int[] arr, int target) {
// 如果数组长度为1,直接比较目标值与数组唯一元素
if (arr.length == 1) return target == arr[0] ? 0 : -1;
// 找到旋转点的索引
int index = findRotationPointIndex(arr);
// 初始化左右边界
int left = 0;
int right = arr.length - 1;
// 确定二分查找的范围
if (index == 0) {
// 数组没有旋转,直接在整个数组上执行二分查找
return binaryFind(arr, left, right, target);
}
if (target >= arr[0]) {
right = index; // 目标值可能在旋转点之前(包括旋转点)
} else {
left = index; // 目标值在旋转点之后
}
// 在确定的范围内执行二分查找
return binaryFind(arr, left, right, target);
}
//二分查找
public static int binaryFind(int[] arr, int left, int right, int target) {
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
6.LeetCode 475 供暖器
- 解法
核心思路就是两句话:
(1)对于每个房屋,要么用前面的暖气,要么用后面的,二者取近的,得到距离;
(2)对于所有的房屋,选择最大的上述距离。
所以我们可以将heaters排好序,然后对每个房屋利用二分法去搜索最近的(相邻的)供暖器即可。
代码实现:
public int findRadius(int[] houses, int[] heaters) {
// 首先对加热器的位置数组进行排序
Arrays.sort(heaters);
// 初始化答案为0
int ans = 0;
int n = houses.length;
// 遍历房屋的位置数组
for (int i = 0; i < n; i++) {
// 对于每个房屋位置,调用二分查找函数找到其最近的加热器,并更新答案
ans = Math.max(binarySearch(heaters, houses[i]), ans);
}
// 返回最大半径
return ans;
}
// 二分查找函数,用于找到距离目标最近的加热器
public int binarySearch(int[] nums, int target) {
int n = nums.length;
// 如果目标大于等于加热器数组中最后一个加热器的位置,直接返回目标与最后一个加热器位置的距离差
if (target >= nums[n - 1]) return target - nums[n - 1];
// 如果目标小于等于加热器数组中第一个加热器的位置,直接返回第一个加热器位置与目标的距离差
if (target <= nums[0]) return nums[0] - target;
// 初始化左右边界
int l = 0, r = n - 1;
// 开始二分查找
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) {
return 0; // 如果找到目标位置,返回距离差为0
} else if (nums[mid] < target) {
l = mid + 1; // 如果目标在当前中间值的右侧,更新左边界
} else {
r = mid - 1; // 如果目标在当前中间值的左侧,更新右边界
}
}
// 循环结束时,l指向的位置即为最终找到的最近的那个比他大的加热器的位置
//取两个差值的最小值
return Math.min(nums[l] - target, target - nums[l - 1]);
}
7.LeetCode 287 寻找重复数
-
这道题博主咋也想不出来能用二分法哈哈哈哈,想破脑壳也找不到二段性
-
但是 还有有二段性滴~ 哈哈哈哈 刚开始看不太明白没关系 博主也琢磨了好一会儿 差点放弃了…
-
自己在纸上找一些例子画一画 慢慢就能get到这个点啦
- 代码实现
// 寻找重复元素的方法,输入是一个整数数组 nums
public int findDuplicate(int[] nums) {
int n = nums.length; // 数组的长度
int l = 1, r = n - 1; // 设定搜索范围,因为题目给出了1到n-1之间的数字重复,所以左边界为1,右边界为n-1
int ans = -1; // 初始化答案为-1,因为题目保证了一定存在重复元素,因此初始值不影响结果
// 开始二分搜索
while (l <= r) {
int mid = l + (r - l) / 2; // 计算中间值
int cnt = 0; // 统计小于等于mid的元素个数
for (int num : nums) {
if (num <= mid) {
cnt++;
}
}
// 如果小于等于mid的元素个数(cnt)小于等于mid本身,则重复元素在[mid+1, r]范围内
if (cnt <= mid) {
l = mid + 1; // 更新左边界,缩小搜索范围到[mid+1, r]
} else {
r = mid - 1; // 否则重复元素在[l, mid-1]范围内
ans = mid; // 更新答案为当前的mid,因为mid可能是重复的数字
}
}
return ans; // 返回找到的重复元素
}