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

树结构思维导图

1.二叉树定义

树结构产生的由来:为了解决数组和链表在修改元素和查找元素的复杂度上做平衡。树是一种半线性结构,经过某种遍历,即可确定某种次序。以平衡二叉搜索树为例,修改与查找的操作复杂度均在O(logn)时间内完成。

树的性质:连通无环图,有唯一的根,每个节点到根的路径唯一。有根有序性。

节点的深度:节点到根部的边的数目。树高为深度最大值。内部节点,叶节点,根部节点。节点有高度,深度,还有度数。

节点高度:对应节点子树的高度,叶节点高度为0.,由该子树某一叶节点的深度确定。节点度数:其孩子总数。

二叉树:应用广泛。每个节点的度数不超过2.有序二叉树树,孩子作为左右区分。

K叉树:每个节点的孩子均不超过K个。

将有序多叉树转化为二叉树:满足条件为同一节点的所有孩子之间满足某一线性次序。转化条件:为每个节点指定两个指针,分别指向其长子和下一兄弟。

应用:编码问题。每一个具体的编码方案都对应于一颗二叉编码树。

例如:原始ASCII文本经过编码器成为二进制流,再经过解码器成为文本信息。每一个文本的基本组成单位都是一个字符,由一个特定字符集构成。编码表表示某一个字符所对应的特定二进制串。关键是确定编码表。根据编码表来解码和编码。

前缀无歧义编码:各字符的编码串互不为前缀,可保证解码无歧义。PFC编码。

二叉编码树:将字符映射到二叉树的叶节点,由叶节点到根部的二进制串。由二叉编码树可构建编码表,可顺利编码。

解码是:从前向后扫描该串,同时在树中移动,直至抵达叶节点,输出字符。再次回到根节点。这一解码过程可在接受过程中实时进行,属于在线算法。

关键问题:如何构造PFC编码树呢?

二,如何构建二叉树

二叉树的基本组成单位:节点

节点成员:节点值,父节点指针,左右孩子节点指针,节点高度。

构造函数:默认构造,初始值构造;

操作接口:节点后代总数,插入左右孩子节点(约定当前节点无左右孩子),取直接后继节点(中序遍历后的次序),子树四种遍历,比较,判等。

在二叉树节点的类的基础上构建二叉树类。

树成员:树规模,根节点指针

树构造函数:默认构造函数,析构函数

树操作接口:规模,判空,树根指针,插入根节点,插入左右孩子或左右子树,删除某节点子树,遍历,比较器,节点高度更新。

高度更新策略:每当有节点加入或离开二叉树,则更新其所有祖先的高度。

在每一节点V处,只需读取其左右孩子的高度并取二者之间的最大值,再计入当前节点本身,就得到了V的新高度。

树的遍历:按照某种约定的次序,对各节点访问依次且一次。

各节点与其孩子之间约定某种局部次序。V,L,R。有VLR,LVR,LRV三种选择。先中后,可知先左后右是必须的,只是V的次序发送变化。最重要的一点就是:找到根->找到左右子树
一直重复这个操作,直到最后一个子节点。
先序遍历的结果是ABDEFC,根据先序得到根节点是A.

中序遍历的结果是DBFEAC,根据中序得到A之前的节点都是左子树,A之后的节点都是右子树

输入:树节点位置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);
            }
}
迭代先序方法二:LVR。访问LV节点,入栈R子树。访问R子树的LV节点循环。批次入栈,然后访问.一般性方法是第一批先入栈并访问,直到叶节点。
               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;
    }
}
将树分为左侧通路和右侧子树结构,
迭代中序方法:LVR。入栈LV节点,访问LV节点。入栈R子树。循环
            trepre(x,visit){
                stack<>s;
                while(true){
                visitFirst(x,visit,s);
                if(s.empty()) break;
                x = S.pop();visit(x);
                x=x->rc;
            }
    }
visitFirst(x.visit,s){
    while(x){
      s.push(x);
      x=x->lc;
    }
}
迭代版后序方法:关键是抓主停止入栈的条件。迭代后序停止入栈的条件为
:左节点为叶节点时,停止入栈。LRV。根节点先入栈最后被访问。LR为一伙。R先入栈,L入栈,若为叶节点,则返回。再访问。每一个节点只出栈一次,只要所有节点均保证入栈一次即可完成。
            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;
    }
}
由以上总结可知,不管是先序,中序还是后序,均可用同一种算法解决。只是先序的第一种算法更加简单。
一般均是while作为尾递归循环,然后是入栈条件,如何入栈顺序,然后就是出栈。访问可放在入栈之前,也可放在出栈之后。

 

树的层次遍历:也即广度优先遍历。节点访问次序为先上后下,先左后右。辅助队列的规模为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;即可。
树的层次遍历,每一层都保存在一个向量中。用两个栈来做中介。在线算法。
void tras2(TreeNode* pRoot,vector<vector<int>>&res,stack<TreeNode*>&s,stack<TreeNode*>&p){
        if(pRoot==nullptr) return ;
        TreeNode*mid=nullptr;
        vector<int>a;
        res.push_back(a);
        int i=0;
        s.push(pRoot);
        while(!s.empty() || !p.empty()){
            while(!p.empty()){
                mid=p.top();
                p.pop();
                res[i].push_back(mid->val);
                if(p.empty()){res.push_back(a);i++;}
                if(mid->right){s.push(mid->right);}
                if(mid->left){s.push(mid->left);}
            }
            while(!s.empty()){
                mid=s.top();
                s.pop();
                res[i].push_back(mid->val);
                if(s.empty()){res.push_back(a);i++;}
                if(mid->left){p.push(mid->left);}
                if(mid->right){p.push(mid->right);}
            }
        }
        res.pop_back();
    }
树的层次遍历,用深度搜索遍历方式:递归的方式。缺点无法反向遍历。需要最后做翻转。
void tras1(TreeNode* pRoot,vector<vector<int>>&res,int i){
        if(pRoot==nullptr) return ;
        if(pRoot->left) {res[i].push_back(pRoot->left->val);}
        if(pRoot->right) {res[i].push_back(pRoot->right->val);}
        tras1(pRoot->left,res,i+1);
        tras1(pRoot->right,res,i+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。

特点:中序遍历单调非降。中序遍历一致的二叉搜索树为等价二叉搜索树。二叉搜索树的前后续遍历符合某种规律性,即在遍历得到的数组中,小于根节点的为左子树,大于根节点为右子树,可根据前后遍历重构树结构。可根据前序或后序判断该是否为二叉搜索树。

class Solution {
public:
    bool VerifySquenceOfBST(vector<int> sequence) {
        if(sequence.empty()) return false;
        int root=sequence[sequence.size()-1];
        sequence.pop_back();
        if(sequence.empty()) return true;
        if(sequence[0]<root){
            int begin=0;
            int end=0;
            bool flag=false;
            for(;begin<sequence.size();begin++){
                if(sequence[begin]>root){
                    end=begin;flag=true;break;
                }
            }
            if(flag){
                for(;end<sequence.size();end++){
                    if(sequence[end]<root){
                        return false;
                    }
                }
            }
            vector<int>A,B;
            for(int i=0;i<sequence.size();i++){
                if( !flag){
                    A.push_back(sequence[i]);
                }else if(flag && i<begin){
                    A.push_back(sequence[i]);
                }else{
                    B.push_back(sequence[i]);
                }
            }
            bool left=VerifySquenceOfBST(A);
            bool right=VerifySquenceOfBST(B);
            if(!flag){return left;}
            else{
                return left && right;
            }
        }else{
            for(int i=0;i<sequence.size();i++){
                if(sequence[i]<root){
                    return false;
                }
            }
            bool right=VerifySquenceOfBST(sequence);
            return right;
        }
           
    }
};

查找算法:减而治之策略,与二分查找类似。返回查找位置,若成功则返回该节点,若失败返回其父亲位置和返回空。

控制查找时间,必须控制二叉搜索树的高度。

插入算法:先查找具体位置,再插入,再更新祖先高度。若有相同节点则失败。取决于树高。

删除算法:分为两种情况,一是只有一个孩子时:将其替换为其孩子也就是其父节点指向其孩子,同时释放该节点,更新祖先高度。

双分支情况: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的高度发生变化。共有4种情况,每一种情况都决定了G,P,V以及4颗子树节点。
                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;

依次类推:对于调整局部的旋转问题,也可按类似方法解决。
ZIG(c,v,p0,p1,p2){
    v->lc=p1;if(p1) p1->parent=v;
    v->rc=p2;if(p2) p2->parent=v;updateHeight(v);
    c->rc=v;v->parent=c;updateHeight(c);
    return c;
}
zag(c,v,p0,p1,p2){
    c->rc=p1;if(p1) p1->parent=c;
    v->rc=p2;if(p2) p2->parent=v;updateHeight(v);
    v->lc=c;c->parent=v;updateHeight(c);
    return v;
}

                                               

二叉树的容器:循键访问元素,关联容器。map<K,T>容器和multimap<K,T> 容器,元素是 pair<const K,T> 类型的对象。键可以是基本类型,也可以是类类型,但是必须可比较。 map 使用 less<K> 对元素排序,一般为平衡二叉搜索树。

构造函数:
类似向量,用列表初始化或者其他的map。

mymap.insert ( std::pair<char,int>('a',100) );成员函数 insert() 会返回一个pair<iterator,bool> 对象。对象的成员 first 是一个迭代器,它要么指向插入元素,要么指向阻止插入的元素。
 mymap['a']="an element";它可以返回一个和键所关联对象的引用。若键不存在,会创建它,并初始化或者赋值。
 std::cout << "mymap['d'] is " << mymap['d'] << '\n';d原本不存在,在该句执行后,有了d并且初始化为0.只针对内置类型。
people.erase(name)。map 的成员函数 erase() 可以移除键和参数匹配的元素,然后返回所移除元素的个数。
it = mymap.find('b');
int c=mymap.count(c);返回一个个数。
支持迭代器操作。
multimap 不支持下标运算符,因为键并不能确定一个唯一元素。

自定义比较函数:如果键是指针的话,就需要使用这种函数。map 容器的比较函数在相等时不能返回 true。
1.键为自定义的类对象。
无非是greater<T>和less<T>,T为键的类型,键可以是自己定义的类,所以需要自己写相应类对象的比较函数。
bool operator>(const Name& name) const
{
    return second > name.second ||(second == name.second && first > name.first);
}
2.键为指针。指针只能指向一个对象,多个指针可以指向一个对象shared_ptr,一个指针只能指向一个对象unique_ptr.
class Key_compare
{
public:
    bool operator () (const std::unique_ptr<string>& p1, const std::unique_ptr <string>& p2) const
    {
        return *p1 < *p2;
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值