二分查找

@[toc] | 题目 | 备注 | | ------------------------------------------------------------ | ---------------------------- | | [153.寻找旋转排序数组中的最小值](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/description/)(补充题) | 查找左边界 | | [33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array) | 查找左边界+标准二分 | | [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/) | 查找左边界+查找右边界 | | [240. 搜索二维矩阵 II](https://leetcode-cn.com/problems/search-a-2d-matrix-ii) | 从右上角出发开始遍历 | | [4. 寻找两个正序数组的中位数](https://leetcode-cn.com/problems/median-of-two-sorted-arrays) | 不好理解,可选择性放弃 | | [287. 寻找重复数](https://leetcode-cn.com/problems/find-the-duplicate-number) | 收缩的不是下标,而是具体数值 | ## 153.寻找旋转排序数组中的最小值 **题目**:https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/description/ **思路**: 题目中所说的旋转次数啥的不要在意,其实目的就是告诉你这个数组是部分有序的,并且题目还指明了不存在重复元素,这不就正好对应了二分查找边界的第一种情况~ 不过这个题目有一点需要考虑的是,就是它没有 target,没有 target 我怎么去进行比较? 这样我们人为地去设定一个 target: - 我们考虑数组中的最后一个元素 x:在最小值右侧的元素(不包括最后一个元素本身),它们的值一定都严格小于 x;而在最小值左侧的元素,它们的值一定都严格大于 x。因此,我们可以根据这一条性质,通过二分查找的方法找出最小值。 ![img](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/image-20220826223027064.png) **示例**: 以示例 `nums = [4,5,6,7,0,1,2]` 为例: 初始状态 - 范围:`left = 0`,`right = 6`,`mid = 3`(值为 `7`)。 - 比较:`nums[mid] = 7` > `nums[right] = 2` ➔ 最小值在右半部分,更新 `left = mid + 1 = 4`。 第二次循环 - 范围:`left = 4`,`right = 6`,`mid = 5`(值为 `1`)。 - 比较:`nums[mid] = 1` ≤ `nums[right] = 2` ➔ 最小值在左半部分或 `mid` 处,更新 `right = mid = 5`。 第三次循环 - 范围:`left = 4`,`right = 5`,`mid = 4`(值为 `0`)。 - 比较:`nums[mid] = 0` ≤ `nums[right] = 1` ➔ 更新 `right = mid = 4`。 终止条件 - `left == right`,循环结束,返回 `nums[left] = 0`。 **题解**: ```java class Solution { public int findMin(int[] nums) { int left = 0; int right = nums.length - 1; while (left < right) { int target = nums[right]; int mid = left + ((right - left) >> 1); // 收缩左边界 if (nums[mid] > target) { // 说明 mid 在最小值的左边(最小值在 mid 的右边) left = mid + 1; } else { // 收缩右边界(说明 mid 在最小值的右边,最小值在 mid 的左边) right = mid; } } return nums[left]; } } ``` ## 33.搜索旋转排序数组 **题目**:https://leetcode.cn/problems/search-in-rotated-sorted-array/description/ **思路**: 先找最小值,把一个部分有序的数组分成两个有序的子数组,然后在这俩子数组中做二分查找。注意特殊 case:最小值等于 taget、最小值是 nums[0] **题解**: ```java class Solution { public int search(int[] nums, int target) { if (nums == null || nums.length == 0) { return -1; } // 找到旋转数组中的最小值 int index = findMin(nums); // 特殊情况:如果最小值 == target,直接返回 if (nums[index] == target) { return index; } // 特殊情况:如果最小值的下标为 0,表示 nums 全局有序,在 nums[0, nums.length - 1] 上做标准二分查找 if (index == 0) { return find(nums, target, 0, nums.length - 1); } // target 在前半部分的有序子数组中 if (target >= nums[0] && target <= nums[index - 1]) { return find(nums, target, 0, index - 1); } // target 在后半部分的有序子数组中 return find(nums, target, index + 1, nums.length - 1); } // 找到 nums 旋转数组中的最小值 private int findMin(int[] nums) { int left = 0; int right = nums.length - 1; while (left < right) { int mid = left + ((right - left) >> 1); if (nums[mid] > nums[right]) { left = mid + 1; } else { right = mid; } } return left; } // nums[left, right] 中查找 target private int find(int[] nums, int target, int left, int right) { while (left <= right) { int mid = left + ((right - left) >> 1); if (nums[mid] == target) { return mid; } else if (nums[mid] > target) { right = mid - 1; } else { left = mid + 1; } } return -1; } } ``` ## 34.在排序数组中查找元素的第一个和最后一个位置 **题目**:https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/description/ **思路**: 查左边界 + 查右边界,所谓查左边界的意思就是,即使 nums[right] == target 了,还要继续往左查直到 target 首次出现(相当于不断收缩右指针) 需要注意左右边界 mid 取值问题,left + (right - left) / 2 偏左,left + (right - left) / 2 + 1偏右 具体步骤如下: 1. 左边界查找: - 使用二分查找,找到第一个等于 `target` 的元素。 - 若中间元素 `< target`,左边界向右收缩;否则右边界向左收缩(即使相等也要向左收缩,这样才能找到左边界)。 - 最终检查左边界是否有效。 2. 右边界查找: - 在左边界有效的前提下,再次使用二分查找。 - 中间元素计算时向右取整,避免死循环。 - 若中间元素 `> target`,右边界向左收缩;否则左边界向右收缩(即使相等也要向右收缩,这样才能找到右边界)。 3. 提前终止优化: - 若左边界未找到,直接返回 `[-1, -1]`,避免无效的右边界查找。 **题解**: ```java class Solution { public int[] searchRange(int[] nums, int target) { int[] res = new int[]{-1, -1}; if (nums == null || nums.length == 0) return res; // 查找左边界 int left = 0, right = nums.length - 1; while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] < target) { left = mid + 1; } else { right = mid; } } if (nums[left] != target) return res; // 提前终止 res[0] = left; // 查找右边界 right = nums.length - 1; while (left < right) { int mid = left + (right - left) / 2 + 1; // 向右取整 if (nums[mid] > target) { right = mid - 1; } else { left = mid; } } res[1] = right; return res; } } ``` ## 240.搜索二维矩阵 II **题目**:https://leetcode.cn/problems/search-a-2d-matrix-ii/description/ **思路**: 数组从左到右和从上到下都是升序的,如果从右上角出发开始遍历呢? 会发现每次都是向左数字会变小,向下数字会变大,有点和二分查找树相似。二分查找树的话,是向左数字变小,向右数字变大。 所以我们可以把 target 和当前值比较: - 如果 target 的值大于当前值,那么就向下走 - 如果 target 的值小于当前值,那么就向左走 - 如果相等的话,直接返回 true 也可以换个角度思考 如果 target 的值小于当前值,也就意味着当前值所在的列肯定不会存在 target 了,可以把当前列去掉,从新的右上角的值开始遍历。 同理,如果 target 的值大于当前值,也就意味着当前值所在的行肯定不会存在 target 了,可以把当前行去掉,从新的右上角的值开始遍历。 总结:每行做二分就可以,但是只用到了行有序,没有用到列有序。可以考虑从右上角开始遍历,往左就是变小,往下就是变大,类似于二叉搜索树 **题解**: ```java class Solution { public boolean searchMatrix(int[][] matrix, int target) { int x = 0; int y = matrix[0].length - 1; while (x <= matrix.length - 1 && y >= 0) { if (matrix[x][y] == target) { return true; } else if (matrix[x][y] > target) { y --; } else { x ++; } } return false; } } ``` ## 4.寻找两个正序数组的中位数 **题目**:https://leetcode.cn/problems/median-of-two-sorted-arrays/description/ **思路**: 核心思路是将其转为求两个正序数组中第 k 大的数。可以直接看官方题解:https://leetcode.cn/problems/median-of-two-sorted-arrays/solution/xun-zhao-liang-ge-you-xu-shu-zu-de-zhong-wei-s-114/ - 要找到有序数组 nums1 和 nums2 的第 k 小的元素,我们可以比较 nums1[k/2 - 1] 和 nums2[k/2 -1]。nums1[k/2 - 1] 和 nums2[k/2 - 1] 的前面(不包括他们本身)都有 k/2 - 1 个元素 - 对于 nums1[k/2 - 1] 和 nums2[k/2 - 1] 中的最小值,最多会有 (k/2 - 1) + (k/2 - 1) = k - 2 个元素比他小,那么他就肯定不是第 k 小的数了! **题解**: ```java class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int len = nums1.length + nums2.length; // 奇数, 中位数是第 len/2 + 1 个数 if (len % 2 == 1) { int k = len / 2 + 1; return findK(nums1, nums2, k); } // 偶数,中位数是第 len/2 和 第 len/2 + 1 个数 int k1 = len / 2; int k2 = len / 2 + 1; return (findK(nums1, nums2, k1) + findK(nums1, nums2, k2)) / 2; } // 返回 nums1 和 nums2 中第 k 小的数的值 private double findK(int[] nums1, int[] nums2, int k) { // nums1 的下标 int index1 = 0; // nums2 d int index2 = 0; while (true) { // nums1 为空,直接 nums2 的第 k 个数就行 if (index1 == nums1.length) { return nums2[index2 + k - 1]; } // nums2 为空,直接 nums1 的第 k 个数就行 if (index2 == nums2.length) { return nums1[index1 + k - 1]; } if (k == 1) { return Math.min(nums1[index1], nums2[index2]); } // nums1 和 nums2 分别取 k/2 个数 int half = k / 2; // nums1[index1, newIndex1] int newIndex1 = Math.min(index1 + half - 1, nums1.length - 1); // nums2[index2, newIndex2] int newIndex2 = Math.min(index2 + half - 1, nums2.length - 1); if (nums1[newIndex1] <= nums2[newIndex2]) { // 第 k 小的数肯定不在 [index1, newIndex1] k -= newIndex1 - index1 + 1; index1 = newIndex1 + 1; } else { // 第 k 小的数肯定不在 [index2, newIndex2] k -= newIndex2 - index2 + 1; index2 = newIndex2 + 1; } } } } ``` ## 287.寻找重复数 **题目**:https://leetcode.cn/problems/find-the-duplicate-number/description/ **思路**: 二分查找数值范围 1. 数值范围二分:将数值范围 `[1, n]` 划分为左右两半,统计左半范围内元素出现的次数。 - 若次数超过左半区间的长度,说明重复数在左半部分。 - 否则,重复数在右半部分。 2. 逐步缩小范围:通过不断二分数值范围,最终锁定重复数。 **示例**: 以示例 `nums = [1, 3, 4, 2, 2]`(`n=4`)为例: 步骤 1:初始范围 [1, 4] - 中间值 `mid = (1+4)/2 = 2`,统计数值在 `[1, 2]` 范围内的元素个数:`1, 2, 2` ➔ 共 3 个。 - 比较:实际个数(3)> 理论个数(2),说明重复数在 `[1, 2]` 区间。 步骤 2:缩小范围至 [1, 2] - 中间值 `mid = (1+2)/2 = 1`,统计数值在 `[1, 1]` 范围内的元素个数:`1` ➔ 共 1 个。 - 比较:实际个数(1)= 理论个数(1),说明重复数在右半部分 `[2, 2]`。 步骤 3:锁定重复数 - 范围缩小至 `min = 2`, `max = 2`,找到重复数 `2`。 **题解**: ```java class Solution { public int findDuplicate(int[] nums) { int min = 1; // 数值范围左边界 int max = nums.length - 1; // 数值范围右边界(n) while (min < max) { int mid = (min + max) / 2; int cnt = 0; // 统计数值在 [min, mid] 范围内的元素个数 for (int num : nums) { if (num >= min && num <= mid) { cnt++; } } if (cnt > mid - min + 1) { max = mid; // 重复数在左半部分 } else { min = mid + 1; // 重复数在右半部分 } } return min; // 最终 min == max,即为重复数 } } ```