【小Y学算法】⚡️每日LeetCode打卡⚡️——4.寻找两个正序数组的中位数

218 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。


📢前言

  • 🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻
  • 🌲 每天打卡一道算法题,既是一个学习过程,又是一个分享的过程😜
  • 🌲 提示:本专栏解题 编程语言一律使用 C# 和 Java 两种进行解题
  • 🌲 要保持一个每天都在学习的状态,让我们一起努力成为算法大神吧🧐!
  • 🌲 今天是力扣算法题持续打卡第4天🎈!
  • 🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻

🌲原题样例

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

示例 1:

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

示例 2:

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

示例 3:

输入:nums1 = [0,0], nums2 = [0,0] 输出:0.00000

示例 4:

输入:nums1 = [], nums2 = [1] 输出:1.00000 示例 5:

输入: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


🌻C#方法一:合并List根据长度找中位数

解题思路

new 一个 List , 并将 nums1 和 nums2 都添加到list 中,然后进行排序。对于排序后的 list, 根据长度计算出中位数的index,进而计算出最终结果。

假设合并后的list长度为13,则从小到大第7个数字为中位数,resultIndex=6;

假设合并后的list长度为14,则从小到大第7,8个数字的平均值为中位数,index 分别为 6,7,此时resultIndex =7,resultIndex-1 =6 , 结果为 ( list[resultIndex-1] + list[resultIndex] ) / 2.0 ;

public class Solution {
     public double FindMedianSortedArrays(int[] nums1, int[] nums2)
    {
        int m = nums1.Length;
        int n = nums2.Length;
        int len = m + n;
        var resultIndex = len / 2;
        List<int> list = new List<int>(nums1);
        list.AddRange(nums2);
        list.Sort();
        if (len % 2 == 0)
        {
            return (list[resultIndex - 1] + list[resultIndex]) / 2.0;
        }
        else
        {
            return list[resultIndex];
        }
    }

}

执行结果

执行通过,执行用时108ms,内存消耗28 MB.

复杂度分析 时间复杂度:O( (m+n)(1+log(m+n) )) 将长度为m,n的两个数组添加到list,复杂度分别为常数级的m和n ;list.Sort()的复杂度根据官方文档可得为 (m+n)log(m+n),所以该方法时间复杂度为 O( m+n+(m+n)log(m+n) ) = O( (m+n)(1+log(m+n) ))

空间复杂度:O(m+n) 使用list的长度为m+n.

🌻C#方法一:归并排序后根据长度找中位数

方法一使用了list.Sort() 方法,可以对list进行排序,但是,若题目给出的nums1 和 nums2 是无序数组,使用 list.Sort() 才算是 物有所用。 本题中 nums1 和 nums2 是有序数组,所以使用 list.Sort() 有写 杀鸡用宰牛刀的感觉,换句话说,这里面存在着效率的浪费。我们可以利用 【nums1 和 nums2 是有序数组】 这个条件,来精简我们的排序。

public class Solution {
    public double FindMedianSortedArrays(int[] nums1, int[] nums2)
    {
        // nums1 与 nums2 有序添加到list中
        List<int> list = new List<int>();
        int i = 0, j = 0;
        int m = nums1.Length;
        int n = nums2.Length;
        int len = m + n;
        var resultIndex = len / 2;

        while (i < m && j < n)
        {
            if (nums1[i] < nums2[j])
                list.Add(nums1[i++]);
            else
                list.Add(nums2[j++]);
        }
        while (i < m) list.Add(nums1[i++]);
        while (j < n) list.Add(nums2[j++]);

        if (len % 2 == 0)
        {
            return (list[resultIndex - 1] + list[resultIndex]) / 2.0;
        }
        else
        {
            return list[resultIndex];
        }
    }

}

执行结果 执行结果 通过,执行用时 112ms,内存消耗 28.1MB

复杂度分析 时间复杂度:O(m+n) i 和 j 一起把长度为 m 和 n 的两个数组遍历了一遍,所以时间复杂度为 O(m+n)

空间复杂度:O(m+n) 使用list的长度为m+n.


🌻Java方法一:二分查找(官方解法)

解题思路 给定两个有序数组,要求找到两个有序数组的中位数,最直观的思路有以下两种:

  • 使用归并的方式,合并两个有序数组,得到一个大的有序数组。大的有序数组的中间位置的元素,即为中位数。

  • 不需要合并两个有序数组,只要找到中位数的位置即可。由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的。维护两个指针,初始时分别指向两个数组的下标 00 的位置,每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。

假设两个有序数组的长度分别为 mm 和 nn,上述两种思路的复杂度如何?

第一种思路的时间复杂度是 O(m+n)O(m+n),空间复杂度是 O(m+n)O(m+n)。第二种思路虽然可以将空间复杂度降到 O(1)O(1),但是时间复杂度仍是 O(m+n)O(m+n)。

如何把时间复杂度降低到 O(\log(m+n))O(log(m+n)) 呢?如果对时间复杂度的要求有 \loglog,通常都需要用到二分查找,这道题也可以通过二分查找实现。

根据中位数的定义,当 m+nm+n 是奇数时,中位数是两个有序数组中的第 (m+n)/2(m+n)/2 个元素,当 m+nm+n 是偶数时,中位数是两个有序数组中的第 (m+n)/2(m+n)/2 个元素和第 (m+n)/2+1(m+n)/2+1 个元素的平均值。因此,这道题可以转化成寻找两个有序数组中的第 kk 小的数,其中 kk 为 (m+n)/2(m+n)/2 或 (m+n)/2+1(m+n)/2+1。

假设两个有序数组分别是 \text{A}A 和 \text{B}B。要找到第 kk 个元素,我们可以比较 \text{A}[k/2-1]A[k/2−1] 和 \text{B}[k/2-1]B[k/2−1],其中 // 表示整数除法。由于 \text{A}[k/2-1]A[k/2−1] 和 \text{B}[k/2-1]B[k/2−1] 的前面分别有 \text{A}[0,..,k/2-2]A[0..k/2−2] 和 \text{B}[0,..,k/2-2]B[0..k/2−2],即 k/2-1k/2−1 个元素,对于 \text{A}[k/2-1]A[k/2−1] 和 \text{B}[k/2-1]B[k/2−1] 中的较小值,最多只会有 (k/2-1)+(k/2-1) \leq k-2(k/2−1)+(k/2−1)≤k−2 个元素比它小,那么它就不能是第 kk 小的数了。

因此我们可以归纳出三种情况:

  • 如果 \text{A}[k/2-1] < \text{B}[k/2-1]A[k/2−1]<B[k/2−1],则比 \text{A}[k/2-1]A[k/2−1] 小的数最多只有 \text{A}A 的前 k/2-1k/2−1 个数和 \text{B}B 的前 k/2-1k/2−1 个数,即比 \text{A}[k/2-1]A[k/2−1] 小的数最多只有 k-2k−2 个,因此 \text{A}[k/2-1]A[k/2−1] 不可能是第 kk 个数,\text{A}[0]A[0] 到 \text{A}[k/2-1]A[k/2−1] 也都不可能是第 kk 个数,可以全部排除。

  • 如果 \text{A}[k/2-1] > \text{B}[k/2-1]A[k/2−1]>B[k/2−1],则可以排除 \text{B}[0]B[0] 到 \text{B}[k/2-1]B[k/2−1]。

  • 如果 \text{A}[k/2-1] = \text{B}[k/2-1]A[k/2−1]=B[k/2−1],则可以归入第一种情况处理。 在这里插入图片描述

可以看到,比较 \text{A}[k/2-1]A[k/2−1] 和 \text{B}[k/2-1]B[k/2−1] 之后,可以排除 k/2k/2 个不可能是第 kk 小的数,查找范围缩小了一半。同时,我们将在排除后的新数组上继续进行二分查找,并且根据我们排除数的个数,减少 kk 的值,这是因为我们排除的数都不大于第 kk 小的数。

有以下三种情况需要特殊处理:

  • 如果 \text{A}[k/2-1]A[k/2−1] 或者 \text{B}[k/2-1]B[k/2−1] 越界,那么我们可以选取对应数组中的最后一个元素。在这种情况下,我们必须根据排除数的个数减少 kk 的值,而不能直接将 kk 减去 k/2k/2。

  • 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 kk 小的元素。

  • 如果 k=1k=1,我们只要返回两个数组首元素的最小值即可。

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

A: 1 3 4 9 B: 1 2 3 4 5 6 7 8 9

两个有序数组的长度分别是 44 和 99,长度之和是 1313,中位数是两个有序数组中的第 77 个元素,因此需要找到第 k=7k=7 个元素。

比较两个有序数组中下标为 k/2-1=2k/2−1=2 的数,即 \text{A}[2]A[2] 和 \text{B}[2]B[2],如下面所示:

A: 1 3 4 9 ↑ B: 1 2 3 4 5 6 7 8 9 ↑

由于 \text{A}[2] > \text{B}[2]A[2]>B[2],因此排除 \text{B}[0]B[0] 到 \text{B}[2]B[2],即数组 \text{B}B 的下标偏移(offset)变为 33,同时更新 kk 的值:k=k-k/2=4k=k−k/2=4。

下一步寻找,比较两个有序数组中下标为 k/2-1=1k/2−1=1 的数,即 \text{A}[1]A[1] 和 \text{B}[4]B[4],如下面所示,其中方括号部分表示已经被排除的数。

A: 1 3 4 9 ↑ B: [1 2 3] 4 5 6 7 8 9 ↑

由于 \text{A}[1] < \text{B}[4]A[1]<B[4],因此排除 \text{A}[0]A[0] 到 \text{A}[1]A[1],即数组 \text{A}A 的下标偏移变为 22,同时更新 kk 的值:k=k-k/2=2k=k−k/2=2。

下一步寻找,比较两个有序数组中下标为 k/2-1=0k/2−1=0 的数,即比较 \text{A}[2]A[2] 和 \text{B}[3]B[3],如下面所示,其中方括号部分表示已经被排除的数。

A: [1 3] 4 9 ↑ B: [1 2 3] 4 5 6 7 8 9 ↑

由于 \text{A}[2]=\text{B}[3]A[2]=B[3],根据之前的规则,排除 \text{A}A 中的元素,因此排除 \text{A}[2]A[2],即数组 \text{A}A 的下标偏移变为 33,同时更新 kk 的值: k=k-k/2=1k=k−k/2=1。

由于 kk 的值变成 11,因此比较两个有序数组中的未排除下标范围内的第一个数,其中较小的数即为第 kk 个数,由于 \text{A}[3] > \text{B}[3]A[3]>B[3],因此第 kk 个数是 \text{B}[3]=4B[3]=4。

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;
            double median = getKthElement(nums1, nums2, midIndex + 1);
            return median;
        } else {
            int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
            double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
            return median;
        }
    }

    public int getKthElement(int[] nums1, int[] nums2, int k) {
        /* 主要思路:要找到第 k (k>1) 小的元素,那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
         * 这里的 "/" 表示整除
         * nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
         * nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
         * 取 pivot = min(pivot1, pivot2),两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
         * 这样 pivot 本身最大也只能是第 k-1 小的元素
         * 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组
         * 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组
         * 由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数
         */

        int length1 = nums1.length, length2 = nums2.length;
        int index1 = 0, index2 = 0;
        int kthElement = 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]);
            }
            
            // 正常情况
            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];
            if (pivot1 <= pivot2) {
                k -= (newIndex1 - index1 + 1);
                index1 = newIndex1 + 1;
            } else {
                k -= (newIndex2 - index2 + 1);
                index2 = newIndex2 + 1;
            }
        }
    }
}

链接:https://leetcode-cn.com/problems/median-of-two-sorted-arrays/solution/xun-zhao-liang-ge-you-xu-shu-zu-de-zhong-wei-s-114/

执行结果 执行结果 通过,执行用时 2ms,内存消耗 39.9MB

复杂度分析

时间复杂度:O(\log(m+n))O(log(m+n)) 空间复杂度:O(1)


🌻Java方法二:第k小数

上边的两种思路,时间复杂度都达不到题目的要求 O(log(m+n)O(log(m+n)。看到 log,很明显,我们只有用到二分的方法才能达到。我们不妨用另一种思路,题目是求中位数,其实就是求第 k 小数的一种特殊情况,而求第 k 小数有一种算法。

解法二中,我们一次遍历就相当于去掉不可能是中位数的一个值,也就是一个一个排除。由于数列是有序的,其实我们完全可以一半儿一半儿的排除。假设我们要找第 k 小数,我们可以每次循环排除掉 k/2 个数。看下边一个例子。

假设我们要找第 7 小的数字。 在这里插入图片描述

我们比较两个数组的第 k/2 个数字,如果 k 是奇数,向下取整。也就是比较第 33 个数字,上边数组中的 44 和下边数组中的 33,如果哪个小,就表明该数组的前 k/2 个数字都不是第 k 小数字,所以可以排除。也就是 11,22,33 这三个数字不可能是第 77 小的数字,我们可以把它排除掉。将 13491349 和 4567891045678910 两个数组作为新的数组进行比较。

更一般的情况 A[1] ,A[2] ,A[3],A[k/2] ... ,B[1],B[2],B[3],B[k/2] ... ,如果 A[k/2]<B[k/2] ,那么A[1],A[2],A[3],A[k/2]都不可能是第 k 小的数字。

A 数组中比 A[k/2] 小的数有 k/2-1 个,B 数组中,B[k/2] 比 A[k/2] 小,假设 B[k/2] 前边的数字都比 A[k/2] 小,也只有 k/2-1 个,所以比 A[k/2] 小的数字最多有 k/1-1+k/2-1=k-2个,所以 A[k/2] 最多是第 k-1 小的数。而比 A[k/2] 小的数更不可能是第 k 小的数了,所以可以把它们排除。

橙色的部分表示已经去掉的数字。 在这里插入图片描述

由于我们已经排除掉了 3 个数字,就是这 3 个数字一定在最前边,所以在两个新数组中,我们只需要找第 7 - 3 = 4 小的数字就可以了,也就是 k = 4。此时两个数组,比较第 2 个数字,3 < 5,所以我们可以把小的那个数组中的 1 ,3 排除掉了。 在这里插入图片描述

我们又排除掉 2 个数字,所以现在找第 4 - 2 = 2 小的数字就可以了。此时比较两个数组中的第 k / 2 = 1 个数,4 == 4,怎么办呢?由于两个数相等,所以我们无论去掉哪个数组中的都行,因为去掉 1 个总会保留 1 个的,所以没有影响。为了统一,我们就假设 4 > 4 吧,所以此时将下边的 4 去掉。 在这里插入图片描述

由于又去掉 1 个数字,此时我们要找第 1 小的数字,所以只需判断两个数组中第一个数字哪个小就可以了,也就是 4。

所以第 7 小的数字是 4。

我们每次都是取 k/2 的数进行比较,有时候可能会遇到数组长度小于 k/2的时候。 在这里插入图片描述

此时 k / 2 等于 3,而上边的数组长度是 2,我们此时将箭头指向它的末尾就可以了。这样的话,由于 2 < 3,所以就会导致上边的数组 1,2 都被排除。造成下边的情况。 在这里插入图片描述

由于 2 个元素被排除,所以此时 k = 5,又由于上边的数组已经空了,我们只需要返回下边的数组的第 5 个数字就可以了。

从上边可以看到,无论是找第奇数个还是第偶数个数字,对我们的算法并没有影响,而且在算法进行中,k 的值都有可能从奇数变为偶数,最终都会变为 1 或者由于一个数组空了,直接返回结果。

所以我们采用递归的思路,为了防止数组长度小于 k/2,所以每次比较 min(k/2,len(数组) 对应的数字,把小的那个对应的数组的数字排除,将两个新数组进入递归,并且 k 要减去排除的数字的个数。递归出口就是当 k=1 或者其中一个数字长度是 0 了。

代码

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int n = nums1.length;
    int m = nums2.length;
    int left = (n + m + 1) / 2;
    int right = (n + m + 2) / 2;
    //将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。
    return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;  
}
    
    private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {
        int len1 = end1 - start1 + 1;
        int len2 = end2 - start2 + 1;
        //让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1 
        if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k);
        if (len1 == 0) return nums2[start2 + k - 1];

        if (k == 1) return Math.min(nums1[start1], nums2[start2]);

        int i = start1 + Math.min(len1, k / 2) - 1;
        int j = start2 + Math.min(len2, k / 2) - 1;

        if (nums1[i] > nums2[j]) {
            return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
        }
        else {
            return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
        }
    }
链接:https://leetcode-cn.com/problems/median-of-two-sorted-arrays/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-w-2/

执行结果 执行结果 通过,执行用时 2ms,内存消耗 39.8MB

复杂度分析

时间复杂度: O(log(m+n)O(log(m+n) 空间复杂度:O(1)


💬总结

  • 今天是力扣算法题打卡的第四天!今天的题有点难,借助力扣大神题解看了半天!
  • 文章采用 C# 和 Java 两种编程语言进行解题
  • 一些方法也是参考力扣大神写的,也是边学习边分享,再次感谢算法大佬们
  • 那今天的算法题分享到此结束啦,明天再见!

请添加图片描述