【算法】二分查找
一、算法理解
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,前提是数据结构必须先排好序,可以在数据规模的对数时间复杂度内完成查找。
二分查找要求线性表具有随机访问的特点(例如数组),也要求线性表能够根据中间元素的特点推测它两侧元素的性质,以达到缩减问题规模的效果。
二、适用场景
- 有序序列查找。
- 元素支持随机访问(指定索引访问)。
三、注意事项
关键点:
- 二分查找的关键变量是 left,mid,right三个元素,根据目标值和mid所在索引的数值和target目标值进行比对,根据[mid]中间值的大小重新确定查找区间。
- 用途:二分法的基础是分治策略的基础,分治用的最多的是二分拆解计算。
- 可以使用 循环查找方式、或递归查找方式。
如:递归查找方式典型模板:(以递增顺序序列为例)
- right指向是查找范围下一个元素(无效元素)
public int search(int[] nums, int target) {
return medianSearchByIndex(nums, 0, nums.length, target); //首次在left[0],right[length]范围查找
}
public int medianSearchByIndex(int[] nums,int left,int right,int target) {
if(left < right) { //注意, right是查询终点后一个元素,所以 <
int mid = (left + right) / 2; // 注意
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
return searchByIndex(nums, mid+1, right, target);
} else {
return searchByIndex(nums, left, mid, target); //注意, right是查询终点后一个元素,所以是mid
}
}
return 0;
}
- right是查找范围最后一个有效元素
public int search(int[] nums, int target) {
return medianSearchByIndex(nums, 0, nums.length, target); //首次在left[0],right[length]范围查找
}
public int medianSearchByIndex(int[] nums,int left,int right,int target) {
if(left <= right) { //注意, right是查询终点最后一个有效元素,最终二分到只有一个元素left==right
int mid = (left + right) / 2; // 注意
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
return searchByIndex(nums, mid+1, right, target);
} else {
return searchByIndex(nums, left, mid-1, target); //注意, right是查询终点最后一个有效元素,所以是mid-1,应为mid已排除,不用重新查。
}
}
return 0;
}
四、案例
1)【困难】寻找2个正序数组中位数
力扣:https://leetcode-cn.com/problems/median-of-two-sorted-arrays/
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。
请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
示例 1:
nums1 = [1, 3]
nums2 = [2]
则中位数是 2.0
示例 2:
nums1 = [1, 2]
nums2 = [3, 4]
则中位数是 (2 + 3)/2 = 2.5
示例3:
输入:nums1 = [], nums2 = [1]
输出:1.00000
说明:
提示:
nums1.length == m
nums2.length == n
0 <= m <= 1000
0 <= n <= 1000
1 <= m + n <= 2000
-106 <= nums1[i], nums2[i] <= 106
【思路】:
-
常规的思路是:num1[]和num2[]合并、排序后,变成一个长度为m+n的有序大序列,找其中位数。m+n是基数,则返回索引[(m+n)/2]的值;m+n是偶数,则返回索引[(m+n)/2]、[((m+n)/2)+1]的均值。
-
如上可知,本质上:对于总长m+n的两个序列,只要找到索引[(m+n)/2]、[((m+n)/2)+1]的值即可。那么考虑用二分查找 从两个序列中找 第k个数 方式来实现呢?
代码参考:
- 采用循环查找方式
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int totalLength = nums1.length + nums2.length;
if (totalLength == 0) {
return 0;
}
if (totalLength % 2 == 1) {
// 第totalLength / 2 + 1个数值
double median = getKthElement(nums1, nums2, totalLength / 2 + 1);
return median;
} else {
// 第totalLength / 2个数值
double median1 = getKthElement(nums1, nums2, totalLength / 2);
// 第totalLength / 2 + 1个数值
double median2 = getKthElement(nums1, nums2, totalLength / 2 + 1);
return (median1 + median2) / 2;
}
}
public int getKthElement(int[] nums1, int[] nums2, int k) {
int length1 = nums1.length, length2 = nums2.length;
int index1 = 0, index2 = 0;
int kthElement = 0;
while (true) {
// 边界情况
if (index1 == length1) {
return nums2[index2 + k - 1];
}
if (index2 == length2) {
return nums1[index1 + k - 1];
}
if (k == 1) {
return Math.min(nums1[index1], nums2[index2]);
}
// 正常情况,第k/2个元素,对应索引k/2-1
int half = k/2;
int newIndex1 = Math.min(index1 + half - 1, length1 - 1);
int newIndex2 = Math.min(index2 + half - 1, length2 - 1);
if (nums1[newIndex1] <= nums2[newIndex2]) {
k -= (newIndex1 - index1 + 1); //排除掉用nums1中0~newIndex1元素,计算个数
index1 = newIndex1 + 1; //刷新排除后的索引
} else {
k -= (newIndex2 - index2 + 1);
index2 = newIndex2 + 1;
}
}
代码参考:
- 采用递归查找方式
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int totalLength = nums1.length + nums2.length;
if (totalLength == 0) {
return 0;
}
if (totalLength % 2 == 1) {
// 第totalLength / 2 + 1个数值
double median = getKthElementLoop(nums1, 0,nums2, 0,totalLength / 2 + 1);
return median;
} else {
// 第totalLength / 2个数值
double median1 = getKthElementLoop(nums1, 0,nums2, 0, totalLength / 2);
// 第totalLength / 2 + 1个数值
double median2 = getKthElementLoop(nums1, 0,nums2, 0, totalLength / 2 + 1);
return (median1 + median2) / 2;
}
}
public int getKthElementLoop(int[] nums1, int startIndex1, int[] nums2, int startIndex2, int k) {
if (startIndex1 < 0 || startIndex1 >= nums1.length) {
return nums2[startIndex2 + k - 1];
}
if(startIndex2 <0 || startIndex2 >= nums2.length) {
return nums1[startIndex1 + k - 1];
}
if (k == 1) {
return Math.min(nums1[startIndex1], nums2[startIndex2]);
}
int half = k/2;
int newIndnex1 = Math.min(startIndex1 + half - 1, nums1.length - 1);
int newIndnex2 = Math.min(startIndex2 + half - 1, nums2.length - 1);
if (nums1[newIndnex1] <= nums2[newIndnex2]) {
return getKthElementLoop(nums1, newIndnex1 + 1, nums2, startIndex2, k-(newIndnex1 - startIndex1 + 1));
}else {
return getKthElementLoop(nums1, startIndex1, nums2, newIndnex2 + 1, k-(newIndnex2 - startIndex2 + 1));
}
}
3)搜索旋转排序数组
升序整数数组nums,数组中的值互不相同。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的索引,否则返回 -1 。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
提示:
1 <= nums.length <= 5000
-10^4 <= nums[i] <= 10^4
nums 中的每个值都 独一无二
nums 肯定会在某个点上旋转
【思路】:
- 遍历,找到旋转点。则:旋转点前的值大,旋转点后的值小。
- 比较 旋转点 和 target大小,分别在 旋转点左侧子序列 或 旋转点右侧子序列 二分查找target。
【微码逻辑】:
1)方式一:如果right指向比较边界外位置(right为无效索引)
- 比较序列后端,right=length,为最后一个元素的下一位置。
- 此时要注意:递归、或while循环下一轮应该是:[left,mid)【mid已比较过了,下轮作为范围外索引】、[mid+1,right)。
- 每轮递归是条件是left < right。如:
- 最后2个位置[3,4),则:mid=3,否则进入下一轮:
- [3,3),3上轮已比较过了,这轮的3是区间外位置。需要跳出。
- 或[4,4),4是区间外位置,需要跳出。
- 最后2个位置[3,4),则:mid=3,否则进入下一轮:
2)方式二:right指向边界最后一个位置(right为有效索引)
- 比较序列后端,right = length-1,为最后一个有效元素。
- 此时要注意:递归、或while循环下一轮应该是:[left,mid-1]【mid已比较过了,下轮有效索引结尾是mid-1】、[mid+1,right]。
- 每轮递归是条件是left <= right。如:
- 循环最后2个位置Index是[x , x+1],则:mid=x,x位置值不相等,则:下一轮进入:
- [x, x-1],跳出。
- 或 [x+1 , x+1],则:mid = x+1,x+1位置值不相等,则下一轮进入:
- [x+1, x]、或[x+2, x+1],跳出
- 如果最后2个位置是Index[0, 1],则:mid=0,0位置不相等,下轮是[0,-1],[1, 1]
- 循环最后2个位置Index是[x , x+1],则:mid=x,x位置值不相等,则:下一轮进入:
一、代码参考1:递归 + right为无效元素
public static int search(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int index = -1;
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] > nums[i + 1]) {
index = i + 1;
}
}
int ret = 0;
if (index == -1) {
ret = searchFor(nums, 0, nums.length, target);
} else if (target >= nums[0]) {
ret = searchFor(nums, 0, index, target);
} else {
ret = searchFor(nums, index, nums.length, target);
}
return ret;
}
public static int searchFor(int[] nums, int left, int right, int target) {
if(left < right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
return mid;
} else if (target < nums[mid]) {
return searchFor(nums, left, mid, target);
} else {
return searchFor(nums, mid + 1, right, target);
}
}
return -1;
}
二、代码参考2:递归 + right为有效元素
public int search(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int index = -1;
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] > nums[i + 1]) {
index = i;
}
}
int ret = 0;
if (index == -1) {
ret = searchFor(nums, 0, nums.length -1, target);
} else if (target >= nums[0]) {
ret = searchFor(nums, 0, index , target);
} else {
ret = searchFor(nums, index + 1, nums.length -1, target);
}
return ret;
}
public static int searchFor(int[] nums, int left, int right, int target) {
if(left <= right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
return mid;
} else if (target < nums[mid]) {
return searchFor(nums, left, mid-1, target);
} else {
return searchFor(nums, mid + 1, right, target);
}
}
return -1;
}
三、代码参考3:while循环 + right为无效元素
public static int search(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int index = -1;
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] > nums[i + 1]) {
index = i + 1;
}
}
int ret = 0;
if (index == -1) {
ret = searchFor(nums, 0, nums.length, target);
} else if (target >= nums[0]) {
ret = searchFor(nums, 0, index , target);
} else {
ret = searchFor(nums, index, nums.length, target);
}
return ret;
}
public static int searchFor(int[] nums, int left, int right, int target) {
while(left < right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
return mid;
}else if (target < nums[mid]) {
right = mid;
} else {
left = mid + 1;
}
}
return -1;
}
四、代码参考4:while循环 + right有效元素
public static int search(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int index = -1;
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] > nums[i + 1]) {
index = i;
}
}
int ret = 0;
if (index == -1) {
ret = searchFor(nums, 0, nums.length -1, target);
} else if (target >= nums[0]) {
ret = searchFor(nums, 0, index , target);
} else {
ret = searchFor(nums, index + 1, nums.length -1, target);
}
return ret;
}
public static int searchFor(int[] nums, int left, int right, int target) {
while(left <= right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
return mid;
} else if (target < nums[mid]) {
right = mid -1;
} else {
left = mid + 1;
}
}
return -1;
}
4)小张刷题计划
力扣:https://leetcode-cn.com/problems/xiao-zhang-shua-ti-ji-hua/submissions/
为了提高自己的代码能力,小张制定了 LeetCode 刷题计划,他选中了 LeetCode 题库中的 n 道题,编号从 0 到 n-1,并计划在 m 天内按照题目编号顺序刷完所有的题目(注意,小张不能用多天完成同一题)。
在小张刷题计划中,小张需要用 time[i] 的时间完成编号 i 的题目。此外,小张还可以使用场外求助功能,通过询问他的好朋友小杨题目的解法,可以省去该题的做题时间。为了防止“小张刷题计划”变成“小杨刷题计划”,小张每天最多使用一次求助。
我们定义 m 天中做题时间最多的一天耗时为 T(小杨完成的题目不计入做题总时间)。请你帮小张求出最小的 T是多少。
示例 1:
输入:time = [1,2,3,3], m = 2
输出:3
解释:第一天小张完成前三题,其中第三题找小杨帮忙;第二天完成第四题,并且找小杨帮忙。这样做题时间最多的一天花费了 3 的时间,并且这个值是最小的。
示例 2:
输入:time = [999,999,999], m = 4
输出:0
解释:在前三天中,小张每天求助小杨一次,这样他可以在三天内完成所有的题目并不花任何时间。
限制:
1 <= time.length <= 10^5
1 <= time[i] <= 10000
1 <= m <= 1000
【题解思路】:
此题最终结果每天最多耗费x时间刚好能读完所有小册、每天花费x-1时间又读不完所有小册。本质上是寻找x值。x取值范围1~10000。
1)思路一:采用x从小到大遍历的方式,知道第一个能读完所有测试的x,即为结果。
2)思路二:采用思路一的方式效率低。可以考虑采用二分法找x,要求x最终满足如上条件。
public int readBookTimes(int[] time, int m) {
int left = 0;
int right = 10000 * time.length;
while (left < right) {
int mid = (left + right)/2;
if (canReadAll(time, m, mid)) {
if (mid == 0) {
return 0;
}
if (!canReadAll(time, m, mid - 1)) {
return mid;
} else {
right = mid;
}
} else {
left = mid + 1;
}
}
return -1;
}
public boolean canReadAll(int[] times, int day, int maxTimes) {
int tmpDay = 1;
int tmpTime = 0;
int tmpMaxTime = 0;
for (int i = 0; i < times.length; i++) {
// 计算当前天读取第i本书的时间
if (times[i] > tmpMaxTime) { // 剔除第i本书,前面临时记录的剔除书时间记入总耗时
tmpTime += tmpMaxTime;
tmpMaxTime = times[i];
} else {
tmpTime += times[i];
}
// 如果加上第i本书,超时每天最大时间,则第i本书下一天在读。注这里:i要--回退。
if (tmpTime > maxTimes) {
i--;
// 清空数据,用于下一天计算
tmpTime = 0;
tmpMaxTime = 0;
// 读书天数++
tmpDay++;
if (tmpDay > day) {
return false;
}
}
}
return true;
}