要求:
因为题目对时间复杂度和改变数组都做了限制,其实第二条限制已经限制了不能给数组排序,因为如果给数组排序,最快的时间复杂度为NlogN,其中比较快的排序算法就是快速排序。因为我们只需要获得中位数的值,所以也不需要将数组全部排序。
所以,可以采取另一种方法:分治法。 快速排序算法在每一次局部递归后都保证某个元素左侧的值都比它右边的值小,右侧的元素的值都比它大。因此,可以利用这个思路快速的找到第N大的元素,而与快速排序算法不同的是,这种方法关注的并不是元素的左右两边,而仅仅是某一边。
当使用一次类快速排序算法后,分割元素的下标pos;
public class GetMid1 {
public static void main(String[] args) {
int nums[] = {1,3,5,9,7,11};
System.out.println(getMid(nums));
}
public static int getMid(int[] nums){
int low = 0;
int high = nums.length-1;
int mid = (low+high)/2;
int pos;
while(true){
pos = partition(nums,low,high);
if(pos == mid)
break;
else if(pos>mid)
high = pos-1;
else
low = pos+1;
}
//如果数组长度为奇数,就直接返回中位数
if(nums.length%2 == 1)
return nums[pos];
//如果数组长度为偶数,需要在找到第二个中位数,则在之后的数中找到最小值
//并和第一个中位数求平均值
else{
int min = getMin(nums,pos+1,nums.length-1);
return (nums[pos]+min)/2;
}
}
public static int getMin(int[] nums,int start,int end) {
int len = end - start + 1;
int min = Integer.MAX_VALUE;
for (int i = start; i <= end; i++) {
if (nums[i] < min)
min = nums[i];
}
return min;
}
public static int partition(int[] nums,int low,int high){
int key = nums[low];
while(low<high){
while(low<high && nums[high]>=key)
high--;
nums[low] = nums[high];
while(low<high && nums[low]<=key)
low++;
nums[high] = nums[low];
}
nums[low] = key;
return low;
}
}
(题源于力扣第四题)
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。
请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
对于这道题,目前有两种思路:
可以先将两个数组合并为一个数组,然后直接找这个合并后数组的中位数,合并数组后,可以按照例题一中的思路来解决,采用的是类快速排序的分治法。
思路比较简单,但是时间复杂度至少为O(n),先贴上代码。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len = nums1.length+nums2.length;
int nums3[] = new int[len];
System.arraycopy(nums1,0,nums3,0,nums1.length);
System.arraycopy(nums2,0,nums3,nums1.length,nums2.length);
int low = 0;
int high = len-1;
int mid = (low+high)/2;
int pos;
while(true){
pos = partition(nums3,low,high);
if(pos == mid)
break;
if(pos>mid)
high = pos-1;
else if(pos<mid)
low = pos+1;
}
double result;
if(len%2 == 1){
result = nums3[pos];
return result;
}else{
result = Double.MAX_VALUE;
for(int i =pos+1;i<len;i++){
if(nums3[i]<result)
result = nums3[i];
}
result = (double) (nums3[pos]+result)/2;
return result;
}
}
private int partition(int[] nums,int low,int high){
int key = nums[low];
while(low<high){
while(low<high && nums[high]>=key)
high--;
nums[low] = nums[high];
while(low<high && nums[low]<=key)
low++;
nums[high] = nums[low];
}
nums[low] = key;
return low;
}
}
首先,这个题目可以化为求两个有序数组中第N小的数,N代表的即为中位数所在的位置。可以结合两个数组都为有序数组的特点,通过二分法来进行查找,以达到更简单的时间复杂度。
二分查找的好处就在于,循环每进行一次,就可以排除一半的数据,假设第K个数即为中位数,针对两个数组,我们每次可以排除k/2个数字,这样可以提高查找的效率。
下面贴一个分析:
对于这个算法,可以使用递归来实现,递归的出口也就是当k的值变为1时,我们需要比较的也就是当前两个数组中还没被排除的数据中的首元素,哪一个更小就说明哪一个是中位数。
注意:
1.上一种解法中需要对数组长度是奇数和偶数两种情况分别处理,但是这种方法中可以对两种情况进行一个合并:
我们可以设定right = (m+n+1)/2;left = (m+n+2)/2。如果长度为奇数,那么这两个值相同,最后会返回相同的两个数,如果为偶数,返回的就是中间的两个数。
2.还有需要注意的一个情况就是要避免一个数组的长度比k/2小的情况,这是只要返回长度和k/2中较小的那一个即可。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int left = (m+n+1)/2;
int right = (m+n+2)/2;
return (getKth(nums1,0,m-1,nums2,0,n-1,left)+
getKth(nums1,0,m-1,nums2,0,n-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;
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));
}
}
}
(关于中位数的题型会持续