前言
这里我会从二叉树的概念开始讲解,其次涉及到概念结构,以及堆的实现和堆排序。
目的是,堆比二叉树简单,同时堆本质上是二叉树的其中一种情况,堆属于二叉树顺序结构的实现
最后完善二叉树的讲解,也就是二叉树的链式结构的实现
二叉树的基本概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
有一个特殊的结点,称为根结点,根结点没有前驱结点
除根结点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1)因此,树是递归定义的。
就像这样
这样
二叉树的基本性质
结点的度
一个结点含有的子树的个数称为该结点的度
如上图:A的为6(BCDEFG)
叶结点或终端结点
度为0的结点称为叶结点
如上图:B、C、H、I...等结点为叶结点(没有孩子的就是叶子节点)
非终端结点或分支结点
度不为0的结点
如上图:D、E、F、G...等结点为分支结点
双亲结点或父结点
若一个结点含有子结点,则这个结点称为其子结点的父结点
如上图:A是B的父结点
孩子结点或子结点
一个结点含有的子树的根结点称为该结点的子结点
如上图:B是A的孩子结点
兄弟结点
具有相同父结点的结点互称为兄弟结点
如上图:B、C是兄弟结点
树的度
一棵树中,最大的结点的度称为树的度
如上图:A树的度为6(最大的多少就是多少)(BCDEFG)
也就是有几个孩子
那么
H的度为0
E的度为2
以此类推
结点的层次
从根开始定义起,根为第1层,根的子结点为第2层
以此类推;(一般情况下从1开始 有些会从0开始)
但是为了方便计算,右兄弟左孩子的时候我是从0 开始的
树的高度或深度
树中结点的最大层次
如上图:树的高度为4
- 树的深度是4,因为从根节点A到最远的叶子节点F,需要经过4条边。
堂兄弟结点
双亲在同一层的结点互为堂兄弟
如上图:H、I互为兄弟结点
结点的祖先
从根到该结点所经分支上的所有结点
如上图:A是所有结点的祖先
子孙
以某结点为根的子树中任一结点都称为该结点的子孙
如上图:所有结点都是A的子孙
森林
由m(m>0)棵互不相交的树的集合称为森林
简单说 的说就是有好几个根节点,也就是好几个A组成(多棵树。并查集)
二叉树的定义
树的关键点是不知道定义几个树的度
1,明确知道的话我们可以写
2,不知道几个树的度,顺序表来写
3,右兄弟左孩子写法
不管多少,我们只定义两个树的度
特殊的二叉树:
1. 满二叉树:
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
2. 完全二叉树:
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。从左到右必须连续(完全二叉树)
对比
右兄弟左孩子
右兄弟左孩子表示法,实际上是一种完全二叉树,需要按照顺序在二叉树里面进行放入
优缺点
优点
空间效率:这种写法允许我们使用数组来存储完全二叉树,而不需要为每个节点分配两个指针的空间。
简单性:它简化了对完全二叉树的遍历和操作。
缺点
限制性:这种方法只适用于完全二叉树,对于非完全二叉树,这种写法不适用。
复杂性:对于不熟悉这种表示法的人来说,理解和实现可能会有些复杂。
定义
在定义里面不管多少,我们只定义两个树的度
逻辑讲解
右兄弟左孩子写法
在这种写法中,每个节点有两个子节点(左孩子和右兄弟),如果一个节点没有左子节点,那么它的左指针会指向它的右兄弟,而不是指向一个子节点。这种表示方法可以有效地利用数组来存储二叉树,同时保持树的结构信息。
例子
假设我们有以下完全二叉树:
在这个数组中,每个元素代表一个节点:
- A是根节点。
- B是A的左孩子。
- C是A的右兄弟。
- D是B的左孩子。
- E是B的右兄弟,同时也是C的左孩子。
- F是C的右兄弟。
- G是F的右兄弟。
简单的说就是,我们存储的时候,我们会按照顺序进行存储,A下面只放两个数值,放满了,我们就往A下面放,A下面放满了两个,我们往B下面放,循环套娃
树的存储方式
顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。
存储方式
二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
计算节点方式
我们可以通过计算找到父子节点,左右兄弟节点
节点计算总结
在数组中,我们可以通过节点的索引来确定其左孩子和右兄弟:
假设父亲在数组里面的下标为i
左孩子位于索引
2 * i + 1
右兄弟位于索引
2 * i + 2
假设孩子在数组里面的下标为j
父亲位于(j-1)/2
如果一个节点是叶子节点,或者在最后一层并且没有右兄弟,那么它的右兄弟指针将指向一个空值或者一个表示终止的特殊值。
二叉树使用注意事项
数组存储只适合满二叉树,或者特殊二叉树
像下面的情况就是不适合,不是不能实现,是不适合实现的
非完全二叉树,适合的实现方式方式是:二叉树链式结构实现
完全二叉树,适合的实现方式是:二叉树顺序结构的实现
下面我们实现,二叉树顺序结构的实现
二叉树顺序结构(堆):堆的概念:
存储方式
二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
还是那句话,堆是一种顺序结构实现的完全二叉树
在逻辑上不是连续的,但是在实际上,是数组进行实现的
堆的性质:
在数据结构中,“堆”是一种特殊的完全二叉树,它满足以下两个性质:
- 结构性质:堆是一个完全二叉树,如果用数组表示,那么除了最后一层外,每一层都被完全填满,且最后一层从左到右填充。
- 堆性质:树中任何节点的值都必须大于或等于其子节点的值(最大堆),或小于或等于其子节点的值(最小堆)。
最大堆(大堆)
在最大堆中,父节点的值总是大于或等于其子节点的值。这意味着堆中的最大值位于根节点。最大堆常用于实现优先级队列,其中最大元素具有最高的优先级。
特点:
- 根节点是堆中的最大元素。
- 任何父节点的值都不小于其子节点的值。
应用场景:
- 堆排序:在堆排序算法中,最大堆用于选择序列中的最大元素。
- 优先级队列:在需要频繁访问最大元素的场景中使用。
最小堆(小堆)
在最小堆中,父节点的值总是小于或等于其子节点的值。这意味着堆中的最小值位于根节点。最小堆同样用于实现优先级队列,但最小元素具有最高的优先级。
特点:
- 根节点是堆中最小元素。
- 任何父节点的值都不大于其子节点的值。
应用场景:
- 堆排序:在堆排序算法中,最小堆用于选择序列中的最小元素。
- 优先级队列:在需要频繁访问最小元素的场景中使用。
二叉树顺序结构的实现(堆):堆的实现:
实现(小堆)
在实际的编程实现中,堆通常用数组来表示,因为这样可以有效地利用内存空间,并且可以快速地通过索引访问父节点和子节点。节点的索引和其父节点或子节点的索引之间有一定的关系:
- 父节点索引:
(i - 1) / 2
- 左孩子节点索引:
2 * i + 1
- 右孩子节点索引:
2 * i + 2
其中
i
是节点的索引。理解大堆和小堆的概念对于在实际应用中选择合适的数据结构和算法非常重要。
创建文件(小堆)
这里我们依旧是创建三个文件来实现顺序结构的堆
创建堆(小堆)
定义一个堆(Heap)的数据结构
这里所需要的头文件,是文件所需要的,这里不做过多解释,主要看的是定义的是数据结构,这里我们是用数组实现的
定义数据类型
HPDataType
:使用typedef
创建了一个新的类型别名HPDataType
,这里指定为int
类型。这意味着HPDataType
可以用来声明整数类型的变量,但在堆结构中,它可以被用作更通用的数据类型。定义结构体
Heap
:创建了一个结构体Heap
,它将用于表示整个堆的元数据和存储空间。成员变量:
HPDataType* _a
:这是一个指针,指向堆中第一个元素的地址,用于访问和操作堆中的元素。int _size
:表示当前堆中元素的数量,即已使用的元素个数。int _capacity
:表示堆的最大容量,即_a
指针所指向的数组能够容纳的元素个数。这个结构体定义为后续实现堆的操作(如插入、删除、调整等)提供了必要的数据结构支持。在实际使用中,你还需要实现一些函数来操作这个
Heap
结构体,比如初始化堆、插入元素、删除最大元素(在最大堆中)或最小元素(在最小堆中)、销毁堆等。创建了
Heap
结构体后,你通常会通过调用相关函数来初始化堆、使用它进行操作,并在最后销毁堆以释放分配的内存#define _CRT_SECURE_NO_WARNINGS 1 #pragma once #include<stdio.h> #include<assert.h> #include<stdbool.h> #include<stdlib.h> #include<time.h> typedef int HPDataType; typedef struct Heap { HPDataType* _a;//首元素地址 int _size;//元素个数 int _capacity;//元素容量 }Heap;
堆的初始化和销毁(小堆)
这里的初始化和销毁和顺序表的初始化以及销毁是差不多的
这里知识掌握不牢固的同学,可以看一下我写的顺序表的篇章
顺序表(增删减改)+通讯录项目(数据结构)+顺序表专用题型-CSDN博客https://blog.csdn.net/Jason_from_China/article/details/137484207
//堆的初始化 void HeapInit(Heap* hp) { //初始化这里不开辟空间 hp->_a = NULL; hp->_capacity = hp->_size = 0; } // 堆的销毁 void HeapDestory(Heap* hp) { assert(hp); //这里不需要循环释放,因为这里是数组实现堆的 free(hp->_a); hp->_a = NULL; hp->_capacity = hp->_size = 0; }
HeapInit
函数接收一个指向Heap
结构体的指针hp
。这个函数的作用是将hp
指向的Heap
结构体初始化为一个空堆:
_a
成员被设置为NULL
,表示没有分配任何内存空间。_capacity
成员被设置为 0,表示堆的最大容量目前为 0,即没有预留空间。_size
成员被设置为 0,表示堆中目前没有任何元素。
HeapDestory
函数接收一个指向Heap
结构体的指针hp
,并执行以下操作来销毁堆:
- 使用
assert
确保传入的hp
不是NULL
。- 使用
free
函数释放_a
指针指向的内存空间。由于_a
在HeapInit
中被初始化为NULL
,这里释放前应确保_a
非NULL
(这通常在其他函数中进行判断和分配)。- 将
_a
设置回NULL
,确保指针不再指向任何内存空间。- 将
_capacity
和_size
重置为 0,恢复堆为一个空状态。
加入数据(小堆)
首先我们需要看堆实现的时候是如何调整的
1,这里最后一个数值是新加入的数值,那么此时我们需要向上调整,也就是我们需要和上一个父亲节点进行对比,
2,如果孩子节点小于父亲节点,那么我们就需要进行交换,因为小堆根是最小的数值
3,并且更新父亲节点想下标和孩子节点的下标
4,备注:这里我们不需要三方进行对比,意思就是,我们不需要三个数值进行对比,因为如果左孩子存在,按照这个逻辑,左孩子最后如果小于父亲节点就一定会进行交换,不小于就不会进行交换,然后插入右孩子时候,右孩子只需要和父亲进行比较就可以了,就算右孩子比父亲数值小,那么此时会和父亲节点进行交换,这个时候我们发现,左孩子一定大于或者等于右孩子,而右孩子也一定大于或者等于父亲节点,因为这里是小堆,越往上越小。
解释一下备注4:
此时我们发现,我们还没有插入左右孩子的节点
此时插入节点还没有交换
进行交换,此时我们发现左节点一定大于父亲节点
插入右孩子,此时还没有交换
进行交换,我们只需要对新插入数值和父亲节点进行对比就可以,不需要1,7,0 三个数值进行对比,从而决定交换不交换,因为左孩子一定小于父亲,如果父亲小于左孩子,交换之后,那么左孩子也一定小于右孩子
这里我们了解一下循环逻辑
循环三要素
1,初始条件
2,循环条件
3,结束条件
那么向上调整的结束逻辑应该是什么,那就是当插入的数值,也就是孩子元素下标到最上面的时候,也就是到0的时候,也就是应该结束循环,所以chile>0,这里是这一种情况也是三种情况下唯一不会产生越界的逻辑。
代码实现
//交换 void Swap(HPDataType* p1, HPDataType* p2) { HPDataType tmp = *p1; *p1 = *p2; *p2 = tmp; } //向上调整 void AdjustUp(HPDataType* a, int chile) { //获取父母所在位置 int parent = (chile - 1) / 2; while (chile > 0)//这个循环条件不会越界,其余两个循环条件都会导致越界,但是也会正常运行 { if (a[chile] < a[parent]) { //传递地址,指针接收 Swap(&a[chile], &a[parent]); //更新父母和孩子的下标 chile = parent; parent = (chile - 1) / 2; } else { break; } } 这个循环条件产生了越界,但是 printf("%d ", ps._a[-1]);(特别大) printf("%d ", ps._a[100]);(特别小) //while (a[chile] < a[parent]) //{ // //传递地址,指针接收 // Swap(&a[chile], &a[parent]); // //更新父母和孩子的下标 // chile = parent; // parent = (chile - 1) / 2; //} } // 堆的插入 void HeapPush(Heap* hp, HPDataType x) { assert(hp); //判断空间大小,并且开辟空间 if (hp->_capacity == hp->_size) { int newcapacity = hp->_a == 0 ? 4 : hp->_capacity * 2; HPDataType* tmp = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * newcapacity); if (tmp == NULL) { perror("HeapPush:tmp:"); exit(1); } hp->_a = tmp; hp->_capacity = newcapacity; } //插入 hp->_a[hp->_size] = x; hp->_size++; //向上调整 AdjustUp(hp->_a, hp->_size - 1); }
AdjustUp
函数用于在插入新元素后,保持最小堆的性质。它接收一个数组a
和一个元素的索引child
。这个元素通常是刚刚插入到数组中的,并且可能违反了堆的性质(即子节点的值可能小于其父节点的值)。
parent
变量用于存储当前child
元素的父节点索引。- 循环会一直执行,直到
child
元素不再违反堆的性质或者child
元素已经是根节点(此时child
为 0)。- 如果子节点的值小于父节点的值,那么通过
Swap
函数交换这两个元素的位置,并更新child
和parent
以继续进行比较。
HeapPush
函数用于将一个新元素x
插入到堆hp
中,并保持堆的性质。
- 首先,使用
assert
确保传入的Heap
结构体指针hp
不是NULL
。- 如果当前堆的
_size
等于_capacity
,意味着堆已经满了,需要扩展空间。通过realloc
函数为数组分配新的更大的空间。新的空间大小是当前容量的两倍或至少为 4(如果数组尚未分配空间)。- 如果
realloc
失败,会打印错误并退出程序。- 将新元素
x
插入到数组的末尾,即索引hp->_size
处,并增加_size
。- 最后,调用
AdjustUp
函数来调整堆,确保插入新元素后的堆仍然满足最小堆的性质。
删除数据(小堆)
数据的删除的逻辑这里我们不能--,或者++什么的,这些会导致堆变的不是堆
所以我们需要的是逻辑:
1,数组的首元素和尾元素进行交换
2,删除尾元素
3,首元素向下调整(这里看清楚了,这里是选取最小的数值进行交换)
代码实现
//向下调整 void AdjustDown(HPDataType* a, int size, int parent) { //我们假设左孩子的数值是最小的 int chile = (parent * 2) + 1; while (chile < size) { //我们需要判断,右孩子是不是存在,并且筛选出最小的数值 if (chile + 1 < size && a[chile] > a[chile + 1]) { ++chile; } //交换条件 if (a[chile] < a[parent]) { Swap(&a[chile], &a[parent]); parent = chile; chile = (chile * 2) + 1; } else { break; } } } // 堆的删除 void HeapPop(Heap* hp) { //首先删除之前不能为null assert(hp && hp->_size > 0); //删除我们删除的是堆头元素,删除尾部是没有意义的 //1,进行交换 //2,删除尾部 //3,向下排序 Swap(&hp->_a[0], &hp->_a[hp->_size - 1]); hp->_size--; //传递的参数分别是,数组的头,头下标(parent是需要变化的,所以需要传递一个参数) AdjustDown(hp->_a, hp->_size, 0); }
AdjustDown
函数用于在堆中移除根节点后,重新调整堆以保持堆的性质。它接收一个数组a
,数组的当前大小size
,以及需要向下调整的节点的父节点索引parent
。
child
变量用于存储当前parent
节点的左孩子的索引。- 循环会一直执行,直到
child
变量超出数组的边界或者当前节点不再违反堆的性质。- 如果右孩子存在且右孩子的值小于左孩子的值,那么将
child
更新为右孩子的索引。- 如果子节点的值小于父节点的值,那么通过
Swap
函数交换这两个元素的位置,并更新parent
和child
以继续进行比较。
HeapPop
函数用于从堆中移除根节点(即堆顶元素),这通常是堆中最大或最小的元素。这个函数适用于最大堆或最小堆,具体取决于堆的性质。
- 首先,使用
assert
确保传入的Heap
结构体指针hp
不是NULL
,并且堆中至少有一个元素。- 将堆顶元素(索引 0)与最后一个元素交换位置。这样做的原因是,移除最后一个元素的成本较低,因为它不需要调整堆。
- 减少
_size
,表示堆中的元素数量减少了一个。- 调用
AdjustDown
函数,传入数组的头部、新的堆大小和根节点索引(0),以调整堆并保持堆的性质。
取堆顶的数据(小堆)
// 取堆顶的数据 HPDataType HeapTop(Heap* hp) { assert(hp && hp->_size > 0); return hp->_a[0]; }
参数接收:函数接收一个指向
Heap
结构体的指针hp
。断言检查:
assert(hp);
:确保传入的hp
不是NULL
。如果hp
是NULL
,assert
将触发断言失败,这通常会导致程序终止。assert(hp->_size > 0);
:确保堆不为空(即_size
大于 0)。如果_size
不大于 0,说明堆为空,此时没有元素可以返回,assert
同样会触发断言失败。返回堆顶元素:
return hp->_a[0];
:返回位于数组_a
第一个元素的值,这个元素在堆的表示中对应堆顶元素。
堆的数据个数(小堆)
// 堆的数据个数 int HeapSize(Heap* hp) { assert(hp); return hp->_size; }
参数接收:函数接收一个指向
Heap
结构体的指针hp
。断言检查:
assert(hp);
:确保传入的hp
不是NULL
。如果hp
是NULL
,assert
将触发断言失败,这通常会导致程序终止。返回元素数量:
return hp->_size;
:返回Heap
结构体中的_size
成员的值。_size
成员表示堆中当前存储的数据元素的数量。
堆的判空(小堆)
// 堆的判空 int HeapEmpty(Heap* hp) { assert(hp); return hp->_size == 0; }
assert(hp);
:这行代码使用assert
宏来确保传入的hp
不是NULL
。如果hp
是NULL
,assert
将触发一个断言失败,这通常会导致程序终止。assert
是一种运行时检查,用于调试目的,确保代码的正确性。
return hp->_size == 0;
:这行代码返回hp
指向的Heap
结构体的_size
成员是否等于 0。_size
成员表示堆中元素的数量。如果_size
等于 0,表示堆中没有任何元素,函数返回 1(在 C 语言中,非零值被视为真),表示堆为空。如果_size
不等于 0,函数返回 0,表示堆不为空。
堆的实现代码(小堆)
#include"Heap.h" //堆的初始化 void HeapInit(Heap* hp) { //初始化这里不开辟空间 hp->_a = NULL; hp->_capacity = hp->_size = 0; } // 堆的销毁 void HeapDestory(Heap* hp) { assert(hp); //这里不需要循环释放,因为这里是数组实现堆的 free(hp->_a); hp->_a = NULL; hp->_capacity = hp->_size = 0; } //交换 void Swap(HPDataType* p1, HPDataType* p2) { HPDataType tmp = *p1; *p1 = *p2; *p2 = tmp; } //向上调整 void AdjustUp(HPDataType* a, int chile) { //获取父母所在位置 int parent = (chile - 1) / 2; while (chile > 0)//这个循环条件不会越界,其余两个循环条件都会导致越界,但是也会正常运行 { if (a[chile] < a[parent]) { //传递地址,指针接收 Swap(&a[chile], &a[parent]); //更新父母和孩子的下标 chile = parent; parent = (chile - 1) / 2; } else { break; } } 这个循环条件产生了越界,但是 printf("%d ", ps._a[-1]);(特别大) printf("%d ", ps._a[100]);(特别小) //while (a[chile] < a[parent]) //{ // //传递地址,指针接收 // Swap(&a[chile], &a[parent]); // //更新父母和孩子的下标 // chile = parent; // parent = (chile - 1) / 2; //} } // 堆的插入 void HeapPush(Heap* hp, HPDataType x) { assert(hp); //判断空间大小,并且开辟空间 if (hp->_capacity == hp->_size) { int newcapacity = hp->_a == 0 ? 4 : hp->_capacity * 2; HPDataType* tmp = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * newcapacity); if (tmp == NULL) { perror("HeapPush:tmp:"); exit(1); } hp->_a = tmp; hp->_capacity = newcapacity; } //插入 hp->_a[hp->_size] = x; hp->_size++; //向上调整 AdjustUp(hp->_a, hp->_size - 1); } //向下调整 void AdjustDown(HPDataType* a, int size, int parent) { //我们假设左孩子的数值是最小的 int chile = (parent * 2) + 1; while (chile < size) { //我们需要判断,右孩子是不是存在,并且筛选出最小的数值 if (chile + 1 < size && a[chile] > a[chile + 1]) { ++chile; } //交换条件 if (a[chile] < a[parent]) { Swap(&a[chile], &a[parent]); parent = chile; chile = (chile * 2) + 1; } else { break; } } } // 堆的删除 void HeapPop(Heap* hp) { //首先删除之前不能为null assert(hp && hp->_size > 0); //删除我们删除的是堆头元素,删除尾部是没有意义的 //1,进行交换 //2,删除尾部 //3,向下排序 Swap(&hp->_a[0], &hp->_a[hp->_size - 1]); hp->_size--; //传递的参数分别是,数组的头,头下标(parent是需要变化的,所以需要传递一个参数) AdjustDown(hp->_a, hp->_size, 0); } // 取堆顶的数据 HPDataType HeapTop(Heap* hp) { assert(hp && hp->_size > 0); return hp->_a[0]; } // 堆的数据个数 int HeapSize(Heap* hp) { assert(hp); return hp->_size; } // 堆的判空 int HeapEmpty(Heap* hp) { assert(hp); return hp->_size == 0; }
堆的实现代码(大堆)
大堆 //堆的初始化 void HeapInit(Heap* hp) { hp->_a = NULL; hp->_capacity = hp->_size = 0; } // 堆的销毁 void HeapDestory(Heap* hp) { assert(hp); free(hp->_a); hp->_capacity = hp->_size = 0; } void Swap(HPDataType* p1, HPDataType* p2) { HPDataType tmp = *p1; *p1 = *p2; *p2 = tmp; } //向上排序 void AdjustUp(HPDataType* a ,int chile) { int parent = (chile - 1) / 2; while (chile > 0) { if (a[chile] > a[parent]) { Swap(&a[chile], &a[parent]); chile = parent; parent= (chile - 1) / 2; } else { break; } } } // 堆的插入 void HeapPush(Heap* hp, HPDataType x) { if (hp->_capacity == hp->_size) { int new_capacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2; HPDataType* tmp = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * new_capacity); if (tmp == NULL) { perror(" "); exit(1); } hp->_capacity = new_capacity; hp->_a = tmp; } //插入数值 hp->_a[hp->_size] = x; hp->_size++; //向上排序 //首元素地址,孩子所在地址 AdjustUp(hp->_a, hp->_size - 1); } //向下调整(大堆) void AdjustDown(HPDataType* a, int n, int parent) { int chile = parent * 2 + 1; //循环条件不能是父亲,不然会导致越界 while (chile < n) { //三个孩子进行比较 if (chile + 1 < n && a[chile] < a[chile + 1]) { chile++; } if (a[chile] > a[parent]) { Swap(&a[chile], &a[parent]); parent = chile; chile = parent * 2 + 1; } else { break; } } } // 堆的删除 void HeapPop(Heap* hp) { Swap(&hp->_a[0], &hp->_a[hp->_size - 1]); hp->_size--; //数值的长度,和父亲下标 AdjustDown(hp->_a, hp->_size, 0); } // 取堆顶的数据 HPDataType HeapTop(Heap* hp) { assert(hp && hp->_size > 0); return hp->_a[0]; } // 堆的数据个数 int HeapSize(Heap* hp) { assert(hp); return hp->_size; } // 堆的判空 int HeapEmpty(Heap* hp) { assert(hp); return hp->_size == 0; }
堆排序问题
需要在数组里面进行排序,我们可以采取堆排序对其解决问题
版本1:
创建一个数组等大的堆,把数组里面的数值输入到堆里面进行堆排序,但是这样的弊端就是,不是顺序排序
版本2:
每次我们取堆顶然后打印,最后出堆,循环
弊端就是这样是时间复杂度我们发现还是o(n),没有必要那么麻烦半天时间复杂度还是这样
版本3:(推荐)
在数组上面进行排序,直接输出顺序排序
逻辑讲解
1,需要在数组里面进行排序,我们可以采取在数组建堆
2,然后交换收尾元素,每次调整的数值减少1
讲解逻辑
首先我们需要知道,
如果我们需要排序的是降序,我们就需要建立小堆
如果我们需要排序的是升序,我们就需要建立大堆
如果我们需要的是升序建立小堆的话
如果我们采取相反的方式的话,就会导致:(出现两个根)
首先n个数建小堆,固定第一个值是最小值
剩下的n-1个数再建堆
效率就很差了如果相反的话,会导致根节点变化,从而导致逻辑混乱,数组里面的数值少的时候是不明显的,但是多的时候就不行了
逻辑实现图解
代码实现
//向下调整(大堆) void AdjustDown(HPDataType* a, int n, int parent) { int chile = parent * 2 + 1; //循环条件不能是父亲,不然会导致越界 while (chile < n) { //三个孩子进行比较 if (chile + 1 < n && a[chile] < a[chile + 1]) { chile++; } if (a[chile] > a[parent]) { Swap(&a[chile], &a[parent]); parent = chile; chile = parent * 2 + 1; } else { break; } } } //堆排序数组内进行调整解决 void sort_test(int* a, int sz) { //放出来的是小堆,所以我们只能排序降序,这样逻辑更融洽 //建堆 for (int i = 0; i < sz; i++) { AdjustUp(a, i); } //交换排序 把首个元素和最后一个交换进行排序 每次-- while (sz > 0) { Swap(&a[0], &a[sz - 1]); AdjustDown(a, sz - 1, 0); sz--; } }
这个
sort_test
函数实现了一个堆排序算法,它接收一个整数数组a
和它的大小sz
:
建堆:首先,函数通过调用
AdjustUp
函数来构建一个小顶堆(最小堆)。建堆过程是从最后一个非叶子节点开始向上调整,直到堆顶。交换和排序:在建堆之后,函数进入一个循环,每次循环中,它将堆顶元素(当前堆中的最小元素)与当前堆的最后一个元素交换。然后,堆的大小减少 1,并且对剩余的堆进行向下调整以保持最小堆性质。
循环结束:循环继续进行,直到堆的大小减小到 0。最终,数组
a
将被排序为降序。
top_k问题
top_k问题时间复杂度的计算
这里提前说明,时间复杂度的计算的目的是来计算向上调整的更优还是向下调整更优,从肉眼看的话向下调整优于向上调整,接下来我们进行时间复杂度的计算。
此时我们会用到等比数列求和以及裂项相消
如图
首先我们假设求的是满二叉树,我们求节点的个数
满二叉树节点个数
建堆问题:
建堆的话往往的倒数第一个非叶子结点建堆,会时间复杂度最优解:也就是
在构建堆(尤其是二叉堆)时,从最后一个非叶子节点开始进行调整是时间复杂度最优解的原因是,这种方法可以减少不必要的调整操作。
为什么从最后一个非叶子节点开始?
叶子节点:在完全二叉树中,叶子节点不包含任何子节点,因此不需要进行调整。
非叶子节点:从最后一个非叶子节点开始,向上逐个进行调整,可以确保每个节点在调整时,其子树已经是堆结构。这样可以减少调整的深度,因为每个节点最多只需要与其子节点交换一次。
减少调整次数:如果从根节点开始调整,那么每个节点可能需要多次调整才能达到堆的性质,特别是那些位于树底部的节点。而从底部开始,每个节点只需要调整一次即可。
时间复杂度分析
构建堆的过程涉及对每个非叶子节点进行调整。对于一个具有 𝑛n 个节点的完全二叉树:
叶子节点:有 ⌈𝑛/2⌉⌈n/2⌉ 个叶子节点,它们不需要调整。
非叶子节点:有 ⌊𝑛/2⌋⌊n/2⌋ 个非叶子节点,需要进行调整。
对于非叶子节点,从最后一个非叶子节点开始向上调整,每个节点最多只需要进行 log𝑘logk(𝑘k 是节点的深度)次交换。但是,由于树的结构,底部的节点不需要进行多次交换,因此整个调整过程的时间复杂度比 𝑂(𝑛log𝑛)O(nlogn) 要低。
实际上,构建堆的时间复杂度是 𝑂(𝑛)O(n),这是因为:
从最后一个非叶子节点开始,每个节点的调整次数与其深度成反比。
根节点的调整次数最多,但只需要一次。
越往下,节点的深度越小,但需要调整的节点数量越多。
总结
从最后一个非叶子节点开始建堆,可以确保每个节点的调整次数与其深度成反比,从而减少总的调整次数。这种方法利用了完全二叉树的性质,使得整个建堆过程的时间复杂度达到最优,即 𝑂(𝑛)O(n)。这是构建堆的最优策略,因为它最小化了必要的调整操作,从而提高了算法的效率。
建堆复杂度讲解:(向下调整建堆计算)
如图:
这里为什么-2呢,因为我们的向下调整只是调整h-1层,第h层的节点的个数是2^h-1,所以第h-1层自然就是-2
所以我们发现,建堆的时候我们h-1高度的节点的个数相加得出的结果
为T(n)
所以我们进行计算
从而得出时间复杂度,为什么时间复杂度是高度,因为向下调整的时候,我们循环终止条件是循环的高度,也就是当父亲节点不小于sz的时候,所以计算出高度也就计算出了时间复杂度
建堆复杂度讲解:(向上调整建堆计算)
如图:
计算图解
所以我们得出结论,这里多了n次
对比
向上调整(
AdjustUp
)和向下调整(AdjustDown
)的时间复杂度通常与堆的高度相关,即 log𝑘logk,其中 𝑘k 是堆中元素的数量。然而,在特定情况下,特别是在构建堆的过程中,这些操作的总时间复杂度可以是 𝑂(𝑛)O(n),这里的 𝑛n 是堆中元素的数量。单个操作的时间复杂度:
向上调整 (
AdjustUp
):对于单个元素,向上调整的时间复杂度是 𝑂(log𝑘)O(logk),因为它可能需要从叶子节点一直调整到根节点,最多涉及 log𝑘logk 层的比较和交换。向下调整 (
AdjustDown
):同样,对于单个元素,向下调整的时间复杂度也是 𝑂(log𝑘)O(logk),因为它可能需要从根节点调整到叶子节点,同样最多涉及 log𝑘logk 层的比较和交换。构建堆的总时间复杂度:
当我们讨论构建一个包含 𝑛n 个元素的堆时,所有元素的向上调整操作的总时间复杂度是 𝑂(𝑛)O(n)。这是因为:
树的非叶子节点大约是 𝑛/2n/2(因为叶子节点也是 𝑛/2n/2 左右)。
每个非叶子节点的调整操作最多涉及 log𝑘logk 的时间,但是由于树的结构,从根到叶的路径上的节点数量总和大致是 𝑛n。
因此,所有节点的向上调整操作加起来的时间复杂度是 𝑂(𝑛)O(n)。
为什么是 𝑂(𝑛)O(n) 而不是 𝑂(𝑛log𝑘)O(nlogk)?
树的结构特性:在完全二叉树中,每个层级的节点数量是指数增长的。从根节点(1个节点)到第二层(2个节点),再到第三层(4个节点),等等。因此,较低层级的节点数量远多于较高层级的节点数量。
调整深度:根节点的调整可能需要 log𝑘logk 的时间,但较低层级的节点只需要较少的调整时间。由于底部层级的节点数量较多,它们较短的调整时间在总体上对总时间复杂度的贡献较小。
总结:
对于单个元素,向上调整和向下调整的时间复杂度是 𝑂(log𝑘)O(logk)。
在构建堆的过程中,所有元素的向上调整操作的总时间复杂度是 𝑂(𝑛)O(n),而不是 𝑂(𝑛log𝑘)O(nlogk),这是由于完全二叉树的结构特性和调整操作的分布。
因此,向上调整和向下调整在构建堆的过程中的总时间复杂度是 𝑂(𝑛)O(n),而不是 𝑂(log𝑛)O(logn)。这个线性时间复杂度是构建堆算法的一个重要特性,使得它在处理大量数据时非常高效。
向上调整和向下调整虽然最后计算的都是O(N)
但是满二叉树最后一层占据一半的节点
所以我们得出结论,向下调整的复杂度优于向上调整的复杂度
top_k问题的实现逻辑
1,首先我们创建一个文件,写入随机数值1000w个
2,如果需要读取文件里面最大的10个数值,那么我们就需要,创建一个小堆
原因:
这样的话,输入数值的时候,如果读取的数值比堆顶大,就会替换堆顶从而进堆,然后进行堆排序。
3,在读取文件的时候,我们需要读取一个接收一个,然后进行数值的对比,从而进行交换。
4,最后打印最大的数值
5,备注:我们如何判断我们的找到的最大的前十个数值的正确的,
也是很简单的,我们设定的随机数值是10000以内的,然后设定完之后,我们不调用,进入TXT里面更改一些数值。设定一些大于一万的数值,此时我们就可以发现我们筛选的数值对不对。
当然如果我们需要找最小的数值,那么我们设定数值最好为-1,因为十万个数值,很可能是有很多0的。但是我们肉眼看不出来。
top_k计算的代码实现
//进行计算 void TOP_K() { int k = 10; //scanf("%d", &k); FILE* ps = fopen("data.txt", "r"); if (ps == NULL) { perror("Error:opening:file"); exit(1); } //创建空间存储 int* tmp = (int*)malloc(sizeof(int) * k); if (tmp == NULL) { perror("TOP_K():Heap* tmp:error"); exit(1); } //读取个数 for (int i = 0; i < 10; i++) { fscanf(ps, "%d", &tmp[i]); } // 建堆,从最后一个非叶子节点开始建堆, // 这里的 -1-1 实际上看起来像是一个错误。 // 通常,当我们需要找到最后一个非叶子节点的索引以开始建堆过程时,我们会从倒数第二个节点开始(因为数组索引从0开始)。对于大小为 k 的数组,最后一个非叶子节点的索引计算如下: // 简单的说就是,k是数值,我们需要传参传递是下标,找到父亲节点需要减去1 除以2 所以就有了-2的情况 for (int i = (k - 1 - 1) / 2; i >= 0; i--) { AdjustDown(tmp, k, i); } //排序 int val = 0; int ret = fscanf(ps, "%d", &val); while (ret != EOF) { if (tmp[0] < val) { tmp[0] = val; AdjustDown(tmp, k, 0); } ret = fscanf(ps, "%d", &val); } //打印 for (int i = 0; i < k; i++) { printf("%d ", tmp[i]); } fclose(ps); }
top_k完整代码
//TOP_K问题的实现 小堆寻找最大值 //创建随机数值 void TOP_K_fopen_w() { FILE* ps = fopen("data.txt", "w"); if (ps == NULL) { perror("FILE* ps :fopen:error"); exit(1); } srand(time(0)); for (int i = 0; i < 100000; i++) { int s = rand() % 10000; fprintf(ps, "%d\n", s); } fclose(ps); } //进行计算 void TOP_K() { int k = 10; //scanf("%d", &k); FILE* ps = fopen("data.txt", "r"); if (ps == NULL) { perror("Error:opening:file"); exit(1); } //创建空间存储 int* tmp = (int*)malloc(sizeof(int) * k); if (tmp == NULL) { perror("TOP_K():Heap* tmp:error"); exit(1); } //读取个数 for (int i = 0; i < 10; i++) { fscanf(ps, "%d", &tmp[i]); } // 建堆,从最后一个非叶子节点开始建堆, // 这里的 -1-1 实际上看起来像是一个错误。 // 通常,当我们需要找到最后一个非叶子节点的索引以开始建堆过程时,我们会从倒数第二个节点开始(因为数组索引从0开始)。对于大小为 k 的数组,最后一个非叶子节点的索引计算如下: // 简单的说就是,k是数值,我们需要传参传递是下标,找到父亲节点需要减去1 除以2 所以就有了-2的情况 for (int i = (k - 1 - 1) / 2; i >= 0; i--) { AdjustDown(tmp, k, i); } //排序 int val = 0; int ret = fscanf(ps, "%d", &val); while (ret != EOF) { if (tmp[0] < val) { tmp[0] = val; AdjustDown(tmp, k, 0); } ret = fscanf(ps, "%d", &val); } //打印 for (int i = 0; i < k; i++) { printf("%d ", tmp[i]); } fclose(ps); }
链式二叉树
链式二叉树的概念:
链式二叉树解决的是非完全二叉树解决不了的问题
什么意思呢,简单的说就是,链式二叉树
可以是下面三种二叉树
但是非链式二叉树只能是前两种
链式二叉树的存储
节点结构:首先定义一个结构体或类来表示二叉树的节点。每个节点通常包含三个部分:
- 存储数据的成员变量(例如,
_data
)。- 指向其左子节点的指针(例如,
_left
)。- 指向其右子节点的指针(例如,
_right
)。链式二叉树的存储方式提供了很高的灵活性,可以轻松地添加和删除节点,同时也使得树结构的实现更加直观和易于操作。
前序/中序/后序遍历
讲解链式二叉树之前我们需要先了解一下,前序/中序/后序,因为在初阶数据结构里,链式二叉树我们是用递归的方式来实现的,非递归的方式,在后续篇章会进行讲解。
这里我们上图,假设是这一张图
前序遍历
前序遍历的顺序是:首先访问根节点,然后递归地进行左子树的前序遍历,最后递归地进行右子树的前序遍历。
遍历顺序:根-左-右
步骤:
- 访问当前节点。
- 遍历左子树。
- 遍历右子树。
示例: 假设有如下的二叉树:
关于前序遍历,我们需要把空节点也看出来
如图
所以根据遍历顺便,我们应该是
1 2 3 NULL NULL NULL 4 5 NULL NULL 6 NULL NULL
中序遍历
中序遍历的顺序是:首先递归地进行左子树的中序遍历,然后访问根节点,最后递归地进行右子树的中序遍历。
遍历顺序:左-根-右
步骤:
- 遍历左子树。
- 访问当前节点。
- 遍历右子树。
正确的是
后序遍历
后序遍历的顺序是:首先递归地进行左子树的后序遍历,然后递归地进行右子树的后序遍历,最后访问根节点。
遍历顺序:左-右-根
步骤:
- 遍历左子树。
- 遍历右子树。
- 访问当前节点。
正确的是
总结
链式二叉树的实现
创建文件
定义链式二叉树结构
typedef int BTDataType; typedef struct BinaryTreeNode { BTDataType _data; struct BinaryTreeNode* _left; struct BinaryTreeNode* _right; }BTNode;
解释:
typedef int BTDataType;
这行代码使用typedef
关键字定义了一个新的别名BTDataType
,它是int
类型的别名。这意味着在代码中,你可以使用BTDataType
作为int
类型数据的一个更有意义的别名。
typedef struct BinaryTreeNode
这行代码开始定义一个名为BinaryTreeNode
的新结构体类型。struct BinaryTreeNode
是结构体的名称,它将用于表示二叉树中的节点。
BTDataType _data; struct BinaryTreeNode* _left; struct BinaryTreeNode* _right; }
这个大括号内的代码定义了BinaryTreeNode
结构体的具体内容:
BTDataType _data;
定义了一个名为_data
的成员变量,它用于存储节点中的数据。由于使用了之前定义的BTDataType
,所以这个成员变量是int
类型的。struct BinaryTreeNode* _left;
定义了一个名为_left
的成员变量,它是一个指向BinaryTreeNode
类型的指针,用于指向当前节点的左子节点。struct BinaryTreeNode* _right;
定义了一个名为_right
的成员变量,它也是一个指向BinaryTreeNode
类型的指针,用于指向当前节点的右子节点。
BTNode;
这行代码为BinaryTreeNode
结构体定义了一个易记的别名BTNode
。这样,在代码中就可以使用BTNode
来声明二叉树的节点。总结来说,这段代码定义了一个用于表示链式二叉树节点的结构体
BTNode
,其中包含一个整型数据_data
和两个指向相同结构体类型的指针_left
和_right
,分别用于存储节点的数据和链接到左右子节点。通过这种定义,可以方便地创建和管理二叉树数据结构。
二叉树的初始化
//二叉树的初始化 BTNode* BuyNode(BTDataType x) { BTNode* newnode = (BTNode*)malloc(sizeof(BTNode)); if (newnode == NULL) { perror("BinaryTreeInit:newnode:"); exit(1); } newnode->_data = x; newnode->_left = NULL; newnode->_right = NULL; return newnode; }
解释:
BTNode* BuyNode(BTDataType x)
- 这是函数的声明行,定义了一个名为
BuyNode
的函数,它接收一个类型为BTDataType
(之前定义的int
类型别名)的参数x
,并返回一个指向BTNode
类型的指针。
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
- 这行代码使用
malloc
函数为新的二叉树节点分配内存。sizeof(BTNode)
计算BTNode
类型的大小,确保分配足够的内存。分配成功后,newnode
指针将指向这块新分配的内存。
if (newnode == NULL)
- 这行代码检查
malloc
是否成功分配了内存。如果newnode
为NULL
,表示内存分配失败。
perror("BinaryTreeInit:newnode:");
- 如果内存分配失败,使用
perror
函数输出错误信息到标准错误。"BinaryTreeInit:newnode:"
是自定义的错误前缀,后跟系统定义的错误信息。
exit(1);
- 紧接着,使用
exit
函数以状态码1
退出程序。状态码1
通常表示程序因错误而终止。
newnode->_data = x;
- 如果内存分配成功,这行代码将参数
x
的值赋给新节点的_data
成员变量,从而初始化节点存储的数据。
newnode->_left = NULL;
- 这行代码将新节点的
_left
指针设置为NULL
,表示当前没有左子节点。
newnode->_right = NULL;
- 类似地,这行代码将新节点的
_right
指针设置为NULL
,表示当前没有右子节点。
return newnode;
- 最后,函数返回指向新创建并初始化的
BTNode
节点的指针。总结来说,
BuyNode
函数负责创建一个新的二叉树节点,初始化其数据和子节点指针,如果内存分配失败,则输出错误信息并终止程序。这个函数是构建二叉树时用于生成节点的基本工具。这里的关键点是认识到 ,左右节点的存在
二叉树的销毁
这里就采取了递归,开始上难度了,这里我先不做讲解,模拟创建二叉树之后我们进行讲解递归
// 二叉树销毁 void BinaryTreeDestory(BTNode* root) { if (root == NULL) { return; } BinaryTreeDestory(root->_left); BinaryTreeDestory(root->_right); free(root); }
解释:
void BinaryTreeDestory(BTNode* root)
- 这是函数的声明行,定义了一个名为
BinaryTreeDestory
的函数,它接收一个指向BTNode
类型的指针root
作为参数。该函数没有返回值(void
类型)。
if (root == NULL)
- 这行代码检查传入的
root
指针是否为NULL
。如果是NULL
,表示二叉树为空或已经被销毁,因此函数直接返回。
BinaryTreeDestory(root->_left);
- 如果
root
不是NULL
,这行代码首先递归调用BinaryTreeDestory
函数,传入当前节点的左子节点root->_left
作为参数。这样做会先销毁左子树。
BinaryTreeDestory(root->_right);
- 接着,这行代码递归调用
BinaryTreeDestory
函数,传入当前节点的右子节点root->_right
作为参数。这会销毁右子树。
free(root);
- 在左子树和右子树都被销毁之后,这行代码使用
free
函数释放当前节点root
所占用的内存。总结来说,
BinaryTreeDestory
函数通过递归的方式,先销毁二叉树的左子树和右子树,然后释放根节点的内存。这种销毁方式确保了二叉树中的所有节点都会被释放,避免了内存泄漏。需要注意的是,在使用这种销毁方式时,确保在销毁二叉树后不再使用任何指向该树的指针,因为整个树的内存已经被释放。
模拟简易链式二叉树
//构建二叉树 void teer() { BTNode* node1 = BuyNode(1); BTNode* node2 = BuyNode(2); BTNode* node3 = BuyNode(3); BTNode* node4 = BuyNode(4); BTNode* node5 = BuyNode(5); BTNode* node6 = BuyNode(6); //BTNode* node7 = BuyNode(7); node1->_left = node2; node2->_left = node3; node1->_right = node4; node4->_left = node5; node4->_right = node6; //node2->_right = node7; } int main() { //构建二叉树 teer(); return 0; }
这里我们模拟实现一个二叉树
就是这样
前序遍历实现
// 二叉树前序遍历 void BinaryTreePrevOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } printf("%d ", root->_data); BinaryTreePrevOrder(root->_left); BinaryTreePrevOrder(root->_right); }
解释:
void BinaryTreePrevOrder(BTNode* root)
- 这是函数的声明行,定义了一个名为
BinaryTreePrevOrder
的函数,它接收一个指向BTNode
类型的指针root
作为参数。该函数没有返回值(void
类型)。
if (root == NULL)
- 这行代码检查传入的
root
指针是否为NULL
。如果是NULL
,表示当前节点为空,不需要遍历。
printf("NULL ");
- 如果当前节点为空,打印字符串
"NULL "
到标准输出。这是一种表示空节点的常见做法,但在实际应用中,通常会省略这一步,或者用其他方式处理空节点。
printf("%d ", root->_data);
- 如果当前节点不为空,这行代码会打印当前节点的
_data
成员变量的值。%d
是格式化输出的占位符,表示后面跟着的参数是一个整数。
BinaryTreePrevOrder(root->_left);
- 这行代码递归调用
BinaryTreePrevOrder
函数,传入当前节点的左子节点root->_left
作为参数。这将执行左子树的前序遍历。
BinaryTreePrevOrder(root->_right);
- 最后,这行代码递归调用
BinaryTreePrevOrder
函数,传入当前节点的右子节点root->_right
作为参数。这将执行右子树的前序遍历。总结来说,
BinaryTreePrevOrder
函数通过递归的方式实现了二叉树的前序遍历。它首先打印当前节点的值,然后递归地遍历左子树,最后递归地遍历右子树。这种遍历方式在树的遍历、搜索、复制等操作中非常有用。图解:
中序遍历实现
// 二叉树中序遍历 void BinaryTreeInOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } BinaryTreeInOrder(root->_left); printf("%d ", root->_data); BinaryTreeInOrder(root->_right); }
解释:
void BinaryTreeInOrder(BTNode* root)
- 这是函数的声明行,定义了一个名为
BinaryTreeInOrder
的函数,它接收一个指向BTNode
类型的指针root
作为参数。该函数没有返回值(void
类型)。
if (root == NULL)
- 这行代码检查传入的
root
指针是否为NULL
。如果是NULL
,表示当前节点为空,不需要遍历。
printf("NULL ");
- 如果当前节点为空,打印字符串
"NULL "
到标准输出。这通常用于表示空节点,但在实际的中序遍历中,通常会忽略空节点而不是打印它们。
BinaryTreeInOrder(root->_left);
- 这行代码递归调用
BinaryTreeInOrder
函数,传入当前节点的左子节点root->_left
作为参数。这将执行左子树的中序遍历。
printf("%d ", root->_data);
- 在左子树遍历之后,这行代码打印当前节点的
_data
成员变量的值。因为在左子树遍历之后访问根节点,所以对于二叉搜索树来说,这会保证按升序打印节点值。
BinaryTreeInOrder(root->_right);
- 最后,这行代码递归调用
BinaryTreeInOrder
函数,传入当前节点的右子节点root->_right
作为参数。这将执行右子树的中序遍历。
后序遍历实现
// 二叉树后序遍历 void BinaryTreePostOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } BinaryTreePostOrder(root->_left); BinaryTreePostOrder(root->_right); printf("%d ", root->_data); }
解释:
void BinaryTreePostOrder(BTNode* root)
- 这是函数的声明行,定义了一个名为
BinaryTreePostOrder
的函数,它接收一个指向BTNode
类型的指针root
作为参数。该函数没有返回值(void
类型)。
if (root == NULL)
- 这行代码检查传入的
root
指针是否为NULL
。如果是NULL
,表示当前节点为空,不需要遍历。
printf("NULL ");
- 如果当前节点为空,打印字符串
"NULL "
到标准输出。这通常用于表示空节点,但在实际的后序遍历中,通常会忽略空节点而不是打印它们。
BinaryTreePostOrder(root->_left);
- 这行代码递归调用
BinaryTreePostOrder
函数,传入当前节点的左子节点root->_left
作为参数。这将执行左子树的后序遍历。
BinaryTreePostOrder(root->_right);
- 在左子树遍历之后,这行代码递归调用
BinaryTreePostOrder
函数,传入当前节点的右子节点root->_right
作为参数。这将执行右子树的后序遍历。
printf("%d ", root->_data);
- 在左子树和右子树都遍历之后,这行代码打印当前节点的
_data
成员变量的值。因为这是在访问完左右子树之后的操作,所以它是后序遍历的一部分。
二叉树节点个数
什么是节点个数,也就是,全部节点个数
// 二叉树节点个数 int BinaryTreeSize(BTNode* root) { if (root == NULL) { return 0; } return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1; }
解释:
int BinaryTreeSize(BTNode* root)
- 这是函数的声明行,定义了一个名为
BinaryTreeSize
的函数,它接收一个指向BTNode
类型的指针root
作为参数。函数返回一个整数值,表示树中的节点总数。
if (root == NULL)
- 这行代码检查传入的
root
指针是否为NULL
。如果是NULL
,表示当前节点为空,不计入节点总数。
return 0;
- 如果当前节点为空,函数返回
0
。这是递归的基本情况,表示空树的节点数为0
。
return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;
- 如果当前节点不为空,这行代码递归调用自身两次:一次计算左子树的节点数
BinaryTreeSize(root->_left)
,一次计算右子树的节点数BinaryTreeSize(root->_right)
。然后将这两个调用的结果相加,并加上1
来表示当前节点本身。这样,你就得到了整棵树的节点总数。图解
二叉树叶子节点个数
什么是叶子结点个数
也就是:
// 二叉树叶子节点个数 int BinaryTreeLeafSize(BTNode* root) { if (root == NULL) return 0; if (BinaryTreeLeafSize(root->_left) == 0 && BinaryTreeLeafSize(root->_right) == 0) return 1; return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right); }
解释:
int BinaryTreeLeafSize(BTNode* root)
- 这是函数的声明行,定义了一个名为
BinaryTreeLeafSize
的函数,接收一个指向BTNode
类型的指针root
作为参数。函数返回一个整数值,表示树中叶子节点的总数。
if (root == NULL)
- 这行代码检查传入的
root
指针是否为NULL
。如果是NULL
,表示当前节点为空,不计入叶子节点的总数。
return 0;
- 如果当前节点为空,函数返回
0
。这是递归的基本情况之一,表示空树的叶子节点数为0
。
if (BinaryTreeLeafSize(root->_left) == 0 && BinaryTreeLeafSize(root->_right) == 0)
- 这行代码是一个条件语句,用于检查当前节点的左子节点和右子节点是否都为空。如果两者都为空,那么当前节点就是一个叶子节点。
return 1;
- 如果当前节点是叶子节点,即它的左右子节点都为空,那么返回
1
,表示叶子节点数为1
。
return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
- 如果当前节点不是叶子节点,这行代码递归调用自身两次:一次计算左子树的叶子节点数
BinaryTreeLeafSize(root->_left)
,一次计算右子树的叶子节点数BinaryTreeLeafSize(root->_right)
。然后将这两个调用的结果相加,得到当前子树的叶子节点总数。总结来说,
BinaryTreeLeafSize
函数通过递归的方式遍历二叉树,计算叶子节点的个数。它首先检查当前节点是否为空,如果不是空,再检查当前节点是否为叶子节点(左右子节点都为空),如果是叶子节点则计数1
,否则递归地计算左右子树的叶子节点数并相加。这种方法可以正确地计算出任意二叉树的叶子节点总数。图解
二叉树高度
高度也就是二叉树的层数
// 二叉树高度 int BinaryTreeHeight(BTNode* root) { if (root == NULL) { return 0; } int heightleft = BinaryTreeHeight(root->_left); int heightright = BinaryTreeHeight(root->_right); return heightleft >= heightright ? heightleft + 1 : heightright + 1; }
这里的关键是理解如何递归地计算左子树和右子树的高度。
int heightleft = BinaryTreeHeight(root->_left);
- 这行代码是对
BinaryTreeHeight
函数的递归调用,目的是计算当前节点的左子树的高度。root->_left
是对当前节点左子节点的引用,如果左子节点存在,就对它调用BinaryTreeHeight
函数,否则返回0
(如果左子节点为空)。
int heightright = BinaryTreeHeight(root->_right);
- 类似于上面的调用,这行代码计算当前节点的右子树的高度。
root->_right
是对当前节点右子节点的引用,同样地,如果右子节点存在,就对它调用BinaryTreeHeight
函数,否则返回0
(如果右子节点为空)。这里的关键点在于
int heightleft = BinaryTreeHeight(root->_left);
int heightright = BinaryTreeHeight(root->_right);这两行代码是递归过程中的关键步骤,它们分别独立地计算左子树和右子树的高度。由于树的高度是递归定义的,即树的高度是其左子树和右子树高度的最大值加
1
(当前节点),所以需要分别计算左右子树的高度。一旦得到
heightleft
和heightright
,函数就会决定:
- 如果
heightleft
大于或等于heightright
,则当前节点的左子树高度是决定整棵树高度的关键,因此返回heightleft + 1
。- 如果
heightright
大于heightleft
,则右子树的高度决定了整棵树的高度,因此返回heightright + 1
。这种递归方法确保了能够找到从根节点到任意叶子节点的最长路径,从而得到二叉树的准确高度。
否则会产生栈溢出的现象
下面有一题题解,我们会进行图解,因为一样,所以这里不进行图解,
先上图
二叉树查找值为x的节点
这里还是有点难度的
// 二叉树查找值为x的节点 BTNode* BinaryTreeFind(BTNode* root, BTDataType x) { if (root == NULL) return NULL; if (root->_data == x) return root; BTNode* ret1 = BinaryTreeFind(root->_left, x); if (ret1 != NULL) { return ret1; } BTNode* ret2 = BinaryTreeFind(root->_right, x); if (ret2 != NULL) { return ret2; } return NULL; }
解释:
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
- 这是函数的声明行,定义了一个名为
BinaryTreeFind
的函数。它接收两个参数:一个指向BTNode
类型的指针root
,表示二叉树的根节点;一个BTDataType
类型的值x
,表示要查找的值。函数返回一个指向BTNode
类型的指针,如果找到匹配的节点,则返回该节点的地址;如果没有找到,则返回NULL
。
if (root == NULL)
- 这行代码检查传入的
root
指针是否为NULL
。如果是NULL
,表示当前节点为空,无法找到匹配的值,因此返回NULL
。
if (root->_data == x)
- 如果当前节点不为空,这行代码比较当前节点的
_data
成员变量与给定值x
是否相等。
return root;
- 如果找到匹配的值,即当前节点的
_data
等于x
,则返回当前节点的指针。
BTNode* ret1 = BinaryTreeFind(root->_left, x);
- 如果当前节点的值不匹配,这行代码递归调用
BinaryTreeFind
函数,搜索左子树。递归调用的结果(一个节点指针)被存储在ret1
变量中。
if (ret1 != NULL)
- 接着,检查
ret1
是否不为空。如果不为空,表示在左子树中找到了匹配的节点,因此返回ret1
。
BTNode* ret2 = BinaryTreeFind(root->_right, x);
- 如果左子树中没有找到匹配的节点,这行代码递归调用
BinaryTreeFind
函数,搜索右子树。递归调用的结果(一个节点指针)被存储在ret2
变量中。
if (ret2 != NULL)
- 然后,检查
ret2
是否不为空。如果不为空,表示在右子树中找到了匹配的节点,因此返回ret2
。
return NULL;
- 如果在当前节点的左子树和右子树中都没有找到匹配的节点,则返回
NULL
。图解
二叉树第k层节点个数
// 二叉树第k层节点个数 int BinaryTreeLevelKSize(BTNode* root, int k) { if (root == NULL) return 0; if (k == 1) return 1; return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1); }
解释:
int BinaryTreeLevelKSize(BTNode* root, int k)
- 这是函数的声明行,定义了一个名为
BinaryTreeLevelKSize
的函数。它接收两个参数:一个指向BTNode
类型的指针root
,表示当前的节点(可以是任意层的节点,包括根节点);一个整型变量k
,表示要查找的目标层级。函数返回一个整数值,表示在第k
层上的节点总数。
if (root == NULL)
- 这行代码检查传入的
root
指针是否为NULL
。如果是NULL
,表示当前节点为空,无法计算节点个数,因此返回0
。
if (k == 1)
- 这行代码检查目标层级
k
是否为 1。如果是 1,表示当前节点位于第 1 层,因此返回1
,表示第 1 层有一个节点。
return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
- 如果当前节点不为空且目标层级不是第 1 层,这行代码递归地调用自身两次,分别计算左子树和右子树在第
k-1
层的节点个数。然后,将这两个调用的结果相加,得到第k
层的节点总数。总结来说,
BinaryTreeLevelKSize
函数通过递归的方式遍历二叉树,计算并返回第k
层上的节点个数。它首先检查当前节点是否为空,如果不为空且目标层级为第 1 层,则返回 1。如果不是第 1 层,则递归地计算左子树和右子树在降低一层后的节点个数,并将它们相加得到结果。这种方法可以正确地计算出任意二叉树在指定层级的节点总数。图解
层序遍历
什么是层序遍历
也就是:顾名思义,一层一层的输出
这里的关键点在于,怎么样一层一层进入,输出
那么此时我们可以采取队列的形式来进行使用,同时进队列,同时出队列
所以,这里我们是直接调用队列
队列的讲解-CSDN博客https://blog.csdn.net/Jason_from_China/article/details/138730053
#include"Queue.h" // 层序遍历 void BinaryTreeLevelOrder(BTNode* root) { // 按照顺序,放入队列里面,先进先出,后进后出的原则 // 1,创建队列,初始化 // 2,如果不为空,根节点入队列 // 3,取栈顶,打印,出队列 Queue ps; QueueInit(&ps); if (root) QueuePush(&ps, root); while (!QueueEmpty(&ps)) { BTNode* ret = QueueFront(&ps); QueuePop(&ps); printf("%d ", ret->_data); if (ret->_left) QueuePush(&ps, ret->_left); if (ret->_right) QueuePush(&ps, ret->_right); } //队列的销毁 QueueDestroy(&ps); }
解释:
#include"Queue.h"
- 这行代码包含了队列数据结构的头文件,该文件中定义了队列操作的相关函数和数据结构。
void BinaryTreeLevelOrder(BTNode* root)
- 这是函数的声明行,定义了一个名为
BinaryTreeLevelOrder
的函数,它接收一个指向BTNode
类型的指针root
作为参数,表示二叉树的根节点。
Queue ps;
- 这行代码声明了一个名为
ps
的队列变量。然而,这可能不足以正确地在某些语言中创建队列,因为队列可能需要动态分配(取决于其实现)。
QueueInit(&ps);
- 这行代码调用
QueueInit
函数来初始化队列ps
。
if (root) QueuePush(&ps, root);
- 如果根节点
root
不为空,这行代码调用QueuePush
函数将根节点入队。
while (!QueueEmpty(&ps))
- 这行代码开始一个循环,它会一直执行,直到队列变空。
QueueEmpty
函数检查队列是否为空。
BTNode* ret = QueueFront(&ps);
- 这行代码调用
QueueFront
函数获取队列前端的节点,但不从队列中移除它。
QueuePop(&ps);
- 这行代码调用
QueuePop
函数从队列中移除前端的节点。
printf("%d ", ret->_data);
- 这行代码打印当前节点
ret
的数据。
if (ret->_left) QueuePush(&ps, ret->_left);
- 如果当前节点
ret
的左子节点存在,这行代码将其入队。
if (ret->_right) QueuePush(&ps, ret->_right);
- 如果当前节点
ret
的右子节点存在,这行代码将其入队。
QueueDestroy(&ps);
- 最后,这行代码调用
QueueDestroy
函数销毁队列,释放所有分配的内存。总结来说,
BinaryTreeLevelOrder
函数使用队列来实现二叉树的层序遍历。它首先初始化一个队列,然后将根节点入队。在循环中,它取出队列前端的节点,打印其数据,然后将其左右子节点(如果存在)依次入队。这个过程一直重复,直到队列为空。这里的关键在于我们要认识到出去一个节点,进去两个节点
判断二叉树是否是完全二叉树
这里的逻辑也是采取队列的形式来进行判断,只是我们需要入空,当最后入的数值为空的时候,我们需要跳出循环,然后进行判断
// 判断二叉树是否是完全二叉树 bool BinaryTreeComplete(BTNode* root) { Queue ps; QueueInit(&ps); if (root) QueuePush(&ps, root); while (!QueueEmpty(&ps)) { BTNode* ret = QueueFront(&ps); QueuePop(&ps); if (ret == NULL) { break; } if (ret->_left) QueuePush(&ps, ret->_left); if (ret->_right) QueuePush(&ps, ret->_right); } while (!QueueEmpty(&ps)) { BTNode* ret = QueueFront(&ps); QueuePop(&ps); if (ret != NULL) { QueueDestroy(&ps); return false; } } //队列的销毁 QueueDestroy(&ps); return true; }
#include"Queue.h"
- 包含队列操作的头文件。
bool BinaryTreeComplete(BTNode* root)
- 函数声明,返回类型为
bool
,表示最终的布尔结果(是或不是完全二叉树)。
Queue ps;
- 声明一个队列
ps
。注意:这可能不足以在某些语言中创建队列,因为队列可能需要动态分配。
QueueInit(&ps);
- 初始化队列。
if (root) QueuePush(&ps, root);
- 如果根节点不为空,则将其入队。
第一个
while
循环:
- 使用队列进行层序遍历,访问所有节点。
BTNode* ret = QueueFront(&ps);
- 获取队列前端的节点。
QueuePop(&ps);
- 将队列前端的节点出队。
if (ret == NULL) { break; }
- 如果节点为
NULL
,则退出循环。这个条件永远不会满足,因为NULL
节点不会被入队。
if (ret->_left) QueuePush(&ps, ret->_left);
- 如果存在左子节点,则将其入队。
if (ret->_right) QueuePush(&ps, ret->_right);
- 如果存在右子节点,则将其入队。
第二个
while
循环:
- 这个循环的目的是检查队列中是否还有非
NULL
节点。
BTNode* ret = QueueFront(&ps);
- 再次获取队列前端的节点。
QueuePop(&ps);
- 再次将队列前端的节点出队。
if (ret != NULL) { QueueDestroy(&ps); return false; }
- 如果节点不为
NULL
,销毁队列并返回false
。这部分逻辑是错误的,因为按照完全二叉树的定义,队列中应该只有NULL
节点。
QueueDestroy(&ps);
- 销毁队列。
return true;
- 返回
true
表示树是完全二叉树。
链式二叉树代码总结
Link_Teer.h
#define _CRT_SECURE_NO_WARNINGS 1 #pragma once #include<assert.h> #include<stdio.h> #include<stdlib.h> #include<stdbool.h> typedef int BTDataType; typedef struct BinaryTreeNode { BTDataType _data; struct BinaryTreeNode* _left; struct BinaryTreeNode* _right; }BTNode; //二叉树的初始化 BTNode* BuyNode(BTDataType x); // 二叉树前序遍历 void BinaryTreePrevOrder(BTNode* root); // 二叉树中序遍历 void BinaryTreeInOrder(BTNode* root); // 二叉树后序遍历 void BinaryTreePostOrder(BTNode* root); // 二叉树节点个数 int BinaryTreeSize(BTNode* root); // 二叉树叶子节点个数 int BinaryTreeLeafSize(BTNode* root); // 二叉树高度 int BinaryTreeHeight(BTNode* root); // 二叉树查找值为x的节点 BTNode* BinaryTreeFind(BTNode* root, BTDataType x); // 二叉树第k层节点个数 int BinaryTreeLevelKSize(BTNode* root, int k); // 二叉树销毁 void BinaryTreeDestory(BTNode* root); // 层序遍历 void BinaryTreeLevelOrder(BTNode* root); // 判断二叉树是否是完全二叉树 bool BinaryTreeComplete(BTNode* root);
Link_Teer.c
#include"Link_Teer.h" //二叉树的初始化 BTNode* BuyNode(BTDataType x) { BTNode* newnode = (BTNode*)malloc(sizeof(BTNode)); if (newnode == NULL) { perror("BinaryTreeInit:newnode:"); exit(1); } newnode->_data = x; newnode->_left = NULL; newnode->_right = NULL; return newnode; } // 二叉树前序遍历 void BinaryTreePrevOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } printf("%d ", root->_data); BinaryTreePrevOrder(root->_left); BinaryTreePrevOrder(root->_right); } // 二叉树中序遍历 void BinaryTreeInOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } BinaryTreeInOrder(root->_left); printf("%d ", root->_data); BinaryTreeInOrder(root->_right); } // 二叉树后序遍历 void BinaryTreePostOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } BinaryTreePostOrder(root->_left); BinaryTreePostOrder(root->_right); printf("%d ", root->_data); } // 二叉树节点个数 int BinaryTreeSize(BTNode* root) { if (root == NULL) { return 0; } return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1; } // 二叉树叶子节点个数 int BinaryTreeLeafSize(BTNode* root) { if (root == NULL) return 0; if (BinaryTreeLeafSize(root->_left) == 0 && BinaryTreeLeafSize(root->_right) == 0) return 1; return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right); } // 二叉树高度 int BinaryTreeHeight(BTNode* root) { if (root == NULL) { return 0; } int heightleft = BinaryTreeHeight(root->_left); int heightright = BinaryTreeHeight(root->_right); return heightleft >= heightright ? heightleft + 1 : heightright + 1; } // 二叉树第k层节点个数 int BinaryTreeLevelKSize(BTNode* root, int k) { if (root == NULL) return 0; if (k == 1) return 1; return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1); } // 二叉树查找值为x的节点 BTNode* BinaryTreeFind(BTNode* root, BTDataType x) { if (root == NULL) return NULL; if (root->_data == x) return root; BTNode* ret1 = BinaryTreeFind(root->_left, x); if (ret1 != NULL) { return ret1; } BTNode* ret2 = BinaryTreeFind(root->_right, x); if (ret2 != NULL) { return ret2; } return NULL; } // 二叉树销毁 void BinaryTreeDestory(BTNode* root) { if (root == NULL) { return; } BinaryTreeDestory(root->_left); BinaryTreeDestory(root->_right); free(root); } #include"Queue.h" // 层序遍历 void BinaryTreeLevelOrder(BTNode* root) { // 按照顺序,放入队列里面,先进先出,后进后出的原则 // 1,创建队列,初始化 // 2,如果不为空,根节点入队列 // 3,取栈顶,打印,出队列 Queue ps; QueueInit(&ps); if (root) QueuePush(&ps, root); while (!QueueEmpty(&ps)) { BTNode* ret = QueueFront(&ps); QueuePop(&ps); printf("%d ", ret->_data); if (ret->_left) QueuePush(&ps, ret->_left); if (ret->_right) QueuePush(&ps, ret->_right); } //队列的销毁 QueueDestroy(&ps); } // 判断二叉树是否是完全二叉树 bool BinaryTreeComplete(BTNode* root) { Queue ps; QueueInit(&ps); if (root) QueuePush(&ps, root); while (!QueueEmpty(&ps)) { BTNode* ret = QueueFront(&ps); QueuePop(&ps); if (ret == NULL) { break; } if (ret->_left) QueuePush(&ps, ret->_left); if (ret->_right) QueuePush(&ps, ret->_right); } while (!QueueEmpty(&ps)) { BTNode* ret = QueueFront(&ps); QueuePop(&ps); if (ret != NULL) { QueueDestroy(&ps); return false; } } //队列的销毁 QueueDestroy(&ps); return true; }
test.c
#include"Link_Teer.h" //构建二叉树 void teer() { BTNode* node1 = BuyNode(1); BTNode* node2 = BuyNode(2); BTNode* node3 = BuyNode(3); BTNode* node4 = BuyNode(4); BTNode* node5 = BuyNode(5); BTNode* node6 = BuyNode(6); //BTNode* node7 = BuyNode(7); node1->_left = node2; node2->_left = node3; node1->_right = node4; node4->_left = node5; node4->_right = node6; //node2->_right = node7; printf(" 二叉树前序遍历测试:\n"); BinaryTreePrevOrder(node1); printf("\n\n\n"); printf("二叉树中序遍历测试:\n"); BinaryTreeInOrder(node1); printf("\n\n\n"); printf("二叉树后序遍历测试:\n"); BinaryTreePostOrder(node1); printf("\n\n\n"); printf("二叉树节点个数测试:\n"); int ret1 = BinaryTreeSize(node1); printf("%d", ret1); printf("\n\n\n"); printf("二叉树叶子节点个数测试:\n"); int ret2 = BinaryTreeLeafSize(node1); printf("%d", ret2); printf("\n\n\n"); printf("二叉树高度测试:\n"); int ret3 = BinaryTreeHeight(node1); printf("%d", ret3); printf("\n\n\n"); printf("二叉树第k层节点个数测试:\n"); int ret4 = BinaryTreeLevelKSize(node1, 3); printf("%d", ret4); printf("\n\n\n"); printf("二叉树查找值为x的节点测试:\n"); BTNode* ret5 = BinaryTreeFind(node1, 6); printf("%d", ret5->_data); printf("\n\n\n"); printf("层序遍历测试:\n"); BinaryTreeLevelOrder(node1); printf("\n\n\n"); printf("判断二叉树是否是完全二叉树测试:\n"); bool ret6 = BinaryTreeComplete(node1); printf("%d", ret6); } int main() { //构建二叉树 teer(); return 0; }
二叉树习题精讲
单值二叉树
965. 单值二叉树 - 力扣(LeetCode)https://leetcode.cn/problems/univalued-binary-tree/description/
判断这里面的所有数值是不是一样
方案1:遍历
方案2:拆分子问题
/** * Definition for a binary tree node. * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; */ bool isUnivalTree(struct TreeNode* root) { //条件满足 if(root == NULL) { return true; } //判断条件 int ret=root->val; if(root->left != NULL && root->left->val != root->val) { return false; } if(root->right != NULL && root->right->val != root->val) { return false; } return isUnivalTree(root->left) && isUnivalTree(root->right); }
解释:
bool isUnivalTree(struct TreeNode* root)
- 这是函数的声明行,定义了一个名为
isUnivalTree
的函数,它接收一个指向TreeNode
结构体的指针root
作为参数。函数返回一个布尔值bool
,如果树是单值树则返回true
,否则返回false
。
if(root == NULL)
- 这行代码检查传入的
root
指针是否为NULL
。如果是NULL
,表示当前节点为空,空树被认为是单值树,因此返回true
。
int ret=root->val;
- 这行代码将当前节点
root
的值赋给变量ret
。这个值将用于和当前节点的左右子节点的值进行比较。
if(root->left != NULL && root->left->val != root->val)
- 这行代码检查当前节点的左子节点是否存在,并且如果存在,它的值是否与当前节点的值
root->val
不同。如果是这样,树不是单值树,因此返回false
。
if(root->right != NULL && root->right->val != root->val)
- 类似地,这行代码检查当前节点的右子节点是否存在,并且如果存在,它的值是否与当前节点的值
root->val
不同。如果是这样,树不是单值树,因此返回false
。
return isUnivalTree(root->left) && isUnivalTree(root->right);
- 如果当前节点的左右子节点的值都与当前节点的值相同,这行代码递归地调用
isUnivalTree
函数,分别检查左子树和右子树是否也是单值树。如果两个子树都是单值树,那么整个树也是单值树,因此返回true
;否则,返回false
。总结来说,
isUnivalTree
函数通过递归的方式检查每个节点的值是否与其子节点的值相同。如果树中的所有节点的值都相同,那么函数返回true
,表示这是一个单值树;如果任何节点的子节点的值与它不同,函数返回false
,表示这不是一个单值树。
相同的树
100. 相同的树 - 力扣(LeetCode)https://leetcode.cn/problems/same-tree/description/
/** * Definition for a binary tree node. * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; */ bool isSameTree(struct TreeNode* p, struct TreeNode* q) { if (p == NULL && q == NULL) return true; if (p == NULL || q == NULL) return false; if (p->val != q->val) return false; return isSameTree(p->left,q->left) && isSameTree(p->right,q->right); }
解释:
bool isSameTree(struct TreeNode* p, struct TreeNode* q)
- 这是函数的声明行,定义了一个名为
isSameTree
的函数。它接收两个参数,分别是指向TreeNode
结构体的指针p
和q
,表示要比较的两棵树的根节点。函数返回一个布尔值bool
,如果两棵树相同则返回true
,否则返回false
。
if (p == NULL && q == NULL)
- 这行代码检查两个节点
p
和q
是否都为空。如果都为空,说明两棵树在当前节点上是相同的(都是空树),因此返回true
。
if (p == NULL || q == NULL)
- 这行代码检查两个节点
p
和q
中是否有一个为空而另一个不为空。如果存在这样的情况,说明两棵树至少在当前节点上就不同,因此返回false
。
if (p->val != q->val)
- 这行代码检查两个节点
p
和q
的值是否不同。如果值不同,说明两棵树在当前节点上不相同,因此返回false
。
return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
- 如果当前节点
p
和q
都不为空且值相同,这行代码递归地调用isSameTree
函数,分别比较p
的左子节点和q
的左子节点,以及p
的右子节点和q
的右子节点。只有当左子树和右子树都相同,整个树才被认为是相同的,因此返回两个递归调用的结果的逻辑与(&&
)。总结来说,
isSameTree
函数通过递归的方式比较两棵树的每个节点。首先检查当前节点是否为空,如果不为空再检查节点的值是否相同。如果所有对应的节点都相同,那么函数返回true
,表示两棵树相同;如果任何一对对应节点不同,函数返回false
,表示两棵树不同。
镜像二叉树
101. 对称二叉树 - 力扣(LeetCode)https://leetcode.cn/problems/symmetric-tree/description/
/** * Definition for a binary tree node. * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; */ bool isSameTree(struct TreeNode* p, struct TreeNode* q) { //遍历结束 if(q==NULL && p==NULL) return true; //不相等条件 if(q==NULL || p==NULL) return false; //不相等条件 if(q->val != p->val) return false; return isSameTree(p->left,q->right) && isSameTree(p->right,q->left); } bool isSymmetric(struct TreeNode* root) { if(root == NULL) return true; int ret = isSameTree(root->left,root->right); return ret; }
这里的逻辑和相同二叉树一样
二叉树的前序遍历
144. 二叉树的前序遍历 - 力扣(LeetCode)https://leetcode.cn/problems/binary-tree-preorder-traversal/description/
/** * Definition for a binary tree node. * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; */ /** * Note: The returned array must be malloced, assume caller calls free(). */ //计算链表长度的大小,从而开辟空间 int preorderTraversal_size(struct TreeNode* root) { return root == NULL?0:preorderTraversal_size(root->left)+preorderTraversal_size(root->right)+1; } //前序遍历,放到数组里面 void preorder(struct TreeNode* root,int* a,int* pi) { //结束递归 if(root==NULL) { return; } a[(*pi)++] = root->val; preorder(root->left,a,pi); preorder(root->right,a,pi); } //输出函数 int* preorderTraversal(struct TreeNode* root, int* returnSize) { *returnSize = preorderTraversal_size(root); int* a = (int*)malloc(sizeof(int)*(*returnSize)); if(a == NULL) { perror("preorder:a == NULL"); exit(1); } int i = 0; preorder(root,a,&i); return a; }
解释:
int preorderTraversal_size(struct TreeNode* root)
- 这个函数用于计算二叉树中节点的数量。
- 如果根节点
root
为空,返回0
。- 否则,递归地计算左子树和右子树的节点数量,然后加
1
(包括根节点)。
void preorder(struct TreeNode* root, int* a, int* pi)
- 这个函数用于前序遍历二叉树,并将节点值存储到数组
a
中。- 如果根节点
root
为空,递归结束。- 否则,将根节点的值赋给数组
a
的第*pi
个位置,并将pi
递增。- 递归地对左子树和右子树执行相同的操作。
int* preorderTraversal(struct TreeNode* root, int* returnSize)
- 这个函数用于初始化前序遍历的过程,并返回一个包含遍历结果的新分配的数组。
*returnSize
被设置为preorderTraversal_size(root)
的返回值,即树中节点的数量。- 为数组分配内存,大小为
*returnSize
。- 如果内存分配失败,输出错误并退出程序。
- 初始化一个计数器
i
,用于跟踪数组中当前的位置。- 调用
preorder
函数,开始前序遍历过程,并将节点值存入数组。- 返回包含前序遍历结果的数组。
总结
preorderTraversal_size
函数通过递归计算二叉树的节点数量。preorder
函数通过递归执行前序遍历,将每个访问到的节点值存入数组。preorderTraversal
函数初始化遍历过程,计算所需的内存大小,分配内存,并调用preorder
函数来填充数组。最终返回一个包含树的前序遍历结果的数组。
另一棵树的子树
572. 另一棵树的子树 - 力扣(LeetCode)
/** * Definition for a binary tree node. * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; */ //判断是不是相同 bool isSameTree(struct TreeNode* p, struct TreeNode* q) { //遍历结束 if(q==NULL && p==NULL) return true; //不相等条件 if(q==NULL || p==NULL) return false; //不相等条件 if(q->val != p->val) return false; return isSameTree(p->left,q->left) && isSameTree(p->right,q->right); } //遍历树 bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot) { if(root == NULL) return false; //遍历 bool ret1 = isSubtree(root->left,subRoot); bool ret2 = isSubtree(root->right,subRoot); //对比 bool ret = isSameTree(root,subRoot); return ret || ret1 || ret2; }
isSameTree
函数通过递归比较两棵树的根节点值,以及它们的左右子树是否相同。isSubtree
函数通过递归地在root
的左子树和右子树中查找subRoot
,并使用isSameTree
来检查当前节点root
是否与subRoot
相同。- 如果
subRoot
是空树,那么任何树root
都是它的子结构,因此isSubtree
直接返回true
。- 如果
root
是空树,它不可能包含subRoot
作为子结构,因此isSubtree
返回false
。isSubtree
函数返回true
,如果root
与subRoot
相同,或者root
的任何子树与subRoot
相同。注意:
isSubtree
函数的逻辑确保了如果subRoot
是root
的子树,或者root
的任何子树包含subRoot
,那么函数将返回true
。
二叉树的遍历
二叉树遍历_牛客题霸_牛客网 (nowcoder.com)https://www.nowcoder.com/practice/4b91205483694f449f94c179883c1fef?tpId=60&&tqId=29483&rp=1&ru=/activity/oj&qru=/ta/tsing-kaoyan/question-ranking
要求就是前序遍历构建一棵树
如图解
前序输入,中序输出
#include <stdio.h> #include <stdlib.h> typedef struct root { struct root* left; struct root* right; char val; }root; //中序遍历二叉树 void root_parent(root* root1) { if (root1 == NULL) { return; } root_parent(root1->left); printf("%c ",root1->val); root_parent(root1->right); } //前序创建二叉树 root* root_malloc(char* a,int* pi) { if (a[*pi] == '#') { (*pi)++; return NULL; } root* newnode=(root*)malloc(sizeof(root)); newnode->val=a[(*pi)++]; newnode->left = root_malloc(a,pi); newnode->right = root_malloc(a,pi); return newnode; } int main() { char arr[100]; scanf("%s",arr); int i=0; root* ret = root_malloc(arr,&i); root_parent(ret); return 0; }
root_parent
函数是一个递归函数,用于中序遍历二叉树。中序遍历的顺序是:左子树、根节点、右子树。
- 如果
root1
为空,直接返回(结束递归)。- 先递归遍历左子树。
- 访问根节点,打印节点的值。
- 递归遍历右子树。
root_malloc
函数根据传入的字符串数组a
创建一棵二叉树。
- 如果当前字符是
'#'
,表示当前位置的节点不存在,递增索引pi
并返回NULL
。- 为新节点分配内存。
- 将当前字符赋值给新节点的
val
,并递增索引pi
。- 递归创建左子树并赋值给新节点的
left
指针。- 递归创建右子树并赋值给新节点的
right
指针。- 返回新创建的节点。
main
函数是程序的入口点。
- 定义一个足够大的字符数组
arr
,用于存储输入的二叉树序列。- 使用
scanf
函数读取用户输入的字符串。- 初始化索引
i
为 0,用于root_malloc
函数中遍历字符串数组。- 调用
root_malloc
函数创建二叉树,并将返回的根节点指针赋值给ret
。- 调用
root_parent
函数对创建的二叉树进行中序遍历,并打印节点值。- 返回 0,表示程序正常结束。
注意
- 输入的字符串应该遵循前序遍历的序列格式,其中
'#'
表示不存在的节点。- 代码中没有释放分配的内存,实际应用中应该考虑内存管理。这里oj题目不需要考虑