剑指offer --- 重建二叉树

目录

前言

一、读懂题目

二、思路分析

三、代码呈现

总结


前言

        本文将探讨如何根据给定的前序遍历序列和中序遍历序列构建二叉树。通过分析前序和中序遍历的特点,我们可以找到根节点以及左右子树的序列,然后利用递归的方式逐步构建整棵树。同时,我们还需要考虑一些边界情况和特殊测试用例,以提高程序的鲁棒性。


一、读懂题目

题目: 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入的前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建出二叉树并输出它的头结点。

        在二叉树的前序遍历序列中,第一个数字总是树的根结点的值。但在中序遍历序列中,根结点的值在序列的中间左子树的结点的值位于根结点的值的左边,而右子树的结点的值位于根结点的值的右边。因此我们需要扫描中序遍历序列,才能找到根结点的值。

二、思路分析

        现就以题中所给前序遍历和中序遍历序列为例,整理利用前中序遍历构建二叉树的步骤:

        由于在中序遍历序列中,有 3 个数字是左子树结点的值,因此左子树总共有 3 个左子结点。同样,在前序遍历的序列中,根结点后面的 3 个数字就是 3 个左子树结点的值,再后面的所有数字都是右子树结点的值。这样我们就在前序遍历和中序遍历两个序列中,分别找到了左右子树对应的子序列。

从而我们对两序列首次划分,画图可得:

(注:此后红色表示根节点,黄色表示左子树,绿色表示右子树

既然我们已经分别找到了左、右子树的前序遍历序列和中序遍历序列,我们可以用同样的方法分别去构建左右子树。也就是说,接下来的事情可以用递归的方法去完成。

以值为 1 为根节点的左子树

以值为 1 为根节点的右子树

现在有两个二级根节点(2,3),因为两子树左右序列并不都为空,所以重复上面过程:

以值为 2 为根节点的左子树

以值为 2 为根节点的右子树:空。

以值为 3 为根节点的左子树:值为5的节点。

以值为 3 为根节点的右子树

现在有两个三级根节点(4,6),重复上面过程:

以值为 4 为根节点的左子树:空。

以值为 4 为根节点的右子树:值为7的节点。

以值为 6 为根节点的左子树:值为8的节点。

以值为 6 为根节点的右子树:空。

现在深度最深的节点都已经是叶节点,此时构建结束。

所以我们可以按照此思路写出相应的函数以实现特定功能,同时需要注意程序的鲁棒性,注意对边界值和特殊测试用例的考量。

三、代码呈现

首先我们给出树节点的定义:

typedef struct TreeNode
{
	int val;
	TreeNode* left;
	TreeNode* right;
}TreeNode;

接着提供一个接口(Construct()函数)用于调用递归函数和接收输入的前序、中序序列:

TreeNode* Construct(int* pre_arr, int* in_arr, size_t length)
{
	assert(pre_arr && in_arr && length > 0);

	TreeNode* result = constructTree(pre_arr, pre_arr + length - 1, in_arr, in_arr + length - 1);
	if (result == NULL)
	{
		printf("构建结果为空!\n");
	}
	return result;
}

编写递归函数(constructTree()函数)的具体实现内容:

TreeNode* constructTree(int* preorder_begin, int* preorder_end, int* inorder_begin, int* inorder_end)
{
	TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
	root->val = preorder_begin[0];     // 数组首元素
	root->left = NULL;
	root->right = NULL;

	if (preorder_begin == preorder_end && inorder_begin == inorder_end)    // 递归退出
	{
		if (*preorder_begin == *preorder_end) { return root; }
		printf("输入的序列无法构建二叉树\n");
		return NULL;
	}

	int* index = inorder_begin;
	while (index < inorder_end && *index != preorder_begin[0])
	{
		++index;
	}
	if (index == inorder_end && *index != preorder_begin[0])     // 中序数组中未找到前序首元素相等的值
	{
		printf("error:中序数组中未找到前序首元素相等的值\n");
		return NULL;
	}

	size_t left_length = index - inorder_begin;
	int* left_preorder_end = preorder_begin + left_length;
	if (left_length > 0)
	{
		root->left = constructTree(preorder_begin + 1, left_preorder_end, inorder_begin, index - 1);
	}

	if (left_length < preorder_end - preorder_begin)
	{
		root->right = constructTree(left_preorder_end + 1, preorder_end, index + 1, inorder_end);
	}

	return root;
}

为了便于测试,同时给出层序遍历函数和释放节点堆内存的函数:

size_t levelOrderPrint(TreeNode* root)
{
	if (root == NULL) { return 0; }

	size_t count = 0;
	queue<TreeNode*> q;
	q.push(root);

	while (!q.empty()) {
		TreeNode* node = q.front();
		q.pop();

		printf("%d ", node->val);

		if (node->left != NULL) {
			q.push(node->left);
		}
		if (node->right != NULL) {
			q.push(node->right);
		}

		count++;
	}

	return count;
}

void freeTree(TreeNode* root)
{
	if (root == NULL) { return; }

	freeTree(root->left);
	freeTree(root->right);

	free(root);
	root = NULL;
}

当然递归函数我们要注意:

        1. 递归函数参数

        2. 递归终止条件

        3. 递归函数功能重要操作

再给出测试用例代码:

void test1()
{
	int pre_arr[] = { 1,2,4,7,3,5,6,8 };
	int in_arr[] = { 4,7,2,1,5,3,8,6 };

	TreeNode* root = Construct(pre_arr, in_arr, sizeof(pre_arr) / sizeof(pre_arr[0]));

	size_t count = levelOrderPrint(root);
	printf("节点个数:%zd\n", count);

	freeTree(root);
}

运行结果:

我们呈现的结果是基于层序遍历实现的,当然也可以通过前中后序遍历自行验证。

特殊测试用例:

1)无法构建二叉树的两序列

void test2()
{
	int pre_arr[] = { 1,4,7,3,5,6,2,8 };
	int in_arr[] = { 4,7,2,1,5,3,8,6 };

	TreeNode* root = Construct(pre_arr, in_arr, sizeof(pre_arr) / sizeof(pre_arr[0]));

	size_t count = levelOrderPrint(root);
	printf("节点个数:%zd\n", count);

	freeTree(root);
}

运行结果:

可以看到出现自定义报错,说明某次递归执行过程中发现逻辑冲突,所以该二叉树无法重建。但是由于C语言本身对于递归过程整体跳出操作的局限性,可以利用C++中异常处理的抛出异常来实现避免后续所有递归环节执行,使得外部及时接收到错误指令并进行相应操作。

2)传入空的数组

void test3()
{
	int pre_arr[10] = {};    // 默认初始化为0
	int in_arr[10] = {};
	TreeNode* root = Construct(pre_arr, in_arr, sizeof(pre_arr) / sizeof(pre_arr[0]));

	size_t count = levelOrderPrint(root);
	printf("节点个数:%zd\n", count);

	freeTree(root);
}

运行结果:

3)构建出的二叉树根节点为空指针

void test4()
{
	int pre_arr[10] = {1,2,3,4,5,6,7,8,9,0};
	int in_arr[10] = {0};
	TreeNode* root = Construct(pre_arr, in_arr, sizeof(pre_arr) / sizeof(pre_arr[0]));

	size_t count = levelOrderPrint(root);
	printf("节点个数:%zd\n", count);

	freeTree(root);
}

运行结果:


总结

        通过本文的介绍,我们了解了如何根据给定的前序遍历序列和中序遍历序列构建二叉树。通过递归的方式,我们可以根据前序序列确定根节点,在中序序列中找到左右子树的序列,然后递归构建子树。同时,我们还讨论了一些特殊情况和边界测试用例,以确保程序的正确性和鲁棒性。在实际应用中,我们可以根据这种构建二叉树的方法解决相关问题。然而,需要注意的是,在实际编程中还需考虑内存管理异常处理等问题,以确保程序的稳定性和可靠性。通过深入理解并应用构建二叉树的算法,我们能够更好地理解和解决与二叉树相关的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

螺蛳粉只吃炸蛋的走风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值