Skip to content

二分查找

更新: 9/19/2025 字数: 0 字 时长: 0 分钟

搜索插入位置

java
/**
 * <p>此方法解决“搜索插入位置”问题,采用经典的<strong>二分查找(Binary Search)算法</strong>。</p>
 * <p>核心思想:题目要求时间复杂度为 O(log n),且数组已排序,这明确指示我们要使用二分查找。
 * 这个问题的本质是找到第一个大于或等于 {@code target} 的元素的索引。如果 {@code target} 不存在,
 * 这个索引就是它的插入位置;如果 {@code target} 存在,这个索引就是它第一次出现的(或唯一存在的)位置。</p>
 *
 * <h3>算法步骤:</h3>
 * <ol>
 *   <li><strong>初始化指针和答案变量:</strong>
 *     <ul>
 *       <li>{@code left = 0}:查找范围的左边界,初始为数组的第一个索引。</li>
 *       <li>{@code right = nums.length - 1}:查找范围的右边界,初始为数组的最后一个索引。</li>
 *       <li>{@code ans = nums.length}:用于记录最终的插入位置。初始化为 {@code nums.length},
 *         这是为了处理 {@code target} 大于数组中所有元素的情况(即 {@code target} 应插入到数组末尾)。</li>
 *     </ul>
 *   </li>
 *   <li><strong>二分查找循环:</strong>
 *     <ul>
 *       <li>{@code while (left <= right)}:当左边界小于或等于右边界时,循环继续。
 *         这个条件保证了我们能够检查到 {@code left == right} 这种情况,甚至在 {@code left} 最终指向插入位置后 {@code left > right} 才结束。</li>
 *       <li>{@code mid = left + (right - left) / 2;}:计算中间索引。这种写法可以避免 {@code left + right} 溢出。</li>
 *       <li><strong>比较 {@code nums[mid]} 与 {@code target}:</strong>
 *         <ul>
 *           <li>{@code if (nums[mid] >= target)}:
 *             <ul>
 *               <li>这表示 {@code nums[mid]} <strong>可能</strong>是 {@code target} 的插入位置,或者 {@code target} 在 {@code mid} 之前(包括 {@code mid} 本身)。
 *                 我们找到了一个候选值,所以更新 {@code ans = mid}。</li>
 *               <li>由于我们寻找的是“第一个”大于或等于 {@code target} 的位置,所以我们需要尝试在左半部分继续查找更小的索引。
 *                 {@code right = mid - 1;}</li>
 *             </ul>
 *           </li>
 *           <li>{@code else (nums[mid] < target)}:
 *             <ul>
 *               <li>这表示 {@code nums[mid]} 太小了,{@code target} 肯定在 {@code mid} 之后。
 *                 所以,在右半部分查找。</li>
 *               <li>{@code left = mid + 1;}</li>
 *             </ul>
 *           </li>
 *         </ul>
 *       </li>
 *     </ul>
 *   </li>
 *   <li><strong>返回结果:</strong>
 *     <ul>
 *       <li>循环结束后,{@code ans} 变量将存储 {@code target} 的正确插入位置。</li>
 *     </ul>
 *   </li>
 * </ol>
 *
 * <h3>性能分析:</h3>
 * <ul>
 *   <li><strong>时间复杂度:</strong> {@code O(log n)}。
 *     <ul>
 *       <li>二分查找在每次迭代中将搜索空间减半,因此对数时间复杂度。</li>
 *     </ul>
 *   </li>
 *   <li><strong>空间复杂度:</strong> {@code O(1)}。
 *     <ul>
 *       <li>只使用了几个常数级别的变量,没有额外的数据结构。</li>
 *     </ul>
 *   </li>
 * </ul>
 */
public int searchInsert(int[] nums, int target) {
    int n = nums.length;
    int left = 0;
    int right = n - 1;
    // 目标值不存在时,返回它将会被按顺序插入的位置
    // 这个变量 ans 用来记录可能插入的位置,初始值设为 n
    // 应对 target 大于所有元素的情况
    int ans = n;
    while (left <= right) {
        int mid = left + (right - left) / 2; // 防止 left + right 溢出
        if (nums[mid] >= target) {
            // 当前 mid 位置的元素大于或等于 target
            // 说明 mid 可能是插入位置,或者 target 应该在 mid 的左边(包括 mid 本身)
            ans = mid; // 记录当前 mid 为一个可能的答案
            right = mid - 1; // 继续向左半部分查找更小的索引
        } else {
            // 当前 mid 位置的元素小于 target
            // 说明 target 肯定在 mid 的右边
            left = mid + 1; // 在右半部分查找
        }
    }
    return ans; // 循环结束后,ans 就是目标值的插入位置
}

搜索二维矩阵

java
/**
 * <p>此方法解决“搜索二维矩阵”问题,采用<strong>两次二分查找</strong>的策略。</p>
 * <p>核心思想:由于矩阵具备两条关键属性:
 * <ol>
 *     <li>每行从左到右递增。</li>
 *     <li>每行的第一个元素大于前一行的最后一个元素。</li>
 * </ol>
 * <p>
 * 这允许我们首先通过二分查找确定 {@code target} 可能存在于哪一行,
 * 然后在该行内再进行一次二分查找来确定 {@code target} 是否存在。
 * </p>
 *
 * <h3>算法步骤:</h3>
 * <ol>
 *   <li><strong>处理边界情况:</strong>
 *     <ul>
 *       <li>如果 {@code matrix} 为空或尺寸为 {@code 0},直接返回 {@code false}。</li>
 *     </ul>
 *   </li>
 *   <li><strong>第一次二分查找 (定位行):</strong>
 *     <ul>
 *       <li>目标:找到 {@code target} 可能所在的行。我们寻找的是第一个行首元素小于等于 {@code target} 的行。</li>
 *       <li>{@code m = matrix.length} (行数), {@code n = matrix[0].length} (列数)。</li>
 *       <li>{@code top = 0}, {@code bottom = m - 1}。</li>
 *       <li>{@code candidateRow = -1}:用于记录最有可能的行索引。</li>
 *       <li>{@code while (top <= bottom)}:
 *         <ul>
 *           <li>{@code midRow = top + (bottom - top) / 2}。</li>
 *           <li>{@code if (matrix[midRow][0] == target)}:如果找到,直接返回 {@code true}。</li>
 *           <li>{@code else if (matrix[midRow][0] < target)}:{@code target} 可能在 {@code midRow} 或其下方的行。
 *             <ul>
 *               <li>{@code candidateRow = midRow;}:记录当前行作为可能的候选行。</li>
 *               <li>{@code top = midRow + 1;}:继续向下方查找。</li>
 *             </ul>
 *           </li>
 *           <li>{@code else (matrix[midRow][0] > target)}:{@code target} 肯定在 {@code midRow} 的上方。
 *             <ul>
 *               <li>{@code bottom = midRow - 1;}:向上方查找。</li>
 *             </ul>
 *           </li>
 *         </ul>
 *       </li>
 *       <li><strong>检查 {@code candidateRow} 的有效性:</strong>
 *         <ul>
 *           <li>{@code if (candidateRow == -1)}:如果没有找到任何行首元素小于 {@code target} 的行,说明 {@code target} 比所有行的首元素都小,不可能存在。返回 {@code false}。</li>
 *           <li>如果 {@code target} 比 {@code candidateRow} 这一行的最后一个元素都大 ({@code matrix[candidateRow][n-1] < target}),也说明 {@code target} 不可能存在于该行。返回 {@code false}。</li>
 *         </ul>
 *       </li>
 *     </ul>
 *   </li>
 *   <li><strong>第二次二分查找 (在确定行内定位元素):</strong>
 *     <ul>
 *       <li>确定要在 {@code matrix[candidateRow]} 这一行中搜索。</li>
 *       <li>{@code rowArray = matrix[candidateRow]}。</li>
 *       <li>{@code left = 0}, {@code right = n - 1}。</li>
 *       <li>{@code while (left <= right)}:
 *         <ul>
 *           <li>{@code mid = left + (right - left) / 2}。</li>
 *           <li>{@code if (rowArray[mid] == target)}:找到,返回 {@code true}。</li>
 *           <li>{@code else if (rowArray[mid] < target)}:向右查找 {@code left = mid + 1}。</li>
 *           <li>{@code else (rowArray[mid] > target)}:向左查找 {@code right = mid - 1}。</li>
 *         </ul>
 *       </li>
 *       <li>如果循环结束仍未找到,返回 {@code false}。</li>
 *     </ul>
 *   </li>
 * </ol>
 *
 * <h3>性能分析:</h3>
 * <ul>
 *   <li><strong>时间复杂度:</strong> {@code O(log m + log n)}。
 *     <ul>
 *       <li>第一次二分查找(定位行)需要 {@code O(log m)} 的时间。</li>
 *       <li>第二次二分查找(定位列)需要 {@code O(log n)} 的时间。</li>
 *       <li>总时间复杂度是两者的和。</li>
 *     </ul>
 *   </li>
 *   <li><strong>空间复杂度:</strong> {@code O(1)}。
 *     <ul>
 *       <li>只使用了几个常数级别的变量。</li>
 *     </ul>
 *   </li>
 * </ul>
 */
public boolean searchMatrix(int[][] matrix, int target) {
    if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
        return false;
    }
    int m = matrix.length;    // 行数
    int n = matrix[0].length; // 列数
    // --- 第一次二分查找:定位 target 可能所在的行 ---
    int top = 0;
    int bottom = m - 1;
    int candidateRow = -1; // 记录最有可能的行索引
    while (top <= bottom) {
        int midRow = top + (bottom - top) / 2;
        if (matrix[midRow][0] == target) {
            return true; // 目标值就是某行第一个元素
        } else if (matrix[midRow][0] < target) {
            // 当前行首元素小于 target,target 可能在当前行或下方的行
            candidateRow = midRow; // 记录当前行作为可能的候选行
            top = midRow + 1;      // 继续向下方查找
        } else {
            // 当前行首元素大于 target,target 肯定在当前行的上方
            bottom = midRow - 1;   // 向上方查找
        }
    }
    // 检查 candidateRow 的有效性
    if (candidateRow == -1) {
        // 如果 candidateRow 仍为 -1,说明 target 比所有行的首元素都小,不可能存在
        return false;
    }
    // 如果 target 比我们定位到的这一行的最后一个元素都大,也不可能存在于该行
    if (matrix[candidateRow][n - 1] < target) {
        return false;
    }
    // --- 第二次二分查找:在确定的行(candidateRow)内查找 target ---
    int[] rowArray = matrix[candidateRow]; // 获取目标行
    int left = 0;
    int right = n - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (rowArray[mid] == target) {
            return true; // 找到目标值
        } else if (rowArray[mid] < target) {
            left = mid + 1; // 向右查找
        } else {
            right = mid - 1; // 向左查找
        }
    }
    return false; // 如果都没有找到,返回 false
}
java
/**
 * <p>此方法解决“搜索二维矩阵”问题,采用<strong>一次模拟二分查找</strong>的更高效策略。</p>
 * <p>核心思想:利用矩阵的两条属性(每行递增,以及每行首元素大于前一行尾元素),
 * 我们可以将整个 {@code m x n} 的二维矩阵逻辑上视为一个长度为 {@code m * n} 的一维有序数组。
 * 然后,我们在这个虚拟的一维数组上执行标准的二分查找。
 * 关键在于,需要将一维数组的索引映射回二维矩阵的 {@code (row, col)} 坐标。</p>
 *
 * <h3>索引映射关系:</h3>
 * <p>对于一个 {@code m x n} 的矩阵,如果将其展平为一维数组,其索引 {@code k} (从 {@code 0} 到 {@code m*n - 1}) 与二维坐标 {@code (row, col)} 的关系为:</p>
 * <ul>
 *   <li>{@code row = k / n} (整数除法,{@code n} 是矩阵的列数)</li>
 *   <li>{@code col = k % n} (取模运算,{@code n} 是矩阵的列数)</li>
 * </ul>
 *
 * <h3>算法步骤:</h3>
 * <ol>
 *   <li><strong>处理边界情况:</strong>
 *     <ul>
 *       <li>如果 {@code matrix} 为空或尺寸为 {@code 0},直接返回 {@code false}。</li>
 *     </ul>
 *   </li>
 *   <li><strong>初始化二分查找范围:</strong>
 *     <ul>
 *       <li>{@code m = matrix.length} (行数)</li>
 *       <li>{@code n = matrix[0].length} (列数)</li>
 *       <li>{@code left = 0}:虚拟一维数组的起始索引。</li>
 *       <li>{@code right = m * n - 1}:虚拟一维数组的结束索引。</li>
 *     </ul>
 *   </li>
 *   <li><strong>二分查找循环:</strong> {@code while (left <= right)}
 *     <ul>
 *       <li>{@code mid = left + (right - left) / 2;}:计算虚拟一维数组的中间索引。</li>
 *       <li><strong>将 {@code mid} 映射到二维坐标:</strong>
 *         <ul>
 *           <li>{@code midRow = mid / n;}</li>
 *           <li>{@code midCol = mid % n;}</li>
 *         </ul>
 *       </li>
 *       <li><strong>比较 {@code matrix[midRow][midCol]} 与 {@code target}:</strong>
 *         <ul>
 *           <li>{@code if (matrix[midRow][midCol] == target)}:找到目标值,返回 {@code true}。</li>
 *           <li>{@code else if (matrix[midRow][midCol] < target)}:中间元素太小,{@code target} 肯定在 {@code mid} 的右侧。
 *             <ul>
 *               <li>{@code left = mid + 1;}</li>
 *             </ul>
 *           </li>
 *           <li>{@code else (matrix[midRow][midCol] > target)}:中间元素太大,{@code target} 肯定在 {@code mid} 的左侧。
 *             <ul>
 *               <li>{@code right = mid - 1;}</li>
 *             </ul>
 *           </li>
 *         </ul>
 *       </li>
 *     </ul>
 *   </li>
 *   <li><strong>返回结果:</strong>
 *     <ul>
 *       <li>如果循环结束仍未找到目标值,返回 {@code false}。</li>
 *     </ul>
 *   </li>
 * </ol>
 *
 * <h3>性能分析:</h3>
 * <ul>
 *   <li><strong>时间复杂度:</strong> {@code O(log(m * n))}。
 *     <ul>
 *       <li>二分查找的搜索空间是 {@code m * n} 个元素,因此时间复杂度是 {@code O(log(m*n))}。</li>
 *     </ul>
 *   </li>
 *   <li><strong>空间复杂度:</strong> {@code O(1)}。
 *     <ul>
 *       <li>只使用了几个常数级别的变量。</li>
 *     </ul>
 *   </li>
 * </ul>
 */
public boolean searchMatrix(int[][] matrix, int target) {
    if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
        return false;
    }

    int m = matrix.length;    // 行数
    int n = matrix[0].length; // 列数

    // 将二维矩阵看作一个展平的一维有序数组进行二分查找
    int left = 0;              // 虚拟一维数组的起始索引
    int right = m * n - 1;     // 虚拟一维数组的结束索引

    while (left <= right) {
        int mid = left + (right - left) / 2; // 计算虚拟一维数组的中间索引

        // 将一维索引 mid 映射回二维矩阵的 (row, col) 坐标
        int midRow = mid / n; // 行索引
        int midCol = mid % n; // 列索引

        if (matrix[midRow][midCol] == target) {
            return true; // 找到目标值
        } else if (matrix[midRow][midCol] < target) {
            // 中间元素小于 target,说明 target 在 mid 的右侧
            left = mid + 1;
        } else {
            // 中间元素大于 target,说明 target 在 mid 的左侧
            right = mid - 1;
        }
    }

    return false; // 循环结束仍未找到目标值
}

::

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

java
/**
 * <p>此方法解决“在排序数组中查找元素的第一个和最后一个位置”问题,
 * 采用<strong>两次变种二分查找(Binary Search)</strong>的策略。</p>
 * <p>核心思想:题目要求时间复杂度为 O(log n),且数组已排序,这明确指示我们要使用二分查找。
 * 由于需要找出目标的起始和结束位置,我们不能仅仅找到任意一个匹配项就停止。
 * 我们需要分别找到目标值第一次出现的位置(左边界)和最后一次出现的位置(右边界)。</p>
 *
 * <h3>查找策略:</h3>
 * <p>我们可以设计一个通用的二分查找辅助函数,该函数能够根据布尔参数返回两种情况的“插入位置”:</p>
 * <ol>
 *   <li><strong>查找左边界 ({@code lowerBound = true}):</strong> 寻找数组中第一个<strong>大于或等于</strong> {@code target} 的元素的索引。
 *       这会找到第一个 {@code target} 或应插入 {@code target} 的位置。</li>
 *   <li><strong>查找右边界 ({@code lowerBound = false}):</strong> 寻找数组中第一个<strong>严格大于</strong> {@code target} 的元素的索引。
 *       这个索引减 {@code 1} 就是 {@code target} 最后一次出现的位置。</li>
 * </ol>
 *
 * <h3>{@code binarySearch(nums, target, lowerBound)} 辅助函数实现:</h3>
 * <ul>
 *   <li>{@code left = 0}, {@code right = nums.length - 1}</li>
 *   <li>{@code ans = nums.length}:初始化答案为 {@code nums.length},表示 {@code target} 可能在数组末尾之外。</li>
 *   <li>{@code while (left <= right)}:标准二分查找循环。
 *     <ul>
 *       <li>{@code mid = left + (right - left) / 2;}</li>
 *       <li><strong>判断条件:</strong>
 *         <ul>
 *           <li>{@code if (nums[mid] > target || (lowerBound && nums[mid] >= target))}
 *             <ul>
 *               <li>如果 {@code nums[mid]} 严格大于 {@code target}:无论哪种查找 ({@code lowerBound} 是 {@code true} 或 {@code false}),都意味着 {@code nums[mid]} 是一个可能的答案(或 {@code target} 在其左侧)。</li>
 *               <li>如果 {@code lowerBound} 为 {@code true} 且 {@code nums[mid]} 等于 {@code target}:这正是我们要找的左边界,也是一个可能的答案。</li>
 *               <li>在这两种情况下:
 *                 <ul>
 *                   <li>{@code ans = mid;}:记录当前 {@code mid} 为一个候选答案。</li>
 *                   <li>{@code right = mid - 1;}:尝试在左半部分继续寻找更小的索引(即更左的边界)。</li>
 *                 </ul>
 *               </li>
 *             </ul>
 *           </li>
 *           <li>{@code else} ({@code nums[mid] < target} 或 {@code (!lowerBound && nums[mid] == target)}):
 *             <ul>
 *               <li>{@code nums[mid]} 太小了,或者它等于 {@code target} 但我们正在找严格大于 {@code target} 的位置。
 *                 这意味着 {@code target} (或其左边界) 肯定在 {@code mid} 的右边。</li>
 *               <li>{@code left = mid + 1;}:在右半部分查找。</li>
 *             </ul>
 *           </li>
 *         </ul>
 *       </li>
 *     </ul>
 *   </li>
 *   <li>返回 {@code ans}。</li>
 * </ul>
 *
 * <h3>{@code searchRange(nums, target)} 主函数实现:</h3>
 * <ul>
 *   <li>{@code int leftBound = binarySearch(nums, target, true);} // 查找第一个 {@code >= target} 的位置</li>
 *   <li>{@code int rightBound = binarySearch(nums, target, false) - 1;} // 查找第一个 {@code > target} 的位置,然后减 1</li>
 *   <li><strong>判断结果的有效性:</strong>
 *     <ul>
 *       <li>{@code if (leftBound <= rightBound && rightBound < nums.length && nums[leftBound] == target && nums[rightBound] == target)}
 *         <ul>
 *           <li>这个条件确保了:
 *             <ul>
 *               <li>{@code leftBound} 不在 {@code rightBound} 右边。</li>
 *               <li>{@code rightBound} 没有越界 (即 {@code target} 没有比所有元素都大)。</li>
 *               <li>{@code leftBound} 和 {@code rightBound} 处的元素确实是 {@code target} (避免了 {@code target} 不存在但返回值看上去有效的情况,例如 {@code [1,3], target=2} 会返回 {@code [1,-1]},需要额外判断)。</li>
 *             </ul>
 *           </li>
 *           <li>如果所有条件都满足,返回 {@code [leftBound, rightBound]}。</li>
 *         </ul>
 *       </li>
 *       <li>{@code else}:如果上述任何条件不满足,说明 {@code target} 不存在于数组中,返回 {@code [-1, -1]}。</li>
 *     </ul>
 *   </li>
 * </ul>
 *
 * <h3>性能分析:</h3>
 * <ul>
 *   <li><strong>时间复杂度:</strong> {@code O(log n)}。
 *     <ul>
 *       <li>进行了两次二分查找,每次都是 {@code O(log n)}。</li>
 *     </ul>
 *   </li>
 *   <li><strong>空间复杂度:</strong> {@code O(1)}。
 *     <ul>
 *       <li>只使用了几个常数级别的变量。</li>
 *     </ul>
 *   </li>
 * </ul>
 */
public int[] searchRange(int[] nums, int target) {
    int leftBound = binarySearch(nums, target, true);  // 查找第一个 >= target 的位置
    int rightBound = binarySearch(nums, target, false) - 1; // 查找第一个 > target 的位置,然后减 1
    // 检查找到的左右边界是否有效
    // 1. leftBound <= rightBound:确保左边界不越过右边界 (例如 target 不存在时可能发生)
    // 2. rightBound < nums.length:确保右边界没有超出数组范围 (例如 target 大于所有元素时)
    // 3. nums[leftBound] == target && nums[rightBound] == target:
    //    确保在有效的边界位置上,确实是 target。这处理了 target 不存在,但二分查找返回了看似有效的插入位置的情况。
    //    例如 nums=[2, 2], target=3. leftBound=2, rightBound=1. 则 leftBound > rightBound 返回 [-1,-1]
    //    例如 nums=[1, 3], target=2. leftBound=1, rightBound=0. 则 leftBound > rightBound 返回 [-1,-1]
    //    例如 nums=[5, 7, 7, 8, 8, 10], target=6.  leftBound=1, rightBound=0. leftBound > rightBound 返回 [-1,-1]
    if (leftBound <= rightBound
            && leftBound < nums.length && rightBound < nums.length // 边界值不能是等于 nums.length 导致越界
            && nums[leftBound] == target
            && nums[rightBound] == target) {
        return new int[]{leftBound, rightBound};
    }
    return new int[]{-1, -1};
}

/**
 * 通用的二分查找辅助函数。
 * 查找目标值 target 在数组中应该插入的位置 (即第一个满足条件的元素的索引)。
 *
 * @param nums       排序数组
 * @param target     目标值
 * @param lowerBound 如果为 true,查找第一个 >= target 的元素的索引 (即左边界)。
 *                   如果为 false,查找第一个 > target 的元素的索引 (即右边界 + 1)。
 * @return 目标值应该插入的位置 (第一个满足条件的元素的索引)。
 */
private int binarySearch(int[] nums, int target, boolean lowerBound) {
    int left = 0;
    int right = nums.length - 1;
    int ans = nums.length; // 默认答案是数组长度,表示 target 比所有元素都大,应插入到末尾
    while (left <= right) {
        int mid = left + (right - left) / 2; // 防止 left + right 溢出
        // 判断条件:
        // 如果 nums[mid] > target (无论找左边界还是右边界,都说明 target 在左边)
        // 或者 lowerBound 为 true 且 nums[mid] == target (我们在找左边界,这个 mid 可能是答案)
        if (nums[mid] > target || (lowerBound && nums[mid] >= target)) {
            ans = mid;         // 记录当前的 mid 为一个可能的答案
            right = mid - 1;   // 尝试在左半部分继续寻找更小的索引 (即更左的边界)
        } else {
            // nums[mid] < target (target 在 mid 右边)
            // 或者 lowerBound 为 false 且 nums[mid] == target (我们需要找严格大于 target 的,所以 current mid 不合适,target 应该在 mid 右边)
            left = mid + 1;    // 在右半部分查找
        }
    }
    return ans;
}

搜索旋转排序数组

java
/**
 * <p>此方法解决“搜索旋转排序数组”问题,采用经典的<strong>变种二分查找(Binary Search)算法</strong>。</p>
 * <p>核心思想:尽管数组被旋转了,但它仍然由两个有序的子数组组成。
 * 因此,我们仍然可以使用二分查找的思想。关键在于,在每次迭代中,
 * 我们需要判断当前 {@code mid} 所在的区间是左边有序子数组还是右边有序子数组,
 * 然后根据 {@code target} 的值在对应的有序区间内进行搜索。</p>
 *
 * <h3>算法步骤:</h3>
 * <ol>
 *   <li><strong>初始化指针:</strong>
 *     <ul>
 *       <li>{@code left = 0}:查找范围的左边界。</li>
 *       <li>{@code right = nums.length - 1}:查找范围的右边界。</li>
 *     </ul>
 *   </li>
 *   <li><strong>二分查找循环:</strong> {@code while (left <= right)}
 *     <ul>
 *       <li>{@code mid = left + (right - left) / 2;}:计算中间索引,防止 {@code left + right} 溢出。</li>
 *       <li><strong>判断是否找到目标值:</strong>
 *         <ul>
 *           <li>{@code if (nums[mid] == target)}:如果找到目标值,直接返回 {@code mid}。</li>
 *         </ul>
 *       </li>
 *       <li><strong>判断 {@code mid} 所在的有序区间:</strong>
 *         <ul>
 *           <li><strong>情况一:左半部分 {@code nums[left...mid]} 是有序的</strong> ({@code nums[left] <= nums[mid]})
 *             <p>这意味着 {@code mid} 在旋转点或旋转点左侧(包含旋转点在 {@code mid} 之前)。</p>
 *             <ul>
 *               <li>{@code if (target >= nums[left] && target < nums[mid])}:
 *                 <ul>
 *                   <li>如果 {@code target} 落在这个有序的左半部分区间内,那么 {@code target} 肯定在 {@code mid} 的左侧。</li>
 *                   <li>{@code right = mid - 1;}:将搜索范围缩小到左半部分。</li>
 *                 </ul>
 *               </li>
 *               <li>{@code else}:
 *                 <ul>
 *                   <li>{@code target} 不在左半部分的有序区间内,那么它肯定在右半部分(包括跨越旋转点后的那段)。</li>
 *                   <li>{@code left = mid + 1;}:将搜索范围缩小到右半部分。</li>
 *                 </ul>
 *               </li>
 *             </ul>
 *           </li>
 *           <li><strong>情况二:右半部分 {@code nums[mid...right]} 是有序的</strong> ({@code nums[left] > nums[mid]})
 *             <p>这意味着 {@code mid} 已经跨过了旋转点,位于旋转点右侧。</p>
 *             <ul>
 *               <li>{@code if (target > nums[mid] && target <= nums[right])}:
 *                 <ul>
 *                   <li>如果 {@code target} 落在这个有序的右半部分区间内,那么 {@code target} 肯定在 {@code mid} 的右侧。</li>
 *                   <li>{@code left = mid + 1;}:将搜索范围缩小到右半部分。</li>
 *                 </ul>
 *               </li>
 *               <li>{@code else}:
 *                 <ul>
 *                   <li>{@code target} 不在右半部分的有序区间内,那么它肯定在左半部分(包括跨越旋转点前的那段)。</li>
 *                   <li>{@code right = mid - 1;}:将搜索范围缩小到左半部分。</li>
 *                 </ul>
 *               </li>
 *             </ul>
 *           </li>
 *         </ul>
 *       </li>
 *     </ul>
 *   </li>
 *   <li><strong>返回结果:</strong>
 *     <ul>
 *       <li>如果循环结束后仍未返回,说明 {@code target} 不存在于数组中,返回 {@code -1}。</li>
 *     </ul>
 *   </li>
 * </ol>
 *
 * <h3>性能分析:</h3>
 * <ul>
 *   <li><strong>时间复杂度:</strong> {@code O(log n)}。
 *     <ul>
 *       <li>每一步二分查找都将搜索空间减半。</li>
 *     </ul>
 *   </li>
 *   <li><strong>空间复杂度:</strong> {@code O(1)}。
 *     <ul>
 *       <li>只使用了几个常数级别的变量。</li>
 *     </ul>
 *   </li>
 * </ul>
 */
public int search(int[] nums, int target) {
    if (nums == null || nums.length == 0) {
        return -1;
    }
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2; // 计算中间索引
        if (nums[mid] == target) {
            return mid; // 找到目标值,返回其索引
        }
        // 判断哪一部分是有序的
        if (nums[left] <= nums[mid]) { // 左半部分 (从 nums[left] 到 nums[mid]) 是有序的
            // 检查 target 是否在有序的左半部分中
            if (target >= nums[left] && target < nums[mid]) {
                // target 在左半部分,将搜索范围缩小到左侧
                right = mid - 1;
            } else {
                // target 不在左半部分,说明在右半部分(无序或有序)
                left = mid + 1;
            }
        } else { // 右半部分 (从 nums[mid] 到 nums[right]) 是有序的
            // 检查 target 是否在有序的右半部分中
            if (target > nums[mid] && target <= nums[right]) {
                // target 在右半部分,将搜索范围缩小到右侧
                left = mid + 1;
            } else {
                // target 不在右半部分,说明在左半部分(无序或有序)
                right = mid - 1;
            }
        }
    }
    return -1; // 循环结束仍未找到目标值
}

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

java
/**
 * <p>此方法解决“寻找旋转排序数组中的最小值”问题,采用经典的<strong>变种二分查找(Binary Search)算法</strong>。</p>
 * <p>核心思想:旋转排序数组由两个有序的子数组组成,最小值是这两个子数组的连接点。
 * 这个最小值有以下特性:它是数组中唯一一个比其右边元素小(除非它是最后一个元素)
 * 且比其左边元素大(除非它是第一个元素)的“局部最低点”。
 * 更直观地,它是唯一一个小于数组第一个元素 {@code nums[0]} 的元素(如果是旋转过的话)。</p>
 *
 * <h3>算法步骤:</h3>
 * <ol>
 *   <li><strong>处理边界情况:</strong>
 *     <ul>
 *       <li>如果 {@code nums} 为空或长度为 {@code 0},根据题目一般默认不考虑,或者返回一个约定值(例如 {@code Integer.MIN_VALUE} 或抛异常),
 *         但在 LeetCode 中,数组通常至少包含一个元素。</li>
 *     </ul>
 *   </li>
 *   <li><strong>处理未旋转的情况:</strong>
 *     <ul>
 *       <li>{@code if (nums[0] <= nums[nums.length - 1])}:
 *         <ul>
 *           <li>如果数组的第一个元素小于或等于最后一个元素,说明数组没有被旋转(或者旋转了 {@code n} 次)。</li>
 *           <li>此时数组本身就是升序的,最小值就是第一个元素 {@code nums[0]}。直接返回。</li>
 *         </ul>
 *         <p>例如:{@code [11,13,15,17]} 或 {@code [0,1,2,4,5,6,7]}。</p>
 *       </li>
 *     </ul>
 *   </li>
 *   <li><strong>初始化指针:</strong>
 *     <ul>
 *       <li>{@code left = 0}:查找范围的左边界。</li>
 *       <li>{@code right = nums.length - 1}:查找范围的右边界。</li>
 *     </ul>
 *   </li>
 *   <li><strong>二分查找循环:</strong> {@code while (left < right)}
 *     <ul>
 *       <li>{@code mid = left + (right - left) / 2;}:计算中间索引,防止 {@code left + right} 溢出。</li>
 *       <li><strong>判断 {@code mid} 所在的区间:</strong>
 *         <ul>
 *           <li>{@code if (nums[mid] < nums[right])}:
 *             <ul>
 *               <li>这表示从 {@code mid} 到 {@code right} 的这段子数组是<strong>有序且递增</strong>的。</li>
 *               <li>由于 {@code nums[mid]} 小于 {@code nums[right]},这意味着 {@code mid} 可能是最小值,或者最小值在 {@code mid} 的左侧。</li>
 *               <li>因此,将右边界 {@code right} 更新为 {@code mid}。</li>
 *               <p>例如:{@code [4,5,1,2,3]}, {@code L=0, R=4, M=2, nums[M]=1}. {@code nums[M] (1) < nums[R] (3)}. 最小值在 {@code [L, M]} 范围内,{@code R = M} (即 {@code 2}).</p>
 *             </ul>
 *           </li>
 *           <li>{@code else} ({@code nums[mid] > nums[right]}):
 *             <ul>
 *               <li>这表示从 {@code mid} 到 {@code right} 的这段子数组是<strong>非有序</strong>的(跨越了旋转点)。</li>
 *               <li>由于 {@code nums[mid]} 大于 {@code nums[right]},这意味着 {@code mid} 肯定不在递增的那一段开头,而是位于较大的部分。
 *                 最小值肯定在 {@code mid} 的右侧,包括 {@code mid+1} 及其之后。</li>
 *               <li>因此,将左边界 {@code left} 更新为 {@code mid + 1}。</li>
 *               <p>例如:{@code [3,4,5,1,2]}, {@code L=0, R=4, M=2, nums[M]=5}. {@code nums[M] (5) > nums[R] (2)}. 最小值在 {@code [M+1, R]} 范围内,{@code L = M+1} (即 {@code 3}).</p>
 *             </ul>
 *           </li>
 *         </ul>
 *       </li>
 *     </ul>
 *   </li>
 *   <li><strong>返回结果:</strong>
 *     <ul>
 *       <li>循环结束后,{@code left} ({@code right} 也等于 {@code left}) 将指向数组中的最小元素。返回 {@code nums[left]}。</li>
 *     </ul>
 *   </li>
 * </ol>
 *
 * <h3>性能分析:</h3>
 * <ul>
 *   <li><strong>时间复杂度:</strong> {@code O(log n)}。
 *     <ul>
 *       <li>二分查找在每次迭代中将搜索空间减半。</li>
 *     </ul>
 *   </li>
 *   <li><strong>空间复杂度:</strong> {@code O(1)}。
 *     <ul>
 *       <li>只使用了几个常数级别的变量。</li>
 *     </ul>
 *   </li>
 * </ul>
 */
public int findMin(int[] nums) {
    if (nums == null || nums.length == 0) {
        // 根据题目一般测试用例不会是空数组,但作为健壮性考虑
        throw new IllegalArgumentException("Array cannot be empty or null.");
    }
    int n = nums.length;
    // 如果数组没有旋转(或者旋转了 n 次),最小值就是第一个元素
    // 这种情况特点是 nums[0] <= nums[n-1]
    // 例如 [1,2,3,4,5] 或者 [11,13,15,17]
    if (nums[0] <= nums[n - 1]) {
        return nums[0];
    }
    int left = 0;
    int right = n - 1;
    // 当 left < right 时进行循环,最终 left 和 right 会指向最小值
    while (left < right) {
        int mid = left + (right - left) / 2; // 计算中间索引
        // 比较 nums[mid] 和 nums[right] 来判断哪部分是有序的
        if (nums[mid] < nums[right]) {
            // 如果 nums[mid] < nums[right],说明 mid 到 right 是一个有序的子数组。
            // 最小值可能在 left 到 mid 之间 (包含 mid 本身),或者 mid 就是最小值。
            // 例如 [4,5,1,2,3],mid=2, nums[mid]=1, nums[right]=3.
            // 1 < 3,所以最小值在左半部分或就是 mid。
            right = mid; // 将右边界缩小到 mid
        } else { // nums[mid] > nums[right]
            // 如果 nums[mid] > nums[right],说明 mid 到 right 这一段是跨越了旋转点的。
            // 此时,最小值一定在 mid 的右侧 (mid+1 到 right 之间)。
            // 例如 [3,4,5,1,2],mid=2, nums[mid]=5, nums[right]=2.
            // 5 > 2,所以最小值在右半部分。
            left = mid + 1; // 将左边界缩小到 mid + 1
        }
    }
    // 循环结束时,left (和 right) 就会指向最小元素的索引
    return nums[left];
}

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

java
/**
 * TODO
 */

贡献者

The avatar of contributor named as LI SIR LI SIR

页面历史