您的位置:首页 > 理论基础 > 数据结构算法

堆排序原理及实现

2017-04-18 09:45 711 查看

堆排序原理及实现

概述

​ 排序算法在程序设计中属于使用频度很高的一类算法,好的排序算法对于程序效率的提升有一定作用。常见的简单排序算法如冒泡排序、插入排序,对于多数情况来说O(n^2)的时间复杂度并不是太理想,效果较好的归并排序倒是时间复杂度达到O(n * lgn)了,可惜要使用额外的数组空间。所幸,有一种和归并排序效率差不多的原地排序算法——堆排序。这里就记录一下堆排序的原理及实现细节。

堆的基本性质

​ 堆是一种数据结构,在堆排序中用到的堆则更特殊一点,是二叉堆。结构类似于二叉树,每个节点至多有2个子节点,它可以近似被看作一个完全二叉树,除了最底下的一层外,其它层都是被充满的。

​ 常见的二叉堆实现是数组形式,因为二叉堆的父子节点在数组中索引有明确的数学关系:

Left(i) = i * 2, Right(i) = i * 2 + 1, i是当前节点索引,Left表达式求解当前节点的左子节点索引,Right表达式求解当前节点的右子节点索引。

​ 二叉堆可以分为2种形式,一种是最大堆,一种是最小堆。2种形式在结构上是相似的,只是性质不同。

​ 对于最大堆:Array[Parent(i)] >= Array[i],该性质用文字描述可以这么表示,任一节点值(除根节点以外)小于或等于其父节点值。

​ 对于最小堆:Array[Parent(i)] <= Array[i],该性质用文字描述可以这么表示,任一节点值(除根节点以外)大于或等于其父节点值。

维护堆性质(以最大堆为例)

​ 维护堆性质,对于堆添加新元素或改变元素值时是很重要的一步。对于最大堆来说,任一节点值(除根节点外)小于等于其父节点值,也就是说,任一节点值必定大于等于其左右子树的所有节点值,当这一性质不满足时,我们需要做的处理是,在其左右子树中找出一个值最大的节点让它”上浮”到当前节点,而当前节点则应该逐层”下沉”到一个合适的位置。这就是维护堆性质的办法。

​ 我们以一个例子来说明维护堆性质的过程,假定有以下最大堆,其中一个节点(索引为2)破坏了堆性质:



​ 发现索引2的节点破坏了堆性质,在其左右子节点中寻找较大的节点,索引为2的节点”下沉”到索引为4的位置上,原先索引为4的子节点”上浮”到索引2的位置。因为这一步操作仅涉及到2个节点的交换,交换位置之后,”上浮”的节点满足小于等于其父节点的要求,”下沉”的节点则需要再检查是否满足堆性质。



​ 继续发现索引4的节点破坏了堆性质,继续”下沉”,在其左右子节点中寻找较大的节点,索引为4的节点”下沉”到索引为9的位置上,索引为9的节点则”上浮”。



继续检查,发现满足堆性质。至此,维护堆性质的过程执行完毕。

​ 维护堆性质的过程大致如此,要注意体会”上浮”和”下沉”这两个操作的意义。具体的维护堆性质的代码如下(以供参考):

void max_heapify(int *arr, size_t index, size_t heap_size)
{
// heap_size是堆中元素数量

size_t largest = index;
size_t l = LEFT(index);
size_t r = RIGHT(index);

if (l > heap_size) {  // 递归退出条件,当节点不存在子节点
return ;
}

if (arr[index] < arr[l]) {
largest = l;
}
else {
largest = index;
}

if (r <= heap_size && arr[largest] < arr[r]) {
largest = r;
}

if (largest != index) {
std::swap(arr[index], arr[largest]);
max_heapify(arr, largest, heap_size);
}
}


建立堆(以最大堆为例)

​ 对于堆的最底下的一层,由于没有子节点,它们必定是各自满足堆性质的,所以最底下一层可以不进行调整。维护堆性质的过程的重要前提条件是:节点的左右子节点必须各自满足堆性质。因此,可以从倒数第二层开始,逐层向上调整即可。倒数第二层的可能需要进行调整的节点的索引可由最底下一层的最后一个节点索引除以2求得。最底下一层的最后一个节点除以2得到的索引,不一定是倒数第二层的最后一个节点,因为堆并不一定是满二叉树。所以除以2这个操作得到的应该是倒数第二层中,可能需要进行调整的节点的索引。

​ 具体代码如下,可以参考思路实现:

void build_max_heap(int *arr, size_t heap_size)
{
size_t i;
for (i = heap_size / 2; i >= 1; --i) {
max_heapify(arr, i, heap_size);
}
}


堆排序

​ 有了上面的基础,直接逐层调用就好了,简单的堆排序可以这样写:

void heap_sort(int *arr, size_t sz)
{
build_max_heap(arr, sz - 1);
}

int main(int argc, const char *argv[])
{
int arr[11] = { INT_MAX, 3, 4, 1, 2, 6, 5, 7, 8, 9, 0 };
heap_sort(arr, 11);

for (size_t i = 1; i <= 10; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
return 0;
}


​ 这里有一些地方需要说明,待排序数组第一个元素填充一个INT_MAX是为了方便处理堆节点的序号,因为以1开始的堆节点索引更容易处理。堆排序的堆有2个属性要区分,一是arr_size,这是数组的大小,可能并未填充满;另一个则是heap_size,这是堆的大小,即待排序的元素数量。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息