二叉树和堆详解

news2024/10/7 16:18:22

一、树的概念及结构

1.1树的概念

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

 1.2树的相关基本概念

  • 空集合也是树,称为空树。空树中没有节点;
  • 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;B是A的孩子节点
  • 节点的度:一个节点含有的子节点的个数称为该节点的度;如A的度是3,B的度是3...
  • 叶节点或终端节点:度为0的节点称为叶节点;如E、F、G...
  • 非终端节点或分支节点:度不为0的节点;如B、C、D、E...
  • 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;如A是B的父节点
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点;如B、C、D是兄弟结点
  • 树的度:一棵树中,最大的节点的度称为树的度;如这颗树的度就是3
  • 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
  • 树的高度或深度:树中节点的最大层次;
  • 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如 G和H就是堂兄弟
  • 节点的祖先:从根到该节点所经分支上的所有节点;A是所有节点的子孙
  • 子孙:以某节点为根的子树中任一节点都称为该节点的子孙;所有节点都是A的子孙
  • 森林:由n(n>=0)棵互不相交的树的集合称为森林

1.3树的表示

树有很多种存储形式,这里展示的是最经典的一种--孩子兄弟表示法

struct TreeNode{
    int val;
    struct TreeNode* firstchild;//代表树的第一个孩子结点
    struct TreeNode* offspring;//孩子的兄弟结点
};

 二、二叉树的概念及结构

2.1二叉树的概念

二叉树是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树

任何一个二叉树都是由下面的结构复合组成

 2.2特殊的二叉树(这里介绍两种基础的)

  • 1、满二叉树:如果一棵二叉树只有度为0的节点和度为2的节点,并且度为0的节点在同一层上,则这棵二叉树为满二叉树 。(也可以说树的结点个数为2^h-1的树就是满二叉树,h是树的层数)

  • 2、完全二叉树:深度为k,有n个节点的二叉树当且仅当其每一个节点都与深度为k的满二叉树中编号从1到n的节点一一对应时,称为完全二叉树

2.3二叉树的性质

性质1:二叉树的第i层上至多有2i-1(i≥1)个节点 [6]  

性质2:深度为h的二叉树中至多含有2h-1个节点 [6]  。

性质3:若在任意一棵二叉树中,有n0个叶子节点,有n2个度为2的节点,则必有n0=n2+1 [6]  。

性质4:具有n个节点的满二叉树深为log2n+1。

性质5:若对一棵有n个节点的完全二叉树进行顺序编号(从0开始),对于编号为ii≥1)的节点,有以下几点性质:

(1)i左孩子结点的编号是2*i+1

(2)i右孩子的编号是2*i+1

(2)i父节点的编号是(i-1)/2

 2.4二叉树的存储结构

一般有两种:一种为链式结构,一种为顺序结构

1.顺序结构:顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树

 2.链式结构:用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,高阶数据结构如红黑树等会用到三叉链

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

堆的性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值;

  • 堆总是一棵完全二叉树

3.1堆的实现

3.1.1代码如下

//这里建立的是小堆---即所有孩子节点的值小于父节点的值
//大堆的代码与这个类似,留给读者思考
#include<stdio.h>
#include<stdlib.h>
#include<assert.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 AdjustDown(HPDataType* a, int n, int parent);
//向上调整堆
void AdjustUp(HPDataType* a, int child);
//交换
void Swap(HPDataType* p1, HPDataType* p2);

//初始化
void HeapInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}

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

//交换
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向上调整堆
void AdjustUp(HPDataType* a, int child)
{
	int 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 AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		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 HeapPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc");
			return;
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size++] = x;
	AdjustUp(php->a, php->size - 1);
}

//删除堆顶元素
void HeapPop(HP* php)
{
	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);
	assert(!HeapEmpty(php));
	return php->a[0];
}

//判断堆是否为空
bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

//得到堆的大小
int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}


3.1.2这里面有两个函数比较重要也比较难理解---AdjustUp函数和AdjustDown函数

3.2堆排序

3.2.1堆排序的实现

基本思想:先建立一个堆,根据堆的性质,堆顶的元素是最大值或最小值,那么我们只要将堆顶的元素与数组的最后一个元素交换,然后根据HeapPop函数的思路,调整前面的数据使得它还是一个堆,如此循环,得到一个有序的序列

void HeapSort(int*a,int n)
{
    //建立堆
    //...

    //调整堆
    for(int i=n-1;i>0;i--)//只要调整n-1次
    {
        Swap(&a[0],&a[i]);
        AdjustDown(a,i,0);
    }
}

这里建立堆有两种思路,分别是AdjustUp和AdjustDown,即向上调整和向下调整

 代码如下

void HeapSort(int*a,int n)
{
    //建立堆
    //AdjustUp
    for(int i=1;i<n;i++)//从第二个元素开始
        AdjustUp(a,i);
    //AdjustDown
    for(int i=(n-1-1)/2;i>=0;i--)//从最后一个叶子节点的父节点开始
        AdjustDown(a,n,i);

    //调整堆
    for(int i=n-1;i>0;i--)//只要调整n-1次
    {
        Swap(&a[0],&a[i]);
        AdjustDown(a,i,0);
    }
}

 3.2.2上面两种建堆的时间复杂度分别时多少?

 3.3Top-K问题(在大量的数据中找到前k个最大值或最小值)

思路:假设在n个数据中找前k个最大值,先建一个大小为k的小堆(反之,建大堆),然后依次比较堆顶元素和n-k个数据,不断调整小堆,最后堆中存放的就是前k个最大值,但不有序

void CreateNDate()
{
	//造数据
	int n = 10000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}
    //将数据放入文件中
	for (size_t i = 0; i < n; ++i)
	{
		int x = rand() % 1000000;//数据在0~1000000
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

void PrintTopK(int k)
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return;
	}
	int* tmp = (int*)malloc(sizeof(int) * k);
	//读取数据
	for (int i = 0; i < k; i++)
	{
		fscanf(pf, "%d", &tmp[i]);
	}
	//建立小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(tmp, k, i);
	}
	//将堆顶元素和其他元素比较,如果大于则交换,调整堆
	while (!feof(pf))
	{
		int val = 0;
		fscanf(pf, "%d", &val);
		if (val > tmp[0])
		{
			tmp[0] = val;
			AdjustDown(tmp, k, 0);
		}
	}
	for (int i = 0; i < k; i++)
		printf("%d ",tmp[i]);
	free(tmp);
}

int main()
{
	int k = 10;
	//CreateNDate();//这个函数只要调用一次
	PrintTopK(k);
	return 0;
}

四、二叉树的链式结构

typedef struct TreeNode{
    int val;
    struct TreeNode*left;
    struct TreeNode*right;
}TreeNode;

4.1二叉树的前、中、后序遍历

  • 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
  •  中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
  • 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

 代码如下

//先序
void PreOrder(TreeNode*root)
{
    if(root==NULL)//结点为空,直接返回
        return;
    printf("%d",root->val);//遍历根结点
    PreOrder(root->left);//遍历左子树
    PreOrder(root->right);//遍历右子树
}


//中序
void InOrder(TreeNode*root)
{
    if(root==NULL)//结点为空,直接返回
        return;
    InOrder(root->left);//遍历左子树
    printf("%d",root->val);//遍历根结点
    InOrder(root->right);//遍历右子树
}

//后序
void PostOrder(TreeNode*root)
{
    if(root==NULL)//结点为空,直接返回
        return;
    PostOrder(root->left);//遍历左子树
    PostOrder(root->right);//遍历右子树
    printf("%d",root->val);//遍历根结点
}

PreOrder函数递归展开图

 如上图,如果将函数递归展开,我们会发现这就是一颗树的形状,并且需要注意的是函数递归是经过NULL结点的!!!中序和后序的展开图与它类似,可以自己画画图加深对递归遍历的理解

4.2二叉树相关问题的思路

二叉树的很多题目都是用递归来做的,本质的思想其实就是分治思想,即将一个问题转化成两个子问题。

1.求二叉树的结点数

很多人的第一想法是用之前的遍历二叉树,将打印的代码变成计数的代码,这种方法当然可以,但是它其实本质是个深度优先遍历的思想,我们现在要求用分治,代码格式如下

int TNodeSize(TreeNode*root){

}

思路:求一颗二叉树的结点数=>求它的左子树的结点数+求它的右子树的结点数+它本身,当然前提是它不是一个空树,这样我们就将问题转换成了两个相同的子问题,代码如下

int TNodeSize(TreeNode*root){
    //如果为空,结点个数就是0
    if(root==NULL)
        return 0;
    //如果不为空,求二叉树节点个数就可以拆成求两个子树结点数的问题,记得加1
    return TNodeSize(root->left)+TNodeSize(root->right)+1;
}

2.求叶子节点的个数

分治的思想依旧如上:求一颗二叉树的叶子结点数=>求它的左子树的叶子结点数+求它的右子树的叶子结点数,而函数返回的条件是树为空,或找到叶子节点

代码如下

int BTLeafSize(TreeNode*root){
    if(root==NULL)//空树
        return 0;
    if(root->left==NULL||root->right==NULL)//叶子节点
        return 1;
    return BTLeafSize(root->left)+BTLeafSize(root->right);
}

3.求树的高度

分治的思想依旧如上:求一颗树的高度=>max{ 左子树的高度,右子树的高度 } +1

代码如下

int BTreeHeight(TreeNode*root){
    if(root==NULL)
        return 0;
    return fmax(BTreeHeight(root->left),BTreeHeight(root->right))+1;
}

下面还有几个题目留给读者思考:

typedef struct TreeNode{
    int val;
    struct TreeNode*left;
    struct TreeNode*left;
}TreeNode;

//求第K层的结点数
int BTLevelSize(TreeNode*root,int k);

//查找结点
TreeNode*BTreeFind(TreeNode*root,int x);

//两棵树是否相等
bool isSame(TreeNode*p,TreeNode*q);

如果觉得这篇博客对你有所帮助的话,请一定不要吝啬你的点赞加评论哦!!!

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

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

相关文章

macOS Ventura 13.5beta2 (22G5038d)发布

系统介绍 黑果魏叔 6 月 1 日消息&#xff0c;苹果今日向 Mac 电脑用户推送了 macOS 13.5 开发者预览版 Beta 2 更新&#xff08;内部版本号&#xff1a;22G5038d&#xff09;&#xff0c;本次更新距离上次发布隔了 12 天。 macOS Ventura 带来了台前调度、连续互通相机、Fac…

数据结构--不带头的单向链表

不带头的单向链表 链表的结构注意点&#xff08;贯穿于整个链表的注意事项&#xff09;结点类型的定义申请新结点函数链表的打印链表的尾插链表的头插链表的尾删链表的头删链表的查找和修改在pos之前插入在pos位置删除在pos之后插入&#xff08;通过交换值&#xff0c;达到在po…

SpringBoot集成XXL-JOB

一、调度中心 首先下载XXL-JOB GitHub&#xff1a;https://github.com/xuxueli/xxl-job GitEE&#xff1a;https://gitee.com/xuxueli0323/xxl-job 项目使用2.3.1版本&#xff1a; https://github.com/xuxueli/xxl-job/releases/tag/2.3.1 使用IDEA打开项目 xxl-job-admin&…

Nginx网站服务详解(Nginx服务的主配置文件 ——nginx.conf)

目录 一、全局配置的六个模块简介 二、Nginx配置文件的详解 1&#xff09;全局配置模块 2&#xff09;I/O 事件配置 3&#xff09;HTTP 配置 4&#xff09;web服务监听设置 5&#xff09;其他设置 location常见配置指令&#xff1a;“root、alias、proxy_pass 对比&a…

【高危】Windows LDAP 服务远程代码执行漏洞

漏洞描述 LDAP (Lightweight Directory Access Protocol)是一个建立在TCP/IP协议栈上的目录访问协议&#xff0c;支持Active Directory服务的Windows服务器通常也支持LDAP协议。 Windows 10至11、Windows Server 2008至2022版本存在远程代码执行漏洞。当目标服务器开启LDAPS时…

PointNet、PointNet++代码解析

最远点采样FPS代码解析 注意&#xff1a;一般深度学习框架中都会使用批操作&#xff0c;来加速收敛。 因此采样函数的输入输出应当也要包含批。 def farthest_point_sample(xyz, npoint):"""Input:xyz: pointcloud data, [B, N, C]npoint: number of samplesR…

2023-6-1-Qt是什么

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f4a5;&#x1f4a5;&#x1f4a5;欢迎来到&#x1f91e;汤姆&#x1f91e;的csdn博文&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f49f;&#x1f49f;喜欢的朋友可以关注一下&#xf…

Python编程环境搭建:Mac OS安装Python环境

和 Linux 发行版类似&#xff0c;最新版的 Mac OS X 也会默认自带 Python 2.x。 我们可以在终端&#xff08;Terminal&#xff09;窗口中输入python命令来检测是否安装了 Python 开发环境&#xff0c;以及安装了哪个版本&#xff0c;如下所示&#xff1a; c.biancheng.net:~ m…

仙境传说ro服务端搭建服务器架设教程

仙境传说ro服务端搭建服务器架设教程 仙境传说&#xff1a;改编同名幻想漫画有代表科幻的中世纪的村庄、精灵居住的森林、烈日曝晒的沙漠、东洋科幻风格的村庄等。深受玩家的热爱&#xff0c;爱与勇气的冒险即将拉开帷幕这一次梦想由我们来守护仙境传说RO全屏大世界冒险RPG RO…

C++核心编程——初识STL——STL的基本概念和六大组件

文章目录&#x1f4ac; 一.前言二.STL基本概念和组成①容器②算法③迭代器④空间配置器⑤适配器⑥仿函数 三.STL工作机制 一.前言 长久以来&#xff0c;软件界一直希望建立一种可重复利用的东西&#xff0c;以及一种得以制造出“可重复运用的东西”的方法,让程序员的心血不止于…

Redis中的动态字符串(SDS)

动态字符串&#xff08;Simple Dynamic String&#xff0c;SDS&#xff09; Redis是用C语言编写的。Redis中的简单动态字符串的设计与实现&#xff0c;兼顾了操作高效、能保存任意数据、以及节省内存的需求。Redis并且还兼容C语言的原生字符串API&#xff0c;从而提高了代码的…

【MTGCD-Net】Detecting Building Changes with Off-Nadir Aerial Images

目录 1.背景 2.网络总体结构 2.1中间辅助任务预测 2.2多任务特征引导模块

【Android】Binder(二)内存划分和Binder实现一次拷贝

MMAP介绍 在 Android 中&#xff0c;Binder 通信机制中使用了 mmap&#xff08;Memory Map&#xff09;技术&#xff0c;用于实现进程间的共享内存。mmap 是一种内存映射文件的方式&#xff0c;可以将一个文件或者设备映射到进程地址空间的一段连续的地址区域中&#xff0c;这…

运维小白必学篇之基础篇第三集:文件管理相关命令实验

文件管理相关命令实验 实验者&#xff1a;胡 阳 上午练习题&#xff1a; 1、创建1.txt文件&#xff0c;文件大小为350m 2、使用echo为文件2.txt写入内容hello&#xff0c;world 3、查看文件2.txt文件的内容 4、为2.txt文件追加写入nihao 5、实时查看2.txt的变化&#xff0c;可…

低代码平台:高效构建供应商信息管理系统的利器

随着数字化时代的来临&#xff0c;企业对于应用系统的需求也越来越高&#xff0c;但同时传统的软件开发方式有很多弊端&#xff0c;比如需要大量的开发人员、开发周期长、维护成本高等&#xff0c;这就导致了低代码平台的兴起。低代码平台是一种适用于企业快速构建应用程序的工…

紫光同创开发板使用教程(一):debug用法

我们这里通过debug抓一个普通led工程的部分信号 下面是代码&#xff0c;输入差分200M 1.整体代码 timescale 1ns / 1ps module led_test( //Differential system clockssys_clk_p,//200Msys_clk_n, rst_n, //S1 led );input sys…

农业度“伏”黑科技——农业气象站

最近气象预警 非常高能 总是一波跟着一波 高温、雨水接连不断 高热、高湿的“桑拿天”频繁出现 酷暑难耐 不愧是一年一度的 六月天 在这样一个 气温高、湿度大、风速小的夏天 “农业”也度日如年 毕竟“上蒸下煮”的模式可真不是盖的 自从入伏后 日照强烈程度UP ↑ …

【CORS策略: Multiple CORS header ‘Access-Control-Allow-Origin‘ not allowed导致的跨域问题】

引起跨域CORS报错的原因有很多种&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 这里我的报错属于最后一条。 探索的原因 正常axios请求接口&#xff0c;调试接口时出现以下问题 Access to XMLHttpRequest at http://192.168.31.111:9000/api/user/sessio…

Java常见的数据结构:栈、队列、数组、链表、二叉树、二叉查找树、平衡二叉树、红黑树

一、数据结构 1.数据结构概述 数据结构是计算机底层存储、组织数据的方式。是指数据相互之间是以什么方式排列在一起的。 通常情况下&#xff0c;精心选择的数据结构可以带来更高的运行或者存储效率 2.常见的数据结构 栈 队列 数组 链表 二叉树 二叉查找树 平衡二叉…

共建、共享开源 EDA 共性技术框架 | 2023 开放原子全球开源峰会开源 EDA 分论坛即将启幕

电子电路设计自动化&#xff08;EDA&#xff09;融合了计算机、微电子、计算数学、图形学和人工智能等众多前沿技术&#xff0c;为集成电路设计、制造和封装等整个产业提供至关重要的自动化辅助设计能力。集成电路是支撑国民经济、社会发展和保障国家安全的基础性、先导性和战略…