数据结构-二叉树

目录

1.树概念及结构

1.1. 树的概念

1.2. 树的其他相关概念

1.3. 树的表示

1.3.1. 思考

1.3.2. 先描述

1.3.2. 再组织

1.4. 树在实际中的运用

2.二叉树概念及结构

2.1. 二叉树的概念

2.2. 特殊的二叉树

2.3. 二叉树的性质:

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

3.1. 二叉树的顺序结构的概念

3.2. 堆的概念及结构 

3.3. 堆的实现框架

3.4.堆的实现具体细节

3.4.1. heap_push的实现

第一步:在堆得尾部直接插入数据

第二步:通过向上调整算法调整堆的结构,使其合法

3.4.1.1. 向上调整算法

3.4.1.2. heap_push的整体实现

3.4.2. heap_pop的实现

3.4.2.1 向下调整算法

3.4.2.2. heap_pop的完整实现 

3.4.3. heap_sort()的实现

3.4.3.1. 建堆

3.4.3.1.1. 以向下调整算法建堆

补充:证明向下调整算法建堆的时间复杂度

 3.4.3.1.2. 以向上调整算法建堆

补充:证明向上调整算法建堆的时间复杂度 

3.4.3.2. 排序

3.4.3.2. heap_sort的完整实现:

3.4.4. topK问题

3.4.4.1 分析:

3.4.4.2. 代码实现: 

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

4.2. 二叉树的遍历

4.2.1. 前序遍历(NLR)

4.2.2. 中序遍历(LNR)

4.2.3. 后序遍历(LRN)

 ​编辑

4.2.4. 求二叉树的有效节点个数

4.2.5. 求二叉树的有效叶子节点个数

4.2.6.  二叉树第k层节点个数

4.2.7. 二叉树查找值为x的节点

4.2.8. 二叉树的销毁

4.2.9. 二叉树的层序遍历

4.2.10. 判断二叉树是否是完全二叉树

4.3. 二叉树的练习题

4.3.1. 单值二叉树

4.3.2. 相同的树

4.3.3. 对称二叉树

4.3.4. 二叉树的前序遍历

4.3.5. 二叉树的中序遍历

4.3.6. 二叉树的后序遍历

 4.3.7. 另一颗树的子树

4.3.8. 二叉树的最大深度

4.3.9. 翻转二叉树


1.树概念及结构

1.1. 树的概念

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

 

树的特点:
a. 有且只有一个根节点,其没有前驱
节点。例如上面的图1.2值为1的节点就是根节点。

b. 除根节点外,其余节点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继。例如上面的图1.2值为2的节点,这个节点只有一个前驱节点,有两个后继节点,并且单独看这个节点,由这个节点和它的后继节点又可以构成与原树结构类似的子树。

因此可以认为,树是递归定义的。

注意:树型结构中,子树之间不可以有交集,否则就不是树型结构。如:图1.3和图1.4

由于子树之间都产生了交集,因此它们都不是树型结构。

 

得出的结论有:

a.  除根节点外,树形结构中的每个节点有且只有一个父节点。

b.  如果树节点有M个,那么它的边数是M-1。

1.2. 树的其他相关概念

声明:为了更好的表示节点,在这里1就表示值为1的节点

为了更好地理解树的一些概念,我们已下面的图1.2为例:


  节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:1节点的度为3
叶子节点或终端节点:度为0 的节点称为叶节点; 如上图:5、11、7、8、12、10 节点为叶子节点或终端节点
非终端节点或分支节点:度不为0 的节点; 如上图:1、2、3、4 等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:2 是5的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:2 是1的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:5、6 是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为3
节点的层次:从根开始定义起,根为第1 层,根的子节点为第2层,以此类推;当然也可以将根定义为0层,根的子节点为第一层,以此类推。虽然都可以,但是更推荐根从第一层开始。因为如果我们从0开始,那么空树的高度(深度)就是-1,不太好
树的高度或深度:树中节点的最大层次(根所在层数为1); 如上图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:6、10 互为堂兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点;如上图:1是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林:由m( m>0)棵互不相交的树的集合称为森林 

1.3. 树的表示

树结构相对线性表就比较复杂了,要存储表示起来比较麻烦,既要保存值域,也要保存结点和结点之间 的关系 ,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法

1.3.1. 思考

假设,现在有一颗多叉树,让我们去表示它的结构,该如何表示呢?

首先,树是多个节点的集合。要想描述一棵树,是不是应该先描述一个节点。既然是多个节点的集合,那么我们应该需要管理这些节点。

而管理如何理解呢?

例如:学校的校长要管理学校的学生,不是说把你这个学生给你叫过来,问你今天干了什么,学习状态好不好等等,而是会把你的行为数据化,例如你的成绩、你的考勤率、你的各种活动的参加情况统一用数据进行描述,而当学校的校长要对某个学生做表扬/批评的时候,是拿着你的数据做参考的。

也就是说,管理的第一步:将被管理的对象数据化 --- 先描述

但是,学校可不会只有一个学生。校长如何管理这么多被描述的对象呢?他会通过某种数据结构,例如顺序表、链表、树等结构将这些被描述的对象进行组织起来。

即管理的第二步:将被描述的对象进行组织起来 --- 再组织

管理 = 先描述在组织。

1.3.2. 先描述

typedef int node_val_type;
// 描述树结构中的任意一个节点
struct tree_node
{
    struct tree_node* _firstchild;  // 第一个孩子节点
    struct tree_node* _nextbrother;  // 指向下一个兄弟节点
    node_val_type _val;    
};

1.3.2. 再组织

1.4. 树在实际中的运用

实际运用:表示文件系统的目录树结构,如:Linux的文件系统

2.二叉树概念及结构

2.1. 二叉树的概念

一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 由一个根节点外加上两棵称为左子树和右子树的二叉树组成

从上面的图我们可以看到:

a. 二叉树不存在度大于 2 的结点,度只有0,1。
b. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
而任意的一颗二叉树都是由下面几种情况复合而成的:

2.2. 特殊的二叉树

1. 满二叉树:一个二叉树,如果每一个层的节点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且结点总数是2^k-1,则它就是满二叉树。
2. 完全二叉树:除了最后一层外,其他层节点都是满的,并且最后一层上的节点从左到右连续排列,满二叉树是完全二叉树的一种特殊情况。

注意: 如下情况,不是完全二叉树;

完全二叉树必须要求最后一层不满,且必须从左向右连续,不可以跳越。

2.3. 二叉树的性质:

  1. 若规定根节点的层数为 1 ,则一棵非空二叉树的第h层上最多有 2^(h-1)个节点.
2. 若规定根节点的层数为 1 ,则 深度为 h的二叉树的最大结点数是 2^h-1个节点
3. 对任何一棵非空二叉树 , 如果度为 0其叶节点个数为A , 度为 2的分支节点个数为B,则有A=B+1,即度为0的节点个数 = 度为2的节点个数 + 1
4. 若规定根节点的层数为 1 ,具有 n个节点的满二叉树的深度,h= log(n+1)
(ps:是log以 2为底,n+1 为对数)
5. 对于具有 n个节点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为 i 的结点有:  
1. 若 i>0 i 位置节点的双亲序号: (i-1)/2 i=0 i为根节点编号,无双亲节点
2. 若 2i+1<n ,左孩子序号: 2i+1 ,如果 2i+1>=n则无左孩子
3. 若 2i+2<n ,右孩子序号: 2i+2 ,如果 2i+2>=n则无右孩子
补充: 对于完全二叉树,度为1的节点只有一个或者零个

假设现在有一颗深度为h的二叉树,求它的最大节点数以及第h层上最多有多少个节点?

假设现在有一颗深度为h的二叉树,节点范围是多少呢?

根据上面的图,我们也可以推出这个范围是:[2^(h-1),2^h-1]

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

3.1. 二叉树的顺序结构的概念

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

为什么我们说堆最好是一棵完全二叉树呢?

非完全二叉树作为堆带来的缺点就是:

a. 空间浪费:由于非完全二叉树在结构上不规则,可能需要为了保持堆的性质而留下额外的空间,这可能导致空间的浪费。

b. 难以维护,非完全二叉树在结构上更加复杂,节点的位置更加不规则,可能需要更复杂的操作来维护堆的性质,还有一些其他的缺点。

结论就是:堆最好是一颗完全二叉树。

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

3.2. 堆的概念及结构 

堆分为大(顶)堆和小(顶)堆:

大(顶)堆:任一个父节点都大于等于它的每一个子节点

小(顶)堆:任一个父节点都小于等于它的每一个子节点

通过下图3.4,我们可以发现一些规律:

对于30这个根节点来说,它的下标为0,它的左孩子的下标为1,右孩子的下标为2我们可以发现它们关于下标之间的规律:

left_child = parent * 2 + 1;

right_child = parent * 2 + 2 = left child + 1;

parent = (child - 1) / 2 ;

3.3. 堆的实现框架

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

typedef int heap_data_type;
typedef struct heap
{
  heap_data_type* _heap_array;
  size_t _capacity;
  size_t _size;
}heap;

void heap_init(heap* hp_ptr);
void heap_destroy(heap* hp_ptr);
// 尾插
void heap_push(heap* hp_ptr,heap_data_type val);
// 删除堆顶数据
void heap_pop(heap* hp_ptr);
bool heap_empty(heap* hp_ptr);
void heap_sort(heap* hp_ptr);
void heap_print(heap* hp_ptr);

3.4.堆的实现具体细节

3.4.1. heap_push的实现

我们知道,如果是大堆,那么任意一个父节点的值都大于等于它的子节点的值,而小堆是任意一个父节点的值都小于等于它的子节点的值。而我们如果要向堆插入数据,那么必须要维持堆的特性。

因此对于堆的插入可以分为两步:

第一步:在堆得尾部直接插入数据

void heap_push(heap* hp_ptr,heap_data_type val)
{
  //1. 堆的地址不为NULL
  assert(hp_ptr);
  //2. 检查容量
  check_capacity(hp_ptr);
  //3. push数据
  hp_ptr->_heap_array[hp_ptr->_size++] = val;
}
第二步:通过向上调整算法调整堆的结构,使其合法
3.4.1.1. 向上调整算法

向上调整算法:前面的数据必须是堆。

时间复杂度:O(logN)

void adjust_up(heap* hp_ptr)
{
  //1. 堆得地址不为NULL
  assert(hp_ptr);
  //2. 确定父节点和子节点的位置
  //a. 子节点就是最后一个节点
  //b. 父节点根据父子关系求得
  int child = hp_ptr->_size-1;
  int parent = (child-1)/2;
  //3. 在这里我们建小堆
  //注意:当子节点到了根节点就结束循环
  while(child > 0)
  {
    // a. 如果孩子小于父亲,就交换父子的值,并更新父子位置
    
    if(hp_ptr->_heap_array[child] < hp_ptr->_heap_array[parent])
    {
      swap(&hp_ptr->_heap_array[child],&hp_ptr->_heap_array[parent]);
      child = parent;
      // 注意:子节点到了根节点后,父节点就不要再更新了
      if(child != 0)
        parent = (child-1)/2;
    }
    else
    {
      break;
    }
  }
}
3.4.1.2. heap_push的整体实现
void heap_push(heap* hp_ptr,heap_data_type val)
{
  //1. 堆的地址不为NULL
  assert(hp_ptr);
  //2. 检查容量
  check_capacity(hp_ptr);
  //3. push数据
  hp_ptr->_heap_array[hp_ptr->_size++] = val;
  //4. 调整堆(我们在这里是小堆)
  adjust_up(hp_ptr);
}

3.4.2. heap_pop的实现

对于一个堆结构来说,我们实现尾删意义是不大的。因为你尾删不会影响堆得结构,实现意义不大。但如果我们在这里要求实现头删呢?即删除根节点,如何实现呢?

分析:

要进行头删,要符合一些条件:

1. 既然你是删除,那么必须要求堆不为NULL;

2. 如果你是头删,那么我们需要保证删除后的结构依旧是一个堆结构。

有了这些条件,我们再进行讨论:

第一个条件,无需讨论,只需要一个判空函数即可。

第二个条件就需要讨论讨论了:

头删的第一种方案:后面的节点依次赋值给前面的节点 

我们可以看到,虽然这删除了第一个节点,但是此时它的左右子树结构就都全乱套了,既不是大堆,也不是小堆。因此这种解决方案是不可行的。

因此我们提出了第二种解决方案:

第一步:交换堆顶和尾部的数据(此时不会影响左右子树的结构)

第二步:对根节点这个位置进行调整堆,此时我们所用的调整方法:向下调整方法

3.4.2.1 向下调整算法

向下调整算法的前提:左右子树都必须是大(小)堆

时间复杂度:O(logN)

根据上面的分析,我们可以写出我们的向下调整算法: 

// n代表有效元素的个数
// root代表父节点(相对的)
void adjust_down(heap* hp_ptr,int root,int n)
{
  //1. 堆不为NULl
  assert(hp_ptr);
  //2. 确定孩子和父亲的初始位置
  int parent = root;
  //3. 默认孩子为左孩子
  int child = 2 * parent + 1;  
  //4. 建小堆
  while(child < n)
  {
     //5. 确定小孩子,如果右孩子更小,就++child
     // 注意:小心右孩子越界
     if(child + 1 < n && hp_ptr->_heap_array[child] > hp_ptr->_heap_array[child + 1])
     {
       ++child;
     }
     //6. 如果孩子小于父亲,则交换,并更新父子的位置
     if(hp_ptr->_heap_array[child] < hp_ptr->_heap_array[parent])
     {
       swap(&hp_ptr->_heap_array[child],&hp_ptr->_heap_array[parent]);
       parent = child;
       child = 2 * parent + 1;
     }
     else
     {
       break;
     }
  }
}
3.4.2.2. heap_pop的完整实现 

有了向下调整算法,删除堆顶数据就简单多了

我们给出的heap_pop有三个步骤:

第一步:交换堆顶和尾部数据,

第二步:更新元素个数

第三步:向下调整

void heap_pop(heap* hp_ptr)
{
  //1. 堆的地址不为NULL
  assert(hp_ptr);
  //2. 堆不可以为NULL
  assert(!heap_empty(hp_ptr));
  //3. 删除数据
  //a. 第一步:交换堆顶和最后一个数据
  swap(&hp_ptr->_heap_array[0],&hp_ptr->_heap_array[hp_ptr->_size-1]);
  //b. 第二步:更新_size
  hp_ptr->_size--;
  //c. 第三步:向下调整堆,且只需要调整堆顶,因为堆顶的左右子树结构没发生改变
  adjust_down(hp_ptr,0,hp_ptr->_size);
}

3.4.3. heap_sort()的实现

堆排序的实现可以看作是两个过程:

第一个过程:建堆。

第二个过程:排序。

3.4.3.1. 建堆
3.4.3.1.1. 以向下调整算法建堆

对于任意一个数组(逻辑结构)来说,它很有可能不是一个堆,而堆排序必须要求被排序对象必须是一个堆结构。

因此现在的主要问题就是,给我们一个任意的数组,如何将它调整为堆?

可以看到,对于上面的这个数组,它既不是一个大堆(父节点>=子节点),也不是一个小堆(父节点<=子节点);那如何将它调整为一个小(大)堆呢?

我们在这里以小堆举例,分析如下:

因此建堆的代码如下:

时间复杂度:O(N)

//1. 最后一个非叶子节点 = 最后一个叶子节点的父亲
//a. 最后一个节点: _size-1
//b. parent = (child-1)/2;
int cur = (hp_ptr->_size-1-1)/2;
//2. 从最后一个非叶子节点开始调,调到根节点(包含根节点)
while(cur >= 0)
{
  adjust_down(hp_ptr,cur,hp_ptr->_size);
  --cur;
}
补充:证明向下调整算法建堆的时间复杂度

我们之前说过,向下调整算法的建堆时间复杂度是O(N),那为什么呢?

因此,经过上面的证明,我们可以得出向下调整算法建堆的时间复杂度是O(N)

 3.4.3.1.2. 以向上调整算法建堆

与上面大同小异,由于向上调整算法建堆的条件:前面的数据必须是一个堆。

我们依旧以图3.8举例:

此时我们不能从最后的一个非叶子节点开始调整,因为无法保证最后一个非叶子节点前面的数据是一个堆。但是我们发现对于根节点来说,由于它没有父节点,因此可以认为它前面的数据是一个堆(虽然前面的数据是未知的)。因此突破口就是根节点。

在这里,我们再调整一下我们的向上调整算法:

void adjust_up(int* ptr, int pos)
{
	//1. 数组的地址不为NULL
	assert(ptr);
	//2. 找父亲
	int child = pos;
	int parent = (child - 1) / 2;
	//3. 在这里我们建小堆
	while (child > 0)
	{
		// a. 如果孩子小于父亲,就交换父子的值,并更新父子位置
		// 注意:子节点到了根节点后,父节点就不要再更新了
		if (ptr[child] < ptr[parent])
		{
			swap(&ptr[child],&ptr[parent]);
			child = parent;
			if (child != 0)
				parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

以向上调整算法建堆:

时间复杂度:O(NlogN)

void build_heap(int* ptr,int n)
{
	// 1.建堆
	assert(ptr);
	// 2.只有一个节点,就不用建堆了
	if (n <= 1)
		return;
    // 3. 从根节点的下一个节点开始调整
	int cur = 1;
	while (cur < n)
	{
		adjust_up(ptr, cur);
		++cur;
	}
}
补充:证明向上调整算法建堆的时间复杂度 

因此,上面的证明,我们可以得出向上调整算法建堆的时间复杂度是O(NlogN)

3.4.3.2. 排序

OK,经过我们上面的处理,任意数组都会被我们调整为小堆。

接下来的问题就是如何排序呢?

排序的过程和heap_pop的原理差不多,我们不能直接将堆顶的数据取下来,这样可能会导致堆顶的左右子树结构发生改变,如果此时重新建堆代价太大,因此为了不破环左右子树的结构,我们还是利用heap_pop的思路,先交换堆顶数据和尾部数据,更新数据个数,向下调整堆。

还有一个问题,值得我们思考,如果是一个小堆,经过我们上面的思路,排序后是升序还是降序呢?

小堆,也意味着堆顶数据最小,而交换堆顶数据和尾部数据后,最小的数据跑到了数组的末端,更新数据个数,调整小堆,以此循环,最大值会在数组的首端,最小值会在数组的末端,因此结论就是:小堆排序后是降序,大堆排序后是升序  

堆排序的时间复杂度:O(NlogN)

//3. 排序, 小堆,排降序
int end = hp_ptr->_size - 1;
while(end > 0)
{
  //a. 交换堆顶和end所处位置的数据
  swap(&hp_ptr->_heap_array[0],&hp_ptr->_heap_array[end]);
  //b. 更新end
  --end;
  //c. 重新建堆
  //注意,每次排序后,向下调整的数组的大小要更新,在这里就是end+1
  adjust_down(hp_ptr,0,end+1);
}
3.4.3.2. heap_sort的完整实现:
void heap_sort(heap* hp_ptr)
{
  // 堆的地址不可以为NULL
  assert(hp_ptr);
  // 堆不可以为NULL
  assert(!heap_empty(hp_ptr));
  //1. 最后一个非叶子节点 = 最后一个叶子节点的父亲
  int cur = (hp_ptr->_size-1-1)/2;
  //2. 从最后一个非叶子节点开始调,调到根节点(包含根节点)
  while(cur >= 0)
  {
    adjust_down(hp_ptr,cur,hp_ptr->_size);
    --cur;
  }
  //3. 排序, 小堆,排降序
  int end = hp_ptr->_size - 1;
  while(end > 0)
  {
    //a. 交换堆顶和end所处位置的数据
    swap(&hp_ptr->_heap_array[0],&hp_ptr->_heap_array[end]);
    //b. 更新end
    --end;
    //c. 重新建堆
    //注意,每次排序后,向下调整的数组的大小要更新,在这里就是end+1
    adjust_down(hp_ptr,0,end+1);
  }
}

3.4.4. topK问题

假设,现在有一组很多的数据,内存存不小(即文件内容在磁盘上),要求我们找到这组数据中的前K个大(小)的数据(K相对于所有数据很小),该如何实现?

3.4.4.1 分析:

第一种思路:对这组数据进行排序,遍历得到前k个数据。

但是这种方式不可行,原因是由于内存存不下这些所有的数据,无法对所有数据进行排序(因为冯诺依曼告诉我们CPU之和内存打交道),内存都存不下这些数据,自然而然就不可以对这些数据进行排序。

第二种思路:将这些数据进行建堆,popK次,我们也就得到了这些数据。

但是这种方式也不可行,跟第一种思路有着同样的问题,就是内存存不下这些数据,无法建堆。

第三章思路:建K个数据的堆

只建K个数据的堆结构,是可行的。 但是问题也随之而来,假如我们现在要取前K个大的数据,我们是建大堆还是建小堆?

假设我们现在是建大堆

因此,根据上面的分析,我们得出的结论就是,要找前K大的数据,必须建小堆

我们不是要找前K个大的数据吗,那我就排除小的数据,怎么排除呢?建小堆,用小堆中堆顶的数据(堆中最小的数据)去和剩下的(N - K)的数据比较,

遇到比堆顶数据大的,就交换,交换完,重新向下调整(时间复杂度logK);遇到比堆顶数据还小的,继续遍历下一个,直到遍历完。所以总的时间复杂度是O(K + logK * (N - K)),但又因为K是远小于N的,所以这个算法可以近似为O(N),可谓相当之厉害;OK,让我们以代码来试试吧

3.4.4.2. 代码实现: 
// 将数据存到文件中
void creat_data()
{
	int n = 100000;
	srand((size_t)time(NULL));
	int *arr = (int*)malloc(sizeof(int)* n);
	assert(arr);
	for (int i = 0; i < n; ++i)
	{
		// 每个数在0-99
		arr[i] = rand() % 100;
	}
	// 特殊处理:前k个大的数据
	arr[888] = 101;
	arr[6123] = 102;
	arr[83] = 103;
	arr[8623] = 106;
	arr[723] = 105;
	arr[124] = 104;
	arr[5215] = 109;
	arr[7437] = 108;
	arr[8965] = 110;
	arr[4241] = 107;
	FILE* fp = fopen("./data.txt", "w");
	assert(fp);

	for (int i = 0; i < n; ++i)
	{
		fprintf(fp, "%d\n", arr[i]);
	}
	free(arr);
	fclose(fp);
}
// 模拟TopK问题
void print_top_k(int K)
{
	FILE* fp = fopen("./data.txt", "r");
	assert(fp);

	int k = K;
	// 最后一个非叶子节点
	int *tmp_arr = (int*)malloc(sizeof(int)* k);
	assert(tmp_arr);
	// 先读10个数据
	for (size_t i = 0; i < 10; ++i)
	{
		fscanf(fp, "%d", &tmp_arr[i]);
	}
	int end = (k - 1 - 1) / 2;
	while (end >= 0)
	{
		// 建立k个数据的堆
		adjust_down(tmp_arr, end, 10);
		--end;
	}
	int tmp = 0;
    //从剩余数据中读取存到tmp里,与堆顶数据进行比较
	while (fscanf(fp, "%d", &tmp) != EOF)
	{
        // 如果比堆顶大,就交换
		if (tmp > tmp_arr[0])
		{
			swap(&tmp, &tmp_arr[0]);
			adjust_down(tmp_arr, 0, 10);
		}
	}
	for (int i = 0; i < k; ++i)
	{
		printf("%d ", tmp_arr[i]);
	}
	free(tmp_arr);
	fclose(fp);
}

结果:101 102 105 103 107 108 106 104 110 109

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

4.2. 二叉树的遍历

4.2.1. 二叉树的前序、中序、后序遍历

二叉树遍历(Traversal) 是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有: 前序 / 中序 / 后序的递归结构遍历
1. 前序遍历 (Preorder Traversal 亦称先序遍历 )——访问根结点的操作发生在遍历其左右子树之前,简单理解 根左右
2. 中序遍历 (Inorder Traversal)—— 访问根结点的操作发生在遍历其左右子树之中(间),简单理解为左根右
3. 后序遍历 (Postorder Traversal)—— 访问根结点的操作发生在遍历其左右子树之后,简单理解为左右根
由于被访问的结点必是某子树的根,所以 N(Node )、 L(Left subtree )和 R(Right subtree )又可解释为 根、根的左子树和根的右子树 NLR LNR LRN分别又称为先根遍历、中根遍历和后根遍历。

4.2.1. 前序遍历(NLR)

前序遍历的规则是根左右(NLR) :

3 -> 7 -> 1 -> 9 -> NULL -> NULL -> 8 -> NULL -> NULL -> 2 -> 10 -> NULL -> NULL
-> NULL -> 4 -> 5 -> NULL -> NULL -> 6 -> NULL -> NULL

接下来,我们利用代码来验证上面的结果,我们写了一个preorder_traversal函数

void preorder_traversal(bt_node* root)
{
    // 节点为NULL就返回
	if (root == NULL)
    {
		printf("NULL->");
		return;
    }
    // 前序遍历(NLR)
	printf("%d->", root->_val);
	preorder_traversal(root->_left);
	preorder_traversal(root->_right);
}

结果为3->7->1->9->NULL->NULL->8->NULL->NULL->2->10->NULL->NULL->NULL->4->5->NULL->NULL->6->NULL->NULL->

为了更好地理解我们上面的递归函数,我们利用递归展开图理解,由于上面节点太多,我们重新创建一个二叉树,以方便验证。

我们预期的结果:

5 -> 2 -> 4 -> NULL -> NULL -> 1 -> NULL -> NULL -> 6 -> NULL -> NULL 

4.2.2. 中序遍历(LNR)

中序遍历的规则是左根右(LNR):

NULL -> 9 -> NULL -> 1 -> NULL -> 8 -> NULL -> 7 -> NULL -> 10 -> NULL

-> 2 -> NULL -> 3 -> NULL -> 5 -> NULL -> 4 -> NULL -> 6 -> NULL

接下来,我们利用代码来验证上面的结果,我们写了一个inorder_traversal函数

void inorder_traversal(bt_node* root)
{
    // 节点为NULL,直接return当前函数
	if (root == NULL)
	{
		printf("NULL->");
		return;
	}
    // 中序遍历左根右(LNR)
	inorder_traversal(root->_left);
	printf("%d->", root->_val);
	inorder_traversal(root->_right);
}

结果为:

NULL->9->NULL->1->NULL->8->NULL->7->NULL->10->NULL->2->NULL->3->NULL->5->NULL->4->NULL->6->NULL->

同样,为了更好地理解我们上面的递归函数,我们利用递归展开图理解,由于上面节点太多,我们重新创建一个二叉树,以方便验证。

4.2.3. 后序遍历(LRN)

 

后序遍历的规则是左右根(LRN) :

NULL -> NULL -> 9 -> NULL -> NULL -> 8 -> 1 -> NULL -> NULL -> 10 -> NULL -> 2 -> 7 

-> NULL -> NULL -> 5 -> NULL -> NULL -> 6 -> 4 ->3

当然,后序和前面两者没有太大差异,因此我在这里就不画递归展开图了,后续的遍历:


void postorder_traversal(bt_node* root)
{
    // 如果是空节点,回到上级栈帧
	if (root == NULL)
	{
		printf("NULL->");
		return;
	}
    // 后序遍历左右根(LRN):
	postorder_traversal(root->_left);
	postorder_traversal(root->_right);
	printf("%d->", root->_val);
}

经过代码,我们得到如下结果:

NULL->NULL->9->NULL->NULL->8->1->NULL->NULL->10->NULL->2->7->NULL->NULL->5->NULL->NULL->6->4->3->

4.2.4. 求二叉树的有效节点个数

例如下面的这棵二叉树,我们如何求它的节点个数呢?

  

第一种解决方案

有可能我们的第一个想法就是弄一个计数器,遍历每一个节点,++这个计数器,但是可行吗?我们试一试

int get_binary_tree_node_size(bt_node* root)
{
	int count = 0;
	if (root == NULL)
		return 0;
	else
	{
		++count;
		get_binary_tree_node_size(root->_left);
		get_binary_tree_node_size(root->_right);
		return count;
	}
}

int main()
{
    int node_size = get_binary_tree_node_size(n1);
	printf("node_size: %d\n", node_size);
    return 0;
}

 但是不好意思啊,答案是1,不是5。那为什么呢?

原因是因为这里的count是一个局部变量,出了函数作用域就会被销毁,每一次递归调用count++,当递归结束,cuunt又会被初始化为0,因此最后一次调用这个函数的时候++count,就是我们看到的结果1 。

我们提出了第二种解决方案:

刚刚出错的原因就是因为count是一个局部变量,我把它搞成静态变量或者是全局变量不就可以了吗?我们试试

int get_binary_tree_node_size(bt_node* root)
{
	static int count = 0;
	if (root == NULL)
		return 0;
	else
	{
		++count;
		get_binary_tree_node_size(root->_left);
		get_binary_tree_node_size(root->_right);
		return count;
	}
}
int main()
{
    // 第一次调用
    int node_size = get_binary_tree_node_size(n1);
	printf("node_size: %d\n", node_size);
    // 第二次调用
    node_size = get_binary_tree_node_size(n1);
	printf("node_size: %d\n", node_size);
    return 0;
}

结果:

node_size: 5
node_size: 10

为什么? 虽然此时的count不是一个局部变量,而是一个静态变量,即出了函数作用域不会销毁,因此第一次调用我们也得到正确的结果,但是当第二次调用的时候此时的count的初始值还是0吗?是5,由于静态变量之会被初始化一次,因此第二次调用会在5的基础上继续累加,结果便是10。因此这种方案也不是较好的方案。

我们提出了第三种解决方案:

不用这个计数器,而是利用函数的返回值做到累加效果,具体方案如下:

int get_binary_tree_node_size(bt_node* root)
{
    // 如果是空树(空节点),那么返回0
	if (root == NULL)
		return 0;
    // 如果是!空树,递归调用得到它的左子树和右子树并+1
	else
		return get_binary_tree_node_size(root->_left) + get_binary_tree_node_size(root->_right) + 1;
}

int main()
{
    // 第一次调用
    int node_size = get_binary_tree_node_size(n1);
	printf("node_size: %d\n", node_size);
    // 第二次调用
    node_size = get_binary_tree_node_size(n1);
	printf("node_size: %d\n", node_size);
    return 0;
}

 结果:

node_size: 5
node_size: 5

结果正确,为了更好地理解这个双路递归,我们利用递归展开图的方式加强理解:

4.2.5. 求二叉树的有效叶子节点个数

叶子节点个数的求法类似于节点个数的求法。只不过我们需要搞清楚什么是叶子节点。

只有某一个节点的左孩子和右孩子都为NULL时才是叶子节点,有了这样的理解,我么可以实现出我们的代码;

int get_leaf_node_size(bt_node* root)
{
    // 当走到空节点或者空树时,返回0
	if (root == NULL)
		return 0;
    // 如果是叶子节点,就+1
	else if (root->_left == NULL && root->_right == NULL)
		return 1;
    // 如果没走到空且不是叶子节点,就继续遍历这个节点的左子树和右子树
	else
		return get_leaf_node_size(root->_left) + get_leaf_node_size(root->_right);
}

 

例如上面的这颗二叉树,我们可以知道其叶子结点个数为3,我们测试一下我们的代码:

int main()
{
    bt_node* n1 = build_node(5);
	bt_node* n2 = build_node(2);
	bt_node* n3 = build_node(6);
	bt_node* n4 = build_node(4);
	bt_node* n5 = build_node(1);
	n1->_left = n2;
	n1->_right = n3;
	n2->_left = n4;
	n2->_right = n5;
    int ret = get_leaf_node_size(n1);
    printf("ret = %d\n", ret);
    free(n1);
	free(n2);
	free(n3);
	free(n4);
	free(n5);
    return 0;
}

结果:

ret = 3

4.2.6.  二叉树第k层节点个数

思路:

我们要求第k层的节点个数:

第k层的节点个数 = k-1层的下一层的节点个数 = k-2层的下两层的节点个数

int get_tree_level_node(bt_node* root, int k)
{
	//二叉树的高度必须大与0
	assert(k > 0);
	if (root == NULL)
		return 0;
	// 如果到了k层的上一层,那么节点个数++,并返回上级栈帧
	else if (k == 1)
		return 1;
	// 如果没有到k的上一层,那么遍历该节点的左子树,和右子树,k-1代表往下一层走
	else
		return get_tree_level_node(root->_left, k - 1) 
			+  get_tree_level_node(root->_right, k - 1);
}

为了更好地理解我们上面的递归程序,利用递归展开图加强理解:

4.2.7. 二叉树查找值为x的节点

如图所示,假如我们要查找到这棵二叉树中的val == 4的节点,该如何查找

思路:

查找特殊节点,我们需要利用前序的思想(NLR),如果当前节点是我们要找的特殊节点,那么就返回,接下来就不用在递归了;如果当前节点不是我们要找的特殊节点,那么就继续递归该节点的左子树和右子树

而问题的关键就在于,如果我们找到了特殊节点,怎么实现不在继续递归了呢?

我们的答案是通过返回值判断是否需要继续递归。如果返回值不为NULL,说没找到了,就不要再递归了,如果返回值为NULL,那么就继续递归。

思路有了,代码如下:

bt_node* get_tree_special_node(bt_node* root, int val)
{
	// 如果是空树或者走到空节点了,直接返回NULL
	if (root == NULL)
		return NULL;
	// 如果找到了这个特殊节点,就返回,不要再递归了
	if (equal_val_node(root, val))
		return root;
	else
	{
		// 走到这里,说明没有找到特殊节点,那么继续递归当前节点的左子树和右子树
		// 注意: 在这里我们要接受左子树的返回结果,如果左子树返回不是NULL,那么右子树就不用再递归了
		bt_node* ret_node = get_tree_special_node(root->_left, val);
		// 如果左子树返回NULL,那么说明没找到,继续递归它的右子树
		if (ret_node == NULL)
			ret_node = get_tree_special_node(root->_right, val);
		return ret_node;
	}
}

 为了更立即的理解,我们利用递归展开图加深对上面的理解程序的理解

 

4.2.8. 二叉树的销毁

如图所示,我们要销毁这个链表,该如何销毁?

 思路: 由销毁链表需要释放节点,如果先将根节点释放了,就需要在释放之前将它的左节点和右节点进行保存(前序的思想);但是我们在这里不采用,而是利用后序的思想,先释放我们的左子树和右子树,最后再释放我们的根节点

例如在这里,我们释放的顺序是4->1->2->6->5

void tree_destroy(bt_node* root)
{
	if (root == NULL)
		return;
	else
	{
		// 依据我们的后续思想(LRN)
		// 先找当前节点的左子树
		tree_destroy(root->_left);
		// 再找当前节点的右子树
		tree_destroy(root->_right);
		// 释放当前节点
		free(root);
	}
}

 为了更好地理解,我们利用递归展开图加深理解:

4.2.9. 二叉树的层序遍历

二叉树的层序遍历是一种广度优先搜索的遍历方式,也被称为宽度优先搜索(BFS)。它按照从上到下、从左到右的顺序逐层访问二叉树的节点。

我们可以利用队列的先进先出的性质完成层序遍历。

  1. 从根节点开始,如果根节点不为NULL ,将根节点放入队列中。
  2. 当队列不为空时,重复以下步骤:
    • 队列中的队头节点作为当前节点。
    • 访问当前节点。
    • 如果当前节点有左子节点,则将左子节点放入队列中。
    • 如果当前节点有右子节点,则将右子节点放入队列中。
    • 出队头
  3. 遍历完所有节点后,即可得到二叉树的层序遍历序列

层序遍历的结果是一个按照层级顺序排列的节点值序列。这种遍历方式可以帮助我们逐层地处理二叉树的节点,常用于查找二叉树的最大/最小深度、判断两个二叉树是否相等等问题。

void level_order(bt_node* root)
{
	Queue Qu;
	QueueInit(&Qu);
	if (root != NULL)
	{
		QueuePush(&Qu,root);
	}
	while (!QueueEmpty(&Qu))
	{
		// 1. 队列中的队头节点作为当前节点。
		bt_node* front = QueueFront(&Qu);      
        // 2. 只要左子树和右子树不为NULL,就入左子树、右子树
		if (front->_left != NULL)
			QueuePush(&Qu, front->_left);
		if(front->_right != NULL)
			QueuePush(&Qu, front->_right);
        //3.打印队头数据
		printf("%d ",front->_val);
		//4.出队头
        QueuePop(&Qu);
	}
	QueueDestroy(&Qu);
}

4.2.10. 判断二叉树是否是完全二叉树

要解决这个问题,首先我们要知道,完全二叉树是从上到下,从左到右连续的一个二叉树。

那么如何判断呢:

非空节点是连续,就是完全二叉树

非空节点是不连续的,中间存在空节点,就不是完全二叉树

有了判断方法,我们的实现思路也就有了:

根据层序遍历的方式,将树的每一层节点都push到队列里,如果遇到NULL就终止循环,进而判断队列里面的剩余数据,如果遇到有效数据则说明不是完全二叉树,否则就是完全二叉树。

代码如下:

bool is_complete_binary_tree(bt_node* root)
{
	bt_node* cur = root;
	Queue Qu;
	QueueInit(&Qu);
	if (cur != NULL)
		QueuePush(&Qu, cur);
	while (!QueueEmpty(&Qu))
	{
		//1.取对头数据
		bt_node* front = QueueFront(&Qu);
		//2. 如果遇到空节点就退出循环
		if (front == NULL)
			break;
		//3. 入队头数据的左孩子和右孩子(包括空节点)
		QueuePush(&Qu, front->_left);
		QueuePush(&Qu, front->_right);
		//4. 出对头数据
		QueuePop(&Qu);
	}
	// 走到这里说明遇到NULL了
	while (!QueueEmpty(&Qu))
	{
		//继续取队列的剩余数据
		// 如果还有非空数据那么就说明不是完全二叉树,return false
		bt_node* front = QueueFront(&Qu);
		if (front != NULL)
		{
			QueueDestroy(&Qu);
			return false;
		}
		QueuePop(&Qu);
	}
	// 走到这里说明没有在队列里遇到非空数据,那么就说明是完全二叉树,return true
	QueueDestroy(&Qu);
	return true;
}

4.3. 二叉树的练习题

4.3.1. 单值二叉树

965. 单值二叉树 - 力扣(LeetCode)

思路:将根和根节点的左子树以及根和根节点的右子树进行比较,只要它们分别相等,就说明是单值二叉树

注意可以将空节点和任意有效节点认为是单值节点。

class Solution {
public:
    // 两个节点的比较函数
    bool equal_val_node(TreeNode* node1,TreeNode* node2)
    {
        // 只要其中一个节点为nullptr,那么就可以认为是单值节点
        if(node1 == nullptr || node2 == nullptr)
            return true;
        // 否则只有当两个有效节点的val相等,返回true
        else
        return node1->val == node2->val;
    }
    bool isUnivalTree(TreeNode* root) {
        // 空节点返回true
        if(root == NULL)
            return true;
        // 只要根节点和它的左节点不相等或者根节点和它的右节点不相等就不是单值二叉树
        if(!(equal_val_node(root,root->right) && equal_val_node(root,root->left)))
        {
            return false;
        }
        // 如果符合前面的条件,继续判断它的左子树和右子树
        else
        {
            return isUnivalTree(root->right) && isUnivalTree(root->left);   
        } 
    }
};

为了更好的理解上面的递归程序,我们利用递归展开图来加强理解:

注:由于代码量有点多,不方便画图,因此,为了更好的画递归展开图,我在这里简化一下,对于equal_val_node这个函数,它是一个判断两个结点的_val是否相同,因此我们将它给隐藏  

4.3.2. 相同的树

 100. 相同的树 - 力扣(LeetCode)

题干要求:

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

思路:相同的树可以转化为对两棵树的根节点、左子树、右子树分别比较。

注意:只有对应节点全部相等,才返回true;而只要其中一个不相等,就返回false;

在这里,空节点和非空节点是不相等的

有了大概思路,代码如下:

class Solution {
public:
    // 判断节点是否为nullptr
    inline bool is_empty_node(TreeNode* root)
    {
        return root == nullptr;
    }
    // 判断两个节点是否相等
    bool is_equal_node(TreeNode* left, TreeNode* right)
    {
        if(is_empty_node(left) && is_empty_node(right))
            return true;
        if(is_empty_node(left) || is_empty_node(right))
            return false;
        if(left->val == right->val)
            return true;
        else
            return false;
    }
    bool isSameTree(TreeNode* rootA, TreeNode* rootB) {
        // 都为nullptr,认为节点相等,返回true
        if(is_empty_node(rootA) && is_empty_node(rootB))
            return true;
        // 其中一个为nullptr,认为节点不相等,返回false
        if(is_empty_node(rootA) || is_empty_node(rootB))
            return false;
        // 只要满足上面的条件,就开始分别判断对应的根、左子树、右子树
        bool ret = is_equal_node(rootA,rootB)
        && is_equal_node(rootA->left,rootB->left)
        && is_equal_node(rootA->right,rootB->right);
        // 只要有一个不相等即返回值ret == 0,那么就不用继续递归左子树和右子树了
        // 如果前面的判断 ret != 0,那么就继续递归左子树和右子树
        if(ret == 1)
            return isSameTree(rootA->left,rootB->left)
        && isSameTree(rootA->right,rootB->right);
        return ret;
    }
};

为了更好的理解上面的递归程序,我们利用递归展开图来加强理解:

4.3.3. 对称二叉树

101. 对称二叉树 - 力扣(LeetCode)

解题思路:

我们之前说过,我们可以将一颗二叉树分为左子树、根、右子树 ,因此对称二叉树可以被拆分成左子树和右子树是否对称,即左子树的左节点是否和右子树的右节点以及左子树的右节点和左子树的左节点是否全部相等,如果相等,就对称。

有了思路根据思路,我们写出了如下代码

// 判断节点是否相等
inline bool is_empty_node(TreeNode* root)
{
    return root == nullptr;
}

// 仿函数 --- 判断两个节点是否同时为nullptr
class two_node_all_empty
{
public:
    bool operator()(TreeNode* rootA, TreeNode* rootB)
    {
        if(is_empty_node(rootA) && is_empty_node(rootB))
            return true;
        else
            return false;
    }
};

// 仿函数 --- 判断两个节点是否有一个为nullptr
class two_node_one_empty
{
public:
    bool operator()(TreeNode* rootA, TreeNode* rootB)
    {
        if(is_empty_node(rootA) || is_empty_node(rootB))
            return true;
        else
            return false;
    }
};

class Solution {
public:
    // 判断两个节点是否相等
    bool is_equal_val_node(TreeNode* rootA,TreeNode* rootB)
    {
        two_node_all_empty all;
        if(all(rootA,rootB))
            return true;
        two_node_one_empty one;
        if(one(rootA,rootB))
            return false;
        if(rootA->val == rootB->val)
            return true;
        else
            return false;
    }
    bool is_equal_tree(TreeNode* rootA,TreeNode* rootB)
    {
        // 如果同时为nullptr,返回true
        two_node_all_empty all;
        if(all(rootA,rootB))
            return true;
        // 只有其中一个为nullptr,返回false
        two_node_one_empty one;
        if(one(rootA,rootB))
            return false;
        // 分别比较左子树和右子树的根
        // 左子树的左节点和右子树的右节点
        // 左子树的右节点和右子树的左节点
        // 全部相等返回true
        bool ret = is_equal_val_node(rootA,rootB)
        && is_equal_val_node(rootA->left,rootB->right)
        && is_equal_val_node(rootA->right,rootB->left);
        // 如果上面的判断为true,继续递归左子树和右子树
        // 如果上面的判断为false,直接return false
        if(ret == 1)
            return is_equal_tree(rootA->left,rootB->right)
        && is_equal_tree(rootA->right,rootB->left);
        return ret;
    }
    bool isSymmetric(TreeNode* root) {
        // 空树直接返回true
        if(is_empty_node(root))
            return true;
        // 只有一个节点,返回true
        two_node_all_empty all;
        if(all(root->left,root->right))
            return true;
        // 只有两个节点,返回false,不对称
        two_node_one_empty one;
        if(one(root->left,root->right))
            return false;
        // 将该二叉树分为左子树和右子树
        // 如果左子树和右子树对称,那么原树就是对称的
        return is_equal_tree(root->left,root->right);
    }
};

4.3.4. 二叉树的前序遍历

根据题意,我们需要将前序遍历的结果存储于一个vector中,并返回;

如果是C实现,有点麻烦,因为还需要我们自己去malloc一个返回的数组(同时这个数组的大小也是未知的,因此需要我们提前获取一下二叉树的节点个数)。并且插入数据的时候还需要下标索引(下标索引,如果这个下标是一个局部变量,也会带来问题,此时我们需要传递下标的地址),有了这些考虑,我们用C代码实现一下:

// 确立节点个数,为了具体的malloc的空间大小
int get_tree_node_size(struct TreeNode* root)
{
    if(root == NULL)
        return 0;
    else 
        return get_tree_node_size(root->left) + get_tree_node_size(root->right) + 1;
}

int* _pre_order(struct TreeNode* root,int* arr,int* pi)
{
    if(root == NULL)
        return arr;
    // 根据前序的遍历规则根左右(NLR):
    else
    {
        //注意:这里较好的方式:传递下标的地址
        arr[(*pi)++] = root->val;
        _pre_order(root->left,arr,pi);
        _pre_order(root->right,arr,pi);
        return arr;
    }
} 

int* preorderTraversal(struct TreeNode* root, int* returnSize){
    *returnSize = get_tree_node_size(root);
    int* ret_arr = (int*)malloc(sizeof(int)*(*returnSize));
    assert(ret_arr);
    int i = 0;
    ret_arr = _pre_order(root,ret_arr,&i);
    return ret_arr;
}

C++的实现更简单一点,因为C++有容器的存在,不需要我们去控制空间的大小

class Solution {
public:
    // 由于这里的ret出了函数作用域,不会被销毁,因此可以用传引用返回
    vector<int>& _pre_order(vector<int>& ret,TreeNode* root)
    {
        if(root == nullptr)
            return ret;
        else
        {
            // 根据左根右的规则(NLR)
            ret.push_back(root->val);
            _pre_order(ret,root->left);
            _pre_order(ret,root->right);
            return ret;
        }
    }
    vector<int> preorderTraversal(TreeNode* root) {
        std::vector<int> ret;
        _pre_order(ret,root);
        return ret;
    }
};

4.3.5. 二叉树的中序遍历

根据题意,我们需要将前序遍历的结果存储于一个vector中,并返回;

与前序遍历没有太大差别,代码如下

class Solution {
public:
    // 因为ret出了函数作用域不会被销毁,因此可以传引用返回
    vector<int>& _in_order(TreeNode* root,vector<int>& ret)
    {
        if(root == nullptr)
            return ret;
        else
        {
            //根据中序的遍历规则左根右(LNR)
            _in_order(root->left,ret);
            ret.push_back(root->val);
            _in_order(root->right,ret);
            return ret;
        }
    }

    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ret;
        _in_order(root,ret);
        return ret;
    }
};

4.3.6. 二叉树的后序遍历

根据题意,我们需要将前序遍历的结果存储于一个vector中,并返回;

与前序遍历没有太大差别,代码如下

class Solution {
public:
    // 由于这里的ret出了函数作用域,不会被销毁,因此可以用传引用返回
    vector<int>& _post_order(TreeNode* root,vector<int>& ret)
    {
        if(nullptr == root)
            return ret;
        else
        {
            //根据后序的遍历规则左右根(LRN)
            _post_order(root->left,ret);
            _post_order(root->right,ret);
            ret.push_back(root->val);
            return ret;
        }
    }
    vector<int> postorderTraversal(TreeNode* root) {
        std::vector<int> ret;
        _post_order(root,ret);
        return ret;
    }
};

 4.3.7. 另一颗树的子树

572. 另一棵树的子树 - 力扣(LeetCode)

思路 :对于该问题,我们可以分解为其中一个的子树是否相等于另一棵树,即遍历原树的每一个节点与目标树进行比较,相等就退出。不相等就继续递归他的左子树和右子树。

代码如下:

// 判断节点是否为nullptr
inline bool is_empty_node(TreeNode* root)
{
	return root == nullptr;
}
// 判断两个节点是否相等
bool is_equal_node(TreeNode* left, TreeNode* right)
{
	if (is_empty_node(left) && is_empty_node(right))
		return true;
	if (is_empty_node(left) || is_empty_node(right))
		return false;
	if (left->val == right->val)
		return true;
	else
		return false;
}
// 判断两棵树是否相等
bool isSameTree(TreeNode* rootA, TreeNode* rootB) {
	if (is_empty_node(rootA) && is_empty_node(rootB))
		return true;
	if (is_empty_node(rootA) || is_empty_node(rootB))
		return false;
	// 只要满足上面的条件,就开始分别判断对应的根、左子树、右子树
	bool ret = is_equal_node(rootA, rootB)
		&& is_equal_node(rootA->left, rootB->left)
		&& is_equal_node(rootA->right, rootB->right);
	// 只要有一个不相等即返回值ret == 0,那么就不用继续递归左子树和右子树了
	// 如果前面的判断 ret != 0,那么就继续递归左子树和右子树
	if (ret == 1)
		return isSameTree(rootA->left, rootB->left)
		&& isSameTree(rootA->right, rootB->right);
	return ret;
}

class Solution {
public:
    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
        if(root == nullptr)
            return false;
        bool ret = 0;
        // 如果当前根节点 == 目标根节点,在判断是否是相同的树结构
        if(is_equal_node(root,subRoot))
        {
            //  分别比较两个数的根、左子树、右子树
            ret = isSameTree(root,subRoot)
            || isSameTree(root->left,subRoot)
            || isSameTree(root->right,subRoot);
        }
        // 如果上面不相等,继续递归它的左子树、右子树
        if(ret == 0)
            ret = isSubtree(root->left,subRoot) 
            || isSubtree(root->right,subRoot);
        return ret;
    }
};

4.3.8. 二叉树的最大深度

104. 二叉树的最大深度 - 力扣(LeetCode)

对于该问题,我们可以将求二叉树的最大深度转化为分别递归求左子树和右子树的深度的较大值。

注意:空树的高度为0;

步骤:

  1. 递归它的左子树和右子树
  2. 比较左子树和右子树
  3. 较大值+1,然后继续递归左子树和右子树

代码实现:

# 方案一: 中途包括了大量的重复计算,不好
// class Solution {
// public:
//     int maxDepth(TreeNode* root) {
//         if(root == nullptr)
//             return 0;
//         else
//            // 虽然结果正确,但最后会时间超限
//            // 原因:三目运算符们没有保存左右调用的结果
//            // 导致最后处理时又进行了一次递归,产生了大量的重复计算
//         {
//             return maxDepth(root->left) > maxDepth(root->right) ?
//             maxDepth(root->left) + 1 : maxDepth(root->right) + 1;
//         }
//     }
// };

# 方案二: 通过用变量保存递归的结果,避免了重复计算
// class Solution {
// public:
//     int maxDepth(TreeNode* root) {
//         if(root == nullptr)
//             return 0;
//         else
//         {
                //通过保存左右数递归的结果,避免了大量的重复计算
//             int left_high = maxDepth(root->left);
//             int right_high = maxDepth(root->right);
//             return left_high > right_high ? left_high + 1 : right_high + 1;
//         }
//     }
// };

# 方案三:通过函数的返回值避免了重复计算
class Solution {
public:
    inline int get_max_two_sum(const int& left,const int& right)
    {
        if(left < right)    return right;
        else return left;
    }
    int maxDepth(TreeNode* root) {
        if(root == nullptr)
            return 0;
        else
        {
            // 通过函数的返回值处理,避免了重复计算
            return get_max_two_sum(maxDepth(root->left),maxDepth(root->right)) + 1;
        }
    }
};

4.3.9. 翻转二叉树

226. 翻转二叉树 - 力扣(LeetCode)

解题思路就是:

结合后序的思想左右根(LRN) ,通过分别递归它的左子树和右子树,递归每一个节点时,交换它的左子树和右子树

代码如下:

class Solution {
public:
    // 判断节点是否为nullptr
    inline bool is_empty_node(TreeNode* node)
    {
        return node == nullptr;
    }
    // 交换两个有效节点
    void swap_two_node(TreeNode*& left,TreeNode*& right)
    {
        TreeNode* tmp = left;
        left = right;
        right = tmp;
    }

    void reverse_tree(TreeNode* tree_node)
    {
        //如果遇到nullptr,直接返回
        if(tree_node == nullptr)
            return ;
        else
        {
            // 结合后续的思想:左右根(LRN)
            reverse_tree(tree_node->left);
            reverse_tree(tree_node->right);
            swap_two_node(tree_node->left,tree_node->right);
        }
    }

    TreeNode* invertTree(TreeNode* root) {
        // 如果是空树,直接返回nullptr
        if(is_empty_node(root))
            return nullptr;
        // 如果只有根节点一个节点,直接返回root
        if(root->left == nullptr && root->right == nullptr)
            return root;
        // 如果有两个及两个以上的节点,进行翻转处理
        else
            reverse_tree(root);
        return root;
    }
};

为了更好地理解,我们画一下下面的图的递归展开图:

 

 

4.3.10. 二叉树遍历

二叉树遍历_牛客题霸_牛客网 (nowcoder.com)

题意:读取用户输入的字符串(前序遍历的结果),让用户构建一个二叉树,并输出其中序结果;

解题思路就是:根据字符串,利用前序的思想构建二叉树

注意:‘#’代表为nullptr,如果遇到'#',就返回上级栈帧

代码如下:

 

#include <iostream>
template<class T>
struct binary_tree_node
{
    binary_tree_node<T>* _left;
    binary_tree_node<T>* _right;
    T _key;
    binary_tree_node(const T& key)
    : _left(nullptr)
    ,_right(nullptr)
    ,_key(key)
    {}
};

binary_tree_node<char>* build_binary_tree(const std::string& str,int& i)
{
    //如果遇到'#',就返回到上级栈帧
    if(str[i] == '#'){
        ++i;
        return nullptr;
    }
    //构建新节点
    binary_tree_node<char>* root = new binary_tree_node(str[i++]);
    // 通过前序的思想根左右(NLR):
    // 递归子树,并通过返回值构成链接关系
    root->_left = build_binary_tree(str,i);
    root->_right = build_binary_tree(str,i);
    return root;
}

template<class T>
void in_order(binary_tree_node<T>* root)
{
    if(root == nullptr)
        return ;
    else
    {
        in_order(root->_left);
        std::cout << root->_key << " ";
        in_order(root->_right);
    }
}

int main()
{
    std::string st;
    std::cin>>st;
    int i = 0;
    // 构建二叉树
    binary_tree_node<char>* node = build_binary_tree(st,i);
    // 中序遍历
    in_order(node);
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值