一、二分查找概述
二分查找(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*midif (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,表示找到完全平方数}}// 循环结束时,未找到完全平方数,返回 falsereturn 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);// 初始化答案为0int 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-1int 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; // 返回找到的重复元素
}