堆的应用1——堆排序

本文详细介绍了堆排序算法,包括其基本原理、建堆(特别是向下调整建堆)的过程,以及堆调整排序中如何选择升序建大堆和降序建小堆。通过代码示例展示了堆排序的实现步骤和时间复杂度分析。
摘要由CSDN通过智能技术生成

堆排序

堆排序是一种基于比较的排序算法,它利用堆这种数据结构所设计。

堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父结点。

堆排序可以分为两个主要步骤:建堆和用堆删除思想调整排序。

1.堆排序实现思路

  1. 建堆
    • 初始时,将待排序序列构造成一个大顶堆(或小顶堆)。此时,整个序列的最大值(或最小值)就是堆顶的根结点。
    • 通常从最后一个非叶子结点开始调整,将其调整为一个堆,然后向前依次调整每一个非叶子结点,直到整个序列都成为一个堆。
  2. 堆调整排序
    • 此时整个序列的最大值(或最小值)就是堆顶的根结点。将其与末尾结点进行交换,此时末尾就为最大值。
    • 然后将剩余n-1个序列重新构造成一个堆,这样会得到n个元素中的次大值。如此反复执行,便能得到一个有序序列。

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

我们看动图

1.1.建堆

那我们到底怎么实现呢?

1.1.1向上调整建堆

 很简单我们以建大堆为例

// 以下部分是向上调整建堆的代码,但已经被注释掉,因为通常使用向下调整建堆,效率更高   
    for (int i = 1; i < n; ++i)  
    {  
        // 对第i个元素执行向上调整操作,使其满足堆的性质  
        // 这种方式建堆的时间复杂度为O(N*logN),不是最优的  
        AdjustUp(a, i);  
    }  
    

1.1.2向上调整建堆和向下调整建堆时间复杂度 

//向上调整建堆——O(N*logN)
for(int i=1;i<n;++i)
{
AdjustUp(a,i);
}

//向下调整建堆——O(N)
for(int i=(n-1-1)/2;i>=0;--i)
{
AdjustDown(a,n,i);
}

1.1.3向下调整建堆 

但是我们很快就会发现,这虽然可以,但是下面的堆调整排序用的是向下调整,我们这里如果使用向上调整,它的时间复杂度比向下调整的大,这会加大消耗,所以我们使用向下调整 

// 建堆 -- 向下调整建堆 -- O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

1.1.4建什么堆 

我们向排升序,我们建什么堆?降序呢?

我们以升序为例

有人说升序建小堆,因为打印小堆的第一个元素最小嘛但是这样子看起来轻松,但是实际上真的轻松吗?

我们要选次小的,只能将除了根节点之外的其他元素视作一个堆,这样子后面的关系全乱了——原来的兄弟变父子了,父子变兄弟了,那就重新建堆了,时间复杂度O(N*logN),代价太大了

不如直接遍历选择O(logN)

所以我们升序建大堆,降序建小堆

1.2.堆调整排序

  • 此时整个序列的最大值(或最小值)就是堆顶的根结点。将其与末尾结点进行交换,此时末尾就为最大值(最小值)。
  • 然后将剩余n-1个序列重新构造成一个堆,这样会得到n个元素中的次大(小)值。如此反复执行,便能得到一个有序序列。

简单的来说就是建堆得最大(小)值然后放到最后面去,对剩下的(除了放到后面去的)元素建堆选出最大(小)值,再放到后面去,再建堆,再放到后面去,依次类推 

假设我们已经建好了一个大堆

int a[]={20,17,4,16,5,3};

则堆排序应该是这样子的 

代码实现

int end = n - 1;
	while (end > 0)
	{
		Swap(&a[end], &a[0]);
		AdjustDown(a, end, 0);

		--end;
	}

4.堆排序代码实现

// HeapSort函数:使用堆排序算法对数组a进行升序排序  
void HeapSort(int* a, int n)  
{   

    // 使用向下调整建堆,从最后一个非叶子节点开始向前遍历  
    for (int i = (n - 1 - 1) / 2; i >= 0; --i)  
    {  
        // 对当前非叶子节点执行向下调整操作,使其及其子树满足堆的性质  
        // 由于每个非叶子节点只调整一次,因此总的时间复杂度为O(N)  
        AdjustDown(a, n, i);  
    }  
  
    // 以下部分是堆排序的主体,将堆顶的最大值(大顶堆)与当前未排序部分的最后一个元素交换  
    // 并重新调整堆,以保持堆的性质,直到整个数组排序完成  
    int end = n - 1; // end指向未排序部分的最后一个元素的索引  
    while (end > 0)  
    {  
        // 将堆顶元素(当前最大值)与未排序部分的最后一个元素交换  
        Swap(&a[end], &a[0]);  
  
        // 重新调整堆,保持堆的性质,此时堆的大小减少了1(因为已经取出了一个元素)  
        AdjustDown(a, end, 0);  
  
        // 缩小未排序部分的范围  
        --end;  
    }  
}

HeapSort 函数是用于执行堆排序的主要函数,它包括建堆和堆排序的两个主要步骤。

这里您选择使用向下调整的方法来建堆,这是一个高效的方法,因为向下调整每个非叶子结点只需要O(logN)的时间,并且整个建堆过程的时间复杂度为O(N)。所以整个堆排序过程的时间复杂度为O(N*logN)。 

5.代码解释

建堆 -- 向下调整建堆 -- O(N)

 for (int i = (n - 1 - 1) / 2; i >= 0; --i)  
 {  
 AdjustDown(a, n, i);  
 } 

这段代码从最后一个非叶子结点开始,向前遍历到根结点,并对每个结点调用AdjustDown函数进行向下调整。

由于非叶子结点的索引是(n - 1) / 2(向下取整),这里(n - 1 - 1) / 2是为了得到正确的最后一个非叶子结点的索引。每个结点的向下调整时间复杂度为O(logN),但由于每个结点只调整一次,所以整个建堆过程的时间复杂度为O(N)。

堆排序 -- O(N*logN)

 int end = n - 1;  
 while (end > 0)  
 {  
 Swap(&a[end], &a[0]);  
 AdjustDown(a, end, 0);  
 --end;  
 } 

这部分代码执行堆排序的主体部分。

它首先将堆顶元素(即当前最大值)与数组的末尾元素交换,然后调整剩下的元素使其恢复为大顶堆的性质。这个过程反复进行,直到堆中只剩下一个元素为止。每次交换和调整的时间复杂度为O(logN),因为需要调整n-1次,所以整个堆排序过程的时间复杂度为O(N*logN)。 

6.测试代码

#include<stdio.h>
typedef int HPDataType;

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType x = *p1;
	*p1 = *p2;
	*p2 = x;
}

//堆的向下调整(大堆)  
void AdjustDown(HPDataType* a, int n, int parent)
{
    int child = parent * 2 + 1; // 计算左子节点的索引  

    // 当 child 索引在数组范围内时执行循环  
    while (child < n)
    {
        // 如果右子节点存在且大于左子节点  
        if (child + 1 < n && a[child + 1] > a[child])
        {
            ++child; // 更新 child 为右子节点的索引  
        }

        // 如果 child 节点(现在是左右子节点中较大的一个)大于 parent 节点  
        if (a[child] > a[parent])
        {
            Swap(&a[child], &a[parent]); // 交换 parent 和 child 的值  
            parent = child; // 更新 parent 为刚刚交换过的 child 的索引  
            child = parent * 2 + 1; // 重新计算左子节点的索引  
        }
        else
        {
            break; // child 节点不大于 parent 节点,无需继续调整,退出循环  
        }
    }
}


// HeapSort函数:使用堆排序算法对数组a进行升序排序  
void HeapSort(int* a, int n)
{

    // 使用向下调整建堆,从最后一个非叶子节点开始向前遍历  
    for (int i = (n - 1 - 1) / 2; i >= 0; --i)
    {
        // 对当前非叶子节点执行向下调整操作,使其及其子树满足堆的性质  
        // 由于每个非叶子节点只调整一次,因此总的时间复杂度为O(N)  
        AdjustDown(a, n, i);
    }

    // 以下部分是堆排序的主体,将堆顶的最大值(大顶堆)与当前未排序部分的最后一个元素交换  
    // 并重新调整堆,以保持堆的性质,直到整个数组排序完成  
    int end = n - 1; // end指向未排序部分的最后一个元素的索引  
    while (end > 0)
    {
        // 将堆顶元素(当前最大值)与未排序部分的最后一个元素交换  
        Swap(&a[end], &a[0]);

        // 重新调整堆,保持堆的性质,此时堆的大小减少了1(因为已经取出了一个元素)  
        AdjustDown(a, end, 0);

        // 缩小未排序部分的范围  
        --end;
    }
}
int main()
{
	int a[] = { 2,6,8,9,4,5,1,3,7 };
	int size = sizeof(a) / sizeof(a[0]);
	HeapSort(a, size);
	for (int i = 0; i < size; i++)
	{
		printf("%d ", a[i]);
	}
}

大家一定要注意:升序建大堆,降序建小堆

  • 40
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 24
    评论
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值