二分查找法模板
https://leetcode-cn.com/leetbook/read/binary-search/x6q6fi/
模板一
二分查找法的标准模板适用于只需要寻找某一个符合条件的值 :
初始化 right = nums.length - 1
更新区间时:left = mid + 1
;right = mid - 1
查找的区间为 [left, right]
,左闭右闭
跳出 while 循环的条件是 left == right + 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public int binarySearch (int [] nums, int target) { if (nums == null || nums.length == 0 ) return -1 ; int left = 0 ; int right = nums.length - 1 ; int mid; while (left <= right) { mid = left + (right - left >> 1 ); if (nums[mid] == target) { return mid; } else if (nums[mid] > target) { right = mid - 1 ; } else if (nums[mid] < target) { left = mid + 1 ; } } return -1 ; }
模板二
二分查找法的标准模板适用于寻找符合条件的最左边界 :
初始化 right = nums.length
更新区间时:left = mid + 1
;right = mid
查找的区间为 [left, right)
,左闭右开
跳出 while 循环的条件是 left == right
之所以更新右指针为 right = mid
:
一是因为要求解最左边界,则在 right 更新时需要保存着当前满足条件的值 (该值左侧可能还有满足条件的值,需要用 right 保存着当前满足的值),最后跳出循环时其位置就是最左满足条件的值
二是因为查找区间为左闭右开 ,不包含 right。因此在发现 mid 位置满足条件时,接下来的二分过程中,要令 right = mid
,这样 mid 因为右开而不被考虑,mid - 1 能正常被考虑。否则若 right = mid - 1
,则下一轮的二分中 mid - 1 就没有被考虑在内,从而漏算了该值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public int binarySearch (int [] nums, int target) { if (nums == null || nums.length == 0 ) return -1 ; int left = 0 ; int right = nums.length; int mid; while (left < right) { mid = left + (right - left >> 1 ); if (nums[mid] >= target) { right = mid; } else if (nums[mid] < target) { left = mid + 1 ; } } return nums[left] == target ? left : -1 ; }
许多求解最左/由边界的题目其实都可以使用模板一,只需要额外借助一个变量 ans,在二分过程中记录下满足条件的值(不要 return),然后继续二分直到跳出循环,这样到最后 ans 就保存了最左/右边界
模板二适合于在遍历过程中,mid 位置很可能就是符合题意的答案,但是因为在二分过程中没有写“遇到答案立即返回”的代码,因此需要考虑到 mid 就是答案的情况,在 right 更新时要包含 mid,不能将其跳过。
关于二分查找法模板的更多细节:https://blog.csdn.net/xiao_jj_jj/article/details/106018702
二分查找法常见题目
可以使用二分法的题目的一个特点:单调性
搜索旋转排序数组
https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/sou-suo-xuan-zhuan-pai-xu-shu-zu-by-leetcode-solut/
整数数组 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 。
思路:这道题的关键是找到单调性 。
最关键的一步就是通过 nums[mid] 和 nums[0] 的大小差别,判断 mid 左侧还是右侧是有序数组(也就是找单调性)。 我们只在有序数组里判断 target 是否在其内,不在其中,那肯定在另一侧无序数组里
通过这点来进行二分查找。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 class Solution { public int search (int [] nums, int target) { if (nums == null || nums.length < 1 ) { return -1 ; } int left = 0 ; int right = nums.length - 1 ; int mid; while (left <= right) { mid = left + (right - left >> 1 ); if (nums[mid] == target) { return mid; } if (nums[0 ] <= nums[mid]) { if (target >= nums[left] && target < nums[mid]) { right = mid - 1 ; } else { left = mid + 1 ; } } else { if (target > nums[mid] && target <= nums[right]) { left = mid + 1 ; } else { right = mid - 1 ; } } } return -1 ; } }
旋转数组的最小数字
https://leetcode-cn.com/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/solution/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-by-leetcode-s/
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
该问题的思路与上一题相似,都是先判断单调性,然后再进行二分,区别在于,这里是判断与 right 值的关系从而确定单调性。上一题是一直判断与 0 位置的关系从而确定单调性。
上一题是找目标值位置,这一题是找最小值
详细分析见官方题解。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public int minArrayTwoSperate (int [] numbers) { int left = 0 ; int right = numbers.length - 1 ; while (left < right) { int mid = left + ((right - left) >> 1 ); if (numbers[mid] > numbers[right]) { left = mid + 1 ; } else if (numbers[mid] < numbers[right]) { right = mid; } else { right--; } } return numbers[left]; }
在排序数组中查找数字
剑指 Offer 53 - I. 在排序数组中查找数字 I :统计一个数字在排序数组中出现的次数。
版本1:两次二分 + 单独变量保存左右边界:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public int search (int [] nums, int target) { if (nums == null || nums.length == 0 ) { return 0 ; } if (nums[0 ] > target || nums[nums.length - 1 ] < target) { return 0 ; } int left = 0 ; int right = nums.length - 1 ; int mid = 0 ; int leftBound = -1 ; int rightBound = -1 ; while (left <= right) { mid = left + (right - left >> 1 ); if (nums[mid] == target) { leftBound = mid; right = mid - 1 ; } else if (nums[mid] > target) { right = mid - 1 ; } else { left = mid + 1 ; } } if (left == -1 ) { return 0 ; } left = 0 ; right = nums.length - 1 ; mid = 0 ; while (left <= right) { mid = left + (right - left >> 1 ); if (nums[mid] == target) { rightBound = mid; left = mid + 1 ; } else if (nums[mid] > target) { right = mid - 1 ; } else { left = mid + 1 ; } } if (leftBound != -1 && rightBound != -1 ) { return rightBound - leftBound + 1 ; } else { return 0 ; } }
版本2:合并等于与小于或大于:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public int search (int [] nums, int target) { if (nums == null || nums.length == 0 ) { return 0 ; } if (nums[0 ] > target || nums[nums.length - 1 ] < target) { return 0 ; } int left = 0 ; int right = nums.length - 1 ; int mid = 0 ; int leftBound = -1 ; int rightBound = -1 ; while (left <= right) { mid = left + (right - left >> 1 ); if (nums[mid] >= target) { leftBound = mid; right = mid - 1 ; } else { left = mid + 1 ; } } if (left == -1 ) { return 0 ; } left = 0 ; right = nums.length - 1 ; mid = 0 ; while (left <= right) { mid = left + (right - left >> 1 ); if (nums[mid] <= target) { rightBound = mid; left = mid + 1 ; } else if (nums[mid] > target) { right = mid - 1 ; } } if (leftBound != -1 && rightBound != -1 ) { return rightBound - leftBound + 1 ; } else { return 0 ; } }
版本3:复用。可以参考 34. 在排序数组中查找元素的第一个和最后一个位置 与 剑指 Offer 53 - I. 在排序数组中查找数字 I 的题解
0~n-1中缺失的数字
剑指 Offer 53 - II. 0~n-1中缺失的数字 :一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
数组可以按照以下规则划分为两部分:
左子数组 :nums[i] = i
右子数组 :nums[i] ≠ i
缺失的数字等于 “右子数组的首位元素” 对应的索引;因此考虑使用二分法查找 “右子数组的首位元素” 。
在二分循环时:
若 nums[m] = m
,则 “右子数组的首位元素 ” 一定在闭区间 [m + 1, right]
中,因此执行 left = m + 1
若 nums[m] ≠ m
,则 “左子数组的末位元素 ” 一定在闭区间 [left, m - 1]
中,因此执行 right = m - 1
跳出循环时,变量 left
和 right
分别指向 “右子数组的首位元素” 和 “左子数组的末位元素”,并且 left= right + 1
。因此返回 left 即可。
这种思想非常好,适用于许多类型的二分问题。将整个数组分为两个区域:左子数组和右子数组,最后退出循环时
left
指向右子数组的首位元素,更新时 left = mid + 1
right
指向左子数组的末位元素,更新时 right = mid - 1
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int missingNumber (int [] nums) { if (nums == null || nums.length == 0 ) { return 0 ; } int left = 0 ; int right = nums.length - 1 ; int mid = 0 ; while (left <= right) { mid = left + (right - left >> 1 ); if (nums[mid] == mid) { left = mid + 1 ; } else { right = mid - 1 ; } } return left; }
寻找峰值
162. 寻找峰值 :峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums
,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。你可以假设 nums[-1] = nums[n] = -∞
。
思路:当 nums[mid]
比左右都大,就说明他是峰值,直接返回,否则向更大的那一侧二分。注意,此时的二分范围是 [1, n-1]
,要把两侧的给排除掉。注意 :退出循环时需要额外判断一下 left
和 right
谁更大
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public int findPeakElement (int [] nums) { if (nums == null || nums.length == 0 ) { return -1 ; } if (nums.length == 1 ) { return 0 ; } else if (nums.length == 2 ) { return nums[0 ] > nums[1 ] ? 0 : 1 ; } int left = 1 ; int right = nums.length - 2 ; int mid = 1 ; while (left <= right) { mid = left + (right - left >> 1 ); if (nums[mid] > nums[mid - 1 ] && nums[mid] > nums[mid + 1 ]) { return mid; } else if (nums[mid] < nums[mid - 1 ]) { right = mid - 1 ; } else { left = mid + 1 ; } } return nums[left] > nums[right] ? left : right; }
题解思路:使用模板二的思路,并不在找到时立即返回,而是最后再返回。该思路将判断左右改为只判断 nums[mid]
和 nums[mid + 1]
,不需要判断左侧元素。代码更加简洁。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public int findPeakElement (int [] nums) { int left = 0 ; int right = nums.length - 1 ; int mid; while (left < right) { mid = left + (right - left >> 1 ); if (nums[mid] < nums[mid+1 ]) { left = mid + 1 ; } else { right = mid; } } return left; }
step sum
step sum 的定义:比如 680,680 + 68 + 6 = 754,则 680 的 step sum 为 754
给定一个正整数 target,判断他是否是某个数的 step sum,若是返回 true,若不是返回 false。
思路:某个数 x 的 stepSum(x) 肯定满足 x <= stepSum(x)
,满足单调性。因此可以使用二分法,从区间 [0, target],在其内不断二分,判断中点 mid 的 stepSum(mid)
与 target 的关系。
若stepSum(mid) < target
,则结果值 x 肯定在 mid 的右边
若stepSum(mid) > target
,则结果值 x 肯定在 mid 的左边
若stepSum(mid) == target
,则找到了结果值 x == mid
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public boolean isStepSum (int target) { int left = 0 ; int right = target; int mid; while (left <= right) { mid = left + (right - left >> 1 ); if (stepSum(mid) < target) { left = mid + 1 ; } else if (stepSum(mid) > target) { right = mid - 1 ; } else { return true ; } } return false ; } public int stepSum (int x) { int sum = 0 ; while (x != 0 ) { sum += x; x /= 10 ; } return sum; }
画家问题
题目:给定一个整型数组 nums,数组中的每个值都是正数,表示完成一幅画作需要的时间,在给定一个整数 num 表示画匠的数量,每个画匠只能画连在一起的画作。所有的画家并行工作,返回完成所有的画作需要的最少时间。
类似题目:410. 分割数组的最大值
注意:题目给的画家数 m 其实并不一定是最优分配情况下的画家数 ,而可能比最优数要大。例如在 m = 4 时的整体最小作画耗时和 m = 3 时的整体最小作画耗时可能是相等的,这一点体现在代码中的 need(nums, mid) <= m
里,很可能存在比当前最小作画耗时更短的情况下,需要的画家数量还更少,这种情况当然是满足题意的。
题目中的两个隐形要求 :
需要的画家数不能超过题目给定的画家数 m
整体的最小作画耗时 h
要尽可能小
整体最小作画耗时 h
的分布区间:
区间越往左侧,h
越小,需要的画家数量也越多,但可能不满足要求1(最小的 h
等于1)
区间越往右侧,h
越大,需要的画家数量也越少,画家数量是可以满足题目要求,但是需要的耗时太大,可能不满足要求2(最大的 h
等于整个数组的累计和,代表一个画家画完所有画,是最费时的一种情况)
题目的目标就是在这个区间中尽可能左侧 的位置(代表 h
尽可能小),找到一个满足要求1:画家数量不超过 m
的作画耗时值。
可以从上述规律中找出单调性 :需要的画家越多,最大耗时越小。因此可以用二分法解决该问题。将求解最短作画耗时的问题转化为二分地判断当前最大作画耗时的情况下,需要几个画家 :
如果需要的画家多于题目给定的 m
(不满足要求1),则说明当前要求的最大作画耗时太小了(太靠近区间左侧了),那么就二分增加该时间限制(向区间右侧二分);
如果需要的画家小于等于题目给定的 m
(满足要求1),则当前的最大作画耗时是满足要求1的(更靠近区间右侧),但可能还不是最优的,还可能在区间左侧有一个更小的时间也是满足该时间对应的画家数量也是小于 m
的(要求1)。因此先记录下来当前时间值 ans
,然后继续向左侧二分;
直到不满足 left <= right
,二分结束,返回此时记录的时间值 ans
,就是既满足要求1,又满足要求2的答案值
这是一种逆向思维 ,将求时间限制的问题转换为:求哪种时间限制的情况下,需要的画家满足题目给定的条件。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 public int splitArray (int [] nums, int m) { int sum = 0 ; for (int i = 0 ; i < nums.length; i++) { sum += nums[i]; } int left = 1 ; int right = sum; int mid; int ans = 0 ; while (left <= right) { mid = left + (right - left >> 1 ); if (need(nums, mid) <= m) { ans = mid; right = mid - 1 ; } else { left = mid + 1 ; } } return ans; } public int need (int [] nums, int h) { for (int i = 0 ; i < nums.length; i++) { if (nums[i] > h) { return Integer.MAX_VALUE; } } int parts = 1 ; int currTime = nums[0 ]; for (int i = 1 ; i < nums.length; i++) { if (currTime + nums[i] > h) { parts++; currTime = nums[i]; } else { currTime += nums[i]; } } return parts; }
峰值元素
https://leetcode-cn.com/problems/find-peak-element/solution/xun-zhao-feng-zhi-by-leetcode-solution-96sj/
该方法适用于模板二。
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution { public int findPeakElement (int [] nums) { int left = 0 ; int right = nums.length - 1 ; int mid; while (left < right) { mid = left + (right - left >> 1 ); if (nums[mid] < nums[mid+1 ]) { left = mid + 1 ; } else { right = mid; } } return left; } }
找到 K 个最接近的元素
658. 找到 K 个最接近的元素 :给定一个排序好的数组 arr
,两个整数 k
和 x
,从数组中找到最靠近 x
(两数之差最小)的 k
个数。返回的结果必须要是按升序排好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 class Solution { public List<Integer> findClosestElements (int [] arr, int k, int x) { if (arr == null && arr.length < k) { return null ; } int [] nums = arr; int left = 0 ; int right = arr.length - 1 ; int mid = 0 ; while (left <= right) { mid = left + (right - left >> 1 ); if (nums[mid] == x) { break ; } else if (nums[mid] < x) { left = mid + 1 ; } else if (nums[mid] > x) { right = mid - 1 ; } } int l = 0 ; int r = 0 ; Queue<Integer> heap = new PriorityQueue<>(); if (nums[mid] == x) { heap.add(nums[mid]); k--; l = mid - 1 ; r = mid + 1 ; } else if (nums[mid] < x) { l = mid; r = mid + 1 ; } else if (nums[mid] > x) { l = mid - 1 ; r = mid; } while (k-- > 0 ) { int lDiff = 1 ; int rDiff = -1 ; if (l >= 0 ) { lDiff = nums[l] - x; } if (r < nums.length) { rDiff = nums[r] - x; } if (lDiff <= 0 && rDiff >= 0 ) { if (Math.abs(lDiff) <= Math.abs(rDiff)) { heap.add(nums[l]); l--; } else { heap.add(nums[r]); r++; } } else if (lDiff == 1 && rDiff != -1 ) { heap.add(nums[r]); r++; } else if (rDiff == -1 && lDiff != 1 ) { heap.add(nums[l]); l--; } } List<Integer> list = new ArrayList<>(); while (!heap.isEmpty()) { list.add(heap.poll()); } return list; int leftindex = index-1 ; int rightindex = index+1 ; while (rightindex-leftindex-1 <k){ if (leftindex<0 ){ rightindex++; continue ; } if (rightindex>=len){ leftindex--; continue ; } if (x-arr[leftindex]<=arr[rightindex]-x) leftindex--; else rightindex++; } } }