二叉树的存储结构(顺序存储)—— 数据结构与算法

在这里插入图片描述

‍️Take your time ! ‍️
个人主页:大魔王
代码仓库:魔王修炼之路
所属专栏:魔王的修炼之路–数据结构
如果你觉得这篇文章对你有帮助,请在文章结尾处留下你的点赞关注,支持一下博主。同时记得收藏✨这篇文章,方便以后重新阅读。


文章目录

  • 一、前言
  • 二、二叉树的顺序存储结构
    • 1、二叉树采取顺序存储的原因
    • 2、堆的概念及结构
  • 三、堆的实现
    • 1、介绍
    • 2、向上调整建堆
      • 说明
      • 实现
    • 3、向下调整建堆
      • 说明
      • 实现
    • 4、向上向下调整时间复杂度
    • 5、堆的插入
    • 6、堆的删除
    • 7、总代码
      • Heap.h
      • Heap.c
      • Test.c
  • 四、堆的应用
    • 堆排序
      • 介绍
      • 实现
    • TOP-K问题
  • 五、总结

一、前言

上一篇博客详细讲述了二叉树的概念,那么接下来这篇博客将讲述二叉树的存储方式的一种:顺序存储。

二、二叉树的顺序存储结构

1、二叉树采取顺序存储的原因

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

2、堆的概念及结构

  • 堆可以这么理解:它是一个完全二叉树,并且完全二叉树都可以转变为堆,为什么说堆是特殊的二叉树呢?因为堆其实就是把这个完全二叉树变得有规律了。

堆分为大根堆和小根堆。
大根堆:所有父节点都不小于子节点。
小根堆:所有父节点都不大于子节点。
需要注意的是:兄弟节点之间没有什么联系,可以大于可以小于也可以相等。

二叉树的存储结构(顺序存储)—— 数据结构与算法_第2张图片

三、堆的实现

1、介绍

实现堆的方法:有两种,分别为向上调整建堆和向下调整建堆。

  • 需要注意的是,无论是向上调整还是向下调整,我们都需要保证的前提是除了要调整的这个元素外,其余节点本身就是一个堆(大根堆或小根堆),也就是说是在原本堆的基础上进行的向下和向上调整。

2、向上调整建堆

说明

向上调整建堆:就像刚才说的,我们进行调整时其本身必须为一个堆,因此我们只能一点一点的调整,所以从第二个开始向上调整(因为第一个没有比较的东西,所以从第二个开始调整),使每次比较都能是在原本就是堆的基础上进行。如图:从红色位置开始向上调整建堆。
二叉树的存储结构(顺序存储)—— 数据结构与算法_第3张图片

实现

  • 建大堆
    二叉树的存储结构(顺序存储)—— 数据结构与算法_第4张图片
//向上调整建大堆
#include 

void AdjustUp(int* a,int n)
{
	int child = n;
	int parent = (child - 1) / 2;
	while (child > 0)
	//判断条件不能为parent,因为parent为整型,但是最后那个结果是-0.5,对于整型来说,浮点数小数点后的忽略,那么此时parent还是0,child也变成0了,它们的值会相等然后return,虽然这样程序照样过了,但是这种想法其实是错的,所以我们通过判断child是否>0来作为是否结束的标志。
	{
		if (a[child] > a[parent])
		{
			int temp = a[child];
			a[child] = a[parent];
			a[parent] = temp;
		}
		else
			return;//如果子节点小于父亲节点那么本次循环就不用比了,因为再向上比的那些之前已经调整过了。
		child = parent;
		parent = (parent - 1) / 2;
	}
}

int main()
{
	int a[7] = { 2,5,6,7,4,9,3 };
	int size = sizeof(a) / sizeof(int);
	int n = 1;
	while (n < size)
	{
		AdjustUp(a, n);
		n++;
	}
	for (int n = 0; n < size; n++)
	{
		printf("%d ", a[n]);
	}
	return 0;
}

3、向下调整建堆

说明

向下调整建堆:和向上调整很像,也必须一点一点的建,只不过是从下面开始,我们跳过最后一层,层倒数第二层开始逐个向下调整(确保除了要调整的这个,其余参与的节点本身构成一个堆)。如图:
二叉树的存储结构(顺序存储)—— 数据结构与算法_第5张图片

实现

  • 大根堆

二叉树的存储结构(顺序存储)—— 数据结构与算法_第6张图片

//向下调整建大堆
#include 
void AdjustDown(int* a, int n,int size)
{
	int parent = n;
	int child = n * 2 + 1;//先假设左子树是孩子中较大的,然后再进行判断,看是否替换为右子树(右子树需要存在)。
	while (child < size)
	{
		if (child + 1 < size && a[child] < a[child + 1])//第一个判断的原因是首先右子树要存在才能比较。
		{
			child = child + 1;
		}

		if (a[parent] < a[child])
		{
			int temp = a[parent];
			a[parent] = a[child];
			a[child] = temp;
		}
		parent = child;
		child = child * 2 + 1;
	}
}
int main()
{
	int a[7] = { 2,5,6,7,4,9,3};
	int size = sizeof(a) / sizeof(int);
	int n = (size - 2) / 2;
	while (n >= 0)
	{
		AdjustDown(a, n,size);
		n--;
	}
	n = 0;
	while (n < size)
	{
		printf("%d ", a[n]);
		n++;
	}
	return 0;
}
  • 就像上面两幅图,虽然相同的数采用向上和向下调整建堆的方法建出来的堆不太一样,但是都是大根堆。

4、向上向下调整时间复杂度

  • 向上调整建堆时间复杂度:O(N*logN) --向上调整建堆时层的结点个数少时向上的层数少(也就相当于每个遍历的次数少),层的结点个数多时向上的层数多(每个节点遍历的次数多),因此时间复杂度大。
  • 向下调整建堆时间复杂度:O(N) --向下调整建堆时层的结点个数多时向下的层数少,层的结点个数少时向下的层数多,所以会比向上调整效率高。
  • 向下调整时间复杂度证明
    二叉树的存储结构(顺序存储)—— 数据结构与算法_第7张图片

向上调整与这个证明过程类似。

5、堆的插入

先将这个元素插入到尾部,然后让这个数据进行向上调整就行了。

6、堆的删除

让根与最后一个元素交换位置,然后让数组删除最后一个元素(size–),然后让这个取代根位置的元素进行向下调整即可。

7、总代码

Heap.h

#pragma once

typedef int HeapDateType;

#include 
#include 
#include 

typedef struct Heap
{
	HeapDateType* a;
	int size;
	int capacity;
}Heap;

//堆的初始化
void HeapCreat(Heap* php);

//堆的销毁
void HeapDestory(Heap* php);

//堆的插入
void HeapPush(Heap* php, HeapDateType x);

//堆的删除
void HeapPop(Heap* php);

//取堆顶的数据
HeapDateType HeapTop(Heap* php);

//堆的数据个数
int HeapSize(Heap* php);

//堆的判空
int HeapEmpty(Heap* php);

Heap.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "Heap.h"


//堆的初始化
void HeapInit(Heap* php)
{
	assert(php);
	php->a = (HeapDateType*)malloc(sizeof(HeapDateType) * 4);
	if (php->a == NULL)
	{
		perror("malloc error");
		assert(php);
	}
	php->size = 0;
	php->capacity = 4;
}

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

//数据交换
void Swap(HeapDateType* a, HeapDateType* b)
{
	int temp = 0;
	temp = *b;
	*b = *a;
	*a = temp;
}

void AdjustUp(HeapDateType* a, int n)
{
	assert(a);
	int child = n - 1;
	int parent = (child - 1) / 2;
	while (child > 0)
    //判断条件不能为parent,因为parent为整型,但是最后那个结果是-0.5,对于整型来说,浮点数小数点后的忽略,那么此时parent还是0,child也变成0了,它们的值会相等然后return,虽然这样程序照样过了,但是这种想法其实是错的,所以我们通过判断child是否>0来作为是否结束的标志。
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);//后面还会用到交换,所以封装成一个函数。
		}
		else
			return;
		child = parent;
		parent = (parent - 1) / 2;
	}
}

//堆的插入
void HeapPush(Heap* php,HeapDateType x)
{
	assert(php);
	if (php->capacity == php->size)
	{
		HeapDateType* inspect = realloc(php->a, sizeof(HeapDateType) * php->capacity * 2);
		if (inspect == NULL)
		{
			perror("realloc error");
			assert(inspect);
		}
		php->a = inspect;
		inspect = NULL;
		php->capacity *= 2;
	}

	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size);
}

//向上向下调整的前提是必须调整前是一个堆(大堆或者小堆)
void AdjustDown(HeapDateType* a, int n,int parent)
{
	assert(a);
	int child = parent*2+1;//默认向下调整是跟左孩子交换,如果下面比较左右孩子大小时右孩子大,那么让child+1即可表示与右孩子交换。
	while (child <= n)
	{
		if (child < n&& (a[child] < a[child + 1]))//前提是要有右兄弟,不然就会越界
		{
			child = child + 1;
		}
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
		}
		else
			return;
		parent = child;
		child = child * 2 + 1;
	}
}

//堆的删除(删除的是堆顶,也就是根)
void HeapPop(Heap* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	Swap(&php->a[php->size - 1], & php->a[0]);//直接让最后一个覆盖根也行,因为根是要删掉的,就算这样交换了,下面size--后,根还是没有了。
	php->size--;
	AdjustDown(php->a, php->size,0);
}

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

//堆的数据个数
int HeapSize(Heap* php)
{
	assert(php);
	return php->size;

}

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

Test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "Heap.h"


int main()
{
	Heap hp;
	HeapInit(&hp);

	HeapPush(&hp, 0);
	HeapPush(&hp, 47);
	HeapPush(&hp, 26);
	HeapPush(&hp, 48);
	HeapPush(&hp, 52);
	HeapPush(&hp, 79);
	HeapPush(&hp, 6);
	HeapPush(&hp, 83);
	HeapPush(&hp, 38);
	HeapPush(&hp, 24);

	HeapPop(&hp);
	HeapPop(&hp);
	HeapPop(&hp);
	HeapPop(&hp);

	return 0;
}

四、堆的应用

堆排序

介绍

  • 虽然我们知道堆分为大根堆和小根堆,子节点和父节点有明确的大小关系,但因为兄弟节点的大小关系不确定,随意实际在数组中并不是顺序存放的,我们利用堆里删除元素的思想对堆进行排序。
  • 规律:升序建大堆,降序建小堆!
  • 原因如下:

对于升序建大堆来说:我们采用的方法是每次将根与最后一个元素交换位置,然后让这个堆忽略掉末尾元素(size–)(也就是原来的根,因为它已经到了应该的位置了),然后一直这样,直到这个堆只剩下一个元素,那么此时就排好了。
二叉树的存储结构(顺序存储)—— 数据结构与算法_第8张图片

  • 注意:堆排序首先是要建堆

实现

//堆排序实现
#include 
#include 
#include 

//数据交换
void Swap(int* a, int* b)
{
	int temp = 0;
	temp = *b;
	*b = *a;
	*a = temp;
}

void AdjustUp(int* a, int n)
{
	assert(a);
	int child = n - 1;
	int parent = (child - 1) / 2;
	while (parent >= 0)
		//判断条件不能为parent,因为parent为整型,但是最后那个结果是-0.5,对于整型来说,浮点数小数点后的忽略,那么此时parent还是0,child也变成0了,它们的值会相等然后return,虽然这样程序照样过了,但是这种想法其实是错的,所以我们通过判断child是否>0来作为是否结束的标志。
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);//后面还会用到交换,所以封装成一个函数。
		}
		else
			return;
		child = parent;
		parent = (parent - 1) / 2;
	}
}

//向上向下调整的前提是必须调整前是一个堆(大堆或者小堆)
void AdjustDown(int* a, int n, int parent)
{
	assert(a);
	int child = parent * 2 + 1;//默认向下调整是跟左孩子交换,如果下面比较左右孩子大小时右孩子大,那么让child+1即可表示与右孩子交换。
	while (child < n)
	{
		if (child < n - 1 && (a[child] < a[child + 1]))//前提是要有右兄弟,不然就会越界
		{
			child = child + 1;
		}
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
		}
		else
			return;
		parent = child;
		child = child * 2 + 1;
	}
}

void HeapSort(int* arr, int size)//假设要求升序排列
{
	向上调整建大堆--时间复杂度为N*logN
	//for (int n = 2; n < size; n++)
	//{
	//	AdjustUp(arr, n);
	//}
	//向下调整建堆--时间复杂度为N
	for (int n = (size - 2) / 2; n >= 0; n--)//size-1是指最后一个元素的下标,另一个-1是公式套的
	{
		AdjustDown(arr, size, n);
	}
	//排序--第一个和最后一个替换,然后让新根向下调整即可,这样就让该堆中最大的数放到了该放的位置。
	while (size--)
	{
		Swap(&arr[0], &arr[size]);
		AdjustDown(arr, size, 0);
	}
}

int main()
{
	int arr[] = { 45,26,91,34,82,41,67,81,47,35 };
	HeapSort(arr, sizeof(arr) / sizeof(int));
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

TOP-K问题

  • TOP-K问题:即当求数据结合中前K个最大或者最小元素,一般情况下数据量会很大。比如:世界500强,富豪榜等等。你可能会想,直接排序不就行了,但是如果当数据量特别大时,整个内存都放不下,这时候最佳方法就是使用堆排序。

求最大的前k个:建小堆。
求最小的前k个,建大堆。

  • 解释:因为TOP-K一般是求前k个最值数,所以我们需要将其找出来,但是当数据特别多时,内存放不下,所以就不能使用堆排序(内存放不下),因此我们用另外的方法:假设求最大的前K个,我们先建一个k个数的小堆,然后我们将剩下的数放进文件(磁盘)里,然后分别与小堆的根(最小的数比较),如果大于这个根,那么直接覆盖上去,并让新根进行向下调整,使得覆盖后仍然是一个小堆。
  • 注意:不能与我们建的堆的最后一个元素比,因为它不一定是最大或最小,只有根我们才能确定它一定是一个最值。

五、总结

  • 升序建大堆,降序建小堆原因:假如升序建小堆,那么第一个当然就是我们要的最小的数,但是之后就不行了,因为虽然这个位置是最小的不用动了,但是当我们找第二小的时,我们就要忽略掉第一个已经定了的这个,那么这次谁当根呢,直接父子兄弟什么的全乱了,我们只能再次通过(N-1)个数进行一次堆排序建立一个新堆然后新堆的根就是第二小的,但是这样就太麻烦了,因为建一次堆的时间复杂度就是O(N*logN)(向上调整建堆)或者O(N)(向下调整建堆)而如果建成相反的(大堆),然后根和最后一个元素换一下再向下调整,这样就能确定最大的数,一次时间复杂度是logN。
  • 关于堆(顺序存储)的所有操作注意:就是尽量不破坏原本堆的结构,不让父子乱辈,不然就可能乱了很多,处理起来就很麻烦,只能重新建堆。
  • TOP-K问题总结:求最大的建小堆,求最小的建大堆。

  • 博主长期更新,博主的目标是不断提升阅读体验和内容质量,如果你喜欢博主的文章,请点个赞或者关注博主支持一波,我会更加努力的为你呈现精彩的内容。

专栏推荐
魔王的修炼之路–C语言
魔王的修炼之路–数据结构初阶
魔王的修炼之路–C++
魔王的修炼之路–Linux
更新不易,希望得到友友的三连支持一波。收藏这篇文章,意味着你将永久拥有它,无论何时何地,都可以立即找到重新阅读;关注博主,意味着无论何时何地,博主将永久和你一起学习进步,为你带来有价值的内容。

你可能感兴趣的:(c语言,链表,算法,数据结构)