文章目录
1. 基本思想
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
1.1 复习数据结构—堆
在此可以顺便把堆复习一下:
说到堆,就得谈谈二叉树的顺序结构,普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。
现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
1.2 堆的概念
- 如果有一个关键码的集合
K={K(0),K(1),K(2)……K(n-1)}
,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:K(i)<=k(2* i+1)且 K(i)<=k(2* i+2)(K(i)>=k(2* i+1)
且K(i)>=k(2*i+2))
i=0,1,2
……则称为小堆(或大堆)。
1.3 堆的性质
堆中某个节点的值总是不大于或不小于其父节点的值;堆总是一棵完全二叉树。
其中这样顺序存储产生最良好的一个性质就是:
- 已知
parent
的下标,那么其逻辑结构的左孩子left
、右孩子right
在存储结构下数组中下标就能够计算得到,即:left = parent * 2 + 1
、right = parent * 2 + 1
,反之已知任意一个孩子的数组下标就能够求得parent
的数组下标,在这利用了int的作除法取整的技巧,将式子进行统一:parent =(child - 1) / 2
,常用求解最后一个非叶子节点,即知道数组大小size
时,可求解最后一个非叶子节点为parent = (size - 1 - 1 ) / 2
1.4 堆的实现
1.4.1 堆的AdjustDown向下调整算法
现在给出一个数组,逻辑上看做一颗完全二叉树。通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
主要注意点及判断顺序如下:
- 传入的根节点,首先判断是否有有孩子,即它是不是叶子节点,
left = 2 * root + 1;
right = 2 * r + 2;
由于是完全二叉树,只判断左孩子子即可,left >= size
,即可说明它为叶子节点,向下调整算法结束,return
即可 - 说明有孩子,再判断有没有右孩子,在这由于数组顺序存储的形式,需要同时判断一步是否越界,右孩子存在根据完全二叉树性质一定存在左孩子,若右孩子存在则找到左右孩子中最小的孩子,即
right < size && array[right] > array[left]
,也是利用与运算符的截断性质 - 将根的值与最小的孩子进行比较,如果根的值小,那么直接
return
即可,否则,交换根与最小孩子的值 - 再将对根进行递归的向下调整即可
AdjustDown(array, size, minchild)
1.4.2 堆的创建
下面给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在通过算法,把它构建成一个堆。根节点左右子树不是堆,怎么调整呢?
int a[] = {1,5,3,8,7,6};
这里从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆,如下图所示:
上面图就很清楚的表明了,我们可以通过上面的方法递归调用向下调整算法将一个逻辑上的完全二叉树变成大根堆,这也是堆排序排升序序列的基础
1.4.3 堆的插入与AdjustUp向下调整算法
先插入一个80到数组的尾上,再进行向上调整算法,直到满足堆
这个向上调整算法就很简单了,就插入元素而言,根据插入进去的child
数组下标找到父节点的数组下标,再根据大堆/小堆的性质进行元素交换即可,最后将交换过的数组child
下标进行更新即可。注意对child == 0
一开始是否需要判断,否则会出现数组越界问题。
1.4.4 堆的删除
要知道:删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
1.4.5 堆排序
前面讲向下调整算法都已经很明白了,再拿这张图走一遍就ok了,这是建的大堆,实行递增排序:
就这几行主要思想和代码:
- 首先排递增序,建个大堆,不能建小堆,会破坏堆的结构
- 交换堆顶元素与数组从末尾到前的未经历
Swap
的元素,再对堆顶元素进行向下调整即可
void HeapSort(int array[], int size) {
CreateHeap(array, size);
for (int i = 0; i < size; ++i) {
Swap(array, array + size - i - 1);
AdjustDown(array, size - i - 1, 0);
}
}
1.4.6 模拟实现堆
#include <stdlib.h>
#include <assert.h>
// array[size] 表示数组及大小
// root 表示要调整的结点的下标
// 前提是 [root] 所在的结点左右子树已经满足堆的性质了
void AdjustDown(int array[], int size, int root) {
while (1) {
int left = 2 * root + 1;
int right = 2 * root + 2;
int min;
if (left >= size){
// 越界
return;
}
// 确定哪个是最小的孩子
if (right < size && array[right] < array[left]) {
min = right;
}
else {
min = left;
}
if (array[root] <= array[min]) {
return;
}
// 交换值
int t = array[root];
array[root] = array[min];
array[min] = t;
//AdjustDown(array, size, min);
root = min;
}
}
// 建堆
// O(n * logN) --> O(n)
void CreateHeap(int array[], int size) {
// 从最后一个非叶子结点开始,调整到 0 结束
// 最后一个非叶子结点就是最后一个结点的双亲结点
for (int i = (size - 2) / 2; i >= 0; i--) {
AdjustDown(array, size, i); // O(log(n))
}
}
typedef int HPDataType;
typedef struct Heap {
HPDataType *array;
int size;
int capacity;
} Heap;
void HeapCreateHeap(Heap *heap, int array[], int size) {
// 暂时不考虑扩容问题
heap->capacity = size * 2;
heap->size = size;
heap->array = (int *)malloc(sizeof(int)* size);
for (int i = 0; i < size; i++) {
heap->array[i] = array[i];
}
CreateHeap(heap->array, heap->size);
}
// 小堆
void AdjustUp(int array[], int size, int child) {
while (child != 0) {
int parent = (child - 1) / 2;
if (array[child] >= array[parent]) {
return;
}
int t = array[parent];
array[parent] = array[child];
array[child] = t;
child = parent;
}
}
// 增加
void HeapInsert(Heap *heap, int val) {
heap->array[heap->size] = val;
heap->size++;
AdjustUp(heap->array, heap->size, heap->size - 1);
}
// 删除(只能删除堆顶元素)
// O(Log(n))
void HeapPop(Heap *heap) {
assert(heap->size > 0);
heap->array[0] = heap->array[heap->size - 1];
heap->size--;
AdjustDown(heap->array, heap->size, 0);
}
// 返回堆顶元素,返回最值
HPDataType HeapTop(Heap *heap) {
assert(heap->size > 0);
return heap->array[0];
}
2. 代码实现
// 堆排序(递增)
// 大顶堆的向下调整
void AdjustDown(int array[], int size, int r) {
int left = 2 * r + 1;
int right = 2 * r + 2;
if (left >= size) { // 是否为叶子节点
return; // 叶子节点直接结束
}
int m = left; // 有左孩子
// 是不是有右孩子,并找最大的孩子
if (right < size && array[right] > array[left]) {
m = right;
}
// 如果根的值大于最大孩子,直接返回
if (array[r] >= array[m]) {
return;
}
Swap(array + r, array + m); // 将最大值最为新的根
AdjustDown(array, size, m); // 递归向下调整
}
void CreateHeap(int array[], int size) {
// i=最后一个非叶子节点
// 已知parent,则 left=2* parent+1,right=2*parent+2
// 已知child,则parent=(child-1)/2,在此均为数组的下标
// 故最后一个非叶子节点,就是数组最后一个下标size-1,再-1,结果除2即可
for (int i = (size - 1 - 1) / 2; i >= 0; --i) {
AdjustDown(array, size, i);
}
}
// 堆排序(递增)
void HeapSort(int array[], int size) {
CreateHeap(array, size);
for (int i = 0; i < size; ++i) {
Swap(array, array + size - i - 1);
AdjustDown(array, size - i - 1, 0);
}
}
测试数据:int array[] = { 3, 9, 1, 4, 2, 8, 2, 7, 5, 3, 6, 11, 9, 4, 2, 5, 0, 6 };
3. 性能分析
时间复杂度
- 最坏 O ( n l o g n ) O(nlogn) O(nlogn) 对数据不敏感
- 平均 O ( n l o g n ) O(nlogn) O(nlogn) 对数据不敏感
- 最好 O ( n l o g n ) O(nlogn) O(nlogn) 对数据不敏感
空间复杂度
- O ( n ) O(n) O(n)
排序稳定性
- 不稳定
堆排序每一趟通过堆调整产生有序区,它是全局有序区,每次调整时间为 O ( l o g n ) O(logn) O(logn)
建堆的时间为 O ( n ) O(n) O(n),比较次数大约为 4 n 4n 4n次。
当然,了解STL同学能够知道:堆(heap)也被称为优先队列(priority-queue),这个应用很广泛,什么前K个高频元素、高频单词、TOP K问题等等,都能够运用到优先队列。
至于想检测一下掌握的怎么样了,推荐去刷刷OJ
题目,如牛客、LeetCode的OJ,对知识的应用很有帮助,