1 何为堆
一个数组序列我们可以将其用完全二叉树或近似完全二叉树表示出来,当数组下标为i时,它的父节点为(i-1)/2,左孩子为(2i+1),右孩子为(2i+2),这种对应关系说明数组下标为0的地方也要存储数据。
堆是在完全二叉树的基础上递归定义的,堆分为大顶堆(大根堆)和小顶堆(小根堆)。
大顶堆:根节点的数值大于孩子节点,完全二叉树的左右子树同时满足这个条件。
小顶堆:根节点的数值小于孩子节点,完全二叉树的左右子树同时满足这个条件。
从这种数据结构中可以发现:大顶堆的根节点也就是数组的第一个元素必定是最大值,而小顶堆必定是最小值。
左边是小根堆,右边是大根堆:
2 堆排序的过程
要想写出堆排序的代码,首先一定要清楚堆排序的过程,根据堆这种数据结构的特性,我总结了一下堆排序的过程:
- 将数组初始化为堆,初始化堆的过程就是移动数组中元素的位置
- 初始化完成之后,如果建立的是大顶堆,那么数组中的第一个元素就是数组的最大值,小顶堆就是最小值
- 然后将最大值(最小值)和堆中的最后一个叶子节点(数组中的最后一个元素)进行交换,并将数组的长度减一(不再对替换到数组尾的最大值或最小值进行操作)
- 重复以上步骤,直到循环结束(数组的长度变为一)
如果是大顶堆,那么数组将会进行升序排序,如果是小顶堆,则会进行降序排序。
2.1 堆的初始化
堆的初始化实际上就是数组元素的移动与交换,只不过这种交换发生在孩子节点与父节点之间。
假设要建立的是大顶堆,只要保证每棵左右子树都是大顶堆那么最后整棵完全二叉树必然是大顶堆。根据完全二叉树的结构可以得到,假设数组有n个元素,对应的完全二叉树的叶子节点就有(n+1)/2个,最后一个子树的根节点下标则是(n/2)-1。
也就说我们从节点(n/2)-1处开始,分别计算出当前节点的左右孩子,先拿出值最大的孩子,然后将此孩子与父节点进行比较,如果孩子节点小于父节点,说明此子树已经是一子堆,直接考虑前一个非叶子节点。如果此孩子大于父节点,则需要将孩子节点与父节点互换后再考虑前一个非叶子结点,直至以这个节点为根的子树是一个堆!
举个例子:假设有数组 [28, 26, 17, 36, 20, 42, 11, 53]
,根节点的下标为i,左右孩子的下标分别为2i+1, 2i+2,最后一个子树的根节点下标为(n/2)-1即8/2-1=3,上述过程可参考以下图解:
2.2 根节点的删除
与其叫做根节点的删除,不如说是根节点与n-i (i=1,2,3… …)处节点的互换,这样就相当于每次将当前数组的最大值放到数组的最后面,也就是实现了升序,互换后数组长度便可以减一(替换到数组尾的最大值不需再参加下一次堆的初始化),当整个数组有序的时候也就是只有一个节点进行堆的初始化的时候。
3 代码实现
如下是按大顶堆对数组进行升序排列的代码实现:
#include<iostream>
using namespace std;
#define N 10
class Heap {
public:
void sort(int array[], int size);
void createHeap(int array[], int i, int size);
void swap(int array[], int local);
};
void Heap :: swap(int array[], int local) {
int temp = array[local];
array[local] = array[0];
array[0] = temp;
}
void Heap :: createHeap(int array[], int i, int size) {
//先找到当前节点的左右孩子节点
int l = 2*i+1;
int r = l+1;
int k;
//保存当前节点的值
int temp = array[i];
cout << "l: " << l << " r: " << r << endl;
while(l < size) {
//先找到数值较大的孩子
if(l == size-1) {
k = l;
} else {
k = (array[l] >= array[r] ? l : r);
}
//将孩子和父节点进行比较
if(array[k] <= temp) {
break ;
} else {
array[i] = array[k];
i = k;
l = 2*i+1;
r = l+1;
}
array[k] = temp;
}
}
void Heap :: sort(int array[], int size) {
//先找到第一个非叶子节点
int not_leafP = size/2-1;
int local = size;
//初始化堆
for(int i = not_leafP; i >= 0; i--) {
//建立子堆
createHeap(array, i, size);
}
//将堆顶元素插入到数组尾的有序区间中
swap(array, --local);
}
int main()
{
int array[N];
Heap heap;
for(int i = 0; i < N; i++) {
cin >> array[i];
}
//当只有一个节点进行初始化堆的时候,数组有序
for(int size = N; size > 1; size--) {
heap.sort(array, size);
}
for(int i = 0; i < N; i++) {
cout << array[i] << ' ';
}
cout << endl;
return 0;
}
小顶堆的实现和大顶堆没有区别,故不在列出。
4 效率分析
时间复杂度:堆排序的时间代价主要花费在堆的初始化上,由代码可知,我们总共建立了n-1次堆,建立新堆时总共进行的比较次数最多为logn,所以堆排序的时间复杂度为O(nlogn)。
空间复杂度:不需要开辟辅助空间,为O(1)。