一、快排思想解决top K问题
public class AlgorithmTest {
//得到最小的k个数
public int[] getLeastNumbers(int[] arr, int k) {
if (arr == null || k <= 0 || arr.length < k) {
return new int[0];
}
return quickSearch(arr, 0, arr.length - 1, k - 1);
}
/**
* 使用快排思想解决topK问题
*
* @param arr 数组
* @param left 左边界
* @param right 右边界
* @param k 我们要查找下标为k-1的数
* @return
*/
private int[] quickSearch(int[] arr, int left, int right, int k) {
//每快排分割一次,找到排序后下标为j的元素,如果j恰好等于k就返回j以及j左边的所有书
int j = partition(arr, left, right);
if (j == k) {
return Arrays.copyOf(arr, j + 1);
}
//否则根据下标j与k的大小关系来决定继续切分左段还是右段
return k < j ? quickSearch(arr, left, j - 1, k) : quickSearch(arr, j + 1, right, k);
}
/**
* 快排的切分,返回下标j,使得arr[j]左边的数都比它小,右边的数都比它大
*
* @param arr
* @param left
* @param right
* @return
*/
private int partition(int[] arr, int left, int right) {
int v = arr[left];
int i = left;
int j = right + 1;
while (true) {
while (++i <= right && arr[i] < v) {
}
while (--j >= left && arr[j] > v) {
}
if (i >= j) {
break;
}
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
arr[left] = arr[j];
arr[j] = v;
return j;
}
public static void main(String[] args) {
int[] arr = {12, 4, 54, 34, 100, 89, 9, 0, 10, 111};
AlgorithmTest test = new AlgorithmTest();
int[] rt = test.getLeastNumbers(arr, 5);
for (int i = 0; i < rt.length; i++) {
System.out.print(rt[i] + " ");
}
}
}
总结:使用快排,每次遍历都是上一次的一半,时间复杂度为O(N).
(1)时间复杂度:
每次遍历都是上一次的一半,时间复杂度期望为O(N) ,最坏情况下的时间复杂度为 O(n^2)。情况最差时,每次的划分点都是最大值或最小值,一共需要划分 n - 1 次,而一次划分需要线性的时间复杂度,所以最坏情况下时间复杂度为 O(n^2)。
(2)空间复杂度:期望为 O(log n),递归调用的期望深度为 O(logn),每层需要的空间为 O(1),只有常数个变量。
最坏情况下的空间复杂度为 O(n)。最坏情况下需要划分 n 次,即 quickSearch 函数递归调用最深 n - 1层,而每层由于需要 O(1)的空间,所以一共需要 O(n)的空间复杂度。
二、大跟堆/小跟堆求topK问题
大跟堆求前K小问题:求前K小问题,只需要维护一个容量为K的大跟堆即可。每次poll出最大的数,剩下的就是前K小了。如果维护一个小跟堆,那么需要将所有的数放进堆中,时间复杂度就是ON(logN),就不是ON(logK)了。
小跟堆求前K大问题:求前K大问题,只需要维护一个容量为K的小跟堆即可。每次poll出最小的数,剩下的就是前K大了。如果维护一个大跟堆,那么需要将所有的数放进堆中,时间复杂度就是ON(logN),就不是ON(logK)了。
public class AlgorithmTest {
public static void main(String[] args) {
int[] arr = {12, 4, 54, 34, 100, 89, 9, 0, 10, 111};
AlgorithmTest test = new AlgorithmTest();
int[] rt = test.getLeastNumbers(arr, 5);
for (int i = 0; i < rt.length; i++) {
System.out.print(rt[i] + " ");
}
}
//得到最小的k歌数
//堆:经过排序的完全二叉树
//大跟堆:根结点的键值是所有堆结点键值中的最大值 =》 求前k小
// Queue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {
// @Override
// public int compare(Integer o1, Integer o2) {
// return o2 - o1;
// }
// });
//小跟堆:根结点的键值是所有堆结点键值中的最小值 =》 求前k大
// Queue<Integer> minHeap = new PriorityQueue<>(k);
public int[] getLeastNumbers(int[] arr, int k) {
if (arr == null || k <= 0 || arr.length < k) {
return new int[0];
}
//定义一个大跟堆,PriorityQueue默认是小跟堆,重写一下比较器
Queue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
for (int num : arr) {
if (maxHeap.size() < k) {
maxHeap.offer(num);
} else if (maxHeap.peek() > num) {
maxHeap.poll();
maxHeap.offer(num);
}
}
int[] resultArr = new int[k];
for (int i = 0; i < k && !maxHeap.isEmpty(); i++) {
resultArr[i] = maxHeap.poll();
}
return resultArr;
}
}
总结:
(1)Java中自带PriorityQueue,实现起来比较简单,时间复杂度为ON(logK),空间复杂度为O(K)。
(2)PriorityQueue中的一些方法说明: