力扣hot100之4:寻找两个正序数组的中位数
力扣hot100之4:寻找两个正序数组的中位数

LeetCode 4:寻找两个正序数组的中位数(Median of Two Sorted Arrays)题解

Median of Two Sorted Arrays 是 LeetCode 上非常经典的一道题。

题目要求给定两个已经按升序排列的整数数组 nums1nums2,请你找出这两个数组合并后的中位数,并返回这个中位数。

很多人看到这道题时,第一反应会想到“先合并,再取中位数”。 而这份代码实际上就是基于这个思路做了一个优化版的模拟合并:

  • 不真正创建完整合并数组

  • 只在遍历过程中记录中位数附近的两个值

  • 当遍历到中间位置时直接返回结果

这种写法虽然没有达到题目进阶要求的 O(log(m+n)),但思路清晰、实现稳定,非常适合作为理解这道题的第一步。


题目示例

输入:nums1 = [1,3], nums2 = [2]
输出:2.0

合并后的有序数组为:

[1,2,3]

中位数就是 2.0

再看一个例子:

输入:nums1 = [1,2], nums2 = [3,4]
输出:2.5

合并后的有序数组为:

[1,2,3,4]

因为总长度为偶数,中位数就是中间两个数 (2 + 3) / 2 = 2.5


解题思路

这段代码的核心思想其实很像“归并排序”中的合并过程:

  1. 使用两个指针分别指向两个数组当前未处理的位置

  2. 每次取两个数组中较小的那个数

  3. 按照从小到大的顺序不断取数

  4. 由于中位数只和中间位置有关,所以我们不需要真的把所有元素都存下来

  5. 只要记录当前取到的数,以及上一个取到的数,就可以在遍历到中间位置时算出答案

这样可以避免额外创建一个新数组,空间复杂度保持在 O(1)


Java 代码(带注释)

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // n 和 m 分别表示两个数组的长度
        int n = nums1.length;
        int m = nums2.length;

        // flag 用来判断总长度是否为偶数
        // 如果为 true,说明最后需要返回中间两个数的平均值
        boolean flag = ((m + n) % 2) == 0;

        // point1 和 point2 分别是两个数组的遍历指针
        int point1 = 0, point2 = 0;

        // remNum1 表示当前取到的数
        // remNum2 表示上一次取到的数
        // 这两个变量用于最后计算中位数
        int remNum1 = 0, remNum2 = 0;

        // 只需要遍历到总长度的一半位置即可
        for (int i = 0; i <= (n + m) / 2; i++) {
            // 在更新当前值之前,先把当前值保存到 remNum2
            remNum2 = remNum1;

            // 如果 nums1 还没遍历完,并且满足以下任一条件:
            // 1. nums2 已经遍历完
            // 2. nums1 当前值小于 nums2 当前值
            // 那么这一次就取 nums1[point1]
            if (point1 < n && (point2 >= m || nums1[point1] < nums2[point2])) {
                remNum1 = nums1[point1++];
            } else {
                // 否则取 nums2[point2]
                remNum1 = nums2[point2++];
            }
        }

        // 如果总长度为偶数,中位数是中间两个数的平均值
        if (flag) {
            return (remNum1 + remNum2) / 2.0;
        } else {
            // 如果总长度为奇数,中位数就是当前这个数
            return remNum1 * 1.0;
        }
    }
}

变量含义拆解

为了更容易理解这段代码,可以先把几个关键变量搞清楚。

point1point2

这两个变量分别表示:

  • point1:当前遍历到 nums1 的哪个位置

  • point2:当前遍历到 nums2 的哪个位置

它们的作用就像两个有序队列的指针。


remNum1remNum2

这两个变量是整段代码里最关键的部分:

  • remNum1:当前取出的元素

  • remNum2:上一个取出的元素

为什么要保存两个数?

因为当总长度是偶数时,中位数需要用到中间两个数的平均值。 所以必须同时保留当前值和前一个值。


flag

boolean flag = ((m + n) % 2) == 0;

这个变量用来判断两个数组合并后的总长度是否为偶数:

  • 偶数:返回中间两个数的平均值

  • 奇数:返回中间那个数


执行过程演示

以这组输入为例:

nums1 = [1,2]
nums2 = [3,4]

总长度为 4,属于偶数情况。

我们只需要遍历到下标 4 / 2 = 2 这个位置。

第 1 次循环

当前指针:

  • point1 = 0

  • point2 = 0

比较:

  • nums1[0] = 1

  • nums2[0] = 3

较小的是 1,所以:

remNum2 = 0
remNum1 = 1

第 2 次循环

比较:

  • nums1[1] = 2

  • nums2[0] = 3

较小的是 2,所以:

remNum2 = 1
remNum1 = 2

第 3 次循环

此时 nums1 已经遍历完,接下来只能取 nums2 的元素。

所以:

remNum2 = 2
remNum1 = 3

遍历结束后:

  • remNum2 = 2

  • remNum1 = 3

因为总长度是偶数,所以中位数为:

(2 + 3) / 2.0 = 2.5

为什么循环只到 (n + m) / 2

因为我们并不需要真的把整个合并数组都构造出来。

中位数只和中间位置有关:

  • 如果总长度是奇数,只需要第 ((n + m) / 2) 个元素

  • 如果总长度是偶数,只需要第 ((n + m) / 2) 个元素和它前一个元素

所以遍历到中间位置就够了,后面的元素不影响答案。


代码中的细节说明

为什么判断条件是这样写的?

if (point1 < n && (point2 >= m || nums1[point1] < nums2[point2]))

这句条件可以拆成两层理解。

第一层:

point1 < n

表示 nums1 还没有遍历完。

第二层:

point2 >= m || nums1[point1] < nums2[point2]

表示满足以下任意一种情况:

  • nums2 已经遍历完了,那只能从 nums1 里取

  • nums1[point1]nums2[point2] 更小,那也应该取 nums1

否则,就从 nums2 里取值。


为什么最后要写 / 2.0* 1.0

因为题目返回值类型是 double

如果写成:

(remNum1 + remNum2) / 2

那么在 Java 中会进行整数除法,结果可能出错。

所以要写成:

(remNum1 + remNum2) / 2.0

这样才能得到正确的小数结果。

同理:

return remNum1 * 1.0;

也是为了把结果转换成 double 类型。


复杂度分析

时间复杂度

O(m + n)

虽然代码没有遍历完整个数组,但最坏情况下仍然要扫描到中间位置,数量级仍然是 O(m + n)

更准确一点,也可以写成:

O((m + n) / 2)

但在复杂度表示中常数会被省略,所以最终写作 O(m + n)

空间复杂度

O(1)

因为整个过程中只使用了若干个变量,没有额外创建新的数组或集合。


这道题的特点

这段代码的优点很明显:

  • 思路自然,容易理解

  • 不需要额外数组

  • 比先完整合并再取中位数更节省空间

  • 很适合在面试或刷题初期快速写出正确答案

但也要注意一点:

这道题的进阶要求是时间复杂度为 O(log(m+n)),而当前这种写法还没有达到那个标准。

所以这份代码更适合当作:

这道题的基础解法 / 过渡解法

当你把这个过程理解清楚以后,再去学习二分解法会更容易。


总结

这道题本质上是在两个有序数组中,模拟合并的过程,然后找到中间位置对应的值。

整段代码最值得学习的地方有两个:

  1. 不真正创建合并数组,而是边遍历边记录答案

  2. remNum1remNum2 保存中位数附近的关键元素

如果你刚开始刷这道题,我很建议先把这种写法彻底理解透。

因为它能帮助你清楚地知道:

  • 中位数到底取哪个位置

  • 奇数长度和偶数长度有什么区别

  • 为什么只遍历到中间位置就够了

这些问题想明白之后,再去看更高阶的二分做法,理解成本会低很多。


适合直接发布的结尾

对于这道题,我觉得比较合适的学习顺序是:

  1. 先理解合并两个有序数组的过程

  2. 再理解为什么中位数只和中间位置有关

  3. 接着掌握这种 O(m+n) 的模拟写法

  4. 最后再挑战 O(log(m+n)) 的二分解法

这样学习会更扎实,也更容易真正吃透这道经典题。

如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇