堆排序(heap sort)
1 堆
1.1 堆是什么?
堆是满足一下性质的完全二叉树:每个结点的值都大于(小于)等于其左右子树中任意结点的值。每个节点都大于等于其左右子树任意结点的称为大顶堆;每个节点小于等于左右子树任意结点的称为小顶堆。
1.2 堆的存储结构
由定义可知堆是一种完全二叉树,所以堆可以由数组来存储,通过数组下标可以知道其父节点和孩子结点所在位置。一个节点在数组中的下标为i,那么其父节点在i/2下标所在位置,其孩子结点是下标为2i和2i+1的数据元素。
1.3 堆的常用操作
1.3.1 往堆中插入元素
往堆中插入数据元素时将待插入数据元素放在数组末尾,将其与父节点值进行比较,如果大于父节点值则交换,直到与堆顶元素比较,这时就构成了新的堆。因为只是不断的与父节点进行比较,比较操作的执行次数最多为完全二叉树的高度,所以插入操作的时间复杂度为O(logn)。
1.3.2 删除堆顶元素
删除堆顶元素后任然要维持堆的结构。可以将数组中末尾的数据元素替换堆顶元素,再对其进行堆化,也就是将新的堆顶元素替换到堆中的合适位置,这个过程的时间复杂度也为O(logn)。
2 堆排序
2.1 出现背景
选择排序过程中,第i趟排序中挑选出待排序列第i大数据元素需要进行n-i次比较操作,但是并没有将比较后的结果保存下来,导致一些数据元素的重复比较。如果在排序过程中将比较的结果保存下来,那么比较次数会少很多,排序效率也会大大提高。
2.2 思想
将待排序列构造成大顶堆,此时堆顶元素就为序列的最大值。将堆顶元素与数组末尾元素进行交换,然后对n-1个数据元素堆化,直到剩余数据元素个数为1。
2.3 实现及关键点
2.3.1 关键点
1)堆化过程中,只要达到父节点大于等于左右孩子结点的处则停止堆化。由堆的定义可知,此时此结点以下已构成堆;
2)构建堆的过程中,为了统一交换堆顶元素的操作,采用自上到下的堆化方法。因为叶子节点向下堆化过程中只有自身,所以从第一个非叶子节点开始堆化;完全二叉树的第一个非叶子节点在数组中的下标为n/2(数组从下标1开始存放数据)。
2.3.2 实现
1 //堆排序 2 //对i位置节点以下的数据元素进行堆化 3 static void Heapify(ElemType* pElem, int size, int i) { 4 int maxIndex = i; //父节点及其左右孩子节点中值最大的节点下标 5 while (true) { 6 if (2 * i <= size && pElem[2 * i] > pElem[i]) maxIndex = 2 * i; //父节点值小于左孩子节点值 7 if (2 * i + 1 <= size && pElem[2 * i + 1] > pElem[2 * i]) maxIndex = 2 * i + 1; //右孩子值为最大值 8 if (maxIndex == i) break; //父节点值为最大值 9 swap(pElem, i, maxIndex); //将最大值放在父节点 10 i = maxIndex; 11 } 12 } 13 //pElem为数组首地址,n位数组末尾节点下标 14 void HeapSort(ElemType* pElem, int n) { 15 //对数组元素构建大顶堆,非叶子节点下标为1 ~ n/2 16 for (int i = n / 2; i > 0; --i) { 17 Heapify(pElem, n, i); 18 } 19 VisitArray(pElem, 1, n); 20 //排序 21 for (int i = n; i > 1; --i) { 22 swap(pElem, 1, i); //交换堆顶元素和未排序末尾元素 23 Heapify(pElem, i - 1, 1); 24 VisitArray(pElem, 1, n); 25 } 26 }
测试结果:
2.4 算法分析
2.4.1 时间复杂度
堆排序的操作主要分两部分,一是构建堆,二是对每次排序剩余元素进行堆化。构建堆就是将各个非叶子节点堆化的过程,堆化过程中叶子节点比较操作次数可以写成:
S = 2 ^ 0 * h + 2 ^ 1 * (h - 1) + ... + 2 ^ (h - 1) * 1
那么,2S = 2 ^ 1 * h + 2 ^ 2 * (h - 1) + ... + 2 ^ (h - 1) * 2 + 2 ^ h * 1
所以,S = -2h + 2 ^ 1 + 2 ^ 2 + ... + 2 ^ (h - 1) + 2 ^ h = 2 ^ h - 2h - 2。
因为h近似等于log2n,所以比较操作执行次数近似n,交换操作次数小于等于比较操作执行次数,所以构建堆的时间复杂度为O(n)。
排序过程中要执行n-1次从堆顶元素的堆化,第i次排序的时间复杂度为logi,所以排序过程的时间复杂度为O(nlogn)。所以堆排序的时间复杂度为O(nlogn)。
2.4.2 空间复杂度
堆排序过程中,只用到少许零时变量,所以堆排序是一种原地排序算法,空间复杂度为O(1)。
2.4.3 稳定性
堆排序的构建堆的过程中只有孩子结点大于父节点时才会进行数据元素之间的交换,所以构建堆的过程中不会改变相同元素的相对位置。但是在排序过程中,数组首元素和剩余排序元素的末尾元素总是跳跃交换,可能会发生相对位置的改变,所以堆排序是不稳定的排序算法。
2.4.4 比较操作执行次数和交换操作执行次数
堆排序两个步骤的时间复杂度分别为O(n)、O(nlogn),所以比较操作执行次数与输入规模的关系为O(nlogn) + O(n)。交换操作执行次数小于比较操作执行次数。
2.5 为什么在实际应用中堆排序比快速应用的少?
1)堆排序数据访问的方式没有快速排序友好。堆排序是父节点、孩子结点的跳跃形式访问,对CPU的缓存不友好;而快速排序是顺序访问的。
2)堆排序要先构建大顶堆,有可能会使得序列的逆序度增加,交换操作执行次数增加。
该篇博客是自己的学习博客,水平有限,如果有哪里理解不对的地方,希望大家可以指正!