数据结构之二叉树

片头

嗨!小伙伴们,大家好,今天我们来学习二叉树这种数据结构,学习二叉树之前,我们先了解一下树的概念做个铺垫,Ready Go ! ! !

一、树

1. 树概念及结构

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

观察上图,我们可以发现:

①有一个特殊的结点,称为根节点,根节点没有前驱结点

②除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、.......、Tm,其中每一个集合Ti (1<=i<=m)又是一颗结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继(BEFJ为一颗子树,CGKL为一颗子树,DHIM为一颗子树)

③因此,树是递归定义的。

这里有一个注意点:在树形结构中,子树之间不能有交集,否则就不是树形结构

1.2 树的相关概念

(1)节点的度:一个节点含有的子树的个数称为该节点的度。例如A节点的度为6。

(2)叶节点或终端节点:度为0的节点。例如:B、C、H、I、P...等节点为叶节点(没有孩子的节点)

(3)分支节点或非终端节点:度不为0的节点。例如:D、E、F、G...等节点为分支节点

(4)双亲节点或父节点:如果一个节点含有子节点,则该节点称为其子节点的父节点。例如:A是B的父节点。

(5)孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点。例如:B是A的子节点。

(6)兄弟节点:具有相同父节点的节点互为兄弟节点。例如:B和C是兄弟节点。

(7)树的度:一个树中最大的节点的度就是树的度。例如:A的度为6,它的度最大,则树的度为6。

(8)节点的层次:从根开始定义,根为第1层,根的子节点为第2层,以此类推。

(9)树的高度或深度:树中节点的最大层次。如上图:树的高度为4。

(10)堂兄弟节点:父节点在同一层的节点互为堂兄弟节点。如上图:H和K互为堂兄弟节点。

(11)节点的祖先:从根到该节点所经分支上的所有节点。例如:A是所有节点的祖先。

(12)子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:A是所有节点的祖先。

(13)森林:由m(m>0)棵互不相交的树的集合称为森林。


二、二叉树

2.1 二叉树的概念

一颗二叉树是结点的一个有限集合,该集合:

1. 或者为空

2. 由一个根节点加上两棵分别称为左子树和右子树的二叉树组成

从上图中可以看出:

1. 二叉树不存在度大于2的结点

2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

注意:对于任意的二叉树都是由以下几种情况复合而成:

2.2 现实中的二叉树

2.3 特殊二叉树

1. 满二叉树:一个二叉树,如果每一层的节点数都达到最大值,则这个二叉树就是满二叉树。换句话说,如果一个二叉树的层数为K,且节点总数是2^{k}-1 ,则它就是满二叉树。(简便记忆:每层结点都是满的二叉树)

2.完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树引申出来的。对于深度为K,有n个节点的二叉树,当且仅当其每一个节点都与深度为K的满二叉树中编号从1到n的节点一一对应时称之为完全二叉树。要注意的是:满二叉树是一种特殊的完全二叉树。(简便记忆:前n-1层都是满的,最后一层可以不满,但是一定是连续的)

满二叉树【补充】:满二叉树每一层都是满的;完全二叉树最后一层可以不满,但是从左到右必须是连续的

2.4 二叉树的性质 

(1)若定义根节点的层数为1,则一棵非空二叉树的第i层上最多有2^{(i-1)}个结点。

(2)若定义根节点的层数为1,则深度为h的二叉树的最大结点数是2^{h}-1

(3)若定义根节点的层数为1,则具有n个结点的满二叉树的高度h = \log {_{2}}{(n+1)} (ps: h = \log {_{2}}{(n+1)}是log以2为底, n+1为对数)

(4)对任何一棵二叉树,如果度为0,其叶节点个数为n_{0},度为2的分支节点个数为n_{2}, 则有 n_{0} =n _{2} + 1

(5)对于具有n个节点的完全二叉树,如果按照从上到下,从左到右的数组顺序对所有节点从0开始编号,则对于序号为i的节点:

  • 若 i > 0,其父节点序号为 (i-1)/2 ;i = 0, i 为根节点编号,无父节点
  • 若 2i + 1 < n, 左孩子序号:\left ( 2i+1 \right ), 若 2i + 1 >= n ,无左孩子
  • 若 2i + 2 < n, 右孩子序号:\left ( 2i+2 \right ), 若 2i + 2 >= n ,无右孩子
2.5 二叉树的存储结构

二叉树一般可以使用2种结构存储,一种是顺序结构,另一种是链式结构。

(1) 顺序存储

顺序结构存储就是使用数组来存储数据,一般使用数组只适合表示完全二叉树,因为非完全二叉树不连续,会造成空间的浪费。

在前面的数据结构之堆中我们提到过,堆也是一种完全二叉树,而平时生活中也只有堆会用数组存储,存储顺序对应二叉树的性质五

二叉树的顺序存储在物理上是一个数组,而在逻辑结构上是一棵二叉树。

(2)链式存储

用链表来表示一棵二叉树,即用链来指示节点之间的逻辑关系。

一般情况下,链表中每个节点由3个域组成:数据域和左右指针域,左右指针分别指向左孩子和右孩子所在的节点地址。链式结构存储又分为二叉链和三叉链,当前我们主要使用二叉链。后面学的内容才会涉及到三叉链。

代码如下:

typedef int BTDataType;
//二叉链
typedef struct BinaryTreeNode
{
        BTDataType data;//节点的数据域
        struct BinaryTreeNode* left;//指向左孩子
        struct BinaryTreeNode* right;//指向右孩子
}BTNode

三、二叉树的相关操作

在学习二叉树的基本操作前,需要先创建一棵二叉树,然后才能学习相关的基本操作。由于现在大家对二叉树结构还不够深入,为了降低学习成本,我们来手撕一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树的真正的创建方式。

3.1 创建一棵二叉树

要创建二叉树,我们要先写一个创建二叉树新结点的函数

//创建一个新结点
BTNode* BuyBTNode(BTDataType x) {
	BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));//创建一个新结点
	if (newNode == NULL)//内存空间不足,创建失败
	{
		perror("malloc fail!\n");
		exit(1);
	}
	newNode->data = x;//将x赋值给新结点的数据域
	newNode->left = NULL;//假设新结点的左孩子指向NULL
	newNode->right = NULL;//假设新结点的右孩子指向NULL
	return newNode;//将新结点返回
}

然后我们用代码实现下图的二叉树

//手搓一棵树
BTNode* CreateTree() {
	BTNode* n1 = BuyBTNode(1);//创建一个新结点n1
	BTNode* n2 = BuyBTNode(2);//创建一个新结点n2
	BTNode* n3 = BuyBTNode(3);//创建一个新结点n3
	BTNode* n4 = BuyBTNode(4);//创建一个新结点n4
	BTNode* n5 = BuyBTNode(5);//创建一个新结点n5
	BTNode* n6 = BuyBTNode(6);//创建一个新结点n6

	n1->left = n2;//n1的左孩子是n2
	n1->right = n4;//n1的右孩子是n4
	n2->left = n3;//n2的左孩子是n3
	n4->left = n5;//n4的左孩子是n5
	n4->right = n6;//n4的右孩子是n6

	return n1;//将n1结点返回
}

上面的方法是最简便的方法,常用于oj题,手撕一棵二叉树 

有没有其他方法来创建一棵二叉树呢?当然有! 哈哈哈哈,我们一起来康康~

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* TreeCreate(BTDataType* a, int* pi);

emmmm,我试着画了这棵树

 这里提供了一个函数,我们需要通过这个函数来构造"ABD##E#H##CF##G##"这棵二叉树,我们需要运用前序遍历的知识,一起来试试呗!

首先, 因为要通过数组才能构建二叉树,肯定要定义一个数组吧?要不然,巧妇难为无米之炊,哈哈哈哈

在main函数中,①定义数组的代码如下: (假设这个数组可以存放100个char类型的元素), ②我们是从键盘录入,因此用scanf函数来获取字符串

char a[100];
scanf("%s",&a)

其次,因为我们需要构建二叉树,所以必须有二叉树节点的结构体(内容同上)

typedef char BTDataType;    //假设重命名char为BTDataType
typedef struct BinaryTreeNode{     //二叉树节点的结构体
    BTDataType data;               //节点的数据域
    struct BinaryTreeNode* left;   //节点的左孩子
    struct BinaryTreeNode* right;  //节点的右孩子
}BTNode;

 接着,我们需要在main函数中定义一个根节点root,来接收TreeCreate函数的返回值。我们还可以对构建好的这棵二叉树进行中序遍历

int i = 0;                      //数组下标i从0开始
BTNode* root = TreeCreate(a,&i);//根节点root来接收TreeCreate函数的返回值
InOrder(root);                  //对构建好的这棵二叉树进行中序遍历

 好啦,main函数里面内容我们都写好了,接下来就是要实现TreeCreate这个函数咯~

怎么用数组构建二叉树呢?很简单,就是将数组里面的元素依次拷贝到二叉树节点的数据域中如果遇到 '#' ,说明当前节点为空,下标i指向下一个元素,同时返回NULL;拷贝完当前节点后,继续将数组里面的元素拷贝到当前节点的左孩子和右孩子中直到读取完整个字符串

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* TreeCreate(BTDataType* a, int* pi){

    //如果遍历到的当前下标对应的元素恰好为'#',说明该节点为空
    //让下标i继续往前走,同时返回NULL
    if(a[(*pi)] == '#'){
        (*pi)++;
        return NULL;
   }
    
    //当前下标对应的元素是有效的
    //开辟一个空间,存放新节点
    //节点的数据域为当前下标i对应的数组元素,完成该节点后,下标i自增一次
    //递归创建节点的左孩子和右孩子
   BTNode* newNode =(BTNode*) malloc(sizeof(BTNode));
   newNode->data = a[(*pi)++];
   newNode->left = TreeCreate(a,pi);
   newNode->right = TreeCreate(a,pi);
    
    //返回当前节点
   return newNode;
}

 中序遍历的代码如下: (待会儿重点讲解哈~,这里看不懂的话,莫捉急)

//二叉树的中序遍历
void InOrder(BTNode* root){
        if(root == NULL){          //如果当前节点为空
           printf("N ");           //打印N
           return;                 //返回上一层
    }
        InOrder(root->left);       //递归当前节点的左子树
        printf("%c ",root->data);  //打印当前节点的数据域
        InOrder(root->right);      //递归当前节点的右子树
}

OK,整体代码如下:

//二叉树结点的结构体
typedef char BTDataType;
typedef struct BTNode {
	BTDataType data;
	struct BTNode* left;
	struct BTNode* right;
}BTNode;

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* TreeCreate(BTDataType* a, int* pi) {
	if (a[*pi] == '#') {
		(*pi)++;
		return NULL;
	}
	BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
	if (newNode == NULL) {
		perror("malloc fail!\n");
		exit(1);
	}
	newNode->data = a[(*pi)++];
	newNode->left = TreeCreate(a, pi);
	newNode->right = TreeCreate(a, pi);

	return newNode;
}


//中序遍历
void InOrder(BTNode* root) {
	if (root == NULL) {
		printf("N ");
		return;
	}
	InOrder(root->left);
	printf("%c ", root->data);
	InOrder(root->right);
}


int main(){
    
    char a[100];
	printf("请输入字符串:\n");
	scanf("%s", &a);//ABD##E#H##CF##G##
	int i = 0;
	BTNode* root = TreeCreate(a, &i);
	InOrder(root);

   return 0;
}

运行结果如下:

3.2 二叉树的遍历

二叉树的遍历即按照一定的顺序依次对二叉树中的节点进行相关操作,且每个节点只操作一次

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

1. 前序遍历/先序遍历:访问根节点的操作发生在遍历其左右子树之前。

2. 中序遍历:访问根节点的操作发生在遍历其左右子树之中(间)。

3. 后序遍历:访问根节点的操作发生在遍历其左右子树之后。

由于被访问的节点必是某子树的根,所以N(Node)、L(Left subtree) 和 R(Right subtree) 可解释为根,根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

//二叉树的前序遍历
void PreOrder(BTNode* root);
//二叉树的中序遍历
void InOrder(BTNode* root);
//二叉树的后序遍历
void PostOrder(BTNode* root);
(1)前序遍历/先序遍历

先访问根节点,再依次访问左子树和右子树,即为前序遍历。

前序遍历的代码如下:

//前序遍历
void PreOrder(BTNode* root) {
	if (root == NULL) {
		printf("N ");//遇到空结点打印NULL
		return;
	}
	printf("%d ", root->data);//数据域
	PreOrder(root->left);//左孩子
	PreOrder(root->right);//右孩子
}

我们测试一下代码:

(2)中序遍历

先访问左子树,再访问根节点,最后访问右子树,即为中序遍历

中序遍历的代码如下:

//中序遍历
void InOrder(BTNode* root) {
	if (root == NULL) {
		printf("N ");//遇到空节点就打印NULL
		return;
	}

	InOrder(root->left);//左孩子
	printf("%d ", root->data);//数据域
	InOrder(root->right);//右孩子
}

我们可以发现,中序遍历和前序遍历只是将代码交换一下即可,后面的后序遍历也是一样。

我们测试一下代码:

 (3)后序遍历

先访问左子树和右子树,最后再访问根节点,即为后序遍历

后序遍历的代码如下:

//后序遍历
void PostOrder(BTNode* root) {
	if (root == NULL) {
		printf("N ");//遇到空节点就打印NULL
		return;
	}
	PostOrder(root->left);//左孩子
	PostOrder(root->right);//右孩子
	printf("%d ", root->data);//数据域
}

我们测试一下代码:

 (4)层序遍历 

从上到下,从左到右逐层访问树的节点的过程就是层序遍历。

与前中后序不同的是,层序遍历不使用递归,而是使用队列来实现。

队列中不是直接存放节点的值,而是存放节点的地址。

其核心思路是:上一层的节点出队列时带入下一层的节点入队列

在写层序遍历的时候,我们把之前的队列搬过来即可

注意啦: 在队列里面存放的是存放节点的地址!因此,我们需要修改添加进队列的元素类型。

typedef struct BTNode*  ElemType;
typedef struct QNode {
	ElemType data;
	struct QNode* next;
}QNode;

typedef struct Queue {
	QNode* front;
	QNode* rear;
	int size;
}Queue;

Queue.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef struct BTNode*  ElemType;
typedef struct QNode {
	ElemType data;
	struct QNode* next;
}QNode;

typedef struct Queue {
	QNode* front;
	QNode* rear;
	int size;
}Queue;

//初始化
void QueueInit(Queue* q);
//进队列
void QueuePush(Queue* q, ElemType x);
//出队列
void QueuePop(Queue* q);
//获取队头元素
ElemType QueueTop(Queue* q);
//获取队尾元素
ElemType QueueBack(Queue* q);
//队列的大小
int QueueSize(Queue* q);
//队列判空
int QueueEmpty(Queue* q);
//队列销毁
void QueueDestroy(Queue* q);

Queue.c

#include"Queue.h"
//初始化
void QueueInit(Queue* q) {
	assert(q);
	q->front = NULL;
	q->rear = NULL;
	q->size = 0;
}
//进队列
void QueuePush(Queue* q, ElemType x) {
	assert(q);
	QNode* newNode = (QNode*)malloc(sizeof(QNode));
	if (newNode == NULL) {
		perror("malloc fail!\n");
		exit(1);
	}
	newNode->data = x;
	newNode->next = NULL;
	if (q->front == NULL) {
		q->front = q->rear = newNode;
	}
	else {
		q->rear->next = newNode;
		q->rear = newNode;
	}
	q->size++;
}
//出队列
void QueuePop(Queue* q) {
	assert(q);
	//0个节点
	assert(q->front != NULL);
	//1个节点
	if (q->front->next == NULL) {
		free(q->front);
		q->front = q->rear = NULL;
	}
	//多个节点
	else
	{
		QNode* del = q->front;
		q->front = del->next;
		free(del);
		del = NULL;
	}
	q->size--;
}
//获取队头元素
ElemType QueueTop(Queue* q) {
	assert(q);
	assert(q->front != NULL);

	return q->front->data;
}
//获取队尾元素
ElemType QueueBack(Queue* q) {
	assert(q);
	assert(q->rear != NULL);

	return q->rear->data;
}

//队列的大小
int QueueSize(Queue* q) {
	assert(q);

	return q->size;
}
//队列判空
int QueueEmpty(Queue* q) {
	assert(q);

	return q->size == 0;
}
//队列销毁
void QueueDestroy(Queue* q) {
	assert(q);

	QNode* p = q->front;
	while (p != NULL) {
		QNode* Q = p->next;
		free(p);
		p = Q;
	}
	q->front = q->rear = NULL;
	q->size = 0;
}

层序遍历的代码如下: (这是写在BinaryTree.c文件中的哦!)

// 层序遍历1
void BinaryTreeLevelOrder(BTNode* root) {
	//创建队列
	Queue que;
	//初始化队列
	QueueInit(&que);
	//如果根节点非空,那么将根节点放入队列中
	if (root != NULL) {
		QueuePush(&que, root);
	}
	
	while (!QueueEmpty(&que)) {
		//获取队头元素
		BTNode* front = QueueTop(&que);
		//打印结点的值
		printf("%d ", front->data);
		//删除队头元素
		QueuePop(&que);

		//如果该结点的左孩子存在,那么将左孩子放入队列中
		if (front->left) {
			QueuePush(&que, front->left);
		}
		//如果结点的右孩子存在,那么将右孩子放入队列中
		if (front->right) {
			QueuePush(&que, front->right);
		}
	}
	//销毁队列
	QueueDestroy(&que);
}

我们测试一下代码:

如果我们想要和前序 、中序、后序遍历一样,遇到空节点就打印“N”,该怎么做呢?

很简单,当前结点不为空时,不管它的左孩子和右孩子为空与否,都要进入队列~

代码如下:

// 层序遍历2
void BinaryTreeLevelOrder1(BTNode* root) {
	//创建队列
	Queue que;
	//初始化队列
	QueueInit(&que);
	//如果根节点非空,那么将根节点放入队列中
	if (root != NULL) 
		QueuePush(&que, root);
	
	while (!QueueEmpty(&que)) {
		//获取队头元素
		BTNode* front = QueueTop(&que);
		//删除队头元素
		QueuePop(&que);

		if (front != NULL) {
			printf("%d ", front->data);
	//带入下一层(不管左右孩子是否存在,都要带入下一层)
				QueuePush(&que, front->left);
				QueuePush(&que, front->right);
		}
		else
		{
	//如果是空,那么就没有下一层了
			printf("N ");
		}
	}
	//销毁队列
	QueueDestroy(&que);
}

我们来测试一下代码:

3.3 二叉树的其他操作

  (1)求二叉树的结点个数

哈哈哈,看到这里,有的小伙伴们可能会说:太简单了,二叉树的结点个数不就是等于左子树结点的个数 + 右子树结点的个数嘛? 最后将结果返回就行了,真的是这样吗?我们实践一下:

错误代码如下:

//这种写法是错误的
//求二叉树的结点个数
int TreeSize(BTNode* root) {
	static int size = 0;//定义静态变量size
	if (root == NULL) //如果根节点为空,返回0
	{
		return 0;
	}
	else {			//结点个数+1
		size++;
	}
	TreeSize(root->left);//递归左子树
	TreeSize(root->right);//递归右子树
	return size;//返回size
}

我们测试一下:

啊哦!出事了!三个代码一模一样,但是运行结果不一样! 

这是因为:局部的静态变量只会被初始化一次,当调用多次的时候,static int size = 0;(这句代码只会执行一次)。局部变量是在第一次调用的时候被初始化,后面再去调用就不会再执行初始化。

那怎么改进呢?

改进方法一:将size定义为全局静态变量或者全局变量,每次使用size之前,都先将size置为0

//改进方法一:
//将size定义为全局静态变量或者全局变量
int size = 0;
//static int size = 0;
int TreeSize(BTNode* root) {
	if (root == NULL)//如果根节点为空,返回0
	{
		return 0;
	}
	else {
		size++;		//结点个数+1
	}
	TreeSize(root->left);//递归左子树
	TreeSize(root->right);//递归右子树
	return size;//返回size
}


int main() {
	//创造一棵二叉树
	BTNode* root = CreateTree();
	printf("二叉树的结点个数为:%d\n", TreeSize(root));
	size = 0;    //每次使用size之前,先将size置为0
	printf("二叉树的结点个数为:%d\n", TreeSize(root));
	size = 0;    //每次使用size之前,先将size置为0
	printf("二叉树的结点个数为:%d\n", TreeSize(root));

	return 0;
}

测试一下:

改进方法二:

//改进方法二:
int TreeSize(BTNode* root,int* psize) {
	if (root == NULL) //如果根节点为空,返回0
	{
		return 0;
	}
	else {
		(*psize)++;
	}

	TreeSize(root->left, psize);//递归左子树
	TreeSize(root->right, psize);//递归右子树
}

 还有没有方法呢?肯定有,而且比前面2种都要简单,我们一起来康康~

改进方法三:我们采用分治思想。

分治,即分而治之,就是将一个复杂的问题分解成子问题,子问题再分解成更小子问题,直到最后一个子问题可以简单求解。

例如:我们要求二叉树的节点个数,我们可以将其拆分成左子树、根和右子树。

左子树的节点个数加上右子树的节点个数,再加上根,即为二叉树的节点个数。

而左子树和右子树也能以相同的方式进行拆分,这样就可以将一个大问题拆分成了许多个相同的小问题。

代码如下:

//改进方法三:分治思想
int TreeSize(BTNode* root) {
	//如果根节点为空,返回0;
	//如果根节点非空,返回左子树结点的个数 + 右子树结点的个数 + 根
	return root == NULL ? 0 :
		TreeSize(root->left) + TreeSize(root->right) + 1;
}

测试一下:

(2)求二叉树的高度

取左右子树的最大值,再加上自身,就是二叉树的高度。

代码如下:

//二叉树的高度
int TreeHeight(BTNode* root) {
	if (root == NULL) //如果根节点为空,返回0;
	{
		return 0;
	}
		//递归求左子树的高度
		int LeftTree = TreeHeight(root->left);
		//递归求右子树的高度
		int RightTree = TreeHeight(root->right);
	//将左子树的高度和右子树的高度进行比较,取大的那一个,再加上根节点自身,最后得出答案
		return LeftTree > RightTree ? LeftTree + 1 : RightTree + 1;
}

这里递归的返回值必须要记录下来,不然会重复计算

测试一下:

(3)求二叉树第k层节点个数

当前树的第k层节点的个数 = 左子树第 k-1 层节点个数 + 右子树第 k-1 层节点个数

代码如下:

//二叉树第k层结点个数
int TreeKLevel(BTNode* root, int k) {
	//如果根节点为空,返回0;
	if (root == NULL)
	{
		return 0;
	}
	//如果是求第1层的结点个数,返回1
	if (k == 1) 
	{
		return 1;
	}
   //当前树的第k层个数 = 左子树第k-1层个数 + 右子树第k-1层个数
	return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);
}

测试一下:

(4) 求二叉树叶子节点的个数

代码如下:

//求二叉树叶子结点的个数
int TreeLeafSize(BTNode* root) {
	//如果根节点为空,返回0;
	if (root == NULL)
	{
		return 0;
	}
	//如果整棵二叉树中只有根节点,返回1
    //如果该结点的左孩子为空,右孩子也为空,返回1
	if (root->left == NULL && root->right == NULL) 
	{
		return 1;
	}
	//将左右子树的叶子结点加起来,将结果返回
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

测试一下: 

(5)判断二叉树中是否存在值为x的节点

先访问根,根不为x则访问左子树

左子树中没找到则访问右子树,找到了则返回 true

右子树中找到了返回 true,没找到则说明树中没有值为x的节点,返回 false

代码如下:

//判断二叉树里面是否存在值为x的结点
bool TreeFind1(BTNode* root, int x) {
	//如果根节点为空,返回false
	if (root == NULL)
	{
		return false;
	}
	//如果根节点的数据域为x,那么返回true
	if (root->data == x) 
	{
		return true;
	}
	//在根节点的右子树中查找x,如果找到,返回true
	// 只要在左子树中找到x,就不需要在右子树中查找了,因此用短路或来连接
	//在根节点的右子树中查找x,如果找到,返回true
	return TreeFind1(root->left, x)
		|| TreeFind1(root->right, x);
}

测试一下:

(6)在二叉树中查找值为x的节点

先访问根,根不为x则访问左子树

左子树中没找到则访问右子树,找到了则返回节点地址

右子树中找到了返回节点地址,没找到则说明树中没有值为x的节点,返回NULL

代码如下:

//查找二叉树里面存在值为x的结点
BTNode* TreeFind(BTNode* root, int x) {
	//如果根节点为空,返回NULL
	if (root == NULL) 
	{
		return NULL;
	}
	//如果根节点的数据域为x,那么返回该节点
	if (root->data == x) {
		return root;
	}
	//在根节点的左子树中查找x,如果找到,返回该结点
	BTNode* ret1 = TreeFind(root->left, x);
	if (ret1) {
		return ret1;
	}
	//在根节点的右子树中查找x,如果找到,返回该结点
	BTNode* ret2 = TreeFind(root->right, x);
	if (ret2) {
		return ret2;
	}
	//在左子树和右子树中都没找到x,返回NULL
	return NULL;
}
(7)判断二叉树是否为完全二叉树

我们先简单回顾一下:

完全二叉树: 前n-1层都是满的,最后一层可以不满,但是要求从左到右的节点一定是连续的。

普通二叉树: 指一颗没有特殊要求的二叉树,,每个节点的左右子树都可以为空或非空。

怎么判断一棵树是否为完全二叉树呢? 很简单,我们可以借助队列这种数据结构,通过层序遍历,来判断是否为完全二叉树。 

举个例子呗~

再来看个例子~

因此,我们可以借助队列的结构,对二叉树进行层序遍历,当遍历到空节点时,跳出循环,作下一步判断。如果空节点的后面仍然有节点,说明不是完全二叉树;如果空节点后面全为空,说明是完全二叉树。

代码如下:

//判断二叉树是否是完全二叉树
bool TreeComplete(BTNode* root) {
	Queue q;			//定义一个队列	
	QueueInit(&q);		//对队列进行初始化

	if (root != NULL)   //如果根节点非空
{					
	QueuePush(&q, root);//将指向根节点的指针入队列
}

	//如果根节点非空,那么执行插入或删除操作
	while (!QueueEmpty(&q)) {
	//获取队头元素
		BTNode* top = QueueTop(&q);
	//删除队头元素
		QueuePop(&q);

	//如果此时队头元素为空,跳出循环,作后续判断
		if (top == NULL) {
			break;
		}
		else {
	//队头元素非空
	//将当前节点的左孩子入队列
	//将当前节点的右孩子入队列
			QueuePush(&q, top->left);
			QueuePush(&q, top->right);
		}
	}
		
	//当队头元素为空时,继续获取它后面的元素
	//如果都是空,那么就是完全二叉树
	//如果存在非空,那么就不是完全二叉树
	while (!QueueEmpty(&q)) {
		BTNode* front = QueueTop(&q);
		QueuePop(&q);
		//存在非空,那么就不是完全二叉树
		if (front != NULL) {
		//销毁队列,防止内存泄漏
		//返回false
			QueueDestroy(&q);
			return false;
		}
	}
	//都是空,那么就是完全二叉树,返回true
	QueueDestroy(&q);
	return true;
}

测试一下:

再测试一下:

(8)销毁二叉树

销毁一棵二叉树最好使用后序,先销毁左子树和右子树,最后销毁根节点

代码如下:

//销毁二叉树
void TreeDestroy(BTNode* root) {
	//如果根节点为空,直接返回
	if (root == NULL)
	{
		return;
	}
	TreeDestroy(root->left);//销毁左子树
	TreeDestroy(root->right);//销毁右子树
	free(root);//释放根节点
}

测试一下:

四、习题部分:

 10.有n个元素的完全二叉树的深度是(   )

A.nlogn

B.nlogn+1

C.logn

D.logn+1

正确答案: D

完全二叉树的节点数量:

 N = 2^0 + 2^1 + 2^2 +......+ 2^(h-3) + 2^(h-2) + 1

2*N =  2^1 + 2^2 + 2^3+......+ 2^(h-2) + 2^(h-1) + 2

2*N - N = 2^(h-1) + 2 - 2^0 - 1 

N = 2^(h-1)

 h-1 = log{_{2}}^{N}

 h = log{_{2}}^{N}+1

此处的底数"2"可以省略

9.设一棵二叉树中有3个叶子结点,有8个度为1的结点,则该二叉树中总的结点数为( )个

A.11

B.12​

C.13

D.14

正确答案: C

设Ni表示度为i的节点个数,则节点总数 N = N0 + N1 + N2

节点个数于节点边的关系: N个节点的树有N-1个边

边与度的关系:N - 1 = N1 + 2 * N2

故:N0 + N1 + N2 - 1 = N1 + 2 * N2

因此,得:N0 = N2 + 1

回到原题,N0 = 3,N1 = 8,可得N2 = 2。

因此答案是 3 + 8 + 2 = 13。

8.在一颗完全二叉树中,某一个结点没有其左孩子,则该结点一定( )

A.是根结点

B.是叶结点

C.是分支结点

D.在倒数第二层

正确答案: B

完全二叉树中如果一个节点没有左孩子,则一定没有右孩子,必定为一个叶子节点,最后一层一定为叶子节点,但是倒数第二层也可能存在叶子节点。

 7.一颗完全二叉树有1001个结点,其叶子结点的个数是( )

A.251

B.500

C.501

D.不能确定

正确答案: C

该题需要用到二叉树性质:在任意二叉树中,度为0的节点都比度为2的节点多1个,即 n0 = n2 + 1

另外,在完全二叉树中,如果节点总个数为奇数,则没有度为1的节点,如果节点总个数为偶数,只有一个度为1的节点

因此:n0 + n1 + n2 = 1001 节点总数为奇数,没有度为1的节点

n0 + 0 + n2 = 2*n0-1 = 1001 ,n0 = 501

6.下列关于二叉树的叙述错误的是(   )

A.二叉树指的是深度为 2 的树

B.一个 n 个结点的二叉树将拥有 n-1 条边

C.一颗深度为 h 的满二叉树拥有 2^h-1 个结点(根结点深度为1)

D.二叉树有二叉链和三叉链两种表示方式

 正确答案: A

A错误: 二叉树指最大孩子个数为2,即树的度为二的树。深度描述的为树的层数。

B正确: 对于任意的树都满足:边的条数比节点个数少1,因为每个节点都有双亲,但是根节点没有

C正确: 正确

D正确: 二叉链一般指孩子表示法,三叉连指孩子双亲表示法,这两种方式是二叉树最常见的表示方式,虽然还有孩子兄弟表示法,该中表示方式本质也是二叉链

3.在用树表示的目录结构中,从根目录到任何数据文件,有( )通道

A.唯一一条

B.二条

C.三条

D.不一定

正确答案: A

树的特点是不相交,所以不可能有多个路径同时到达一个点。

4.在一颗度为3的树中,度为3的结点有2个,度为2的结点有1个,度为1的结点有2个,则叶子结点有( )个

A.4

B.5

C.6

D.7

正确答案: C

设度为i的节点个数为ni, 该树总共有n个节点,则n=n0+n1+n2+n3. 

有n个节点的树的总边数为n-1条.

根据度的定义,总边数与度之间的关系为:n-1=0*n0+1*n1+2*n2+3*n3.

联立两个方程求解,可以得到n0 = n2 + 2n3 + 1,  n0=6

 5.一颗拥有1000个结点的树度为4,则它的最小深度是( )

A.5

B.6

C.7

D.8

正确答案: B

如果这棵树每一层都是满的,则它的深度最小,假设它为一个四叉树,高度为h,则这个数的节点个数为(4^h - 1) / 3,当h = 5, 最大节点数为341, 当h = 6, 最大节点数为1365,所以最小深度应该为6。

 11.已知某二叉树的前序遍历序列为5 7 4 9 6 2 1,中序遍历序列为4 7 5 6 9 1 2,则其后序遍历序列为( )

A.4 2 5 7 6 9 1

B.4 2 7 5 6 9 1

C.4 7 6 1 2 9 5

D.4 7 2 9 5 6 1

正确答案: C

通过前序遍历找到子树的根,在中序遍历中找到根的位置,然后确定根左右子树的区间,即根的左侧为左子树中所有节点,根的右侧为右子树中所有节点。

故:根为: 5

5的左子树:4 7   5的右子树: 6 9  1  2

5的左子树的根为: 7  5的右子树的根为:9

7的左子树: 4 7的右:空  9的左子树:6  9的右子树:2

后序遍历: 4 7 6 1 2 9 5

12.已知某二叉树的中序遍历序列为JGDHKBAELIMCF,后序遍历序列为JGKHDBLMIEFCA,则其前序遍历序列为( )

A.ABDGHJKCEFILM

B.ABDGJHKCEILMF

C.ABDHKGJCEILMF

D.ABDGJHKCEIMLF

正确答案: B

由后序遍历确定子树的根,后序遍历从后向前看,最后一个元素为根,和前序遍历刚好相反,从后向前看后序遍历,应该是根,右,左,根据中序遍历确定子树的左右区间

故:根为: A

A的左子树:JGDHKB       A的右子树:ELIMCF

A的左子树的根:B        A的右子树的根:C

B的左子树:JGDHK  B的右子树:空  C的左子树:ELIM C的右子树:F

B的左子树的根:D         C的左子树根:E

D的左子树的根:G D的右子树的根:H  E的右子树的根:I

故前序遍历:

A B D G J H K C E I L M F


片尾

今天我们学习了二叉树这个数据结构,重点介绍了二叉树的相关操作,希望看完这篇文章能对友友们有所帮助 ! ! !

点赞收藏加关注 ! ! !

谢谢大家 ! ! !

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值