今天是来记录leetcode刷题的第五天,今天得问题是寻找第K大。
题目要求如下
有一个整数数组,请你根据快速排序的思路,找出数组中第 k 大的数。
给定一个整数数组 a ,同时给定它的大小n和要找的 k ,请返回第 k 大的数(包括重复的元素,不用去重),保证答案存在。
要求:时间复杂度 O(nlogn),空间复杂度 O(1)
我再看到这个问题的第一瞬间,就知道这一定跟快排相关了,果不其然,题目要求根据快排的思路(这算不算坑呢),找到答案。
对题目略加分析,求其第K大,即求其排序后的第(n-K)个数字。
于是我手撸一套快排的代码,提交,发现指通过了 8/10 ,剩余的由于超时而被pass了,如下图。
于是我开始对快排进行优化,即随机基准值。再次提交,发现果然通过了,但是其效果不尽如人意,只超越了1%的前辈。
于是,我继续对其进行优化,我得优化思路有两个
-
发掘题目要求,发现他只要第k大,对其余的数字并没有排序要求,再结合它要求我们结合快排的思想,而不是直接进行快排,这样问题就变成了二分求解了,即每次
partition之后,根据获取到的边界来确定K在哪个区间,然后再决定是继续partition还是直接返回。 -
由于出现的问题是数组的数据量太大导致超时而被pass掉,而有些排序在处理长数组/短数组的排序上性能要优于快排,所以我们可以在一定区间上使用快排,在其他的区间上使用别的排序算法,但是对于排序算法在哪些区间快,还没进行实践,所以这一方法暂时不太可行。
综上,我还是选择了第一种方案去实现,由于其实现是快排+分治,都是比较经典的算法,所以此处我就不再进行过多赘述了。
直接上代码吧。
/**
* 随机基准值
*/
public int findKth1(int[] a, int n, int K) {
// write code here
quickSort(a, n);
return a[n - K];
}
private void quickSort(int[] a, int len) {
quickSort(a, 0, len - 1);
}
private void quickSort(int[] arr, int L, int R) {
if (L < R) {
// 找到随机基准值,并与最后一位进行交换
swap(arr, R, (int) (Math.random() * (R - L) + L));
int[] edges = partition(arr, L, R);
quickSort(arr, L, edges[0]);
quickSort(arr, edges[1], R);
}
}
private int[] partition(int[] arr, int l, int r) {
int pivot = arr[r];
int indexL = l - 1;
int indexR = r + 1;
while (l < indexR) {
if (arr[l] < pivot) {
swap(arr, l++, ++indexL);
} else if (arr[l] > pivot) {
swap(arr, l, --indexR);
} else {
l++;
}
}
return new int[]{indexL, indexR};
}
private void swap(int[] arr, int i, int j) {
if (i != j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
@Test
public void test() {
int[] a = new int[]{10, 10, 9, 9, 8, 7, 5, 6, 4, 3, 4, 2};
findKth2(a, 12, 3);
}
/**
* 利用快排机制,而不是将数组排序
*/
public int findKth2(int[] a, int n, int K) {
// write code here
return quickFind(a, 0, n - 1, n - K);
}
private int quickFind(int[] a, int L, int R, int k) {
swap(a, R, (int) (L + (Math.random() * (R - L))));
int[] p = partition(a, L, R);
if (p[0] >= k) {
return quickFind(a, L, p[0], k);
} else if (p[1] <= k) {
return quickFind(a, p[1], R, k);
} else {
return a[k];
}
}