首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >LeetCode 刷题笔记——day 3

LeetCode 刷题笔记——day 3

作者头像
h-t-m
发布于 2022-11-24 16:53:12
发布于 2022-11-24 16:53:12
5270
举报
文章被收录于专栏:h-t-mh-t-m

LeetCode 刷题笔记——day 3

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

难度:困难

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

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

示例 1:

代码语言:javascript
复制
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

示例 2:

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

示例 3:

代码语言:javascript
复制
输入:nums1 = [0,0], nums2 = [0,0]
输出:0.00000

示例 4:

代码语言:javascript
复制
输入:nums1 = [], nums2 = [1]
输出:1.00000

示例 5:

代码语言:javascript
复制
输入:nums1 = [2], nums2 = []
输出:2.00000

提示:

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • -106 <= nums1[i], nums2[i] <= 106

我的答案:

大致思路就是把两个数组合并到一个新数组里,然后对这个数组进行排序,最后直接求中位数即可,粗暴。虽然通过了力扣测试,但并没有达到时间复杂度的要求。

代码语言:javascript
复制
class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int a = nums1.size();
        int b = nums2.size();

        vector<int> nums(a + b);
        int i = 0;
        for(; i < a; i++) {
            nums[i] = nums1[i];
        }
        for(; i < a + b; i++) {
            nums[i] = nums2[i - a];
        }

        for(int i = 0; i < a + b; i++) {
            for(int j = i; j < a + b; j++) {
                if(nums[i] > nums[j]) {
                    int temp = nums[i];
                    nums[i] = nums[j];
                    nums[j] = temp;
                }
            }
        }

        if((a + b) % 2) {
            return (double)nums[(a + b) / 2];
        } else {
            return (double)(nums[(a + b) / 2] + nums[(a + b) / 2 - 1]) / 2.0;
        }
    }
};
  • 执行用时: 236 ms
  • 内存消耗: 87.3 MB

官方答案

二分查找

在题解中,又给我们引入了新的方法:二分查找。

根据中位数的定义,当 m+n 是奇数时,中位数是两个有序数组中的第 (m+n)/22 个元素,当 m+n 是偶数时,中位数是两个有序数组中的第 (m+n)/2 个元素和第 (m+n)/2+1 个元素的平均值。因此,这道题可以转化成寻找两个有序数组中的第 k 小的数,其中 k 为 (m+n)/2 或 (m+n)/2+1。 假设两个有序数组分别是 A 和 B。要找到第 k 个元素,我们可以比较 A[k/2−1] 和 B[k/2−1],其中 / 表示整数除法。由于 A[k/2−1] 和 B[k/2−1] 的前面分别有 A[0 ~ k/2−2] 和 B[0 ~ k/2−2],即 k/2−1 个元素,对于 A[k/2−1] 和 B[k/2−1] 中的较小值,最多只会有 (k/2−1)+(k/2−1)≤k−2 个元素比它小,那么它就不能是第 k 小的数了。 因此我们可以归纳出三种情况:

  • 如果 A[k/2−1]<B[k/2−1],则比 A[k/2−1] 小的数最多只有 A 的前 k/2−1 个数和 B 的前 k/2−1 个数,即比 A[k/2−1] 小的数最多只有 k−2 个,因此 A[k/2−1] 不可能是第 k 个数,A[0] 到 A[k/2−1] 也都不可能是第 k 个数,可以全部排除。
  • 如果 A[k/2−1]>B[k/2−1],则可以排除 B[0] 到 B[k/2−1]。
  • 如果 A[k/2−1]=B[k/2−1],则可以归入第一种情况处理。

可以看到,比较 A[k/2−1] 和 B[k/2−1] 之后,可以排除 k/2 个不可能是第 k 小的数,查找范围缩小了一半。同时,我们将在排除后的新数组上继续进行二分查找,并且根据我们排除数的个数,减少 k 的值,这是因为我们排除的数都不大于第 k 小的数。 有以下三种情况需要特殊处理:

  • 如果 A[k/2−1] 或者 B[k/2−1] 越界,那么我们可以选取对应数组中的最后一个元素。在这种情况下,我们必须根据排除数的个数减少 k 的值,而不能直接将 k 减去 k/2。
  • 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 k 小的元素。
  • 如果 k=1,我们只要返回两个数组首元素的最小值即可。

用一个例子说明上述算法。假设两个有序数组如下:

代码语言:javascript
复制
A: 1 3 4 9
B: 1 2 3 4 5 6 7 8 9

两个有序数组的长度分别是 4 和 9,长度之和是 13,中位数是两个有序数组中的第 7 个元素,因此需要找到第 k=7 个元素。 比较两个有序数组中下标为 k/2−1=2 的数,即 A[2] 和 B[2],如下面所示:

代码语言:javascript
复制
A: 1 3 4 9
       ↑
B: 1 2 3 4 5 6 7 8 9
       ↑

由于 A[2]>B[2],因此排除 B[0] 到 B[2],即数组 B 的下标偏移(offset)变为 3,同时更新 k 的值:k=k−k/2=4。 下一步寻找,比较两个有序数组中下标为 k/2−1=1 的数,即 A[1] 和 B[4],如下面所示,其中方括号部分表示已经被排除的数。

代码语言:javascript
复制
A: 1 3 4 9
     ↑
B: [1 2 3] 4 5 6 7 8 9
             ↑

由于 A[1]<B[4],因此排除 A[0] 到 A[1],即数组 A 的下标偏移变为 2,同时更新 k 的值:k=k−k/2=2。 下一步寻找,比较两个有序数组中下标为 k/2−1=0 的数,即比较 A[2] 和 B[3],如下面所示,其中方括号部分表示已经被排除的数。

代码语言:javascript
复制
A: [1 3] 4 9
         ↑
B: [1 2 3] 4 5 6 7 8 9
           ↑

由于 A[2]=B[3],根据之前的规则,排除 A 中的元素,因此排除 A[2],即数组 A 的下标偏移变为 3,同时更新 k 的值: k=k−k/2=1。 由于 k 的值变成 1,因此比较两个有序数组中的未排除下标范围内的第一个数,其中较小的数即为第 k 个数,由于 A[3]>B[3],因此第 k 个数是 B[3]=4。

代码语言:javascript
复制
A: [1 3 4] 9
           ↑
B: [1 2 3] 4 5 6 7 8 9
           ↑

作者:LeetCode-Solution

研读了题解之后,我又重新更改了一遍代码:

代码语言:javascript
复制
class Solution {
public:
    int getNum(vector<int>& nums1, vector<int>& nums2, int k) {
        int a = nums1.size();
        int b = nums2.size();
        int index1 = 0, index2 = 0;

        while(1) {
            if(index1 == a) {
                return nums2[index2 + k - 1];
            }
            if(index2 == b) {
                return nums1[index1 + k - 1];
            }
            if(k == 1) {
                return min(nums1[index1], nums2[index2]);
            }

            int newind1 = min(index1 + k / 2 - 1, a - 1);
            int newind2 = min(index2 + k / 2 - 1, b - 1);

            if(nums1[newind1] <= nums2[newind2]) {
                k -= newind1 - index1 + 1;
                index1 = newind1 + 1;
            } else {
                k -= newind2 - index2 + 1;
                index2 = newind2 + 1;
            }
        }
    }

    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int len = nums1.size() + nums2.size();
        if(len % 2) {
            return getNum(nums1, nums2, len / 2 + 1);
        } else {
            return (getNum(nums1, nums2, len / 2) + getNum(nums1, nums2, len / 2 + 1)) / 2.0;
        }
    }
};
  • 执行用时: 20 ms
  • 内存消耗: 86.9 MB

虽然代码理论复杂了不少,但不可否认的是,运行快了很多。

用 Java 再次练习了一遍:

代码语言:javascript
复制
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int len = nums1.length + nums2.length;
        if(len % 2 == 1) {
            return getNum(nums1, nums2, len / 2 + 1);
        } else {
            return (getNum(nums1, nums2, len / 2) + getNum(nums1, nums2, len / 2 + 1)) / 2.0;
        }
    }

    public int getNum(int[] nums1, int[] nums2, int k) {
        int a = nums1.length;
        int b = nums2.length;
        int index1 = 0, index2 = 0;

        while(true) {
            if(index1 == a) {
                return nums2[index2 - 1 + k];
            }
            if(index2 == b) {
                return nums1[index1 - 1 + k];
            }
            if(k == 1) {
                return Math.min(nums1[index1], nums2[index2]);
            }

            int newind1 = Math.min(index1 + k / 2 - 1, a - 1);
            int newind2 = Math.min(index2 + k / 2 - 1, b - 1);

            if(nums1[newind1] <= nums2[newind2]) {
                k -= newind1 - index1 + 1;
                index1 = newind1 + 1;
            } else {
                k -= newind2 - index2 + 1;
                index2 = newind2 + 1;
            }
        }
    }
}
  • 执行用时: 3 ms
  • 内存消耗: 39.4 MB

这里挖个坑, 为什么相同代码 Java 的内存消耗和执行用时都比 C++ 要小好多?

划分数组

在这道题的官方题解中,还提到了第二种方法: 划分数组。

说明 方法一的时间复杂度已经很优秀了,但本题存在时间复杂度更低的一种方法。这里给出推导过程,勇于挑战自己的读者可以进行尝试。 思路与算法 为了使用划分的方法解决这个问题,需要理解「中位数的作用是什么」。在统计中,中位数被用来: 将一个集合划分为两个长度相等的子集,其中一个子集中的元素总是大于另一个子集中的元素。 如果理解了中位数的划分作用,就很接近答案了。

作者:LeetCode-Solution

再次用 C++ 练习了一下这个方法:

代码语言:javascript
复制
class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        if(nums1.size() > nums2.size()) {
            return findMedianSortedArrays(nums2, nums1);
        }

        int a = nums1.size();
        int b = nums2.size();
        int left = 0, right = a;
        int medmax = 0, medmin = 0;

        while(left <= right) {
            int i = (left + right) / 2;
            int j = (a + b + 1) / 2 - i;

            int med_in1 = i == 0 ? INT_MIN : nums1[i-1];
            int med_in2 = j == 0 ? INT_MIN : nums2[j-1];
            int med_ax1 = i == a ? INT_MAX : nums1[i];
            int med_ax2 = j == b ? INT_MAX : nums2[j];

            if(med_in1 <= med_ax2) {
                medmax = max(med_in1, med_in2);
                medmin = min(med_ax1, med_ax2);
                left = i + 1;
            } else {
                right = i - 1;
            }
        }

        return (a + b) % 2 == 0 ? (medmax + medmin) / 2.0 : medmax;
    }
};
  • 执行用时: 28 ms
  • 内存消耗: 86.7 MB

代码不算很复杂,但推理的过程确实非常巧妙。

今天还是只搞这一道题,一直对时间/空间复杂度的概念不是很清楚,这里记录一下来自维基百科的解释:

算法效率

时间复杂度

空间复杂度

在计算机科学中,一个算法或程序的空间复杂度定性地描述该算法或程序运行所需要的存储空间大小。空间复杂度是相应计算问题的输入值的长度的函数,它表示一个算法完全执行所需要的存储空间大小。

和时间复杂度类似,空间复杂度通常也使用大O记号来渐进地表示,例如O(n)、O(nlogn)、

等;其中nnn用来表示输入的长度,该值可以影响算法的空间复杂度。

就像时间复杂度的计算不考虑算法所使用的空间大小一样,空间复杂度也不考虑算法运行需要的时间长短。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-11-15,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • LeetCode 刷题笔记——day 3
    • 4. 寻找两个正序数组的中位数
      • 我的答案:
      • 官方答案
    • 算法效率
      • 时间复杂度
      • 空间复杂度
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档