数据结构与算法初阶7:基于二叉树的堆和堆排序知识精讲

本文详细解读了堆数据结构,包括大根堆与小根堆的概念,堆的顺序存储实现,以及堆排序的原理与步骤,通过实例演示了堆操作和堆排序的过程。

《数据结构与算法初阶6:树与二叉树基础知识精讲》https://blog.youkuaiyun.com/King_lm_Guard/article/details/125885126       在初阶6中,博主为大家详细讲解了树和二叉树的基本概念及相关特性,这一讲将以从二叉树中衍生出的“堆和堆排序”为主要讲解对象,下面跟着博主一起来学习吧。

目录

1、堆相关概念及结构特性

1.1 回顾二叉树顺序结构

1.2 堆基本概念及结构特性

 1.3 堆基础概念训练营

2、堆的实现

2.1  堆结构体实现

2.2 堆初始化

2.3 堆打印函数

2.4 堆销毁

2.5 堆的数据插入

2.6堆的向上调整函数

2.7堆的数据删除

2.8 堆的向下调整函数

2.9 取堆顶的数据

2.10 堆的判空

2.11 求解堆的数据个数

3、堆排序的实现

3.1、建堆

3.2 利用堆删除数据思想进行排序

4、结语


 1、堆相关概念及结构特性

      在详细分析堆的特性之前,需要读者们注意:这里提到的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。大家不要混淆,注意区分,二者可以说一点关联都没有。

1.1 回顾二叉树顺序结构

       初阶6中我讲过,二叉树有两种存储结构:顺序存储和链式存储,并详细区分了这两种存储结构的特性及适用场景,相信阅读过博主的第6讲应该知道:顺序存储就是适用数组存储,所以这种存储方式只适合表示完全二叉树,为了避免空间的浪费,一般的二叉树采用的是链式存储。本节开讲的堆可以理解成完全二叉树的一种特性,所以堆适合采用顺序存储的方式进行存储。

1.2 堆基本概念及结构特性

      我们已经知道堆是采用顺序存储,也就是数组方式存储数据,但堆不同于一般完全二叉树的特性在于:堆中数据存储分为大堆存储和小堆存储,为了便于理解,读者们可以从下面的示意图中加以理解。 

大堆存储:

       所谓大堆存储(又叫大根堆存储):就是在数据存储过程中,父结点存储的数据是最大的,其次就是与父结点相关的下一层的左结点和右结点,数据大小依次往下一层变小,我们把完全二叉树的这种特性称之为大堆存储或者大根堆存储。

大堆存储示意图

注意:

1、我们提到的二叉树的概念实际上是一种逻辑结构,也就是我们思维上的理解,便于使用者理解其特性,但在计算机中实际的存储结构仍然是链表或者数组形式存储,在这里就是数组方式存储,这是读者们需要注意的。

2、大堆存储满足的条件就是:相应的父结点比其对应的左结点和右结点大,但左结点和右结点谁大谁小无所谓。如上图中间的案例所示。

小堆存储:

        所谓小堆存储(又叫小根堆存储):就是在数据存储过程中,根结点存储的数据是最小的,其次就是与父结点相关的下一层的左结点和右结点,数据大小依次往下一层变大,我们把完全二叉树的这种特性称之为小堆存储或者小根堆存储。可以发现大堆存储和小堆存储就是两种存储数据大小相反的促存储方式。

小堆存储示意图

注意:

1、我们提到的二叉树的概念实际上是一种逻辑结构,也就是我们思维上的理解,便于使用者理解其特性,但在计算机中实际的存储结构仍然是链表或者数组形式存储,在这里就是数组方式存储,这是读者们需要注意的。

2、小堆存储满足的条件就是:相应的父结点比其对应的左结点和右结点小,但左结点和右结点谁大谁小无所谓。如上图中间的案例所示。

总结:

1、堆总是一颗完全二叉树;

2、对于大根堆而言:树中所有的父亲都是大于等于孩子结点;树形结构整体是降序的。

3、对于小根堆而言:树中所有的父亲都是小于等于孩子结点;树形结构整体是升序的。

 1.3 堆基础概念训练营

1.下列关键字序列为堆的是:()
A 100,60,70,50,32,65
B 60,70,65,50,32,100
C 65,100,70,32,50,60
D 70,65,100,32,50,60
E 32,50,100,70,65,60
F 50,100,70,65,60,32

 

解析:根据前面所讲的堆的特性,我们可以画出堆的逻辑结构示意图,以选项ABC为例:

依次根据堆特性判断,A选项正确。

2、堆的实现

2.1  堆结构体实现

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

//二叉树中的堆问题,用数组表示
typedef int HPDataType;
typedef struct Heap
{
	HPDataType*a; //堆数据存放的数组
	int size; //记录堆中元素个数
	int capacity;//记录堆开辟的存储容量
}HP;

//函数声明

//初始化
void HeapInit(HP*php);
//打印
void HeapPrint(HP*php);
//销毁
void HeapDestory(HP*php);
//堆的插入
void HeapPush(HP*php, HPDataType x);
//堆的删除
void HeapPop(HP*php);
//取堆顶的数据
HPDataType HeapTop(HP*php);
//堆的判空
bool HeapEmpty(HP*php);
//堆的数据个数
int HeapSize(HP*php);
//向上调整算法
void Adjustup(HPDataType*a, int child);
//向下调整算法
void Adjustdown(HPDataType*a, int size, int parent);

2.2 堆初始化

//初始化
void HeapInit(HP*php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}

2.3 堆打印函数

//打印
void HeapPrint(HP*php)
{
	assert(php);//断言,如果php为空直接结束
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

2.4 堆销毁

//销毁
void HeapDestory(HP*php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;
}

2.5 堆的数据插入

堆的插入数据工作原理示意图(以降序为例)

//交换函数
void Swap(HPDataType*p1, HPDataType*p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向上调整函数
void Adjustup(HPDataType*a, int child)
{
	int parent = (child - 1) / 2;
	while (child>0)
	{
		if (a[child] < a[parent]) //这里"<"是建升序,如果是需要建降序,则这里改为“>”符号
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}

//堆的数据插入
void HeapPush(HP*php, HPDataType x) //x表示待插入的数据
{
	assert(php);
	if (php->capacity == php->size)
	{
		//扩容
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType*tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType)*newcapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;

	}
	//开始插入数据
	php->a[php->size] = x;
	php->size++;
	//插入数据以后,需要排序:假设是建升序
	//调用排序函数
	Adjustup(php->a, php->size-1);
}

 2.6堆的向上调整函数

//向上调整函数
void Adjustup(HPDataType*a, int child)
{
	int parent = (child - 1) / 2;
	while (child>0)
	{
		if (a[child] < a[parent]) //这里"<"是建升序,如果是需要建降序,则这里改为“>”符号
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}

2.7堆的数据删除

       核心思想就是将首元素与尾元素交换,然后通过数组元素减减,实现数据的删除,但这种删除的方法,会破坏原有的堆结构,为此,需要进行再次排序,在堆删除中,我们采用的是向下调整函数,从根部出发,根据需要升序还是降序,合理设置左右孩子结点与父结点的大小关系,就可以恢复堆的数据顺序。

堆的删除数据工作原理示意图(以降序为例)
//交换函数
void Swap(HPDataType*p1, HPDataType*p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向下调整函数
void Adjustdown(HPDataType*a, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child<size)
	{
		//如果是升序,选出左右孩子中小的那个,然后将小的孩子与父亲比较,如果比父亲小,则交换
		if (child + 1 < size && 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 HeapPop(HP*php)
{
	assert(php);
	assert(php->size > 0);
	//先将最后一个元素与根结点交换
	Swap(&(php->a[0]), &(php->a[php->size - 1]));
	//然后元素个数减一
	php->size--;
    //元素删除需要排序,假设采用升序
	Adjustdown(php->a, php->size, 0);
}

2.8 堆的向下调整函数

//向下调整函数
void Adjustdown(HPDataType*a, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child<size)
	{
		//如果是升序,选出左右孩子中小的那个,然后将小的孩子与父亲比较,如果比父亲小,则交换
		if (child + 1 < size && a[child + 1] < a[child]) //注意判断右孩子在不在;注意如果是升序,则这里用<“,如果是降序,这里是“>”符号
		{
			child++;
		}

		//此时孩子和父亲比较,如果孩子比父亲小,则交换
        //注意如果是升序,则这里用<“,如果是降序,这里是“>”符号
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

 2.9 取堆顶的数据

//取堆顶的数据
HPDataType HeapTop(HP*php)
{
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}

 2.10 堆的判空

//堆的判空
bool HeapEmpty(HP*php)
{
	assert(php);
	return php->size == 0;
}

 2.11 求解堆的数据个数

//堆的数据个数
int HeapSize(HP*php)
{
	assert(php);
	return php->size; 
//因为在堆的数据插入中,插入数据以后size会加加1次,所以堆的数据个数就是size的大小。
}

3、堆排序的实现

      堆排序的思想就是利用前面分析的建堆的思想进行的,实现堆排序需要分两部分:建堆和利用堆删除思想进行排序。但需要注意一点就是:利用向下调整算法有一个前提:左右子树必须是一个堆,才能够调整!!!!!为此我们在进行堆排序时,首先需要确保数组是堆。

3.1、建堆

        通过前面第2节的分析我们可以知道:可以利用向上调整算法和向下调整算法建堆,但二者的时间复杂度是不一样的,在这里我就不详细展开讨论了,直接给出结论:

向下调整算法的时间复杂度是:O(N);(注:N表示总结点个数)

向上调整算法的时间复杂度是:O(N*logN),(注:这里的logN表示以2为底N的对数)。

       从时间复杂度的比较中我们可以看到:向下调整算法的时间复杂度更低,为此我们在建堆的时候采用向下调整算法。

!! 重点:如果我们需要将数组排成升序,则需要建大堆;因为这样在后面利用堆删除的思路的时候,就可以依次将堆顶的首元素即最大值交换到最后面,然后重新建大堆,重新交换,就可以实现升序; 同理,如果是需要排降序,则需要建小堆,思路同上。

3.2 利用堆删除数据思想进行排序

       堆删除的思想在第2节中已经为大家详细分析过,我们知道堆删除同样采用向下调整算法。所以我们要实现堆排序,只要掌握了向下删除算法即可

这里我们直接以案例带着大家以具体的案例进行分析:

#define _CRT_SECURE_NO_WARNINGS 

#include<stdio.h>

void Swap(int*p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}



//向下调整算法,时间复杂度:O(N)
void AdjustDown(int* a, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child<size)
	{
		//找出左右孩子较小的一个
		if (child + 1 < size && 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 HeapSort(int*a, int size)
{

//如果要实现升序,需要先建大堆,然后通过堆删除即可实现整体的数组是升序的;
//如果要实现降序,需要先建小堆,然后通过堆删除即可实现整体的数组是降序的;

//方法:利用向下调整算法将数组创建成堆,但需要注意利用向下调整算法必须满足左右子树必须是一个堆,才能调整。
	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, size, i);
	}

	for (int i = 0; i < size; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");

	//然后再堆排序
	int end = size - 1;
	while (end>0)
	{
	  // 交换一次调整一次
		Swap(&a[0], &a[end]);
		//经过前面从下到上的向下调整将根的左子树和右子树变成了堆,所以看可以利用从根结点开始进行向下调整排序。
		AdjustDown(a, end, 0);
		--end;
	}
}

int main()
{
	//int arr[10] = { 2,4,3,1,5,6,7,8,9,10 };
	int arr[6] = { 1,5,3,8,7,6 };
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ",arr[i]);
	}
	printf("\n");
	HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

!!!重点:分析1:以数组[1,5,3,8,7,6]为例,为了实现降序,我们分两步走:

1.1、因为是降序,所以我们需要建小堆,代码细节如下:

size表示原数组的大小,size-1表示数组最后一个元素,此时我们需要用向下调整算法,则需要获得最后一个元素的父结点,利用第6讲的孩子父亲位置对应公式:

父亲=(孩子-1)/2

所以此时最后一个结点对应的父结点就是:(size-1-1)/2。然后依次从后向前循环调用向下调整函数,从而实现小堆的创建。

	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, size, i);
	}
//向下调整算法,时间复杂度:O(N)
void AdjustDown(int* a, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child<size)
	{
		//找出左右孩子较小的一个
		if (child + 1 < size && a[child + 1] < a[child])
		{
			child++;
		}
		//孩子和父亲比较
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}

	}
}

 1.2、因为是降序,所以选出左右孩子结点小的那一个,然后和父结点比较,如果比父结点小则交换二者的数据,然后将这个孩子结点作为新的父结点,在size范围内,然后向下循环调整。循环结束小堆就构建成功。

1.3、 然后再利用堆删除思想,同样采用向下调整算法,但要注意,此时利用end变量记录尾部元素位置,然后与堆顶元素交换,此时最小的元素已经被取到了,然后需要调整堆,因为堆已经被破坏了,此时调用向下调整函数,数组大小传end,将堆顶位置作为父结点,即parent=0作为参数传值给该函数,然后end--,此时交换后的元素保持在数组末尾,然后同样的方法分析前end-1个元素。

分析2:如果我们需要将数组进行升序,此时我们需要建大堆,然后同样利用向下调整算法进行调整,不过此时我们需要注意调整向下调整函数中大于小于符号的变换,此时需要把符号变化成下图所示:

//向下调整算法,时间复杂度:O(N)
//此时是排升序
void AdjustDown(int* a, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child<size)
	{
		//找出左右孩子较大的一个
		if (child + 1 < size && a[child + 1] > a[child])
		{
			child++;
		}
		//孩子和父亲比较
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}

	}
}

 

我们以数组:[2,4,3,1,5,6,7,8,9.10]为例说明:假设是升序,结果示意如图所示:

 

我们数组:[1,2,3,4,5,6,7,8,9,10]为例说明:假设是降序,结果示意如图所示:

4、结语

       今天这一讲主要为读者们详细介绍了堆和堆排序,在使用堆排序时,我们要掌握使用向下调整算法就可以了,然后就是控制好升序和降序中大于和小于符号的判断。制作不易,欢迎大家点赞、关注、支持!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值