<数据结构与算法>二叉树堆的实现

news2024/9/23 5:29:37

目录

前言

一、树的概念及结构

1 树的概念

2 树的相关概念

 二、二叉树的概念及结构

1.二叉树的概念

2. 特殊的二叉树

3. 二叉树的性质

4.二叉树的存储结构

 三、二叉树的顺序结构及实现 

1.堆的性质

2.堆的插入

3.堆的实现

堆的结构体

HeapInit 初始化

HeapPush 插入

HeapPop 删除

HeapTop 堆顶元素 

HeapEmpty 判空函数

HeapSize 数据个数

4.堆的代码 

Heap.h

Heap.c

Test.c


前言

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


一、树的概念及结构

1 树的概念

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

  • 有一个特殊的结点,称为根结点根节点没有前驱结点
  • 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1T2……Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。因此,树是递归定义的。

 注意:树形结构中,子树之间不能有交集,否则就不是树形结构

2 树的相关概念

  •  节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
  • 叶节点或终端节点度为0的节点称为叶节点; 如上图:BCHI...等节点为叶节点
  • 非终端节点或分支节点:度不为0的节点; 如上图:DEFG...等节点为分支节点
  • 双亲节点或父节点若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:AB的父节点
  • 孩子节点或子节点一个节点含有的子树的根节点称为该节点的子节点; 如上图:BA的孩子节点
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:BC是兄弟节点
  • 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
  •  节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
  • 树的高度或深度树中节点的最大层次; 如上图:树的高度为4
  • 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:HI互为兄弟节点
  • 节点的祖先从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
  • 子孙以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
  • 森林:mm>0)棵互不相交的树的集合称为森林;  

树的一种结构体储存形式:左孩子右兄弟

typedef int DataType;
struct TreeNode
{
    struct TreeNode* pNextBrother;
    struct TreeNode* firestChild1;
    DataType data;
    
};

树在实际中的运用,例如树状目录结构

在数据结构中,我们基本不使用多分枝树这一结构,而使用特殊的树——二叉树

 二、二叉树的概念及结构

1.二叉树的概念

一棵二叉树是结点的一个有限集合,该集合:

1. 或者为空

2. 由一个根节点加上两棵别称为左子树右子树的二叉树组成

 注意:

1. 二叉树不存在度大于2的结点

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

2. 特殊的二叉树

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

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

前h-1层是满的,最后一层是连续的

 3. 二叉树的性质

1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^( i - 1) 个结点.

2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^h - 1

3. 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为 n2,则有 n0= n2+1

4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度

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

  • i>0i位置节点的双亲序号:(i-1)/2i=0i为根节点编号,无双亲节点
  • 2i+1<n,左孩子序号:2i+12i+1>=n否则无左孩子
  • 若2i+2<n,右孩子序号:2i+22i+2>=n否则无右孩子

例题1:

度为0的结点数为N0

度为1的结点数为N1

度为2的结点数为N2

2n = N0 + N1 + N2

2n = N0 + N1 + N0 - 1

又因为是完全二叉树,所以N1要么是0那么是1

2n = 2N0 - 1 + N1

又因为奇偶数,所以N1必为1,选A

例题2:

因为如果一个完全二叉树高度为h,则它的结点数在[2^(h-1) ,2^h -1 ] 

所以是B

例题3:

767 = 2N0 -1 + N1 

所以N1为0,N0为384,选B

4.二叉树的存储结构

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

4.1 顺序结构

顺序结构存储时使用数组来存储适合表示满二叉树和完全二叉树,因为完全二叉树不会有空间的浪费顺序结构在物理逻辑上时一个数组,在逻辑结构上是一棵二叉树。

规定根节点在数组内下标是0,根据满二叉树性质,我们可以推导出父子节点在数组中的下标关系

  • parent = (child - 1)/  2
  • leftchild  = parent *2 +1
  • rightchild = parent*2 + 2

4.2 链式结构 

如果是非完全二叉树,那么顺序存储就不适合了,因为会造成空间的浪费,空间利用率不高,所以数组存储表示二叉树只适合完全二叉树

 三、二叉树的顺序结构及实现 

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

注意:堆在物理逻辑上时一个数组,在逻辑结构上是一棵二叉树。

1.堆的性质

  • 堆中某个节点的值总小于等于或大于等于其父节点的值
  • 堆是一颗完全二叉树
  • 所有父节点大于等于孩子的堆称为大根堆,反之所有父节点都小于等于孩子的堆称为小根堆

2.堆的插入

        根据堆是大根堆或小根堆,来选择向上调整或向下调整,向堆内插入数据,应按照数组储存顺序挨个插入,在插入后,要判断其与父节点的大小关系,选择向上调整(大根堆)或向下调整(小根堆),直到满足条件为止

3.堆的实现

堆的结构体

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

 HeapInit 初始化

void HeapInit(HP* php)
{
	assert(php);

	php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}

	php->size = 0;
	php->capacity = 4;
}

 HeapPush 插入

  • 插入后需要和父节点比较大小,进行向上调整或向下调整,所以单独封装一个函数实现该功能
  • 判断要插入时,size与capacity的值是否相等,进而判断是否要扩容(在顺序表和栈中我们都学过)
  • 最坏情况为新插入的数据最大,一直交换到根节点
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType x = *p1;
	*p1 = *p2;
	*p2 = x;
}

// 除了child这个位置,前面数据构成堆
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;//父节点就这样算
	//while (parent >= 0) 当child = 0的时候,parent计算之后也是0,还会进入循环,这是错误的,但是又因为if条件不满足,所以break跳出循环了,这就是著名的虽然错误但是莫名其妙跑起来了

	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)
	{
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * php->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->a = tmp;
		php->capacity *= 2;
	}

	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1);//向上调整,因为size++了,所以size-1
}

 HeapPop 删除

  • 我们要考虑有意义的删除,即如果删除尾部数据,堆并没有什么实际的作用,而如果我们删除堆顶数据,那么我们可以得到第二大或第二小的数据,这在现实生活中是有意义的,因为生活中有许多排序,找到排名前k的数据,这是一个有实际需求的功能,所以,我们删除堆顶元素
  • 要删除堆顶数据的话,不能挪动删除(将后面的数据前移),因为这样搞不仅效率低,而且搞完之后父子兄弟关系全都会乱最优方法是,将堆顶数据与最后一个数据进行交换,再删除最后一个数据,这样既不会使父子兄弟间关系混乱,也能做到有实际意义
  • 与会后一个数据交换后,堆的结构已经发生改变,我们要恢复堆的结构,将堆顶元素向下调整即与它的两个孩子中最大的孩子比较大小,进而进行交换调整,最坏情况到叶子节点
  • 在向下调整函数中,会出现数组越界风险,在循环时要加以判断


// 左右子树都是大堆/小堆
void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		// 选出左右孩子中大的那一个
		//这里有小问题,该点是否有右孩子,如果没有那么child+1就越界了,所以要先判断右孩子是否存在,并且还要注意逻辑判断不能写反
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}

		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapPop(HP* php)//删除要删堆顶数据,因为删尾没有什么意义
					 //删了堆顶,那么第二大或第二小的数据就会显现出来,这对于现实top k问题都是有意义的
{
	assert(php);
	assert(!HeapEmpty(php));

	// 删除数据
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;//size--,减小有效范围

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

HeapTop 堆顶元素 

HPDataType HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}

 HeapEmpty 判空函数

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

HeapSize 数据个数

int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

4.堆的代码 

Heap.h

#pragma once

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

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

void HeapInit(HP* php);
void HeapDestroy(HP* php);

void HeapPush(HP* php, HPDataType x);
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
bool HeapEmpty(HP* php);
int HeapSize(HP* php);

void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);

Heap.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"

void HeapInit(HP* php)
{
	assert(php);

	php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}

	php->size = 0;
	php->capacity = 4;
}

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType x = *p1;
	*p1 = *p2;
	*p2 = x;
}

// 除了child这个位置,前面数据构成堆
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;//父节点就这样算
	//while (parent >= 0) 当child = 0的时候,parent计算之后也是0,还会进入循环,这是错误的,但是又因为if条件不满足,所以break跳出循环了,这就是著名的虽然错误但是莫名其妙跑起来了

	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)
	{
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * php->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->a = tmp;
		php->capacity *= 2;
	}

	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1);//向上调整,因为size++了,所以size-1
}

// 左右子树都是大堆/小堆
void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		// 选出左右孩子中大的那一个
		//这里有小问题,该点是否有右孩子,如果没有那么child+1就越界了,所以要先判断右孩子是否存在,并且还要注意逻辑判断不能写反
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}

		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;//先移动parent
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
//不能挪动删除,因为这样搞不仅效率低,而且搞完之后父子兄弟关系全都会乱
void HeapPop(HP* php)//删除要删堆顶数据,因为删尾没有什么意义
					 //删了堆顶,那么第二大或第二小的数据就会显现出来,这对于现实top k问题都是有意义的
{
	assert(php);
	assert(!HeapEmpty(php));

	// 删除数据
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

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

HPDataType HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}

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

int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"

int main()
{
	HP hp;
	HeapInit(&hp);
	HeapPush(&hp, 4);
	HeapPush(&hp, 18);
	HeapPush(&hp, 42);
	HeapPush(&hp, 12);
	HeapPush(&hp, 21);
	HeapPush(&hp, 3);
	HeapPush(&hp, 5);
	HeapPush(&hp, 5);
	HeapPush(&hp, 50);
	HeapPush(&hp, 5);
	HeapPush(&hp, 5);
	HeapPush(&hp, 15);
	HeapPush(&hp, 5);
	HeapPush(&hp, 45);
	HeapPush(&hp, 5);

	int k = 0;
	scanf("%d", &k);
	while (!HeapEmpty(&hp) && k--)
	{
		printf("%d ", HeapTop(&hp));
		HeapPop(&hp);
	}
	printf("\n");

	return 0;
}


总结

        本节简单学习了二叉树的概念及顺序存储堆的实现,下节将讲解如何使用堆排序,分析时间复杂度。

 最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/901369.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

攻防世界-warmup

原题解题思路 只有一张图片&#xff0c;就查看源代码&#xff0c;有一个source.php。 查看source.php&#xff0c;白名单中还有一个hint.php。 hint.php告诉我们flag的位置ffffllllaaaagggg 但是直接跳转是没用的&#xff0c;构造payload。 http://61.147.171.105:55725/sourc…

Android12之com.android.media.swcodec无法生成apex问题(一百六十三)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

excel中有哪些通配符、excel配置问题,数学函数篇1之sum系列

学习excel前需要明确的是事&#xff1a;   在学习excel函数之前&#xff0c;大家需要明确一件事&#xff0c;excel现在设计到了一些新函数&#xff0c;这些新函数只能存在于office365、office2019及更 新版本之中&#xff0c;所以建议大家在学习时安装较新的版本&#xff0c;…

Qt与电脑管家3

1.ui页面设计技巧 最外面的widget&#xff1a; 上下左右的margin都置相同的值 这里有4个widget&#xff0c;做好一个后&#xff0c;后面3个可以直接复制.ui文件&#xff0c;然后进行微调即可。 2.现阶段实现的效果&#xff1a; 3.程序结构&#xff1a; btn1--->btn btn1---…

基于51单片机直流电机转速数码管显示控制系统

一、系统方案 本文主要研究了利用MCS-51系列单片机控制PWM信号从而实现对直流电机转速进行控制的方法。本文中采用了三极管组成了PWM信号的驱动系统&#xff0c;并且对PWM信号的原理、产生方法以及如何通过软件编程对PWM信号占空比进行调节&#xff0c;从而控制其输入信号波形等…

python、numpy、pytorch中的浅拷贝和深拷贝

1、Python中的浅拷贝和深拷贝 import copya [1, 2, 3, 4, [11, 22, 33, [111, 222]]] b a c a.copy() d copy.deepcopy(a)print(before modify\r\n a\r\n, a, \r\n,b a\r\n, b, \r\n,c a.copy()\r\n, c, \r\n,d copy.deepcopy(a)\r\n, d, \r\n)before modify a [1, 2…

机器人制作开源方案 | 送餐机器人

作者&#xff1a;赖志彩、曹柳洲、王恩开、李雪儿、杨玉凯 单位&#xff1a;华北科技学院 指导老师&#xff1a;张伟杰、罗建国 一、作品简介 1. 场景调研 1.1项目目的 近年来&#xff0c;全国多地疫情频发&#xff0c;且其传染性极高&#xff0c;食品接触是传播途径之一。…

Redis之List类型解读

目录 List简介 数据结构 常见命令 概述 ​LPUSH key value1 [value2] ​ LPUSHX key value LINDEX key index LLEN key LPOP key LRANGE key start stop List简介 列表list是一个单键多值的 Redis 列表是简单的字符串列表&#xff0c;按照插入顺序排序。你可以添加…

k8s 常见面试题

前段时间在这个视频中分享了 https://github.com/bregman-arie/devops-exercises 这个知识仓库。 这次继续分享里面的内容&#xff0c;本次主要以 k8s 相关的问题为主。 k8s 是什么&#xff0c;为什么企业选择使用它 k8s 是一个开源应用&#xff0c;给用户提供了管理、部署、扩…

Python将网络文件下载到本地

Python将网络文件下载到本地 前言相关介绍Python将网络文件下载到本地 前言 由于本人水平有限&#xff0c;难免出现错漏&#xff0c;敬请批评改正。更多精彩内容&#xff0c;可点击进入Python日常小操作专栏、YOLO系列专栏、自然语言处理专栏或我的个人主页查看基于DETR的人脸伪…

Kubernetes 安全机制 认证 授权 准入控制

客户端应用若想发送请求到 apiserver 操作管理K8S资源对象&#xff0c;需要先通过三关安全验证 认证&#xff08;Authentication&#xff09;鉴权&#xff08;Authorization&#xff09;准入控制&#xff08;Admission Control&#xff09; Kubernetes 作为一个分布式集群的管理…

FreeCAD的傻瓜式初级使用教程

起因&#xff1a;自己想DIY一套线性手刹和序列档&#xff0c;以便和我之前的freejoy控制器相连接应用&#xff0c;需要自己制图和在某宝找代加工的商家&#xff0c;但我又不想安装体积巨大的AutoCAD&#xff0c;所以找了以下开源、免费的解决方案&#xff0c;所以就有了这篇文章…

这是真的“技术驱动”的公司吗?

“ 软件交付团队的DevOps能力&#xff0c;恰恰是技术能力的最好体现。” 01 — “我们是技术驱动的公司” 跟我们合作的软件供应商&#xff0c;每次发生软件变更或升级就一地鸡毛&#xff0c;而且经过屡次沟通&#xff0c;都没有什么本质改善。 当我们跟他们的高层投诉时&#…

JVM面试题-1

1、什么是JVM内存结构&#xff1f; jvm将虚拟机分为5大区域&#xff0c;程序计数器、虚拟机栈、本地方法栈、java堆、方法区&#xff1b; 程序计数器&#xff1a;线程私有的&#xff0c;是一块很小的内存空间&#xff0c;作为当前线程的行号指示器&#xff0c;用于记录当前虚拟…

数据结构:队列Queue详解

文章目录 一、队列的概念和特点二、队列的使用三、队列的简单实现四、循环队列 一、队列的概念和特点 队列:只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表。进行插入操作的一端称为队尾&#xff0c;删除操作的一端称队头。 入队列&#xff…

使用Nginx调用网关,然后网关调用其他微服务

问题前提&#xff1a;目前我的项目是已经搭建了网关根据访问路径路由到微服务&#xff0c;然后现在我使用了Nginx将静态资源都放在了Nginx中&#xff0c;然后我后端定义了一个接口访问一个html页面&#xff0c;但是html页面要用到静态资源&#xff0c;这个静态资源在我的后端是…

windows服务器下java程序健康检测及假死崩溃后自动重启应用、开机自动启动

前两天由于项目需要&#xff0c;一个windows上的批处理任务&#xff08;kitchen.bat&#xff09;&#xff0c;需要接到mq的消息通知后执行&#xff0c;为了快速实现这里我们通过springboot写了一个jar程序&#xff0c;用于接收mq的消息&#xff0c;并调用bat文件。 本程序需要实…

【欧拉计划】偶数斐波那契数

题目链接&#xff1a;偶数斐波那契数 解法一&#xff1a;暴力枚举 看见题目&#xff0c;第一反应就是先找到小于400万的所有斐波那契数&#xff0c;再从这些斐波那契数中筛选出偶数进行求和。 由于递归方法求斐波那契数的时间复杂度较高&#xff0c;故这里采用迭代的方法。 先…

C++笔记之全局函数做友元与类做友元

C笔记之全局函数做友元与类做友元 code review! 文章目录 C笔记之全局函数做友元与类做友元1.全局函数做友元2.类作友元 1.全局函数做友元 代码 #include <iostream> using namespace std;class MyClass { private:int x; public:MyClass(int a) : x(a) {}friend void…

互斥锁概念使用

互斥锁的创建两种方式 1.动态方式 #include <pthread.h> #include <stdio.h> #include <unistd.h> #include <string.h> FILE *fp; void *func2(void *arg) {pthread_detach(pthread_self());printf("this is func2 thread\n");char str2[]…