学习笔记—数据结构—二叉树顺序实现(小堆)

目录

前言:

树

概念

相关术语

表示

二叉树

概念

特点

满二叉树

定义

性质

完全二叉树

定义

性质

二叉树的存储方式

顺序结构

链式结构

实现顺序结构二叉树

堆

定义

分类

性质

二叉树的实现(顺序表,小根堆)

结构

初始化

销毁

插入

※AdjustUp(向上调整)

删除

※AdjustDown(向下调整)

取顶元素

是否为空

总结

头文件Heap.h

Heap.c

测试文件test.c


前言:

今天的所介绍的二叉树是一种非线性数据结构

树

概念

树是⼀种非线性的数据结构,它是由 n ( n>=0 ) 个有限结点组成⼀个具有层次关系的集合。

●有⼀个特殊的结点,称为根结点,根结点没有前驱结点。

● 除根结点外,其余结点被分成 M(M>0) 个互不相交的集合 T1 、 T2 、 ...... 、 Tm ,其中每⼀个集合 Ti(1 <= i <= m) ⼜是⼀棵结构与树类似的⼦树。每棵⼦树的根结点有且只有⼀个前驱,可以 有 0 个或多个后继。因此,树是递归定义的。

非树行结构

1.子树是不相交的

2.除了根结点外,每个结点有且仅有⼀个⽗结点

3.⼀棵N个结点的树有N-1条边

相关术语

父结点/双亲结点:若⼀个结点含有⼦结点,则这个结点称为其⼦结点的父结点;如上图:A是B的父 结点

子结点/孩⼦结点:⼀个结点含有的⼦树的根结点称为该结点的子结点;如上图:B是A的孩⼦结点

结点的度:⼀个结点有几个孩⼦,他的度就是多少;比如A的度为6,F的度为2,K的度为0 树的度:⼀棵树中,最大的结点的度称为树的度;如上图:树的度为 6 叶

子结点/终端结点:度为 0 的结点称为叶结点;如上图: B 、 C 、 H 、 I... 等结点为叶结点

分⽀结点/⾮终端结点:度不为 0 的结点;如上图: D 、 E 、 F 、 G... 等结点为分⽀结点

兄弟结点:具有相同父结点的结点互称为兄弟结点(亲兄弟);如上图: 比特就业课 B 、 C 是兄弟结点

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

树的⾼度或深度:树中结点的最⼤层次;如上图:树的高度为 4

结点的祖先:从根到该结点所经分⽀上的所有结点;如上图: A 是所有结点的祖先

路径:⼀条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列;⽐如A到Q的路径为: A-E-J-Q;H到Q的路径H-D-A-E-J-Q

子孙:以某结点为根的⼦树中任⼀结点都称为该结点的⼦孙。如上图:所有结点都是A的⼦孙

森林:由 m ( m>0 ) 棵互不相交的树的集合称为森林;

表示

孩子兄弟表示法:

复制代码
struct TreeNode
{
	struct Node* child;   // 左边开始的第⼀个孩⼦结点

	struct Node* brother; // 指向其右边的下⼀个兄弟结点

	int data;             //节点中的数据域
};

二叉树

概念

在树形结构中,我们最常用的就是二叉树,是树的一种特殊形式,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。

特点

⼆叉树不存在度大于 2 的结点

⼆叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

满二叉树

定义

1.满二叉树是一种特殊的二叉树,它的特点是每一层的节点数都达到最大值,即每一层的节点数都恰好为2的幂次(除了最后一层以外)。

2.如果满二叉树的深度为k,那么它的节点总数为2^k - 1。在满二叉树中,除了最后一层的节点之外,其余每一层的节点都是完全填满的,而且最后一层的节点也是从左到右连续排列的

性质
  1. 满二叉树的深度为k,节点总数为2^k - 1。
  2. 满二叉树的每一层的节点数都达到最大值,即每一层的节点数为2^(i-1),其中i是层数。
  3. 满二叉树的叶子节点全部位于最后一层。
  4. 满二叉树的节点要么是叶子节点(度为0),要么是度为2的节点,不存在度为1的节点

完全二叉树

定义
  1. 除了最后一层外,其他每一层的节点数都达到最大数量,即该层的节点数等于2的幂减去1。
  2. 最后一层的节点都集中在最左边,并且右边的节点可以少,但不能有空位。
性质

1.完全二叉树的节点编号从1开始,对于编号为i的节点,其左子节点的编号为2i,右子节点的编号为2i+1。

2.如果一棵完全二叉树有n个节点,那么它的深度大约为log2(n)+1。

3.完全二叉树的特点是叶子节点只能出现在最下层和次下层,且最下层的叶子节点集中在树的左部。

二叉树的存储方式

二叉树⼀般可以使用两种结构存储,⼀种顺序结构,⼀种链式结构。

顺序结构

顺序结构存储就是使⽤数组来存储,⼀般使⽤数组只适合表⽰完全⼆叉树,因为不是完全⼆叉树会有 空间的浪费,完全⼆叉树更适合使⽤顺序结构存储。

链式结构

⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。 通常的⽅法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址 。链式结构⼜分为⼆叉链和三叉链,当前我们学习中⼀般都是⼆叉链。

实现顺序结构二叉树

堆

定义

堆是一种特殊的完全二叉树,它可以被视为一种特殊的顺序表。

分类

堆的特点是父节点的值总是大于或小于其子节点的值,根据这个特点,堆可以分为大根堆和小根堆。在大根堆中,根节点的值是最大的,而在小根堆中,根节点的值是最小的。

性质

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

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

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

若 2i+2<n,右孩子序号2i+2,2i+2>=n否则无右孩子

二叉树的实现(顺序表,小根堆)

结构

复制代码
//定义堆的结构----数组
typedef int HPDataType;

typedef struct Heap
{
	HPDataType* arr;
	int size;       //有效的数据个数
	int capaciti;   //空间大小
}HP;

初始化

复制代码
//初始化
void HPInit(HP* php)
{
	assert(php);
	php->arr = NULL;
	php->capaciti = php->size = 0;
}

销毁

复制代码
//销毁
void HPDestroy(HP* php)
{
	assert(php);
	if (php->arr)
	{
		free(php->arr);
	}

	php->arr = NULL;
	php->capaciti = php->size = 0;
}

插入

数据从尾部插入,但是要保证小堆结构,我们调用函数 AdjustUp(向上调整)

复制代码
//插入
void HPPush(HP* php, HPDataType x)
{
	assert(php);
	//扩容
	if (php->size == php->capaciti)
	{
		int newcapacity = php->capaciti == 0 ? 4 : 2 * php->capaciti;
		HPDataType* tmp = ((HPDataType*)realloc(php->arr, newcapacity * sizeof(HPDataType)));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		php->arr = tmp;
		php->capaciti = newcapacity;
	}
	php->arr[php->size] = x;
	AdjustUP(php->arr, php->size);
	php->size++;
}
※AdjustUp(向上调整)

我们向堆中插入一个数据6,但是插入之后就不是小堆了

所以我们要进行调整了

我们通过(i-1)/2能够找到刚刚插入的6的父节点56

找到之后我们进行两个数据的比较,谁小谁就进行向上调整

那么我们就将6和56交换位置

然后6就从i=6的位置换到i=2的位置,

我们用6找到父节点10

进行两个数据的比较,那么我们就将6和10交换位置

那么这个就是堆的向上调整算法

复制代码
//堆的向上调整
void AdjustUP(HPDataType* arr, int child)
{
	//建立用来存储父结点的函数
	int parent = (child - 1) / 2;
	//当child结点走到根节点时,就不用交换了
	while (child > 0)
	{
		//当子结点大于父结点时
		if (arr[child] > arr[parent])
		{
			//父子结点的值互换
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		//子结点大于等于父结点时
		else
		{
			//直接跳出
			break;
		}
	}
}

Swap(交换)

复制代码
void Swap(int* x, int* y)
{
	int a = *x;
	*x = *y;
	*y = a;
}

删除

在这展示的是首节点的删除,尾节点删除的意义不大,由于尾结点互换到了头结点,需要进行AdjustDown(向下调整)

复制代码
//删除
void HPPop(HP* php)
{
	assert(php && php->size);

	//arr[0] arr[size-1]
	Swap(php->arr[0], php->arr[php->size - 1]);
	--php->size;

	AdjustDown(php->arr, 0, php->size);
}
※AdjustDown(向下调整)

出的是堆顶的数据,就是下标为0的数据,但是不好删除

但是假如我们删除的是最后一个数据的话就很好解决

直接size--

将70和10的位置进行换一下,然后size--得到了

那么现在我们只需要将这个堆变成小堆就行了

现在的70是父节点,我们通过2i+1得到左边的子节点,比较左右的两个子节点,挑选出小的,

然后将小的子节点与父节点进行交换

对于25和30来说的话,现在的70是父节点,因为我们的小堆需要父节点小于子节点

那么我们需要挑选出小的子节点和父节点进行交换

那么最后我们就得到了小堆

复制代码
//堆的向下调整
void AdjustDown(HPDataType* arr, int parent, int n)
{
	//左孩子
	int child = parent * 2 + 1;
	//不能child++越界
	while (child < n)
	{
		//左右孩子比较找出小的
		if (child + 1 < n && arr[child] > arr[child + 1])
		{
			child++;
		}
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

取顶元素

复制代码
HPDataType HPTop(HP* php)
{
	assert(php && php->size);

	return php->arr[0];
}

是否为空

复制代码
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

总结

头文件Heap.h

复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

//定义堆的结构----数组
typedef int HPDataType;

typedef struct Heap
{
	HPDataType* arr;
	int size;       //有效的数据个数
	int capaciti;   //空间大小
}HP;

void HPInit(HP* php);                  //初始化
void HPDestroy(HP* php);               //销毁

void HPPush(HP* php, HPDataType x);    //入堆
void HPPop(HP* php);                   //出堆
HPDataType HPTop(HP* php);             //取堆顶数据
bool HPEmpty(HP* php);                 //判空

Heap.c

复制代码
#include"Heap.h"

//初始化
void HPInit(HP* php)
{
	assert(php);
	php->arr = NULL;
	php->capaciti = php->size = 0;
}

//销毁
void HPDestroy(HP* php)
{
	assert(php);
	if (php->arr)
	{
		free(php->arr);
	}

	php->arr = NULL;
	php->capaciti = php->size = 0;
}

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

//堆的向上调整
void AdjustUP(HPDataType* arr, int child)
{
	//建立用来存储父结点的函数
	int parent = (child - 1) / 2;
	//当child结点走到根节点时,就不用交换了
	while (child > 0)
	{
		//当子结点大于父结点时
		if (arr[child] > arr[parent])
		{
			//父子结点的值互换
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		//子结点大于等于父结点时
		else
		{
			//直接跳出
			break;
		}
	}
}

//插入
void HPPush(HP* php, HPDataType x)
{
	assert(php);
	//扩容
	if (php->size == php->capaciti)
	{
		int newcapacity = php->capaciti == 0 ? 4 : 2 * php->capaciti;
		HPDataType* tmp = ((HPDataType*)realloc(php->arr, newcapacity * sizeof(HPDataType)));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		php->arr = tmp;
		php->capaciti = newcapacity;
	}
	php->arr[php->size] = x;
	AdjustUP(php->arr, php->size);
	php->size++;
}

//堆的向下调整
void AdjustDown(HPDataType* arr, int parent, int n)
{
	//左孩子
	int child = parent * 2 + 1;
	//不能child++越界
	while (child < n)
	{
		//左右孩子比较找出小的
		if (child + 1 < n && arr[child] > arr[child + 1])
		{
			child++;
		}
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//删除
void HPPop(HP* php)
{
	assert(php && php->size);

	//arr[0] arr[size-1]
	Swap(php->arr[0], php->arr[php->size - 1]);
	--php->size;

	AdjustDown(php->arr, 0, php->size);
}

HPDataType HPTop(HP* php)
{
	assert(php && php->size);

	return php->arr[0];
}

bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

测试文件test.c

复制代码
#include"Heap.h"

void test01()
{
	HP hp;
	HPInit(&hp);
	int arr[] = { 15, 18, 27, 36, 42, 12 };
	for (int i = 0; i < 6; i++)
	{
		HPPush(&hp, arr[i]);
	}

	//HPPop(&hp);
	int i = 0;
	while (!HPEmpty(&hp))
	{
		arr[i++] = HPTop(&hp);
		HPPop(&hp);
	}
	for (int i = 0; i < 6; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	
	HPDestroy(&hp);
}



int main()
{
	test01();
	return 0;
}
相关推荐
yi0116 分钟前
LeetCode 643. 子数组最大平均数 I|从暴力枚举到滑动窗口
人工智能·笔记·python·算法·leetcode·滑动窗口
罗西的思考20 分钟前
[Agent Memory / 强化学习] MemPO源码学习笔记 — (6)— 奖励机制
人工智能·笔记·学习
无忧.芙桃28 分钟前
C++语言原理与实践(十二):list类的底层实现
c++·windows·list
沫璃染墨31 分钟前
《Linux工程实践篇(二):线程池实战——从线程安全单例到完整源码实现》
linux·开发语言·c++·单例模式·设计模式·系统架构
极光通讯36 分钟前
服务器内存来料检验(IQC)实操:批次追溯、外观判定与上机验证
java·服务器·算法
拾饵9421 小时前
第六周第二个项目笔记:FinInsRAG 项目学习笔记(零基础版)以及简历书写
笔记·学习
淡海水1 小时前
02-04-原理篇-GC Roots与对象图遍历
算法·unity·c#·图·gc·遍历·roots
han68891 小时前
selenium之实战
笔记·python·selenium·测试工具·自动化
ClickHouseDB1 小时前
ClickHouse 大规模 OpenTelemetry 数据接入架构的可靠性演进
算法
有只小白叫岳飒1 小时前
c++智能指针 unique_ptr与函数调用
c++