本文选自博客,可以前往博客以获得更好的阅读体验。
堆排序
堆排序是排序算法中的一种,算法时间复杂度是 O ( n l o g ( n ) ) O(n log(n)) O(nlog(n))。
堆
堆是计算机科学中一类特殊的数据结构的统称,堆通常可以被看做是一棵完全二叉树的数组对象。
性质
- 如果一个节点的位置为 k k k ,则它的父节点的位置为 k 2 \cfrac{k}{2} 2k ,而它的两个子节点的位置则分别为 2 × k 2 \times k 2×k 和 2 × k + 1 2 \times k + 1 2×k+1 。
- 每个节点的值都大于或等于其左右孩子节点的值,称为大顶堆
- 每个节点的值都小于或等于其左右孩子节点的值,称为小顶堆
堆排序
基本思想
- 将待排序序列构造成一个堆。
- 进行堆调整使其变为大根堆,整个序列的最大值就是堆顶的根节点。
- 将其与末尾元素进行交换,此时末尾就为最大值。
- 然后将剩余n-1个元素重新构造成一个堆,这样会得到n-1个元素的次小值。如此反复执行,便能得到一个有序序列了。(剪枝)
代码实现
C代码
#include <stdio.h>
#include <stdlib.h>
void swap(int a[],int i,int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
void heapify(int tree[],int n ,int i ){
if(i >= n) //递归出口
return;
int c1 = 2 * i + 1;
int c2 = 2 * i + 2;
int max = i;
if(c1 < n && tree[c1] > tree[max] )
max = c1;
if(c2 < n && tree[c2] > tree[max])
max = c2;
if(max != i){
swap(tree,max,i); //将最大值移动至父节点
heapify(tree,n,max); //递归维护下面的节点
}
}
void build_heap(int tree[],int n){ //构造堆
int last_node = n - 1;
int parent = (last_node - 1) / 2; //求出最后一个父节点
for(int i = parent;i >= 0;i--){ //向上维护节点
heapify(tree, n , i);
}
}
void heap_sort(int tree[],int n){
build_heap(tree,n);f
for(int i = n - 1;i >= 0;i--){ //每次取出根节点后,堆减少一个节点
swap(tree,i,0);
heapify(tree,i,0);
}
}
int main(){
int tree[] = {2, 5, 3, 1, 10, 4};
int n = 6;
heap_sort(tree,n);
for(int i = 0;i < n;i++){
printf("%d ",tree[i]);
}
return 0;
}
思路
一、heapify:堆调整
- 针对节点 i,将其两个子节点找出来,此三个节点构成一个最小单位的完全二叉树(越界的忽略)
- 找到这个最小单位的完全二叉树 的最大值,并将其交换至父节点的位置
- 递归调用,维护交换后 子节点与其子节点被破坏的堆关系,递归出口为叶节点
堆调整是自顶向下进行调整,无需考虑下标i小于0的情况
二、 build_heap:构造堆
-
用一维数组表示:堆
a [ i ] a[ i ] a[i] 的父节点为 a [ i − 1 2 ] a[\cfrac{i-1}{2}] a[2i−1],两个子节点为 a [ 2 × i + 1 ] a[2 \times i + 1] a[2×i+1] 和 a [ 2 × i + 2 ] a[2 \times i + 2] a[2×i+2]
-
先找到最后一个父节点,再从最后一个父节点开始向上调用 heapify维护节点。
三、heap_sort:利用堆进行堆排序
尽管我们可以构造出大根堆,但是我们也发现,大根堆并没有排好序
大根堆是根节点的值最大,这时我们可以将根节点移走,即剪去根节点,并将剩余的再次构造一个新堆,循环往复,就进行了排序操作。
那么,拿走根节点后,根节点的数值又应该由谁取代?
结合刚刚的分析,我们知道,该节点需满足:移动到根节点对堆的破坏最小。那么,结果便很明了了,最后一个节点是最好的选择。
我们可以用最后一个节点替换根节点,此时根节点和第二层的两个节点构成一个最小单位的最小二叉树,此时就可以直接调用 heapify函数,形成一个新的堆。但是我们并没有必要使用 build_heap函数,因为此时最大值就在第二层,根节点会因为递归维护被替换到最后一层,其他节点也会因为递归维护而得到调整。
最后,经过堆排序,数值便从小到大排序,同时也构成了小根堆。
后记
C语言需要我们自行手写出代码。而实际上,许多编程语言并非需要我们手写堆排序。
在这里,我们可以引入一个名为优先队列的数据结构。
优先队列
优先队列可以完成以下操作:
- 插入一个数值
- 取出最小的数值(获得数值,并且删除)
经过刚刚堆排序的知识,我们很容易就会发现优先队列与堆排序的相似性。可以说,优先队列进行了大根堆的堆排序,在输出时按从大到小的顺序输出。
在C++中,STL里的priority_queue就是其中之一。
#include <queue>
#include <cstdio>
using namespace std;
priority_queue<int> p;
int main{
//插入元素
p.push(3);
p.push(5);
p.push(1);
//不断循环直至空为止
while(!p.empty()){
//获取并删除最大值
printf("%d\n",p.top());
p.pop();
}
return 0;
}