前言
在用Dart实现几个基础的排序算法中,只是用注释粗略的分析了一下堆排序的实现过程。本篇算是其的扩展篇。将详细讲一下二叉堆和堆排序。
二叉堆
二叉堆是完全二叉树或者是近似完全二叉树。
二叉堆满足堆的两个特性:
-
结构性:堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入
-
堆序性:父节点的值总是保持固定的序关系(大于或小于)于任何一个子节点的键值
当父节点的值总是大于或等于任何一个子节点的键值时为“最大堆”。
当父节点的值总是小于或等于任何一个子节点的键值时为“最小堆”。
结构性质
因为完全二叉树是有规律的,所以一般总是用一个数组来表示它。
-
如果根节点在数组中的位置是1,第n个位置的子节点分别在2n和2n+1。
-
如果存储数组的下标基于0,那么下标为i的节点的子节点是2i+1与2i+2,子节点i的父节点在位置(i-1)/2
下图中的数组对应上图中的完全二叉树
堆序性
堆序性质让二叉堆快速操作的根本,因为它的父节点总是大于(小于)子节点,所以要找出最大(最小)元素就变的很容易了。
在下图中左边的树是一个堆,而右边的则不是(虚线部分的堆序被破坏了)
基本的堆操作
插入(上滤策略)
在将一个元素插入X插入堆时,为了保证树是一个完全树,我们需要在下一个可用位置创建一个空穴,然后将元素X放置在该空穴中。如果X在当前位置并没有破坏堆的堆序性,则插入完成。然而,往往事实上并非如此。这时我们就需要将空穴的父节点与当前新创建的节点进行交换。这样不断的与父节点比较,直到X到达它合适的位置,即满足堆序性位置。用图片演示过程如下:
原始树 | 在下一个可用位置创建一个空穴,然后将元素67放置在该空穴中 |
---|---|
发现不满足堆序性 | 和父节点交换位置 |
发现不满足堆序性 | 和父节点交换位置 |
动画效果如下: |
这种策略叫做上滤,新元素在堆中不断向上找到合适的位置。
使用下面代码很容易实现插入操作:
///插入操作
void insert(int num) {
///将数据按顺序向后排序
heapList.add(num);
///开始上滤
percolateUp(num);
}
///上滤
void percolateUp(int num) {
///首先找到最后一位新加入的元素
///并不断与其父节点比较。如果大于它的父节点,则将其向上移动
///不断将向上交换,直至目标不再大于父节点
for (var hole = heapList.length - 1;
heapList[hole].compareTo(heapList[((hole - 1) ~/ 2)]) > 0;
hole = (hole - 1) ~/ 2) {
heapList[hole] = heapList[(hole - 1) ~/ 2];
heapList[(hole - 1) ~/ 2] = num;
}
}
复制代码
删除根节点(最大或者最小元素)(下滤策略)
找到最大或者最小元素很简单,它就是根节点。困难的是删除它。当删除根节点时,会在根处建立一个空穴。此时堆少了一个元素,那么最后一个元素必须移动到合适的位置。我们可以首先直接把最后一个元素放入到空穴内,然后和其两个儿子进行比较,不断的将目标穴位向下推。类似于插入的方式,其动画效果如下:
使用下面代码实现二叉堆的删除操作:
///删除操作
void deleteRoot() {
///将最后一个节点的元素放入第一个节点,并删除最后一个节点
heapList[0] = heapList[heapList.length - 1];
heapList.removeAt(heapList.length - 1);
///开始下滤
percolateDown();
systemOutList('删除根节点');
}
///下滤
void percolateDown({int targetIndex = 0}) {
///首先找到根节点,此时里面已经是放置着删除之前的最后一个元素
///并不断与其字节点最小的对比,并将其和大于它的元素中最小的交换
///不断将向下交换,直至目标不再小于任意子节点
var heapLength = heapList.length - 1;
int targetChild;
for (var hole = targetIndex; 2 * hole + 1 <= heapLength; hole = targetChild) {
///找到左子节点,并默认为目标穴位
targetChild = 2 * hole + 1;
///找到最小的子节点,作为目标穴位
///如果左子节点小于右子节点,则目标节点为右子节点
if (targetChild < heapList.length - 1 &&
heapList[targetChild].compareTo(heapList[targetChild + 1]) < 0) {
targetChild++;
}
if (heapList[targetChild].compareTo(heapList[hole]) > 0) {
///如果目标穴位里的值小于其最小的子节点
///那么进行下滤,将二者交换;
var temp = heapList[hole];
heapList[hole] = heapList[targetChild];
heapList[targetChild] = temp;
} else {
break;
}
}
}
复制代码
数组堆化
当你想把一个数组转换成二叉堆数组时,你可以使用n次插入方法去实现。但是,还有另外一种更好的方法去实现它。
一般的操作时将数据按任意顺序放入树中,此时只保持结构性。此时我们找到最后一个非叶子节点。根据上文中描述的结构性:最后一个叶子节点的父节点就是最后一个非叶子节点,而且子节点i的父节点在位置(i-1)/2,那么第一个非叶子节点就是(length-1)/2。找到最后一个非叶子节点后,对他进行下滤操作。然后遍历所有的非叶子节点,实现较大数据的上滤。
总结步骤如下:
-
将数据随意放入树中,但要保持堆的结构性;
-
找到第一个非叶子节点,并尝试对它进行下滤操作;
-
继续向上寻找上一个非叶子节点,并尝试对它进行下滤操作;
-
重复第三步,直到根节点位置。
其动画过程如下:
代码也很简单:
///将数组堆化
void buildHeap(List<int> list){
heapList = List.from(list);
///beginIndex为第一个非叶子节点
var beginIndex = heapList.length~/2 -1;
///用第一个非叶子节点和它的子节点对比
///找出左右两个子节点中大于当前节点的数据并与父节点交换
///不断的将最大值推到堆的根部。
///注意此处,是将当前位置的值进行下滤
///相对的,对于较大的值,是一个上滤的过程
for (var i = beginIndex; i >= 0; i--) {
percolateDown(targetIndex: i);
}
}
复制代码
堆排序
到了这里,堆排序已经呼之欲出了——我们只需要把数组堆化,然后不断的取出根节点就可以了:
///原始的堆排序
List<int> heapSore(){
var sortList= <int>[];
///堆化后的数据
var rank = heapList.length;
for(var i = 0;i < rank;i++){
///取出跟节点
sortList.add(heapList[0]);
deleteRoot();
}
print('排序后的数组:$sortList');
return sortList;
}
复制代码
动画演示一个排序的过程:
优化后的排序代码可参考:堆排序
总结和参考
更多排序算法请参考:基础的排序算法
二叉堆动画演示:Binary Heap
相关源码请参考:Demo