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

目录

题目

解题思路

官方解法

1.二分查找

2.划分数组

合并数组解法

优化合并数组算法(消耗的空间只有标准合并的一半)


题目

解题思路

这题虽然标记的是困难,但是很明显我们可以看出,一点也不困难,拍脑子一下就能想到的办法就是合并数组并且再去中间数就可以了。如果你没有这个想法,说明你不一定学过归并,那建议还是先去把算法学一下再来刷题,可能会轻松不少。

先给大家看一下,我的解题还是比较好看的[doge],但是因为是内存分布,波动还是比较大的,因为我主要采用的是合并数组,虽然我也不知道为什么内存远超人家二分的,但是只能说是比较看运气的。那每次我遥遥领先的时候,我都会让大家看看官方的解法情况,那我们先来看看官方的解法。

官方解法

1.二分查找

二分查找,相比大家也都不陌生,因为题目里有说明,是两个正序的数组,所以我们可以使用二分查找。那假如真有不知道的兄弟们也可以看我简述一番,二分查找就是砍半查,然后再砍半再查,就是这么个过程,然后如何判断取决于实际情况。

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;
            }
        }
    }
}

对于这个代码,其实也很简单,就是使用二分之前我们先判断两个数组的长度合起来是否为奇数,如果是奇数说明中位数只有一个,如果是偶数,说明要先找到中间两个,我们用10来举例子,10/2=5,所以中间数一个是5,另外一个呢,很明显是6,我们再看看其他数字,比如说20,20/2=10,所以中间数是10和11,那为什么代码里不是+1而是-1呢,那必然是因为,我们的下标是从0开始,所以要-1。

然后做两个数组同时的二分的时候,用了一个值为k,但是官方的操作有点不太好让他除以了2,看过以往文章的观众们应该知道,计算机里计算乘除想当的消耗资源,既然操作的乘与除的数是2的倍数,那我们应该选择使用右移或者左移,这里应该写k >> 1。

二分的操作是先取到中间然后比较谁小谁涨下标。并且用k值减掉已经去掉的下标,模拟了单个数组的二分操作。

2.划分数组

我们上来先提出的思路就是合并数组当然,合并数组和划分数组还是两个不一样的操作的,我们来看看官方的代码。

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        if (nums1.length > nums2.length) {
            return findMedianSortedArrays(nums2, nums1);
        }

        int m = nums1.length;
        int n = nums2.length;
        int left = 0, right = m;
        // median1:前一部分的最大值
        // median2:后一部分的最小值
        int median1 = 0, median2 = 0;

        while (left <= right) {
            // 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
            // 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
            int i = (left + right) / 2;
            int j = (m + n + 1) / 2 - i;

            // nums_im1, nums_i, nums_jm1, nums_j 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
            int nums_im1 = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
            int nums_i = (i == m ? Integer.MAX_VALUE : nums1[i]);
            int nums_jm1 = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
            int nums_j = (j == n ? Integer.MAX_VALUE : nums2[j]);

            if (nums_im1 <= nums_j) {
                median1 = Math.max(nums_im1, nums_jm1);
                median2 = Math.min(nums_i, nums_j);
                left = i + 1;
            } else {
                right = i - 1;
            }
        }

        return (m + n) % 2 == 0 ? (median1 + median2) / 2.0 : median1;
    }
}

不难发现,我们可以看到官方用了个递归操作,但是这个递归并不是真正的递归,而是确定num1的长度是最长的。然后基于num1是最长的进行运算。简述一下他的循环过程就是他获得了最接近中间的两位数,然后直接根据是否为奇数进行返回,因为如果是奇数的话那他就只有一个中位数,如果是偶数的话就有两个中位数,还需取两者的平均值。

合并数组解法

比起划分数组来说,归并的方式反而是最好理解的,因为你只要会归并排序你就会这个操作,但确实略微难于二分,我们直接来看代码。

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int[] arr = new int[nums1.length + nums2.length];
        int arrIndex = 0, sArrIndex = 0, bArrIndex = 0;
        double ans = 0;
        while (sArrIndex < nums1.length && bArrIndex < nums2.length) {
            if (nums1[sArrIndex] > nums2[bArrIndex])
                arr[arrIndex++] = nums2[bArrIndex++];
            else
                arr[arrIndex++] = nums1[sArrIndex++];
        }
        while (sArrIndex < nums1.length) {
            arr[arrIndex++] = nums1[sArrIndex++];
        }
        while (bArrIndex < nums2.length) {
            arr[arrIndex++] = nums2[bArrIndex++];
        }
        if (arr.length % 2 == 1) {
            ans = arr[arr.length >> 1];
        } else {
            int i = arr.length >> 1;
            ans = ((double) (arr[i] + arr[i - 1])) / 2;
        }
        return ans;
    }
}

很标准的合并数组操作,但是我们不难发现,其实这个操作是有点多余了,因为中位数的话他就在最中间,我们完全没有必要开辟一个两个数组大小的数组,然后再来合并这个数组,这样子太浪费空间了。所以我们可以通过上来就计算数组总长是奇数还是偶数我们来选择存到一半还是一半多一格。

优化合并数组算法(消耗的空间只有标准合并的一半)

我们继续上优化过的代码。

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int len1 = nums1.length, len2 = nums2.length;
        int length = ((len1 + len2) >> 1) + 1;
        int[] arr = new int[length];
        int arrIndex = 0, sArrIndex = 0, bArrIndex = 0;

        double ans = 0;
        while (sArrIndex < len1 && bArrIndex < len2 && arrIndex < length) {
            if (nums1[sArrIndex] > nums2[bArrIndex])
                arr[arrIndex++] = nums2[bArrIndex++];
            else
                arr[arrIndex++] = nums1[sArrIndex++];
        }
        while (sArrIndex < len1 && arrIndex < length) {
            arr[arrIndex++] = nums1[sArrIndex++];
        }
        while (bArrIndex < len2 && arrIndex < length) {
            arr[arrIndex++] = nums2[bArrIndex++];
        }
        if ((len1 + len2) % 2 == 1) {
            ans = arr[length - 1];
        } else {
            ans = ((double) (arr[length - 1] + arr[length - 2])) / 2;
        }
        return ans;
    }
}

我们直接让两个合起来除以2作为他的长度,这里我们必然是使用右移一位作为除以2的操作然后再加上一位,假如他长度是9就说明是012345678中的4对吧,那我们需要存5位,然后9/2之后是4因为整型,所以我们需要加1。

假如他是10也就是偶数的话那就是0123456789的下标,我们需要的是4与5,所以长度得是6,所以加一在这里也是正好够用的情况,我们在返回的时候就根据他的长度是奇还是偶选择返回是一位还是两位一起运算一下。

我这里说道我用了>>1代替除以2的时候,是不是有小伙伴发现我们在末尾返回偶数的时候使用了除法,这里必须强调一点,我们想要使用位移的时候必须是整型,但是ans是double类型,所以这是没办法的,必须使用/2了。

对你如果有帮助的话不要忘记点赞收藏一下。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值