标签:
本章主要讲解内容为:
二叉树的递归定义:二叉树是每个结点最多含有两棵子树的树结构。
二叉树的递归定义标识着它具有很多递归性质。
二叉树的遍历、查找、构建、删除、复制和计数等全部可以用递归来实现,详见代码。
二叉树的构建方法有:硬编码生成、扩展前缀、前缀结合中缀等。我实现了后两种方法。
二叉树的递归遍历非常简单,参见代码。
主要分析二叉树的非递归遍历,除了书上的版本外,我自己另写了一个版本,称为通用方法,参照了指令及状态机所设计。不同之处:书上的代码仅用一个栈,而通用方法使用两个栈——指令栈和数据栈。
递归遍历方法如下(伪代码):
结合(根-左-右)遍历方式,思考递归方法下前序遍历的调用栈情况(步骤):
由此可以看出调用规律:
综上可以写出算法:
template <class T, class N> void BiTree<T, N>::PreOrderNoRecursion2() { stack<N*> s; N *p = root; while (p != NULL || !s.empty()) { while (p != NULL)//遍历到最左结点,同时记录路径,输出路径(根-左=(根->最左)路径) { cout << p->data; s.push(p); p = p->lchild; } if (!s.empty()) { p = s.top(); s.pop();//访问后弹出最左结点,当前为最左父结点 p = p->rchild;//访问最左父结点的右孩子 } } }
递归遍历方法如下(伪代码):
结合(左-根-右)遍历方式,思考递归方法下中序遍历的调用栈情况(步骤):
由此可以看出调用规律:(其实与前序遍历有相似之处)
综上可以写出算法:(就输出结果的那一行换了位置,总体逻辑是一样的,说明调用栈的变化规律也是一样的)
template <class T, class N> void BiTree<T, N>::InOrderNoRecursion2() { stack<N*> s; N *p = root; while (p != NULL || !s.empty()) { while (p != NULL)//定位到最左结点 { s.push(p); p = p->lchild; } if (!s.empty()) { p = s.top(); cout << p->data;//从最左结点开始访问 s.pop(); p = p->rchild;//访问最左结点(依次)的父结点的右孩子 } } }
后序遍历较前序、中序复杂,调用栈的变化规律不同于前序、中序。究其原因,是在输出之前有两次递归调用,因此,无法通过取栈顶知晓遍历的上一个结点(遍历的直接前驱),故必须以一变量来记录上一次访问的结点。
递归遍历方法如下(伪代码):
结合(左-根-右)遍历方式,思考递归方法下后序遍历的调用栈情况(步骤):
设遍历过程中的前驱(上次遍历结点)为pre,由此可以看出调用规律:
算法的实现需要解决几个问题:
以后序遍历为基础,结合pre这个前驱变量的特征,可以罗列出pre的指向:
要解决问题1,只能通过栈来解决。将要访问的孩子结点索性一次性保存到栈中,由于兄弟结点的遍历顺序是先左再右,故而进栈顺序为先右再左。
那么按照这个方法,处理当前结点时,将其孩子压栈,这是父结点向孩子结点的过渡,是通过栈的,没有借助pre。
因此可以写出基本步骤:
现在,问题简化成:
以上两种情况之间互斥。
因而有:
综上可以写出算法:
template <class T, class N> void BiTree<T, N>::PostOrderNoRecursion2() { stack<N*> s; N *cur = root; //当前结点 N *pre = NULL; //前一次访问的结点 s.push(root); while (!s.empty() && cur) { cur = s.top(); if ((cur->lchild == NULL&&cur->rchild == NULL) || ((pre == cur->lchild || pre == cur->rchild))) { //当前为叶子结点或上一次访问为孩子结点,即按左-右-根(孩子-根)顺序,孩子全部访问过,接着访问父结点 cout << cur->data; s.pop(); pre = cur; } else { //当前为从上到下访问,孩子没访问过,则孩子入栈 if (cur->rchild != NULL) s.push(cur->rchild); if (cur->lchild != NULL) s.push(cur->lchild);//左-右-根,入栈顺序为(根)-右-左 } } }
按指令拆解visit方法:
假设数据栈为s,指令栈为sip
设一变量为ins,代表指令所在行,按正常的运行顺序,应是0->1->2->3->end。
如果在某处需要返回,则只需将指令出栈即可。
接下来,我们就可以在ins#0 #1 #2 #3这四处地方写上相应的处理程序。若无返回或者调用,则当前ins自增。
故算法如下:
template <class T, class N> void BiTree<T, N>::MainOrderNoRecursion(typename BiTree<T, N>::NoRecursionType type) { if (root == NULL) return; //非递归树遍历通用版本,结合状态机指令 stack<N*> s;//结点栈 stack<int> sip;//状态机 s.push(root); sip.push(0); while (!s.empty() || !sip.empty()) { N* bt = s.top();//取结点栈顶 switch (sip.top())//取指令栈顶 { case 0: sip.top()++; if (bt == NULL)//遍历到NULL,出栈 { s.pop(); sip.pop(); continue; } case 1: sip.top()++; if (type == PREORDER) cout << bt->data; if (bt->lchild != NULL) { s.push(bt->lchild); sip.push(0); continue; } case 2: sip.top()++; if (type == INORDER) cout << bt->data; if (bt->rchild != NULL) { s.push(bt->rchild); sip.push(0); continue; } case 3: if (type == POSTORDER) cout << bt->data; s.pop(); sip.pop(); continue; } throw "非法IP!"; } }
所谓扩展前缀,顾名思义,必须是前缀编码,扩展就是以“#”代替空结点。
如常见的算术表达式:3+4*5,扩展前缀就是+3##*4##5##。其中?##代表叶子结点。
扩展前缀构建也采用递归调用方式。
将前缀看作[Head] [Left] [Right]三个部分,返回一棵树。[Head]只有一个元素,直接取出来,作为父结点。那两个子结点就从[Left] [Right]这两个前缀中生成,这即是递归调用。
template <class T, class N> N *BiTree<T, N>::CreateByPre(int& ipre) { if (ipre >= (int)pre.size()) throw "输入串错误"; T e = pre[ipre++]; if (e == ‘\0‘) return NULL; if (e == ‘#‘) return NULL; N *bt = New(); bt->data = e; bt->lchild = CreateByPre(ipre); // 建左子树 bt->rchild = CreateByPre(ipre); // 建右子树 return bt; }
把扩展前缀的“#”规则拿掉,那普通的前缀字串就无法生成一棵唯一的树了,究其原因,是无法知晓递归调用的出口,而“#”恰恰是递归的出口。
知道一棵树的前缀和中缀,就能够还原这棵树,条件是树的结点值不能有重复,也就是说,前缀和中缀能够完全确定一棵树,如何证明?
假设前缀为pre,中缀为in,pre和in的长度是n。将其作划分:
现在,假设in中Head的下标为k,则Head-Left中缀的范围就是[0,k-1],Head-Right中缀的范围就是[k+1,n-1]。
这样,经过一轮划分,生成一棵不完全中缀树——父结点为Head,孩子为Head-Left和Head-Right,且此树是唯一的。
接下来,按照同样方法,只不过这次划分的对象是Head-Left和Head-Right,重复直到Head-Left或Head-Right长度为1(即叶子结点)。
现在思想为什么树的结点值不能重复,关键在于在in中寻找Head——如果Head有多个,就不能保证找到了正确的Head。
举例:
第一次划分后:
第二次划分后:
有优先级(括号)的中缀可以确定一棵二叉树,因此,该方法有效,一般的前缀和中缀可以还原二叉树。
template <class T, class N> N* BiTree<T, N>::CreateByPreMid(int ipre, int imid, int n) { if (n == 0) return NULL; N *p = New(); p->data = pre[ipre];// 前缀为根-左-右 int i; for (i = 0; i < n; i++) // 在中序序列中定位根结点 { if (pre[ipre] == mid[imid + i]) break; } if (i == n) throw "前缀和中缀字符不匹配!"; p->lchild = CreateByPreMid(ipre + 1, imid, i);// 建左子树 p->rchild = CreateByPreMid(ipre + i + 1, imid + i + 1, n - i - 1);// 建右子树 return p; }
二叉树是计算机数据结构当中的核心内容,它本身有着优美的递归性质。
树结构在查找方面有平衡二叉树AVL、红黑树RBT等,在数据压缩方面有哈夫曼树等,在图形学领域有四叉树、八叉树等等,因而,掌握好树结构对于学习计算机算法而言是不可或缺的。
标签:
原文地址:http://www.cnblogs.com/bajdcc/p/4779175.html