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

文章介绍了两种方法计算两个有序数组的合并后中位数:一种是通过归并排序合并数组后直接找中位数,另一种是利用二分查找技巧,分别在两个数组中移动指针。这两种方法都达到了O(log(m+n))的时间复杂度。
摘要由CSDN通过智能技术生成

使用归并的方式合并两个有序数组,并找出中位数:

通过归并排序合并两个数组,然后直接根据合并后数组的长度计算中位数。

class Solution {
    public double findMedianSortedArraysMerge(int[] nums1, int[] nums2) {
        int[] merged = new int[nums1.length + nums2.length];
        int i = 0, j = 0, k = 0;

        // 归并排序的合并过程
        while (i < nums1.length && j < nums2.length) {
            if (nums1[i] < nums2[j]) {
                merged[k++] = nums1[i++];
            } else {
                merged[k++] = nums2[j++];
            }
        }

        // 如果有剩余元素,直接复制到 merged 数组
        while (i < nums1.length) {
            merged[k++] = nums1[i++];
        }
        while (j < nums2.length) {
            merged[k++] = nums2[j++];
        }

        // 计算中位数
        if (merged.length % 2 == 0) {
            return (merged[merged.length / 2] 
                    + merged[merged.length / 2 - 1]) / 2.0;
        } else {
            return merged[merged.length / 2];
        }
    }
}

不合并数组,直接找到中位数的位置:

通过两个指针在两个数组中移动,直到找到中位数的位置。

class Solution {
    public double findMedianSortedArraysDirect(int[] nums1, int[] nums2) {
        int totalLength = nums1.length + nums2.length;
        int count = 0, i = 0, j = 0;
        int a = 0, b = 0; // 用于存储中间位置的数

        while (count <= totalLength / 2) {
            a = b; // 存储上一轮的 b,当总长度是偶数时需要用到

            // 移动指针,找到中位数的位置
            if (i < nums1.length && (j >= nums2.length || nums1[i] < nums2[j])) {
                b = nums1[i];
                i++;
            } else {
                b = nums2[j];
                j++;
            }
            count++;
        }

        // 根据总长度是奇数还是偶数,计算中位数
        if (totalLength % 2 == 0) {
            return (a + b) / 2.0;
        } else {
            return b;
        }
    }
}

二分查找:

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 计算两个数组的总长度
        int length1 = nums1.length, length2 = nums2.length;
        int totalLength = length1 + length2;
        // 判断总长度是奇数还是偶数,以决定如何计算中位数
        if (totalLength % 2 == 1) {
            // 如果是奇数,直接找到中间位置的数
            int midIndex = totalLength / 2;
            return getKthElement(nums1, nums2, midIndex + 1);
        } else {
            // 如果是偶数,找到中间两个数,计算平均值
            int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
            return (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
        }
    }

    // 主要逻辑方法,用于找到两个排序数组中的第 k 小的数
    public int getKthElement(int[] nums1, int[] nums2, int k) {
        int length1 = nums1.length, length2 = nums2.length;
        int index1 = 0, index2 = 0;

        while (true) {
            // 处理边界情况
            if (index1 == length1) {
                return nums2[index2 + k - 1];
            }
            if (index2 == length2) {
                return nums1[index1 + k - 1];
            }
            if (k == 1) {
                return Math.min(nums1[index1], nums2[index2]);
            }

            // 正常情况,比较两个数组中的第 k/2 个元素
            int half = k / 2;
            // 新的索引位置,不能超过数组长度
            int newIndex1 = Math.min(index1 + half, length1) - 1;
            int newIndex2 = Math.min(index2 + half, length2) - 1;
            int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
            // 比较并更新索引,减少 k 的值
            if (pivot1 <= pivot2) {
                k -= (newIndex1 - index1 + 1);
                index1 = newIndex1 + 1;
            } else {
                k -= (newIndex2 - index2 + 1);
                index2 = newIndex2 + 1;
            }
        }
    }
}

让我们一步步分析这段代码和其中的逻辑,以便更好地理解如何通过二分查找达到 O(log(m + n)) 的时间复杂度。

这个算法的关键在于每次迭代排除 (k/2) 个元素,而不是逐个排除,从而将时间复杂度降低到对数级别。

  1. 初始条件:我们有两个排序数组 nums1nums2,以及一个目标值 k,表示我们需要找到两个数组合并后的第 k 个元素。

  2. 二分查找的核心:在每一轮迭代中,我们比较 nums1[k/2 - 1]nums2[k/2 - 1]。根据二分查找的策略,我们可以排除较小值所在数组的前 k/2 个元素。

  3. 边界条件处理

    • 如果 nums1nums2 为空,直接从另一个数组中返回第 k 个元素。

    • 如果 k == 1,返回 nums1[0]nums2[0] 中的较小值。

    • 如果 nums1[k/2 - 1]nums2[k/2 - 1] 越界,我们应选择相应数组的最后一个元素进行比较。

  4. 递归或迭代:根据比较结果,我们排除一部分元素。例如,如果 nums1[k/2 - 1] < nums2[k/2 - 1],那么我们可以安全地排除 nums1 的前 k/2 个元素。接下来,我们更新 k 的值为 k - k/2,继续在剩下的元素中查找。

  5. 更新索引和 k 的值:根据排除的元素数量更新 index1index2,同时减少 k 的值,因为我们已经排除了一些元素,所需查找的位置也相应地减少。

通过这种方式,每次迭代我们至少排除 k/2 个元素,这就是算法能达到 O(log(m + n)) 时间复杂度的原因。每一轮迭代后,k 的值都会减半,最终在对数次迭代后达到递归基。这种方法避免了直接合并数组,从而大大提高了效率。

在这段代码中,findMedianSortedArrays 方法首先判断总长度是奇数还是偶数,并相应地调用 getKthElement 方法找到中位数或两个中位数的平均值。getKthElement 方法通过二分查找的方式逐渐缩小搜索范围,直到找到第 k 小的元素。通过每次排除掉 k/2 个元素,该方法有效地将时间复杂度降低到了O(log(m+n))。

FQA:

为什么维护为 k ?k 是干什么的?

理解为什么更新 (k) 为 (k - k/2) 是理解这个算法核心部分的关键。

假设我们有两个排序数组 (A) 和 (B),我们需要找到这两个数组合并后的第 (k) 小的元素。为了做到这一点,我们可以同时在两个数组中进行查找,比较两个数组中第 (k/2) 个元素(具体而言,是索引为 (k/2 - 1) 的元素)。

这里的关键点是,如果 (A[k/2 - 1]) 小于 (B[k/2 - 1]),那么 (A[k/2 - 1]) 以及它之前的所有元素(共 (k/2) 个元素)都不可能是第 (k) 小的元素。为什么呢?让我们来详细分析一下:

  1. (A[k/2 - 1]) 是数组 (A) 中的第 (k/2) 小的元素。

  2. (B[k/2 - 1]) 是数组 (B) 中的第 (k/2) 小的元素。

由于 (A[k/2 - 1]) 小于 (B[k/2 - 1]),我们可以确定,在 (B) 数组中至少有 (k/2) 个元素是大于 (A[k/2 - 1]) 的。因此,合并后的数组中,至少有 (k/2 + k/2 = k) 个元素是大于或等于 (A[k/2 - 1]) 的。

这意味着 (A[k/2 - 1]) 最好的情况是第 (k) 小的元素(如果 (A[k/2 - 1] = B[k/2 - 1])),但通常情况下,它的排名要低于第 (k) 小。因此,我们可以安全地排除 (A) 中的前 (k/2) 个元素,然后在剩下的元素中寻找第 (k - k/2) 小的元素。

通过这种方式,每一步我们都能排除掉一部分元素,从而缩小搜索范围,这是达到 (O(\log(m + n))) 时间复杂度的关键。

  • 23
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值