这道题目其实是快速排序算法的一个延伸,如何做到在O(n)的时间复杂度内查找数组内第K大的元素?
学习过快速排序就知道,快排每次分区会把数据分为三个部分,a[0,p-1],a[p]以及a[p+1,n-1]三个区间。每次分区我们都将大于分区点元素放置在分区元素左边,小于分区元素的统一放到分区元素右边,那么这样一来,我们就可以利用快速排序实现从大到小的数组排序了。但是,这里说这个不是为了排序,而是想说一个关于从大到小排序后数组的一些规则:
1 如果P+1=k,那么a[P]就是要找的元素。
2 如果P+1<k,那么要继续在a[p+1,n-1]之间找。
3 如果P+1>k,那么要继续在a[0,p-1]之间找。
我们要查找第K大的数据,P作为分区点,那么利用以上规则就可以找到这个第K大元素,如果你还不明白,看下下边这个图:
这个图示我要查第K大元素,k=4时的图解过程。可以看出来每次分区我都可以把范围缩小。我只需要在这个小范围内找就可以了。
时间复杂度如何推导呢?
第一次分区,我需要对大小为n的数组执行分区操作,需要遍历n个元素。第二次分区查找,只需要对n/2个数据执行分区,只需要遍历n/2个数组,当然这里说的都是平均情况。依次类推,分区遍历的元素个数为n/2、n/4、n/8一直到区间为元素为1为止。把每次分区遍历的元素个数加起来:n+n/2+n/4+n/8...+1,最终等比数列求和的结果就是2n-1,所以时间复杂度为O(n).
代码如下:
package com.study.algorithm.array;
/**
* 在O(n)的时间复杂度内查找第K大的元素
* @author Administrator
*
*/
public class FindNData {
/**
* 快速排序
* @param arr
* @param n
*/
public static void quickSort(int[] arr,int n,int k){
quickSort(arr,0,n-1,k);
}
/**
* 根据分区点,递归继续分解子分区
* @param arr
* @param p
* @param r
*/
public static void quickSort(int[] arr,int p,int r,int k){
if(p>=r){
System.out.println("第"+k+"大元素为:"+arr[p]);
return;
}
int q = partition(arr,p,r);
if(q+1==k) {
System.out.println("第"+k+"大元素为:"+arr[q]);
return;
}else if(q+1<k){
quickSort(arr,q+1, r,k);
}else if(q+1>k) {
quickSort(arr,p,q-1,k);
}
}
/**
* 快排分区
*随机生成[p,r]区间内pivot,将比pivot大的放在其右侧,比pivot小的放在其左侧
* @param arr
* @param p
* @param r
*/
public static int partition(int[] arr,int p,int r){
int pivot = arr[r];
int i=p;
for (int j = p; j < r; j++) {
//当前元素比分区点小,则交换当前元素arr[j]到arr[i],也就是将小的移动到左侧,大的移动到右侧,而这个大小也是相对于分区点来说的,将来分区点会放到中间位置
if(arr[j] > pivot){
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
//交换完位置(每确定完一个小元素后)以后,移动i指针到下一位(这个位置也是为下一个小元素准备的),只要arr[j]比分区点小,就将其交换到指针i的位置,并将i后移
i++;
}
}
//迭代完成后,所有相对于pivot小的元素都被移动到靠左的位置(i指针动态指向的位置),所有相对于pivot大的元素都被移动到右侧,但是还是需要pivot将大小区间分割开
int temp = arr[i];
arr[i]=arr[r];
arr[r]=temp;
return i;
}
public static void main(String[] args) {
int[] arr= {6,1,3,5,7,2,4,9,11,8};
int k=4;
quickSort(arr,10,k);
}
}
执行结果:
第4大元素为:7