非递归建立二叉树
前言
使用递归(Recursion)建立二叉树(Binary Tree)的非顺序存储结构(即二叉链表),可以简化算法编写的复杂程度,但是递归效率低,而且容易导致堆栈溢出,因而很有必要使用非递归算法。
引入
无论是单链表还是二叉树,创建时要解决问题就是关系的建立,即单链表中前驱节点与当前节点的关系和二叉树中父节点与子节点的关系。
首先,思考一下建立单链表的过程,为了使链表各个节点连接起来,在创建当前节点(q)的时候,需借助一个指针(p)指向前一个节点,然后p->next = q。
由此推广至二叉树,把二叉树每一层比作是链表的节点,接着借助一个指针列表(parent_list)存放父层的所有节点,然后每创建当前层的一个节点(current_node)时就与父层次的节点建立关系。
分析
引入中提出了创建二叉树的整体思想,同时也抛出一个问题,如何建立父节点与子节点的关系?
下图为一棵普通的二叉树,下面对其进行一些处理。
首先,将其补全,用#代表空节点,补全规则为:将只有一个或没有子节点的节点(空节点除外),用空节点补全为两个子节点。
为便于后续分析,将其节点左结构化并去掉关系线。
现在,回顾一下引入中提到的“把二叉树每一层比作链表的节点”,而建立单链表每次都只涉及两个节点,因而下面每次分析都只涉及两层。
其中,规定第二层为当前层,第一层为当前层的父层,且当前层为下次分析的父层。
在规定,父层为一个数组p[i](i为父层节点数,后同),当前层为数组q[j]。
下图为第一次,选中层的节点标记为深灰底色。
可以容易看出,当前层的两个节点与父层节点的关系:
p[0]->next = q[0]
p[0]->next = q[1]
为其添加关系线,然后再看下一次。
同样,其关系如下:
p[0]->next = q[0]
p[0]->next = q[1] = #
p[1]->next = q[2]
p[2]->next = q[3]
由前两次不难得出,j/2 = i (注意这里 / 运算结果只取整数),这个结果和完全二叉树的性质相同,但是注意这里不是一棵完全二叉树。
接着看下一次。
如图所示,为了建立正确的二叉树关系,父层的节点一定不能为空节点。
总观整个结构,可以得出一个规律(该规律可用于动态改变父层列表,以减少内存占用):父层节点数(不包括空节点)的两倍恰好为当前层的节点数(包括空节点)。
此时,还有个小问题,便是判断当前节点是左子树(Left Subtree)还是右子树(Right Subtree)?
这里解决方法很简单,便是计算 j % 2 ,若为0则为左子树,否则为右子树。
实现
现在,理清一下思路:
1.以从上到下、从左到右的顺序创建二叉树,因此有两层循环。
2.有个父层列表(parent_list)用于存放父层所有节点的地址,且外层循环一次就更新一次(即让父层列表等于当前层列表,代码中为tmp_list),同时释放旧父层列表。
3.内层循环创建每一层的节点,若输入数据不为“#”则不为空节点,然后申请内存空间并赋值,再根据上述论述进行父节点和当前节点(current_node)建立关系。
下面给出代码:
#include <stdio.h> #include <malloc.h> // 布尔类型 typedef enum {FALSE=0,TRUE=1} bool; // 用于标识当前建立左子树还是右子树 typedef enum {LEFT=0,RIGHT=1} flag; // 节点存放数据的类型 typedef char data_type; // 二叉树节点类型 typedef struct node { data_type data; struct node *left_subtree, *right_subtree; } node , *bin_tree; bool create_bin_tree(bin_tree *root) { /* 创建根节点 */ data_type data = '\0'; scanf("%c",&data); if(data == '#'){ return FALSE; // 根节点为空,创建失败 } else { *root = (node*)malloc(sizeof(node)); (*root)->data = data; (*root)->left_subtree = NULL; (*root)->right_subtree = NULL; } /* 创建非根节点 */ // 存放父层的节点列表 node **parent_list = (node**)malloc(sizeof(node*)); parent_list[0] = *root; // 父节点个数 int parent_amount = 1; while(1) { // 当前节点个数,设置为父节点个数的两倍 int current_amount = parent_amount * 2; // 创建临时列表存放当前深度的节点 node **tmp_list = (node**)malloc(sizeof(node*) * current_amount); // 用于记录当前深度节点非空节点个数 int count = 0; // 创建当前层次的所有节点 int j = 0; for(;j < current_amount;++j) { data = '\0'; scanf("%c",&data); if(data != '#') // 不为空节点 { // 新建节点并赋值 node *current_node = (node*)malloc(sizeof(node)); current_node->data = data; current_node->left_subtree = NULL; current_node->right_subtree = NULL; // 加入到临时列表中 tmp_list[count] = current_node; // 非空节点数加1 count++; // 与父节点建立关系 if(j%2 == LEFT) { (parent_list[j/2])->left_subtree = current_node; } else { (parent_list[j/2])->right_subtree = current_node; } } } // for循环结束 // 释放父层列表 free(parent_list); // 更新父层列表 parent_list = tmp_list; // 更新父节点数 parent_amount = count; // 若非空节点数为0,则停止创建 if(count == 0) break; } return TRUE; }
上述代码中,由于根节点较特殊且需要传出地址,为了降低代码编写复杂程度,因而独立于内层循环。
后话
写文章除了用于记录外,其实也无形中能理清想问题的思路。如写这篇文章前,代码虽实现了,但总感觉思路很乱。而文章写完了,便也豁然开朗了。