二叉树的构建

先复习一下二叉树的遍历:

前序遍历:根结点—>左子树 —>右子树

中序遍历:左子树—>根结点 —>右子树

后序遍历:左子树 —>右子树 —>根结点

如果知道了某个遍历序列,能否还原出那个二叉树呢?

要构建一棵二叉树,仅靠前序、中序和后序中的任何一个都不能,因为不能唯一确定一棵二叉树。

二叉树的构建主要有以下方法:

  1. 中序 + 前序
  2. 中序 + 后序
  3. 中序 + 层序
  4. 扩充二叉树的前序
  5. 扩充二叉树的后序

对于 1-3:前序、后序、层序用来提供根结点信息,中序用来区分左右子树;

对于 4-5:根据二叉树对应的扩充二叉树的前序或者后序序列来确定。注意,扩充二叉树的中序不能唯一确定一棵二叉树

什么是扩充二叉树

扩充二叉树是指在二叉树中出现空子树的位置增加空树叶所形成的二叉树。

举个例子,下图中右边的是左边的扩充二叉树(扩充结点用 -1 表示)。

在这里插入图片描述

因为扩充二叉树包含空的叶子结点,可以用来判断子树是否为空,因此可以通过扩充二叉树的前序或者后序序列唯一构建一棵二叉树。

扩充二叉树的前序遍历

为了验证代码的正确性,我们要把构造完成的树打印输出。可以打印出对应的扩充二叉树的前序,因为从这个序列还原出的二叉树是唯一的。

代码是:

//扩充二叉树的前序遍历(递归)

void extended_binary_tree_pre_traverse(node_t *root)
{
	if(root == NULL){
		printf("#");
		return;
	}
	
	printf("%d",root->data);
	extended_binary_tree_pre_traverse(root->left);
	extended_binary_tree_pre_traverse(root->right);
}

注意:扩充出的节点用 “#” 表示。

在后面的例子中会调用这个函数来验证代码的正确性。

二叉树构建:前序 + 中序

构建过程:
(1)根据给定的树写出前序和中序序列;
(2)前序序列中的第一个数字为根结点 R,构造根结点;
(3)找到根结点在中序序列中的位置,此位置左右两边分别为左子树和右子树的中序序列,根据左右子树结点的数量可以在前序序列根节点 R 后面分别找到左子树和右子树的前序序列;
(4)递归处理左右子树,完成构造。

在这里插入图片描述

前序:{1,2,4,3,5,6}

中序:{2,4,1,3,6,5}

在这里插入图片描述

先看这段代码:

// 结点的结构体
typedef struct node{
	int data;
	struct node *left;
	struct node *right;
}node_t;


int search_node(int key, int *arr, int len)
{
	int loc = -1;
	for(int i=0; i<len; ++i){
		if(arr[i] == key){
			loc = i;		
			break;
		}
	}
	
	return loc;	
}

search_node 是一个辅助函数,用来在数组中寻找某个关键字(假设关键字不重复),找到则返回其下标,找不到返回 -1;

// @brief: 根据前序序列和中序序列构建二叉树
// @param: 
//       pre :前序序列数组的指针
//       mid :中序序列数组的指针
//       len :数组长度,即节点数
// @ret: 二叉树根结点

node_t *construct_pre_and_mid(int* pre, int* mid, int len)
{
	//异常处理
	if((pre == NULL) || (mid == NULL) || (len == 0)){
		printf("input error\n"); 
		return NULL; 
	}
	
	//前序遍历的第一个值就是根节点
	int root = pre[0];
	node_t *new_node = malloc(sizeof(node_t));
	if(new_node == NULL){
		printf("malloc failed\n");
		return NULL;
	}
	new_node->data = root;
	new_node->left = NULL;
	new_node->right = NULL;
	
	// 只有一个节点
	if (len == 1) {
		if (pre[0] == mid[0]) {
			return new_node;
		} else {
			printf("input error\n");			
			return NULL;
		}
	}
	
	// 在中序序列中找根节点	
	int loc = search_node(root, mid, len);
	if(loc < 0){
		printf("input error\n");			
		return NULL;		
	}
	
	int left_len = loc;
	int right_len = len - 1 - left_len;
	// 构建左子树
	if(left_len > 0){
		new_node->left = construct_pre_and_mid(pre + 1, 
					mid, left_len);	
	}
	// 构建右子树
	if(right_len > 0){
		new_node->right = construct_pre_and_mid(pre + 1 + left_len, 
					mid + loc + 1, right_len);
	}
	
	return new_node;
	
}

测试代码:

int main(void) 
{
	int preorder[] = {1,2,4,3,5,6};
	int midorder[] = {2,4,1,3,6,5};
	
	node_t* root = construct_pre_and_mid(preorder, 
			midorder, sizeof preorder/ sizeof preorder[0]);
	extended_binary_tree_pre_traverse(root);
}

输出结果:

12#4##3#56###

二叉树构建:后序 + 中序

这种方法和前面的“前序+中序”构建过程很像,不同在于:因为是后序,所以根结点是后序序列的最后一个。

还是以上面的二叉树为例,给出构建过程。
(1)根据给定的二叉树,得到后序和中序序列;
(2)后序序列中的最后一个数字为根结点 R,构造根结点;
(3)找到根结点在中序序列中的位置,此位置左右两边分别为左子树和右子树的中序序列,根据左右子树结点数量可以在后序序列根结点 R 前面分别找到左子树和右子树的后序序列;
(4)递归处理左右子树,完成构造。

在这里插入图片描述

在这里插入图片描述

代码:

node_t *construct_post_and_mid(int* post, int* mid, int len)
{
	//异常处理
	if((post == NULL) || (mid == NULL) || (len == 0)){
		printf("input error\n"); 
		return NULL; 
	}
	
	//后序遍历的最后一个值就是根节点
	int root = post[len-1];
	node_t *new_node = malloc(sizeof(node_t));
	if(new_node == NULL){
		printf("malloc failed\n");
		return NULL;
	}
	new_node->data = root;
	new_node->left = NULL;
	new_node->right = NULL;
	
	// 只有一个节点
	if (len == 1) {
		if (post[0] == mid[0]) {
			return new_node;
		} else {
			printf("input error\n");			
			return NULL;
		}
	}
	
	// 在中序序列中找根节点
	
	int loc = search_node(root, mid, len);
	if(loc < 0){
		printf("input error\n");			
		return NULL;		
	}
	
	int left_len = loc;
	int right_len = len - 1 - left_len;
	// 构建左子树
	if(left_len > 0){
		new_node->left = construct_post_and_mid(post, 
					mid, left_len);	
	}
	// 构建右子树
	if(right_len > 0){
		new_node->right = construct_post_and_mid(post + left_len, 
					mid + loc + 1, right_len);
	}
	
	return new_node;
	
}

测试代码

int main(void) 
{	
	int midorder[] = {2,4,1,3,6,5};
	int postorder[] = {4,2,6,5,3,1};
	
	node_t* root = construct_post_and_mid(postorder, 
			midorder, sizeof postorder/ sizeof postorder[0]);
	extended_binary_tree_pre_traverse(root);
}

输出结果:

12#4##3#56###

二叉树构建:层序 + 中序

还是以上面的二叉树为例,给出构建过程。

(1)根据给定的二叉树,得到层序序列 {1,2,3,4,5,6,} 和中序序列为 {2,4,1,3,6,5};
(2)层序序列中第一个数字为根结点 R,构造根结点;
(3)找到根结点 R 在中序序列中的位置,此位置左右两边分别为左子树和右子树的中序序列。根据层序序列,找到左子树和右子树的根结点;
(4)递归处理左右子树,完成构造。

对于(3),要解释一下,怎样找到左子树或者右子树的根结点呢?

在这里插入图片描述

请看图中用红圈圈出的部分:对于左子树,根结点是 2,仔细观察,可以发现在层序序列中, 2 比 4 要先出现;同理,对于右子树,根结点是 3,仔细观察,发现在层序序列中,3 比 5 和 6 要先出现。

于是得出结论:层序序列中先出现的就是根结点。

为了迅速判断子树中的哪一个值是在层序序列中最先出现的,可以构造一个辅助数组,用来记录中序序列中每个元素在层序序列中的下标。

在这里插入图片描述

比如,中序序列的第一个值是 2,它在层序序列中的下标是 1;再比如,中序序列最后一个值是 5,它在层序序列中的下标是 4;

构造好辅助序列后,就可以抛开层序序列了。利用中序序列和辅助序列就可以递归构造出整个树。

首先找到辅助序列中最小的值——0,它对应中序里面的 1,那么 1 就是根结点;再看左子树{2,4},看它们的辅助序列,2 比 4 小,所以 2 是根结点;同理判断出右子树的根结点是 3;同理,递归构建左子树和右子树。

思路有了,看看代码。

// 构造辅助数组
int build_aux(int* lvl, int* mid, int len, int *aux)
{
	if((lvl == NULL) || (mid == NULL) 
		|| (aux == NULL) || (len == 0)){
		return -1;
	}
	
	// 查找 mid[i] 在 lvl[] 中的下标,填入 aux[i]
	for(int i=0; i<len; ++i){
		aux[i] = search_node(mid[i], lvl, len);		
	}
	return 0;
}
// 找出数组中最小的值,返回下标
int find_min_loc(int *arr, int len)
{
	int loc = 0;
	for(int i=1; i<len; ++i){
		if(arr[i] < arr[loc])
			loc = i;
	}
	return loc;	
}
node_t *construct_level_and_mid(int* lvl, int *mid, int len)
{
	if((mid == NULL) || (lvl == NULL) || (len == 0)){
		printf("input error\n");
		return NULL; //异常处理
	}
	// 为辅助数组分配空间
	int *aux = malloc(sizeof(lvl[0]) * len);
	if(aux == NULL){
		printf("malloc failed\n");
		return NULL;
	}
	
	if(0 != build_aux(lvl, mid, len, aux)){		
		printf("build aux array failed\n");
		free(aux);
		return NULL;
	}
	
	node_t *root = __construct_level_and_mid(mid, aux, len);
	free(aux);
	return root;	
}

上面第 20 行才是递归的核心代码

node_t *__construct_level_and_mid(int* mid, int *aux, int len)
{
	if((mid == NULL) || (aux == NULL) || (len == 0)){
		printf("input error\n");
		return NULL; //异常处理
	}
		
	int root_idx = find_min_loc(aux, len); // 最先出现的就是树根
			
	int root = mid[root_idx];
	node_t *new_node = malloc(sizeof(node_t));
	if(new_node == NULL){
		printf("malloc failed\n");
		return NULL;
	}
	new_node->data = root;
	new_node->left = NULL;
	new_node->right = NULL;
	
		// 只有一个节点
	if (len == 1) {		
		return new_node;		
	}
	
	int left_len = root_idx;
	int right_len = len - 1 - left_len;
	
	if(left_len > 0)
		new_node->left = __construct_level_and_mid(mid, 
				aux, left_len);
	
	if(right_len > 0)
		new_node->right = __construct_level_and_mid(
				mid+root_idx+1, aux+root_idx+1, right_len);	
	return new_node;
}

测试代码

int main(void) 
{
	int midorder[] = {2,4,1,3,6,5};
	int levelorder[] = { 1,2,3,4,5,6 };
	
	node_t* root = construct_level_and_mid(levelorder, midorder, 6);
	extended_binary_tree_pre_traverse(root);
}

输出结果:

12#4##3#56###

二叉树构建:扩充二叉树前序

构建过程:
(1)根据给定的树写出扩充二叉树的前序序列;
(2)前序序列中的第一个数字为根结点 R,构造根结点;
(3)R 后面第一个数字作为新序列的头,用这个数列构造左子树,构造完成后,返回新的头
(4)根据新的头,构造右子树
(5)如果遇到 -1 ,说明子树为空,仅返回新的头

这里的难点是,每一次构造,数列的头都不一样。

在这里插入图片描述

举个例子,图中的扩充二叉树的先序序列是(用 # 表示 -1):

12#4##3#56###

首先,1 是根结点;2#4##3#56### 来构造 1 的左子树

2 是根结点,用 #4##3#56### 构造 2 的左子树

遇到了 #,返回;然后用 4##3#56### 构造 2 的右子树

3#56### 构造 1 的右子树

注意黑体字的部分,构造左子树传入的序列和右子树不一样,所以,要想办法知道每次构造应该从数组的哪里开始,即“头”是什么

先来一个错误的写法

// 错误的写法!!!
node_t *construct_extended_binary_tree_pre_order(int *order)
{	
	if(*order == -1){
		order++;
		return NULL;
	}
	
	node_t *new_node = malloc(sizeof(node_t));
	if(new_node == NULL)
	{
		printf("malloc failed\n");
		return NULL;
	}
	new_node->data = *order;
	order++;
	
	new_node->left = construct_extended_binary_tree_pre_order(order);
	new_node->right = construct_extended_binary_tree_pre_order(order);
	return new_node;	
}

错在哪里了?

order 是一个输入型参数,指向数组的第一个元素,但是它用完就完了,在函数返回的时候,它的值无法传递给调用它的函数。

乍一看,第 16 行的 order++; 改变了数组的头,也传给了下一次调用(第 18 行)

但是,第 19 行怎么办?这里的 order 和第 18 行的 order 是一个值,显然是不对的。

所以,要把 order 改成 in-out 型参数。它既可以标识开始调用时头在哪里,也可以反映函数退出时新的头在哪里。

int *order 要改为 int **order

如果不理解,举个例子,假设要给一个整数加 3,你传入一个 int 类型的参数没有用,因为虽然在函数里面加 3 了,但是这个结果没法传出来(假设不用 return)

void add(int a)
{
    a += 3;
}

要想把结果回传出来,要用 int* 类型

void add(int *a)
{
    *a += 3;
}

这样才能达到目的。

所以,正确的写法是:

node_t *construct_extended_binary_tree_pre_order(int **order)
{
	if((order == NULL) || (*order == NULL)){
		printf("input error\n");
		return NULL; //异常处理
	}
	
	if(**order == -1){
		*order += 1;// 改变数组的“头”
		return NULL;
	}
	
	node_t *new_node = malloc(sizeof(node_t));
	if(new_node == NULL){
		printf("malloc failed\n");
		return NULL;
	}
	new_node->data = **order;
	*order += 1; // 改变数组的“头”
	new_node->left = construct_extended_binary_tree_pre_order(order);
	new_node->right = construct_extended_binary_tree_pre_order(order);
	return new_node;	
}

函数的参数 order 是指向数组的首地址的指针

第 9 行,*order += 1; 就是 *order = *order + 1;

解释一下,你可以想象 order 是一个容器,里面盛放着数组的首地址,*order 就代表数组首地址,

*order + 1 表示首地址指向下一个元素,即新的首地址,最后再赋值给 *order,即把新的首地址放到 order 这个容器里

测试代码:

int main(void) 
{
	int ext_btree_pre[] = {1, 2, -1, 4, -1, -1, 3, -1, 5, 6, -1 ,-1, -1};	
	int* p = ext_btree_pre; // p 指向 ext_btree_pre[0]
	
	// 传入 p 的地址,这样可以改变 p 的值
	node_t* root = construct_extended_binary_tree_pre_order(&p); 		
	extended_binary_tree_pre_traverse(root);
}

输出结果:

12#4##3#56###

除了上面这种写法,我还能想到一种方法——利用函数内的静态变量

node_t *construct_extended_binary_tree_pre_order_2(int *order)
{
	static int idx = 0;
	
	if(order == NULL){
		printf("input error\n");
		return NULL; //异常处理
	}
	
	if(order[idx] == -1){
		idx++;
		return NULL;
	}
	
	node_t *new_node = malloc(sizeof(node_t));
	if(new_node == NULL){
		printf("malloc failed\n");
		return NULL;
	}
	new_node->data = order[idx];
	idx++;
	
	new_node->left = construct_extended_binary_tree_pre_order_2(order);
	new_node->right = construct_extended_binary_tree_pre_order_2(order);
	return new_node;		
}

函数的参数是数组的首地址,为了记录“头”的变化,用了静态变量 idx

注意,在整个递归过程中,order 的值不变,一直是数组的首地址

测试代码:

int main(void) 
{
	int ext_btree_pre[] = {1, 2, -1, 4, -1, -1, 3, -1, 5, 6, -1 ,-1, -1};	
	node_t* root = construct_extended_binary_tree_pre_order_2(ext_btree_pre);	
	extended_binary_tree_pre_traverse(root);
}

输出结果:

12#4##3#56###

二叉树构建:扩充二叉树后序

在这里插入图片描述

后序(左-》右-》根)序列是:

-1, -1, -1, 4, 2, -1, -1, -1, 6, -1, 5, 3, 1

如果你从后往前看这个数组,就是按照 “根-》右-》左”

所以,可以参考前序的代码,从数组尾巴开始,再对调左右子树的创建顺序

代码:

node_t *construct_extended_binary_tree_post_order(int **order)
{
	if((order == NULL) || (*order == NULL)){
		printf("input error\n");
		return NULL; //异常处理
	}
	
	if(**order == -1){
		*order -= 1;// 改变数组的“头”
		return NULL;
	}
	
	node_t *new_node = malloc(sizeof(node_t));
	if(new_node == NULL){
		printf("malloc failed\n");
		return NULL;
	}
	new_node->data = **order;
	*order -= 1; // 改变数组的“头”
	
	new_node->right = construct_extended_binary_tree_post_order(order);
	new_node->left = construct_extended_binary_tree_post_order(order);
	
	return new_node;	
}

注意,函数的参数是序列的末尾元素的指针的指针

测试代码:

int main(void) 
{	
	int ext_btree_post[] = {-1, -1, -1, 4, 2, -1, -1, -1, 6, -1, 5, 3, 1};
	
	int *p = ext_btree_post +         // 让 p 指向数组最后一个元素
				sizeof ext_btree_post/ sizeof ext_btree_post[0] - 1;
	
	node_t* root = construct_extended_binary_tree_post_order(&p);			

	extended_binary_tree_pre_traverse(root);
}

输出结果:

12#4##3#56###

另一个版本:

node_t *construct_extended_binary_tree_post_order(int *order)
{
	static int idx = 0;
	
	if(order == NULL){
		printf("input error\n");
		return NULL; //异常处理
	}
	
	if(order[idx] == -1){
		idx--;
		return NULL;
	}
	
	node_t *new_node = malloc(sizeof(node_t));
	if(new_node == NULL){
		printf("malloc failed\n");
		return NULL;
	}
	new_node->data = order[idx];
	idx--;
	new_node->right = construct_extended_binary_tree_post_order(order);
	new_node->left = construct_extended_binary_tree_post_order(order);
	
	return new_node;		
}

测试代码:

int main(void) 
{

	int ext_btree_post[] = {-1, -1, -1, 4, 2, -1, -1, -1, 6, -1, 5, 3, 1};

	node_t* root = construct_extended_binary_tree_post_order(ext_btree_post+
					sizeof ext_btree_post/ sizeof ext_btree_post[0] - 1);
		
	extended_binary_tree_pre_traverse(root);
}

第 6 行传入的参数是数组最后一个元素的地址

输出结果:

12#4##3#56###

【end】


参考资料

二叉树的构建

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值