堆排序原理及实现

堆排序是一种原地排序算法,效率与归并排序相当。本文深入探讨堆的基本性质,包括最大堆和最小堆,并详细阐述如何维护堆性质、建立堆以及堆排序的完整过程。通过实例解释了节点的"上浮"和"下沉"操作,提供了维护堆和堆排序的代码实现思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

堆排序原理及实现

概述

​ 排序算法在程序设计中属于使用频度很高的一类算法,好的排序算法对于程序效率的提升有一定作用。常见的简单排序算法如冒泡排序、插入排序,对于多数情况来说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个节点的交换,交换位置之后,”上浮”的节点满足小于等于其父节点的要求,”下沉”的节点则需要再检查是否满足堆性质。

调整不满足的节点(1)

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

调整不满足的节点(2)

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

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

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,这是堆的大小,即待排序的元素数量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值