数据结构初阶---二叉树

一、二叉树链式结构的实现

二叉树的接口实现一般涉及到函数的递归,这是因为二叉树包含根、左子树、右子树三部分 ,而子树又有与之对应的根、左子树、右子树,符合递归的特征。

1.二叉树的遍历

二叉树的遍历有4种形式:前序遍历、中序遍历、后序遍历、层序遍历。

前序遍历是深度优先遍历DFS、层序遍历是广度优先遍历BFS。

(中序和后序也属于深度优先遍历)

前序遍历:也称先序遍历、前根遍历,访问根节点的操作发生在遍历其左右子树之前。

(根--->左子树--->右子树)

中序遍历:访问根节点的操作发生在遍历左子树之后、遍历右子树之前。

(左子树--->根--->右子树)

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

(左子树--->右子树--->根)

上述三种遍历,每次遍历都是在遍历到NULL的时候结束。

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

2.二叉树的接口

二叉树的结构

cpp 复制代码
//链式二叉树
typedef struct BinaryTreeNode
{
	BTreeNodeDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTreeNode;

①前序遍历PreOrder

void PreOrder(BTreeNode* root);

(根--->左子树--->右子树)

我们优先访问根节点,再进入子树,依旧优先访问根节点,一直按照根--->左子树--->右子树的顺序进行访问。

cpp 复制代码
//前序遍历PreOrder---根-左子树-右子树
void PreOrder(BTreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	printf("%d ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

②中序遍历InOrder

void InOrder(BTreeNode* root);

(左子树--->根--->右子树)

我们优先访问左子树,再进入子树,依旧优先访问左子树,一直按照左子树--->根--->右子树的顺序进行访问。

cpp 复制代码
//中序遍历InOrder---左子树-根-右子树
void InOrder(BTreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}

③后序遍历PostOrder

void PostOrder(BTreeNode* root);

(左子树--->右子树--->根)

我们优先访问左子树,再进入子树,依旧优先访问左子树,一直按照左子树--->右子树--->根的顺序进行访问。

cpp 复制代码
//后序遍历PostOrder---左子树-右子树-根
void PostOrder(BTreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

④层序遍历LevelOrder

void LevelOrder(BTreeNode* root);

二叉树遍历每一层,层序遍历也是一种广度优先遍历BFS。

思路:使用队列,利用其先进先出的规则,对二叉树进行队尾插入以及队头获取与删除操作来遍历。

cpp 复制代码
//层序遍历LevelOrder---队列先进先出
//根结点进入队列 ---> 根结点出、删除、打印 ---> 带入左孩子结点、右孩子结点
void LevelOrder(BTreeNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTreeNode* 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");
	QueueDestroy(&q);
}
Extra---如何打印每层后换行?

思路:使用一个变量levelSize控制个数,具体来讲,第一层1个结点,while循环进行1次,levelSize的值取决于队列的规模(大小)。始终保证队列中的结点是某一层的结点。

cpp 复制代码
//如果想层序遍历,并每一层换行打印
void EveryLevelOrder(BTreeNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);
	//创建一个每层结点个数的变量levelSize来控制
	int levelSize = 1;
	while (!QueueEmpty(&q))
	{
		//每一层结点置入队列
		while(levelSize--)
		{
			BTreeNode* front = QueueFront(&q);
			QueuePop(&q);

			printf("%d ", front->data);
			if (front->left)
				QueuePush(&q, front->left);
			if (front->right)
				QueuePush(&q, front->right);
		}
		//队列规模始终为每层中结点个数
		levelSize = QueueSize(&q);
		printf("\n");
	}
	printf("\n");
	QueueDestroy(&q);
}

⑤结点个数BTreeSize

int BTreeSize(BTreeNode* root);

使用递归求二叉树结点个数,我们分为左子树与右子树与根 ===> 左子树结点 + 右子树结点 + 1(根结点) 。

cpp 复制代码
//二叉树结点个数
int BTreeSize(BTreeNode* root)
{
	if (root == NULL)
		return 0;
	else
		return 1 + BTreeSize(root->left) + BTreeSize(root->right);

    //等效于下方
	//return root == NULL ? 0 : 1 + BTreeSize(root->left) + BTreeSize(root->right);
}

⑥叶子结点个数BTreeLeafSize

int BTreeLeafSize(BTreeNode* root);

叶子节点即没有孩子结点的结点,则当其子节点均为NULL表示这是叶子结点,需要记录个数:

我们分为左子树的叶子节点 + 右子树的叶子节点。

当左子树与右子树根结点不是叶子节点 ===> 那么进行递归。

当结点是叶子结点时,返回1计数。

cpp 复制代码
//二叉树叶子结点个数
int BTreeLeafSize(BTreeNode* root)
{
if (root == NULL)
	return 0;
if (root->left || root->right)
	return BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
else
	return 1;

	//等效于下方
	//return (root->left || root->right) ? BTreeLeafSize(root->left) + BTreeLeafSize(root->right) : 1;
}

⑦高度(深度)BTreeHeight

int BTreeHeight(BTreeNode* root);

以递归返回的1为准:每次进行递归,相当于遍历走过了一层,+1。

同时以最高层为准:分为左子树与右子树,但是比较两者BTreeheight的大小取较大者进行递归。

cpp 复制代码
int MAX(int x, int y)
{
	return x > y ? x : y;
}
//二叉树的高度
int BTreeHeight(BTreeNode* root)
{
	if (root == NULL)
		return 0;
	return 1 + MAX(BTreeHeight(root->left), BTreeHeight(root->right));
}

⑧第k层结点个数BTreeLevelK

int BTreeLevelK(BTreeNode* root , int k);

二叉树第k层的结点个数 ===> 二叉树左右子树第k-1层的结点个数 ===> 左右子树的左右子树的第k-1-1层的结点个数 ......

直到k减至1 ===> 意味着在递归后对应的根节点处,为1个结点,因此返回1。

总结如下:

结点为NULL ,返回0;

结点不为NULL , k为1 ,返回1;

结点不为NULL , k>1 ,返回左子树k-1层结点数+右子树k-1层结点数。

cpp 复制代码
//第k层的结点个数
//思路:第k层的结点 == 左子树的k-1层结点数 + 右子树的k-1层结点数
// == 左左子树的k-1-1层结点数 + 右子树的k-1-1层结点数
//递归
int BTreeLevelK(BTreeNode* root, int k)
{
	assert(k > 0);
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;
	return BTreeLevelK(root->left, k - 1) + BTreeLevelK(root->right, k - 1);
}

部分递归展开图:

⑨查找值为x的结点

BTreeNode* BTreeFind(BTreeNode* root, BTreeNodeDataType x);

cpp 复制代码
//二叉树查找值为x的结点
BTreeNode* BTreeFind(BTreeNode* root, BTreeNodeDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
	BTreeNode* ret1 = BTreeFind(root->left,x);
	if (ret1)
		return ret1;

	BTreeNode* ret2 = BTreeFind(root->right, x);
	if (ret2)
		return ret2;
	return NULL;
}

⑩判断是否是完全二叉树

bool BinaryTreeComplete(BTreeNode* root);

我们使用层序遍历的方式,能够轻松判断一棵树是否为完全二叉树。

对于一棵完全二叉树,如果入队,那么必然在最后获取到NULL,因此我们将NULL也一起置入队列,判断队头为NULL时停止获取与删除,跳出循环,在队列中遍历判断是否存在非NULL结点,若存在,说明不是完全二叉树,若不存在,安全结束循环,说明是完全二叉树。

cpp 复制代码
//判断一棵树是否为完全二叉树
bool BinaryTreeComplete(BTreeNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
		QueuePush(&q, root);
    //结点置入队列并获取删除
	while (!QueueEmpty(&q))
	{
		BTreeNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)
			break;//队头为空,跳出循环,进入判断队列阶段
		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}
    //判断队列中是否有非空结点
	while (!QueueEmpty(&q))
	{
		BTreeNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

⑪销毁二叉树

void BTreeDestroy(BTreeNode* root);

在销毁二叉树释放结点空间时,需要注意,如果采取前序遍历或者中序遍历对二叉树进行销毁,那么由于优先销毁根节点,我们就需要额外去保存左子结点与右子结点以确保没有产生内存泄漏,因此下面我们采用后序遍历的方式进行销毁二叉树。

cpp 复制代码
//销毁二叉树
//采取后序遍历,前序和中序也可以,但是需要保存结点,因为销毁根后可能无法找到左子树或者右子树
void BTreeDestroy(BTreeNode* root)
{
	if (root == NULL)
		return;
	BTreeDestroy(root->left);
	BTreeDestroy(root->right);
	free(root);
}
//在外面root置空

在使用销毁接口后,需要在主函数中将root置空。或者更改销毁接口的返回类型,使其返回NULL让实参root接收,或者使用root的地址,传二级指针。总之确保没有野指针的问题。

3.前序遍历数组创建二叉树

通过一个前序遍历后的数组来创建二叉树===>// 前序遍历的数组"ABD##E#H##CF##G##"

#代表NULL,那么实际上该二叉树的模样为:

数组中i下标处为#,说明为NULL;不为#,说明需要开空间来放置结点,结点中的数据data存放该下标处的值。

cpp 复制代码
//构建二叉树
//通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树 === #表示NULL
//a为给的数组,传入i的地址,i为下标
BTreeNode* BTreeCreate(BTreeNodeDataType* a, int* pi)
{
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}
	
	//给结点开空间
	BTreeNode* node = (BTreeNode*)malloc(sizeof(BTreeNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	//将数组中该下标处值赋入,并链接
	node->data = a[(*pi)++];
	node->left = BTreeCreate(a, pi);
	node->right = BTreeCreate(a, pi);

	return node;
}

4.二叉树的性质

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

②若规定根节点的层数为1,则深度为 h 的二叉树的最大结点数是 2^h - 1

③对任何一棵二叉树, 如果其叶结点(度为0)个数为N0 , 度为2的分支结点个数为N2 ,则有 N0N2+1 。(重要)

④若规定根节点的层数为1,具有n个结点的满二叉树 的深度 h=log(n+1)(ps:以2为底,n+1为对数)

⑤对于具有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否则无右孩子

  4. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( B
    A 不存在这样的二叉树
    B 200
    C 198
    D 199
    N0 = N2 + 1

  5. 下列数据结构中,不适合采用顺序存储结构的是( A
    A 非完全二叉树
    B 堆
    C 队列
    D 栈

  6. 在具有 2n 个结点的完全二叉树中,叶子结点个数为( A
    A n
    B n+1
    C n-1
    D n/2

一个完全二叉树,度为1的分支节点只有一个或者没有,一共有偶数个结点,那么说明最后一行有一个度为1的分支节点。即N1 =1 ,又已知N0 = N2 + 1 ===>存在等式:N1 + N2 + N3 = 2n 带入N0 = N2 + 1与N1 = 1计算即可。N0 = n。

  1. 一个具有 767 个节点的完全二叉树,其叶子节点个数为( B
    A 383
    B 384
    C 385
    D 386
    奇数结点个数的完全二叉树,度为1的分支节点个数为0,N0+N2 = 767 ===> 2N0 - 1 =767 ===>N0 = 384
  2. 一棵完全二叉树的节点数位为 531 个,那么这棵树的高度为( B
    A 11
    B 10
    C 8
    D 12

完全二叉树的结点个数与深度的关系:[ 2^(h-1) , 2^h-1]

相关推荐
御风@户外30 分钟前
质数生成函数、质数判断备份
算法·acm
m0_6896182839 分钟前
数学建模助力干细胞研究,配体纳米簇如何影响干细胞命运
笔记·数学建模
闻缺陷则喜何志丹43 分钟前
【C++动态规划】1105. 填充书架|2104
c++·算法·动态规划·力扣·高度·最小·书架
Dong雨1 小时前
六大排序算法:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序
数据结构·算法·排序算法
析木不会编程1 小时前
【C语言】动态内存管理:详解malloc和free函数
c语言·开发语言
达帮主1 小时前
7.C语言 宏(Macro) 宏定义,宏函数
linux·c语言·算法
是十一月末1 小时前
机器学习之KNN算法预测数据和数据可视化
人工智能·python·算法·机器学习·信息可视化
chenziang11 小时前
leetcode hot100 路径总和
算法
lyx1426061 小时前
leetcode 3083. 字符串及其反转中是否存在同一子字符串
算法·leetcode·职场和发展