数据结构——原来二叉树可以这么学?(2:堆的详解)

前言:

  小编在前文讲述了一个全新的数据结构——树以及它的分支二叉树的概念,下面,我将承接上一篇,开始进行二叉树顺序结构实现的讲解。

.


目录

1.堆的概念和结构

1.1.堆的概念

1.2.根的结构

2.堆的特性与性质

2.1.堆的性质

 2.2.堆的特性

3.堆功能的实现 (小堆)

3.1.堆的初始化

 3.2.堆的销毁 

3.3.向上调整建堆法

 3.4.入堆

 3.5.向下调整堆

 3.6.出堆

 3.7.判断堆是否为空

 3.8.取堆顶的元素

4.代码展示

 4.1.Heap.h

 4.2.Heap.c

5.总结


正文

1.堆的概念和结构

  可能有很多读者朋友看到这个标题会有疑惑,堆是个什么玩意?这篇博客不是讲的二叉树的顺序结构的实现吗?不要着急,小编会慢慢的进行解释的。首先,堆是一种特殊的二叉树,在具备了一些二叉树的特性的同时,还具备了一些其他的性质,我们可以把堆看做成一个完全二叉树来理解,小编在之前也说过,完全二叉树是要用顺序结构来实现的,所以一般堆来说也是用数组来实现的,下面小编先开始讲述一下堆的概念以及结构。

1.1.堆的概念

  如果一个关键码的集合K = {k0,k1,k2,k3……kn-1},把它的元素按照完全二叉树的顺序储存方式储存,在一个一维的数组中,并满足:ki <= k2 * i  + 1 (ki >= k 2 * i + 1并且ki <= k2 * i + 1),则称为为小堆(大堆),可能很多读者朋友看到这个数学公式会觉的不理解,因为这牵扯到小编后面讲述的堆的一些特性,小编在这里用自己的语言来说一下,如果一个堆(看成完全二叉树),它的孩子结点的数值不小于其父结点的值,那么我们可以把这个堆称之为小堆;对于大堆,它和小堆的概念是完全相反的,对于一个堆,如果它的孩子结点不大于它的父亲结点,那么我们可以把它称之为大堆;大小堆的概念还是很通俗易懂的。我们把根结点最大的堆被称作大根堆,那么根结点最小的堆自然被称为小根堆。

1.2.根的结构

  上面小编讲述了堆的一些概念,讲述了什么叫大堆(大根堆),小堆(小根堆);为了让各位读者朋友更好的理解根的结构,下面小编就给这两个的结构图:

  以上便就是堆的概念和结构,下面小编将要讲述的堆的一些特性,系好安全带,准备出发!

2.堆的特性与性质

2.1.堆的性质

  堆具有以下的性质:

1.堆中某个结点的值总是不大于或者不小于其父结点的值(区分大小堆,这个特性)

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

  上面小编曾经提到过一个数学公式,小编在那里交代着小编将会在特性来讲述一下为什么有那个数学公式,这牵扯到了二叉树的一些特性,下面小编就给出这几条特性:

 2.2.堆的特性

  对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点有:

 1.若i > 0,i位置的结点的双亲序号:(i - 1)/ 2;i = 0,i为根结点的编号,没有双亲结点。

 2.若2i + 1 < n,左孩子序号:2i + 1,2i + 1 >= n 则无左孩子。

 3.若2i + 2 < n,右孩子序号:2i + 2,2i + 2 >= n 则无右孩子。

  这些特性各位读者朋友记住就好,具体的推导过程小编就不详细的说了(老实说我也没深究这个),通过堆的特性,我们就可以通过一个结点来寻找到它的孩子结点,这对于我们等会堆的创建有着很大的帮助,以上便就是堆的相关知识点,我们仅仅知道知识点但不会去实际运用它也是不可以的,下面小编就开始进行堆的实现喽(相信这是大部分读者朋友点进去这篇博客所想看到的),对了,这里小编实现的是小堆,会了小堆的话各位自然就会大堆了,读者朋友可以自行实现。

3.堆功能的实现 (小堆)

  在进行堆各种功能的实现之前,小编先把堆的结构体实现出来,其实和顺序表的结构体是一样的,下面小编直接给出代码:

typedef int HPDateType;  //不确定堆储存的是什么类型

typedef struct Heap
{
	HPDateType* arr;
	int size;  //实际元素大小
	int capciaty; //总空间大小
}HP;

3.1.堆的初始化

  对于堆,我们首先的操作肯定要先对里面的内容进行初始化,对于其的初始化,其实和当初我们写顺序表的初始化操作是一样的,无非就是把arr给置为空,然后让实际元素大小和总空间都等于0即可,下面小编直接给出代码:

void HPInit(HP* s1)
{
	assert(s1);
	s1->arr = NULL;
	s1->capciaty = s1->size = 0;
}

 3.2.堆的销毁 

  有初始化必定有销毁操作,销毁操作也和当初我们写顺序表是一样的,我们首先要确定此时的arr是否已经动态内存开辟了空间,如果动态开辟了空间,那么我们就释放它,然后把它再次置为空,避免其成为野指针,之后我们再让有效个数和空间大小变为0即可,仔细一看,嘿,跟我们之前实现顺序表的操作一模一样,下面给上代码:

void HPDestory(HP* s1)
{
	assert(s1);
	if (s1->arr)
	{
		free(s1->arr);
		s1->arr = NULL;
	}
	s1->capciaty = s1->size = 0;
}

3.3.向上调整建堆法

  在讲述插入操作之前,小编先给各位讲述一下一个建堆的方法,向上调整建堆,因为我们插入数据以后,还要保证这个堆是小堆,所以我们需要调用此方法来对插完数据以后的堆进行调整,保证它的父结点要小于等于孩子结点,此时的向上调整建堆法可以完美的去解决这个问题,下面进入讲解环节:

  首先,我们此时的需要使用到arr,以及arr中最后一个元素(用child来简写了)所在的位置,把它俩作为形参,之后,通过名字我们就可以知道,我们是从堆中最后一个元素向上进行调整的,所以我们需要找到最后一个结点的父结点,此时我们就需要使用到堆的特性来找到它的父结点,即让child / 2 - 1,此时我们用一个变量来接受它(parent),之后我们就要使用到循环的方式来进行向上的调整,循环的条件等会再说,在循环体里,我们需要先判断父结点和孩子结点的大小,如果父亲结点大于孩子结点,那么此时我们就要让父结点与孩子结点的值进行交换,此时小编写了交换函数来把两个数进行交换,交换函数相比各位都不陌生了,下面小编直接给出代码:

void Swap(HPDateType* x, HPDateType* y)
{
	int m = *x;
	*x = *y;
	*y = m;
}

  之后我们先让孩子结点走到父结点的位置,之后在让孩子结点寻找到父结点的位置然后赋值给parent,之后继续循环比较关系;所以此时我们就可以知晓循环的条件是什么,如果此时的孩子结点小于0的话,此时我们的数组就会越界,所以循环的条件自然是child < 0;当然,如果我们在中间某一个过程的父结点本身就比孩子结点小的话,那么此时我们就没有在进行循环的必要了,break即可,此时我们就实现了向上调整建堆的功能,下面小编给出代码。

void AdjustUp(HPDateType* arr, int childen)
{
	int parents = childen / 2 - 1;
	while (childen > 0)
	{
		if (arr[childen] < arr[parents])
		{
			Swap(&arr[childen], &arr[parents]);
			childen = parents;
			parents = childen / 2 - 1;
		}
		else
		{
			break;
		}
	}
}

 3.4.入堆

  在进行完初始化操作以后,我们就要进行插入操作了,堆的插入操作我们一般称之为入堆操作,其实入堆操作的前半部分和我们在写顺序表时候的尾插是一样的,我们同样需要一个元素插入到堆的尾部,在顺序表操作的时候我们此时仅需让size++就好了,但是堆不一样,小编在前面说了,此时我们建立的是小堆,我们得按照小堆的性质进行排序,即父结点的值小于等于孩子结点;此时我们就要使用到一种方法对堆里面的数据进行改变了,此时小编使用的是向上调整建堆法(向上调整建堆法在后面小编在上面已经讲述了),在进行调整完以后,此时我们的堆已经就是小堆了,这时候在让size++就好了,此时我们就成功的建立起堆,下面小编给出代码:

void HPPush(HP* s1, HPDateType x)
{
	assert(s1);
	if (s1 -> capciaty == s1 -> size) //判断堆的数组是否是满的,如果是满的就继续开辟空间
	{
		int newcapciaty = s1->capciaty == 0 ? 4 : s1->capciaty * 2;
		HPDateType* arr1 = (HPDateType*)realloc(s1->arr, sizeof(HPDateType) * newcapciaty);
		assert(arr1);
		s1->arr = arr1;
		s1->capciaty = newcapciaty;
	}
	s1->arr[s1->size] = x;
	AdjustUp(s1 -> arr,s1 -> size);
	++s1->size;
}

 3.5.向下调整堆

  在讲述出堆之前,我们同样也需要考虑到此时堆里面的元素改变的情况,此时我们就需要对堆在进行调整操作,这里为了更好的讲述,小编先来说一下我们是如何进行出堆的,此时我们的出堆并不是直接把堆最后一个元素删除,出堆操作其实是堆顶的删除,此时我们需要先把堆顶和堆底进行交换,之后我们在进行堆的调整,之后我们让size--即可,所以我们需要从堆顶开始往下进行调整堆,所以我们才命名为向下调整堆,下面小编详细解释一下这个方法:

  首先,此时我们需要有arr,0(堆顶,parent),n(堆中元素的个数),之后我们需要通过父结点找到孩子结点,此时用到了堆的特性,我们先创立一个变量(child),这个变量是来储存孩子结点,我们先找左孩子结点(2 * parent + 1),然后我们就要进入循环了,循环条件小编先不说,此时我们需要判断左孩子和右孩子到底谁小,我们选出那个最小的和父结点进行比较,如果右孩子结点更小的话,我么直接让child++就好了,不过此时我们需要先判断右孩子是否存在,如果child + 1 > n的话,那么我们就可以直接判断右孩子结点是不存在的,此时我们就无需在进行比较了;在比较完之后,我们需要让这个小的和父结点进行比较,如果父结点更大的话,我们让父结点和孩子结点进行交换,让父结点走到孩子结点的位置,孩子结点继续称为父结点的左孩子,然后我们继续进行循环,所以我们可以知道此时的循环条件应该是孩子结点小于n,因为孩子结点走的是最快的,如果孩子结点超过了n,那么可能出现让一个已经删除的数据和父结点比较的情况(小编遇到过),此时就乱套了,所以此时的循环条件是这个;当然,如果父亲结点本身就比子结点小的话,我们直接结束循环即可,此时就是向下调整堆的方法,下面小编给出代码:

void AdjustDown(HPDateType* arr, int parents, int n)
{
	int childen = parents * 2 + 1;  //左孩子
	while (childen < n)
	{
		//先判断左右孩子的大小
		if (childen + 1 < n &&arr[childen] > arr[childen + 1])
		{
			childen++;
		}
		if (arr[parents] > arr[childen])
		{
			Swap(&arr[parents], &arr[childen]);
			parents = childen;
			childen = parents * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

 3.6.出堆

  在讲述完入堆以后,我们就可以进行出堆操作,堆的出堆操作并不是直接讲堆最后的元素删除,而是把堆顶的数据删除,如果我们直接删除堆顶的话,那么会直接乱掉这个堆,所以小编的方法就是先把堆顶和堆底的数据进行交换,然后我们在进行向下调整堆的操作(上面小编就说过了),然后我们直接让size--就好了,可能这里有读者朋友会疑惑交换操作,因为如果我们直接让堆顶和堆底交换的话,此时我们并没有破坏掉中间的小堆结构,我们仅仅向下调整就可以做到重新建堆操作,如果我们直接删除堆顶的数据,此时堆顶就换成了左孩子结点了,此时的堆的中间部分也乱套了,所以小编不推荐直接删除堆顶,下面小编给出代码:

void HPPop(HP* s1)
{
	assert(s1 && s1 -> size);
	Swap(&s1->arr[0], &s1->arr[s1->size - 1]);
	s1->size--;
	AdjustDown(s1->arr, 0, s1->size);
}

 3.7.判断堆是否为空

  这个操作算是最轻松的一个操作了,我们仅需要判断堆的有效数据个数是否为0就好,下面小编直接给出代码:

bool HPEmpty(HP s1)
{
	return s1.size == 0;
}

 3.8.取堆顶的元素

  这个操作也是很简单的,不过我们再取堆顶之前,我们首先要判断堆是否为空。直接调用上面的函数即可,然后我们返回arr[0]即可,下面给出代码图:

HPDateType HPTop(HP* s1)
{
	assert(s1 && !HPEmpty(*s1));
	return s1->arr[0];
}

  以上便就是堆的一些基本功能的实现,可能这些功能没有涉及的很全,如果今后小编有学到了新的功能小编就随时的加上,可能有很多读者朋友想知道完整的代码怎么写,下面小编就给各位展示完整的代码,当然此时的堆小编也是用三个文件写的(装函数的头文件,写函数的源文件,测试文件,读者朋友以后写一些数据结构的时候也可以干)。

4.代码展示

 4.1.Heap.h

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

//此时是建立小堆

typedef int HPDateType;


typedef struct Heap
{
	HPDateType* arr;
	int size;  //实际元素大小
	int capciaty; //总空间大小
}HP;

//堆的初始化
void HPInit(HP* s1);

//堆的销毁
void HPDestory(HP * s1);

//入堆
void HPPush(HP* s1, HPDateType x);

//向上调整建堆
void AdjustUp(HPDateType* arr, int childen);

//交换元素
void Swap(HPDateType* x, HPDateType* y);

//出堆(其实出堆顶的操作)
void HPPop(HP* s1);

//判断堆是否为空
bool HPEmpty(HP s1);

//取堆顶的元素
HPDateType HPTop(HP* s1);

 4.2.Heap.c

#include"Heap.h"

void HPInit(HP* s1)
{
	assert(s1);
	s1->arr = NULL;
	s1->capciaty = s1->size = 0;
}


void HPDestory(HP* s1)
{
	assert(s1);
	if (s1->arr)
	{
		free(s1->arr);
		s1->arr = NULL;
	}
	s1->capciaty = s1->size = 0;
}

void Swap(HPDateType* x, HPDateType* y)
{
	int m = *x;
	*x = *y;
	*y = m;
}

void AdjustUp(HPDateType* arr, int childen)
{
	int parents = childen / 2 - 1;
	while (childen > 0)
	{
		if (arr[childen] < arr[parents])
		{
			Swap(&arr[childen], &arr[parents]);
			childen = parents;
			parents = childen / 2 - 1;
		}
		else
		{
			break;
		}
	}
}

void HPPush(HP* s1, HPDateType x)
{
	assert(s1);
	if (s1 -> capciaty == s1 -> size) //判断堆的数组是否是满的,如果是满的就继续开辟空间
	{
		int newcapciaty = s1->capciaty == 0 ? 4 : s1->capciaty * 2;
		HPDateType* arr1 = (HPDateType*)realloc(s1->arr, sizeof(HPDateType) * newcapciaty);
		assert(arr1);
		s1->arr = arr1;
		s1->capciaty = newcapciaty;
	}
	s1->arr[s1->size] = x;
	AdjustUp(s1 -> arr,s1 -> size);
	++s1->size;
}

void AdjustDown(HPDateType* arr, int parents, int n)
{
	int childen = parents * 2 + 1;  //左孩子
	while (childen < n)
	{
		//先判断左右孩子的大小
		if (childen + 1 < n &&arr[childen] > arr[childen + 1])
		{
			childen++;
		}
		if (arr[parents] > arr[childen])
		{
			Swap(&arr[parents], &arr[childen]);
			parents = childen;
			childen = parents * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HPPop(HP* s1)
{
	assert(s1 && s1 -> size);
	Swap(&s1->arr[0], &s1->arr[s1->size - 1]);
	s1->size--;
	AdjustDown(s1->arr, 0, s1->size);
}


bool HPEmpty(HP s1)
{
	return s1.size == 0;
}

HPDateType HPTop(HP* s1)
{
	assert(s1 && !HPEmpty(*s1));
	return s1->arr[0];
}

5.总结

  以上便就是二叉树的顺序结构——堆的实现,本来这篇文章小编在学完堆的时候就可以开写,不过小编那时候就欠了不少文章了,所以一直拖延到现在,也不知道这篇博客我会什么时候发出去,希望是在国庆之前,这几天小编一直在狂补数据结构的知识,小编学校的专业课也开数据结构这门课了,小编得回来复习一下从而可以让我上课可以划水~(读书朋友不要学我,上课好好学习)如果文章有错误,请在评论区指出,小编一定会即使的做出更改,那么,我们下一篇文章见喽!

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值