【数据结构】二叉树

在这里插入图片描述每一个不曾起舞的日子,都是对生命的辜负。

1. 二叉树的链式结构

二叉树节点的结构:

typedef int BTDataType;
typedef struct BinaryTreeNode
{
    BTDataType data;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
}BTNode;

二叉树是:

  1. 空树
  2. 非空:根节点,根节点的左子树、根节点的右子树组成的。

因此,为了快速闯创建二叉树,我们以下图为模板:(注:下述遍历以此图讲述)

在这里插入图片描述

将其用代码实现:

BTNode* CreateTree()//创建二叉树
{
	BTNode* n1 = (BTNode*)malloc(sizeof(BTNode));
	assert(n1);
	BTNode* n2 = (BTNode*)malloc(sizeof(BTNode));
	assert(n2);
	BTNode* n3 = (BTNode*)malloc(sizeof(BTNode));
	assert(n3);
	BTNode* n4 = (BTNode*)malloc(sizeof(BTNode));
	assert(n4);
	BTNode* n5 = (BTNode*)malloc(sizeof(BTNode));
	assert(n5);
	BTNode* n6 = (BTNode*)malloc(sizeof(BTNode));
	assert(n6);

	n1->data = 1;
	n2->data = 2;
	n3->data = 3;
	n4->data = 4;
	n5->data = 5;
	n6->data = 6;
	

	n1->left = n2;
	n1->right = n4;
	n2->left = n3;
	n2->right = NULL;
	n3->left = NULL;
	n3->right = NULL;
	n4->left = n5;
	n4->right = n6;
	n5->left = NULL;
	n5->right = NULL;
	n6->left = NULL;
	n6->right = NULL;

	
	return n1;
}

从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。

2. 二叉树的遍历

前中后序遍历是按照递归实现。层序遍历

2.1 二叉树的前序遍历

前序遍历:即按照:根->左子树->右子树 的顺序访问。

意为访问一条路上所有的根,再访问所有的左子树,最后访问所有的右子树。

前序遍历递归图解:

在这里插入图片描述

访问的顺序为 : 1、2、3、NULL、NULL 、NULL、4、5、NULL 、NULL、6、NULL、NULL

即按照递归调用的实际顺序:

在这里插入图片描述

代码实现:

//二叉树前序遍历
void PreOrder(BTNode* root)
{
    if (root == NULL)//递归结束条件
	{
		printf("NULL ");
		return;
	}

    printf("%d ", root->data);//先打印
    PreOrder(root->left);//左子树遍历
    PreOrder(root->right);//右子树遍历
}

在这里插入图片描述

有的描述中可能不含有NULL,碰到root == NULL 直接return,但是打印NULL这样才能让我们更容易理解。

对于递归这里,就个人而言,由于每一步递归的方式都相同。我更喜欢把它进行形象化的假设。就此结构,有多个分支,6个节点,但是我会将他看成一个根节点和两个子树:

)(C:\Users\Dell\AppData\Roaming\Typora\typora-user-images\image-20220814175004228.png)]

或者更加精确一点,即一个根节点,两个树叶:

在这里插入图片描述

如果只看成这样,那就是先打印根,再按照相同的方式访问left、right即可

将这个以前序遍历打印的形式实现的话,代码是这样:

printf("%d ", root->data);//先打印
    printf("%d ", root->left->data);//左子叶打印
    printf("%d ", root->right->data);//右子叶打印

再看一下前序遍历的主体代码:

    printf("%d ", root->data);//先打印
    PreOrder(root->left);//左子树遍历
    PreOrder(root->right);//右子树遍历

不难发现,两个的区别就是在left和right时的方式不一样,一个是递归,一个是直接打印,但终究的思想还是一样的。将思路化繁为简,把递归看成一步,再由这一步通过递归化成无数步,才是递归的精髓。由于递归不能一直进行,因此才有了root == NULL的结束条件。

即递归有两个必要的条件:

  1. 递归的方式(如何递归)
  2. 截止条件(通过题干了解什么时候该从下往上返回)

2.2 二叉树的中序遍历

中序遍历:即按照: 左子树->根->右子树 的顺序访问。

在这里插入图片描述

仍然是这个二叉树,根据中序遍历的条件,先访问的应该是1的左子树,然后访问1,最后访问1的右子树。当我们看到2也就是1的左子树的根时,发现2仍然作为左子树的根,因此,访问时应该先访问2的左子树,再访问2,最后访问2的右子树。当我们看到3也就是2的左子树的根时,其左子树右子树都为空,但仍相当于有左右子树,仍然先访问3的左子树,再访问3,最后访问3的右子树。因此,左子树->根->右子树的打印的顺序如下:

在这里插入图片描述

①-> ②-> ③-> ④-> ⑤-> ⑥-> ⑦-> ⑧-> ⑨-> ⑩-> ⑪-> ⑫-> ⑬

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

事实上,中序遍历的代码也与所描述的递归顺序一样,对比前序,只有顺序上的差别。

二叉树的中序遍历的风格与前序遍历相同,只不过在顺序上有了变化,通过对于前序遍历递归的理解,这里采用上述提到的由简到繁的思维帮助实现。

先思考一下只有两层的二叉树:

在这里插入图片描述

那么代码打印的方式为:

void InOrder(BTNode* root)
{
	printf("%d ", root->left->data);//打印左孩子
	printf("%d ", root->data);//打印根
	printf("%d ", root->right->data);//打印右孩子
}

通过一层的方式改变成多层,因此改成递归,将打印替换成递归,并加上截止条件:

//二叉树中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	InOrder(root->left);//将left的改成递归
	printf("%d ", root->data);
	InOrder(root->right);//将right的改成递归
}

在这里插入图片描述

通过上述的一系列探讨,为什么前序中序都这么改递归呢,为什么改左右呢?当然了,这个问题的答案是一边写一边思考的,写到这里也算是发现了其中的一些规律吧。首先,对于递归,看重的除了递归方式和截止条件之外,还需要看该递归函数传的参数,此代码参数为root,第一次调用的即我们所认为的根,对于每层调用的参数本身(即root),是需要进行你想要的运算的,毕竟是你传进来的参数,而当我们在这层函数中继续传参,按照此代码的root->leftroot->right 继续传进去之后,一样会变成下一层的root ,因此,才说只需要将每一层的root该有的运算处理好,其左右的子树一样会处理好!

2.3 二叉树的后序遍历

中序遍历:即按照: 左子树->右子树->根 的顺序访问。

在这里插入图片描述

①-> ②-> ③-> ④->⑤-> ⑥-> ⑦-> ⑧-> ⑨-> ⑩-> ⑪->⑫->⑬

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

通过前面的大量探讨及描述,后序遍历直接写也已经可以写出来了,方式及思想与前面相同,可以直接写,也可以先按照两层的写,再改成递归。那么重复的话也不多说,直接上代码:

//二叉树的后序遍历
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

在这里插入图片描述

2.4 二叉树的层序遍历(难点)

层序遍历: 除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

在这里插入图片描述

那么如何进行层序遍历呢?

这里直接引入:利用队列

队列具有先入先出的特性,当遍历二叉树的节点不为空,就将这个节点指针存入队列,当打印这个节点指向的数据时,由于遍历的特性,将这个节点指向的leftright拽到队列中,再弹出这个root 。那么我们具体展开试试。

在这里插入图片描述

因此,像如图这样的流程思想,同时注意需要强调理解的,我们则需要将队列的Queue.h,Queue.c引入。(与之前文章的队列对比,这个队列的data的类型变成了BTNode*.

Queue.h

#pragma once

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

typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;//只是将原data的int类型变成了这个节点的指针类型

typedef BTNode* QDataType;
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType data;
}QNode;

typedef struct Queue
{
	//int size;
	QNode* head;
	QNode* tail;
}Queue;

void QueueInit(Queue* pq);
void QueueDestory(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
bool QueueEmpty(Queue* pq);
int QueueSize(Queue* pq);

Queue.c的功能不变,这里就不引入了。(队列的文章里面有)

接下来就是层序遍历的代码:

void TreeLevelOrder(BTNode* root)//层序遍历,利用队列
{
	//队列是用链表实现的,队列节点的data保存的是二叉树节点的指针
	Queue q;

	QueueInit(&q);

	if (root)
		QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		printf("%d ", front->data);

		if (front->left)//递归思想
			QueuePush(&q, front->left);
		
		if (front->right)//递归思想
			QueuePush(&q, front->right);
	}
	printf("\n");

	QueueDestory(&q);
}

在这里插入图片描述

3. 二叉树节点的个数

对于求二叉树节点个数而言,为了遍历所有节点,仍需要递归。当然仍然可以利用我上文提到的思想:化繁为简,由简到繁 (emm……自己编的句子哈哈哈)化繁为简当然就是把n层就看成两层的思想,由简到繁就是将两层的代码以递归的方式写出来,就会变成n层。

即先看看两层:在这里插入图片描述

通过化繁为简,那么代码就可以写成这样:

int TreeSize(BTNode* root)
{
	return 1+1+1;//可按顺序看成root->left+root->right+root
}

再由简到繁,就是改成递归,求n层的节点个数:在这里插入图片描述

//二叉树节点的个数
int TreeSize(BTNode* root)
{
    if(root == NULL)
        return 0;
    
	return TreeSize(root->left) + TreeSize(root->right) + 1;
    //即相对于参数root,每层只针对root计数,root->left/right在递归中的下一层栈帧一定也会被调用。
}

当然,这里也可以采用static亦或是全局变量的形式,不过都有缺点,static的缺点是只有第一次是正确的的调用,全局变量则是每次计算之后都需要手动将其置为0。

4.二叉树叶子节点的个数

对于求叶子结点的个数,应该采取分治的思想。举个例子:有一位校长,两个院长,四个辅导员,八个班长,十六名普通同学,现在校长想统计普通同学的个数,让其下面的两个院长去计数,而每一位院长又让其直系的两个辅导员去计数,辅导员又让班长计数,最后经过班长通知,普通同学因此自己报上名字。这个例子中的普通同学就代表叶子结点,因此,只要遍历到最后一层的数,我们就返回1,相当于每一位同学报名字,除此之外不加1,因为只统计同学的数量,因此,代码可以通过这种思路实现了:

在这里插入图片描述

//求叶子节点个数(即求度为0的节点的个数)
int TreeLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;

	if (root->left == NULL && root->right == NULL)
		return 1;

	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

在这里插入图片描述

5.二叉树第k层节点的个数

这个问题相当于求叶子节点个数的进阶版本,因为当k为最后一层的代表时,即变成了求叶子节点的个数。事实上,与上一个函数相比,这个仅仅多了一次限制,假设k经过每一层减1,那么当k=1时,就得返回此层节点的个数了,对于每一个节点来说,同样与上一个相同,返回1。因此,代码可以这样来写:

在这里插入图片描述

int TreeLevel(BTNode* root, int k)
{
	assert(k > 0);
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;

	k--;
	return TreeLevel(root->left, k) + TreeLevel(root->right, k);

}

在这里插入图片描述

6.二叉树前k层节点的个数

在这里插入图片描述

相比较前一个函数,此函数仍然可以使用化繁为简的思想,即让根+左子树+右子树,直到k=1为止。思想上比上一个多加了子树的根。

int TreeLevelSum(BTNode* root, int k)
{
	assert(k > 0);
	if (root == NULL)
		return 0;

	if (k == 1)
		return 1;
	k--;
	return TreeLevelSum(root->left, k) + TreeLevelSum(root->right, k) + 1;//左+右+根
}

在这里插入图片描述

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

在这里插入图片描述

按照前序遍历的思想去实现,因为左边如果找到了,没必要再去右面找,此外,对于查找,与上述代码的思想类似,即看成两层,将其改成递归:

BTNode* TreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return root;


	if (TreeFind(root->left, x))
		return TreeFind(root->left, x);
    if (TreeFind(root->right, x))
		return TreeFind(root->right, x);

	return NULL;
}

当然也可以设置一个临时变量:

BTNode* TreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return root;

	BTNode* lret = TreeFind(root->left, x);
	if (lret)
		return lret;
	BTNode* rret = TreeFind(root->right, x);
	if (rret)
		return rret;

	return NULL;
}

在这里插入图片描述

8. 求二叉树的最大深度

为了突出最大深度,这里让其创建节点的时候2多加一个节点,即如下图:

在这里插入图片描述

仍然按照文章中突出的思想,看成两层,比较其长度,返回大的那个,即:

int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;

	if (root->left == NULL && root->right == NULL)
		return 1;

	 

	return TreeHeight(root->left) + 1 > TreeHeight(root->right) + 1 ? TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;

}

为了不让后面的长度过长影响美观,可以这样:

//求最大深度
int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;

	int lefthight = TreeHeight(root->left);
	int righthight = TreeHeight(root->right);

	return lefthight > righthight ? lefthight + 1 : righthight + 1;
}

在这里插入图片描述

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

在这里插入图片描述

对于此图,不是一个二叉树,那么如何判断呢这个不为二叉树呢?

当我们把此二叉树的节点一一罗列(指针不为空的以其指向的data代替)

按照前序遍历:(将最后一层的左右孩子也包括在内)

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

那完全二叉树又是怎样罗列的呢?下面给个完全二叉树的图同样按照前序遍历来看看:

在这里插入图片描述

1->2->4->3->6->NULL->NULL->NULL->NULL->NULL->NULL

这时候我们会发现,完全二叉树的罗列过程中,只要出现了第一个空,后面就全为空,对于非完全二叉树,出现了第一个空之后,后面也会出现非空的节点,因此,二者的区别我们就看出来了,对于这种问题,仍然需要以队列的方向去考虑,即如层序遍历一样,先Push一个,当Pop掉时,让其将两个孩子拽到队列里面来,唯一区别就是节点为空也要拽入,上面的层序遍历已经提到,队列的data储存的是节点指针,即便为空,也能储存。

代码:

//判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)
		{
			break;
		}
		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front != NULL)
		{
			QueueDestory(&q);
			return false;
		}
	}
	QueueDestory(&q);
	return true;
}

在这里插入图片描述

10. 销毁二叉树

销毁二叉树经过上面的各种描述应该很容易理解了

//销毁树 思路:找到左右子树之后再销毁本身,否则找不到子树
void BinaryTreeDestroy(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}

	BinaryTreeDestroy(root->left);
	BinaryTreeDestroy(root->right);
	free(root);
}

11. 由二叉树的遍历序列构造二叉树

这里是后续加上的pdf笔记

12. 总结

本文章所讲的二叉树仅仅是入门的二叉树,但相比之前学的,这里的递归二叉树仍是大家面对的一个前所未有的难点,但是只要记住这里的化繁为简,由简到繁的思想,碰到层数多的二叉树,就想象成两层去做,这样才能把握住其中的奥妙,而二叉树的oj题,我打算单独写成一篇,目的就是让大家先掌握这些基本的函数再去训练,否则效果不会很明显,让我们一起攻克难关,拿捏二叉树!

  • 30
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 30
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天都要进步呀~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值