感谢参考文章博主:https://blog.csdn.net/qq_36186690/article/details/82505569
对算法复杂度数量级的详细说明:https://blog.csdn.net/qq_25800311/article/details/82345252
一、排序算法复杂度
各种排序算法的时间复杂度统计(图片来自网络):
- 如果题目明确要求时间复杂度优于nlogn,常用方法中我们可以堆排序或归并排序。
- 如果题目中由要求出topk,可以选择堆排序,保证时间复杂度nlogn同时节省内存空间。
堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
以大顶堆为例,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
此处我们再来回忆一下用数组结构存储树结构(从下标0开始存储)
- 父节点i的左子节点在位置(2i);
- 父节点i的右子节点在位置(2i+1);
- 第一个叶子节点(n/2)
二、一般堆排序算法框架
- 堆排序的基本思想*是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
【将最大(小)值与末尾元素交换,故一般升序采用大顶堆,降序采用小顶堆】Step 1: 构建初始堆
据升序降序需求选择大顶堆或小顶堆。从最后一个非叶子结点开始,从左至右,从下至上进行调整。//建立初始大顶堆 for(i = n/2-1; i>=0; i--){ //从第一个非叶子节点从下往上开始调整 adjustHeap(arr, i, n); }
Step 2: 堆顶元素换至末尾,然后继续调整堆。
for(i = n-1; i>0; i--){ swap(arr, 0, i); adjustHeap(arr, 0, i); }
堆的调整过程
给定初始无序序列自然形成的“堆”结构如下图所示:
从最后一个非叶子结点(6)开始向下调整
代码1:堆排序模板
class Solution { //因为有k的限制,这两道题的堆处理都可以优化 public int[] smallestK(int[] arr, int k) { int i, j, n = arr.length; //建立初始大顶堆 for(i = n/2-1; i>=0; i--){ //从第一个非叶子节点从下往上开始调整 adjustHeap(arr, i, n); } for(i = n-1; i>0; i--){ swap(arr, 0, i); adjustHeap(arr, 0, i); } return arr; } public void adjustHeap(int[] arr, int i, int n){ int temp = arr[i]; for(int k = i*2+1; k<n; k=k*2+1){ if(k+1<n && arr[k]<arr[k+1]){ k++; //k指向右子节点 } if(arr[k]>temp){ arr[i] = arr[k]; i = k; } else{ break; } } arr[i] = temp; } public static void swap(int []arr, int a ,int b){ int temp=arr[a]; arr[a] = arr[b]; arr[b] = temp; } }