• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 众包
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
IcoveJ
博客园    首页    新随笔    联系   管理    订阅  订阅

二叉树(一)

二叉树--(邓书学习)

在前面我们学到了向量、列表、栈、队列等线性的数据结构,但它们或多或少都有时间或者空间上的妥协,比如数组,我们可以通过下标或秩的方式以常数时间找到目标对象,并进行读取其内容,但要进行删除或者插入时,都需要耗费线性的时间;而链表虽然允许我们借助引用或位置对象,在常数时间内实现删除或者插入,但为了找到特定的目标元素,不得不花费线性的时间对整个结构进行遍历查找。

于是引入二叉树.

1 二叉树及其子部分的概念

1.1 树

树的结构是非线性结构,它的元素之间并不存在天然的直接后继或直接前驱。但加上一些限制条件后(比如约束),就能够在树结构的元素之间确定某种线性次序,也可称树为半线性结构。

那什么是树呢?数由一组顶点(vertex/node)以及连接与其间的若干条边(edge)组成。

1.1.1 深度

我们将沿每个节点v到根r的唯一通路所经过边的数目称作v的深度。

对于根节点,约定其深度depth(r) = 0,故其属于第0层。

1.1.2 祖先(父代)、后代、子树

沿任一节点v到根结点的路径上,经过的所有节点都可以称为v的祖先,相邻的那个祖先称其为v的父代。特别的,v的祖先包括了树根以及v本身,其中除v外的其余祖先都可称之为真祖先;对称的讲,v的后代也包括了其本身,其余的后代都称之为真后代。

而子树,对于任一节点v的所有后代及其之间的连边称作子树。

1.1.3 叶子与高度

对于任一树,都会存在无任何孩子的节点,就将这种节点称作叶节点(leaf)*或*叶子。

对于一棵树T,其所有节点深度的最大值称作T的高度(height)。就会发现,树的高度必定总是由其中某一叶节点的深度确定的。对于只含单个节点的树而言,其高度一般定为1,空树的高度也就定为0(有一些会分别定为0和-1)。

不难理解,树的高度就是根节点的高度,即height(T) = height(r)。

2 二叉树的实现

什么是二叉树?我们将每个节点的孩子均被限制不超过两个的树称作二叉数。

对应的,将各节点的孩子(或分支)数目即不确定也没有上限的树称作多叉树。

在实际应用中,我们常常将多叉树转变为二叉树来实现 ---- 采用”长子+兄弟“的策略,比如:

一个多叉树,根节点R有三个子节点,分别为A、B、C,A有两个子节点D、E,C有一个子节点F,F有三个子节点G、H、K,转换成二叉树就是:根节点R的子节点为A,A的左子节点为原A的长子D,右子节点为兄弟B;D没有左子节点,只有右子节点,原D的兄弟E;B也只有右子节点,原B的兄弟C;C只有左子节点,原C的子节点F;F只有左子节点。原F的子节点G;G只有右子节点,原G的兄弟H;H也只有右子节点,原H的兄弟K。

从这个例子可以明白,长子都是作为左子节点,最近的那个兄弟作为右子节点使用。

2.1 二叉树节点

#define BinNodePosi(T) BinNode<T>*//节点位置
#define stature(p) ((p) ? (p)->height : -1)//节点高度
typedef enum{RB_RED, RB_BLACK} RBColor;//节点颜色,红黑树

template <typename T>
class BinNode{//二叉树节点模板类
    public:
    T data;//数值
    BinNodePosi(T) parent;//父节点
    BinNodePosi(T) LC;//左孩子
    BinNodePosi(T) RC;//右孩子
    int height;//高度
    int npl;//Null Path Length(左式堆)
    RBColor color;//颜色,红黑树
    
    //构造函数
    BinNode() : parent(NULL), lChild(NULL), rChild(NULL), height(0), npl(1), color(RB_RED) { }//默认构造器
    BinNode(T e, BinNodePosi(T) p = NULL, BinNodePosi(T) lc = NULL, BinNodePosi(T) rc = NULL, 
            int h = 0, int l = 1, RBColor c = RB_RED)
      : data(e), parent(p), lChild(lc), rChild(rc), height(h), npl(l), color(c) { }//构造器
    
    //操作接口
    int size();//统计当前节点后代的总数,也就是以其为根节点的子树的规模
    BinNodePosi(T) insertAsLC(T const&);//作为当前节点的左孩子插入新节点
    BinNodePosi(T) insertAsRC(T const&);//作为当前节点的有孩子插入新节点
    BinNodePosi(T) succ();//取当前节点的直接后继
    template <typename VST> void travLevel(VST&);//子树层次遍历
    template <typename VST> void travPre(VST&);//子树先序遍历
    template <typename VST> void travIn(VST&);//子树中序遍历
    template <typename VST> void travPost(VST&);//子树后序遍历
    
    //五种比较器
    bool operator<(BinNode const& bn) { return data < bn.data; }//小于
    bool operator>(BinNode const& bn) { return data > bn.data; }//大于
    bool operator<=(BinNode const& bn) { return data <= bn.data; }//大于等于
    bool operator=>(BinNode const& bn) { return data => bn.data; }//小于等于
    bool operator==(BinNode const& bn) { return data == bn.data; }//等于
}

BinNode节点由多个成员变量组成,它们记录了当前节点的父节点与子节点的位置、节点内存放的数据、节点的高度等,这些都是二叉树相关算法赖以实现的基础。

这里引入节点颜色的原因是有些种类的二叉树需要用到颜色来描述节点状态,例如红黑树。

对于上述代码中常用的内容或者使用频繁的检查、判断二叉树节点的状态与性质、定位与之相关的(“兄弟”、”叔叔“)特定节点等功能,可以直接以宏的形式进行重述:

/*BinNode状态与性质的判断*/
#define IsRoot(x) (!((x).parent))
#define IsLC (!IsRoot(x) && (&(x) == (x).parent->LC))
#define IsRC (!IsRoot(x) && (&(x) == (x).parent->RC))
#define HasParent(x) (!IsRoot(x))
#define HasLC(x) ((x).LC)
#define HasRC(x) ((x).RD)
#define HasChild(x) (HasLC(x) || HasRC(x))//至少拥有一个孩子
#define HasBothChild(x) (HasLC(x) && HasRC(x))//同时拥有两个孩子
#define IsLeaf(x) (!HasChild(x))

/*与BinNode具有特定关系的节点与指针*/
#define Silbing(p) (\
IsChild(*(p))?\
    (p)->parent->RC:\
    (p)->parent->LC\
)//兄弟

#define uncle(x)(\
IsChild(*((x)->parent))?\
    (x)->parent->parent->RC:\
    (x)->parent->parent->LC:\
)//叔叔

#define FromParentTo(x)(\
    IsRoot(x) ? _root:(\
    IsCL(x) ? (x).parent->LC : (x).parent->RC\
)\
)//来自父亲的指针

其中,一些具体函数的实现如下:

template <typename T>
BinNodePosi(T) BinNode<T>::insertAsLC(T const& e)//将e作为当前节点的左孩子插入二叉树
{ return LC = new BinNode<T>(e,this);}

template <typename T>
BinNodePosi(T) BinNode<T>::insertAsRC(T const& e)//将e作为当前节点的右孩子插入二叉树
{ return RC = new BinNode<T>(e,this);}

实现插入时必须指明是以左孩子还是右孩子的身份插入。

对二叉树实现遍历,一共有四种方法:先序、中序、后序和层次,其可以通过迭代和递归实现。

template <typename T>
template <typename VST>
void BinNode<T>:;travIn(VSt& visit){
    switch(rand()%5){
        case 1: travIn_C1(this, visit); break;//先序
        case 2: travIn_C2(this, visit); break;//中序
        case 3: travIn_C3(this, visit); break;//后序
        case 4: travIn_C4(this, visit); break;//层次
        default: travIn_R(this, visit); break;//
    }
}//遍历的实现会在后面给出

2.2 二叉树

主要就是实现左右子节点的插入、左右子树的接入以及删除和分离。

BinTree类是在BinNode类的基础上定义实现的:

#include "BinNode.h"
template <typename T>
class BinTree{
    protected:
    int size;//规模
    BinNodePosi(T) root;//根节点
    virtual int updateHeight(BinNodePosi(T) x);//更新节点高度
    void updateHeightAbove(BinNodePosi(T) x);//更新节点x及其祖先的高度
    public:
    BinTree(): size(0), root(NULL) {}//构造函数
    ~BinTree() { if(0 < size) remove(root); }//析构函数,删除以根节点的树
    
    int& size() { return size; }//规模
    bool empty() const { return !root; }//判空
    BinNodePosi(T) root() const { return root; }//树根
    BinNodePosi(T) insertAsRoot(T const& e);//插入根节点
    BinNodePosi(T) insertAsLC(BinNodePosi(T) x, T const& e);//e作为x的左孩子插入(原节点无左孩子)
    BinNodePosi(T) insertAsRC(BinNodePosi(T) x, T const& e);//e作为x的右孩子插入(原节点无右孩子)
    BinNodePosi(T) attachAsLC(BinNodePosi(T) x, BinTree<T>* &T);//T作为x左子树接入
    BinNodePosi(T) attachAsRC(BinNodePosi(T) x, BinTree<T>* &T);//T作为x右子树接入
    int remove(BinNodePosi(T) x);//删除以该节点为根节点的子树,返回该子树的原规模
    BinTree<T>*  secede(BinNodePosi(T) x);//将子树x从当前树中摘除,并将其转换为一颗独立的树
    
    template <typename VST>
    void travLevel(VST& visit) { if(root) root->travLevel(visit); }//层次遍历
    template <typename VST>
    void travPre(VST& visit) { if(root) root->travPre(visit); }//先序遍历
    template <typename VST>
    void travIn(VST& visit) { if(root) root->travIn(visit); }//中序遍历
    template <typename VST>
    void travPost(VST& visit) { if(root) root->travPost(visit); }//后序遍历
    
    //比较器
    bool operator<(BinTree<T> const& t) { return _root && t._root && (_root < t._root); }
    bool operator>(BinTree<T> const& t) { return _root && t._root && (_root > t._root); }
    bool operator==(BinTree<T> const& t) { return _root && t._root && (_root == t._root); }
    bool operator<=(BinTree<T> const& t) { return _root && t._root && (_root <= t._root); }
    bool operator>=(BinTree<T> const& t) { return _root && t._root && (_root >= t._root); }
    
}

2.2.1 高度更新

我们知道有一个很特殊的节点---根节点,它的高度是-1;而其他节点的高度,根据定义,不难发现都是左右子节点的最大高度值再加1.所以通过宏定义一个等价的高度函数来表明特殊请况:

#define stature(p) ((p) ? (p)->height : -1)//在BinNodePosi类中

template <typename T>
int BinTree<T>::updateHeight(BinNodePosi(T) x)
{ return x->height = 1 + max(stature(x->LC), stature(x->RC)); }

template <typename T>
void BinTree<T>::updateHeightAbove(BinNodePosi(T) x)
{ while(x){ updateHeight(x); x = x->parent; } }

在BinTree类中,我们将updateHeight()方法定义为虚函数,这是因为不同的树对高度的定义不同,定义为虚,方便在派生类中进行重写(override)

2.2.2 节点的插入

节点的插入一共分为三种:根节点、左子节点、右子节点

/*二叉树根节点的插入:前提是二叉树当前为空*/
template <typename T>
BinNodePosi(T) BinTree<T>::insertAsRoot(T const& e)
{ szie = 1; retur root = new BinNode<T>(e); }

/*二叉树左子节点的插入:前提是x节点处的左孩子是空*/
template <typename T>
BinNodePosi(T) BinTree<T>::insertAsLC(T const& e)
{ size++; x->insertAsLC(e); updateHeightAbove(x); return x->LC;}

/*二叉树右子节点的插入:前提是x处节点的右孩子是空*/
template <typename T>
BinNodePosi(T) BinTree<T>::insertAsRC(T const& e)
{ size++; x->insertAsRC(e); updateHeightAbove(x); return x->RC;}

当插入的是左/右子节点时,插入完毕后必须随时更新的高度。

2.2.3 子树插入

子树的插入也有两种:左侧和右侧

/*将T作为节点x的左子树接入,前提是位置x处节点的左孩子当前为空*/
template <typename T>
BinNodePosi(T) BinTree<T>::attachAsLC(BinNodePosi(T) x, BinTree<T>* &T){
   x->CL = T->root; if(HasChild(*x)) x->LC->parent = x;//接入
    size += T->size; updateHeight(x);//更新接入后的树的规模与x的高度
    T->root = NULL; T->size = 0; release(T); T = NULL;//释放原树
    return x;
}

/*将T作为节点x的右子树接入,前提是位置x处节点的右孩子当前为空*/
template <typename T>
BinNodePosi(T) BinTree<T>::attachAsRC(BinNodePosi(T) x, BinTree<T>* &T){
   x->CL = T->root; if(HasChild(*x)) x->RC->parent = x;//接入
    size += T->size; updateHeight(x);//更新接入后的树的规模与x的高度
    T->root = NULL; T->size = 0; release(T); T = NULL;//释放原树
    return x;
}

2.2.4 子树的删除

子树的删除过程与子树的接入过程恰恰相反,还需要注意释放节点空间归还给系统

/*删除位置x处的节点及其后代并返回被删除节点的数值*/
template <typename T>
int BinTree<T>::remove(BinNodePosi(T) x){
    FromParentTo(*x) = NULL;//切断来自父节点的指针
    updateHeightAbove(x->parent);//更新高度
    int n = removeAt(x); xize -= n;//使用递归删除节点x及其后代,并更新规模
    return n;//返回被删除节点总数
}

template <typename T>
static int removeAt(BinNodePosi(T) x){
    if(!x) return 0;
    int n = 1 + removeAt(x->LC) + remove(x->RC);//递归释放左右子树
    release(x->data); release(x);
    return n;
}

2.2.5 遍历

二叉树的遍历一共有四种:前序、中序、后序和层次

将会在会面进行阐述。

3 二叉树的应用实例-PFC编码树

通讯编码算法的实现是二叉树的一个应用实例。每一个具体的编码方案都对应一颗二叉编码树。以字符集∑ = {‘A’,‘E’,‘G’,‘M’,‘S’}为例。

字符 A E G M S
编码 00 01 10 110 111

设待编码的文本为MESSAGE,那么对应的二进制编码串就为11001111111001001

当M的编码变更为11时,二进制的编码串就会变为1101111111001001

我们在解编这个串的过程中就会发现会解出两种文本():

二进制编码 当前匹配字符 解出原文
1101111111001001 M M
01111111001001 E ME
111111001001 S MES
111001001 S MESS
001001 A MESSA
1001 G MESSAG
01 E MESSAGE
二进制编码 当前匹配字符 解出原文
1101111111001001 M M
01111111001001 E ME
111111001001 M MEM
1111001001 M MEMM
11001001 M MEMMM
001001 A MEMMMA
1001 G MEMMMAG
01 E MEMMMAGE

出现这个根源就在于我们变更了编码表,所以说编码表的制定很重要。于是人们就提出了前缀无歧义编码,即PFC编码。而PFC编码使用二叉树结构实现。

从根节点出发,每次向左(右)都对应一个0(1)比特位

graph TB; 1((根节点))-->2((0)) 1((根节点))-->3((1)) 2((0))-->4((A/00)) 2((0))-->5((E/01)) 3((1))-->6((G/10)) 3((1))-->7((1)) 7((1))-->8((M/110)) 7((1))-->9((S/111))

如果我们自上而下的来构造这个棵树,就会发现构造过程很麻烦。于是我们采用自下而上的策略,根据字母的编码逆推,再构成并集,就相当以”小树“于构成了”森林“。

int main(int argc, char* argv[]){
    PFCForest* forest = initForest();//初始化PFC森林
    PFCTree* tree = generateTree(forest); release(forest);//生成PFC编码树
    PFCTable* table = generateTable(tree);//将PFC编码树转换为编码表
    for(itn i = 1; i < argc; i++){
        Bitmap codeString;//二进制编码串
        int n = encode(table, codeString, argv[i]);//根据编码表来生成
        decode(tree, codeString, n);//利用编码树,对长度为n的二进制编码串解码
    }
    release(table); releasr(tree); return 0;//释放空间
}

3.1 PFC编码所需数据结构

#include "../BinTree/BinTree.h"//用BinTree实现PFC树
typedef BinTree<char> PFCTree;//PFC树

#include "../Vector/Vector.h"//用Vector实现PFC森林
typedef Vector<PFCTree*> PFCForest;//PFC森林

#include "../Bitmap/Bitmap,h"//使用位图表示二进制编码
#inlucde "../Skiplist/Skiplist.h"//使用跳转表表示编码表
typedef Skiplist<char, char*> PFCTable;//PFC编码表

#define N_CHAR (0x80 - 0x20)

3.2 PFC森林的初始化

PFCForest* iniForest(){
    PFCForest* forest = new PFCForest;//首先创建空森林
    for(int i = 0; i < N_CHAR; i++){
        forest->insert(i, new PFCTree());
        (*forest)[i]->insertAsRoot(0x20 + i);//创建一棵对应的PFC编码树,初始时其中只包含对应的一个节点(叶或根)
    }
    return forest;//返回包含N_CHAR棵树的森林,其中每棵树只包含一个字符
}

3.3 构造PFC编码树

PFCTree* generateTree(PFCForest* forest){
    srand((unsigned int)time(NULL));//随机取数合并,就需要先设置随机种子
    while(1 < forest->size()){//共做|forest|-1次合并
        PFCTree* s = new PFCTree; s->insertAsRoot('^');//创建根树,根标记为“^”
        int r1 = rand() % forest->size();//随机选取r1
        s->attachAsLC(s->root(), (*forest)[r1]);//作为左子树接入
        forest->remove(r1);//剔除原树r1
        PFCTree* s = new PFCTree; s->insertAsRoot('^');//创建根树,根标记为“^”
        int r2 = rand() % forest->size();//随机选取r2
        s->attachAsRC(s->root(), (*forest)[r2]);//作为右子树接入
        forest->remove(r2);//剔除原树r2
        forest->insert(forest->size(), s);//合并后的树重新植入FPC编码树
    }
    return (*forest)[0];//至此只存在一棵树,即全局PFC编码树
}

3.4 生成PFC编码表

void generateCT(Bitmap* code, int length, PFCTable* table, BinNodePosi(char) v){
    if(IsLeaf(*v))//若是叶节点
    { table->put(v->data, code->bits2srting(length)); return; }
    if(HasLC(*v))//Left=0
    { code->clear(length); generateCT(code, length+1, table, v->LC); }
    if(HasRC(*v))
    { code->clear(length); generateCT(code, length+1, table, v->RC); }
}

PFCTable* generateTable(PFCTree* tree){
    PFCTable* table = new PFCTable;//创建以Skiplist实现的编码表
    Bitmap* code = new Bitmap;//用于记录RPS的为图
    generateCT(code, 0, table, tree->root());//遍历以获取各字符(叶节点)的RPS
    release(code); return table;//释放编码位图,返回编码表
}

3.5 编码

int encode(PCFTable* table, Bitmap& codeString, char* s){
    int n = 0;
    for(unsigned int i = 0, i < strlen(s); i++){
        cnar** pCharCode = table->get(s[i]);//取出其对应的编码串
        if(!pCharCode) pCharCode = table->get(s[i] + 'A' - 'a');//小写转为大写
        if(!pCharCode) pCharCode = table->get(' ');//无法识别的字符统一视作空格
        printf("%s", *pCharCode);//输出当前字符的编码
        for(unsigned int j = 0; j < strlen(*pCharCode); j++)
            '1' == *(*pCharCode + j) ? codeString.set(n++) : codeString.clear(n++);
    }
    return ;
}

3.6 解码

void decode(PFCTree* tree, Bitmap& code, int n){
    BinNodePosi(char) x = tree->root();
    for(int i = 0; i < n; i++){
        x = code.test(i) ? x->RC : x->LC;
        if(IsLeaf(*x)){ printf("%c", x->data); x = tree->root(); }
    }
}

4 四种遍历的实现

4.1 前序遍历---PreOrder

4.1.1 递归版

前序又称先序,输入/出顺序是
$$
根节点\rightarrow左子节点\rightarrow右子节点
$$
我们通过递归的简洁性来实现前序遍历算法

//邓公代码
template <typename T>
void travPre_R(BinNodePosi(T) node, VST& visit){
    visit(node->data);
    if (HasLC(*node)) travPre_R<T, VST>(node->LC, visit);
    if (HasRC(*node)) travPre_R<T, VST>(node->RC, visit);
}
//自己的代码(二叉树(二)文件)
void BiTree::PreOrder(BiNode * t) {
	if (t != NULL) {//将每一个子节点都视作所在子树的根节点
		cout << t->data << ' ';
		PreOrder(t->LChild);//转至左子树迭代
		PreOrder(t->RChild);//转至右子树迭代
	}
}

遍历也同创建是一样的,前序遍历时总是先沿一边进行遍历,比如:

graph TB 1((a))-->2((b)) 1((a))-->3((c)) 2((b))-->4((e)) 2((b))-->5((f)) 3((c))-->6((g)) 3((c))-->7((h)) 4((e))-->8((i)) 4((e))-->9((j)) 5((f))-->10((k)) 5((f))-->11((l)) 6((g))-->12((m)) 6((g))-->13((n)) 7((h))-->14((o)) 7((h))-->15((p))

最后前序遍历的结果是:
$$
a\rightarrow b\rightarrow e\rightarrow i\rightarrow j\rightarrow f\rightarrow k\rightarrow l\rightarrow c\rightarrow g\rightarrow m\rightarrow n\rightarrow h\rightarrow o\rightarrow p
$$

4.1.2 迭代版①

但要注意,这个是递归的最后的输出顺序,当使用迭代法的时候,因为一定会使用栈,第一个入栈以及出栈的必定是根节点,当根节点已经出栈后,对于剩下的子节点,根据栈“后进先出”的原理,以及前序遍历的左孩子先输出的要求,右孩子应先于左孩子入栈。例如:

graph TB 1((a))-->2((b)) 1((a))-->3((c)) 3((c))-->4((d)) 3((c))-->5((f)) 4((d))-->6((#)) 4((d))-->7((e))

(#代表空节点)其迭代顺序就是:

建立好栈后,根节点a入栈,a出栈;c入栈,b入栈,b出栈,c出栈;f入栈,d入栈,d出栈;

注意,在这个例子中,由于此时d作为c的子树,d还有右子树,所以f不会出栈,e会入栈,e再出栈,f最后出栈。

最后的结果就是:
$$
a\rightarrow b\rightarrow c\rightarrow d\rightarrow e\rightarrow f
$$
所以基于以上的思想,我们在设计前序遍历的迭代算法时,我一定是先向栈中push右子节点。因此,前序遍历的迭代实现是:

void BiTree::preOrder_2() {
	BiNode *p;
	p = root;
	stack<BiNode*> my_stack;
	if (p) my_stack.push(p);
	while (!my_stack.empty()) {
		p = my_stack.top();
		my_stack.pop();
		cout << p->data << ' ';
		if (p->RChild != NULL)
			my_stack.push(p->RChild);
		if (p->LChild != NULL)
			my_stack.push(p->LChild);
	}
	cout << endl;
}

4.1.3 迭代版②

仔细的同学就会发现我们在迭代①中也是采用了递归的思想的:最后两个if()结构,它像极了递归,那么能不能彻底摆脱递归的思想呢?是有的。

我们知道前序遍历的实质永远是从根节点出发,到左节点,再到右节点。那么为什么不直接先沿左侧通路自顶而下访问沿途节点,再自底而上依次遍历这些节点的右子树。

graph TB 1((a))-->2((b)) 1((a))-->3((c)) 3((c))-->4((d)) 3((c))-->5((f)) 4((d))-->6((#)) 4((d))-->7((e))

还是这棵树,直接左侧通路遍历:a->b;再转右子树,遍历右子树的左子树:c->d->#,再自底而上:e->f,综合在一起:
$$
a\rightarrow b\rightarrow c\rightarrow d\rightarrow e\rightarrow f
$$
基于此,设计出新的迭代算法:

void BiTree::preOrder_3() {
	BiNode *p;
	p = root;
	stack<BiNode*> my_stack;
	while (true) {
		while (p)
		{
			cout << p->data << ' ';
			my_stack.push(p->RChild);//右子树节点入栈暂时存储
			p = p->LChild;//沿左分支深入一层
		}//从当前节点出发,逐批访问
		if (my_stack.empty()) break;
		p = my_stack.top();
		my_stack.pop();
	}
	cout << endl;
}

4.2 中序遍历---InOrder

4.2.1 递归版

中序遍历的输入/输出顺序是:
$$
左子节点\rightarrow根节点\rightarrow右子节点
$$
例如:

graph TB 1((a))-->2((b)) 1((a))-->3((c)) 2((b))-->4((e)) 2((b))-->5((f)) 3((c))-->6((g)) 3((c))-->7((h)) 4((e))-->8((#)) 4((e))-->9((j)) 5((f))-->10((k)) 5((f))-->11((#)) 6((g))-->12((#)) 6((g))-->13((#)) 7((h))-->14((o)) 7((h))-->15((p))

"#"代表空节点,中序遍历的顺序就是:
$$
e\rightarrow j\rightarrow b\rightarrow k\rightarrow f\rightarrow a\rightarrow g\rightarrow c\rightarrow o\rightarrow h\rightarrow p
$$
代码如下:

//邓公代码
template <typename T>
void travPost_R(BinNodePosi(T) node, VST& visit){
    if(HasLC(*node)) travPost_R<T, VST>(node->LC, visit);
    if(HasRC(*node)) travPost_R<T, VST>(node->RC, visit);
}
//自己的代码(二叉树(二)文件)
void BiTree::InOrder(BiNode * t) {
	if (t != NULL) {
		InOrder(t->LChild);
		cout << t->data << ' ';
		InOrder(t->RChild);
	}
}

4.2.2 迭代版

中序和后序遍历的迭代其实也可以如前序一样分为两种:类递归和全新的迭代。但在我们分析的过程中就会发现,中序和后序的递归并不是完全的尾递归,中序的左子树绝对不是,后序的左右子树也都不是,所以若用类递归的方式,就会发现这三棵子树很困难,所以就直接采用第二种迭代的思路进行算法设计。

中序的本质也是从左子节点出发,到根节点,再到右子节点。于是我们就选择顺着左侧通路,自底而上依次访问沿途节点及其右子树节点

还是拿上述二叉数为例(#为空节点):

graph TB 1((a))-->2((b)) 1((a))-->3((c)) 2((b))-->4((e)) 2((b))-->5((f)) 3((c))-->6((g)) 3((c))-->7((h)) 4((e))-->8((#)) 4((e))-->9((j)) 5((f))-->10((k)) 5((f))-->11((#)) 6((g))-->12((#)) 6((g))-->13((#)) 7((h))-->14((o)) 7((h))-->15((p))

采用新思路就是:
$$
e\rightarrow i\rightarrow b\rightarrow k\rightarrow f\rightarrow a\rightarrow g\rightarrow c\rightarrow o\rightarrow h\rightarrow p
$$
与递归的结果是一样的,于是就有了新算法:

void BiTree::inOrder_2() {
	BiNode *p;
	p = root;
	stack<BiNode*> my_stack;
	while (true) {
		while (p) {
			my_stack.push(p);//当前节点入栈暂时存储
			p = p->LChild;
		}
		if (my_stack.empty()) break;
		p = my_stack.top();
		my_stack.pop();
		cout << p->data << ' ';
		p = p->RChild;
	}
	cout << endl;
}

我们以”AB##C##“高度为2的二叉树来分析算法:

p是根节点,所以p->data = A,进入while(p)的循环,并将A压入栈中,再将p->data = B,再进入循环,将B压入栈中,B的左子节点赋给p,再进入循环,此时的p是空的,所以直接跳出循环,进行栈判空。取出栈顶元素B,再将B的右节点赋给p。------得到B

进入循环,B的右子节点为空,所以直接对栈进行判空,输出栈顶元素A,并将A的右子节点赋给p。------得到A

此时的p->data = C,进入循环将C压入栈中,并将C的左子节点赋给p,p为空,下次循环直接跳出,判空后,取出C。------得到C

此时是将C的右子节点赋给p,其为空,直接跳出,栈中也就没有元素,判空后直接break。

最终就是B->A->C。

4.2.3 后继节点

在中序遍历中,有一个特殊的定义——后继节点,它的重要性会在二叉搜索树中体现出来。

什么是后继节点呢?就是在二叉树的中序遍历的序列中,当前节点的紧随节点。

例如在刚刚的例子“AB##C##”中,A就是B的后继节点,C是A的后继,而C的后继为NULL

怎么找当前节点的后继节点在树上的位置?分为两种情况(设当前节点为node):

①node有右子树,那么其后继节点就是右子树上最左的节点;

②node没有右子树,如果node是它祖先的左孩子,那么其祖先就是它的后继;如果node是其祖先的右孩子,那么就向上寻找,直到找到其父代是某一祖先的左子节点,这个祖先就是node的直接后继。

至此,我们就会发现,对于第②种情况,其后继节点一定是它的某一祖先,且是将当前节点纳入其左子树的最低节点。

void BiTree::inOrder_3() {
    BiNode *p;
    p = root;
    bool backtrack = false;
    while(true)
        if(!backtrack && p->LChild)
            p = p->LChild;
        else{
            cout << p->data << ' ';
            if(p->RChild){
                p = p->RChild;
                backtrack = false;
            }
            else{
                if(!succ(p)) break;
                backtrack = true;
            }
        }
}
int BiTree::succ(BiNode *p){
    if(p->RChild){
        p = p->RChild;
        while(p->LChild) p = p->LChild;
    }eles{
        while(p->RChild) p = p->parent;
        p = p->parent;
    }
}

这种方法就不会是用到栈或队列,它用到了回溯的方法。

通过将辅助栈替换成一个布尔型变量backstack。每到一个节点,都借助这个变量判断此前是否刚刚做过一次自下而上的回溯。没有的话,根据中序遍历的本质,就优先遍历左子树。于此,若刚刚发生过一次回溯,那么就意味着当前节点的左子树已经遍历完毕,于是访问当前节点然后深入右子树继续遍历。

每个节点被访问后,都直接转向其在遍历序列中的直接后继,要么是其右子树,要么是某一先祖,若是后者,就立即回溯。直至最后一个节点。

通过比较就可以发现,使用后继节点的迭代算法的附加空间仅仅只需要o(1),虽然时间依然是o(n),但有很大的提升了,那么能不能继续改进呢?

4.2.4 后继节点的改进

在第二种迭代算法中,我们虽然没有再借助栈来辅助,但是借助了一个辅助标志位---布尔变量backtrack,于是我们来改进去掉这个辅助标志位,整体的设计思路依然不变。

void BiTree::inOrder_4() {
    BiNode *p;
    while(true)
        if(p->LChild) 
            p = p->LChild;
    else{
            cout << p->data << ' ';
            while(p->RChild = NULL)
                if(!succ(p)) return;
            else cout << p->data << ' ';
        p = p->RChild;
        }
}

4.3 后序遍历---PostOrder

后序遍历的输入/输出顺序是:
$$
左子树\rightarrow右子树\rightarrow节点
$$
例如:

graph TB 1((a))-->2((b)) 1((a))-->3((c)) 2((b))-->4((e)) 2((b))-->5((f)) 3((c))-->6((g)) 3((c))-->7((h)) 4((e))-->8((#)) 4((e))-->9((j)) 5((f))-->10((k)) 5((f))-->11((#)) 6((g))-->12((#)) 6((g))-->13((#)) 7((h))-->14((o)) 7((h))-->15((p))

遍历的顺序就是
$$
j\rightarrow e\rightarrow k\rightarrow f\rightarrow b\rightarrow q\rightarrow o\rightarrow p\rightarrow h\rightarrow c\rightarrow a
$$
后序遍历以两种方式实现:迭代和递归

4.3.1 递归版

//邓公代码
template <typename T>
void travPost_R(BinNodePosi(T) node, VST& visit){
    if(HasLC(*node)) travPost_R<T, VST>(node->LC, visit);
    if(HasRC(*node)) travPost_R<T, VST>(node->RC, visit);
}
//自己的代码(二叉树(二)文件)
void BiTree::PostOrder_1(BiNode * t) {
	if (t != NULL) {
		PostOrder(t->LChild);
		PostOrder(t->RChild);
		cout << t->data << ' ';
	}
}

4.3.2 迭代版

void BiTree::postOrder_2() {
	BiNode *p, *r;
	r = NULL;
	p = root;
	stack<BiNode*> my_stack;
	while (p != NULL || !my_stack.empty()) {
		if (p) {
			my_stack.push(p);
			p = p->LChild;
		}
		else {
			p = my_stack.top();
			if (p->RChild != NULL & p->RChild != r) {
				p = p->RChild;
				my_stack.push(p);
				p = p->LChild;
			}
			else {
				p = my_stack.top();
				my_stack.pop();
				cout << p->data << ' ';
				r = p;
				p = NULL;
			}
		}
	}
	cout << endl;
}

4.4 层次遍历

层次遍历就是按照从上到下、从左到右的顺序访问节点,此时需要一个队列或栈来实现,且多用迭代,递归并不好实现层次遍历

这里我采用的是队列

void BiTree::levelOrder() {
	if (root == NULL)
		return;
	queue<BiNode*> q;
	q.push(root);
	while (!q.empty()) {
		BiNode * t;
		t = q.front();
		q.pop();
		cout << t->data << ' ';
		if (t->LChild != NULL)
			q.push(t->LChild);
		if (t->RChild != NULL)
			q.push(t->RChild);
	}
	cout << endl;
}

4.5 遍历总结

无论是哪一种遍历,都有递归与迭代两种,但前序、中序与后序以递归实现较为方便,层次以迭代实现较为方便。

再者,我们会发现前三种遍历方式与第四种有很大的区别,我们也将前三种统称为深度优先算法(DFS),后一种我们也称之为广度优先算法(BFS)/宽度优先算法。

如其名,深度优先算法以深度优先,在二叉树中就是以树的高度优先,先深入至树的底部,再进行操作。广度/宽度优先算法,在二叉树中就以树宽,逐层读取,这种算法一般要借助栈或队列。

posted @ 2020-11-09 10:44  IcoveJ  阅读(218)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3