使用快速排序
快排中的每一次循环,都是将数据分为两组,其中一组的任何数都小于另一组中的任何数,再不断地对数据进行分割直到不能再分即完成排序,其平均时间复杂度为O(n*log2n)。
快排求TopK思路:假设n个数存储在数组S中,从S中找出一个元素X,它把数组分为比它大的和比它小的两部分,假设比它大的一部分数组是Smax,比它小的部分是Smin。
这时有两种可能:
1. Smax中元素不足K个,说明Smax中的所有数和Smin中最大的K-|Smax|个元素就是数组S中最大的K个数;
2. Smax中元素的个数大于或等于K,则需要返回Smax中最大的K个元素。
优 点: 该算法的最优时间复杂度可以为O( k*log2n ),空间复杂度为O( 1 )
缺 点: 由于需要修改数组中的元素,所以需要将数组载入内存中,当数组元素过多时,需要考虑内存的使用问题。
public static void main(String[] args) {
/*start 使用快排找到TopK
时间复杂度大于等于O( k*log(n) ),小于O( n*log(n) ) n为数组长,k为输入的参数
空间复杂度为O(1)
*/
int k = 7;//从1开始
int[] arr = new int[]{4, 3, 6, 9, 3, 2, 0, 8};
quickSortTopK(arr, 0, arr.length - 1, k);
System.out.println("第" + k + "大元素为" + (k > arr.length ? "null" : arr[k - 1]));
System.out.println(Arrays.toString(arr));//这里得到的数组不一定是排好序的,排序是否完成和k有关
//end 使用快排找到TopK
}
private static void quickSortTopK(int[] arr, int left, int right, int k) {
if (k - 1 > right) {
return;
}
if (left >= right) {//查找结束
return;
}
//分治法,按pivot将数组分成两部分达到分治的效果,该方法按降序分区数组
int pivot = partition(arr, left, right);
if (pivot == k) {//此处的pivot虽然不会在比较k次后刚好为k,但循环过程中,一定会拆分到k==pivot的位置
return;
}
quickSortTopK(arr, left, pivot - 1, k);
quickSortTopK(arr, pivot + 1, right, k);
}
private static int partition(int[] arr, int left, int right) {
int pivot = arr[left];
int pivotIndex = left;
while (left <= right) {//一轮比较结束
//循环从后向前比较pivot。如果大于pivot,end--;如果小于pivot,right位置值填入坑,并作为新坑,旧坑位置比较完成(left++),跳出
while (left <= right) {
if (pivot < arr[right]) {
pivotIndex = right;
arr[left] = arr[right];
left++;
break;
}
right--;
}
//循环从前往后比较pivot,如果大于pivot,start++;如果小于pivot,left位置值填入坑,并作为新坑,旧坑位置比较完成(right--),跳出
while (left <= right) {
if (arr[left] < pivot) {
pivotIndex = left;
arr[right] = arr[left];
right--;
break;
}
left++;
}
}
arr[pivotIndex] = pivot;
return pivotIndex;
}
使用小顶堆
时间复杂度分析:
- 构建堆的时间复杂度O( k )
- 遍历原数组中剩余元素的时间复杂度为O( n-k )
- 调整堆的时间复杂度为O( log2k );
根据算法执行关系可得总的时间复杂度为O( k+(n-k)log2k ),k为输入的待寻找的位置,n为数组长度。
当k远小于n时,则可以认为该方法下的时间复杂度为O( nlog2k ),空间复杂度为O( 1 )
优点: 可以使用原数组的空间,空间复杂度低,排序性能相比于快速排序稳定。
public static void main(String[] args) {
/**
* start 使用小顶堆找TOPK
* 时间复杂度为 O( n * log(k) ),空间复杂度为O( 1 )
*/
int k = 8;//从1开始
int[] arr = new int[]{4, 3, 6, 9, 3, 2, 7, 8};
Integer i = heapSortTopK(arr, k);
System.out.println("第" + k + "大元素为" + (i == null ? "null" : arr[i]));
//end 使用小顶堆找TOPK
}
/**
* 使用小顶堆寻找数组中的topK
*/
private static Integer heapSortTopK(int[] arr, int k) {
if (k > arr.length || k <= 0) {
return null;
}
//在原数组上构建小顶堆
builderHeap(arr, k);
//比小顶堆堆顶元素大的数组元素,进入小顶堆
for (int i = k; i < arr.length; i++) {
//修改堆顶元素为当前数组中的元素
if (arr[i] > arr[0]) {
arr[0] = arr[i];
downAdjust(arr, 0, k);
}
}
return 0;
}
/**
* 在数组中构建长为k的小顶堆
*/
private static void builderHeap(int[] arr, int k) {
//从最后一个非叶子节点调整,下沉元素
for (int i = (k - 1) / 2; i >= 0; i--) {
downAdjust(arr, i, k);
}
}
/**
* 循环下沉给定的parentIndex节点
*/
private static void downAdjust(int[] arr, int parentIndex, int k) {
//避免交换变量
int temp = arr[parentIndex];
int childIndex = parentIndex * 2 + 1;
//下沉parentIndex
while (childIndex < k) {
//在数组长度范围内,拿到最小的孩子节点
if (childIndex + 1 < k && arr[childIndex] > arr[childIndex + 1]) {
childIndex++;
}
//此处需要注意:1.父节点小于子节点,则证明不用下沉 2.跳出后续父子节点索引修改的操作,避免最后赋值(temp)的一步产生出错
if (temp < arr[childIndex]) {
//此处表示该节点已经不需要下沉了
break;
}
//上浮子节点(因父节点已经被记录,则只需要赋值到父即可),更新父子节点的索引
arr[parentIndex] = arr[childIndex];
parentIndex = childIndex;
childIndex = parentIndex * 2 + 1;
}
arr[parentIndex] = temp;
}