5.2 树的遍历、哈夫曼树

一、先序遍历

  • 遍历:按照某种次序访问树中的各节点,每个节点被访问恰好一次
  • 递归实现
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);
}//T(n) = O(1) + T(a) + T(n-a-1) = O(n) 
  • 迭代实现
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>//分摊O(1)
static void visitAlongLeftBranch(
	BinNoddePosi(T) x,
	VST& vist,
	Stack<BinNodePosi(T)> &S)
{
	while(x){//反复的
		visit<x->data);//访问当前节点
		S.push(x->rChild);//右孩子(右子树)入栈(将来逆序出栈)
		x = x->lChild;//沿左侧链下行
	}//只有右孩子,NULL会入栈,但是不会右任何实质性的输出
}

template<typename T, typename VST>
void travPre_I2(BinNoddePosi(T)> x, VST & visit){
	Stack<BinNodePosi(T)> S;//辅助栈
	while(true){//以(右)子树为单位,逐批访问节点
		visitAlongLeftBranch(x, visit, S);//访问子树x的左侧链,右子树入栈缓冲
		if(S.empty()) break;//栈空即退出
		x = S.pop();//弹出下一子树的根
	}//#pop = #push = #visit = O(n) = 分摊O(1)
}

二、中序遍历

  • 递归实现
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);
}//T(n) = T(a) + O(1) + T(n-a-1) = O(n)
  • 迭代实现
    • 首先接受访问的应该是左侧链的末端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();//x的左子树为空,或已遍历(等效为空),故可以
		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);
}//T(n) = O(1) + T(a) + T(n - a - 1) = O(n)
  • 迭代实现
    • 分析(藤缠树):从根出发下行,尽可能沿左分支,实不得已,才沿右分支。最后一个节点,必是叶子,而且,是按中序遍历次序最靠左者,也是递归版中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()){//x始终为当前节点
		if(S.top() != x->parent)//若栈顶非x之父(而为右兄)
			gotoLeftmostLeaf(S);//则在其右兄子树中找到最靠左的叶子(递归深入)
		x = S.pop();//弹出栈顶(即前一节点之后继)以更新x
		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走,走到叶子就重新开始解下一个编码
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值