二叉树是一种重要的数据结构,在排序、信息编码、表达式转化等方面有着重要的作用。由于本人之前已在此平台中发布了有关二叉树递归遍历算法的文档(链接见下),所以本篇博客本人主要讲述二叉树的非递归遍历算法。
二叉树的递归遍历与相关统计
非递归遍历的基本原理
对递归调用有了初步了解的读者就会知道,任何递归调用无论时间空间复杂度如何,都需要使用到栈这一数据结构。递归的过程是不仅仅是一个函数自身调用自身的循环过程,本质上说,应该是一个回溯之后再还原的过程:回溯的阶段,函数要不断保存每一次递归的信息,以便在之后的还原过程中,一步步通过每一次递归的信息,找到最初的状态,最终求得问题的答案… 可以发现,回溯过程中最后一次保存的递归的信息,往往是还原过程中第一步要读取的信息… 依照这个规律推导下去,就不难发现,递归的实现要用到栈(越晚入栈的递归信息,在还原时越先被访问)。
上面的解释可以说明,非递归遍历二叉树的算法中,一定要用到栈这一数据结构。依照根结点遍历的顺序,元素入栈出栈的顺序会有所不同。
非递归遍历的算法
为了方便算法的设计与分析,本人用二叉链表的方式表示一棵二叉树,仍然用递归的方式,创建一棵二叉树,其对应的代码段如下(C++):
class tNode{
public:
char ch;
tNode *lchild;
tNode *rchild;
};
tNode *initTree(){
tNode *root;
char ch;
scanf("%c", &ch);
if(ch=='#')
root = NULL; //输入‘#’说明此时创建的这个结点是一个空结点
else{
root = new tNode;
root->ch = ch;
root->lchild = initTree();
root->rchild = initTree();
}
return root;
}
下面就前序、中序和后序遍历的三大模式,给出对应的算法。
前序遍历(DLR)
- 先判断二叉树是否为空树,不是空树时,定义一个tNode 的指针node指向根结点;
- 打印输出node->ch;
- 若node->rchild不为空,则将node->rchild入栈;
- node = node->lchild,若node不为空,跳转到2,否则跳转到5;
- 退栈直到node->rchild不为空为止,转2,直到栈为空为止。
上面是算法描述看似很复杂,但实际上大的动作无外乎五个:结点值打印输出、右子树入栈、转左子树、退栈、转右子树。其中,第一二三个操作可以在一个循环内进行,而退栈操作,要单独使用一个if-else语句。核心的代码部分,是一个大循环中嵌套一个子循环和if-else的结构。
函数源码:
void DLR(tNode *root){
stack<tNode *> s_tree;
tNode *node = root;
while(node || !s_tree.empty()){
while(node){
cout << node->ch << " ";
s_tree.push(node);
node = node->lchild;
}
if(!s_tree.empty()){
node = s_tree.top();
s_tree.pop();
node = node->rchild;
}
}
}
中序遍历(LDR)
- 先判断二叉树是否为空树,不是空树时,定义一个tNode 的指针node指向根结点;
- 若node不为空,则node入栈,node=node->lchild;
- 若node为空,则退栈直到node->rchild不为空,访问node所指的结点;
- node=node->rchild,转2,直到栈空为止。
以上的算法描述和DLR有着相似之处,只是三大步骤的顺序不同罢了,LDR的三大步骤顺序为右子树入栈、结点访问、转左子树、出栈、转右子树。在代码中,应该注意的要点也和DLR一样。
函数源码:
void LDR(tNode *root){
stack<tNode *> s_tree;
tNode *node = root;
while(node || !s_tree.empty()){
while(node){
s_tree.push(node);
node = node->lchild;
}
if(!s_tree.empty()){
node = s_tree.top();
s_tree.pop();
cout << node->ch << " ";
node = node->rchild;
}
}
}
后序遍历(LRD)
后序遍历的算法在空间复杂度上比DLR与LRD要复杂,主要在于要引入标志栈s_tag,来存储各个结点的访问状态。
- 引入一个bool类型的标志栈s_tag,用于存储结点栈中结点的访问信息;
- 先判断二叉树是否为空树,不是空树时,定义一个tNode 的指针node指向根结点;
- 若node不为空,则node入栈,并在s_tag中入栈一个false,node=node->lchild;
- 若node为空,则进入退栈的循环(s_tag和s_node同时退栈),退栈的条件是:node->rchild为空,或node对应在s_tag中的状态为true;停止退栈的条件是:node->rchild为空,且node对应在s_tag中的状态为false,当遇到这种情形时,还要将s_tag中栈顶的元素改为true;
- node=node->rchild,转3,直到栈空为止。
由于LRD算法涉及两个栈的使用,所以这里再说一下代码的基本结构。
从上面的算法描述,可以将LRD算法分为三个步骤,分别为左子树入栈、s_node与s_tag退栈、修改s_tag栈顶元素、转换到右子树。其中,左子树入栈单独使用一个循环、s_node与s_tag退栈和修改s_tag栈顶元素使用一个循环,这两个循环嵌套到一个大循环里,其中第二个循环内使用if条件判断语句,作为循环结束的条件。
函数源码:
void LRD(tNode *root){
stack<tNode *> s_node;
tNode *node = root, *bottom;
stack<bool> s_tag;
if(!root);
else{
do{
for(; node; node = node->lchild){
s_node.push(node);
s_tag.push(false);
if(s_node.size() == 1)
bottom = node;
}
while(1){
node = s_node.top();
if(!node->rchild || s_tag.top()){
cout << node->ch << " ";
s_node.pop();
s_tag.pop();
if(node == bottom)
break;
}
else{
s_tag.pop();
s_tag.push(true);
break;
}
}
node = node->rchild;
}while(!s_node.empty());
}
}
本人在写这个程序的时候,犯了一个错误,就是第二个循环中少写了一个循环结束的条件(node==bottom),后来本人才弄明白,当最后一个元素(即根结点)出栈后,内层循环未能退出,导致栈空之后又执行了s_node.top()的语句,从而程序发生了异常,所以出错了。这也是本人引入bottom这个变量的理由。
层次遍历(level order)
层次遍历也是一种二叉树的遍历方式,此方式的实现与之前的三种方式有很大区别,不需要使用栈,但是需要使用队列。
- 先判断二叉树是否为空树,不是空树时,定义一个tNode 的指针node指向根结点,node入队列;
- node指向此时的队首元素,打印输出node的信息;
- 此时队列中的队首元素出队;
- 若node->lchild不为空,node->lchild入栈;若node->rchild不为空,node->rchild入栈;
- 转2,直到队列为空。
上述描述的算法比较简单,这里直接给出源码:
void level_order(tNode *root){
queue<tNode *> qu;
tNode *node;
if(root)
qu.push(root);
else
return;
while(!qu.empty()){
node = qu.front(); //Who will dequeue is the front.
cout << node->ch << " ";
qu.pop();
if(node->lchild)
qu.push(node->lchild);
if(node->rchild)
qu.push(node->rchild);
}
}
代码整合
以上给出的只是某些函数的代码,完整的代码如下,此代码可以直接运行得到相应的结果。代码中包含了C++中实现栈和队列的模板库。
代码整合