《链式二叉树》

本文详细介绍了链式二叉树的四种遍历方法:前序、中序、后序和层序,并提供了相应的递归实现。此外,文章还讨论了如何创建二叉树,包括节点的创建、手动链接以及通过数组构建二叉树。同时,文章提到了如何利用遍历判断二叉树是否为完全二叉树。
摘要由CSDN通过智能技术生成

本文主要讲解链式二叉树的遍历和创建等知识点


前言

我们在学习链表的时候,我们都学习他的增删查改,但是我们的二叉树,对于这种复杂的结构,我们的增删查改就没有意义,你要怎么插入,插入那个还是不是二叉树,这是一个很大的问题,但是我们的遍历是有意义的。


1、链式二叉树的遍历的介绍

链式二叉树的遍历一共有四种

  1. 前序遍历
  2. 中序遍历
  3. 后序遍历
  4. 层序

(1)前序遍历

又称为先根遍历,遵循的原则就是访问根结点的操作发生在遍历其左右子树之前。

例如:采用前序遍历下面的树

在这里插入图片描述

前序遍历结果:
1 2 3 NULL NULL NULL 4 5 NULL NULL 6 NULL NULL
首先我们是根结点,第一个根是1,然后左子树,对于左子树的根是2,然后再分,2的左子树是的根是33的左子树是NULL,然后再进行3的右子树也是NULL,当2的左子树遍历完,就该遍历2的右子树,而2的右子树是NULL,然后将1的左子树遍历完了,就该遍历1的右子树,1的右子树的根是4,然后4的左子树的55的左子树是NULL右子树是NULL,然后再遍历4的右子树的根是6,他的左右子树也是NULL
这样就遍历完了

前序遍历的递归图解
在这里插入图片描述

(2)中序遍历

遵循的原则就是访问根结点的操作发生在遍历其左右子树之中(间)

例如:采用中序遍历下面的树

在这里插入图片描述

前序遍历结果:
NULL 3 NULL 2 NULL 1 NULL 5 NULL 4 NULL 6 NULL
因为先遍历左子树,对于1来说,他是整个数的根,所以先遍历他的坐姿是,但是对于左子树来说,2就是他的根,对于2 这颗树他的左子树是3,3的左子树是NULL,所以第一个就是NULL,然后是他的根3,然后就是他的右子树NULL,当2的左子树的遍历完,然后就是根2,然后就是2的右子树NULL,这时就把1的左子树遍历完了,就是根1,然后就是1的右子树,下面的思路就不写了

(3)后序遍历

遵循的原则就是访问根结点的操作发生在遍历其左右子树之后。

例如:采用后序遍历下面的树

在这里插入图片描述

前序遍历结果:
NULL NULL 3 NULL 2 NULL NULL 5 NULL NULL 6 4 1

(3)层序遍历

层序就是一层一层遍历,就是1,2,4,3,5,6

用队列的方式实现层序

:这一部分可以先跳过,等后面前中后以及二叉树的问题结束后再去理解这个

其大思路就是,先将根节点放入队列,然后将根节点出队列,再把根节点的左右节点放入队列,即:出一层,带入下一层
在这里插入图片描述

// 二叉树层序遍历
void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root != NULL)
	{
		QueuePush(&q, root); //将根节点放入队列
	}
	while (!QueueEmpty(&q))
	{
		QDataType front = QueueFront(&q);  //获取对头,front不可能为空,因为我们只把非空的传到队列里
		QueuePop(&q); //删除对头
		printf("%d ", front->data);

		if (front->left) //将当前根节点的左节点放入队列
		{
			QueuePush(&q, front->left);
		}

		if (front->right)//将当前根节点的右节点放入队列
		{
			QueuePush(&q, front->right);
		}
	}
	QueueDestroy(&q);
}

用队列判断二叉树是否为完全二叉树

完全二叉树:前h-1层为满二叉树,最后可以不满,但是必须连续
在这里插入图片描述

我们可以使用层序遍历去解决这个问题,我们先将根节点放入队列,然后获取队头,出队列,判断队头是不是空,队头不是空就继续循环把他的左右节点入队列,如果队头是空的时候。我们结束循环,判断队列里面的是不是全部为NULL,如果是那就是完全二叉树,反之则否
在这里插入图片描述

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

	if (root != NULL)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		QDataType front = QueueFront(&q);  //front不可能为空,因为我们只把非空的传到队列里
		QueuePop(&q);
		if (front == NULL)
		{
			break;
		}
		else
		{
			QueuePush(&q, front->left);
			QueuePush(&q, front->right);
		}
	}

	//将剩余的继续出队列,如果后面的全部为空,那就是完全,否则就不是
	while (!QueueEmpty(&q))
	{
		QDataType front = QueueFront(&q);  //front不可能为空,因为我们只把非空的传到队列里
		QueuePop(&q);
		if (front != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

总结

我们的前面三个遍历,前序,中序,后序,三个遍历。
我们可以发现我们的遍历,都是将大问题转换为小问题,然后等我们遇到空指针就是结束,这是不是就把我们实现递归的两个条件实现了

2、递归解决二叉树的问题

我们首先自己创建一个二叉树,来实现二叉树的各种遍历,最后我们再写一个真正的二叉树创建函数(将数组转化为二叉树)

(1)二叉树节点的创建

我们知道一个链式二叉包含data,left right

typedef char BTDataType;

//节点的结构
typedef struct BTNode
{
	BTDataType data;
	struct BTNode* left;
	struct BTNode* right;
}BTNode;

(2)为每个节点开辟一块空间

//开辟一个节点空间
BTNode* BuyNode(BTDataType x)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->data = x;
	newnode->left = NULL;
	newnode->right = NULL;
	return newnode;
}

(3)手动链接一个二叉树

我们创建6个节点,然后自己手动链接起来成为一个二叉树

//自己创建一个二叉树
BTNode* CreatBinaryTree()
{
	BTNode* n1 = BuyNode(1);
	BTNode* n2 = BuyNode(2);
	BTNode* n3 = BuyNode(3);
	BTNode* n4 = BuyNode(4);
	BTNode* n5 = BuyNode(5);
	BTNode* n6 = BuyNode(6);
	//BTNode* n7 = BuyNode(7);

	n1->left = n2;
	n1->right = n4;
	n2->left = n3;
	//n2->right = n7;
	n4->left = n5;
	n4->right = n6;
	return n1;
}

(4)二叉树前序遍历

前序遍历就是先打印根,再打印左子树,再打印右子树

// 二叉树前序遍历
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	printf("%c ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

完成递归的两个条件
1.有结束递归截至条件
2.每次递归都要接近这个条件,将我们大问题转换为小问题

首先我们问一下,我们的遍历满不满足递归的两个条件
将大问题转换为小问题,
我们要遍历一整棵树=>遍历左子树和右子树,对于左子树和右子树来说,他也有自己的左右子树,又分为了更小的问题,知道遇到了NULL就结束,这个就是它们的结束条件

在这里插入图片描述

(5)二叉树中序遍历

前序遍历就是先打印左子树,再打印根,再打印右子树

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

(6)二叉树后序遍历

// 二叉树后序遍历
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

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

还是大问题转换为小问题,求节点的个数=左子树的结点个数+右子树的结点个数+1(表示自身节点个数)。遇到空指针,然回0,表示个数为0.

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

(8)求叶子结点的个数

叶节点:左右子树都为NULL的节点
我们还是跟上面差不多,遇到空就返回0,遇到叶子就返回1,然后将左右子树的叶子加在一起就行。
但是注意的点是防止空指针的解引用,我们要加一个判断

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

(9)求树的高度

错误代码展示:

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

其实这个会造成极大的浪费,我们光看代码没有任何的问题,但是我们再算出左面和右面的高度,并没有保存,然后后面在比较的时候有进行了依次递归,然后才取出我们的高度。大家不要小看这个,高度越高,我们的最后一层进行的次数越多,改进方法就是将我们的高度保留下来,在比较。
具体的可以画一下递归展开图

正确代码:

// 二叉树的高度
int BinaryTreeHeight(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	int leftHeight = BinaryTreeHeight(root->left);  //递归求左子树的高度并且保存
	int rightHeight = BinaryTreeHeight(root->right); //递归求左子树的高度并且保存
	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

(9)第K层的节点个数 k >= 1

分解成子问题,就是第k层=左子树第k-1层的个数+右子树的第k-1层的个数
知道k==1,并且不等于NULL返回1.

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}

(10)二叉树查找值为x的结点

这种查找的同样需要保存,因为递归是一层一层递归的,我们需要保存返回值!!

错误代码:

//二叉树查找值为x的结点
BTNode* TreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	TreeFind(root->left, x);
	TreeFind(root->right, x);

	return NULL;

}

解释:为什们会写出这样的代码,首先遇到空指针,就说明找不到我们就不找了,返回,遇到等于x就直接返回这个头结点,然后我们在直接进行遍历,就可以了。我第一回也是这样想的。但是这样想就是没有搞清楚递归的返回值。

假设我们找到了我们的节点,我们返回指针,我们并不是直接返回外面,而是我们一层一层的返回到最外层,但是这个时候根本没有返回值接收!!只要理解了这一点,我相信大家都能够想明白。大家可以自己将递归的过程画出来,就能明白了

正确代码:

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	
	//找左子树
	BTNode* ret1 = BinaryTreeFind(root->left, x);
	if (ret1)
	{
		return ret1;
	}

	//找右子树
	BTNode* ret2 = BinaryTreeFind(root->right, x);
	if (ret2)
	{
		return ret2;
	}
	return NULL;
}

(11)二叉树的销毁

用后序遍历销毁,我们先将左右子树销毁,然后再销毁根结点。因为如果先销毁根节点,那么就找不到左右子树了

// 二叉树销毁
void BinaryTreeDestory(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreeDestory(root->left);
	BinaryTreeDestory(root->right);
	free(root);
}

3、二叉树的创建

这个其实就是传给你一个数组,你要帮我变成链式二叉树

例如:通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树

//通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}
	//构建节点并连接
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->data = a[(*pi)++];  //将数组元素放入。(*pi)++,继续遍历

	newnode->left = BinaryTreeCreate(a, n, pi);  //递归创建下一个节点,链接在左子树上
	newnode->right = BinaryTreeCreate(a, n, pi);
	return newnode;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值