编程语言
首页 > 编程语言> > 求两个有序数组合并后的上中位数的非递归算法实现 - JAVA版

求两个有序数组合并后的上中位数的非递归算法实现 - JAVA版

作者:互联网

package test;

public class FindMedian {
    // 求两个有序数组合并后的上中位数。折半方法(二分查找),时间复杂度为O(logN),其中N是小数组的长度
    // 中位数特性:1、数组一半不超过该值,一半不小于该值;2、从首尾各删除相同个数元素,中位数不变
    public static int findMedianNum(int[] arr1, int[] arr2) throws RuntimeException{
        // 判断存在空数组的情况,直接返回结果
        if (arr1 == null && arr2 == null) throw new RuntimeException("null arr");
        // 因为数组下标是从0开始,所以上中位数的下标为(startIndex + endIndex)/2,如一个长度为12的数组,中位数下标为(0+11)/2=5,即第六个元素
        else if (arr1 == null || arr1.length == 0) return arr2[(arr2.length - 1)/2];
        else if (arr2 == null || arr2.length == 0) return arr1[(arr1.length -1 )/2];
        // 将长度小的数组放在前面,便于判断和二分,因为每次两个数组都要减掉小数组长度的二分之一
        int[] tempArr1 = arr1, tempArr2 = arr2;
        if (arr1.length > arr2.length) {tempArr1 = arr2; tempArr2 = arr1;} 
        int length1 = tempArr1.length, length2 = tempArr2.length;
        int start1 = 0, end1 = length1 - 1, start2 = 0, end2 = length2 - 1;
        int mid1, mid2, subLength;
        while (start1 < end1) { // 每次都会缩小数组比对的范围
            mid1 = (start1 + end1)/2; mid2 = (start2 + end2)/2; // 待比对数组范围的中位数下标
            subLength = length1/2; // 缩减的范围长度:小数组长度的二分之一
            if (tempArr1[mid1] == tempArr2[mid2]) {
                return tempArr1[mid1]; // 如果两个数组的中位数相等,那么这个值也是整个合并后数组的中位数
            } else if (tempArr1[mid1] < tempArr2[mid2]) { // 说明合并后的中位数在tempArr1的后半部分或者tempArr2的前半部分
                // 虽然可以确定在两个数组的哪半段,但是不能单纯各减一半,而是两个数组缩小相同的长度(小数组长度的一半),否则中位数的位置就发生偏移了。
                // 注:是从中位数不在的那半边中减去对应长度。
                start1 = start1 + subLength; end2 = end2 - subLength;
                length1 = length1 - subLength; length2 = length2 - subLength;
            } else if (tempArr1[mid1] > tempArr2[mid2]) { // 说明合并后的中位数在tempArr1的前半部分或者tempArr2的后半部分
                end1 = end1 - subLength; start2 = start2 + subLength; 
                length1 = length1 - subLength; length2 = length2 - subLength;
            }
        }
        // 当start1=end1时,退出循环,即短的数组里只剩下了一个数。此时用这个数跟长数组剩余元素的中位数做比对,以确定最终中位数
        mid2 = (start2 + end2)/2;
        if (length1 == length2) { // 两个数组长度都剩1,元素值小的那个是前中位数
            return tempArr1[start1] < tempArr2[start2] ? tempArr1[start1] : tempArr2[start2];
        } else if (length2 % 2 == 0) { 
            // 如果长数组剩余长度为偶数,则合并后长度是奇数,所以应在tempArr1[start1]、tempArr2[mid2]、tempArr2[mid2+1]三个数中找中间值,其中后两者之间有序
            if (tempArr1[start1] >= tempArr2[mid2+1]) return tempArr2[mid2+1];
            else if (tempArr1[start1] <= tempArr2[mid2]) return tempArr2[mid2];
            else return tempArr1[start1];
        } else { // length2 % 2 == 1的时候
            // 如果长数组剩余长度为奇数,则合并后长度是偶数,所以应在tempArr1[start1]、tempArr2[mid2-1]、tempArr2[mid2]三个数中找中间值,其中后两者之间有序
            if (tempArr1[start1] >= tempArr2[mid2]) return tempArr2[mid2];
            if (tempArr1[start1] <= tempArr2[mid2-1]) return tempArr2[mid2-1];
            else return tempArr1[start1];
        }
    }
    
    // 求两个有序数组合并后的上中位数。采用合并排序找第n小的方法,时间复杂度为O((length1 + length2)/2)
    public static int findMedianNum2(int[] arr1, int[] arr2) throws RuntimeException{
        // 判断存在空数组的情况,直接返回结果
        if (arr1 == null && arr2 == null) throw new RuntimeException("null arr");
        // 因为数组下标是从0开始,所以上中位数的下标为(startIndex + endIndex)/2,如一个长度为12的数组,中位数下标为(0+11)/2=5,即第六个元素
        else if (arr1 == null || arr1.length == 0) return arr2[(arr2.length - 1)/2];
        else if (arr2 == null || arr2.length == 0) return arr1[(arr1.length -1 )/2];
        
        int length1 = arr1.length, length2 = arr2.length;
        int[] conArr = new int[length1 + length2]; // 合并后的数组容器,如果单纯求中位值,这个容器可有可无
        int midIndex = (arr1.length + arr2.length - 1)/2; // 因为index从0开始,所以分子需要减1
        int i = 0, j = 0, k = 0; // i为arr1下标,j为arr2下标,k为合并数组下标
        while (i < length1 && j < length2) {
            if (arr1[i] <= arr2[j]) { // 如果有合并后数组容器,此时应将arr1[i]放入数组
                conArr[k] = arr1[i];
                if (k == midIndex) return arr1[i]; // k为本次遍历完成后,合并数组的下标
                i++; k++; // 下次遍历,合并数组和分数组的下标
            } else { // 如果有合并后数组容器,此时应将arr2[j]放入数组
                conArr[k] = arr2[j];
                if (k == midIndex) return arr2[j];
                j++; k++;
            }
        }
        // 如果其中一个数组已经遍历完还累计到midIndex,则继续遍历剩下的那个数组
        if (i == length1) {
            while (j < length2) {
                conArr[k] = arr1[i];
                if (k == midIndex) return arr2[j];
                j++; k++;
            }
        } else {
            while (i < length1) {
                conArr[k] = arr2[j];
                if (k == midIndex) return arr1[i];
                i++; k++;
            }
        }
        // 按照逻辑,上面的循环内一定会方法返回。此处的return只是为了让代码能够编译通过,实际不会执行到。
        return 0;
    }

    public static void main(String[] args) {
        int[] arr1 = new int[] {0,1,2,3,4,7,10};
        int[] arr2 = new int[] {5,6,7,8,9,11};
        System.out.println(FindMedian.findMedianNum(arr1, arr2));
        System.out.println(FindMedian.findMedianNum2(arr1, arr2));
    }

}

算法思想参考了:

https://www.cnblogs.com/TenosDoIt/p/3554479.html

https://blog.csdn.net/zuochao_2013/article/details/80031530

这两位博主都是使用递归方式实现的,我是使用非递归的方式实现的,但其实算法底层的逻辑是一样的。以上代码经过了简单测试,但由于本人水平有限,有可能会有没考虑到的细节或者漏洞,欢迎大家交流指正!

标签:JAVA,递归,start1,tempArr1,中位数,tempArr2,数组,mid2
来源: https://www.cnblogs.com/oceanbaxia/p/11063131.html