LeetCode004——两个排序数组的中位数

https://blog.csdn.net/qq_41231926/article/details/81805795
https://leetcode-cn.com/articles/median-of-two-sorted-arrays/

题目描述:

知识点:二分查找,分治算法

思路一:设两个指针依次从小到大遍历两个有序数组,得到中位数

这是我的第一个想法,求中位数不就是求中间那个数吗?而给我们的又是两个有序数组,我们完全可以设两个指针分别遍历两个数组,将其值存入另一个数组中。

这里需要注意的是中位数的下标值问题,我们假设nums1数组的长度为n1,nums2数组的长度为n2,假设将nums1数组和nums2数组合并成一个数组,那么该数组的长度为n1 + n2,我们要求的中位数就是该新数组中的中位数。如果n1 + n2是一个偶数,那么我们要求的中位数是两个数的平均值,这两个数的下标为(n1 + n2) / 2和(n1 + n2) / 2 - 1。如果n1 + n2是一个奇数,那么我们要求的中位数就是一个数,该数的下标为(n1 + n2) / 2。因此我们设合并数组的长度的时候完全没必要将其设为n1 + n2,只需设为(n1 + n2) / 2 + 1即可。

对于这样的解法,时间复杂度是O(m + n)级别的,其中m表示nums1数组的长度,n表示nums2数组的长度,显然不满足题目要求的O(log(m + n))的时间复杂度的要求。但我们还是可以把这个解法当作一个思路,而且这个解法在LeetCode中是能够得到通过的。

JAVA代码:


 
 
  1. public class Solution {
  2. public double findMedianSortedArrays(int[] nums1, int[] nums2) {
  3. int n1 = nums1.length;
  4. int n2 = nums2.length;
  5. int i = 0;
  6. int j = 0;
  7. int[] array = new int[(n1 + n2) / 2 + 1];
  8. int index = 0;
  9. while(index < array.length) {
  10. if(i < n1 && j < n2) {
  11. if(nums1[i] < nums2[j]) {
  12. array[index] = nums1[i];
  13. i++;
  14. } else {
  15. array[index] = nums2[j];
  16. j++;
  17. }
  18. } else if(i >= n1 && j < n2) {
  19. array[index] = nums2[j];
  20. j++;
  21. } else if(i < n1 && j >= n2) {
  22. array[index] = nums1[i];
  23. i++;
  24. }
  25. index++;
  26. }
  27. if((n1 + n2) % 2 == 0) {
  28. return 0.5 * (array[array.length - 1] + array[array.length - 2]);
  29. } else {
  30. return array[array.length - 1];
  31. }
  32. }
  33. }

LeetCode解题报告:

思路二:转换问题思路为在两个有序数组中求第k小的元素

题目要求的时间复杂度是O(log(m + n)),要产生这样级别的时间复杂度我们只有采用二分查找法,用分治递归的思路来考虑这个问题。

为了能更明确地使用分治递归的思路来解决问题,我们需要转换题目中求中位数的问题为求第k小数的问题,这里我定义我的k是从1开始计数的。这是解决问题最重要的一步。

如果两个数组的长度和是奇数,那么我们只需寻找第(m + n) / 2 + 1小的数即可。如果两个数组的长度和是偶数,那么我们还需要寻找第(m + n) / 2小的数,并取两数的平均值。

接下来,我们的问题就变成了:如何在两个有序数组中以O(log(m + n))的时间复杂度求第k小的数

在求解整个问题的过程中,我们始终需要考虑一个很重要的问题——数组索引越界问题。

下面将详细地分析整个递归流程。

(1)首先是我们递归函数的定义,我们定义我们的递归函数的作用为:寻找两个有序数组nums1数组中[left1, right1]范围内和nums2数组[left2, right2]范围内第k小的数,k从1开始计数。由于在Java语言中没有指针这一概念,对于数组的操作我们只能通过索引来进行,因此我们需要传入7个变量,分别是nums1数组,nums1数组的寻找范围的左边界left1,nums1数组的寻找范围的右边界right1,nums2数组,nums2数组的寻找范围的左边界left2,nums2数组的寻找范围的右边界right2,需要寻找第k小的元素的k值。

(2)用n1来记录nums1数组中寻找范围的长度,用n2来记录nums2数组中寻找范围的长度。

(3)如果我们要寻找的k大于了n1 + n2,这就像是在只有3个数的区域里寻找第4小的数,显然,这时候输入的k值是非法的,我们不可能求得解,我们直接抛出一个异常表明k值太大了。

(4)如果n1 > n2,我们将递归调用该函数,不过将与nums1数组有关的参数和与nums2数组有关的参数进行了换位操作。这样我们就确保了之后的操作都是在n1 <= n2的条件下进行的。事实上这一步操作完全可以省去,如果省去了这步操作,在后面的操作中我们要新增对n2为0的情况的分析。

(5)递归的终止条件

a.当n1为0时,说明只有一个数组中有元素,我们直接取该数组中的第left2 + k - 1位元素即可。

b.当k为1时,说明我们要取的是两个有序数组中的最小值,我们直接返回两个数组的第一个元素中的较小值即可。

(6)递归过程

由于要求的是第k小的数,而且是在两个有序数组中求。我们分不是按照两个数组的长度来分,而是按照k值来分。我们取一个变量i为n1和k / 2中的较小者,之所以这么取,是为了防止left1 + k / 2 - 1这个索引在数组nums1中已经越界了。再取一个变量j为n2和k / 2中的较小着,这也是为了防止left2 + k / 2 - 1这个索引在数组nums2中已经越界。

接下来我们比较nums1[left1 + k / 2 - 1]和nums2[left2 + k / 2 - 1]这两个值。

如果nums1[left1 + i - 1] <= nums2[left2 + j - 1],显然,nums1数组中索引为left1 + i - 1及之前的元素不可能是中位数,即我们缩小了查找范围,去除了nums1数组中[left1, left1 + i - 1]范围内的元素,我们递归调用该函数,此时在nums1中的查找范围变成了nums1[left1 + i, right1],我们要找的也不应该是第k小的元素,因为我们已经剔除了i个比k小的元素,因此我们要找的元素变成了第k - i小的元素。

如果nums1[left1 + i - 1] > nums2[left2 + j - 1],显然,nums2数组中索引为left2 + j - 1及之前的元素不可能是中位数,即我们缩小了查找范围,去除了nums2数组中[left2, left2 + j - 1]范围内的元素,我们递归调用该函数,此时在nums2中的查找范围变成了nums2[left2 + j, right2],我们要找的也不应该是第k小的元素,因为我们已经剔除了j个比k小的元素,因此我们要找的元素变成了第k - j小的元素。

为什么这个算法的时间复杂度是O(log(m + n))呢,因为如果假设数组长度足够长,我们每次剔除的元素都是k / 2,那么我们需要多少次才能找到第k小数呢?显然我们需要log(k)次,这和二分查找法是同理的,而我们要找的k值要么是(m + n) / 2 + 1,要么是在此基础之上再加上(m + n) / 2,因此我们算法的时间复杂度是O(log(m + n))级别的。

JAVA代码:


 
 
  1. public class Solution {
  2. public double findMedianSortedArrays(int[] nums1, int[] nums2) {
  3. int n1 = nums1.length;
  4. int n2 = nums2.length;
  5. int mid1 = findKthInTwoArrays(nums1, 0, n1 - 1, nums2, 0, n2 - 1, (n1 + n2) / 2 + 1);
  6. if((n1 + n2) % 2 != 0) {
  7. return mid1;
  8. } else {
  9. int mid2 = findKthInTwoArrays(nums1, 0, n1 - 1, nums2, 0, n2 - 1, (n1 + n2) / 2);
  10. return 0.5 * (mid1 + mid2);
  11. }
  12. }
  13. private int findKthInTwoArrays(int[] nums1, int left1, int right1, int[] nums2, int left2, int right2, int k) {
  14. int n1 = right1 - left1 + 1;
  15. int n2 = right2 - left2 + 1;
  16. if(k > n1 + n2) {
  17. throw new IllegalArgumentException( "The input k is too big!");
  18. }
  19. if(n1 == 0) {
  20. return nums2[left2 + k - 1];
  21. }
  22. if(k == 1) {
  23. return Math.min(nums1[left1], nums2[left2]);
  24. }
  25. int i = Math.min(n1, k / 2);
  26. int j = Math.min(n2, k / 2);
  27. if(nums1[left1 + i - 1] > nums2[left2 + j - 1]) {
  28. return findKthInTwoArrays(nums1, left1, right1, nums2, left2 + j, right2, k - j);
  29. } else {
  30. return findKthInTwoArrays(nums1, left1 + i, right1, nums2, left2, right2, k - i);
  31. }
  32. }
  33. }

LeetCode解题报告:

为什么O(log(m + n))时间复杂度的思路二比O(m + n)时间复杂度的思路一还要慢呢?

1.程序运行的不稳定性,就算是同一个程序,在LeetCode中运行两次,得到的用时可能是相差巨大的。因此我们在LeetCode中解题的,一般只要超过50%的提交记录即可视为满意。

2.常数级别的差异,我们比较复杂度的时候千万别忘了常数级别的差异,真正地比较复杂度,应该倍增输入数据规模,来观察用时的变化情况,而不是把两个不同复杂度的解法一起进行比较。具体如何比较可以参阅我的另一篇博文的结尾:https://blog.csdn.net/qq_41231926/article/details/81501389

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值