数据结构——链式二叉树

        在上一篇博客中,已经介绍了顺序二叉树——堆的实现,这次我们接着上一次的成果,继续学习有关于链式二叉树的相关知识。

4.链式二叉树

4.1 链式二叉树的结构定义

        对于二叉链的树而言,我们以链表的形式组织整棵树结构。因为二叉链要求在携带数据的同时,需要标明其左右孩子,因此我们定义的结构体中,有着两个指针,分别指向左右孩子。

typedef char BTDataType;

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

4.2 链式二叉树的接口函数

        对于链式二叉树而言,递归是我们解决其问题的重要思想。一个链式二叉树可以看做是由根节点与其左右子树组成的,所以在对树进行处理的时候,我们一般采用递归分治,对其左右子树进行不断分治处理。初学者可以多多注意这一点。

        我们按照由简到难的顺序来接触二叉树的接口函数。

4.2.1 二叉树结点统计

4.2.1.1 统计结点个数

        这里采用的是很基础的递归方案,即结点个数 = 左树结点个数+右树结点个数 + 1,当遇到了空树时,计0即可。

int BinaryTreeSize(BTNode* root)
{
	return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
4.2.1.2 统计叶子结点个数

        相似的思路:叶子结点个数 = 左树叶子结点个数+右树叶子结点个数,当遇到叶子结点时计1,遇到空树时计0。

int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return root->left == NULL && root->right == NULL ? 1 : BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
4.2.1.3 统计第k层的结点个数

        在递归这个函数时,需要注意记录层数问题,当到达第k层时,对非空结点进行计数。同样的,第k层结点个数 = 左树第k-1层结点个数+右树第k-1层结点个数。

int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	else
	{
		return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
	}
}

4.2.2 二叉树遍历

        二叉树的遍历一般认为前序遍历、中序遍历、后序遍历、层序遍历,其主要描述的是我们访问一棵树的顺序问题。

4.2.2.1 前序遍历

        根 → 左子树 → 右子树

        前序遍历首先访问根结点,然后访问左右子树,根据这个思路,递归代码很容易写出来。前序遍历和深度优先遍历的思想一致。

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

        左子树 → 根 → 右子树

void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("# ");
		return;
	}
	BinaryTreeInOrder(root->left);
	printf("%c ", root->data);
	BinaryTreeInOrder(root->right);
}
4.2.2.3 后序遍历

        左子树 → 右子树 → 根

void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("# ");
		return;
	}
	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);
	printf("%c ", root->data);
}
 4.2.2.4 层序遍历

        层序遍历不比之前三个遍历,层序遍历是按照树的结构进行逐层遍历。这与广度优先搜索类似,在访问一个结点后需要将其两个子结点放入队列中,这样才可以完整遍历整棵树,这也就代表了其代码模式与bfs的形式是相似的。

        此处使用C语言实现,所以需要自己手搓一个队列出来,我就直接套用了之前写过的队列。

void BinaryTreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root != NULL)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		printf("%c ", front->data);
		if (front->left != NULL)
		{
			QueuePush(&q, front->left);
		}
		if (front->right != NULL)
		{
			QueuePush(&q, front->right);
		}
	}
	QueueDestroy(&q);
}

 4.2.3 二叉树遍历的应用

        上文介绍的二叉树的四种遍历在我看来更像是模板,告诉我们需要遍历二叉树时有哪些方案可以选择,在我们处理问题时可以灵活做出选择。在运用以上遍历模式时,需要在适当位置下加入对应的操作代码,这样才能利用遍历来解决我们的问题。

4.2.3.1 查找值为x的结点

        在对树进行查找时,我们需要合理安排策略,由于这个函数具有返回值(需要返回结点地址),所以需要妥善处理不同情况下的返回值。

        首先对根结点进行判断,根结点为空直接返回空;根节点找到符合要求的结点直接返回。否则就对左树递归,如果左树没有找到,那么再对右树进行查找。很明显这是前序遍历在对树进行搜索。

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	BTNode* tmp = BinaryTreeFind(root->left, x);
	if (tmp != NULL)
	{
		return tmp;
	}
	return BinaryTreeFind(root->right, x);
}
4.2.3.2 判断二叉树是否为完全二叉树

        我们已经很明确了完全二叉树的形式特征,判断是否为完全二叉树只需要对树进行层序遍历,当遍历到空结点时就是我们做出判断的时候。如果空结点之后没有其他结点,即队列此时为空,则说明是完全二叉树;如若队列不为空,说明空结点之后存在结点,那么就不是完全二叉树。

bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);

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

		if (front == NULL)
		{
			break;
		}

		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* first = QueueFront(&q);
		QueuePop(&q);
		if (first != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}
4.2.3.3 构建二叉树

        这里介绍的构建二叉树是以前序遍历的方式给出。实际就是将我们前序遍历输出的字符串反向写成一个二叉树。在通过这种方式构建二叉树时,只要给定字符串正确,那么就会保证所有结点都会被字符覆盖到,同时也不需要在意何时结束,因为字符串结束的时刻二叉树一定被构建完成,所有分支都被处理到了。

        当字符为‘#’时为其链接空并返回,因为空结点不存在左右子树。否则,链接对应的字符,并且需要递归链接其左树右树,链接完成后返回根节点。整体与前序遍历相差不大,但是需要注意的是参数pi是标识字符串处理位置的记号。

4.2.3.4 销毁二叉树

        销毁二叉树时,由于左右子树都需要通过根节点访问,所以需要采用后序遍历,先将左右子树分别销毁后,在释放根节点,避免释放过程中丢失子树。

void BinaryTreeDestory(BTNode** root)
{
	if (*root == NULL)
	{
		return;
	}
	BinaryTreeDestory(&(*root)->left);
	BinaryTreeDestory(&(*root)->right);
	free(*root);
	*root = NULL;
}

5、二叉树总结

        这两篇博客我们一共接触了两种二叉树的实现方式,分别是顺序二叉树和链式二叉树。其在实践中有着不可小觑的作用。对于顺序二叉树,我们需要对堆的数据存储方式加强理解,掌握大堆小堆的特征与控制方式;对于链式二叉树,因为其采用了大量的递归,所以对我们训练递归的思维是个很好的训练,而且从控制二叉树的过程中,能窥见一些更深奥的知识。

        树在数据结构中有着举足轻重的地位,未来我相信我们有数不清的机会与它打交道,熟练掌握实现方式,勤加练习,理解思想才是我们学习它的最佳状态。

你可能感兴趣的:(数据结构)