数据结构与算法(四)二叉树结构

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;

依次类推:对于调整局部的旋转问题,也可按类似方法解决。

                                               

猜你喜欢

转载自blog.csdn.net/u013070875/article/details/85340113
今日推荐