1.二叉树定义
树结构产生的由来:为了解决数组和链表在修改元素和查找元素的复杂度上做平衡。树是一种半线性结构,经过某种遍历,即可确定某种次序。以平衡二叉搜索树为例,修改与查找的操作复杂度均在O(logn)时间内完成。
树的性质:连通无环图,有唯一的根,每个节点到根的路径唯一。有根有序性。
节点的深度:节点到根部的边的数目。树高为深度最大值。内部节点,叶节点,根部节点。节点有高度,深度,还有度数。
节点高度:对应节点子树的高度,叶节点高度为0.,由该子树某一叶节点的深度确定。节点度数:其孩子总数。
二叉树:应用广泛。每个节点的度数不超过2.有序二叉树树,孩子作为左右区分。
K叉树:每个节点的孩子均不超过K个。
将有序多叉树转化为二叉树:满足条件为同一节点的所有孩子之间满足某一线性次序。转化条件:为每个节点指定两个指针,分别指向其长子和下一兄弟。
应用:编码问题。每一个具体的编码方案都对应于一颗二叉编码树。
例如:原始ASCII文本经过编码器成为二进制流,再经过解码器成为文本信息。每一个文本的基本组成单位都是一个字符,由一个特定字符集构成。编码表表示某一个字符所对应的特定二进制串。关键是确定编码表。根据编码表来解码和编码。
前缀无歧义编码:各字符的编码串互不为前缀,可保证解码无歧义。PFC编码。
二叉编码树:将字符映射到二叉树的叶节点,由叶节点到根部的二进制串。由二叉编码树可构建编码表,可顺利编码。
解码是:从前向后扫描该串,同时在树中移动,直至抵达叶节点,输出字符。再次回到根节点。这一解码过程可在接受过程中实时进行,属于在线算法。
关键问题:如何构造PFC编码树呢?
二,如何构建二叉树
二叉树的基本组成单位:节点
节点成员:节点值,父节点指针,左右孩子节点指针,节点高度。
构造函数:默认构造,初始值构造;
操作接口:节点后代总数,插入左右孩子节点(约定当前节点无左右孩子),取直接后继节点(中序遍历后的次序),子树四种遍历,比较,判等。
在二叉树节点的类的基础上构建二叉树类。
树成员:树规模,根节点指针
树构造函数:默认构造函数,析构函数
树操作接口:规模,判空,树根指针,插入根节点,插入左右孩子或左右子树,删除某节点子树,遍历,比较器,节点高度更新。
高度更新策略:每当有节点加入或离开二叉树,则更新其所有祖先的高度。
在每一节点V处,只需读取其左右孩子的高度并取二者之间的最大值,再计入当前节点本身,就得到了V的新高度。
树的遍历:按照某种约定的次序,对各节点访问依次且一次。
各节点与其孩子之间约定某种局部次序。V,L,R。有VLR,LVR,LRV三种选择。先中后,可知先左后右是必须的,只是V的次序发送变化。
输入:树节点位置X
输出:向量visit,即为遍历后的次序
递归调用:
travaPre(x,visit){
if x 为空,则返回;
visit(x->data);
travaPre(x->lc,visit);
travaPre(x->rc,visit);
}
递归版均为线性时间,但常系数较大。
可观察知右子树为尾递归,左子树为接近于尾递归,且不为线性递归。
一般消除尾递归,可用while循环解决。
迭代版:消除尾递归的一般性方法,即借助辅助栈来解决。
先序递归的访问局部次序为根,左,右。要保证每个节点均会被访问到,且只能访问一次。即要求每个节点均会被入栈,且也会被弹出,且均只有一次。当栈为空,则结束。可简单推导出栈的弹出规律。
迭代先序方法一:简单式方法,根据访问次序,严格尾递归解决。trePre(x,visit){
stack<>s;
if(x) s.push(x);
while(!s.empty()){
x=s.pop();visit(x);
if(x->rc) s.push(x->rc);
if(x->lc) s.push(x->lc);
}
}
迭代先序方法二:批次入栈,然后访问.一般性方法是第一批先入栈并访问,直到叶节点。
trepre(x,visit){
stack<>s;
while(true){
visitFirst(x,visit,s);
if(s.empty()) break;
x = S.pop();
}
}
visitFirst(x.visit,s){
while(x){
visit(x);
if(x->rc) s.push(x->rc);
x=x->lc;
}
}
迭代中序方法:trepre(x,visit){
stack<>s;
while(true){
visitFirst(x,visit,s);
if(s.empty()) break;
x = S.pop();visit(x);
}
}
visitFirst(x.visit,s){
while(x){
s.push(x);
x=x->lc;
}
}
迭代版后序方法:关键是抓主停止入栈的条件。迭代后序停止入栈的条件为
:左节点为叶节点时,停止入栈。trepre(x,visit){
stack<>s;
if(x) s.push(x);
while(true){
if(x->parent!=s.top())
{visitFirst(s.top(),visit,s);}
if(s.empty()) break;
x = S.pop();visit(x);
}
}
visitFirst(x.visit,s){
while(x){
if(x->right) s.push(x->right);
if(x->left) ) s.push(x->left);x=x->left;
if(!x->left && x->right) x=x->right;
if(!x->left && !x->right) break;
}
}
由以上总结可知,不管是先序,中序还是后序,均可用同一种算法解决。只是先序的第一种算法更加简单。
树的层次遍历:也即广度优先遍历。节点访问次序为先上后下,先左后右。辅助队列的规模为n/2,包含满二叉树。算法如下:
迭代式层次遍历:队列来解决。
travel(x,visit){
Queue<> q;
if(x) q.enqueue(x);
while(!q.empty()){
x=q.dequenue();visit(x);
if(x->lc) q.enqueue(x->lc);
if(x->rc) q.enqueue(x->rc);
}
}
按入队的次序将从0起将各节点X编号为r(x).则从0-n都对应于完全二叉树中的某一个节点。将所有节点存入向量结构,各节点的rank即为其编号。即完全二叉树节点以层次遍历所得到的顺序存入向量结构中。即可提高对树的存储和处理效率。那么又如何知道节点之间的关系呢?满足以下规律:
r(L)=r(x)*2+1;即可。
完全二叉树:叶节点只能出现在最底部的两层,且最底层叶节点均处于次底层叶节点的左侧。高度为h的完全二叉树,规模介于2h和2h-1之间。规模为n的完全二叉树,高度为log2N.
满二叉树:所有叶节点均处于最底层。
三 如何构建PFC编码树
ASCII文本---->编码器(编码树,向量实现编码森林)---->解码器(基于树的遍历)---->文本。
根据字符集构造编码树,从而得编码表也就是字典得形式,从而将文本转换为二进制流。
根据编码树得遍历对二进制流解码为字符。
可自底而上地构造PFC编码树。首先,由每一个字符分别构造一颗单节点二叉树,并将其视作一个森林。此后,反复从森林中取出两颗树合二为一。经过n-1次迭代后,初始森林中得n颗树将合并为一颗完整得PFC编码树。接下来,再将PFC编码树转译为编码表。算法如下:
算法总体框架:向量实现PFC森林,其中各元素对应于一颗编码树,其data为相应字符。
1.初始化PFC森林:
创建空森林,对每一个可打印得字符,创建一颗相应得PFC编码树,并
将字符作为根节点插入到PFC编码树中。返回PFC森林。
2.构造完整得PFC编码树:
设置随机数time
while循环字符数-1次:
创建新树S“^”;随机选取森林中的第r1颗树,将其作为S的左子树接入,
然后剔除森林中的r1树,随机选取森林中的r2树,将其作为S的右子树接入。
然后剔除森林中的r2树,合并后的PFC树重新植入森林。
最后,该向量只剩一棵树,并返回。
3.生成PFC编码表:
通过遍历的方式获取从根节点到叶节点的字符串。
如何记录该字符串?用string或者位图。
类似先序遍历的递归模式。局部子结构为VLR。也就是说先序遍历模式可用来获取从根节点到叶节点的每一条路径。
结果,返回字典,记录每一个字符所对应的字符串。
该树的叶节点均为字符树,内部节点和根节点均为字符“^”.
PFC编码树的高度不统一,不平衡的状态表明其并不一定是最优编码树。还可以优化。
平均编码长度也就是叶节点平均深度。最优编码树不唯一但存在。其特点是:真二叉树,叶节点深度之差不超过一。真完全树满足要求。构造方法:创建包含2*n-1个节点的真完全二叉树,并将字符分配给n个叶节点,即可得到一颗最优编码树。
四,如何构建Huffman编码算法
最优编码树的实际应用价值并不大,所以如何衡量平均编码长度?
1.带权平均编码长度 与字符出现概率有关。退出最优带权编码方案。
策略与算法:对于字符出现概率已知的任一字符集A,可采用如下算法构造以下编码树:
HUFFMAN编码算法:
1.对于字符集中的每一个字符,分别建立一颗树,其权重为该字符的频率。
2.从该森林中取出两颗权重最小的树,创建一个新节点,合并它们,其权重取作二者权重之和。依次迭代即可
3.再次强调HUFFMAN编码树只是最优带权编码树中 的一颗。
关键点是如何找到森林中权重最小的两颗树?用遍历法。
首先在计算字符集的频率时就已知其顺序。那么在构造森林时,即可按顺序排列,用向量来做。从小到大。
首先取出两个最小的,移除后,再合并插入原向量,就要查找位置,用二分查找。然后插入。从而更新顺序。
移除和查找,还有插入均花时间。移除O(n),查找o(logn),插入。
用列表来做,查找最小值花时间,插入和删除很快。
五,平衡二叉搜索树
要求对象集合的组成可以高效率的调整,又可以高效率的查找,所以需要有树。查找分为循RANK访问,循关键码访问。数据对象均表示为词条形式。词条拥有两个变量KEY ,VALUE。KEY可以比较。
二叉搜索树。条件:顺序性。任一节点的左子树的所有节点必不大于该节点,其右子树的所有节点必不小于该节点。也就是说:R>=V>=L。
特点:中序遍历单调非降。中序遍历一致的二叉搜索树为等价二叉搜索树。
查找算法:减而治之策略,与二分查找类似。返回查找位置,若成功则返回该节点,若失败返回其父亲位置和返回空。
控制查找时间,必须控制二叉搜索树的高度。
插入算法:先查找具体位置,再插入,再更新祖先高度。若有相同节点则失败。取决于树高。
删除算法:分为两种情况,一是只有一个孩子时:将其替换为其孩子也就是其父节点指向其孩子,同时释放该节点,更新祖先高度。
双分支情况:1.找到该节点后继,交换二者的数据项,将后继节点等效视为待删除的目标节点。转到情况一。总体复杂度也取决于树的高度。
平衡二叉搜索树:采取的平衡为适度平衡,而不是理想平衡。AVL树,伸展树,红黑树,kd-树均属于平衡二叉搜索树。
适度平衡性是通过对树中的每一局部增加某种限制条件形成的。任何二叉搜索树均可等价变换为平衡二叉搜索树,但在最坏情况下可花费O(n)时间。
局部性失衡调整方法:围绕特定点的旋转。
ZIG:两个节点,三个子树,旋转。节点,C,V,子树X,Y,Z,。C为V的左孩子,Z为V的右孩子,X,Y为C的左右孩子。
V的ZIG旋转:V的父节点指向C,C的左右孩子为X,V。V的左右孩子为Y,Z。V的右旋,V成为C的右孩子。
同理:zag:节点C,V。C为V的右孩子。V的父节点指向C,C的左右孩子为V,Z。V的左右孩子为X,Y。V的左旋,V成为C的左孩子。
六,AVL树
定义:平衡因子受限的二叉搜索树,各节点的左右子树高度相差不超过一。插入删除均在O(LOGN)时间内完成。
1.完全二叉搜索树必是AVL树。
经过插入与删除而失衡的搜索树重新恢复平衡的调整算法。
插入节点后失衡的节点为X的祖先且高度不低于X的祖父。
平衡算法:从X节点自低向上找到第一个失衡者。记为G,在X与G的通路上,P为G的孩子,V为P的孩子。
V可能为X,也可能为X的祖先。
最重要的是G,P,V三个节点,找到它们。经过旋转,使得G重新平衡,则整树可恢复平衡。
插入算法:
确认目标节点不存在,返回其父节点。
从父节点出发,找到第一个失衡节点:
若失衡则:
该节点为G,找到节点V,根据G的孩子高的为P,P的孩子高的为V。若等高,优先取V与P同向 者。
1.根据G,P,V的不同情况,而进行不同的旋转。G,P,V的高度发生变化。
2.根据3+4算法使其恢复平衡。
退出。
不失衡:更新该节点高度。
删除与插入算法一样,只是删除算法中只有一个失衡节点。
“3+4算法”:
根据G,P,V三者的顺序不同,所以connect34的参数也不同。
P,V同一方向节点,则P->PARENT=G->PARENT,不同则V->PARENT=G->PARENT;
connect34(a,b,c,T0,T1,T2,T3);
a,b,c代表G,P,V三者的中序遍历顺序。T0,T1,T2,T3代表四颗子树的遍历顺序。
connect34: a->lc=T0;if(T0) T0->parent=a;
a->rc=T1,if(T1) T1->parent=a;updateHeight(a);
c->lc=T2,if(T2) T2->parent=c;
c->rc=T3;if(T3) T3->parent=c;updateheight(c);
b->lc=a;a->parent=b;
b->rc=c;c->parent=b;updateheight(b);
return b;
依次类推:对于调整局部的旋转问题,也可按类似方法解决。