数据结构——二叉树的顺序结构及实现(堆)

数据结构——二叉树的顺序结构及实现(堆),第1张

前言

此文章主要讲解二叉树的顺序结构,堆的实现。


在阅读本文之前,希望大家可以对二叉树进行一定程度了解。


数据结构——树与二叉树_Massachusetts_11的博客-CSDN博客

1. 二叉树的顺序结构 普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。


而完全二叉树更适合使用顺序结构存储。


现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和 *** 作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是 *** 作系统中管理内存的一块区域分段。


2.堆的概念及结构

以上是堆的定义,看不懂不重要,我们接下来慢慢解释:

1.首先堆必须时一棵完全二叉树

2.堆一般分为两种:

  • 小根堆:任意一个节点的两个子节点都比自己的值大或相等,也就是根最小,整个二叉树从上到下递增。


  • 大根堆: 任意一个节点的两个子节点都比自己的值小或相等,也就是根最大,整个二叉树从上到下递减。


转换原理:

那么问题来了,这里的存储结构是如何向逻辑结构去实现的呢?

不知大家是否还记得树与二叉树一文中二叉树性质的这几条

也就是说:对于任意一个节点,我们设它的下标为i,则它的左孩子节点下标就是2*i +1 ,右孩子就是2*i + 2,父节点下标就是(i - 1) / 2。


(可能大家也发现了,这里的父节点如果用两个子节点去倒推应该有两个不同的结果,但我们所求的下标必须是一个整型,由于c语言向零取整的原则,两个表达式可合成一个  (i - 1) / 2    )

我们不妨来验证一下:

 这里元素28的下标是4,它的父节点下标计算后是(4-1)/ 2 = 1,恰好是18元素

它的左子节点2*4+1 = 9,恰好是37元素,它的右子节点2*4+2 = 10,恰好是10元素。


有了以上这些基础,我们也可以进行一下堆的实现了。


3. 堆的实现

与顺序表相同,我们同样也实现一下堆的增删查改

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	size_t size;
	size_t capacity;
}HP;
//初始化堆
void HeapInit(HP* php);
//销毁堆
void HeapDestroy(HP* php);
//打印堆
void HeapPrint(HP* php);
//尾插元素
void HeapPush(HP* php, HPDataType x);
//头删元素
void HeapPop(HP* php);
//判断是否为空
bool HeapEmpty(HP* php);
//计算节点个数
size_t HeapSize(HP* php);
//返回堆头节点
HPDataType HeapTop(HP* php);
3.1初始化

既然要实现,我们就需要从物理存储角度去思考。


与动态顺序表相同,我们为堆设置一个数组,同时为它附加两个记录:元素个数和容量大小(方便扩容)

在没有元素之前,数组置空,元素个数和容量都为0,后续在尾插元素时,再对堆进行判断,进行扩容

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

对静态开辟的数组进行释放;

元素个数和容量都置零;

void HeapDestroy(HP* php)
{
	assert(php);
	assert(php->a);
	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;
}
3.2打印

可以选择打印出二叉树的外形,但太复杂了🥶,我们这里仅仅遍历打印了数组,如果各位大神有实现树状打印的可以在讨论区分享一下哈哈哈哈哈

void HeapPrint(HP* php)
{
	assert(php);
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}
3.3尾插

我们的重头戏终于来了。


这里我们尾插后一定要保证它依旧还是大堆或者小堆,所以我们要先实现一下向上调整的算法,用来保证我们每次插入的元素都能向上插入自己该去的地方。


向上调整算法

以下讲解都以小堆为例,首先上个图免得想不到

 

 这里,我们想把元素10向上调整,在看这个具体案例之前,我们先了解一下原则:

让当前节点与父节点进行比较,若小于父节点,则交换两节点,接下来判断父节点与父节点的父节点,以此类推,直至出现当前节点不大于父节点。


我们来看一下它的可行性:

每次交换后都能保证父节点为首的这棵子树是堆,如这一次交换就保证了下面这个红圈是个堆

当下一次交换:10位置与18位置交换,说明原来10位置的数比18位置的小,也一定比25位置的小,也就是左右都比10大,这时又保证了大三角是个堆

以此类推,直至大三角框住整棵树,此时整棵树就成堆了。


看一下代码:

这里这个交换可以写个函数,方便表示,以后也能用:

void Swap(HPDataType* pa, HPDataType* pb)
{
	assert(pa && pb);
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

向上调整: 

void AdjustUp(HPDataType* a, size_t child)
{
	assert(a);
	size_t 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)
{
	assert(php);
	if (php->size == php->capacity)
	{
		size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			printf("realloc failed\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newCapacity;
	}
	php->a[php->size] = x;
	AdjustUp(php->a, php->size);
	php->size++;
}

3.4头删

首先,我们想一下,可以把头元素删掉,后面的元素向前移动吗?

答案肯定是不行的,且不说这个O(N)的算法有点蠢,当我们把所有元素往前挪,这时节点之间的父子关系都被破坏了,十有八九它就不是个堆了,那我们就看看这种方法:

1.第一个数(根位置)和最后一个数进行交换。


2.删除最后一个数(size--就行)

3.把头顶的那个大数字再用一定的方式往下调,调成一个堆。


这时我们就需要一个下调整的算法。


向下调整算法

同样摆个图先看着,这里我们要把28向下调.

向下调整原则:

1.找出左右孩子中小的那个;

2.跟父亲比较,如果父亲小,交换;

3.从交换的孩子位置继续往下调。


可行性分析:

其实向下调整就是为了保证头插的元素通过向下调可以让这个二叉树又形成堆,

调整过程中,挑个小的换,就能保证红圈以外是个堆

再换一次,红圈继续缩:

直到最后没有子节点,也就是圈缩成是一个元素,这时就成堆了

向下调整代码:

void AdjustDown(HPDataType*a,size_t size,size_t root)
{
	assert(a);
	size_t parent = root;
	size_t child = parent * 2 + 1;
	while (child < size)
	{
		if (child+1

头删代码;

void HeapPop(HP* php)
{
	assert(php);
	Swap(php->a, &php->a[php->size - 1]);
	php->size--;
	AdjustDown(php->a, php->size, 0);
}
3.5剩余 

 剩余函数较简单,相信大家一看就明白了

bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}
size_t HeapSize(HP* php)
{
	assert(php);
	return php->size;
}
HPDataType HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}

4.堆——总代码 4.1 头文件:Heap.h

注:这里默认是小堆,如果对头文件BIGHEAP的宏进行解引用可实现大堆

#pragma once
#include
#include
#include
#include
#include
//#define BIGHEAP//若大堆,则解除注释,否则加注释
#define SIGN <
#ifdef BIGHEAP
#undef SIGN
#define SIGN >
#endif

// 小堆
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	size_t size;
	size_t capacity;
}HP;

void Swap(HPDataType* pa, HPDataType* pb);
void HeapInit(HP* php);
void HeapDestroy(HP* php);
void HeapPrint(HP* php);

// 插入x以后,保持他依旧是(大/小)堆
void HeapPush(HP* php, HPDataType x);

// 删除堆顶的数据。


(最小/最大) void HeapPop(HP* php); bool HeapEmpty(HP* php); size_t HeapSize(HP* php); HPDataType HeapTop(HP* php);

4.2源文件:Heap.c
#include"Heap.h"

void Swap(HPDataType* pa, HPDataType* pb)
{
	assert(pa && pb);
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
void HeapInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}
void HeapDestroy(HP* php)
{
	assert(php);
	assert(php->a);
	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;
}
void HeapPrint(HP* php)
{
	assert(php);
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}
void AdjustUp(HPDataType* a, size_t child)
{
	assert(a);
	size_t parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] SIGN a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}

	}
}
void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			printf("realloc failed\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newCapacity;
	}
	php->a[php->size] = x;
	AdjustUp(php->a, php->size);
	php->size++;
}
void AdjustDown(HPDataType*a,size_t size,size_t root)
{
	assert(a);
	size_t parent = root;
	size_t child = parent * 2 + 1;
	while (child < size)
	{
		if (child+1a, &php->a[php->size - 1]);
	php->size--;
	AdjustDown(php->a, php->size, 0);
}

bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}
size_t HeapSize(HP* php)
{
	assert(php);
	return php->size;
}
HPDataType HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}
4.3测试文件:test.c
#include"Heap.h"
int main()
{
	HP h;
	HeapInit(&h);
	HeapPush(&h, 2);
	HeapPush(&h, 5);
	HeapPush(&h, 7);
	HeapPush(&h, 1);
	HeapPush(&h, 4);
	HeapPrint(&h);
	HeapPop(&h);
	HeapPrint(&h);
	printf("%d", HeapTop(&h));
}
4.4测试结果

5. 堆的应用 5.1堆排序

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

        1.建堆

        2.利用堆删除的思想进行排序

5.1.1基础版本

我们先上个简单的版本,用刚刚实现的堆来完成堆排序。


整体思路为:

        1.首先新建一个堆,遍历整个数组,将所有元素Push到堆中。


        2.此时如果是小堆,那堆首元素一定最小,我们就将这个元素存入数组,然后把这个元素从堆中头删掉。


        3.循环第二个动作,直至将堆清空。


代码:

void HeapSort(int* a,size_t size)
{
	HP hp;
	HeapInit(&hp);
	//建堆
	for (int i = 0; i < size; i++)
	{
		HeapPush(&hp, a[i]);
	}
	//堆删除
	for (int i = 0; i < size; i++)
	{
		a[i] = HeapTop(&hp);
		HeapPop(&hp);
	}
	HeapDestroy(&hp);
}

我们看一它的时间复杂度:

循环遍历是O(N);

Push *** 作最坏情况走的是树的高度步,假设每次都是n个节点,则高度就是logN,也就是O(logN);

整个建堆过程就是O(N*logN);

堆删除与建堆基本形同,也是O(N*logN);

整个堆排序的时间复杂度也就是O(N*logN)。


再来看一下空间复杂度:
我们额外建堆,就需要一个与数组大小相等的数组,空间复杂度也就是O(N);

时间复杂度是O(N*logN),对于一个堆排序来说就是标准,但空间复杂度的O(N)完全没有必要,我们是否可以在原数组上进行排序呢?而且排个序还要写个堆,未免有些小蠢。


那么我们来看看下面的升级版。


5.1.2升级版

原则与上面基本相同,还是建堆、删堆,只不过我们现在要脱离建堆删掉等封装好的函数,直接用算法在数组上原地 *** 作。


建堆:

这时,然我们一起回忆一下向上调整,和向下调整这两个神奇的算法。


向上调整:从堆尾插入一个元素,只要这个元素经历过一次向上调整算法,他就能找到自己该去的位置,从而有形成一个堆。


向下调整:当堆顶节点的左右子树都是堆,但政整棵二叉树不是堆,这时通过对堆顶元素向下调整就可以形成一个堆。


有了这两个前提,我们再看看怎么原地建堆:

1.向上调整建堆:

  • 对数组的第二个元素进行向上调整,此时最前面的两个元素就形成了一个堆。


  • 然后对第三个元素进行向上调整,这时前三个元素又形成一个堆,
  • 再进行第3,4,5个元素向上调整,直至调整完尾元素,此时就原地建好了一个堆。


2.向下调整建堆:

相对上面有点难想,上个图先:

 这里我们从下面开始,但不是最后一个元素(下面没东西也没法往下调),我们从元素8开始进行向下调,调整之后,这部分就成了堆:

然后再是对7向下调:

以此逻辑直至整棵树成堆:

3.时间复杂度比较:

 大家可能觉得这两种方式建堆

 

 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/577877.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-11
下一篇 2022-04-11

发表评论

登录后才能评论

评论列表(0条)