详解二叉树

【本节目标】

  • 1.树的概念和结构

  • 2.二叉树的概念和结构

  • 3.二叉树的顺序结构及实现

  • 4.二叉树的链式结构及实现


1.树的概念及结构

1.1树的概念

树是一种非线性的数据结构,它由一个根结点和n(>=0)个子树构成,之所以叫做树,是因为它很像生活中的树倒过来的样子

注意:

  • 子树是不相交的
  • 每个结点有且只有一个父结点

1.2树相关的概念:

结点的度:一个结点拥有孩子的个数就叫该结点的度,比如A的度为6,E的度为0;

叶结点或终端结点:度为0的结点就是叶结点,比如B,H,I,J......就是叶结点

分支结点或非终端结点:度不为0的结点就是分支结点,比如A,C,D......就是分支结点

父结点:若一个结点含有子节点,则称该结点为其子结点的父结点,比如A就是B的父结点

子结点:一个结点其子树的根结点称为该结点的子结点,比如B就是A的子节点

兄弟结点:两个结点的父节点是同一个结点称为这两个结点是兄弟结点,比如I和J就是兄弟结点

树的度:一颗树中所有结点的度中最大值就是树的度,比如上面的树的度是6

结点的层次:从根结点开始定义,根为第一层,往下依次递增,比如I结点所在的层次为3

树的高度或深度:树中结点的最大层次,比如上面的树的高度为3

堂兄弟结点:两个结点的父结点在同一层,并且这两个结点不为兄弟结点,称这两个结点为堂兄弟结点,比如H和I是堂兄弟结点

结点的祖先:从根结点到该结点的路径上所有的结点都叫该结点的祖先,比如I的祖先是A和D

子孙:以某结点为根的子树下,所有的结点都是该节点的子孙,比如所有的结点都是A的子孙

森林:m(>0)颗互不相交的树的集合叫做森林


1.3树的结构

  • 由于每个结点我们并不知道它有几个子节点,可以定义一个指针数组,规定了每个结点有SIZE个子节点
typedef int TreeDataType;#define SIZE 5struct Node
{TreeDataType val;struct Node* a[SIZE];
};

这样定义的问题是,实际上每个结点的子节点树是不确定,有可能某些结点的子节点没有SIZE个,就造成了空间的浪费

在使用中,我们更常用名为左孩子右兄弟的结构表示法:

typedef int TreeDataType;typedef struct Node
{TreeDataType val;struct Node* leftChild;struct Node* rightBrother;
}Node;


2.二叉树的概念和结构

2.1二叉树的概念

度数<=2的树叫做二叉树

2.2特殊的二叉树

  1. 满二叉树:一个二叉树,如果每一层的结点数都达到最大值,也就是说,如果一个高度为h的二叉树,它的总结点数为2^{h}-1,则称该二叉树为满二叉树
  2. 完全二叉树:一个二叉树的高度为h,如果它的前h-1层是满二叉树,且h层的结点从左到右是连续的,则称该二叉树为完全二叉树

2.3二叉树的性质:

规定二叉树的层数从1开始

  1. 一颗非空二叉树,第i层最多有2^{i-1}个结点
  2. 深度为h的二叉树最多有2^{h}-1个结点
  3. 一颗满二叉树,有N个结点,它的高度是h=\log_{2}{(N+1)}
  4. 一颗完全二叉树,有N个结点,它的高度范围是\left [ \log_{2}{N}+1,\log_{2}{(N+1)} \right ]
  5. 对于任何一颗二叉树,如果它度数为0的结点的个数为n_{0},度数为2的结点的个数为n_{2},则n_{0}=n_{2}+1
  6. 若将一个二叉树从上到下,从左到右按照数组的方式依次编号,则父结点和子结点之间的关系:
    1)假设父结点的下标为i,由父结点算子结点:左孩子为2*i+1,右孩子为2*i+2
    2)假设子结点下标为j,由子结点算父结点:父结点为(j-1)/2

2.4二叉树的存储结构

二叉树的存储结构由两种:

1.顺序结构存储

所谓用顺序结构存储,就是用数据存储,但是用数组存储的二叉树最好是满二叉树或完全二叉树,因为这两个二叉树的结点是连续的,正好与数组连续相对应;如果是普通的二叉树用数组存储,数组中间有的位置需要空出来

2.链式结构存储

链式结构存储就是用链表将数据串起来;通常有二叉链,三叉链,二叉链是一个结点中两个指针,一个指针指向左子树,另一个指向右子树;而三叉链在二叉链的基础上又加了一个指向父节点的指针,目前我们只考虑二叉链。

结构定义:

//二叉链
typedef int BTNDataType;typedef struct BinaryTreeNode
{BTNDataType val;//值struct BinaryTreeNode* leftChild;//指向左孩子struct BinaryTreeNode* rightChild;//指向右孩子
}BinaryTreeNode;

2.5二叉树顺序结构的应用

2.5.1堆的概念

  • 堆是一种完全二叉树,分为大堆和小堆
  • 由于完全二叉树的特性,堆中的数据适合用数组来进行存储

2.5.2堆的性质

  • 大堆中,所有父结点均大于子结点;小堆中,所有父结点均小于子结点

2.5.3堆的实现

结构定义:
  • 前面说过,堆适合用数组存储,因此我们堆的结构就类似一个顺序表
typedef int HeapDataType;typedef struct Heap
{HeapDataType* a;int size;//有效数据个数int capacity;//容量
}Heap;

实现接口:
//初始化
void HeapInit(Heap* php);//销毁
void HeapDestroy(Heap* php);//入数据
void HeapPush(Heap* php, HeapDataType x);//出数据
void HeapPop(Heap* php);//判空
bool HeapEmpty(Heap* php);//获取堆顶数据
HeapDataType HeapTop(Heap* php);//获取堆的数据个数
int HeapSize(Heap* php);

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

入数据:

我们以大堆为例,小堆同理

  • 假设入数据前,我们的数据是一个大堆,此时入的数据有两种情况:
    1)入的数据比其父结点大:那么为了保持大堆的性质,我们得将其和其父结点交换;如果此时该数据还比其父结点大,那么还要进行交换,直到比其父结点小或其成为根结点
    2)入的数据小于等于其父结点:此时就相当于尾插,不需要动数据

我们把插入的数据往上调整的过程叫做向上调整算法

向上调整算法的实现:
  •  如果孩子比父亲大,则交换两者,再更新孩子和父亲,直到父亲大于孩子或孩子成为根结点
void Swap(HeapDataType* p1, HeapDataType* p2)
{HeapDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}//向上调整算法
void AdjustUp(HeapDataType* a, int child)
{int parent = (child - 1) / 2;while (child > 0){if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (parent - 1) / 2;}else{break;}}
}
//入数据
void HeapPush(Heap* php, HeapDataType x)
{assert(php);//判断是否需要扩容if (php->capacity == php->size){int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;HeapDataType* tmp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * newCapacity);if (tmp == NULL){perror("HeapPush:realloc fail");exit(-1);}php->capacity = newCapacity;php->a = tmp;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1);
}

出数据:

很多人会想,出数据不就是size--吗?在之前的顺序表,链表等数据结构中,出数据的确就只是移除数据;但是,到了这个阶段,我们得考虑到,出数据不仅仅是为了出数据了,要进一步想,我做这个操作有什么作用?

在堆中,如果出数据就只是移除堆尾的元素,那出了数据有什么意义呢?

因此,在堆中,我们的出数据是出掉堆顶的数据,那这样做有什么意义呢?根据堆的性质,堆顶的数据是数组中的最大值(最小值),将最大值(最小值)删除,接下来就可以筛选次大值(次小值)了......往下一一筛选,是不是就能降序(升序)我们的数据

那么是不是直接出掉堆顶的数据呢?如果直接出掉堆顶的数据,此时所有结点的父子关系都乱了,且此时的二叉树不一定是一个堆了;因此,我们出数据的操作是,先将堆顶数据和堆尾数交换,再出掉堆尾数据,就相当于出掉堆顶数据,且此时根结点的子树的关系不变

  • 此时的二叉树有可能不是大堆,但其子树肯定都是堆,需要我们调整,我们将根结点向下调整叫做向下调整算法
向下调整算法:
  • 我们需要将孩子当中较大的那个与父亲交换,按照常规写法,需要先将其中一个孩子与父亲比较,如果孩子大于父亲,则交换,否则和另一个孩子比较;要求我们写两个逻辑,但这两个逻辑的本质又是一样的,就显得有些冗余,于是想到我们之前写过的假设法
    先假设比较的孩子为左孩子,如果右孩子大于左孩子,则将待比较的孩子换成右孩子
  • 如果孩子大于父亲,则交换,之后更新孩子和父亲,直到孩子小于父亲或者孩子越界了
  • 有种特殊情况,孩子正好在堆尾,此时比较右孩子和左孩子时,右孩子越界了;应当控制一下比较的条件
void AdjustDown(HeapDataType* a, int size, int parent)
{int child = parent * 2 + 1;while (child < size){if (child + 1 < size && a[child + 1] > a[child]){child++;}if (a[parent] < a[child]){Swap(&a[parent], &a[child]);parent = child;child = child * 2 + 1;}else{break;}}
}

因此我们的出数据代码就是:

//出数据
void HeapPop(Heap* php)
{assert(php);Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}

判空:
//判空
bool HeapEmpty(Heap* php)
{assert(php);return php->size == 0;
}

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

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

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

想要排序数据:

void TestHeap()
{int a[] = { 3, 4,7,2,1,8,9,22,73,24 };Heap hp;HeapInit(&hp);for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++){HeapPush(&hp, a[i]);}while (!HeapEmpty(&hp)){HeapDataType ret = HeapTop(&hp);printf("%d ", ret);HeapPop(&hp);}printf("\n");
}

 


2.5.4堆的应用

堆的常见应用:

  1. 堆排序
  2. TOPK问题

上面我们用堆的插入和删除操作完成了数据的排序,但实际上改变的只是堆里面的数据,对外面的a数组并没有改变,当然了,我们也可以在最后拷贝到原数组;但其实不需要那么麻烦,在下篇博客,我将会讲讲堆真正强大的功能——堆排序

还有一个非常经典的问题,在N个数据中,取出最大的前K个数:

我们常见的思路是,将该数据弄成一个大堆,然后再PopK次

时间复杂度是:O(N*\log_{2}{N}+K*\log_{2}{N}),也就是O(n*\log_{2}{N} ),好像效率也还行

但如果N取非常大,10亿,甚至100亿呢?此时我们的内存是存不下这么多数据的,这些数据只能放在文件中,此时我们的思路是:

  1. 先取数据中的前K个数据,将这K个数据建小堆,再将后面的数据依次跟堆顶数据比较,如果大于堆顶数据,就替换堆顶数据进堆,再调整成小堆
  2. 依次往后比较,最后比完的这K个数就是最大的前K个数

可能有人疑惑的是为什么是建小堆,如果是建大堆,由于大堆的性质,堆顶的数是最大值,如果这N个数中的最大值在第一次建堆时进去了,那么后面比较时就没有数据比堆顶数据更大,我们的堆就不能完成更新;只有建小堆,堆顶的数据是这K个数中最小的,那么最大的前K个数一个会将其他数排挤出去

时间复杂度:O(N*\log_{2}{K} )

具体代码的实现也会在下篇博客详解

需要堆的实现的源码的小伙伴可以去我的Gitee主页获取

Heap/Heap · baiyahua/LeetCode - 码云 - 开源中国 (gitee.com)

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

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

相关文章

Hive数据库与表操作

文章目录 一、准备工作二、Hive数据库操作&#xff08;一&#xff09;Hive数据存储&#xff08;二&#xff09;创建数据库&#xff08;三&#xff09;查看数据库&#xff08;四&#xff09;修改数据库信息 一、准备工作 二、Hive数据库操作 &#xff08;一&#xff09;Hive数据…

Python Selenium 图片资源自动搜索保存 项目实践

实现访问首页 from os.path import dirnamefrom selenium import webdriverclass ImageAutoSearchAndSave:"""图片自动搜索保存"""def __init__(self):"""初始化"""self.driver webdriver.Chrome(executable_pa…

西南科技大学数字电子技术实验二(SSI逻辑器件设计组合逻辑电路及FPGA实现 )FPGA部分

一、实验目的 1、掌握用SSI(小规模集成电路)逻辑器件设计组合电路的方法。 2、掌握组合逻辑电路的调试方法。 3、学会分析和解决实验中遇到的问题。 4、学会用FPGA实现本实验内容。 二、实验原理 包括:原理图绘制和实验原理简述 1、1位半加器 2、1位全加器 3、三…

2021年全国硕士研究生入学统一考试管理类专业学位联考英语(二)试题

文章目录 2021年全国硕士研究生入学招生考试英语二试题SectionⅠUse of EnglishSection Ⅱ Reading ComprehensionText 1Text 2Text 2Text 3Text 4 Section III TranslationSection Ⅳ Writing 2021年全国硕士研究生入学招生考试英语二试题 SectionⅠUse of English Directio…

oracle数据库备份2(expdp)

使用exp命令定时进行数据库备份的操作前面已经记录过&#xff1a; oralce数据库定时备份 下面记录下使用更加高效的expdp命令和impdp&#xff0c;这两个命令同样是用来做数据库备份和还原的&#xff0c;但速度更快&#xff0c;效率更高&#xff0c;缺点是只能用在服务器端进行…

阿里云ACE认证之国际版与国内版对比!

大厂疯狂裁员&#xff0c;互联网行业迎来寒冬&#xff0c;技术人员被动陷入疯狂内卷。在愈加内卷的IT领域&#xff0c;“云计算”作为少有的蓝海&#xff0c;无疑是打工人未来实现职场提升、摆脱内卷的绝佳选择&#xff01; 对于云计算行业的人来说&#xff0c;最值得考的肯定是…

洪泽湖流域建筑物、人口密度与土地利用数据技术服务

一&#xff0e;背景介绍 人类社会发展离不开土地&#xff0c;没有土地就没有人类&#xff0c;土地利用随着人类的出现而发生。人类为了一定的社会或经济方面的目的&#xff0c;会通过利用、改造等活动。从土地上获得更多的资源。土地利用既要受自然条件的制约&#xff0c;同时也…

2023年国赛试题:配置inux1 为 CA 服务器

试题内容:配置 linux1 为 CA 服务器,为 linux 主机颁发证书。证书颁发机构有 效期 10 年,公用名为 linux1.skills.lan。申请并颁发一张供 linux 服务器使用的证书,证书信息:有效期 =5 年,公用名=skills.lan, 国家=CN,省=Beijing,城市=Beijing,组织=skills,组织单位…

Unity UGUI的自动布局-LayoutGroup(水平布局)组件

Horizontal Layout Group | Unity UI | 1.0.0 1. 什么是HorizontalLayoutGroup组件&#xff1f; HorizontalLayoutGroup是Unity UGUI中的一种布局组件&#xff0c;用于在水平方向上对子物体进行排列和布局。它可以根据一定的规则自动调整子物体的位置和大小&#xff0c;使它们…

亚马逊云科技实现了奇瑞捷豹路虎SAP系统的上云目标并保持成本优化

11月23日&#xff0c;“2023第八届IDC中国数字化转型年度盛典”正式开启并揭晓“2023 IDC中国未来企业大奖-卓越奖”获奖企业&#xff0c;奇瑞捷豹路虎汽车有限公司&#xff08;以下简称“奇瑞捷豹路虎”&#xff09;凭借“基于云原生的智慧化运营平台”项目&#xff0c;继获得…

自动驾驶HWP功能规范

HWP功能规范 Highway Pilot Functional Specification 文件状态&#xff1a; 【√】草稿 【】正式发布 【】正在修改 文件起草分工 撰写&#xff1a; 审核&#xff1a; 编制&#xff1a; 签名&#xff1a; 日期&#xff1a; 审核&#xff1a; 签名&#xff1a; 日期&am…

企业业务场景如何实现自动化连接?

为什么要实现企业业务场景的自动化连接&#xff1f; 可提高效率&#xff0c;自动化连接可以减少人工操作和手动干预的需求&#xff0c;从而提高业务处理的速度和效率。通过自动化连接&#xff0c;不同的系统、应用程序和流程可以自动协同工作&#xff0c;减少了人工处理的时间和…

【计算机组成原理】存储系统

&#x1f384;欢迎来到边境矢梦的csdn博文&#x1f384; &#x1f384;本文主要梳理计算机组成原理中 存储系统的知识点和值得注意的地方 &#x1f384; &#x1f308;我是边境矢梦&#xff0c;一个正在为秋招和算法竞赛做准备的学生&#x1f308; &#x1f386;喜欢的朋友可以…

打游戏NVIDIA怎么设置性能最好?

打游戏NVIDIA怎么设置性能最好&#xff1f;当前很多用户都在Win10电脑上畅玩游戏&#xff0c;所以想知道NVIDIA控制面板最佳设置方法&#xff0c;更好地发挥NVIDIA控制面板性能&#xff0c;用户就能享受更棒的游戏乐趣。接下来小编给大家详细介绍NVIDIA显卡游戏最佳设置步骤教程…

【Proteus仿真】【51单片机】智能晾衣架设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真51单片机控制器&#xff0c;使用LCD1604液晶、按键、蜂鸣器、L298N电机、PCF8591 ADC模块、DHT11温湿度传感器、雨滴传感器、风速、光线传感器等。 主要功能&#xff1a; 系统运行…

<Linux>(极简关键、省时省力)《Linux操作系统原理分析之Linux 进程管理 6》(10)

《Linux操作系统原理分析之Linux 进程管理 6》&#xff08;10&#xff09; 4 Linux 进程管理4.6 Linux 管道4.6.1 管道的概念4.6.2 无名管道1.终端使用2.程序中使用 4.6.2 命名管道1.终端使用2.程序中使用 4 Linux 进程管理 4.6 Linux 管道 4.6.1 管道的概念 1、管道是 linu…

2019年10月17日: Go生态洞察:在Go 1.13中处理错误

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

Mysql数据库多表数据查询问题

1、背景 线上某个业务数据分表存储在10个子表中&#xff0c;现在需要快速按照条件&#xff08;比如时间范围&#xff09;筛选出所有的数据&#xff0c;主要是想做一个可视化的数据查询工具&#xff0c;给产研团队使用。 2、实践 注意&#xff1a;不要在线上真实数据库操作&am…

用户选择PowerFlex的十大理由

既有高性能      满足AI、VDI、数据库等工作负载      又有开放架构      4个节点起步可扩展至上千节点      支撑起不断变化的新兴应用负载      还能与云互通      实现云上云下互联      Dell PowerFlex      连续八季度销量增长      …

提升企业文档处理效率,尽在Readiris PDF Corporate下载

在现代企业中&#xff0c;大量的文档处理工作是不可避免的。然而&#xff0c;传统的文档处理方法往往效率低下&#xff0c;浪费了企业宝贵的时间和资源。为了帮助企业提升文档处理效率&#xff0c;我们推荐使用Readiris PDF Corporate软件。 Readiris PDF Corporate是一款功能…