【数据结构】二叉树

前面的文章的内容是堆,它的结构基础是完全二叉树,那么假如我们现在给的是随意的一棵二叉树呢,结构随意,那么再用数组存储就有些位置是空的,会被浪费掉,这就有些不礼貌了,所以我们引出今天要谈的链式二叉树。

ps:(这里脱离一下主题:我们现在谈的是普通二叉树,作用不大,因为如果你单纯用来存储数据,它的结构太复杂,完全不如顺序表和链表,所以我们在研究二叉树时,不再研究它的增删查改,而是赋予它一个新的特点:搜索二叉树,这棵树的特点就是,它的所有子树都满足左子树的值小于右子树的值,这时它的价值就体现在搜索上了,非常适合查找数据。)

目录

二叉树的遍历

前序遍历(顺序:根 左子树 右子树)

中序遍历(顺序:左子树 根 右子树)

后序遍历(顺序: 左子树 右子树 根)

求这棵树结点个数

求这棵树的高度

 求第k层的结点个数

查找值为x的结点

层序遍历

判断一棵树是否是完全二叉树

二叉树的销毁


二叉树的遍历

每棵树我们都可以按照根,左子树,右子树把它拆解成三个部分,根据三者访问的顺序我们分成前序遍历,中序遍历,后序遍历三种,外加一个层序遍历(顾名思义就是按照层进行遍历)。首先看前序遍历:

前序遍历(顺序:根 左子树 右子树)

以上图为例,按照规则,它的顺序应该是:

1  2  3  NULL  NULL  NULL  4  5  NULL  NULL  6  NULL  NULL

注意这里一定要严格按照顺序,我们将上述遍历结果画图理解一下:

 通过图我们可以更直观的发现这里由于根,左右子树都是相对的,所以出现递归现象,所以在代码实现上主要是递归思想

下面我们就可以代码实现一下了,首先给出二叉树结构

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

 由于我们不学二叉树的增删查改,所以需要我们先造一个二叉树:

BTNode* BuyNode(BTDataType x)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	node->data = x;
	node->left = NULL;
	node->right = NULL;

	return node;
}

BTNode* CreatTree()
{
	BTNode* node1 = BuyNode(1);
	BTNode* node2 = BuyNode(2);
	BTNode* node3 = BuyNode(3);
	BTNode* node4 = BuyNode(4);
	BTNode* node5 = BuyNode(5);
	BTNode* node6 = BuyNode(6);


	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;

	return node1;
}

这样我们就造了一棵树,这棵树就是我们之前图上的那棵树

接下来就可以写前序遍历的代码了

void PreOrder(BTNode* root) {
	if (root == NULL) {
		printf("NULL ");
		return;
	}

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

 这里的代码由于使用了递归思想所以代码其实很简洁,我们重点通过代码理解其含义

 我们通过画递归展开图理解其含义

(ps:作者不才,这里的图画的些许抽象,色彩搭配也有些辣眼,希望可以帮助读者理解,顺序已通过箭头和序号的方式标好)

 代码运行起来打印结果发现没有问题,这就是前序遍历。

中序遍历(顺序:左子树 根 右子树)

 有了上面的前序遍历代码的基础,我们直接给出中序遍历的代码:

void InOrder(BTNode* root) {
	if (root == NULL) {
		printf("NULL ");
		return;
	}

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

这里就不再赘述递归展开图了,不明白的老铁可自行画一下递归展开图帮助理解。 

后序遍历(顺序: 左子树 右子树 根)

 

 后序遍历代码如下:

void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

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

 这里再给出中序遍历和后序遍历的测试结果,验证代码的正确性。

求这棵树结点个数

注意这里可能有人会在TreeSize这个函数中定义一个局部变量size统计结点个数,这样做是错误的,因为函数每递归一次都会产生一个size变量,但是函数结束就会销毁,所以,不可能统计到总数,所以,这里需要用全局变量size

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

 这里计算size依然是运用了前序遍历的思想

但回头看这里,仔细思考,你会发现这里的全局变量是很危险的,比如本来这里的调用TreeSize计算的结果是6,但是如果你调用两次,结果就成了12了,因为size不会重置,所以为了避免出现这样的问题,需要你每调用一次,重置size为0

所以这里还有一种写法是直接再加一个参数,用来计算个数,当然需要传一个指针,因为为了避免一个问题就是:形参的改变不改变实参

int TreeSize(BTNode* root,int* psize)
{
	if (root == NULL)
	{
		return;
	}
	++*psize;
	TreeSize(root->left,psize);
	TreeSize(root->right,psize);
}

 这里再提供第三种解决方法:采用分治思想,就是我要统计这个树的结点个数,那么我可以统计子树下的结点个数,子树下的结点个数又可以继续统计子树的子树的结点个数,一直往下走,直到NULL,其实就是递归带返回值,这样处理你会发现代码十分简洁,这也是递归的特点。

int TreeSize(BTNode* root)
{
	return root == NULL ? 0 : TreeSize(root->left) + TreeSize(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;
}

 求第k层的结点个数

思路:根的第k层个数=左子树的第k-1层个数+=右子树的第k-1层个数

int TreeKLevel(BTNode* root, int k)
{
    assert(k>0);
	if (root == NULL)
		return 0;
	if (k == 1)//k==1时,没有必要继续下面各层的计算了,无关了
		return 1;
	int leftK = TreeKLevel(root->left, k - 1);
	int rightK = TreeKLevel(root->right, k - 1);
	return leftK + rightK;
}

查找值为x的结点

这里依然是先给出代码,然后画递归展开图

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
	BTNode* lret = BinaryTreeFind(root->left, x);
	if (lret)
		return lret;
	BTNode* rret = BinaryTreeFind(root->right, x);
	if (rret)
		return rret;

	return NULL;
}

 棕色为调用函数,绿色为返回函数

层序遍历

层序遍历是按层进行遍历,这里用队列很简单就实现了,还是上图中的那棵树,我们建一个队列,让根1入队,出队时,将它的孩子带入,即2 4,按照队列先进先出原则,此时让2出队,同时将2的孩子带入,此时队中数据为4 3,让4出队并把它的孩子带入,此时队中数据为3 5 6,依然是上面的思路,但因为它们的孩子都是NULL,所以直接出,最后结果就是1 2 4 3 5 6.

关键在于出一层,带入下一层

由于这里我们需要队列,所以我们直接将之前写好的队列导入到现在的文件中,由于这里队列中的数据传入的是结点,所以我们需要将之前定义的DataType修改成这里的结构体指针,指针指向的是结点

typedef struct BinaryTreeNode* QDataType;
void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(front);
		printf("%d ", front->data);
		if (front->left)
			QueuePush(&q, front->left);
		if (front->right)
			QueuePush(&q, front->right);
	}
	QueueDestroy(&q);
}

判断一棵树是否是完全二叉树

利用层序遍历,但是需要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);
		if (front==NULL)
		{
			break;
		}
		else
		{
			QueuePush(&q, front->left);
			QueuePush(&q, front->right);
		}
	}
	//判断是否是完全二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		//后面有非空,说明非空结点不是完全连续
		if (front)
		{
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

二叉树的销毁

 销毁最好使用后序遍历,因为假设你使用前序遍历,先处理根,那么还得先保存左右指针,否则会找不到左右子树

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

到这二叉树就基本结束了,下一篇文章是关于二叉树的练习,我们下一篇见~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值