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));
}
}
我是使用非递归的方式实现的,但其实算法底层的逻辑是一样的。以上代码经过了简单测试