堆的实现以及堆排序

   一 堆的介绍:

    堆其实就是一个特殊完全二叉树,分为两类,当任一父节点大于自己的子节点时称为大根堆,当大任一父节点小于自己的子节点时称为大根堆,堆只有这两种。接下来我们就说说如何利用堆来快速排序。堆排序首先要先建立相应的堆,如果我们要排升序,那我们就建立大堆,至于为什么呢?我们根据图来看。我们先建立了一个大堆,

  二 堆排序介绍

然后把最大值交换到最后一个节点,

 此时的二叉树从数组来看,我们这些数字中的最大值20就已经到了合适的位置,我们再把3向下调整,直到遇到子节点比自己小或者下面已经无子节点,由于3在最开始向下调整的选最大的子节点数接替自己,由大堆的特性此时根节点就是数组元素中的次大值,我们只要重复上述操作就可以把一个个数的顺序排出来。这其实是借鉴了堆删除的思想,堆删除是删除堆顶元素并且不能破坏堆的父子节点之间的大小关系,也就是说之前是大堆,删除之后用必须保持为大堆。

三 堆删除

接下来我们画图看看堆删除

先把堆顶元素交换到堆数组的最后一个位置,然后像删除数组中最后一个元素一样,size--就好了 

但我们还要把此时换上来的3向下调整,将堆恢复为大堆,以此来看,堆排序就是利用堆删除的思想。

四 堆的实现代码介绍

Heap.h(放函数的声明和头文件)

#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>
typedef int HeapData;
typedef struct Heap
{
	HeapData* a;
	int size;
	int capcity;
}Heap;
//堆的初始化
void HeapInit(Heap* Hp);
//堆的销毁
void HeapDestroy(Heap* Hp);
// 堆的插入
void HeapPush(Heap* hp, HeapData x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HeapData HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
bool HeapEmpty(Heap* hp);
//向下调整
void AdjustDown(HeapData* a, int size, int parent);
//交换函数
void swap(HeapData* p1, HeapData* p2);
//堆排序
//void HeapSort(Heap* hp);
//数组堆排序
void HeapSort(int* a, int n);

Heap.c(函数定义)

#include"Heap.h"
void HeapInit(Heap* Hp)
{
	assert(Hp);
	Hp->a = (HeapData*)malloc(sizeof(HeapData)*4);
堆存储数据其实就是一个数组来存数据,二叉树的形式只是我们看待数据的方式
	Hp->size = 0;
	Hp->capcity = 4;
}
//堆销毁
void HeapDestroy(Heap* Hp)
{
	assert(Hp);
	Hp->capcity = 0;
	Hp->size = 0;
	free(Hp);//动态开辟的空间一定要记得释放
}
//交换 在向下调整和向上调整经常有交换动作,所以特地写个函数来简化
void swap(HeapData* p1, HeapData* p2)
{
	assert(p1&&p2);
	HeapData tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
//向上调整
void AdjustUP(HeapData* a, int child)
{
	assert(a);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[parent] < a[child])//父亲节点小于子节点,交换建大堆
		{
			swap(&a[parent], &a[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
//堆的插入,采取向上调整
void HeapPush(Heap* Hp, HeapData x)
{
	assert(Hp);
	if (Hp->size == Hp->capcity)//判断堆空间是否充足
	{
		HeapData* new = (HeapData*)realloc(Hp->a,sizeof(HeapData)*(Hp->capcity+2));
		if (new == NULL)
		{
			perror("HeapPush realloc");
			return;
		}
		Hp->a = new;
		new = NULL;
		Hp->capcity += 2;
	}
	Hp->a[Hp->size++] = x;
	AdjustUP(Hp->a,Hp->size-1);//插入新数据,采用向上调整建堆
如果是面对一个数组,数组尚未建堆,此时我们可以模拟插入建堆,下标一个个访问数组元素,
不断向上调整,也能达到建堆的效果。
}
//向下调整
void AdjustDown(HeapData* a, int size,int parent)
{
	assert(a);
	int child = parent * 2 + 1;
	while (child<size)
	{
		//若是child+1超出范围则无需找最大或最小孩子节点
		if (child+1<size && a[child]< a[child + 1])//找更大或更小的孩子节点代替
		{
			child++;
		}
		if(a[parent]<a[child])//建大堆,如果父节点小于子节点,交换建大堆
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
//堆的删除
void HeapPop(Heap* Hp)
{
	assert(!HeapEmpty(Hp));//是空返回true,非空返回false
	assert(Hp);
	swap(&Hp->a[Hp->size-1],&Hp->a[0]);
	Hp->size--;
	AdjustDown(Hp->a,Hp->size-1,0);
}
//取堆顶元素
HeapData HeapTop(Heap* Hp)
{
	assert(Hp);
	return Hp->a[0];
}
//获取堆的元素个数
int HeapSize(Heap* Hp)
{
	assert(Hp);
	return Hp->size;
}
//判空
bool HeapEmpty(Heap* Hp)
{
	assert(Hp);
	return Hp->size == 0;
}

//数组堆排序
void HeapSort(int* a, int n)
{
	//建堆
	int i = 0;
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n,i);向下调整建堆,比向上调整建堆的时间复杂度稍微低一点
	}
	i = n;
	printf("排序前为:");
	for (int j = 0; j < i; j++)
	{
		printf("%d ", a[j]);//打印验证
	}
	printf("\n");
	i = n;
	//堆排序
	while (--i)
	{
	   swap(&a[i], &a[0]);//交换后一个元素位置确立
		 AdjustDown(a, i,0);//每次交换完待排序的i(size)自减1
	}
	printf("排序后为:");
	i = n;
	for(int j=0;j<i;j++)
	{
		printf("%d ", a[j]);//打印验证
	}
}

五 top-k问题

  有时候我们并不是要排序整个数组,而是要从数字找最大或最小的前十个数,这意味着我们没必要将数字排序后取十个数字,有种方法是取大堆的top,然后pop,再从子节点中选较大的接替根节点,依次取十个数字,但这是有限制,堆是数组中建立的,如果我们的数据太多,比如40个G的数据,栈无法存储如此多的数据,那我们就只能将数据放在文件中,但是我们无法在文件中建立堆,因为文件的随机访问特别麻烦,所以较好的解决方法是从文件中读取k个数据建立小堆,然后读剩下的数据来替换小堆中的数据,至于为什么是小堆呢,那是怕最大的数挡在堆的前面,导致未能选出最大的前k个数

void AdjustDown(int* top,int k,int size)
{
   assert(top);
	int child = k*2+1;
	int parent = k;
	
	while (child<size)
	{
      要先判断右子节点是否越界,不然后面访问会导致越界访问
		if (child + 1 <size && top[child] > top[child + 1])
		{
			child++;//找小的子节点
		}
		if (top[parent] > top[child])
		{
			swap(&top[parent], &top[child]);
			parent= child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void Creatnum()
{
	const char*file= "data.txt";
文件名是字符串常量,字符串常量不可修改,所以指针用const修饰,使其在语法层面编译不通过
	FILE* fin = fopen(file, "w");fin管理文件data
	srand(time(0));
	int n = 1000;
	for (int i = 0; i < n; i++)//输出1000个随机数到文件中去
	{
		int x = rand() % 1000;
		fprintf(fin,"%d\n",x);
	}
	fclose(fin);
}
void PrintTop(const char* file, int k)
{
	int* top = (int*)malloc(sizeof(int) * k);
	if (top == NULL)
	{
		perror("malloc fail");
		return;
	}
	FILE* fin = fopen(file, "r");
	//读k个数据
	for (int i = 0; i < k; i++)
	{
		fscanf(fin, "%d", &top[i]);
	}
	//建小堆--向下建堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(top,i,k);
	}
	找最大的k个数
	int val = 0;
	int ret=fscanf(fin, "%d",&val);从文件中读取一个数字,并且保存返回值
	while (ret != EOF)
	{
		if (val > top[0])//从文件继续读数,
		{
			top[0] = val;如果val比堆顶数据大那就替换掉堆顶数据,并将val向下调整
			AdjustDown(top, 0, k);
		}
		ret = fscanf(fin, "%d", &val);读取fsanf返回值,判断是否到达文件尾部
	}
打印最大的k个数
	for (int i = 0; i < k; i++)
	{
		printf("%d ", top[i]);该k个最大的数并非按顺序排列
	}
fclose(fin);
}
int main()
{
	int k = 10;
	Creatnum();
	PrintTop("data.txt", k);
	return 0;
}

六:堆排序时间复杂度分析

堆排序首先要建立堆,建堆分向上建堆和向下建堆,下面分别讨论两种时间复杂度 

向上调整建堆复杂度:

    首先第一层是没办法向上调整的,所以我们要从第二层开始调整算时间复杂度按最坏的情况,第二层的每个数值都要向上调整到第一层,由于我们是考虑满二叉树的时间复杂度,所以每层的节点数我们是知道的,第二层每个节点调到第一层次数为一次,第三层的每个节点要调整两次,依次类推,第h层节点数有2^(h-1)个,他们要调整到第一层要h-1次,由此列式的T(n)=2^1*(1)+2^2*(2)+2^3*(3)......2^(h-1)*(h-1), 2T(n)=2^2*(1)+2^3*(2)......2^(h-1)*(h-2)+2^(h)*(h-1),错位相减法得,2T(n)-T(n)=-2-2^2-2^3...-2^(h-1)+2^(h)*(h-1)=2-2^h+2^h*h-2^h,由于总节点数n=2^h-1,所以T(n)约等于NlogN-2N,量级来看向上调整建堆是O(NlogN)。

向下调整建堆复杂度:

   这个时候是从倒数第二层开始调整,每个节点只需要向下调整一次,依次类推,第二层节点只需要向下调整h-2次,第一层节点则需要调整h-1次,总调整次数列式得T(n)=2^0*(h-1)+2^1*(h-2)+2^2*(h-3)+2^3*(h-4)......2^(h-2)*1,从式子上看我们就知道向下调整建堆比向上调整建堆要好,节点数量大的调整次数少最后同样用错位相减法得出,T(n)=2^h-2+1-h,约等于n-logn,所以向下调整建堆的时间复杂度为O(N)

    建完堆后就到排序了,排序的步骤我们先前都说了,先交换,然后把根节点向下调整,由于最后一层实际上占总结点的一半,且都比较小,所以我们认为所有节点都是从根节点向下调整到接近最后一层,调整次数为logn,n个节点总调整次数为NlogN,所以堆排序最后的时间复杂度是O(NlogN),所以不管是向上调整建堆还是向下调整建堆,堆排序的量级都是O(NlogN),但是向下调整建堆能让堆排序的时间耗费更小。

    还有就是堆排序的基本操作有交换和比较,我认为算时间复杂度时两者均认为是1,所以堆排序可以只考虑交换,而不考虑比较次数,即使比较次数接近交换次数,但是乘2对于时间复杂度的量级来说是不变的,所以这就是为什么堆排序只考虑交换次数的原因。

以上就是我对于堆的理解,如果有错误和问题,欢迎来指正!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小何只露尖尖角

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值