文章

hot100二分查找题解

35. 搜索插入位置

问题描述

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1:

1
2
输入: nums = [1,3,5,6], target = 5
输出: 2

示例 2:

1
2
输入: nums = [1,3,5,6], target = 2
输出: 1

示例 3:

1
2
输入: nums = [1,3,5,6], target = 7
输出: 4

提示:

  • $1 <= nums.length <= 10^4$
  • $-10^4 <= nums[i] <= 10^4$
  • nums无重复元素升序 排列数组
  • $-10^4 <= target <= 10^4$

题解

以下是关于题目 “35. 搜索插入位置” 的C++题解,使用二分查找实现以满足 $O(\log n)$ 的时间复杂度:

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
#include <vector>
#include <iostream>

class Solution {
public:
    int searchInsert(std::vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid; // 找到目标值,返回索引
            } else if (nums[mid] < target) {
                left = mid + 1; // 在右半部分继续搜索
            } else {
                right = mid - 1; // 在左半部分继续搜索
            }
        }
        return left; // 返回插入位置
    }
};

// 示例测试
int main() {
    Solution solution;
    std::vector<int> nums = {1, 3, 5, 6};
    int target1 = 5;
    int target2 = 2;
    int target3 = 7;

    std::cout << "输入: [1, 3, 5, 6], target = 5, 输出: " << solution.searchInsert(nums, target1) << std::endl;
    std::cout << "输入: [1, 3, 5, 6], target = 2, 输出: " << solution.searchInsert(nums, target2) << std::endl;
    std::cout << "输入: [1, 3, 5, 6], target = 7, 输出: " << solution.searchInsert(nums, target3) << std::endl;

    return 0;
}

解题思路

  1. 二分查找
    • 使用二分查找的模板实现,逐步缩小搜索范围。
    • 如果找到目标值,直接返回其索引。
    • 如果未找到目标值,最终的 left 即为目标值应该插入的位置。
  2. 复杂度分析
    • 时间复杂度:$O(\log n)$,因为是二分查找。
    • 空间复杂度:$O(1)$,只使用了常量空间。

示例输出

运行代码将输出:

1
2
3
输入: [1, 3, 5, 6], target = 5, 输出: 2
输入: [1, 3, 5, 6], target = 2, 输出: 1
输入: [1, 3, 5, 6], target = 7, 输出: 4

74. 搜索二维矩阵

问题描述

给你一个满足下述两条属性的 m x n 整数矩阵:

  • 每行中的整数从左到右按非严格递增顺序排列。
  • 每行的第一个整数大于前一行的最后一个整数。

给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false

示例 1:

img

1
2
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true

示例 2:

img

1
2
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13
输出:false

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 100
  • $-10^4 <= matrix[i][j], target <= 10^4$

题解

以下是解决“74. 搜索二维矩阵”问题的 C++ 题解,利用二维矩阵的性质,将其看作一个按行展开的有序一维数组,并使用二分查找:

解题代码

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
#include <vector>
#include <iostream>

class Solution {
public:
    bool searchMatrix(std::vector<std::vector<int>>& matrix, int target) {
        if (matrix.empty() || matrix[0].empty()) return false;

        int m = matrix.size();
        int n = matrix[0].size();
        int left = 0, right = m * n - 1;

        while (left <= right) {
            int mid = left + (right - left) / 2;
            int midValue = matrix[mid / n][mid % n]; // 转换为二维矩阵的元素
            
            if (midValue == target) {
                return true; // 找到目标值
            } else if (midValue < target) {
                left = mid + 1; // 搜索右半部分
            } else {
                right = mid - 1; // 搜索左半部分
            }
        }

        return false; // 未找到目标值
    }
};

// 示例测试
int main() {
    Solution solution;
    std::vector<std::vector<int>> matrix = {
        {1, 3, 5, 7},
        {10, 11, 16, 20},
        {23, 30, 34, 60}
    };

    int target1 = 3;
    int target2 = 13;

    std::cout << "输入: matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3, 输出: "
              << (solution.searchMatrix(matrix, target1) ? "true" : "false") << std::endl;

    std::cout << "输入: matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13, 输出: "
              << (solution.searchMatrix(matrix, target2) ? "true" : "false") << std::endl;

    return 0;
}

解题思路

  1. 二维矩阵映射到一维数组

    • 对于矩阵的第 i 行第 j 列元素 matrix[i][j]

      • 在一维数组中的索引为 $\text{index} = i \times n + j$。
    • 反之,根据索引 $index$

      找到矩阵中的元素:

      • 行号:$i = \text{index} / n$
      • 列号:$j = \text{index} \% n$
  2. 二分查找

    • 将矩阵视为一个一维数组,在范围 [0, m*n - 1] 上进行二分查找。
    • 每次取中间值,通过索引转换计算出对应的矩阵元素。
  3. 复杂度分析

    • 时间复杂度:$O(\log(m \cdot n))$,二分查找的时间复杂度。
    • 空间复杂度:$O(1)$,只使用了常量空间。

示例输出

运行代码将输出:

1
2
输入: matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3, 输出: true
输入: matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13, 输出: false

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

问题描述

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

1
2
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

1
2
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

1
2
输入:nums = [], target = 0
输出:[-1,-1]

提示:

  • $0 <= nums.length <= 10^5$
  • $-10^9 <= nums[i] <= 10^9$
  • nums 是一个非递减数组
  • $-10^9 <= target <= 10^9$

题解

下面是解决 “34. 在排序数组中查找元素的第一个和最后一个位置” 的 C++ 题解,使用二分查找实现,满足 $O(\log n)$ 的时间复杂度:

解题代码

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
#include <vector>
#include <iostream>

class Solution {
public:
    std::vector<int> searchRange(std::vector<int>& nums, int target) {
        std::vector<int> result = {-1, -1}; // 默认返回 [-1, -1]
        int left = findBound(nums, target, true);  // 查找第一个位置
        if (left == -1) return result; // 没找到目标值
        int right = findBound(nums, target, false);  // 查找最后一个位置
        return {left, right};
    }

private:
    // 查找目标值的边界,isFirst为true时查找第一个位置,false时查找最后一个位置
    int findBound(const std::vector<int>& nums, int target, bool isFirst) {
        int left = 0, right = nums.size() - 1, result = -1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                result = mid;
                if (isFirst) {
                    right = mid - 1;  // 继续查找左边界
                } else {
                    left = mid + 1;   // 继续查找右边界
                }
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return result;
    }
};

// 示例测试
int main() {
    Solution solution;
    std::vector<int> nums1 = {5, 7, 7, 8, 8, 10};
    int target1 = 8;
    std::vector<int> nums2 = {5, 7, 7, 8, 8, 10};
    int target2 = 6;
    std::vector<int> nums3 = {};
    int target3 = 0;

    auto result1 = solution.searchRange(nums1, target1);
    auto result2 = solution.searchRange(nums2, target2);
    auto result3 = solution.searchRange(nums3, target3);

    std::cout << "输入: nums = [5,7,7,8,8,10], target = 8, 输出: [" << result1[0] << "," << result1[1] << "]" << std::endl;
    std::cout << "输入: nums = [5,7,7,8,8,10], target = 6, 输出: [" << result2[0] << "," << result2[1] << "]" << std::endl;
    std::cout << "输入: nums = [], target = 0, 输出: [" << result3[0] << "," << result3[1] << "]" << std::endl;

    return 0;
}

解题思路

  1. 二分查找
    • 我们将问题分为两个子任务:查找目标值的第一个位置和最后一个位置。
    • 使用一个辅助函数 findBound 来完成这个任务。通过二分查找,我们可以根据 isFirst 参数来决定是否查找第一个位置或最后一个位置。
  2. 处理边界情况
    • 如果数组为空,返回 [-1, -1]
    • 在找到目标值时,如果是查找第一个位置,我们继续向左查找;如果是查找最后一个位置,我们继续向右查找。
  3. 时间复杂度
    • 由于每次二分查找都能将搜索范围缩小一半,时间复杂度为 $O(\log n)$。
  4. 空间复杂度
    • 只使用了常量空间,空间复杂度为 $O(1)$。

示例输出

运行代码将输出:

1
2
3
输入: nums = [5,7,7,8,8,10], target = 8, 输出: [3,4]
输入: nums = [5,7,7,8,8,10], target = 6, 输出: [-1,-1]
输入: nums = [], target = 0, 输出: [-1,-1]

33. 搜索旋转排序数组

问题描述

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k0 <= 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

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

1
2
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

示例 2:

1
2
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1

示例 3:

1
2
输入:nums = [1], target = 0
输出:-1

提示:

  • 1 <= nums.length <= 5000
  • $-10^4 <= nums[i] <= 10^4$
  • nums 中的每个值都 独一无二
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • $-10^4 <= target <= 10^4$

题解

以下是解决 “33. 搜索旋转排序数组” 的 C++ 题解,利用二分查找来处理旋转排序数组,满足 $O(\log n)$ 的时间复杂度:

解题代码

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
#include <vector>
#include <iostream>

class Solution {
public:
    int search(std::vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            
            if (nums[mid] == target) {
                return mid; // 找到目标值,返回索引
            }
            
            // 判断哪一部分是有序的
            if (nums[left] <= nums[mid]) {
                // 左半部分有序
                if (nums[left] <= target && target < nums[mid]) {
                    right = mid - 1; // target 在左半部分
                } else {
                    left = mid + 1; // target 在右半部分
                }
            } else {
                // 右半部分有序
                if (nums[mid] < target && target <= nums[right]) {
                    left = mid + 1; // target 在右半部分
                } else {
                    right = mid - 1; // target 在左半部分
                }
            }
        }
        return -1; // 没有找到目标值
    }
};

// 示例测试
int main() {
    Solution solution;
    std::vector<int> nums1 = {4, 5, 6, 7, 0, 1, 2};
    int target1 = 0;
    std::vector<int> nums2 = {4, 5, 6, 7, 0, 1, 2};
    int target2 = 3;
    std::vector<int> nums3 = {1};
    int target3 = 0;

    std::cout << "输入: nums = [4,5,6,7,0,1,2], target = 0, 输出: " << solution.search(nums1, target1) << std::endl;
    std::cout << "输入: nums = [4,5,6,7,0,1,2], target = 3, 输出: " << solution.search(nums2, target2) << std::endl;
    std::cout << "输入: nums = [1], target = 0, 输出: " << solution.search(nums3, target3) << std::endl;

    return 0;
}

解题思路

  1. 旋转数组的性质
    • 旋转后的数组仍然保持了部分有序的性质。在数组的某个位置,数组被分为两部分:左部分是有序的,右部分也是有序的。
  2. 二分查找的修改
    • 在普通的二分查找中,我们只需直接比较 nums[mid]target,判断目标值的大小。
    • 对于旋转数组,我们需要判断哪一部分是有序的:
      • 如果左半部分有序(即 nums[left] <= nums[mid]),我们就能判断 target 是否在左半部分,进一步更新 leftright 的值。
      • 如果右半部分有序(即 nums[mid] < nums[right]),则同理判断 target 是否在右半部分。
  3. 复杂度分析
    • 时间复杂度:$O(\log n)$,因为我们仍然在每次循环中将搜索范围减半。
    • 空间复杂度:$O(1)$,只使用了常量空间。

示例输出

运行代码将输出:

1
2
3
输入: nums = [4,5,6,7,0,1,2], target = 0, 输出: 4
输入: nums = [4,5,6,7,0,1,2], target = 3, 输出: -1
输入: nums = [1], target = 0, 输出: -1

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

问题描述

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

1
2
3
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例 2:

1
2
3
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

示例 3:

1
2
3
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。

提示:

  • n == nums.length
  • 1 <= n <= 5000
  • -5000 <= nums[i] <= 5000
  • nums 中的所有整数 互不相同
  • nums 原来是一个升序排序的数组,并进行了 1n 次旋转

题解

以下是解决“153. 寻找旋转排序数组中的最小值”问题的 C++ 题解,使用二分查找实现 $O(\log n)$ 的时间复杂度:


解题代码

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
#include <vector>
#include <iostream>

class Solution {
public:
    int findMin(std::vector<int>& nums) {
        int left = 0, right = nums.size() - 1;

        while (left < right) {
            int mid = left + (right - left) / 2;

            // 如果中间值比右边界大,说明最小值在右半部分
            if (nums[mid] > nums[right]) {
                left = mid + 1;
            } else {
                // 如果中间值小于等于右边界,最小值在左半部分或就是 mid
                right = mid;
            }
        }

        // 最小值在 left 位置
        return nums[left];
    }
};

// 示例测试
int main() {
    Solution solution;
    std::vector<int> nums1 = {3, 4, 5, 1, 2};
    std::vector<int> nums2 = {4, 5, 6, 7, 0, 1, 2};
    std::vector<int> nums3 = {11, 13, 15, 17};

    std::cout << "输入: nums = [3,4,5,1,2], 输出: " << solution.findMin(nums1) << std::endl;
    std::cout << "输入: nums = [4,5,6,7,0,1,2], 输出: " << solution.findMin(nums2) << std::endl;
    std::cout << "输入: nums = [11,13,15,17], 输出: " << solution.findMin(nums3) << std::endl;

    return 0;
}

解题思路

  1. 数组特性
    • 旋转后的数组由两个升序区间组成,其中最小值出现在分界点。
    • 如果 nums[mid] > nums[right],说明最小值在右半部分。
    • 如果 nums[mid] <= nums[right],说明最小值在左半部分或就是 mid
  2. 二分查找
    • 使用二分查找逐步缩小搜索范围。
    • left == right 时,区间被缩小到只剩一个元素,该元素即为最小值。
  3. 复杂度分析
    • 时间复杂度:$O(\log n)$,因为每次循环都将搜索范围缩小一半。
    • 空间复杂度:$O(1)$,仅使用了常量空间。

示例输出

运行代码将输出:

1
2
3
输入: nums = [3,4,5,1,2], 输出: 1
输入: nums = [4,5,6,7,0,1,2], 输出: 0
输入: nums = [11,13,15,17], 输出: 11

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

问题描述

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

算法的时间复杂度应该为 O(log (m+n))

示例 1:

1
2
3
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

示例 2:

1
2
3
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

提示:

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • $-10^6 <= nums1[i], nums2[i] <= 10^6$

题解

解决“4. 寻找两个正序数组的中位数”问题的 C++ 实现,使用二分查找满足 $O(\log(m+n))$ 的时间复杂度:


解题代码

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
#include <vector>
#include <iostream>
#include <algorithm>

class Solution {
public:
    double findMedianSortedArrays(std::vector<int>& nums1, std::vector<int>& nums2) {
        // 确保 nums1 是较短的数组
        if (nums1.size() > nums2.size()) {
            return findMedianSortedArrays(nums2, nums1);
        }

        int m = nums1.size();
        int n = nums2.size();
        int totalLeft = (m + n + 1) / 2;

        int left = 0, right = m;
        while (left <= right) {
            int i = left + (right - left) / 2;
            int j = totalLeft - i;

            int nums1LeftMax = (i == 0) ? INT_MIN : nums1[i - 1];
            int nums1RightMin = (i == m) ? INT_MAX : nums1[i];
            int nums2LeftMax = (j == 0) ? INT_MIN : nums2[j - 1];
            int nums2RightMin = (j == n) ? INT_MAX : nums2[j];

            if (nums1LeftMax <= nums2RightMin && nums2LeftMax <= nums1RightMin) {
                // 找到合适的分割点
                if ((m + n) % 2 == 0) {
                    return (std::max(nums1LeftMax, nums2LeftMax) + 
                            std::min(nums1RightMin, nums2RightMin)) / 2.0;
                } else {
                    return std::max(nums1LeftMax, nums2LeftMax);
                }
            } else if (nums1LeftMax > nums2RightMin) {
                right = i - 1; // i 太大
            } else {
                left = i + 1; // i 太小
            }
        }

        throw std::invalid_argument("Input arrays are not valid.");
    }
};

// 示例测试
int main() {
    Solution solution;

    std::vector<int> nums1_1 = {1, 3};
    std::vector<int> nums1_2 = {2};
    std::cout << "输入: nums1 = [1,3], nums2 = [2], 输出: " 
              << solution.findMedianSortedArrays(nums1_1, nums1_2) << std::endl;

    std::vector<int> nums2_1 = {1, 2};
    std::vector<int> nums2_2 = {3, 4};
    std::cout << "输入: nums1 = [1,2], nums2 = [3,4], 输出: " 
              << solution.findMedianSortedArrays(nums2_1, nums2_2) << std::endl;

    return 0;
}

解题思路

  1. 分割数组
    • 假设在 nums1nums2 的合并数组中,找到分割点,使得左侧元素总数等于右侧元素总数(或左侧比右侧多一个)。
    • 分割点将数组划分为两个部分,左侧最大值和右侧最小值用于计算中位数。
  2. 二分查找
    • 将问题转化为在较短的数组 nums1 上进行二分查找。
    • 令分割点为 inums2 的分割点为 j = (m + n + 1) / 2 - i
    • 根据条件调整 i 的范围,找到满足:
      • nums1[i - 1] <= nums2[j]
      • nums2[j - 1] <= nums1[i]
  3. 边界处理
    • 如果 i == 0i == m,对应的边界使用 INT_MININT_MAX 处理。
  4. 时间复杂度
    • 二分查找的时间复杂度为 $O(\log(\min(m, n)))$。
    • 空间复杂度为 $O(1)$。

示例输出

运行代码将输出:

1
2
输入: nums1 = [1,3], nums2 = [2], 输出: 2
输入: nums1 = [1,2], nums2 = [3,4], 输出: 2.5
本文由作者按照 CC BY 4.0 进行授权