📢前言
🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻🌻
🌲 每天打卡一道算法题,既是一个学习过程,又是一个分享的过程😜
🌲 提示:本专栏解题 编程语言一律使用 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 两种编程语言进行解题
一些方法也是参考力扣大神写的,也是边学习边分享,再次感谢算法大佬们
那今天的算法题分享到此结束啦,明天再见!