二分查找刷题总结

推荐使用闭区间的方式去做二分查找的题目

如果数量比较少,那么建议使用顺序遍历的方式

二分结束时一定有: i指向首个大于 target 的元素,j指向首个小于 target 的元素。易得当数组不包含 target 时,插入索引为i

74. 搜索二维矩阵

问题描述:从一个二维数组中搜索到target

1、mod是列值
2、这里要考虑,只有一行或者只有一列的情况,这种情况,要恒定为0

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int left = 0;
        int right = matrix.length * matrix[0].length - 1;
        if (right == 0) {
            return matrix[0][0] == target;
        }

        int mod = matrix[0].length;
        while(left <= right) {
            int mid = left + (right - left) / 2;

            int row =  mid / mod;
            int col =  mid % mod;

            if (matrix[row][col] > target) {
                right = mid - 1;
            } else if (matrix[row][col] < target) {
                left = mid + 1;
            } else {
                return true;
            }
        }

        return false;
    }
}

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        int top = 0, bottom = m - 1;
  
        // 首先确定在哪一行
        while (top <= bottom) {
            int midRow = top + (bottom - top) / 2;
            if (matrix[midRow][0] <= target && target <= matrix[midRow][n-1]) {
                // 在这一行,进行列二分
                int left = 0, right = n - 1;
                while (left <= right) {
                    int mid = left + (right - left) / 2;
                    if (matrix[midRow][mid] == target) {
                        return true;
                    } else if (matrix[midRow][mid] < target) {
                        left = mid + 1;
                    } else {
                        right = mid - 1;
                    }
                }
                return false;
            } else if (target < matrix[midRow][0]) {
                bottom = midRow - 1;
            } else {
                top = midRow + 1;
            }
        }
        return false;
    }
}

162. 寻找峰值

寻找最大值,这个也可以理解,嗯

大的一侧为什么一定有峰值?注意题目条件,在题目描述中出现了 nums[-1] = nums[n] = -∞,这就代表着 只要数组中存在一个元素比相邻元素大,那么沿着它一定可以找到一个峰值

沿着大的方向走,肯定会存在一个峰值

判断逻辑:不用 mid 和 left 比,而是和 mid+1 比,核心是「通过上坡 / 下坡确定峰值方向」,保证二分的有效性;
边界控制:循环条件 l < r,收缩规则 l=mid+1/r=mid,始终保证「峰值在 [l, r] 区间内」;
结果推导:循环终止时 l==r,区间只剩一个元素,这个元素就是峰值,因此返回 l 即可。

class Solution {
    public int findPeakElement(int[] nums) {
        int l = 0, r = nums.length - 1;
        while (l < r) {
            int mid = l + ((r - l) >> 1);
            if (nums[mid] < nums[mid + 1]) {
                l = mid + 1;
            } else {
                r = mid;
            }
        }
        return l;
    }
}

33. 搜索旋转排序数组

我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的

当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:

  • 如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [nums[l],nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
  • 如果 [mid, r] 是有序数组,且 target 的大小满足 [nums[mid+1],nums[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。

image


public int search(int[] nums, int target) {
    int lo = 0, hi = nums.length - 1, mid = 0;
    while (lo <= hi) {
        mid = lo + (hi - lo) / 2;
        if (nums[mid] == target) {
            return mid;
        }
        if (nums[mid] >= nums[lo]) { // left到mid是有序数据
            if (target >= nums[lo] && target < nums[mid]) {
                hi = mid - 1;
            } else {
                lo = mid + 1;
            }
        } else { // mid --> right是有序数组
            if (target > nums[mid] && target <= nums[hi]) {
                lo = mid + 1;
            } else {
                hi = mid - 1;
            }
        }
    }
    return -1;
}

34. 在排序数组中查找元素的第一个和最后一个位置

ACE了

class Solution {
    public int[] searchRange(int[] nums, int target) {
        return new int[] { left(nums, target), right(nums, target) };
    }

    public int left(int[] nums, int target) {
        int i = binarySearch(nums, target);
        if (i == nums.length || nums[i] != target) {
            return -1;
        }
        return i;
    }

    public int right(int[] nums, int target) {
        int i = binarySearch(nums, target + 1);
        int j = i - 1;
        if (j == -1 || nums[j] != target) {
            return -1;
        }
        return j;
    }

    public int binarySearch(int[] nums, int target) {
        int left = 0;
        int 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;
            } else {
                right = mid - 1;
            }

        }

        return left;
    }
}

153. 寻找旋转排序数组中的最小值

第二次做,思路还是错误的,这里要利用它的特点,如果mid>small,那么直接left = mid + 1, 否则 right = mid - 1;

解题总结:

1、边界条件判断

2、如果单调的,直接返回第一个元素即可

3、动态调整small,当前最small为nums[0]

4、如果大于small,那么缩小左边的边界

5、如果小于small,那么缩小右边的边界,然后更新small的值

6、最后循环结束的时候,已经记录到了small。

class Solution {
    public int findMin(int[] nums) {
        int l = 0;
        int r = nums.length - 1;

        if (nums.length == 1) {
            return nums[0];
        }

        if (nums[0] < nums[nums.length - 1]) {
            return nums[0];
        }

        int small = nums[0];

        while(l <= r) {
            int m = (l + r) / 2;
            if (nums[m] >= small) {
                l = m + 1;
            } else {
                r = m - 1;
                small = nums[m];
            }
        }

        return nums[l];
    }
}

4. 寻找两个正序数组的中位数

这个题目确实是LeetCode的经典难题(Hard级别),核心难点在于要求O(log(m+n))的时间复杂度(如果只是合并数组找中位数,O(m+n)的复杂度很简单,但不符合最优要求)。我们先从“为什么难”开始,一步步拆解代码的逻辑。

一、先明确问题:中位数的定义

中位数是把一个有序数组分成等长的两半后,“中间”的那个数:

  • 如果总长度是奇数:中位数 = 第 (len/2 + 1) 小的数(比如总长度5,第3小的数);
  • 如果总长度是偶数:中位数 = (第 len/2 小的数 + 第 len/2 + 1 小的数)/ 2(比如总长度4,第2小+第3小的平均数)。

比如:nums1=[1,3],nums2=[2] → 合并后[1,2,3] → 总长度3(奇数)→ 第2小的数是2(中位数);
nums1=[1,2],nums2=[3,4] → 合并后[1,2,3,4] → 总长度4(偶数)→ (2+3)/2=2.5(中位数)。

二、核心思路:把“找中位数”转化为“找第k小的数”

代码的关键是 getKthElement 函数(找两个有序数组中第k小的数),因为:

  • 奇数长度:找第 (totalLen/2 + 1) 小的数;
  • 偶数长度:找第 totalLen/2totalLen/2 + 1 小的数,再取平均。

为什么要这么转化?因为“找第k小”可以用二分法实现O(log(m+n))的复杂度,而直接合并数组是O(m+n),不符合最优要求。

三、拆解 getKthElement 函数(核心中的核心)

3.1 二分法的核心思想:“排除不可能的数”

找第k小的数,我们不需要合并数组,而是通过每次排除掉k/2个不可能是第k小的数,把问题规模缩小,直到k=1(直接取两个数组当前首元素的最小值)。

举个例子理解:
nums1=[1,4,7,9],nums2=[2,3,5,8],找第5小的数(k=5)。

  • 第一步:k=5 → 取k/2=2(整除)→ 看nums1的第2个元素(nums1[1]=4)、nums2的第2个元素(nums2[1]=3);
  • 比较4和3:3更小 → 说明nums2的前2个元素(2,3)都不可能是第5小的数(因为最多只有1+2=3个数比3小,3最多是第4小);
  • 排除nums2的前2个元素 → 问题变成:nums1=[1,4,7,9],nums2=[5,8],找第5-2=3小的数(k=3);
  • 第二步:k=3 → k/2=1 → 看nums1的第1个元素(1)、nums2的第1个元素(5);
  • 1更小 → 排除nums1的前1个元素 → 问题变成:nums1=[4,7,9],nums2=[5,8],找第3-1=2小的数(k=2);
  • 第三步:k=2 → k/2=1 → 看nums1的第1个元素(4)、nums2的第1个元素(5);
  • 4更小 → 排除nums1的前1个元素 → 问题变成:nums1=[7,9],nums2=[5,8],找第2-1=1小的数(k=1);
  • k=1 → 取min(7,5)=5 → 第5小的数是5(验证:合并数组[1,2,3,4,5,7,8,9],第5小确实是5)。

3.2 代码逐行拆解 getKthElement

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) {
        // 边界情况1:nums1已经排除完所有元素 → 直接取nums2的第k个元素
        if (index1 == length1) {
            return nums2[index2 + k - 1];
        }
        // 边界情况2:nums2已经排除完所有元素 → 直接取nums1的第k个元素
        if (index2 == length2) {
            return nums1[index1 + k - 1];
        }
        // 边界情况3:k=1 → 直接取两个数组当前首元素的最小值
        if (k == 1) {
            return Math.min(nums1[index1], nums2[index2]);
        }
  
        // 正常情况:计算要比较的“候选位置”
        int half = k / 2; // 每次排除k/2个元素
        // 候选位置 = 当前起始位置 + half - 1(因为数组索引从0开始)
        // 注意:要防止候选位置超过数组长度(比如nums1只剩1个元素,half=2)
        int newIndex1 = Math.min(index1 + half, length1) - 1;
        int newIndex2 = Math.min(index2 + half, length2) - 1;
        // 两个候选值
        int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];

        // 比较候选值,排除更小的那个数组的前半部分
        if (pivot1 <= pivot2) {
            // 排除nums1中index1到newIndex1的元素 → 共(newIndex1 - index1 + 1)个
            k -= (newIndex1 - index1 + 1);
            // 更新nums1的起始位置(排除后的下一个位置)
            index1 = newIndex1 + 1;
        } else {
            // 排除nums2中index2到newIndex2的元素
            k -= (newIndex2 - index2 + 1);
            // 更新nums2的起始位置
            index2 = newIndex2 + 1;
        }
    }
}

3.3 边界情况的解释

  • 边界1/2:比如nums1=[1,2],nums2=[3,4,5],k=4 → 排除nums1的2个元素后,nums1已经空了 → 直接取nums2的第4-2=2个元素(nums2[0+2-1]=nums2[1]=4);
  • 边界3:k=1时,两个数组的当前首元素中更小的那个,就是第1小的数(比如nums1=[4], nums2=[5] → min(4,5)=4)。

3.4 关键细节:为什么 newIndex1 = Math.min(index1 + half, length1) - 1

比如nums1=[1,3](length1=2),index1=0,half=3(k=6)→ index1+half=3,超过length1=2 → 所以newIndex1取length1-1=1(nums1的最后一个元素),避免数组越界。

四、拆解主函数 findMedianSortedArrays

主函数的逻辑很简单,就是把“找中位数”转化为“找第k小的数”:

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int length1 = nums1.length, length2 = nums2.length;
    int totalLength = length1 + length2;
    if (totalLength % 2 == 1) { // 奇数长度
        int midIndex = totalLength / 2;
        // 第(midIndex + 1)小的数(比如totalLength=5,midIndex=2 → 第3小)
        double median = getKthElement(nums1, nums2, midIndex + 1);
        return median;
    } else { // 偶数长度
        int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
        // 第(midIndex1 + 1)小 和 第(midIndex2 + 1)小 的平均数
        double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
        return median;
    }
}

举个例子验证:
nums1=[1,2],nums2=[3,4] → totalLength=4(偶数)→ midIndex1=1,midIndex2=2 → 找第2小和第3小的数(2和3)→ (2+3)/2=2.5,正确。

五、整体流程总结(用一个例子串起来)

以nums1=[1,3],nums2=[2]为例:

  1. totalLength=3(奇数)→ midIndex=1 → 找第2小的数;
  2. 调用getKthElement(nums1, nums2, 2):
    • index1=0,index2=0,k=2;
    • half=1 → newIndex1=0(0+1-1),newIndex2=0(0+1-1);
    • pivot1=1,pivot2=2 → 1<=2 → 排除nums1的前1个元素(k=2-1=1,index1=1);
    • 现在k=1 → 返回min(nums1[1]=3, nums2[0]=2) → 2;
  3. 主函数返回2 → 正确。

六、为什么这个算法是O(log(m+n))?

每次循环都会把k缩小一半(排除k/2个元素),直到k=1。而k的最大值是m+n,所以循环次数是log2(m+n),也就是时间复杂度O(log(m+n)),符合最优要求。

七、常见疑问解答

1. 为什么排除的是“更小的候选值所在数组的前半部分”?

比如pivot1 <= pivot2 → 说明nums1的前half个元素都比pivot2小,这些元素最多只能是第k-1小的数(因为nums2的前half个元素已经比pivot1大),所以不可能是第k小的数,直接排除。

2. 为什么k要减去排除的元素个数?

因为排除的元素都是比第k小的数更小的数,所以剩下的问题是“在剩下的元素中找第k-排除个数小的数”。

3. 有没有更简单的方法?

有,但复杂度更高:把两个数组合并成一个有序数组,然后直接取中位数(O(m+n))。但题目隐含要求最优复杂度(O(log(m+n))),所以必须用二分法。

八、简化记忆

  1. 中位数 = 奇数取第(len/2+1)小,偶数取(len/2和len/2+1)小的平均;
  2. 找第k小:每次排除k/2个不可能的数,直到k=1;
  3. 排除规则:比较两个数组的第k/2个元素,排除更小的那个数组的前k/2个元素;
  4. 边界:数组空了直接取另一个数组的第k个元素,k=1取最小值。

这个题目确实需要多刷几遍,建议先手动模拟2-3个例子(比如上面的例子),再对照代码走一遍,就能慢慢理解二分排除的逻辑了。

posted @ 2025-11-23 21:57  coder江  阅读(135)  评论(0)    收藏  举报