堆排序
想要了解堆排序,就得先了解什么是堆。
想要了解什么是堆,先要了解什么叫完全二叉树。
想要了解什么是完全二叉树,先要了解什么是满二叉树。
想要了解满二叉树…
emm
到满二叉树就够了。
一些基本的概念
满二叉树,很容易理解,就是一个把节点铺满的二叉树。
一个n层的二叉树,除了第n层节点全是叶子节点之外,其他层的节点,必须都有左右两个子节点。
如下图:
完全二叉树的定义比较抽象,可以借助满二叉树理解。
完全二叉树可以视为一个满二叉树在最后一层从右至左不间断地删除节点,比如:
这两个都是完全二叉树。
但是要注意,一定是“从右到左不间断地”,如下图就不是一个完全二叉树:
完全二叉树还可以视为在满二叉树的最后一层之下,从左至右不间断地添加节点。还是要注意这个“不间断”。
完全二叉树“完全”在哪里
我们知道,二叉树的存储除了链式存储,还有一种是顺序存储,就是按照层序从左至右依次存储。
在顺序存储时,完全二叉树不会浪费存储空间,如下图:
如果节点发生“间断”,在顺序存储时,是不能直接去掉该节点,然后将之后的元素向前移的,那样二叉树的结构会发生改变,所以只能用inf或者null占位,这就会造成存储空间的浪费。比如上图,删除6这个节点,顺序存储上应该变为:2 4 7 5 inf 3,而不是2 4 7 5 3。
完全二叉树如果满足它的每一个节点的元素的关键词都大于等于(小于等于)该节点的子节点的关键词,就称这个完全二叉树为大根堆(小根堆)。
升序排序用大根堆,降序排序用小根堆。
建立堆的过程统称“建堆”(Heapify),建立大根堆的过程可以称作“下沉”,建立小根堆的过程可以称作“上浮”。
堆排序的过程
堆排序的思路
刚刚说到,二叉树是可以顺序存储的。对于待排序的序列,可能是数组或者其他线性表形式,可以看成一个完全二叉树的顺序存储。
进一步地,将这个完全二叉树通过某种算法,使它转化成一个大根堆(以后简称堆)。那么,由于堆的结构,堆顶元素一定是所有元素中关键词最大的那个。
我们把堆顶元素取下来,放在序列末尾,并“忽略”这个元素的存在(实际操作直接把标识堆规模的变量-1)。再将另一个元素(为了方便实现,一般选用最后一个叶子节点)移到堆顶,重新建堆(由于忽略了一个元素,这是一个规模更小的堆)。
如此反复,每一次“忽略”一个最大的元素,每一次堆的规模都会减1,当堆的规模为0时,所有元素都将变得有序。
一个实例:
具体实现
如何定位到一个节点的父节点或者子节点?
若序列是从0开始的,则下标为i的节点的左儿子节点的下标为:2i+1,右儿子节点的下标为2i+2,父节点为(i-1)/2;
若是从1开始的序列,则分别为:2i、2i+1和i/2;
如何建堆?
建堆的过程,本质上就是一个交换的过程,先写一个小学二年级的交换函数:
void swap(int& a,int