目录
引言
在解决一些实际问题中,往往不是只需要排序挑选最大值和最小值就能得到最便捷的解决方案,而堆排序就是实际问题解决的一种方案,它的特点在于父级始终是最大的数
视频教程
这里有比较好的视频教程,不喜欢看文章可以去看视频教程,对某一步不懂可以看文章,我会将每一步介绍的很详细。堆排序
学习堆排序之前的一些必要知识
什么是完全二叉树
这个二叉树是数据结构中的内容,但是我们不需要把属于他的内容全部介绍,只需要了解什么是完全二叉树和为什么堆排序必须满足完全二叉树即可。
如图,完全二叉树就是从上至下父级节点始终大于或者小于子级节点,并且二叉树必须从左到右完整,不能是左边还没有右边就出来一节分支(如图)。
放代码
#include<iostream>
using namespace std;
void swap(int tree[], int max, int i) {
int temp = tree[i];
tree[i] = tree[max];
tree[max] = temp;
}
void heapify(int tree[],int n,int i) { //这个是单独的对某一节点进行成堆操作
int c1 = 2 * i + 1;
int c2 = 2 * i + 2;
int max = i;
if (i >= n)return;
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 allheap(int tree[], int n) { //这个是对所有节点进行heapify成堆操作
int last_node = n - 1; //这个表示的是最后一个节点
int last_parentnode = last_node - 1; //这个表示最后一个节点的父节点
for (int i = last_parentnode; i >= 0; i--) {
heapify(tree, n, i);
}
}
void heap_sort(int tree[],int n) {
heap_sort(tree, n);
for (int i = n - 1; i >= 0; i--) {
swap(tree, i, 0);
heapify(tree, i, 0);
}
}
int main() {
int n, a[] = { 2,5,3,1,10,4 }; //举个栗子
n = 6;
allheap(a, n);
for (int i = 0; i <n; i++) {
cout << a[i]<<endl;
}
return 0;
}
代码解释
代码分为四个函数模块,待会会一一讲解,在main函数中的数组列子是采用了开头给的视频中的例子
解释前需要了解的几个名词概念和数学关系
1:heap
这个就是堆的意思,堆是一种完全二叉树或者近似完全二叉树,所以效率极高,像十分常用的排序算法。
2:heapify
这个按我的理解是将一棵普通二叉树转化为完全二叉数(变成堆)的操作,转化为完全二叉树后就可以进行堆排序。
3:父级和子级的数学关系
假设知道父级节点位置是 i ,两个分支子级节点为c1和c2,那么两个子级位置关系就满足
c1 = 2 * i + 1
c2 = 2 * i + 2
假设知道了某一个子级的位置为 x。 那么父级的位置就是
i = (x-1)/2
这里不用管是子级一还是子级二了,因为默认数值是向下取整,是一还是二没有影响!
模块1:heapify和swap
这里将两个模块一起讲。
先看heapify。
void heapify(int tree[],int n,int i) { //这个是单独的对某一节点进行成堆操作
int c1 = 2 * i + 1;
int c2 = 2 * i + 2;
int max = i;
if (i >= n)return; //这个是递归的出口(我个人感觉这个可有可无,视频里有)
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); //这个递归是为了保证交换后也会是一颗完全二叉树,也就是将最大数放父级 ,如果不是一颗完全二叉树就有可能导致排序出错
}
首先把两个分支给表示出来,用max记录当前要heapify的位置,方便后续来进行判断下一父级节点需不需要进行heapify(这一步是必须的,否则往下的二叉树不能全部转化为完全二叉树)
注意:这里有一个递归出口,但我觉得是没必要加这个出口的,但视频里加了那还是加上吧,我是业余的(我是垃圾)。
接下来就是判断两个子级与父级的关系了,如果有其中一个子级是大于父级的(这里我采用父级严格大于子级),那么就让他们的只进行交换,也是就swap函数的用途。如果没有的话,就不交换,因为这一层级已经是一个完全二叉树了。
但是代码里面是先用max来记录,如果子级大于父级,会将子级的编号赋值给max,用来后续进行判断,
if (max != i) {
swap(tree, max, i);
这里就是判断有没有子级大于父级的并进行值交换的操作(swap函数没什么好说的,就是一个值交换的操作,要注意的是交换的是数组值的大小,不是编号交换!)
如果进行了值交换,就会递归从max得到值的那个编号在进行一次heapify,保证了下一层级也能成为一个完全二叉树,这里的话是将max当作下一次的i发送了过去,如果max已经是最后一层级了,其实这时候max对应父级的两个子级已经爆了,没有子级了,这时候其实什么都不会做,max的值也不会变,递归也会结束,这就是为什么我觉得那个递归出口没有必要的的原因(这只是我的观点,写上也不会错)
这里注意要特判节点c1和c2有没有爆,就是说这两个节点有没有比n大,n是总节点数,爆了说明到最底层级了,就不要进行操作了。
进行完后,就实现了对某一节点进行成堆。
那么怎么对一整颗二叉树实现成堆操作呢?进入模块二allheap
模块2:allheap
void allheap(int tree[], int n) { //这个是对所有节点进行heapify成堆操作
int last_node = n - 1; //这个表示的是最后一个节点
int last_parentnode = last_node - 1; //这个表示最后一个节点的父节点
for (int i = last_parentnode; i >= 0; i--) {
heapify(tree, n, i);
}
}
这里只需要记住一个关系,只要从最后一个父级节点因此往上进行heapify,就可以实现对一整颗二叉树变成堆!
而每个父级之间的关系是依次递减的
又因为我们知道了任意子节点可以求父节点的操作,因此就有了代码中的last_node和last_parent这两个变量
for (int i = last_parentnode; i >= 0; i--) {
heapify(tree, n, i);
}
这个就是从最后一个父节点到第一个父节点进行heapify的操作(至于为什么i>=0,这里是数组下标代表节点编号而已)
模块3:进行堆排序
在千辛万苦之下,我们终于把一颗普通二叉树转换为了一颗完全的二叉树,接下来就是排序的操作了,堆排序的操作比较是神奇,是把最顶点的节点和最后一个节点进行交换,然后将最后一个砍掉,如下图
然后最大值就出来了。
接下来怎么做呢,它对第一层级进行了heapify,如图
如图所示,这就是为什么在heapify函数里有一个递归,递归在这里起到了关键性的作用,没有他就不能实现排序!
如此重复就可以实现排序了,这里是从小到大排序,要从大到小反着来就好了!