思维导图——树

573 阅读18分钟

树与二叉树

上思维导图先:

Ch5.树与二叉树

5.1 树的基本概念

树的定义

  • 树是n(n≥0)个结点的有限集。当n=0时,称为空树

  • 任意一颗非空树应满足

    • 有且仅有一个特定的称为根的结点
    • 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,···,Tm,其中每个集合本身又是一棵树,并且称为根的子树
  • 递归的数据结构

  • 逻辑结构+分层结构

    • 树的根结点没有前驱,除根结点外的所有结点有且仅有一个前驱
    • 树中所有结点可以有零个或多个后继

基本术语

  • 考虑结点K

    • K的祖先:根A到结点K的唯一路径上的任意结点
    • 子孙:结点B是结点K的祖先,而结点K是结点B的子孙
    • 双亲与孩子:路径上最接近结点K的结点E称为K的双亲,而K为结点E的孩子
    • 兄弟:有相同双亲的结点称为兄弟,如结点K和结点L有相同的双亲E,即K和L为兄弟
  • 结点的度

    • 树中一个结点的孩子个数,树中结点的最大度数称为树的度
  • 分支结点(非终端结点)

    • 度大于0的结点
  • 叶子结点(终端结点)

    • 度为0(没有子女结点)的结点
  • 结点的层次

    • 从树根开始定义,根结点为第1层,它的子结点为第2层,以此类推
  • 结点的深度

    • 从根结点开始自顶向下逐层累加的
  • 结点的高度

    • 从叶结点开始自底向上逐层累加的
  • 树的高度(或深度)

    • 树中结点的最大层数
  • 有序树和无序树

    • 树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树
  • 路径和路径长度

    • 树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数
  • 森林

    • 森林是m(m≥0)棵互不相交的树的集合

      • 把树的根结点删去就成了森林
      • 给m棵独立的树加上一个结点,并把这m棵树作为该结点的子树,该森林就变成了树

树的性质

  • 树中的结点数等于所有结点的度数之和加1

  • 高度为h的m叉树至少有h个结点

  • 高度为h、度为m的树至少有h+m-1个结点

  • 具有n个结点的m叉树的最小高度为

  • 比较

    • m叉树

      • 任意结点的度≤m(最多m个孩子)
      • 允许所有结点的度都<m
      • 可以是空的
    • 度为m的树

      • 任意结点的度≤m(最多m个孩子)
      • 至少有一个结点度=m
      • 一定是非空树,至少有m+1个结点

5.2 二叉树的概念

二叉树的定义及其主要特性

  • 定义

    • 二叉树是另一种树形结构,其特点是每个结点至多有两棵子树,并且二叉树的子树有左右之分,其次序不能任意颠倒

    • 5种基本形态:

    • 二叉树与度为2的有序树的区别

      • 度为2的树至少有3个结点,而二叉树可以为空
      • 度为2的有序树的孩子的左右次序是相对于另一孩子而言的,若某个结点只有一个孩子,则这个孩子就无序区分左右次序。而二叉树无论其孩子数是否为2,均需确定其左右次序
  • 特殊的二叉树

    • 满二叉树

      • 一棵高度为h,且含有2^h-1个结点的二叉树

      • 对于编号为i的结点

        • 双亲为 ⌊i/2⌋
        • 若有左孩子,则左孩子为2i
        • 若有右孩子,则右孩子为2i+1
    • 完全二叉树

      • 高度为h、有n个结点的二叉树,其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应

      • 特点

        • 若i≤ ⌊n/2⌋,则结点为分支结点,否则为叶子结点
        • 叶子结点只可能在层次最大的两层上出现。对于最大层次的叶子结点,都依次排列在该层最左边的位置上
        • 若有度为1的结点,则只可能有一个,且该结点只有左孩子而无右孩子
        • 按层序编号后,一旦出现某结点(编号为i)为叶子结点或只有左孩子,则编号大于i的结点均为叶子结点
        • 若n为奇数,则每个分支结点都有左孩子和右孩子
        • 若n为偶数,则编号最大的分支结点(n/2)只有左孩子,没有右孩子,其余分支结点左右孩子都有
    • 二叉排序树

      • 左子树上所有结点的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于根结点的关键字:左子树和右子树又各是一棵二叉排序树
    • 平衡二叉树

      • 树上任一结点的左子树和右子树的深度之差不超过1
  • 二叉树的性质

    • 非空二叉树上的叶子结点等于度为2的结点数加1,即n0=n2+1

    • 非空二叉树第k层上至多有2^k-1个结点(k ≥ 1)

    • 高度为h的二叉树至多有2^h-1个结点(h≥1)

    • 对完全二叉树按从上到下、从左到右的顺序依次编号1,2,…,n,则

      • 当i>1时,结点i的双亲的编号为 ⌊i/2⌋

      • 当2i≤n时,结点i的左孩子编号为2i,否则无左孩子

      • 当2i+1≤n时,结点i的右孩子编号为2i+1,否则无右孩子

      • 结点i所在层次为⌊log2 i⌋ +1

      • 具有n个(n>0)结点的完全二叉树的高度为 ⌈log2(n+1)⌉或 ⌊log2 n⌋ +1

      • 在完全二叉树中,n1只能取0或1

        见王道数据结构p129题14解析

二叉树的存储结构

  • 顺序存储结构

    建议从数组下标1开始存储树中的结点

    • 用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i的结点元素存储在一维数组下标为i-1的分量中

      • 建议从数组下标1开始存储树中的结点,以满足二叉树的性质4
    • 完全二叉树和满二叉树采用顺序存储比较合适

      树中结点的序号可以唯一地反映结点之间的逻辑关系; 既能最大可能地节省存储空间,又能利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系

    • 一般的二叉树,为了能让数组下标反映二叉树结点之间的逻辑关系,只能添加一些并不存在的空结点,让其每个结点与完全二叉树上的结点相对照,再存储到一维数组的相应分量中

      最坏情况下,一个高度为h且只有h个结点的单支树却需要占据近2^h-1个存储单元

  • 链式存储结构

    typeof struct BiTNode { ElemType data; struct BiTNode *lchild, *rchild; }BiTNode, *BiTree;

    • 由于顺序存储的空间利用率比较低,因此二叉树一般都采用链式存储结构
    • 含有n个结点的二叉链表中,含有n+1个空链域

5.3 二叉树的遍历和线索二叉树

二叉树的遍历

  • 前中后

    • 递归

      • 先序遍历

        void PreOrder(BiTree T){ if(T!=NULL){ visit(T); PreOrder(T->lchild); PreOrder(T->rchild); } }

        • 访问根结点
        • 先序遍历左子树
        • 先序遍历右子树
      • 中序遍历

        void InOrder(BiTree T){ if(T!=NULL){ InOrder(T->lchild); visit(T); InOrder(T->rchild); } }

        • 中序遍历左子树
        • 访问根结点
        • 中序遍历右子树
      • 后序遍历

        void PostOrder(BiTree T){ if(T!=NULL){ PostOrder(T->lchild); PostOrder(T->rchild); visit(T); } }

        • 后序遍历左子树
        • 后序遍历右子树
        • 访问根结点
    • 非递归

      • 前序遍历

        void PreOrder2(BiTree T){ InitStack(S); BiTree p=T; while(p || IsEmpty(S) ){ if(p){ visit(p); Push(S, p); p=p->lchild; } else{ Pop(S,p); p=p->rchild; } } }

        • 1). 沿着根的左孩子,依次访问并入栈,直到左孩子为空,说明已找到可以输出的结点
        • 2). 栈顶元素出栈:若其右孩子为空,继续执行2); 若其右孩子不空,将右子树转执行1)
      • 中序遍历

        void InOrder2(BiTree T){ InitStack(S); BiTree p=T; while(p || IsEmpty(S) ){ if(p){ Push(S, p); p=p->lchild; } else{ Pop(S,p); visit(p); p=p->rchild; } } }

        • 1). 沿着根的左孩子,依次入栈,直到左孩子为空,说明已找到可以输出的结点
        • 2). 栈顶元素出栈并访问:若其右孩子为空,继续执行2); 若其右孩子不空,将右子树转执行1)
      • 后序遍历

        void PostOrder2(BiTree T){ InitStack(S); BiTree p=T; BiTree r=NULL; while(p || IsEmpty(S)){ if(p){ Push(S, p); p=p->lchild; } else{ GetTop(S, p); if(p->rchild && p->rchild!=r) p=p->rchild; else{ Pop(S, p); visit(p); r=p; p=NULL; } } } }

        • 1). 沿着根的左孩子,依次入栈,直到左孩子为空
        • 2). 读栈顶元素:若其右孩子不为空且未被访问过,将右子树转执行1);否则,栈顶元素出栈并访问
  • 层次遍历

    void LevelOrder(BiTree T){ 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); } }

    • 利用队列实现

    • 实现

      • 先将二叉树根结点入队,然后出队,访问出队结点
      • 若它有左子树,则将左子树根结点入队
      • 若它有右子树,则将右子树根结点入队
      • 然后出队,访问出队结点……如此反复,直至队列为空
  • 由遍历序列构造二叉树

    • 二叉树的先序(后序)序列和中序序列可以唯一地确定一颗二叉树

      先序和后序无法唯一确定一棵树

      • 先序找根,中序依根拆分,递归往复
    • 层序遍历与中序(后序)遍历可以确定一棵二叉树

线索二叉树

  • 基本概念

    • 用空指针存放指向结点前驱或后继的指针

    • 规则

      • 若无左子树,令lchild指向其前驱结点
      • 若无右子树,令rchild指向其后继结点
    • 结点结构

      typedef struct ThreadNode{ ElemType data; struct ThreadNode *lchild, *rchild; int ltag, rtag; }ThreadNode, *ThreadTree;

    • 标志域含义

      • ltag

        • 0

          • lchild域指示结点的左孩子
        • 1

          • lchild域指示结点的前驱
      • rtag

        • 0

          • rchild域指示结点的右孩子
        • 1

          • rchild域指示结点的后继
  • 中序线索二叉树

    • 概念

      • 二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索
      • 而前驱或后继的信息只有在遍历时才能得到,因此线索化的实质就是遍历一次二叉树
    • 建立

      void InThread(ThreadTree p, ThreadTree pre){ if(p!=NULL){ InThread(p->lchild, pre); if(p->lchild==NULL){ p->lchild=pre; p->ltag=1; } if(pre!=NULL && pre->rchild==NULL){ pre->rchild=p; pre->rtag=1; } pre=p; InThread(p->rchild, pre); } }

      void CreateInThread(ThreadTree T){ ThreadTree pre=NULL; if(T!=NULL){ InThread(T, pre); pre->rchild=NULL; pre->rtag=1; } }

      • 附设指针pre指向刚刚访问过的的结点,指针p指向正在访问的结点,即pre指向p的前驱

      • 在中序遍历过程中

        • 检查p的左指针是否为空,若为空就将它指向pre
        • 检查pre的右指针是否为空,若为空就将它指向p
    • 遍历

      //求中序线索二叉树中序序列下的第一个结点 ThreadNode *getFirstNode(ThreadNode *p){ while(p->ltag==0) p=p->lchild; return p; }

      //求中序线索树结点p在中序序列下的后继 ThreadNode *getNextNode(ThreadNode *p){ if(p->rtag == 0)  return getFirstNode(p->rchild); return p->rchild; }

      //中序线索树的中序遍历算法 void InOrder(ThreadNode *T){ for(ThreadNode *p=getFirstNode(T);p!=NULL;p=getNextNode(T)) visit(p); }

      • 若其右标志为"1",则右链为线索,指示其后继,否则遍历右子树中第一个访问的结点(右子树最左下的结点)为其后继
  • 先序线索树和后序线索树

    • 先序线索二叉树查找结点的先序后继很简单,而查找先序前驱必须知道该结点的双亲结点
    • 后序线索二叉树查找结点的后序前驱很简单,而查找后序后继必须知道该结点的双亲结点

5.4 树、森林

树的存储结构

  • 双亲表示法

    #define MAX_TREE_SIZE 100 typedef struct{ ElemType data; int parent; }PTNode; typedef struct{ PTNode nodes[MAX_TREE_SIZE]; int n; }PTree;

    • 采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置

    • 根结点下标为0,其伪指针域为-1

    • 优点

      • 利用了每个结点(根结点除外)只有唯一双亲的性质,可以很快地得到每个结点的双亲结点
    • 缺点

      • 求结点的孩子时需要遍历整个结构
  • 孩子表示法

    • 将每个结点的孩子结点都用单链表链接起来形成一个线性结构,此时n个结点就有n个孩子链表(叶子结点的孩子链表为空表)

    • 优点

      • 寻找子女的操作非常直接
    • 缺点

      • 寻找双亲的操作需要遍历n个结点中孩子链表指针域所指向的n个孩子链表
  • 孩子兄弟表示法

    typedef struct CSNode{ ElemType data; struct CSNode *firstchild, *nextsibling; }CSNode, *CSTree;

    • 以二叉树链表作为树的存储结构,此方法每个结点包括三个内容:结点值、指向结点第一个孩子结点的指针,及指向结点下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点)

    • 优点

      • 比较灵活,可以方便地实现树转换为二叉树的操作,易于查找结点的孩子等
    • 缺点

      • 从当前结点查找其双亲结点比较麻烦

        若为每个结点增设一个parent域指向其父结点,则查找其双亲结点也很方便。

树、森林与二叉树的转换

  • 树转换为二叉树的规则

    "左孩子右兄弟"

    • 每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟
  • 树转换为二叉树的画法

    • 在兄弟结点之间加一连线
    • 对每个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉
    • 以树根为轴心,顺时针旋转45°
  • 森林转换成二叉树的规则

    • 先将森林中的每棵树转换为二叉树,把森林中第二棵树根视为第一棵树根的右兄弟,第三棵树视为第二棵树根的右兄弟……以此类推
  • 森林转换成二叉树的画法

    • 将森林中的每棵树转换成相应的二叉树
    • 每棵树的根也可视为兄弟关系,在每棵树根之间加一根连线
    • 以第一棵树的根为轴心旋转45°
  • 二叉树转换成森林的规则

    • 若二叉树非空,则二叉树的根及其左子树为第一棵树的二叉树形式,故将根的右链断开
    • 二叉树根的右子树又可视为一个由除第一棵树外的森林转换后的二叉树
    • 应用同样的方法,直到只剩下一颗没有右子树的二叉树为止,最后再将每棵二叉树依次转换为二叉树

树和森林的遍历

  • 树的遍历

    • 先根遍历

      • 若树非空,先访问根结点,再依次遍历根结点的每棵子树,遍历子树时仍遵守先根后子树的规则
      • 其遍历序列与这棵树相应二叉树的先序序列相同
    • 后根遍历

      • 若树非空,先依次遍历根结点的每棵子树,再访问根结点,遍历子树时仍遵守先子树后根的规则
      • 其遍历序列与这棵树相应二叉树的中序序列相同
  • 森林的遍历

    • 先序遍历

      • 访问森林中第一棵树的根结点
      • 先序遍历第一棵树中根结点的子树森林
      • 先序遍历除去第一棵树之后剩余的树构成的森林
    • 中序遍历

      • 中序遍历森林中第一棵树的根结点的子树森林
      • 访问第一棵树的根结点
      • 中序遍历除去第一棵树之后剩余的树构成的森林

树的应用--并查集

算法学习笔记(1) : 并查集 - 知乎 (zhihu.com)

  • 通常使用树的双亲表示作为并查集的存储结构

    • 每个子集合以一棵树表示,所有表示子集合的树,构成表示全集合的森林
  • 三种操作

    #define SIZE 100 int UFSets [SIZE] ;//双亲结点下标 void Initial (int S[]) {//初始化 for(int i=0; i<size; i++) S[i]=-1; } //查找 int Find(int S[],int x){ while (S [x]>=0)//如果是正数就不是根节点 x = S[x]; return x } //合并 void Union(int S[],int Root1,int Root2) { S [Root2] =Root1 ; }

    • Union(S, Root1, Root2)

      • 把集合S中的子集合Root2并入子集合Root1。要求Root1和 Root2互不相交,否则不执行合并
    • Find(S, x)

      • 查找集合S中单元素x所在的子集合,并返回该子集合的名字
    • Initial(S)

      • 将集合S中的每个元素都初始化为只有一个单元素的子集合

5.5 应用

二叉排序树

二叉查找树

  • 定义

    对二叉树排序树进行中序遍历,可以得到一个递增的有序序列。

      1. 若左子树非空,则左子树上所有结点的值均小于根结点的值
      1. 若右子树非空,则右子树上所有结点的值均大于根结点的值
      1. 左右子树也分别是一棵二叉排序树
  • 查找

    非递归查找算法: BSTNode *BST_Search(BiTree T, ElemType key){ while(T!=NULL && key!=T->data){ if(key < T->data) T=T->rchild; else T=T->lchild; } return T; }

    • 从根结点开始,沿某个分支逐层向下比较的过程

      • 先将给定值与根结点比较,若相等则查找成功
      • 若不等,如果小于根结点的关键字,则在根结点的左子树上查找
      • 否则在根结点的右子树上查找
  • 插入

    int BST_Insert(BiTree &T,KeyType k){ if (T == NULL){ T = (BiTree)malloc(sizeof(BSTNode)); T->key = k; T->lchild = T->rchild = NULL; return 1; } else if (k == T->key){ return 0; } else if (k < T-key){ return BSTNode(T->lchild,k); } else if (k > T-key){ return BSTNode(T->rchild,k); } }

    • 过程

      • 若原二叉树为空,则直接插入结点
      • 若关键字k小于根结点,则插入到左子树
      • 若关键字k大于根结点,则插入到右子树
  • 构造

    void Create_BST(BiTree &T,keyType str[],int n){ //str存放插入的元素,n为插入的个数 T = NULL; int i = 0; while(i<n){ BST_Insert(T,str[i]); i++; } }

    • 构造的过程是一个动态的过程,不断的调用插入函数来进行构造
  • 删除

    • 若删除结点为叶子结点,则直接删除
    • 若删除结点z仅有一颗子树y,那么选取这棵子树y代替该结点z的位置
    • 若删除结点z有两颗子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删除这个直接后继(或前驱),这样就转换成了前两种情况
  • 查找效率分析

    • 平衡二叉树的平均查找长度是
    • 若二叉排序树是单支树,则平均查找长度是

平衡二叉树

  • 定义

    • 目的:避免树的高度增长过快,降低二叉排序树的性能
    • 规定在插入和删除二叉树结点时,要保证任意结点的左右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树,简称平衡树
    • 平衡因子:结点左子树与右子树的高度差
    • 平衡二叉树结点的平衡因子只可能是-1、0或1
  • 特性

    • 所有非叶结点的平衡因子均为1即为平衡二叉树满足平衡的最少结点情况
  • 插入

    • 基本思想

      • 每当在二叉排序树中插入(或删除)一个结点时,首先检查其插入路径上的结点是否因为此次操作而导致了不平衡。
      • 若导致了不平衡,则先找到插入路径上离插入结点最近的平衡因子的绝对值大于1的结点A,再对以A为根的子树,在保持二叉排序树特性的前提下,调整各结点的位置关系,使之重新达到平衡。
    • 调整规律

      • LL平衡旋转

        右单旋转

      • RR平衡旋转

        左单旋转

      • LR平衡旋转

        先左后右双旋转

      • RL平衡旋转

        先右后左双旋转

  • 删除(新增)

    • 1)用二叉排序树的方法对结点w进行删除操作
    • 2)从结点w开始,向上回溯,找到第一个不平衡的结点z,即最小不平衡子树,使用上面插入的调整规律
    • 注:删除和插入的区别:删除先对z为根的子树进行平衡调整,如果调整后子树的高度减1,就有可能需要对z的祖先结点进行平衡调整,甚至回溯到根结点
  • 查找

    • 与二叉排序树过程相同,因此,与给定值进行比较的关键字个数不超过树的深度
    • 平衡二叉树的平均查找长度是

红黑树(22新增)

  • 原因

    • 为了保持AVL树的平衡性,插入和删除操作后,非常频繁地调整全树整体拓扑结构,代价较大。为此在AVL树的平衡标准上进一步放宽条件,引入了红黑树的概念
  • 性质

    • 1.每个结点或是红色,或是黑色
    • 2.根结点是黑色的
    • 3.叶结点(外部节点、NULL结点)都是黑色的
    • 4.不存在两个相邻的红结点(即红结点的父结点和孩子结点均是黑色)
    • 5.对每个结点,从该结点到任一叶结点的简单路径上,所含黑结点的数目相同

哈夫曼树和哈夫曼编码

  • 哈夫曼树的定义

    • 结点的权:树中结点被赋予的一个表示某种意义的数值

    • 结点的带权路径长度:从树的根到结点的路径长度与该结点上权值的乘积

    • 树的带权路径长度:树中所有叶结点的带权路径长度之和

    • 哈夫曼树:在含有n个带权叶结点的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树

  • 哈夫曼树的构造

    • 给定n个权值分别为w1, w2, ... , wn的结点,构造算法如下:

        1. 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F
        1. 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和
        1. 从F中删除刚才选出的两棵树,同时将新得到的树加入F中
        1. 重复步骤2) 和3),直至F中只剩下一棵树为止
    • 特点

      • 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
      • 构造过程中一共建立了n-1个结点,因此哈夫曼树的结点总数为2n-1
      • 每次构造都选择两棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点
  • 哈弗曼编码

    • 编码方式

      • 固定长度编码

        • 对每个字符用相同长度的二进制位表示
      • 可变长度编码

        • 允许对不同字符用不等长的二进制位表示
        • 对频率高的字符赋以短编码,对频率较低的字符则赋以较长一些的编码
      • 前缀编码

        • 没有一个编码是另一个编码的前缀
      • 哈夫曼编码

        • 将每个出现的字符当作一个独立的结点,其权值为它出现的频度(或次数),构造出相应的哈夫曼树
        • 可以将字符的编码为从根至该字符的路径上边标记的序列,其中边标记为0表示“转向左孩子”,标记为1表示“转向右孩子”

工具:XMind
参考书:王道数据结构