链式二叉树及其相关操作

本文详细介绍了链式二叉树的结构,包括二叉树结点的定义,以及四种遍历方法:前序、中序、后序和层序遍历的实现代码。此外,还讨论了进阶问题如每层打印一行的遍历,计算二叉树结点、叶子节点个数,求树的高度,找第k层节点数,查找特定值的结点,判断完全二叉树以及二叉树的销毁方法。
摘要由CSDN通过智能技术生成

链式二叉树

链式二叉树的增删查改没有什么价值,对于其来说,注重的是结构

总而言之,贯彻一种思想:每一棵都可以分成根+左右子树,包括左右子树也看出一棵树,其可分为根+左右子树,直至空树(NULL)不可再分。光剩一个结点也是可以分成根+两个空树。

也就是说,二叉树的每一个结点其实都是根节点!!!是其子树的根节点

这里还有就是递归,函数栈帧的概念。每“递”一次,就会产生一个新的函数栈帧;每“归”一次,就会销毁一个函数栈帧

二叉树结点的定义
//定义二叉树结点
typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;
四种遍历方法

链式二叉树的四种遍历方法分别为:前序遍历、中序遍历、后序遍历、层序遍历

前序遍历

先根、再左子树、最后右子树

每一棵左子树或右子树都要将其看作一棵独立的树,然后用先根、再左子树、最后右子树这样的逻辑来看

image-20221226171832156

以这棵二叉树为例,那么这棵树的遍历序列应该为什么呢?

我们把这棵二叉树完善一下,应该是这样的:

image-20221226172121312

再按照前序遍历的话,所得序列为:

image-20221226185648927

尽管在实际序列中没有这些NULL,但这种带NULL的序列可以更深刻的反映出遍历的过程

代码:

//前序遍历
void PrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	printf("%d ", root->data);
	PrevOrder(root->left);
	PrevOrder(root->right);
}
中序遍历

先左子树,再根,最后右子树

image-20221226191250079

代码:

//中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}
后序遍历

先左子树,再右子树,最后根

image-20221226191312953

代码:

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

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

从上到下,从左往右,一层一层遍历

image-20221226191347364

层序遍历与上面三种遍历不同,层序遍历不是递归遍历

代码思路:借助队列实现。先将整个二叉树的根节点入队。然后开始循环,队列每出一个节点,就将该节点的左右非空孩子入队列,当队列为空时循环结束。至此,层序遍历结束

void LevelOrder(BTNode* root)
{
	//先创建一个队列
	Queue q;
	QueueInit(&q);

	//先将根节点入队列
	if (root)
		QueuePush(&q, root);

	//只要队列不为空
	while (!QueueEmpty(&q))
	{
		//根节点打印并出队
		BTNode* front = QueueFront(&q);
		printf("%d ", front->data);
		QueuePop(&q);

		//左右孩子入队列
		if (front->left)
			QueuePush(&q, front->left);
		if (front->right)
			QueuePush(&q, front->right);
	}
	printf("\n");

	QueueDestroy(&q);
}

注意,由于进出队列的是二叉树结点指针,所以队列相关的代码需改动一下。即将原本的typedef int QDataType改为typedef struct BinaryTreeNode* QDataType;

进阶-每一层打印一行

实现思路:定义一个遍历用来记录每一层的结点数。每次出队都是出一层,当这一层出完,下一层也全进队列了,如此循环。

void ProLevelOrder(BTNode* root)
{
	//先创建一个队列
	Queue q;
	QueueInit(&q);

	//先将根节点入队列
	if (root)
		QueuePush(&q, root);

	int levelsize = 1;	//记录当前层结点个数

	//只要队列不为空
	while (!QueueEmpty(&q))
	{
		while (levelsize--)
		{
			//根节点打印并出队
			BTNode* front = QueueFront(&q);
			printf("%d ", front->data);
			QueuePop(&q);

			//左右孩子入队列
			if (front->left)
				QueuePush(&q, front->left);
			if (front->right)
				QueuePush(&q, front->right);
		}
		levelsize = QueueSize(&q);
		printf("\n");
	}
	printf("\n");

	QueueDestroy(&q);
}

由于,队列中每次只会存一层的结点,所以我们可以直接用QueueSize来获取每一层的结点数

求二叉树结点个数
方法一

通常思路是:遍历整个二叉树,遇到结点就++

按照上述思路,以前序遍历为例,代码如下:

int TreeSize1(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
    
    int size=0;
    size++;
	TreeSize1(root->left);
	TreeSize1(root->right);
    
    return size;
}

只要结点不是NULL,那么size就++

但上述代码是有问题的。因为这里的size是其各自函数栈帧中的局部变量。所以上述代码并不能得到正确结果

image-20221229133405602

显然,这个结果与实际的二叉树结点个数为:6,是不相符的

针对上述原因,我们想着将size给弄到全局区或静态区是否就能解决问题呢?我们先试着移到静态区,即static int size=0;

image-20221229133556715

问题貌似是解决了。可当我们多次调用求二叉树结点这一函数时,又出现问题了

image-20221229133702615

这是因为静态变量只会在第一次执行代码时初始化一次,后面将不再执行初始化代码。

静态变量也行不通,再用全局变量试试

int size=0;
void TreeSize1(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
    
    size++;
	TreeSize1(root->left);
	TreeSize1(root->right);
}

image-20221229134107168

这时,问题就全部解决了。

方法二

实际上,刚刚的方法并不好。我们接下来用递归的思想来求解这个问题

int TreeSize2(BTNode* root)
{
    return root==NULL ? 0 : 
             TreeSize2(root->left)+TreeSize2(root->right)+1
}

image-20221229135437088

从递归的角度来看:我们将整个树分成根+左右子树,要求二叉树的结点个数就是求左子树的结点个数+右子树的结点个数+自身这一根结点

下面是逻辑图:

image-20221230185138909

求二叉树叶子节点的个数

这里和上面求二叉树结点个数的思路一样,只不过多了一些限制条件而已。

//求二叉树叶子节点的个数
int TreeLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	if (root->left == NULL && root->right == NULL)
		return 1;
	else
		return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
求二叉树的高度

二叉树高度等于左右子树中较高的树的高度+1

//求树的高度
int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;
    
    int leftHeight=TreeHeight(root->left);
    int rightHeight=TreeHeight(root->right);
    
	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
//求树的高度
int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;

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

这段代码是我一开始写的。但这段代码存在很大的问题。因为TreeHeight(root->left) > TreeHeight(root->right)这里比较过后并没有将左右子树的高度进行保存,所以导致后面TreeHeight(root->left) + 1TreeHeight(root->right) + 1时,需要重新递归获得左子树或右子树的高度,从而导致了严重的内存浪费

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

二叉树的第k层=左子树的第k-1层+右子树的第k-1层

//求二叉树第k层结点数
int TreeKLevelSize(BTNode* root, int k)
{
	if (root == NULL)
		return 0;

	if (k == 1)
		return 1;
	else
		return TreeKLevelSize(root->left, k - 1) + TreeKLevelSize(root->right, k - 1);
}
二叉树中查找值为x的结点

思路:先查根节点,如果不是那就分别查左右子树,这样递归下去。如果最终查不到就返回NULL

BTNode* TreeFind(BTNode* root, BTDataType x)
{
    if(root==NULL)
        return NULL;
    //先查根节点
    if(root->data==x)
        return root;
    //再分别查左右子树
    BTNode retleft=TreeFind(root->left,x);
    if(retleft)
        return retleft;
    
    BTNode retright=TreeFind(root->right,x);
    if(retright)
        return retright;
    
    //左右子树也查不到,说明该树中无值x
    return NULL;
}
判断二叉树是否为完全二叉树

思路:层序遍历二叉树,当队列第一次出到NULL时则停止。此时开始判断:如果后面出队列的全是NULL,则是完全二叉树;但凡出队列出到一个非空,则不是完全二叉树

上述思路的理论支撑:

image-20221231174334679

这种情况就不是完全二叉树,因为第一次出完NULL后,又出了5

image-20221231180414187

这种情况就是完全二叉树了,第一次出完NULL后,出的全是NULL

//判断二叉树是否为完全二叉树
//思路:层序遍历一遍。当第一次出到NULL后,开始判断:如果后面出队的全是NULL,则是完全二叉树;但凡后面出队有非空,则不是完全二叉树
bool TreeComplete(BTNode* root)
{
	//新建一个队列
	Queue q;
	QueueInit(&q);

	//先将根节点入队
	if (root)
		QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		//第一次出到NULL,退出循环,开始判断
		if (front == NULL)
		{
			break;
		}
		else
		{
			//左右孩子都入队,不管是不是NULL
			QueuePush(&q, front->left);
			QueuePush(&q, front->right);
		}
	}

	//开始判断
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		//出现非空,则返回false
		if (front)
		{
			QueueDestroy(&q);
			return false;
		}
	}

	//走到这说明,这是一棵完全二叉树,则返回true
	QueueDestroy(&q);
	return true;
}
二叉树的销毁

二叉树由于是链表实现,所以应该从最末端开始往前销毁。销毁时要注意最后释放根节点,如果先释放根节点就找不到它的左右孩子了。因此我们采用后序销毁的办法

//二叉树的销毁
//思路:利用后序遍历的思路逐步销毁,即先释放左子树,再释放右子树,再释放根
void TreeDestroy(BTNode* root)
{
	if (root == NULL)
		return;

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

需要注意的是,由于这里传的是一级指针,所以再调用完销毁函数时,我们还需对实参进行置空

如果传的二级指针,就可以在上述代码的最后来个*root=NULL。但由于上面的代码是一级指针,形参的改变不影响实参,所以需要我们自己在外部置空一下

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值