第五章 树与二叉树

树的基本概念

数的定义和基本术语

  • 树:从树根生发,逐级分支。
  • 空树:结点数为0的树。
  • 非空树的特性:
    • 有且仅有一个根结点。
    • 没有后继的结点称为“叶子结点”(或终端结点)。
    • 有后继的结点称为“分支结点”(或非终端结点)。
    • 除了根结点外,任何一个结点都有且仅有一个前驱。
    • 每个结点可以有一个或多个后驱。
  • 树是 n (n >= 0)个结点的有限集合,n = 0 时,称为空树,这是一种特殊情况。在任意一颗非空树中应满足:
    1. 有且仅有一个特定的称为的结点。
    2. 当 n > 1 时,其余结点可分为 m (m > 0)个互不相交的有限集合 T1,T2,…Tm,其中每个集合本身又是一颗树,并且称为根结点的子树
  • 树是递归定义的数据结构。
  • 结点之间的关系描述:
    • 祖先结点
    • 子孙结点
    • 双亲结点(父结点)
    • 孩子结点
    • 兄弟结点
    • 堂兄弟结点
    • 两个结点之间的路径:只能从上往下
    • 路径长度:经过几条边
  • 结点和树的属性描述:
    • 结点的层次(深度)——从上往下数
    • 结点的高度——从下往上数
    • 树的高度(深度)——总共多少层
    • 结点的度——有几个孩子(分支)
    • 树的度——各结点的度的最大值
  • 有序树:逻辑上看,树中结点的各子树从左至右是有次序的,不能互换。
  • 有序树:逻辑上看,树中结点的各子树从左至右是无次序的,不能互换。
  • 森林:森林是 m(M >= 0)棵互不相交的树的集合。

树的性质

  • 结点数 = 总度数 + 1

  • 树的度——各结点度的最大值

  • m叉树——每个结点最多只能有m个孩子的树

  • 度为 m 的树m 叉树
    任意结点的度 <= m(最多 m 个孩子)任意结点的度 <= m(最多 m 个孩子)
    至少有一个结点度 = m(有 m 个孩子)允许所有结点的度都 < m
    一定是非空树,至少有 m + 1 个结点可以是空树
  • 度为 m 的树第 i 层至多有 mi-1个结点(i >= 1)

    • m 叉树第 i 层至多有mi-1个结点(i >= 1)
  • 高度为 h 的 m 叉树至多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m1mh1(等比求和)

  • 高度为 h 的 m 叉树至少有 h 个结点;高度为 h 同时度为 m 的树至少有 h+m-1 个结点。

  • 具有 n 个结点的 m 叉树的最小高度为 logm(n(m-1)+1)。

二叉树

二叉树的定义和基本术语

  • m 叉树是 n (n >= 0)个结点的有限集合:
    • 或者为空二叉树,即 n = 0。
    • 或者由一个根结点和两个互不相交的被称为根的左子树右子树组成。左子树和右子树分别是一颗二叉树。
    • 特点:
      • 每个结点至多只有两棵子树
      • 左右子树不能颠倒(二叉树是有序树
  • 二叉树是递归定义的数据结构。
  • 二叉树的五种状态:
    • 空二叉树
    • 只有左子树
    • 只有右子树
    • 只有根结点
    • 左右子树都有
  • 几个特殊的二叉树:
    • 满二叉树。一棵高度为 h,且含有2h-1个结点的二叉树。特点:
      • 只有最后一层有叶子结点。
      • 不存在度为1的结点。
      • 按层序从1开始编号,结点 i 的左孩子为 2i,右孩子为 2i+1;结点 i 的父结点为 i/2。
    • 完全二叉树。当且进当其每个结点都与高度为 h 的满二叉树中编号为 1~n 的结点一一对应时,称为完全二叉树。特点:
      • 只有最后两层可能有叶子结点。
      • 最多只有一个度为1的结点。
      • 按层序从1开始编号,结点 i 的左孩子为 2i,右孩子为 2i+1;结点 i 的父结点为 i/2。
      • i <= n/2 为分支结点,i > n/2 为叶子结点。
      • 如果某结点只有一个孩子,那么一定是左孩子。
    • 二叉排序树。一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
      • 左子树上所有结点的关键字小于根结点的关键字。
      • 右子树上所有结点的关键字大于根结点的关键字。
      • 左子树和右子树又各是一颗二叉排序树。
    • 平衡二叉树。树上任一结点的左子树右子树深度之差不超过1
      • 平衡二叉树能有更高的搜索效率。

二叉树的性质

  • 设非空二叉树度为0、1和2的结点个数分别为n0、n1和n2,则 n0 = n2 + 1(叶子结点比二分支结点多一个)
  • 二叉树第 i 层至多有 2i-1个结点(i >= 1)
  • 高度为 h 的二叉树至多有 2 h − 1 2^h-1 2h1(等比求和)
  • 具有 n 个(n > 0)结点的完全二叉树的高度 h 为 log2(n+1) 或 log2n + 1
  • 对于完全二叉树,可以由结点数 n 推出度为0、1和2的结点个数分别为n0、n1和n2
    • 完全二叉树最多只有一个度为1的结点,即 n1 = 0或1,而n0 = n2 + 1故n0 + n2 一定是奇数
      • 若完全二叉树有 2k (偶数)个结点,则必有n1 = 1,n0 = k,n2 = k-1
      • 若完全二叉树有 2k-1 (奇数)个结点,则必有n1 = 0,n0 = k,n2 = k-1

二叉树的存储结构

二叉树的顺序存储

  • 顺序存储的实现

    #define MaxSize 100
    struct TreeNode {
        ElemType value;	//结点中的数据元素
        bool isEmpty;	//结点是否为空
    }
    
    TreeNode t[MaxSize];
    

    定义一个长度为 MaxSize 的数组 t,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点。

  • 几个重要的基本操作

    • i 的左孩子—— 2i
    • i 的右孩子—— 2i+1
    • i 的父结点—— i/2
    • i 所在的层次—— log2(n+1))或 log2n + 1
  • 完全二叉树中共有 n 个结点

    • 判断 i 是否有左孩子——2i <= n
    • 判断 i 是否有右孩子——2i+1 <= n
    • 判断 i 是否是叶子或分支结点——i > n/2
  • 如果不是完全二叉树,依然按层序将各结点顺序存储,则无法从结点编号反映出结点间的逻辑关系。

  • 二叉的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来。

  • 最坏情况:高度为 h 且只有 h 个结点的单支树(所有结点只有右孩子),也至少需要 2h-1个存储单元。

二叉树的链式存储

  • 链式存储的代码实现

    typedef struct BiTNode {
        ElemType data;	//数据域
        struct BiTNode *lchild, *rchild;	//左、右孩子指针
    } BiTNode, *BiTree;
    

    n 个结点的二叉链表共有 n+1 个空链域。

  • 二叉树的建立

    struct ElemType {
        int value;
    }
    
    typedef struct BiTNode {
        ElemType data;
        struct BiTNode *lchild, *rchild;
    } BiTNode, *BiTree;
    
    //定义一棵空树
    BiTree root = NULL;
    
    //插入根结点
    root = (BiTree)malloc(sizeof(BiTree));
    root->data = {1};
    root->lchild = NULL;
    root->rchild = NULL;
    
    //插入新结点
    BiTNode * p = (BiTNode *)malloc(sizeof(BiTNode));
    p->data = {2};
    p->lchild = NULL;
    p->rchild = NULL;
    root->lchild = p;	//作为根结点的左孩子
    
  • 三叉链表——方便找父结点

    typedef struct BiTNode {
        ElemType data;	//数据域
        struct BiTNode *lchild, *rchild;	//左、右孩子指针
        struct BiTNode *parent;	//父结点指针
    } BiTNode, *BiTree;
    

二叉树的先/中/后序遍历

  • 遍历:按照某种次序把所有结点都访问一遍。

  • 层次遍历:基于树的层次特性确定的次序规则。

  • 先/中/后序遍历:基于树的递归特性确定的次序规则。

    • 先序遍历:根左右(NLR)
    • 中序遍历:左根右(LNR)
    • 后序遍历:左右根(LRN)
  • 二叉树的递归特性:

    • 要么是个空二叉树。
    • 要么就是由“根结点+左子树+右子树”组成的二叉树。
  • 算术表达式的“分析树”

    • 先序遍历:根左右 -> 前缀表达式
    • 中序遍历:左根右 -> 中缀表达式(需要加界限符)
    • 后序遍历:左右根 -> 后缀表达式
  • 先序遍历的代码实现

    1. 若二叉树为空,则什么都不做
    2. 若二叉树非空:
      1. 访问根节点
      2. 先序遍历左子树
      3. 先序遍历右子树
    typedef struct BiTNode {
        ElemType data;
        struct BiTNode *lchild, *rchild;
    } BiTNode, *BiTree;
    
    //先序遍历
    void PreOrder(BiTree T) {
        if (T != NULL) {
            visit(T);	//访问根结点
            PreOrder(T->lchild);	//递归遍历左子树
            PreOrder(T->rchild);	//递归遍历右子树
    	}
    }
    
  • 中序遍历的代码实现

    1. 若二叉树为空,则什么都不做
    2. 若二叉树非空:
      1. 先序遍历左子树
      2. 访问根节点
      3. 先序遍历右子树
    typedef struct BiTNode {
        ElemType data;
        struct BiTNode *lchild, *rchild;
    } BiTNode, *BiTree;
    
    //中序遍历
    void InOrder(BiTree T) {
        if (T != NULL) {
            InOrder(T->lchild);	//递归遍历左子树
            visit(T);	//访问根结点
            InOrder(T->rchild);	//递归遍历右子树
    	}
    }
    
  • 后序遍历的代码实现

    1. 若二叉树为空,则什么都不做
    2. 若二叉树非空:
      1. 先序遍历左子树
      2. 先序遍历右子树
      3. 访问根节点
    typedef struct BiTNode {
        ElemType data;
        struct BiTNode *lchild, *rchild;
    } BiTNode, *BiTree;
    
    //后序遍历
    void PostOrder(BiTree T) {
        if (T != NULL) {
            PostOrder(T->lchild);	//递归遍历左子树
            PostOrder(T->rchild);	//递归遍历右子树
            visit(T);	//访问根结点
    	}
    }
    
  • 先序遍历——第一次路过时访问结点

  • 中序遍历——第二次路过时访问结点

  • 后序遍历——第三次路过时访问结点

  • 应用:求树的深度

    int treeDepth(BiTree T) {
        if (T == NULL) {
            return 0;
    	}
        else {
            int l = treeDepth(T->lchild);
            int r = treeDepth(T->rchild);
            //树的深度 = Max(左子树深度,右子树深度)+1
            return l > r ? l+1 : r+1;
        }
    }
    

二叉树的层序遍历

  • 算法思想:

    1. 初始化一个辅助队列
    2. 根结点入队
    3. 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
    4. 重复上一步直至队列为空
  • 代码实现

    //二叉树的结点(链式存储)
    typedef struct BiTNode {
        ElemType data;
        struct BiTNode *lchild, *rchild;
    } BiTNode, *BiTree;
    
    //链式队列结点
    typedef struct LinkNode {
        BiTNode * data;	//存指针而不是结点
        struct LinkNode *next;
    }LinkNode;
    
    typedef struct {
        LinkNode *front, *rear;	//队头队尾
    }
    //层序遍历
    void LevelOrder(BiTree T) {
        LinkQueue Q;
        InitQueue(Q);	//初始化辅助队列
        BiTree p;
        EnQueue(Q, T);	//将根结点入队
        while (!IsEmpty(Q)) {	//队列不空则循环
            DeQueue(Q, p);	//队列结点出队
            visit(p);	//访问出队结点
            if(p->lchild != NULL)
                EnQueue(Q, p->lchild);	//左孩子入队
            if(p->rchild != NULL)
                EnQueue(Q, p->rchild);	//右孩子入队
        }
    }
    

由遍历序列构造二叉树

  • 一个中/前/后/层序遍历序列可能对应多种二叉树形态。中序 + 前/后/层序遍历可得到唯一二叉树。
  • 由前序/后序/层序找到树的根结点,并根据中序序列划分左右子树,再找到左右子树根结点。

线索二叉树

  • 如何找到指定结点 p 在中序遍历序列中的前驱和后继:从根结点出发,重新进行一次中序遍历,指针 q 记录当前访问的结点,指针 pre 记录上一个被访问的结点。

  • 当 q == p 时,pre 为前驱。

  • 当 pre == p 时,q 为后继。

  • 中序线索二叉树:

    • 前驱线索:左孩子指针充当
    • 后继线索:右孩子指针充当
  • 线索二叉树的存储结构

    //二叉树的结点(链式存储)
    typedef struct BiTNode {
        ElemType data;
        struct BiTNode *lchild, *rchild;
    } BiTNode, *BiTree;
    
    //线索二叉树结点
    typedef struct ThtreadNode {
        ElemType data;
        struct ThreadNode *lchild, *rchild;
        int ltag, rtag;	//左、右线索标志 tag:1 表示线索  tag:0 表示孩子
    } TreadNode, *TreadTree;	
    

二叉树线索化

  • 借用 pre 指针找到中序前驱

    //中序遍历
    void InOrder(BiTree T) {
        if (T != NULL) {
            InOrder(T->lchild);	//递归遍历左子树
            visit(T);	//访问根结点
            InOrder(T->rchild);	//递归遍历右子树
    	}
    }
    
    //访问结点 q
    void visit(BiTNode *q) {
        if (q == p)	//当前访问结点刚好是结点 p
            final = pre;	//找到 p 的前驱
        else 
            pre = q;	//pre 指向当前访问的结点
    }
    
    //辅助全局变量,用于查找结点 p 的前驱
    BiTNode * p;	//p 指向目标结点
    BiTNode * pre == NULL;	//指向当前访问的结点的前驱
    BiTNode * final == NULL;	//用于记录最终结果
    
  • 中序线索化

    //全局变量 pre,指向当前访问结点的前驱
    ThreadNode *pre = NULL;
    
    //中序线索化二叉树 T
    void CreateInThread(ThreadTree T) {
        pre = NULL;	//pre初始化为NULL
        if (T != NULL) {	//非空二叉树才能线索化
            InThread(T);	//中序线索化二叉树
            if (pre->rchild == NULL)
                pre->rtag = 1;	//处理遍历的最后一个结点
    	}
    }
    //线索二叉树结点
    typedef struct ThtreadNode {
        ElemType data;
        struct ThreadNode *lchild, *rchild;
        int ltag, rtag;	//左、右线索标志 tag:1 表示线索  tag:0 表示孩子
    } TreadNode, *TreadTree;
    
    //中序遍历二叉树,一边遍历一边线索化
    void InThread(ThreadTree T){
        if (T != NULL) {
            InThread(T->lchild);	//递归遍历左子树
            visit(T);	//访问根结点
            InThread(T->rchild);	//递归遍历右子树
    	}
    }
    
    void visit(ThreadNode *q) {
        if (q->lchild == NULL) {	//左子树为空,建立前驱线索
            q->lchild = pre;
            q->ltag = 1;
        }
        if (pre != NULL && pre->rchild == NULL) {
            pre->rchild = q;	//建立前驱结点的后继线索
            pre->rtag = 1;
    	}
        pre = q;
    }
    
  • 先序线索化

    //全局变量 pre,指向当前访问结点的前驱
    ThreadNode *pre = NULL;
    
    //先序线索化二叉树 T
    void CreatePreThread(ThreadTree T) {
        pre = NULL;	//pre初始化为NULL
        if (T != NULL) {	//非空二叉树才能线索化
           PreThread(T);	//先序线索化二叉树
            if (pre->rchild == NULL)
                pre->rtag = 1;	//处理遍历的最后一个结点
    	}
    }
    //线索二叉树结点
    typedef struct ThtreadNode {
        ElemType data;
        struct ThreadNode *lchild, *rchild;
        int ltag, rtag;	//左、右线索标志 tag:1 表示线索  tag:0 表示孩子
    } TreadNode, *TreadTree;
    
    //先序遍历二叉树,一边遍历一边线索化
    void PreThread(ThreadTree T){
        if (T != NULL) {
            visit(T);	//访问根结点
            if (T->ltag == 0)
            	PreThread(T->lchild);	//递归遍历左子树
            PreThread(T->rchild);	//递归遍历右子树
    	}
    }
    
    void visit(ThreadNode *q) {
        if (q->lchild == NULL) {	//左子树为空,建立前驱线索
            q->lchild = pre;
            q->ltag = 1;
        }
        if (pre != NULL && pre->rchild == NULL) {
            pre->rchild = q;	//建立前驱结点的后继线索
            pre->rtag = 1;
    	}
        pre = q;
    }
    
  • 后序线索化

    //全局变量 pre,指向当前访问结点的前驱
    ThreadNode *pre = NULL;
    
    //后序线索化二叉树 T
    void CreatePostThread(ThreadTree T) {
        pre = NULL;	//pre初始化为NULL
        if (T != NULL) {	//非空二叉树才能线索化
           PostThread(T);	//后序线索化二叉树
            if (pre->rchild == NULL)
                pre->rtag = 1;	//处理遍历的最后一个结点
    	}
    }
    //线索二叉树结点
    typedef struct ThtreadNode {
        ElemType data;
        struct ThreadNode *lchild, *rchild;
        int ltag, rtag;	//左、右线索标志 tag:1 表示线索  tag:0 表示孩子
    } TreadNode, *TreadTree;
    
    //后序遍历二叉树,一边遍历一边线索化
    void PreThread(ThreadTree T){
        if (T != NULL) {
            PreThread(T->lchild);	//递归遍历左子树
            PreThread(T->rchild);	//递归遍历右子树
            visit(T);	//访问根结点
    	}
    }
    
    void visit(ThreadNode *q) {
        if (q->lchild == NULL) {	//左子树为空,建立前驱线索
            q->lchild = pre;
            q->ltag = 1;
        }
        if (pre != NULL && pre->rchild == NULL) {
            pre->rchild = q;	//建立前驱结点的后继线索
            pre->rtag = 1;
    	}
        pre = q;
    }
    

线索二叉树——找前驱/后继

  • 在中序线索二叉树中找到指定结点 *p 的中序后继 next

    • 若 p->rtag == 1,则 next = p->rchild
    • 若 p->rtag == 0,则 next = p的右子树中最左下结点
    //找到以 p 为根的子树中,第一个被中序遍历的结点
    ThreadNode *Firstnode(ThreadNode *p) {
        //循环找到最左下结点(不一定是叶结点)
        while(p->ltag == 0)
            p = p->lchild;
        return p;
    }
    
    //在中序线索二叉树中找到结点 p 的后继结点
    ThreadNode *Nextnode(ThreadNode *p) {
        //右子树中最左下结点
        if  (p->rtag == 0)
            return Firstnode(p->rchild);
        else
            return p->rchild;	//rtag == 1直接返回后继线索
    }
    
    //对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)	空间复杂度:O(1)
    void Inorder(ThreadNode *T) {
        for(ThreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p))
            visit(p);
    }
    
  • 在中序线索二叉树中找到指定结点 *p 的中序前驱 pre

    • 若 p->ltag == 1,则 next = p->lchild
    • 若 p->ltag == 0,则 next = p的左子树中最右下结点
    //找到以 p 为根的子树中,最后一个被中序遍历的结点
    ThreadNode *Lastnode(ThreadNode *p) {
        //循环找到最右下结点(不一定是叶结点)
        while(p->ltag == 0)
            p = p->lchild;
        return p;
    }
    
    //在中序线索二叉树中找到结点 p 的前驱结点
    ThreadNode *Prenode(ThreadNode *p) {
        //左子树中最右下结点
        if  (p->ltag == 0)
            return Firstnode(p->lchild);
        else
            return p->lchild;	//rtag == 1直接返回后继线索
    }
    
    //对中序线索二叉树进行逆向中序遍历
    void RevInorder(ThreadNode *T) {
        for(ThreadNode *p = Lastnode(T); p != NULL; p = Prenode(p))
            visit(p);
    }
    
  • 在先序线索二叉树中找到指定结点 *p 的先序后继 next

    • 若 p->rtag == 1,则 next = p->rchild
    • 若 p->rtag == 0,则
      • 若 p 有左孩子,则先序后继为左孩子
      • 若 p 没有左孩子,则先序后继为右孩子
  • 在先序线索二叉树中找到指定结点 *p 的先序前驱 pre

    • 若 p->ltag == 1,则 next = p->lchild
    • 若 p->ltag == 0,则
      • p 必有左孩子。由于先序遍历中左右子树中的结点只可能是根的后继,不可能是前驱。只能从头开始先序遍历找前驱。
      • 若改用三叉链表可以找到父节点
        • p 为左孩子,则 p 的父结点即为其前驱
        • p 为右孩子,
          • 其左兄弟为空,则 p 的父结点即为其前驱
          • 其左兄弟非空,则 p 的前驱为左兄弟子树中最后一个被先序遍历的结点
  • 在后序线索二叉树中找到指定结点 *p 的后序前驱 pre

    • 若 p->ltag == 1,则 next = p->lchild
    • 若 p->ltag == 0,则 p 必有左孩子
      • 若 p 有右孩子,则后序前驱为右孩子
      • 若 p 没有右孩子,则后序前驱为左孩子
  • 在后序线索二叉树中找到指定结点 *p 的先序后继 next

    • 若 p->rtag == 1,则 next = p->rchild
    • 若 p->rtag == 0,则
      • p 必有右孩子。由于先序遍历中左右子树中的结点只可能是根的后继,不可能是前驱。只能从头开始先序遍历找前驱。
      • 若改用三叉链表可以找到父节点
        • p 为右孩子,则 p 的父结点即为其前驱
        • p 为左孩子,
          • 其右兄弟为空,则 p 的父结点即为其前驱
          • 其右兄弟非空,则 p 的前驱为右兄弟子树中第一个被后序遍历的结点

树和森林

树的存储结构

  • 双亲表表示法(顺序存储)

    • 用数组顺序存储各个结点。每个结点中保存数据元素、指向双亲结点(父节点)的“指针”
    #define MAX_TREE_SIZE 100	//树中最多结点数
    typedef struct {	//树的结点定义
        ElemType data;	//数据元素
        int parent;	//双亲位置域
    } PTNode;
    typedef struct {	//树的类型定义
        PTNode nodes[MAX_TREE_SIZE];	//双亲表示
        int n;	//结点数
    } PTree;
    
    • 优缺点:

      • 优点:找双亲(父结点)方便

      • 缺点:找孩子不方便,只能从头到尾遍历整个数组

      • 适用于“找父亲“多,”找孩子“少的应用场景。如:并查集

    • 双亲表示法也可表示森林,每棵树的根结点双亲指针 = -1。

  • 孩子表示法(顺序存储 + 链式存储)

    • 用数组顺序存储各个节点。每个结点中保存数据元素、孩子链表头指针
    struct CTNode {
        int child;	//孩子结点在数组中的位置
        struct CTNode *next;	//下一个孩子
    };
    typedef struct {
        ElemType data;
        struct CTNode *firstChild;	//第一个孩子
    } CTBox;
    typedef struct {
        CTBox nodes[MAX_TREE_SIZE];
        int n, r;
    } CTree;
    
    • 优缺点:
      • 优点:找孩子很方便
      • 缺点:找双亲(父结点)不方便,只能遍历每个链表
      • 适用于“找父亲“少,”找孩子“多的应用场景。如:服务流程树
    • 用孩子表示法存储森林,需要记录多个根的位置。
  • 孩子兄弟表示法(链式存储)

    typedef struct CSNode {
        ElemType data;
        struct CSNode *firstchild, *nextsibling;
    } CSNode, *CSTree;
    
    • 树的孩子兄弟表示法与二叉树类似,采用二叉链表实现。每个结点内保存数据元素两个指针,但两个指针的含义和二叉树结点不同。
    • 存储森林时,森林中每棵树的根结点视为平级的兄弟关系。

树、森林和二叉树的转化

  • 树转换为二叉树
    1. 先在二叉树中,画一个根结点。
    2. 按“树的层序”依次处理每个结点。
      • 处理一个结点的方法是:如果当前处理的结点在树中有孩子,就把所有孩子结点用右指针串成一串,并在二叉树中把第一个孩子挂在当前结点的左指针上。
  • 森林转换为二叉树
    1. 先把所有树的根结点画出来,在二叉树中用右指针串成一串。
    2. 按“森林的层序”依次处理每个结点。
      • 处理一个结点的方法是:如果当前处理的结点在树中有孩子,就把所有孩子结点用右指针串成一串,并在二叉树中把第一个孩子挂在当前结点的左指针上。
  • 二叉树转换为树
  1. 先画出树的根结点
  2. 从树的根结点开始,按“树的层序”恢复每个结点的孩子
    • 如何恢复一个结点的孩子,在二叉树中,如果当前处理的结点有左孩子,就把左孩子和一整串右指针拆下来,按顺序挂在当前结点的下面。
  • 二叉树转换为森林
    1. 先把二叉树的根结点和一整串右指针拆下来,作为多棵树的根结点
    2. 按“森林的层序”恢复每个结点的孩子
      • 如何恢复一个结点的孩子,在二叉树中,如果当前处理的结点有左孩子,就把左孩子和一整串右指针拆下来,按顺序挂在当前结点的下面。

树、森林的遍历

  • 树的先根遍历(深度优先遍历)。若树非空,先访问根结点,再依次对每棵子树进行先根遍历。

    //树的先根遍历
    void PreOrder(TreeNode *R) {
        if (R != NULL) {
            visit(R);	//访问根结点
            while (R 还有下一个子树 T)
                Prerder(T);	//先根遍历下一棵子树
    	}
    }
    
  • 树的后根遍历(深度优先遍历)。若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。

    //树的后根遍历
    void PostOrder(TreeNode *R) {
        if (R != NULL) {
            while (R 还有下一个子树 T)
                Prerder(T);	//后根遍历下一棵子树
            visit(R);	//访问根结点
    	}
    }
    

    树的后根遍序列与这棵树相应二叉树的中序序列相同。

  • 数的层次遍历(用队列实现)(广度优先遍历)

    1. 若树非空,则根结点入队
    2. 若队列非空,队头元素出队并访问,同时该元素的孩子依次入队
    3. 重复上一步直至队空
  • 先序遍历森林。

    • 若森林为非空,则按如下规则进行遍历:
      • 访问森林中第一棵树的根结点
      • 先序遍历第一棵树中根结点的子树森林
      • 先序遍历除去第一棵树之后剩余的树构成的森林
    • 效果等同于依次对二叉树的先序遍历。
  • 中序遍历森林。

    • 若森林为非空,则按如下规则进行遍历:
      • 中序遍历森林中第一棵树的根结点的子树森林
      • 访问第一棵树的根结点
      • 中序遍历除去第一棵树之后剩余的树构成的森林
    • 效果等同于依次对各个树进行后根遍历
    • 转化为二叉树后效果等同于依次对二叉树的中序遍历

哈夫曼树

  • 结点的:有某种现实含义的数值(如:表示结点的重要性等)
  • 结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
  • 树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL,Weighted Path Length)
  • 在含有 n 个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树
  • 哈夫曼树的构造:给定 n 个权值分别为 w1,w2,…,wn 的结点。
    1. 将这 n 个结点分别作为 n 棵仅含一个结点的二叉树,构成森林 F。
    2. 构造一个新结点,从 F 中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
    3. 从 F 中删除刚才选出的两棵树,同时将新得到的树加入 F 中。
    4. 重复以上两步,直至 F 中只剩下一棵树为止。
  • 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
  • 哈夫曼树的结点总数为 2n-1。
  • 哈夫曼树中不存在度为1的结点。
  • 哈夫曼树并不唯一,但 WPL 必然相同且为最优。
  • 固定长度编码——每个字符用相等长度的二进制位表示。
  • 可变长度编码——允许对不同字符用不等长的二进制位表示。
  • 若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。非前缀编码有歧义。
  • 由哈夫曼树得到哈夫曼编码——字符集中的每个字符作为一个叶子结点,每个字符出现的频度作为结点的权值,再构建哈夫曼树。

并查集

  • 用互不相交的树,表示多个“集合”

    • 如何“”到一个元素到底属于哪一个集合——从指定元素出发,找到根结点
    • 如何把两个集合“”为一个集合——让一棵树成为另一棵树的子树即可
  • “并查集”的存储结构:参考树的双亲表示法

    • 集合的两个基本操作——“并”和“查”
      • Find——“查”操作:确定一个指定元素所属集合
      • Union——“并”操作:将两个不相交的集合合并为一个
      • 并查集(Disjoint Set)是逻辑结构——集合的一种具体实现,只进行“并”和“查”两种基本操作
  • “并查集”的代码实现——初始化

    #define SIZE 13
    int UFSets[SIZE];
    
    //初始化并查集
    void Initial(int S[]) {
        for (int i = 0; i < SIZE; i++)
            S[i] = -1;
    }
    
  • “并查集”的代码实现——并、查

    //Find “查”操作,找 x 所属集合(返回 x 所属根结点)	最坏时间复杂度:O(n)
    int Find(int S[], int x) {
        while(S[x] >= 0)	//循环寻找 x 的根
            x = S[x];
        return x;	//根的S[]小于0
    }
    
    //Union “并”操作,将两个集合合并为一个	时间复杂度:O(1)
    void Union(int S[], int Root1, int Root2) {
        //要求 Root1 与 Root2 是不同的集合
        if (Root1 == Root2)
            return;
        //将根 Root2 连接到另一根 Root1 下面
        S[Root2] = Root1;
    }
    
  • 优化思路:在每次 Union 操作构建树的时候,尽量不让树长高

    • 用根结点的绝对值表示树的结点总数(例如 -6 而不是 -1)
    • Union 操作,让小树合并到大树
    //Union “并”操作,小树合并到大树
    void Union(int S[], int Root1, int Root2) {
        if (Root1 == Root2)
            return;
        if (S[Root2] > S[Root1]) {	//Root2 结点数更少
            S[Root1] += S[Root2];	//累加结点总数
            S[Root2] = Root1;	//小树合并到大树
        } else {
            S[Root2] += S[Root1];	//累加结点总数
            S[Root1] = Root2;	//小树合并到大树
    	}
    }
    

    该方法构造的树高不超过 log2n+1

  • 并查集的进一步优化

    • Find 操作的优化(压缩路径):先找到根结点,再将查找路径上所有结点都挂到根结点下。

      //Find “查”操作优化,先找到根结点,再进行“压缩路径”
      int Find(int S[], int x) {
          int root = x;
          while (S[root] >= 0)
              root = S[root];	//循环找到根
          while (x != root) {	//压缩路径	
              int t = S[x];	//t 指向 x 的父结点下
              S[x] = root;	//x 直接挂到根结点下
              x = t;
          }
          return root;	//返回根结点编号
      }
      

      每次 Find 操作,先找根,再“压缩路径”,可使树的高度不超过 O(α(n))。α(n) 是一个增长很缓慢的函数,对于常见的 n 值,通常 α(n) <= 4,因此优化后并查集的 Find、Union 操作时间开销都很低。

  • 8
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值