算法:排序、选择与查找:插入/冒泡/选择/快速/归并/堆排序/快速选择/二分

1. 排序:912.排序数组

给你一个整数数组 nums,请你将该数组升序排列。

你必须在 不使用任何内置函数 的情况下解决问题,时间复杂度为 O(nlog(n)),并且空间复杂度尽可能小。

1.1 插入排序

时间复杂度:O(n方)

数组前面是已排序部分,后面是未排序部分。

思路:从未排序部分开始遍历并插入到已排序部分。

选择数组后面未排序的元素插入到数组前面已排序的部分,从第二个元素开始选择。

cpp 复制代码
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        // 插入排序,遍历未排序部分插入到前面已排序部分,初始时第一个元素看作已排序
        for(int unsortedBegin = 1; unsortedBegin < n; unsortedBegin++){
            int cur = nums[unsortedBegin];
            int j = unsortedBegin - 1; // 已排序部分末尾
            while(j >= 0 && nums[j] > cur){
                nums[j + 1] = nums[j];
                j--;
            }
            nums[j + 1] = cur;
        }
        return nums;
    }
};

1.2 冒泡排序

时间复杂度:O(n方)

数组前面是未排序部分,后面是已排序部分。

思路:从未排序部分选择最大的放到已排序部分前面一个位置。

双重循环,第一重循环共 n - 1 次因为每次循环可以排序一个元素,共需要排序 n - 1 个元素,第二重循环两两比较,选择更大的元素不断交换到数组后面成为已排序的一部分。

cpp 复制代码
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        for(int i = 0; i < n - 1; i++){
            for(int j = 0; j < n - 1; j++){
                if(nums[j] > nums[j + 1]) swap(nums[j], nums[j + 1]);
            }
        }
        return nums;
    }
};
cpp 复制代码
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        int sortedBegin = n;
        // 冒泡排序,不断选择未排序部分中最大的放到已排序部分前面一个位置
        for(int i = 0; i <= n - 2; i++){
            for(int j = 0; j <= sortedBegin - 2; j++){
                if(nums[j] > nums[j + 1]) swap(nums[j], nums[j + 1]);
            }
            // 内层循环结束,最大的元素被交换到了 sortedBegin - 1 的位置
            sortedBegin--;
        }
        return nums;
    }
};

1.3 选择排序

时间复杂度:O(n方)

数组前面是已排序的,后面是未排序的。

思路:从未排序部分选择最小的,放到已排序部分的末尾后一个位置。

cpp 复制代码
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        int sortedBegin = n;
        // 选择排序,不断从未排序部分选择最小的插入到已排序部分末尾后面一个位置
        for(int unsortedBegin = 0; unsortedBegin < n; unsortedBegin++){
            int minIdx = unsortedBegin;
            for(int j = unsortedBegin; j < n; j++){
                if(nums[j] < nums[minIdx]) minIdx = j;
            }
            // 遍历结束,找到最小元素 minIdx
            // 将其放到未排序部分后面一个位置,即已排序部分开头
            swap(nums[minIdx], nums[unsortedBegin]);
        }
        return nums;
    }
};

1.4 快速排序

  • 分解 :选择一个基准值,将数组分成两部分:左边都比它小,右边都比它大。
  • 解决:递归地对左右两部分重复上述过程。

时间复杂度:O(nlogn)

思路:遍历后面的未选择区域,把比pivot小的放在前面已选择区域末尾后一个位置。

cpp 复制代码
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        auto partition = [&](int left, int right) -> int {
            // 选择中点
            int pivotIdx = left + (right - left) / 2;
            int pivot = nums[pivotIdx];

            // 将基准值放最后方便后面换回来
            swap(nums[pivotIdx], nums[right]);

            // 小于pivot区域的末尾
            int end = left - 1;

            // 把小于pivot的放在前面
            for(int i = left; i < right; i++){
                if(nums[i] < pivot){
                    end++;
                    swap(nums[i], nums[end]);
                }
            }

            // 把 pivot 放到正确的位置
            swap(nums[right], nums[end + 1]);

            // 返回pivot的位置
            return end + 1;
        };
        auto quickSort = [&](this auto&& quickSort, int left, int right) -> void {
            if(left < right){
                int pivotIdx = partition(left, right);
                quickSort(left, pivotIdx - 1);
                quickSort(pivotIdx + 1, right);
            }
        };
        quickSort(0, n - 1);
        return nums;
    }
};

1.5 归并排序

  1. 分解:将数组从中间切开,递归地对左右两半进行排序。
  2. 合并:将两个已经有序的子数组合并成一个大的有序数组。

时间复杂度:O(nlogn)

cpp 复制代码
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        // 归并排序,递归,将mid左右两边的通过一个临时数组合并在一起
        // 合并[left, mid]和[mid + 1, right]
        auto merge = [&](int left, int mid, int right) -> void {
            vector<int> tmp; // 临时数组
            int i = left, j = mid + 1; // 双指针遍历两个数组
            // 1. 把两个数组中较小的那一个放进去
            while(i <= mid && j <= right){
                if(nums[i] < nums[j]){
                    tmp.push_back(nums[i]);
                    i++;
                }else{
                    tmp.push_back(nums[j]);
                    j++;
                }
            }
            // 2. 把剩下的都放进去
            while(i <= mid){
                tmp.push_back(nums[i]);
                i++;
            }
            while(j <= right){
                tmp.push_back(nums[j]);
                j++;
            }
            // 3. tmp中现在是排序好的,拷贝到原数组中
            for(int k = 0; k < tmp.size(); k++){
                nums[left + k] = tmp[k];
            }
        };
        auto mergeSort = [&](this auto&& mergeSort ,int left, int right) -> void {
            if(left < right){
                int mid = left + (right - left) / 2;
                // 对左右两边进行排序
                mergeSort(left, mid);
                mergeSort(mid + 1, right);
                // 左右两边排序完成了,现在将其合并
                merge(left, mid, right);
            }
        };
        mergeSort(0, n - 1);
        return nums;
    }
};

1.6 堆排序

1.6.1 大根堆

大根堆 是一种特殊的**完全二叉树,**它满足两个核心条件:

  1. 结构性质 :必须是一棵完全二叉树(除了最后一层,其他层都填满了,且最后一层的节点都靠左排列)。
  2. 堆序性质 :树中任意一个节点的值,都大于或等于其左右孩子节点的值 。这意味着堆顶(根节点)一定是整个堆中最大的元素。
  • 逻辑视角: 它是一棵
    • 有根节点、左孩子、右孩子。
    • 有层级关系。
    • 你可以画成一个三角形的树状图。
  • 物理视角: 它通常是一个数组
    • 因为它是"完全二叉树",中间没有空缺,所以非常适合用数组紧凑存储,不浪费空间
    • 通过下标计算来模拟父子关系,不需要像链表那样存指针。缓存友好:数组在内存中是连续的,CPU 缓存命中率高,速度更快。
关系 公式 说明
左孩子 2 * i + 1 找到左边孩子的下标
右孩子 2 * i + 2 找到右边孩子的下标
父节点 (i - 1) / 2 找到父节点的下标 (向下取整)

大根堆不是静态的,它支持动态操作,操作后会通过"调整"来维持大根堆的性质:

  1. 插入(Push)
    • 先把新元素放到数组末尾。
    • 然后让它向上浮(Sift Up):如果它比父节点大,就和父节点交换,直到满足大根堆性质。
  2. 删除堆顶(Pop)
    • 通常只能删除最大值(即根节点)。
    • 把数组最后一个元素移到根节点覆盖掉原来的根。
    • 然后让它向下沉(Sift Down):如果它比孩子小,就和较大的那个孩子交换,直到满足大根堆性质。

1.6.2 算法

时间复杂度:O(nlogn)

  • 把最大的数拿到最左边(建堆)
  • 把最左边的数放到末尾(swap)
  • 忽略末尾的数,重复前两步

在数组存储的完全二叉树中,父子索引关系是固定的:

  • 父节点索引:i
  • 左孩子索引:2 * i + 1
  • 右孩子索引:2 * i + 2
  • 索引 n / 2 - 1 一定是最后一个拥有孩子的节点。任何大于这个索引的节点(即 n/2n-1),都一定是叶子节点。
特性 std::priority_queue(优先队列) 堆排序 (Heap Sort)
目的 随时获取当前最大值,并支持动态插入/删除。 把整个数组彻底排好序
建堆 只做一次:for (n/2-1 -> 0) heapify。 只做一次:for (n/2-1 -> 0) heapify。
后续操作 动态维护 : 每次 push 新元素,执行一次"上浮"; 每次 pop 顶元素,执行一次"下沉"。 (不一次性把所有数排好) 循环提取 : 循环 n 次: 1. 交换堆顶和末尾 2. 缩小范围 3. 执行一次 heapify (直到数组完全有序)
最终结果 数组内部依然是堆结构(乱序的,只是满足堆性质)。 数组变成了完全有序(升序)。
cpp 复制代码
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        auto heapify = [&](this auto&& heapify, int end, int root) -> void {
            int largest = root; // 假设root为当前最大值
            int left = 2 * root + 1; // 左孩子
            int right = 2 * root + 2; // 右孩子
            if(left < end && nums[left] > nums[largest]) largest = left;
            if(right < end && nums[right] > nums[largest]) largest = right;
            // 下沉
            if(largest != root){
                swap(nums[largest], nums[root]);
                heapify(end, largest);
            }
        };
        // 1. 把数组转化为大根堆
        for(int i = n/2 - 1; i >= 0; i--){
            heapify(n, i);
        }
        // 2. 循环排序,依次将堆顶元素(最大值)与末尾交换,然后缩小堆范围再调整
        for(int end = n - 1; end >= 0; end--){
            swap(nums[0], nums[end]);
            heapify(end, 0);
        }
        return nums;
    }
};

2. 快速选择

时间复杂度:O(n)

1. 快速排序的逻辑:

  • 目标:把整个数组排好序。
  • 终止条件 :当区间长度为 1 (left == right) 时,它已经是有序的了,不需要再划分。
  • 所以 :用if (left < right) 。如果是left == right,直接返回,不做任何事。

2. 快速选择的逻辑:

  • 目标:找到第 k 大的元素(即下标为 targetIdx 的元素)。
  • 区别 :不关心整个数组是否有序,只关心 targetIdx 位置上的值
  • 场景 : 假设数组缩小到了只剩一个元素,且这个元素的下标正好就是要找的 targetIdx
    • 此时 left == right == targetIdx。
    • 如果用 <
      • 条件 left < right (即 targetIdx < targetIdx) 为
      • 函数直接返回,导致答案丢失
    • 如果用 <=
      • 条件 left <= right
      • 进入函数,执行 partition
      • 因为只有一个元素,partition 返回的下标 pivotIdx 必然等于 left (也就是 targetIdx)。
      • 进入判断 if (pivotIdx == targetIdx),成功找到并记录答案。

2.1 215.数组中的第K个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

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

类快速排序的标准递归写法:

cpp 复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        int n = nums.size();
        // 第 k 大的元素在排序数组中的小标是 n - k
        int targetIdx = n - k;
        auto partition = [&](int left, int right) -> int {
            // 随机选择基准点
            int pivotIdx = left + (right - left) / 2;
            int pivot = nums[pivotIdx];

            // 把基准值放最后方便后续操作
            swap(nums[right], nums[pivotIdx]);

            // 已选择部分的末尾
            int end = left - 1;

            // 把比pivot小的放前面
            for(int i = left; i < right; i++){
                if(nums[i] < pivot){
                    end++;
                    swap(nums[end], nums[i]);
                }
            }

            // 把基准值放回去
            swap(nums[right], nums[end + 1]);

            return end + 1;
        };
        int ans = 0;
        auto quickSort = [&](this auto&& quickSort, int left, int right) -> void {
            if(ans != 0) return;
            if(left <= right){
                int pivotIdx = partition(left, right);
                if(pivotIdx == targetIdx){
                    ans = nums[pivotIdx];
                }else if(pivotIdx > targetIdx){
                    quickSort(left, pivotIdx - 1);
                }else{
                    quickSort(pivotIdx + 1, right);
                }
            }
        };
        quickSort(0, n - 1);
        return ans;
    }
};

迭代写法:

cpp 复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        int n = nums.size();
        // 第 k 大的元素在排序数组中的小标是 n - k
        int targetIdx = n - k;
        auto partition = [&](int left, int right) -> int {
            // 随机选择基准点
            int pivotIdx = left + rand() % (right - left + 1);
            int pivot = nums[pivotIdx];

            // 把基准值放最后方便后续操作
            swap(nums[right], nums[pivotIdx]);

            // 已选择部分的末尾
            int end = left - 1;

            // 把比pivot小的放前面
            for(int i = left; i < right; i++){
                if(nums[i] < pivot){
                    end++;
                    swap(nums[end], nums[i]);
                }
            }

            // 把基准值放回去
            swap(nums[right], nums[end + 1]);

            return end + 1;
        };
        int left = 0, right = n - 1;
        srand(time(NULL));        
        while(left <= right){
            int pivotIdx = partition(left, right);
            if(pivotIdx == targetIdx){
                return nums[pivotIdx];
            }else if(pivotIdx > targetIdx){
                right = pivotIdx - 1;
            }else{
                left = pivotIdx + 1;
            }
        }
        return -1;
    }
};

2.2 347. 前 K 个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

思路:统计频率后对频率进行快速排序。

类快速排序的标准递归写法:

cpp 复制代码
class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 1. 统计频率
        unordered_map<int, int> m;
        for(int i = 0; i < nums.size(); i++){
            m[nums[i]]++;
        }

        // 2. 转化为数组
        vector<pair<int, int>> vec;
        for(auto& p : m){
            vec.push_back(p);
        }

        auto partition = [&](int left, int right) -> int {
            int pivotIdx = left + rand() % (right - left + 1);
            int pivot = vec[pivotIdx].second;
            swap(vec[right], vec[pivotIdx]);
            int end = left - 1;
            for(int i = left; i < right; i++){
                if(vec[i].second < pivot){
                    end++;
                    swap(vec[end], vec[i]);
                }
            }
            swap(vec[end + 1], vec[right]);
            return end + 1;
        };

        // 3. 找到前 k 大的,需要在升序中找到第 n - k 个元素,以及后面的所有元素
        int n = vec.size();
        int targetIdx = n - k;
        int flag = 0;
        auto quickSort = [&](this auto&& quickSort, int left, int right) -> void {
            if(flag == 1) return;
            if(left <= right){
                int pivotIdx = partition(left, right);
                if(pivotIdx == targetIdx){
                    flag = 1;
                    return;
                }else if(pivotIdx > targetIdx){
                    quickSort(left, pivotIdx - 1);
                }else{
                    quickSort(pivotIdx + 1, right);
                }
            }
        };
        quickSort(0, n - 1);
        // 找到 targetIdx 即可,不需要排序,因为在选择的过程中已经确保右边的都是大于 partition 的
        // 且没有要求返回答案的顺序

        // 4. 提取后 k 个元素
        vector<int> ans;
        for(int i = n - 1; i >= targetIdx; i--){
            ans.push_back(vec[i].first);
        }

        return ans;
    }
};

迭代写法:

cpp 复制代码
class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 1. 统计频率
        unordered_map<int, int> m;
        for(int i = 0; i < nums.size(); i++){
            m[nums[i]]++;
        }

        // 2. 转化为数组
        vector<pair<int, int>> vec;
        for(auto& p : m){
            vec.push_back(p);
        }

        auto partition = [&](int left, int right) -> int {
            int pivotIdx = left + rand() % (right - left + 1);
            int pivot = vec[pivotIdx].second;
            swap(vec[right], vec[pivotIdx]);
            int end = left - 1;
            for(int i = left; i < right; i++){
                if(vec[i].second < pivot){
                    end++;
                    swap(vec[end], vec[i]);
                }
            }
            swap(vec[end + 1], vec[right]);
            return end + 1;
        };

        // 3. 找到前 k 大的,需要在升序中找到第 n - k 个元素,以及后面的所有元素
        int n = vec.size();
        int targetIdx = n - k;
        int left = 0, right = n - 1;
        while(left <= right){
            int pivotIdx = partition(left, right);
            if(pivotIdx == targetIdx){
                break;
            }else if(pivotIdx > targetIdx){
                right = pivotIdx - 1;
            }else{
                left = pivotIdx + 1;
            }
        }
        // 找到 targetIdx 即可,不需要排序,因为在选择的过程中已经确保右边的都是大于 partition 的
        // 且没有要求返回答案的顺序

        // 4. 提取后 k 个元素
        vector<int> ans;
        for(int i = n - 1; i >= targetIdx; i--){
            ans.push_back(vec[i].first);
        }

        return ans;
    }
};

3. 二分查找

二分查找的关键在于查找的是存在的值还是不存在的值。

如果确定该值一定存在数组中,则:

cpp 复制代码
int n = nums.size();
int l = 0, r = n - 1;
while(l < r){
    int mid = l + (r - l) / 2;
    if(条件){
    }else{
    }
}
return l;

因为 l 的范围是 [0, n - 1],最后返回的 l 一定是要查找的值。

如果不确定该值是否存在,则:

cpp 复制代码
int n = nums.size();
int l = 0, r = n;
while(l < r){
    int mid = l + (r - l) / 2;
    if(条件){
    }else{
    }
}
return l;

因为 l 的范围是 [0, n] ,该值需要放插入到的位置可能在数组末尾

3.1 35.搜索插入位置

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

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

关键:循环结束时,l = r,l 永远指向"第一个 >= target"的位置

cpp 复制代码
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int l = 0, r = nums.size();
        while(l < r){
            int mid = l + (r - l) / 2;
            if(nums[mid] < target){
                l = mid + 1;
            }else{
                r = mid;
            }
        }
        return l;
    }
};

3.2 74.搜索二维矩阵

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

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

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

将该矩阵展平即为一个升序的数组,可使用双指针进行二分查找。

cpp 复制代码
class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int row = matrix.size();
        int col = matrix[0].size();
        int l = 0, r = row * col;
        while(l < r){
            int mid = l + (r - l) / 2;
            int num = matrix[mid / col][mid % col];
            if(num < target){
                l = mid + 1;
            }else{
                r = mid;
            }
        }
        if(l <= row * col - 1 && matrix[l / col][l % col] == target)
            return 1;
        return 0;
    }
};

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

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

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

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

使用二分法查找起始位置,再用二分法查找 target + 1的起始位置。

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int n = nums.size();
        int l = 0, r = n;
        while(l < r){
            int mid = l + (r - l) / 2;
            if(nums[mid] < target){
                l = mid + 1;
            }else{
                r = mid;
            }
        }
        if(l == n || nums[l] != target) return {-1, -1};
        int start = l;
        l = 0, r = n;
        while(l < r){
            int mid = l + (r - l) / 2;
            if(nums[mid] < target + 1){
                l = mid + 1;
            }else{
                r = mid;
            }
        }
        return {start, l - 1};
    }
};

3.4 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) 的算法解决此问题。

一旦数组"断开"了(旋转了),看起来不连续了,直觉上会觉得二分查找失效。但实际上,二分查找的核心本质不是"数组必须全局有序",而是"每次比较后,能够确定目标值在哪一半,从而排除掉另一半" 。只要能找到一种方法,每次都能排除掉一半不可能存在答案的区间,二分查找就可以使用。

二分查找不需要全局单调,只需要满足:对于当前的 mid,能判断出答案在左边还是右边。

  • 在普通有序数组中:nums[mid] < target,因为全局递增,所以答案一定在右边。
  • 在旋转数组找最小值中:nums[mid] < nums[high],因为右半部分局部有序 ,所以最小值一定不在 (mid, high] 之间,答案一定在左边(包含 mid)。

只要能根据 mid 的信息,扔掉一半的数据,这就是二分查找。 旋转数组的特殊结构(两段有序)恰好提供了这个扔掉一半数据的依据。

cpp 复制代码
class Solution {
public:
    int findMin(vector<int>& nums) {
        int n = nums.size();
        int l = 0, r = n - 1;
        while(l < r){
            int mid = l + (r - l) / 2;
            if(nums[mid] < nums[n - 1]){
                r = mid;
            }else{
                l = mid + 1;
            }
        }
        return nums[l];
    }
};
  • findMin (寻找旋转数组最小值)
    • 最小值一定在数组里
    • 答案 l 的下标范围是 [0, n-1]
    • 所以 r = n - 1 是安全的,足够覆盖所有可能。
  • searchInsert (寻找插入位置)
    • 插入位置可能在数组末尾之后
    • 答案 l 的下标范围是 [0, n](共 n+1 种可能)。
    • 如果 target 比数组所有元素都大,应该返回 n
    • 如果写 r = n - 1,那么 l 最大只能变成 n - 1永远无法返回 n

3.5 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) 的算法解决此问题。

cpp 复制代码
class Solution {
public:
    int search(vector<int>& nums, int target) {
        // 1. 寻找旋转排序数组中的最小值
        int n = nums.size();
        int l = 0, r = n - 1;
        while(l < r){
            int mid = l + (r - l) / 2;
            if(nums[mid] < nums[n - 1]){
                r = mid;
            }else{
                l = mid + 1;
            }
        }
        int minIdx = l;
        // 2. 二分查找
        if(target > nums[n - 1]){
            // 在左边搜
            l = 0, r = minIdx;
            while(l < r){
                int mid = l + (r - l) / 2;
                if(nums[mid] < target){
                    l = mid + 1;
                }else{
                    r = mid;
                }
            }
        }else{
            // 在右边搜
            l = minIdx, r = n;
            while(l < r){
                int mid = l + (r - l) / 2;
                if(nums[mid] < target){
                    l = mid + 1;
                }else{
                    r = mid;
                }
            }
        }
        if(nums[l] == target) return l;
        return -1;
    }
};

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

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

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

有两个有序数组 nums1nums2。 如果我们将这两个数组分别切一刀(分成左右两部分),使得:

  1. 左半部分的元素个数 = 右半部分的元素个数(如果总长度是奇数,左边多一个)。
  2. 左半部分的所有元素 ≤ 右半部分的所有元素

那么,中位数就只取决于分割线附近的四个数字:

  • nums1 左边的最大值 (num1_left)
  • nums1 右边的最小值 (num1_right)
  • nums2 左边的最大值 (num2_left)
  • nums2 右边的最小值 (num2_right)

中位数的计算:

  • 总长度为奇数 :中位数就是左边最大的那个数,即 max(num1_left, num2_left)
  • 总长度为偶数 :中位数是左边最大和右边最小的平均值,即 (max(num1_left, num2_left) + min(num1_right, num2_right)) / 2.0
cpp 复制代码
class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        // 用较小的数组进行二分
        if(nums1.size() > nums2.size()) return findMedianSortedArrays(nums2, nums1);
        int n1 = nums1.size(), n2 = nums2.size();
        // 闭区间[0, n1],因为循环内的是切割点,切割点可能在数组最后, 切割点即 nums1 左边元素的个数
        int left = 0, right = n1;
        int mid1 = 0, mid2 = 0;
        // 用 = 是因为要覆盖并检查所有可能的切割点
        while(left <= right){
            int idx1 = left + (right - left) / 2; // 第一个数组切割点
            int idx2 = (n1 + n2 + 1) / 2 - idx1; // 第二个数组切割点,保证左边与右边相等或者多一个
            // 切割后数组两部分长度相同,只要判断大小是否满足即可
            int num1_left = idx1 == 0 ? INT_MIN : nums1[idx1 - 1];
            int num1_right = idx1 == n1 ? INT_MAX : nums1[idx1];
            int num2_left = idx2 == 0 ? INT_MIN : nums2[idx2 - 1];
            int num2_right = idx2 == n2 ? INT_MAX : nums2[idx2];
            // 如果大小匹配则为中位数
            if(num1_left <= num2_right && num2_left <= num1_right){
                mid1 = max(num1_left, num2_left);
                mid2 = min(num1_right, num2_right);
                break;
            }else if(num1_left > num2_right){
                // 分割点向左
                right = idx1 - 1;
            }else{
                left = idx1 + 1;
            }
        }
        return (n1 + n2) % 2 == 0 ? (mid1 + mid2) / 2.0 : mid1;
    }
};
相关推荐
一叶落4381 小时前
LeetCode 191. 位1的个数(Hamming Weight)——三种解法详解(C语言)
c语言·数据结构·算法·leetcode
满分观察网友z2 小时前
刷 LeetCode 看不懂题解?我做了一个能"播放"算法的开源可视化平台
前端·算法·leetcode
liu****2 小时前
4.哈希扩展
c++·算法·哈希算法·位图·bitset
Σίσυφος19002 小时前
PCL聚类 之K-Means
算法·kmeans·聚类
Flying pigs~~2 小时前
机器学习之数据挖掘时间序列预测
人工智能·算法·机器学习·数据挖掘·线性回归
仰泳的熊猫2 小时前
题目1882:蓝桥杯2017年第八届真题-k倍区间
数据结构·c++·算法·蓝桥杯
Darkwanderor2 小时前
图论——拓扑排序和图上DP
c++·算法·动态规划·图论·拓扑排序
有时间要学习2 小时前
面试150——第六周
算法·面试·深度优先
请叫我大虾2 小时前
数据结构与算法-分裂问题,将数字分成0或1,求l到r之间有多少个1.
java·算法·r语言
hetao17338372 小时前
2026-03-04~03-06 hetao1733837 的刷题记录
c++·算法