2024年最新堆排序算法---C语言实现(超详细解析!!!!,2024年最新看这篇足矣了

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

既然上文说到可以直接把它看作二叉树,那不妨把逻辑结构画出来看看:

接下来,我们就要进行建堆了,有两种方法:

  1. 使用向上建堆,插入数据的思想建堆
  2. 使用向下调整建堆
💦向上调整建堆

**✨思路:**首先,**我们把第一个数字看成堆,也就是4,当第二个数字插入进去的时候,进行向上调整算法,使其确保为小堆,**向上调整的算法在上篇博文已详细讲解过,不过多赘述。具体插入数据过程就是遍历数组,确保数组里每一个数进行向上调整算法

✨:向上/下调整算法: 详解向上/下调整算法

✨:画图演示:(小根堆)

✨:代码演示

void swap(HPDatatype* x, HPDatatype* y)
{
	int temp = 0;
	temp = *x;
	*x = *y;
	*y = temp;
}

void AdjustUp(HPDatatype* a, int child)
{
	// 计算父亲的小标
	int parent = (child - 1) / 2;
	// 当 child 的小标大于 0 就继续 (也就小于是根节点位置)
	while (child > 0)
	{
		// 小堆 <
		// 大堆 >
		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

// 向上调整建堆
void Heap(int* a, int n)
{
    //建堆
    int i = 0;
    for (i = 1; i < n; i++) //应该从i=1时遍历,因为第一个数据在堆里不需要调整,后续再插入时调整
    {
        AdjustUp(a, i);
    }
    printf("堆:\n");
        for (int j = 0; j < n; j++)
        {
            printf("%d ", a[j]);
        }
}



int main()
{
    // 建堆测试
    int a[100];
    int j = 0;
    int sum ;
    printf("请输入你要插入堆的数据(-1结束):>\n");
    for (int i = 0; i < 100; i++)
    {
        scanf("%d", &sum);
        if (sum == -1)
        {
            break;
        }
        a[j++] = sum;
    }

    Heap(a, j);
    return 0;
}

✨:效果演示:

符合小根堆的性质

💦向下调整建堆
  • **问题:**能直接进行向下建堆吗?

答案:不能

解析:首先回顾下使用向下调整的前提是什么?必须得确保根结点的左右子树均为小堆才可,而这里,数组为乱序的,无法直接使用。

  • 解决办法:从倒数第一个非叶结点开始向下调整,从下往上调

**✨:****分析:**从该解决方案中,我们首先要找到这个倒数第一个非叶结点的数在哪?其实最后一个结点的父亲即为倒数第一个非叶结点。当我们找到这个非叶结点时,把它和它的孩子看成一个整体,进行向下调整。调整后,再将次父节点向前挪动,再次向下调整,依次循环下去。

  • 再回顾下父亲和孩子间的关系:
  1. leftchild = parent*2 + 1
  2. rightchild = parent*2 + 2
  3. parent = (child - 1) / 2

✨:画图分析

✨:代码演示:建小堆(数据:4 ,2,7,8,5,0,6)

void swap(HPDatatype* x, HPDatatype* y)
{
	int temp = 0;
	temp = *x;
	*x = *y;
	*y = temp;
}

// 向下调整
// 向下调整的前提:后面的数据是堆
void AdjustDown(HPDatatype* a, int n, int parent)
{
	// 左孩子
	int child = parent * 2 + 1;
	// 孩子可能会超出数组范围
	while (child < n)
	{
		// 如果右孩子小于左孩子
		// 找出小的那个孩子
		// 保证右孩子存在且不越界
		// 大堆 >
		// 小堆 <
		if (child + 1 < n && a[child+1] < a[child])
		{
			//跳转到有孩子那里
			child++;
		}
		// 开始向下调整
		// 大堆 >
		// 小堆 <
		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}

	}
}

// 向下调整建堆
void Heap(int* a, int n)
{
    //建堆
    int i = 0;
    for (int i = ((n - 1) - 1) / 2; i >= 0; i--)
      {
           // 给定一个 a 数组,从 i 这个位置进行建堆, n 表示数组的大小
           AdjustDown(a, n, i);
      }
    printf("堆:\n");
        for (int j = 0; j < n; j++)
        {
            printf("%d ", a[j]);
        }
}



int main()
{
    // 建堆测试
    int a[100];
    int j = 0;
    int sum ;
    printf("请输入你要插入堆的数据(-1结束):>\n");
    for (int i = 0; i < 100; i++)
    {
        scanf("%d", &sum);
        if (sum == -1)
        {
            break;
        }
        a[j++] = sum;
    }

    Heap(a, j);
    return 0;
}

✨:效果演示:

符合小根堆的性质

💦时间复杂度分析(向上建堆和向下建堆熟优?)

首先,我们画张图看下向上和向下建堆后的样子。

从上图中,我们可以看出,使用不同的方式建堆最后的样子是不同的,那哪种方式好呢?

**✨:**接下来,我将通过时间复杂度的方式为大家解惑:以一颗满二叉树为例:

✨****向上建堆的时间复杂度:

时间复杂度计算的是其调整的次数,根据上文的知识我们已经知晓其是从数组的第二个元素开始的,也就是可以理解为第二层的第一个节点。计算的思想非常简单:计算每层有多少个节点乘以该层的高度次,然后累计相加即可。如下:

通过计算得知:向上建堆的时间复杂度为O(N*logN)

✨****向下建堆的时间复杂度:

向下调整我们前面已经知道它是从倒数第1个非叶节点开始调整的,每层的调整次数为,该层的节点个数*该层高度减1,一直从第1层开始调直至倒数第2层,并将其依次累加,此计算过程和向上调整差不多,都是等比*等差的求和,过程如下:

通过计算得知:向下建堆的时间复杂度为****O(N)

通过上述计算,我们得到如下:

  1. 向上建堆:O(N*logN)
  2. 向下建堆:O(N)

由此可见,使用向下建堆的方式更优,其时间复杂度较小。当然,使用向上建堆也是可以的,只不过向下建堆更好一点。

💦升序(排序)建大堆还是小堆?

✨****思考:**排升序,建小堆可以吗?-- 可以是可以,但没啥意思。
首先对 n 个数建小堆,选出最小的数,接着对剩下的 n-1 个数建小堆,选出第2小的数,不断重复上述过程……。建 n 个数的堆时间复杂度是O(N)
,****所以上述操作时间复杂度为O(N2),效率太低,**尤其是当数据量大的时候,效率更低,同时堆的价值没有被体现出来,还不如用直接排序。

【最佳方法排升序,因为数字越来越大,需要找到最大的数字,得建大堆

  1. 首先对 n 个数建大堆。
  2. 将最大的数(堆顶)和最后一个数交换,把最大的数放到最后
  3. 前面 n-1 个数的堆结构没有被破坏(最后一个数不看做堆里面的),根节点的左右子树依旧是大堆,所以我们进行一次向下调整成大堆即可选出第2大的数,放到倒数第二个位置,然后重复上述步骤……。

时间复杂度】:建堆时间复杂度为O(N),向下调整时间复杂度为O(logN),这里我们最多进行N-2次向下调整。

所以堆排序时间复杂度为 :O = O(N) + O(N-2)*O(logN) = O(N*logN),效率是很高的。

**✨解决方案:**升序建大堆,降序建小堆

💦完整的堆排序(升序)图形和代码演示
  • 先看下建好大堆的样子:
  • ✨思路:首先,得明确我们建堆后,此时堆顶就是最大的数据,现在我们把第一个数字和最后一个数字交换,把最后一个数字不看做堆里的,只需要数组个数N–即可。此时的左子树和右子树依旧是大堆,再进行向下调整即可。
  • 画图解析过程:
  • ✨:代码演示:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

// 数据类型存储
typedef int HPDatatype;

// 数据交换
void Swap(HPDatatype* x, HPDatatype* y)
{
HPDatatype temp = 0;
temp = *x;
*x = *y;
*y = temp;
}

// HPDatatype* a :表述动态数组
// n :表示堆内(数组内)的数据个数
// parent :父节点的下标(开始在根节点)
void AdjustDown(HPDatatype* a, int n, int parent)
{
// 左孩子
int child = parent * 2 + 1;
// 防止孩子跑出数组范围(最后一个节点为叶子节点)
while (child < n)
{
// 判断左右孩子那个大,将大的与父节点进行交换
// child+1 < n 表示确保右孩子存在
// 小堆 if (child + 1 < n && a[child] > a[child+1])
if (child + 1 < n && a[child] < a[child+1])
{
// 如果右孩子比左孩子大
child++;
}

  // 开始向下调整
  // 小堆:if (a[child] < a[parent])
  if (a[child] > a[parent])   // 大堆
  {
  	Swap(&a[child], &a[parent]);
  	parent = child;
  	child = parent * 2 + 1;
  }
  else

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

{
  	Swap(&a[child], &a[parent]);
  	parent = child;
  	child = parent * 2 + 1;
  }
  else

[外链图片转存中…(img-S5i44Wdb-1715669928612)]
[外链图片转存中…(img-aGOUCmEh-1715669928613)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 15
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值