一、先序遍历
- 遍历:按照某种次序访问树中的各节点,每个节点被访问恰好一次
- 递归实现
template <typename T, typename VST>
void traverse(BinNodePosi(T) x, VST& vist){
if(! x) return;
visit(x->data);
traverse(x->lChild, visit);
traverse(x->rChild, visit);
}
template<typename T, typename VST)
void teavPre_I1(BinNodePosi(T) x, VST & visit){
Stack<BinNodePosi(T)> S;
if(x) S.push(x);
while(!S.empty()){
x = S.pop(); visit(x->data);
if(HasRChild(*x)) S.push(x->rChild);
if(HasLchild(*x)) S.push(x->lChild);
}
}
- 思路:首先是自顶向下地依次访问左侧链上的沿途节点,再倒过来,自底而上地依次遍历各个层次上的每一颗右子树
template<typename T,typename VST>
static void visitAlongLeftBranch(
BinNoddePosi(T) x,
VST& vist,
Stack<BinNodePosi(T)> &S)
{
while(x){
visit<x->data);
S.push(x->rChild);
x = x->lChild;
}
}
template<typename T, typename VST>
void travPre_I2(BinNoddePosi(T)> x, VST & visit){
Stack<BinNodePosi(T)> S;
while(true){
visitAlongLeftBranch(x, visit, S);
if(S.empty()) break;
x = S.pop();
}
}
二、中序遍历
template <typename T, typename VST>
void traverse(BinNodePosi(T) x, VST& visit){
if(!x) return;
traverse(x->lChild, visit);
visit(x->data);
traverse(x->rChild, visit);
}
- 迭代实现
- 首先接受访问的应该是左侧链的末端Ld,继而该节点的右子树Td将被遍历,此后控制权才重新地回到该节点在左链上的上层节点,此时Ld及它的右子树Td已经被访问完毕
- 整个访问过程可以分为多个阶段,左侧链有多长就有多个阶段。每个阶段均是先访问根节点,再遍历右子树。一直到最终,访问根节点,再访问全局的右子树。
- 分摊分析
- 递归版本时间复杂度为O(n),尽管它的常系数非常大
- 迭代版本时间复杂度为O(n),且其常系数远胜于递归版本。虽然迭代版本有两层循环,但其内层循环访问之和充其量也不过为n,外层循环访问之和也为n,形象地说,在估计时使用了一个n*n的长条,它们累计不过这个方框的面积,而更准确的说,它们的累计长度也不过是和n在同一个数量级O(n)
template<typename T>
static void goAlongLeftBranch(BinNodePosi(T) x, Stack<BinNodePosi(T)> & S)
{ while(x) { S.push(x); x = x->lChild;}}
template<typename T, typename V>
void travIn_I1(BinNodePosi(T) x, V& visit){
Stack <BinNodePosi(T)> S;
while(true){
goAlongLeftBranch(x, S);
if(S.empty()) break;
x = S.pop();
visit(x->data);
x = x->rChild;
}
}
三、后序遍历
- 递归实现
- 应用:BinNode::size() + BinTree :: updateHeight()
template <typename T, typename VST>
void traverse(BinNodePosi<T> x, VST & visit){
if(!x) return;
traverse(x->lc, visit);
traverse(x->rc, visit);
visit(x->data);
}
- 迭代实现
- 分析(藤缠树):从根出发下行,尽可能沿左分支,实不得已,才沿右分支。最后一个节点,必是叶子,而且,是按中序遍历次序最靠左者,也是递归版中visit()首次执行处,这片叶子将首先接受访问。
template <typename T> static void gotoLeftmostLeaf(Stack<BinNodePosi<T>> & S){
while(BinNodePosi<T> x = S.top())
if(HasLChild(* x)){
if(HasRChild(* x))
S.push(x->rc);
S.push(x->lc);
}else
S.push(x->rc);
S.pop();
}
template <typename T, typename VST>
void travPost_I(BinNodePosi<T> x,VST & visit){
Stack<BinNodePosi<T> > S;
if(x) S.push(x);
while(!S.empty()){
if(S.top() != x->parent)
gotoLeftmostLeaf(S);
x = S.pop();
visit(x->data);
}
}
- 应用:表达式树
- 后序遍历表达式树,得到原来表达式的运算顺序(RPN)
四、层序遍历
template <typename T> template<typename VST>
void BinNode<T>::travLevel(VST & visit){
Queue<BinNodePosi(T)> Q;
Q.enqueue(this);
while(!Q.empty()){
BinNoddePosi(T) x = Q.dequeue();
visit(x->data);
if(HasLChild(*x)) Q.enqueue(x->LChild);
if(HasRChild(*x)) Q.enqueue(x->RChild);
}
}
五、二叉树的重构
- [ 先序 | 后序 ] + 中序
- 之所以必需有中序是因为可能有歧义,左右子树均可能为空,只有右子树为空和只有左子树为空,这两种情况的先序与后序序列均相同。
- [ 先序 + 后序 ] · 真
- 真二叉树,其中每个节点的度数都必需是偶数,除了一度的节点就是二度的节点。
六、哈夫曼编码
- 编码;通过二叉树实现,各字符分别为一个叶子节点,给边赋值0或1,一个路径为一个编码,任何一个叶子不会在另一个叶子的通路上,自然也不会有前缀的情况出现
- 最优编码树:越平衡越好,特完全的,真完全树即是最优编码树。与其说是要平衡,不如说是杜绝那种深度差过大的情况,一旦有这种情况出现,就要通过交换的方法使其尽可能平衡。亦即,叶子只能出现在倒数两层以内。
- 优化:根据已知字符频率优化树
- 最优带权编码树:频率高/低的字符,应尽可能放在高/低处。故此,通过适当交换,可以缩短
- Huffman算法
- 贪心策略:频率低的字符优先引入,位置亦更低。为每个字符创建一颗单节点的树,组成森林F。按照出现的频率,对所有树排序。每次取出频率最小的两棵树,将它们合并成一棵新树。
- 如何获得哈夫曼编码:先/中/后序遍历树,每到一个叶子节点输出一次
- 解码:在树上按照01走,走到叶子就重新开始解下一个编码