预备知识
堆排序
堆排序是一种利用 堆 这种数据结构完成的 选择排序 , 它的最坏,最好,平均时间复杂度均为O(nlogn),也是不稳定排序。
堆
堆是一种 完全二叉树,它分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。
大顶堆:每个结点的值都大于或等于其左右孩子结点的值。(左右孩子的顺序不区分)
小顶堆:每个结点的值都小于或等于其左右孩子结点的值。(左右孩子的顺序不区分)
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序基本思想及步骤
堆排序的基本思想:
将原始序列构调整一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与数组末尾元素交换,此时最大元素"沉"到数组末端。然后将剩余n-1个元素重新调整成一个堆,这样会得到n-1个元素的最大值,将其与数组倒数第二位元素交换,此时次大值也确定了位置。如此反复执行,便能得到一个有序序列。
调整步骤
建堆和调整堆中都有调整动作,他们步骤是一致的。还是以大顶堆为例,如果当前节点小于它的左右孩子之一,则与这个孩子交换位置;如果比任何一个孩子都小。则与较大孩子交换。因交换动作导致子树顺序发生变化的,按此顺序递归地往下调整。
实例
步骤一:构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
1.假设给定无序序列结构如下
2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
4.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
此时,我们就将一个无需序列构造成了一个大顶堆。
步骤二 :调整堆。将堆顶元素与末尾元素进行交换,使末尾元素最大(被换到末尾的元素不再参与后续的调整动作)。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素9和末尾元素4进行交换
b.重新调整结构,使其继续满足堆定义
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
复杂度分析
-
时间复杂度
建堆时间为,之后有次向下调整操作,每次调整的时间复杂度为。故在最好、最坏、平均情况下时间复杂度都为
-
空间复杂度:
算法特性
-
跳跃交换,导致不稳定
-
从后往前调整交换,导致适用于顺序结构,但不适用于链式结构
-
任何情况下时间复杂度都与归并排序相同,但空间复杂度为,这是它相对于归并排序的最大优点。
在最坏情况下时间复杂度也是,这是它相对于快速排序的最大优点。同时空间复杂度也有领先。
-
堆排序适合的场景是元素很多的情况,典型的例子是从10000个元素中选出前10个最小的,这种情况用堆排序最好。如果元素较少,则不提倡使用堆排序,因为初始建堆所需的比较次数较多。
代码实现
import java.util.Arrays;
public class A {
static int[] arr = {1, 5, 3, 2, -7, 8, 0, 9, 4, 6};
static int len = arr.length;
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
public static void heapSort() {
// 建初堆
for (int i = len / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length); // i初始为len/2-1,表示它是最后一个非终端结点。从这里开始i--,一直往前分析
}
// 调整堆
for (int j = len - 1; j > 0; j--) {
swap(arr, 0, j); // 元素交换,把大顶堆的根元素,放到数组的最后
adjustHeap(arr, 0, j); // 元素交换之后,毫无疑问,最后一个元素无需再考虑排序问题了。调整剩下的元素从新变成堆
}
}
// 非递归调整方法
public static void adjustHeap(int[] array, int i, int length) {
// 先把当前元素取出来,因为当前元素可能要一直移动
int temp = array[i];
// 传进来当前结点编号i, k = 2*i+1 和 k+1 分别是他的左右孩子
// for循环的目的是:如果当前结点已经很靠上了,如果调整一下可能会影响下面的子树,所以要持续地往下检查是否满足要求
for (int k = 2 * i + 1; k < length; k = 2 * k + 1) { // 结点从i开始一个一个处理,直到
// 这个if语句的目的是判断左右孩子节点哪个大。k是左孩子,k+1是右孩子。如果右孩子大,就k++,代表k指向右子结点
if (k + 1 < length && array[k] < array[k + 1]) {
k++;
}
// 判断孩子节点中大较大者是否比父节点值要大。如果是,就交换,然后进入下一个for循环,检查子树是否还满足要求;如果不是,就break跳出函数,重新传入前一个非终端节点
if (array[k] > temp) { //如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
swap(array, i, k); // 交换
i = k; // 为了配合下一个for循环堆其子树的检查,这里要更新一下当前位置
} else {
break;
}
}
//arr[i] = temp; // 这一句加不加都可以。
}
//
// // 递归调整方法
// public static void adjustHeap(int[] array, int i, int length) {
// int left = 2 * i + 1; // 左孩子
// int right = 2 * i + 2; // 右孩子
// int maxIndex = i;
// if (left < length && array[left] > array[maxIndex]) maxIndex = left;
// if (right < length && array[right] > array[maxIndex]) maxIndex = right;
// if (maxIndex != i) {
// swap(array, i, maxIndex);
// adjustHeap(array, maxIndex, length);
// }
// }
public static void main(String[] args) {
heapSort();
System.out.println(Arrays.toString(arr));
}
}
如果想升序,就用最大堆,即上面的代码;如果想降序,就把 for循环 里两个 if语句 的比较符号都反过来,> 变成 <,< 变成 > 。
重要的应用:求最小的k个数
如果想利用最小堆来计算前k小的数,改完上面提到的比较符号后,替换掉 heapSort函数 代码为下面的,就够了。
public static ArrayList heapSort() {
ArrayList<Integer> list = new ArrayList<>();
// 建初堆
for (int i = len / 2 - 1; i >= 0; i--) { // 这里也可以改成i>=len/2-1-k,但有时候i会变成负数,越界
adjustHeap(arr, i, arr.length);
}
// 调整堆
for (int j = len - 1; j > len -1 - k; j--) {
list.add(arr[0]); // 依次装填堆顶的最小值
swap(arr, 0, j);
adjustHeap(arr, 0, j);
}
return list;
}
另外,也可以使用最大堆来巧妙地求求最小的k个数。性能更好。代码如下:
import java.util.ArrayList;
public class Solution {
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
// 原最大堆的调整代码,不动
public static void adjustHeap(int[] array, int i, int length) {
int temp = array[i];
for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {
if (k + 1 < length && array[k] < array[k + 1]) {
k++;
}
if (array[k] > temp) {
swap(array, i, k);
i = k;
} else {
break;
}
}
}
public static ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
ArrayList<Integer> list = new ArrayList<>();
int[] arr = new int[k]; //用于放最小的k个数
for (int i = 0; i < k; i++)
arr[i] = input[i];//先放入前k个数
// 建立前k个无序元素的初堆
int len = arr.length;
for (int i = len / 2 - 1; i >= 0; i--) {
// i初始为len/2-1,表示它是最后一个非终端结点。从这里开始i--,一直往前分析
adjustHeap(arr, i, len);
}
// 动态调整堆
for (int i = k; i < input.length; i++) {
// 此时arr[0]是前k个无序元素的最大者。然后比较原无序数组里前k个元素后面是否还有比这个最大值小的,如果有,就替换掉,再重新调整堆
if (input[i] < arr[0]) { //存在更小的数字时
arr[0] = input[i]; // 替换
//重新调整最大堆。因为数组的末端是已经求得的最小值,不能再动了,所以每次要k-1
adjustHeap(arr, 0, k - 1);
}
}
// 最后arr中剩下的就是原无序数组里前k小的值
for (int i : arr)
list.add(i);
return list;
}
public static void main(String[] args) {
int[] arr = {4, 5, 1, 6, 2, 7, 3, 8};
System.out.println(GetLeastNumbers_Solution(arr, 4));
}
}