图解代码 堆的构建 及 堆排序

堆的本质

堆的本质完全二叉树,但堆分为大堆小堆
大堆:树中所以的父亲都大于左右孩子
小堆:树中所有的父亲都小于左右孩子
在这里插入图片描述

堆的一般作用

选出前n个最大或最小的值,选大用大堆选小用小堆,以及堆排序的应用
但本文只涉及堆的建立及选出前n个最大或最小的值

建堆的主要思想及动图解释

以大堆为列,从二叉树下方插入数据(暂且称为孩子)然后让此孩子与父亲节点比较如果他大于父亲节点。则父亲节点与与孩子节点交换位置,交换完后再与当前节点的父亲节点比较如果仍比父亲大则交换,如果小于等于则调整结束

这个调整过程由于是从二叉树的下方向上比较并调整,所以咱可以把他称为向上调整,如下图是建大堆的过程建议观看两遍以上
请添加图片描述

堆中数据的删除及动图解释

上面已经说了堆一般都是用于解决前n个较大或较小的值,或者堆排序。所以堆中数据的删除都是删除堆顶,删除中间的值意义不大但思想相同。所以这里主要讲解如何删除堆顶数据,以及删除后如何调整。
请添加图片描述
总结:第一步将堆最后一位数据覆盖到堆顶数据位置,然后与左右孩子中较大的一位比较,如比他小则交换位置。交换完后再与当前位置的左右孩子中较大的孩子比较,如比他小则交换位置。直至没有孩子或比左右孩子都大为止。

这个调整过程由于是从二叉树的上方向下方比较并调整,所以咱可以把他称为向下调整

堆的存储方式 及 父亲和孩子节点的计算

堆虽然是一颗完全二叉树,但它的存储方式一般为数组存储,就是说堆的所有节点都存储在数组当中。
如下图
在这里插入图片描述
以下公式数据类型为int

孩子节点的计算
假设父节点的下标是parent,那么它的左孩子下标就是 2*parent+1;它的右孩子下标就是 2*parent+2
比如父亲节点下标为 1 , 则其左孩子下标为 2 * 1 + 1 = 3 ;右孩子下标为 2 * 1 + 2 = 4(不懂对照图多读几遍)

父亲节点的计算
假设孩子节点下标为son(左右孩子均可),那他的父节下标(son-1)/2
如孩子下标为4 ,则父亲下标为 (4 - 1)/ 2 = 1;

堆的构建

堆的插入

比如插入一个10到数组的尾上,再进行向上调整算法,直到满足堆的结构要求。
向上调整:从二叉树下方插入数据(暂且称为孩子)然后让此孩子与父亲节点比较如果他大于父亲节点。则父亲节点与与孩子节点交换位置,交换完后再与当前节点的父亲节点比较如果仍比父亲大则交换,如果小于等于则调整结束
在这里插入图片描述

堆的删除

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
向下调整:第一步将堆最后一位数据覆盖到堆顶数据位置,然后与左右孩子中较大的一位比较,如比他小则交换位置。交换完后再与当前位置的左右孩子中较大的孩子比较,如比他小则交换位置。直至没有孩子或比左右孩子都大为止。
在这里插入图片描述
下面是代码实现功能包括:初始化,堆的销毁,插入数据,删除堆顶数据,查看堆顶数据,打印堆中数据等;
分三个源文件Heap.h是程序头文件,heap.c是堆的功能性函数源文件,main.c是测试heap.c中函数功能用的

下面是heap.c

#include"Heap.h"
void HpInit(HP* head)//初始化
{
    
    
	assert(head);
	head->data = NULL;
	head->size = head->capacity = 0;
}
void HpDestroy(HP* head)//销毁
{
    
    
	assert(head);
	free(head->data);
	head->data = NULL;
	head->size = head->capacity = 0;
}
void Swp(HP* head, int sor, int parent)//交换函数
{
    
    
	HeapData cur = head->data[sor];
	head->data[sor] = head->data[parent];
	head->data[parent] = cur;
}
//插入调整函数
void Adjustup(HP* head)
{
    
    
	assert(head);
	int sor = head->size;//要调整的数
	while (sor > 0)
	{
    
    
		int parent = (sor - 1) / 2;//求其父亲节点
		if (head->data[sor] <= head->data[parent])
		{
    
    
			break;//比父亲节点小或等于跳出
		}
		Swp(head, sor, parent);//如果比父亲节点大就交换
		sor = parent;
	}
}
void HpPus(HP* head , HeapData data)//插入数据
{
    
    
	assert(head);//
	if (head->capacity == head->size)//相等说明存满了
	{
    
    
		head->capacity == 0 ? (head->capacity = 4) : (head->capacity *= 2);
		//head->capacity = 4;
		HeapData* cur = (HeapData*)realloc(head->data, sizeof(HeapData) * head->capacity);
		if (cur == NULL)//如果内存申请失败
		{
    
    
			perror("realloc");
			exit(-1);
		}
		head->data = cur;
	}
	//开始插入
	head->data[head->size] = data;
	//调整部分
	Adjustup(head);
	head->size++;//插入完成
}
void print(HP* head)//打印
{
    
    
	assert(head);
	int i = 0;
	while (i < head->size)
	{
    
    
		printf("%d ", head->data[i]);
		if ( i != 0 && i % 10 == 0 )
		{
    
    
			printf("\n");
		}
		i++;
	}
}
//堆判空
bool HpEmpty(HP* head)
{
    
    
	assert(head);
	return !head->size;//为空返回真
}
void AdjustDown(HP* head)//删除后向下调整
{
    
    
	assert(head);
	int parent = 0;
	while (parent < head->size)
	{
    
    
		int leftsor = parent * 2 + 1;//左孩子下标
		if (leftsor >= head->size)//已经调整完成,没有这个不会有bug,误打误撞对了而已
		{
    
    
			break;
		}
		if (leftsor + 1 < head->size && head->data[leftsor] < head->data[leftsor + 1])
		{
    
    
			leftsor++;//找出左右孩子中较大的一位,注意leftsor + 1不要越界
		}
		if (head->data[parent] >= head->data[leftsor])//已经调整完成
		{
    
    
			break;
		}
		//如果parent小于他的孩子则交换
		Swp(head, leftsor, parent);
		parent = leftsor;
	}
	//调整完成结束
}
//堆的中间删没意义,一般都是删除堆顶
void HpPop(HP* head)
{
    
    
	assert(head);
	if (HpEmpty(head))
	{
    
    
		printf("无数据\n");
		return;
	}
	head->data[0] = head->data[head->size - 1];//删除头部数据
	head->size--;
	//向下调整数据
	AdjustDown(head);
	//删除完成
}
HeapData HpTop(HP* head)//查看头部数据
{
    
    
	assert(head);
	if (HpEmpty(head))
	{
    
    
		printf("无数据\n");
		return -1;
	}
	 return head->data[0];
}
int Hpsize(HP* head)//查看数据个数
{
    
    
	assert(head);
	return head->size;
}

下面是main.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
void Text1()
{
    
    
	HP head;
	HpInit(&head);
	int i = 0;
	int x = 0;
	do
	{
    
    
		printf("\n1 插入  2 删除  3 打印  4 查看头部数据 \n");
		printf("请输入需要的功能\n");
		scanf("%d", &i);
		switch (i)
		{
    
    
		case 1:
			printf("请输入数据\n");
			scanf("%d", &x);
			HpPus(&head, x);
			break;
		case 2:
			HpPop(&head);
			break;
		case 3:
			print(&head);
			break;
		case 4:
			printf("%d", HpTop(&head));
			break;
		}

	} while (i);
}
int main()
{
    
    
	Text1();

	return 0;
}

下面是Heap.h

#pragma once
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int HeapData;

typedef struct heap
{
    
    
	HeapData* data;
	int size;//实际数据个数
	int capacity;//空间容量
}HP; 

void HpInit(HP* head);//初始化
void HpDestroy(HP* head);//销毁
void HpPus(HP* head, HeapData data);//插入数据
void print(HP* head);//打印
void HpPop(HP* head);//头删
HeapData HpTop(HP* head);//查看头部数据
int Hpsize(HP* head);//查看数据个数

在无序数组上建堆(用数组中已有数据构建)

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
在这里插入图片描述
下面是调整图解,第一次调整的是最后一个数据的父亲节点(假设孩子节点下标为son(左右孩子均可),那他的
父节下标(son-1)/2
。如孩子下标为4 ,则父亲下标为 (4 - 1)/ 2 = 1;);

调整完后如最后一个数据的父亲节点下标为,i ,则直接 i-- ,继续将 i 下标视为堆顶继续向下调整,直至i < 0 为止;
在这里插入图片描述

建堆时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
在这里插入图片描述
下面是代码实现(为不破坏原有数组我是复制数组内容后再进行建堆的):

void AdjustDown(HeapData* arr,int parent, int size)
{
    
    
	int leftson = parent * 2 + 1;//左孩子
	while (leftson < size)//
	{
    
    
		if (leftson + 1 < size && arr[leftson + 1] > arr[leftson])
		{
    
    
			leftson++;//如果右孩子大于左孩子
		}
		if (arr[leftson] < arr[parent])//如果孩子小于父亲
		{
    
    
			break;
		}
		Sawp(arr, leftson, parent);
		parent = leftson;
		leftson = parent * 2 + 1;
	}
}
void CreateHeap(HP* head, int* arr, int size)
{
    
    
	assert(head);
	int parent, son = size - 1;
	head->data = (HeapData*)malloc(sizeof(int) * size);//给数组的复制开辟空间
	if (NULL == head->data)//如果开辟失败
	{
    
    
		perror("malloc");
		exit(-1);
	}
	memcpy(head->data, arr, sizeof(int) * size);//拷贝需要建堆的数组
	head->capacity = head->size = size;//开区间
	for (parent = (son - 1) / 2; parent >= 0; parent--)//传最后一个数字据的父亲开始建堆
	{
    
    
		AdjustDown(head->data, parent,head->size);//将父节点首地址传过去
	}
}

堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

  1. 建堆
    升序:建大堆
    降序:建小堆
  2. 利用堆删除思想来进行排序
    建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆序。

堆排序的实现

首先得用上面的的方法先建一个堆,再如下图,先把堆顶数据与最后一位互换,交换结束再进行一次向下调整把换上来的数调整到合适的地方要满足堆的结构要求注意堆顶数据换到尾部后就不参与建堆了让其保持不动
不断的换,及调整直至堆只剩一个数
在这里插入图片描述
下面是代码实现:

void Sawp(HeapData* arr, int x, int y)//交换函数
{
    
    
	HeapData ch = arr[x];
	arr[x] = arr[y];
	arr[y] = ch;
}
void AdjustDown(HeapData* arr,int parent, int size)//向下调整
{
    
    
	int leftson = parent * 2 + 1;//左孩子
	while (leftson < size)//
	{
    
    
		if (leftson + 1 < size && arr[leftson + 1] > arr[leftson])
		{
    
    
			leftson++;//如果右孩子大于左孩子
		}
		if (arr[leftson] < arr[parent])//如果孩子小于父亲
		{
    
    
			break;
		}
		Sawp(arr, leftson, parent);
		parent = leftson;
		leftson = parent * 2 + 1;
	}
}
//堆排序
void HeapSort(int* arr, int size)
{
    
    
	assert(arr);
	int parent, son = size - 1;
	for (parent = (son - 1) / 2; parent >= 0; parent--)//传最后一个数据的父亲开始建堆
	{
    
    
		AdjustDown(arr, parent, size);//将父节点首地址传过去
	}
	//上面是建堆,下面需要进行堆排序
	int Hpsize = size-1;//数组最后一位
	for (Hpsize; Hpsize > 0; Hpsize--)
	{
    
    
		Sawp(arr, 0, Hpsize);//每次将较大值换到最后一位
		AdjustDown(arr, 0 , Hpsize);//交换完后调整,不调整刚刚换的那一位
	}
}

到这就结束啦,不足的地方欢迎评论区指教

猜你喜欢

转载自blog.csdn.net/ZhuGeBin26/article/details/128502864