数据结构--关于堆的那些事

1.树的概念及结构

1.1树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因 为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

注意:树型结构中,子树之间不能有交集。

image-20220403113820023

1.2树的相关概念

节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6

叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点

父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点

子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点

树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6

节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推

树的高度或深度:树中节点的最大层次; 如上图:树的高度为4

1.3树的表示

树的结构相对来说比较复杂,我们想要存储表示出来,既要保存值,又要保存节点与节点之间的关系。实际上树的表示方式比较多,我们这里就介绍一下最常用的孩子兄弟表示法。

typedef int DataType;
struct Node
{
    struct Node* firstChild;//第一个孩子
    struct Node* nextBrother;//下一个兄弟
    DataType data;//节点的值
}

我们创建这样的一个结构体,就将树很好的表示了出来

image-20220403115234870

2.二叉树

2.1二叉树的概念

二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树。

image-20220403115729553

注意:对于任意的二叉树都是由以下几种情况复合而成的:

image-20220403115817263

2.2特殊的二叉树

  1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是 说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。

  2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对 应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

image-20220403115949455

2.3二叉树的性质

1.一棵非空二叉树的第i层上最多有 2^(i-1) 个结点

2.深度为h的二叉树的最大结点数是 2^h -1。

3.度为0的节点个数比度为2的节点个数大1。

4.n个结点的满二叉树的深度,h= log2(n+1)。

5.父子节点下标的关系:

​ leftchild = 2*parent + 1;

​ rightchild = 2* parent + 2;

​ parent = (child - 1) / 2;

3.二叉树的顺序结构及实现

3.1二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

image-20220403123002883

3.2堆的概念及结构

image-20220403123248019

堆的性质:

1堆中某个节点的值总是不大于或不小于其父节点的值;

2堆总是一棵完全二叉树。

3.3堆的实现

3.3.1堆向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整 成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

image-20220403125030074

image-20220403125057479

image-20220403125116609

3.3.2堆的创建

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算 法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的 子树开始调整,一直调整到根节点的树,就可以调整成堆。

下面我们以如何调成一个大堆为例

image-20220403130916098

image-20220403130927080

image-20220403130941648

image-20220403130950781

3.3.3建堆的时间复杂度

O(N)

3.3.4堆的代码实现

heap.h

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HeapDataType;
#define NUM 4
typedef struct Heap
{
	HeapDataType* arr;
	size_t size;//有效元素个数
	size_t capacity;//容量
}Heap;

void HeapInit(Heap* php);
void HeapDestroy(Heap* php);
void HeapPush(Heap* php, HeapDataType data);
void HeapPop(Heap* php);
HeapDataType HeapTop(Heap* php);
bool HeapEmpty(Heap* php);
int HeapSize(Heap* php);
void HeapPrint(Heap* php);

heap.c

#include "Heap.h"

void HeapInit(Heap* php)
{
	assert(php);
	php->arr = NULL;
	php->size = php->capacity = 0;
}

void HeapDestroy(Heap* php)
{
	assert(php);
	free(php->arr);
	php->arr = NULL;
	php->size = php->capacity = 0;
}

void CheckCapacity(Heap* php)
{
	if (php->size == php->capacity)
	{
		int newCapacity = php->capacity == 0 ? NUM : php->capacity * 2;
		Heap* tmp = (Heap*)realloc(php->arr, sizeof(HeapDataType) * newCapacity);
		if (NULL == tmp)
		{
			printf("realloc fail\n");
			exit(1);
		}
		php->arr = tmp;
		php->capacity = newCapacity;
	}
}

void AdjustUP(HeapDataType* arr, int child)
{
	int father = (child - 1) / 2;
	while (child > 0)
	{
		HeapDataType tmp = arr[father];
		arr[father] = arr[child];
		arr[child] = tmp;
		child = father;
		father = (child - 1) / 2;
		if (arr[child] > arr[father])
		{
			//说明不需要继续向上调整了,提前结束循环
			break;
		}
	}
}
void HeapPush(Heap* php, HeapDataType data)
{
	assert(php);
	CheckCapacity(php);
	php->arr[php->size] = data;//先把新数据放在最后,接下来在判断是否需要调整位置
	
	//今天咱们以建小堆为例,如果新插入的元素不比父亲小则不用调整,否则需要向上调整 
	// 如果是插入第一个元素,那么也不用调整
	//首先需要明确,父子节点间的位置关系
	//LeftChild = 2 * father + 1;
	//RightChild = 2 * father + 2;
	//father = (Chile - 1) / 2;
	if (php->size == 0 || data >= php->arr[(php->size-1) / 2])
	{
		php->size++;
		return;
	}
	//下面需要向上调整
	AdjustUP(php->arr, php->size);
	php->size++;
}

void AdjustDown(HeapDataType* arr, int end)
{
	int father = 0;
	int LeftChild = 2 * father + 1;
	int RightChild = 2 * father + 2;
	
	while (1)
	{
		if (RightChild > end)
		{
			if (LeftChild > end)
			{
				//左右孩子都越界了,不用在调了
				break;
			}
			else
			{
				//左孩子存在,右孩子越界
				//左孩子就是最后一个,调最后一次
				if (arr[father] > arr[LeftChild])
				{
					HeapDataType tmp = arr[father];
					arr[father] = arr[LeftChild];
					arr[LeftChild] = tmp;
					break;
				}
				else
				{
					//只剩下左孩子但是不需要调了
					break;
				}
			}
		}
		//左右孩子都存在
		if (arr[father] < arr[LeftChild] && arr[father] < arr[RightChild])
		{
			//左右孩子都存在但是都比父亲大,说明已经调完了
			break;
		}
		HeapDataType tmp = arr[father];
		if (arr[LeftChild] < arr[RightChild])
		{
			arr[father] = arr[LeftChild];
			arr[LeftChild] = tmp;
			father = LeftChild;
		}
		else
		{
			arr[father] = arr[RightChild];
			arr[RightChild] = tmp;
			father = RightChild;
		}
		if (father == end)
		{
			//此时已经调整到最后了
			break;
		}
		LeftChild = 2 * father + 1;
		RightChild = 2 * father + 2;
	}

}
void HeapPop(Heap* php)
{
	//堆的删除删的是堆顶的元素
	//假如说,我们直接将顶部数据删除,然后把后面所有元素往前挪动一步,那么这样会破坏堆的结构
	//为了删除堆顶元素之后保证堆的结构不被破幻,我们应该
	//1.将堆顶元素和最后一个元素互换
	//2.size--即是删除堆顶元素
	//3.由于原来的最后一个元素被挪到了堆顶,破坏了堆的结构,自顶向下调整保证堆的结构

	assert(php);
	//先处理一下以下三种 元素个数为0,1,2的情况。这种不需要向下调整。
	if (php->size == 0)
	{
		return;
	}
	if (php->size == 1)
	{
		php->size--;
		return;
	}
	if (php->size == 2)
	{
		php->arr[0] = php->arr[1];
		php->size--;
		return;
	}

	//下面开始处理
	HeapDataType tmp = php->arr[0];
	php->arr[0] = php->arr[php->size-1];
	php->arr[php->size-1] = tmp;
	php->size--;
	AdjustDown(php->arr, php->size-1);
}
void HeapPrint(Heap* php)
{
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->arr[i]);
	}
	printf("\n");
}
bool HeapEmpty(Heap* php)
{
	assert(php);
	return php->size == 0 ? true : false;
}
HeapDataType HeapTop(Heap* php)
{
	assert(php);
	if (HeapEmpty(php))
	{
		printf("堆中无元素\n");
		exit(2);
	}
	return php->arr[0];
}

int HeapSize(Heap* php)
{
	assert(php);
	return php->size;
}
  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

逃跑的机械工

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

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

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

打赏作者

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

抵扣说明:

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

余额充值