二分专题讲解
二分法查找某个有序数组中的指定数字可以达到log(n)的时间复杂度。
运用二分法的前提一定是针对某个有序集合去进行操作,如果该集合非有序的,一定是不能进行二分操作的。
在目前的刷题单中,遇到用二分的场景有两大类:
1. 二分查找有序数组的某个值
在这种大类下,又分为三种情况:
- 查找有序数组某个值(数组中该值有且仅出现一次)
LeetCode704 二分查找
class Solution { public int search(int[] nums, int target) { if (nums == null || nums.length == 0) { return -1; } int n = nums.length; // 如果传进来的nums数组已经有序了,就不需要进行这一步排序操作了。反之,则需要 // Arrays.sort(nums); int l = 0; int r = n - 1; while (l <= r) { // 防止溢出 int mid = l + (r - l) / 2; if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { l = mid + 1; } else { r = mid - 1; } } return -1; } }
- 查找有序数组第一次出现的某个值(数组中该值出现的超过1 次),左区间
class Solution { public char nextGreatestLetter(char[] letters, char target) { int length = letters.length; if (target >= letters[length - 1]) { return letters[0]; } int low = 0, high = length - 1; while (low < high) { int mid = (high - low) / 2 + low; if (letters[mid] > target) { high = mid; } else { low = mid + 1; } } return letters[low]; } }
- 查找有序数组最后一次出现的某个值(数组中该值出现的超过1次),右区间
LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置
class Solution { public int[] searchRange(int[] nums, int target) { int l = binarySearchLeft(nums, target); int r = binarySearchRight(nums, target); return new int[]{l,r}; } /** * * @param nums 传入一个有序数组 * @param target 需要在数组中查找出现的目标值中最左边的一个目标值 * @return 如果target在nums中存在则返回第一次出现的下标位置,否则返回-1 */ public int binarySearchLeft(int[] nums, int target) { if (nums == null || nums.length == 0) { return -1; } int n = nums.length; // 如果传进来的nums数组已经有序了,就不需要进行这一步排序操作了。反之,则需要 // Arrays.sort(nums); int l = 0; int r = n - 1; while (l < r) { int mid = l + (r - l) / 2; if (nums[mid] > target) { r = mid - 1; } else if (nums[mid] < target){ l = mid + 1; } else { r = mid; } } // 确认是否存在:如果l不在合法范围内或者nums[l] != target,那么直接返回-1,否则返回l return (l >= n) ? -1 : (nums[l] == target ? l : -1); } /** * * @param nums 传入一个有序数组 * @param target 需要在数组中查找出现的目标值中最右边的一个目标值 * @return 如果target在nums中存在则返回最后一次出现的下标位置,否则返回-1 */ public int binarySearchRight(int[] nums, int target) { if (nums == null || nums.length == 0) { return -1; } int n = nums.length; // 如果传进来的nums数组已经有序了,就不需要进行这一步排序操作了。反之,则需要 // Arrays.sort(nums); int l = 0; int r = n - 1; while (l < r) { int mid = l + (r - l + 1) / 2; if (nums[mid] > target) { r = mid - 1; } else if (nums[mid] < target){ l = mid + 1; } else { l = mid; } } // 确认是否存在:如果l不在合法范围内或者nums[l] != target,那么直接返回-1,否则返回l return (l >= n) ? -1 : (nums[l] == target ? l : -1); } }
综上所述,我这里总结了二分查找数组中某个目标值的下标的三类情况分别对应的代码:
public class Main { public static void main(String[] args) { Main main = new Main(); int[] nums = {0,1,1,1,1,4,6,9,10}; int[] arr = {0,1,4,6,9,10}; int[] arr2 = {2,2}; int index = main.binarySearch(arr,4); int indexLeft = main.binarySearchLeft(nums, 1); int indexRight = main.binarySearchRight(arr2, 3); System.out.println(index); System.out.println(indexLeft); System.out.println(indexRight); } /** * * @param nums 传入一个有序数组,且需要保证「nums中target要么仅仅出现一次,要么没有出现」 * @param target 需要在数组中查找出现的目标值中 * @return 如果target在nums中存在则返回出现的下标位置,否则返回-1 */ public int binarySearch(int[] nums, int target) { if (nums == null || nums.length == 0) { return -1; } int n = nums.length; // 如果传进来的nums数组已经有序了,就不需要进行这一步排序操作了。反之,则需要 // Arrays.sort(nums); int l = 0; int r = n - 1; while (l < r) { int mid = l + (r - l) / 2; if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { l = mid + 1; } else { r = mid - 1; } } return -1; } /** * * @param nums 传入一个有序数组 * @param target 需要在数组中查找出现的目标值中最左边的一个目标值 * @return 如果target在nums中存在则返回第一次出现的下标位置,否则返回-1 */ public int binarySearchLeft(int[] nums, int target) { if (nums == null || nums.length == 0) { return -1; } int n = nums.length; // 如果传进来的nums数组已经有序了,就不需要进行这一步排序操作了。反之,则需要 // Arrays.sort(nums); int l = 0; int r = n - 1; while (l < r) { int mid = l + (r - l) / 2; if (nums[mid] > target) { r = mid - 1; } else if (nums[mid] < target){ l = mid + 1; } else { r = mid; } } // 确认是否存在:如果l不在合法范围内或者nums[l] != target,那么直接返回-1,否则返回l return (l >= n) ? -1 : (nums[l] == target ? l : -1); } /** * * @param nums 传入一个有序数组 * @param target 需要在数组中查找出现的目标值中最右边的一个目标值 * @return 如果target在nums中存在则返回最后一次出现的下标位置,否则返回-1 */ public int binarySearchRight(int[] nums, int target) { if (nums == null || nums.length == 0) { return -1; } int n = nums.length; // 如果传进来的nums数组已经有序了,就不需要进行这一步排序操作了。反之,则需要 // Arrays.sort(nums); int l = 0; int r = n - 1; while (l < r) { int mid = l + (r - l + 1) / 2; if (nums[mid] > target) { r = mid - 1; } else if (nums[mid] < target){ l = mid + 1; } else { l = mid; } } // 确认是否存在:如果l不在合法范围内或者nums[l] != target,那么直接返回-1,否则返回l return (l >= n) ? -1 : (nums[l] == target ? l : -1); } }
2. 二分查找限定范围内,操作限定次数下的最值问题
可以先看一道经典的题
LeetCode 875. 爱吃香蕉的珂珂
珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。
珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。
示例 1:
输入:piles = [3,6,7,11], h = 8
输出:4
示例 2:
输入:piles = [30,11,23,4,20], h = 5
输出:30
示例 3:
输入:piles = [30,11,23,4,20], h = 6
输出:23
提示:
class Solution { public int minEatingSpeed(int[] piles, int h) { int l = 1; int r = (int) (1e9); while (l <= r) { int mid = l + (r - l) / 2; if (check(piles, mid, h)) { // 如果说超过了指定的时间h,说明吃香蕉的速度还要再快一些,也就是l需要增大 l = mid + 1; } else { // 如果说没超过指定的时间h,说明吃香蕉的速度还要再慢一些,也就是r需要减小 r = mid - 1; } } // 要返回的是 h 小时内吃掉所有香蕉的最小速度 k,也就是如果k再大,那就要超过h。因此返回的是左区间的l return l; } public boolean check(int[] piles ,int k, int h) { int cnt = 0; for (int pile : piles) { cnt += (1 + (pile - 1) / k); // 向上取整和下面效果一样 // if (pile % k == 0) { // cnt += pile / k; // } else { // cnt += 1 + pile / k; // } } return cnt > h; } }
还可以做下面这些经典的二分题
https://www.luogu.com.cn/training/111#problems
P1182 数列分段 Section II
题目描述
输出格式
一个正整数,即每段和最大值最小为多少。
样例
样例输入
5 3 4 2 4 5 1
样例输出
6
提示
import java.io.*; import java.util.*; public class Main { public static boolean check(int mid, int[] arr, int m) { long sum = 0; int cnt = 0; int n = arr.length; for (int i = 0 ; i < n; i++) { if (sum + arr[i] <= mid) { sum += arr[i]; } else { cnt++; sum = arr[i]; } } return cnt >= m; } public static void main(String[] args) throws IOException { StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in))); st.nextToken(); int n = (int)st.nval; st.nextToken(); int m = (int)st.nval; int[] arr = new int[n]; int l = 0; int r = 0; for (int i = 0; i < n; i++) { st.nextToken(); arr[i] = (int)st.nval; l = Math.max(arr[i], l); r += arr[i]; } while (l <= r) { int mid = (l + r) >> 1; if (check(mid,arr,m)) { l = mid + 1; } else { r = mid - 1; } } System.out.println(l); } }
总结一下:
对于二分查找限定范围内,操作次数限定下的最值问题。