文章目录

  • 第六章 树
    • 树的定义
      • 结点的分类
    • 树的抽象数据类型
    • 树的存储结构
      • 双亲表示法
      • 孩子表示法
      • 孩子兄弟表示法
    • 二叉树
      • 特殊二叉树
        • 斜树
        • 满二叉树
        • 完全二叉树
    • 二叉树的性质
    • 二叉树的存储结构
    • 遍历二叉树
      • 二叉树遍历方法及算法
        • 前序遍历
        • 中序遍历
        • 后序遍历
        • 层序遍历
        • 推导遍历结果
    • 二叉树的建立
      • JAVA实现普通二叉树的创建与遍历
    • 线索二叉树
      • 线索二叉树原理
      • 线索二叉树实现
      • JAVA实现线索化与遍历
    • 树、森林与二叉树的转换
      • 树转换为二叉树
      • 森林转换为二叉树
      • 二叉树转换为树
      • 二叉树转换为森林
      • 树与森林的遍历
    • 赫夫曼树及其应用
      • 赫夫曼树定义与原理
      • 赫夫曼编码
  • 思维导图

第六章 树

树的定义

树的定义:树(Tree)是n个结点的有限集,n=0时称为空树。在任意一棵非空树中:

  1. 有且仅有一个特定的根(Root)的结点
  2. 当n>1时,其余的结点可分为m个互不相交的有限集 T 1 , T 2 , . . . T m T_1,T_2,...T_m T1​,T2​,...Tm​,其中每一个集合本身优势一棵树,并且称为根的子树(SubTree)


错误的树
子树相交了


结点的分类

结点的度:结点拥有的子树树被称为结点的度(Degree)。

叶结点/终端结点:度为0的结点称为叶结点(Leaf)或终端结点。

非终端结点/分支结点:度不0的结点。

内部结点:除了根结点之外,分支结点也称为内部结点。

树的度:树内各结点的度的最大值。

结点间关系

  1. 结点的子树的根称为该结点的孩子(Child),相应地,该节点称为孩子的双亲(Parent)。
  2. 同一个双亲的孩子之间互称兄弟(Sibling)。
  3. 结点的祖先是从根到该结点所经分支上的所有结点。
  4. 以某结点为根的子树中的任意结点都称为该节点的子孙。

树的其他相关概念

  • 结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。
  • 双亲在同一层的结点互为堂兄弟
  • 树中结点的最大层次称为树的深度(Depth)或高度。深度是绝对的,从根结点开始算,高度是相对的,看自己的子树有多高。
  • 如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则为无序树
  • 森林(Forest)是m棵互不相交的树的集合,对树中的每个结点而言,其子树的集合即为森林



树的抽象数据类型


树的存储结构

利用顺序存储和链式存储结构的特点,完全可以实现对树的存储结构的表示

双亲表示法

思路:在每个结点中,附设一个指示器指示其双亲结点到链表中的位置,知道爹妈是谁,不知道孩子是谁(段正淳?)。

结点结构定义

#define MAX_TREE_SIZE 100
typedef int TElemType;
typedef struct PTNode    // 结点结构
{TElemType data;    // 结点数据int parent;    // 双亲位置
} PTNode;typedef struct    // 树结构
{PTNode nodes[MAX_TREE_SIZE];int r, n;    // 根的位置和结点数
} PTreee;

根节点没有双亲,所以约定根结点的位置域设置为-1。

可以根据需求,再增加一个结点最左边孩子的域,叫长子域,如果没有孩子结点,长子域设置为-1:

还可以增加右兄弟域,如果存在右兄弟,就记录下右兄弟的小标,不存在就设置为-1:

那么左右是怎么确定的呢?


孩子表示法

多重链表表示法:由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这张方法叫做多重链表表示法。

然而树的每个结点的度是不同的,有两种解决方案。

方案一:
让指针域的个数就等于树的度:

其中data是数据域,child1到childd是指针域,用来指向该结点的孩子结点。

缺点:但是这样很浪费空间,除非说各个树的结点度相差很小的时候,才有优势。

方案二
每个指针结点域的个数等于该结点的度,专门取一个位置来存储结点指针域的个数:

其中data为数据域,degree为度域,也就是存储该结点的孩子结点的个数,child1到childd为指针域,指向该结点的各个孩子的结点。

缺点:虽然对空间的利用率提高了,但是由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗。

孩子表示法:把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空,然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中

分而治之?

孩子链表的孩子结点

其中child是数据域,用来存放结点在表头数组中的下标;next是指针域,存放指向某结点的下一个孩子结点的指针。

表头数组的表头结点


其中data是数据域,存储某结点的数据信息;firstchild是头指针域,存储该结点的孩子链表的头指针

孩子表示法结构定义

#define MAX_TREE_SIZE 100
typedef struct CTNode
{int child;    // 表头下标struct CTNode *next;
} *ChildPtr;
typedef struct CTBox   // 表头结构
{TElemType data;ChildPtr firstchild;
};
typedef struct CTree   // 树结构
{CTBox nodes[MAX_TREE_SIZE];    // 表头结构数组,就是结点数组int r, n;    // 根的位置和结点数
};

优点:对于查找某个结点的某个孩子,或者找某个结点的兄弟,遍历等比较方便

感觉兄弟不好找呀,比如找A的兄弟,要遍历找到包含A的子链表,才能找到A的兄弟

缺点:无法找到双亲,找双亲需要遍历。

所以可以把双亲表示法和孩子表示法综合一下,得到双亲孩子表示法


孩子兄弟表示法

理论基础:任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的,因此,设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。

其中data是数据域;firstchild为指针域,存第一个孩子的地址;righsib也是指针域,存储右兄弟结点的地址。

结构定义代码

typedef struct CSNode
{TElemType data;struct CSNode *firstchild, *rightsib;
} CSNode, *CSTree;

如果有需要可以加parent指针域。

好处:给查找某个结点的孩子带来了方便,同时把一棵复杂的树变成了一棵二叉树


二叉树

定义:二叉树是n个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成

二叉树特点

  • 每个结点最多两棵子树,所以二叉树中不存在度大于2的结点。
  • 左子树和右子树是有顺序的,次序不能任意颠倒。
  • 即使某结点只有一棵子树,也要区分他是左子树还是右子树

二叉树的五种基本形态

  1. 空二叉树
  2. 只有一个根结点
  3. 根结点只有左子树
  4. 根结点只有右子树
  5. 根结点既有左子树又有右子树

三个结点的二叉树有五种形态


特殊二叉树

斜树

定义:所有的结点都只有左子树的二叉树叫左斜树,只有右子树叫右斜树,这两者统称为斜树。

左斜树

右斜树

特点:每一层都只有一个结点,结点的个数与二叉树的深度相同。


满二叉树

定义:在一棵二叉树中,如果所有的分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树被称为满二叉树

特点

  1. 叶子只能出现在最下层
  2. 非叶子结点的度一定是2
  3. 在同深度的二叉树中,满二叉树的节点个数最多,叶子数最多

完全二叉树

定义:对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点的位置,和同深度的满二叉树中 i 结点的位置相同,那这颗树就叫完全二叉树。

没满,但是存在的结点结构和满二叉树相同

按层序编号后的结点应连贯,满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树,下面是非完全二叉树

特点

  1. 叶子结点只能出现在最下两层
  2. 最下层的叶子一定集中在左部连续位置
  3. 倒数二层,若有叶子结点,一定都在右部连续位置
  4. 如果结点度为1,则该结点只有左孩子,不存在只有右孩子的情况
  5. 同样结点树的二叉树,完全二叉树的深度最小

子树的生成以左边为主,但是不可只有左边,倒数第二层必须有右边,倒数第二层开始网上必须都完全填满,所以深度会最小


二叉树的性质

性质1:在二叉树的第 i 层上至多有 2 i − 1 2^{i-1} 2i−1个结点(i>=1)

性质2:深度为k的二叉树至多有 2 k − 1 2^{k}-1 2k−1个结点(k>=1)

性质3:对任何一棵二叉树T,如果其终端结点数位 n 0 n_0 n0​,度位2的结点数为 n 2 n_2 n2​,则 n 0 = n 2 + 1 n_0 = n_2 + 1 n0​=n2​+1

度为2的结点:ABCD,4个,设为 n 2 n_2 n2​
度为1的结点:E,1个,设为 n 1 n_1 n1​
度为0的结点:FGHIJ,5个,设为 n 0 n_0 n0​
总分支线 = 4 × 2 + 1 × 1 = 9 = n − 1 = n 1 + 2 n 2 4 \times 2 + 1 \times 1 = 9 = n-1 = n_1 + 2n_2 4×2+1×1=9=n−1=n1​+2n2​
其中 n = n 0 + n 1 + n 2 n = n_0 + n_1 + n_2 n=n0​+n1​+n2​,可以对推导出 n 0 + n 1 + n 2 − 1 = n 1 + 2 n 2 n_0 + n_1 + n_2 -1 = n_1 + 2n_2 n0​+n1​+n2​−1=n1​+2n2​,得出结论: n 0 = n 2 + 1 n_0 = n_2 + 1 n0​=n2​+1。

性质4:具有n个结点的完全二叉树深度为 [ log ⁡ 2 n ] + 1 [\log_2n]+1 [log2​n]+1,[x]表示不大于x的最大整数。
由 n = 2 k − 1 n=2^k-1 n=2k−1倒推而来。

性质5:如果对一棵有n个结点的完全二叉树的结点按层序编号(从第1层到第 [ log ⁡ 2 n ] + 1 [\log_2n]+1 [log2​n]+1层),对任一结点i(1<=i<=n)有:

  1. 如果i=1,则结点 i 是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]
  2. 如果2i>n,则结点 i 无左孩子;否则其左孩子是结点2i
  3. 如果2i+1>n,则结点 i 无有孩子;否则右孩子是结点2i+1


二叉树的存储结构

二叉树顺序存储结构

极端情况造成的浪费:

所以顺序存储结构一般只用于完全二叉树

二叉链表
二叉树每个结点最多两个孩子,设计一个数据域和两个指针域来完成:

其中data是数据域,lchild和rchild是指针域

二叉链表结构定义:

typedef struct BiTNode    // 结点结构
{TElemType data;struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

跟普通的树也差不多


遍历二叉树

原理:二叉树遍历值从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个节点被访问一次且访问访问一次

二叉树遍历方法及算法

按照从左到右的习惯方式,主要分为四种:

前序遍历

算法:若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树,下图的遍历顺序为:ABDGHCEIFA

代码实现

// 递归的方式
void PreOrderTraverse(BiTree T)
{if (T==Null)return ;printf("%c", T->data);    // 显示结点PreOrderTraverse(T->lchild);    // 先遍历左子树PreOrderTraverse(T->rchild);    // 然后遍历右子树
}

中序遍历

算法:若树为空,则空操作返回,否接从根结点开始(注意不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树,下图的顺序为:GDHBAEICF

从最下层结点的左子树开始,然后是结点,然后是右子树,位置不存在元素的话不会影响顺序。

代码实现

void InOrderTraverse(BiTree T)
{if (T=NULL)return ;InOrderTraverse(T->lchild);    // 先左子树到底,然后再开始打印输出printf("%c", T->data);    // 和前序的区别是先到底,再输出InOrderTraverse(T->rchild);
}

后序遍历

算法:若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点,下图的顺序为:GHDBIEFCA

代码实现

void InOrderTraverse(BiTree T)
{if (T=NULL)return ;InOrderTraverse(T->lchild);InOrderTraverse(T->rchild);printf("%c", T->data);    // 最后才显示
}

层序遍历

算法:若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上到下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问,下图的顺序为:


推导遍历结果

问题1:假设一二叉树的前序遍历序列为ABCDEF,中序遍历序列为CBAEDF,问后续遍历结果?

推导:前序遍历的第一个字母是根结点,所以A是根结点,由中序遍历可得,A的左边为左子树,右边为右子树,所以左子树是CB,右子树是EDF。根据前序中BC的打印顺序,说明B是A的左孩子,而C不能确定是B的左孩子还是右孩子,此时根据中序中CB顺序判断,可知C为B的左孩子,此时可得:

再看前序中的DEF,所以D是A的有孩子,E为D的孩子,F可能和E是兄弟,也可能是父子关系,此时看中序的EDF,可以说明E是D的左孩子,F是D的有孩子,由此可推导出二叉树为:

所以,后序遍历结果为:CBEFDA

问题2:已知中序为ABCDEFG,后序为BDCAFGE,求前序序列?

推导:由后续可知根结点为E,此时可由中序得出左子树为ABCD,右子树为FG,由后序的BDCA,可知A是E的左孩子;由中序的ABCD,知BCD是A结点的右子孙,由后序的BDC知道C结点是A结点的右孩子;中序序列AVCD,得到B是C的左孩子,D是C的右孩子,所以前序序列目前是EACBD;由后续的AFGE,得到G是E的右孩子,于是F就是G的孩子,所以最后前序序列为:EACBDGF。

二叉树遍历性质:已知中序和前/后序序列,可以唯一确定一棵二叉树。


二叉树的建立


使用右边的拓展二叉树有助于一个遍历确定一个二叉树,虚结点用#表示,所以右边的就是AB#D##C##。

// 按前序输入二叉树中的结点的值
// #表示空树,构造二叉链表表示二叉树T
void CreateBiTree(BiTree *T)
{TElemType ch;scanf("%c", &ch);if (ch=='#')*T = NULL;else{*T = (BiTree)malloc(sizeof(BiTNode));if (!*T)    // 没内存了?exit(OVERFLOW);(*T)->data = ch;    // 生成根结点CreateBiTree(&(*T)->lchild);    // 构造左子树CreateBiTree(&(*T)->rchild);    // 构造右子树}
}

JAVA实现普通二叉树的创建与遍历

package Tree;import java.util.Scanner;public class CreateTree {public static void main(String[] args) {BitNode bitNode = new BitNode();create(bitNode);show(bitNode);}private static void create(BitNode bitNode){System.out.println("输入:");char ch = new Scanner(System.in).next().charAt(0);if (ch=='#')bitNode.data = null;else{bitNode.data = ch;bitNode.lchild = new BitNode();bitNode.rchild = new BitNode();// 前序遍历法,会顺着左孩子一直向下,当到头的时候,会处理右孩子// 处理完了之后在往上一层,处理上一层的右孩子create(bitNode.lchild);create(bitNode.rchild);}}private static void show(BitNode bitNode){if(bitNode.data != null){// 前序遍历System.out.println(bitNode.data);show(bitNode.lchild);show(bitNode.rchild);}}
}class BitNode
{public Object data;public BitNode lchild, rchild;public BitNode() {}
}

线索二叉树

线索二叉树原理

动机:普通的二叉链表中,我们只知道每个结点指向其左右孩子结点的地址,而不知道某个结点的前驱和后继,想要知道需要遍历一次,所以考虑在创建时就记住这些前驱和后继,会节省很多时间。

线索二叉树:把指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就就成为线索二叉树(Threaded Binary Tree)。

线索二叉树构建

  1. 在空的左指针域填入前驱
  2. 在空的右指针域填入后驱
  3. 每个指针域结点再加个标签,来区分是左孩子还是前驱

后驱插入

前驱插入

线索化:对二叉树以某种次序遍历使其变成线索二叉树的过程称作是线索化

结点结构

其中tag为0时指向孩子,为1时指向前后驱,修改后如下图:


线索二叉树实现

二叉树的线索存储结构

typedef enum(Link, Thread) PointerTag;    // Link==0表示指向左右孩子指针
typedef struct BiThrNode
{TElemType data;struct BiThrNode *lchild, *rchild;    // 左右孩子指针PointerTag LTag;PointerTag RTag;    // 左右标志
} BiThrNode, *BiThrTree;

线索化代码
线索化过程就是在遍历过程中修改空指针的过程

// 中序遍历进行中序线索化
BiThrTree pre;    // 全局变量,始终指向刚刚访问过的结点
void InThreading(BiThrTree p)
{if (p){InThreading(p->lchild);    // 递归左子树线索化if (!p->lchild){p->LTag = Thread;    // 前驱线索p->lchild = pre;    // 左孩子指针指向前驱}if (!pre->rchild){pre->RTag = Thread;    // 后驱线索pre->rchild = p;    // 前驱右孩子指针指向后继}pre = p;    // 保持pre指向p的前驱InThreading(p->rchild);    // 递归右子树线索化}
}

代码很神奇,之前的左递归方法和中序高度契合,可以发现这个递归就是按照中序的顺序来的,所以在这里设置pre=p效果拔群,如果是处理前序或后续就没这么简单的写法了
左孩子为空和前驱有什么关联呢?左孩子为空说明前驱点一定不是左孩子,一定来自跳跃?
不对,这种结构就是要填满,左边右边都要填上,右边用后面填,左边用前面填,刚才想的太复杂了。

左指针为空的话,因为刚访问量前一个,所以直接把前一个(pre)当成前驱;
因为后面的没法预计,所以每个结点的后继的赋值都是在后一个结点中完成的,毕竟走到了下个结点才知道后继是啥,这个时候再给前一结点赋值后继。

遍历操作
线索二叉树的遍历就跟双向链表差不多,要添加一个头结点,让其lchild域的指针指向二叉树的根结点(图中1),让rchild域的指针指向中序遍历时访问的最后一个结点(图中2);同时,让二叉树中序序列中的第一个结点的lchild域指针最后一个结点的rchild域指针均指向头结点。
这样的好处是既可以从第一个结点起顺后继遍历,也可以从最后一个结点起顺前驱遍历。

遍历代码

// T指向头结点
Status InOrderTraverse_Thr(BiThTree T)
{BiThrTree p;p = T->lchild;    // p指向根结点// 循环1while (p!=T){// 循环2while (p->LTag==Link)    // 当LTag==0时循环到中序序列第一个结点p = p->lchild;printf("%c", p->data);    // 显示结点数据// 循环3while (p->RTag==Thread && p->rchild!=T){p = p->rchild;printf("%c", p->data);}p = p->rchild;    // p进至其右子树根}return OK;
}

代码解读:其中Link=0, Thread=1用来判断是孩子还是前后驱,循环1是判断是否遍历结束全部结点;循环2就是顺着当前结点左子树找到头;循环3就是顺着循环2找到的结点,判断是否还能往下走,如果不能就通过后继指针跳上去,转到并打印后继结点,如果遇到有孩子的情况,就停止跳跃。

使用环境:时间复杂度为O[n],如果是使用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么适合采用线索二叉链表的存储结构。


JAVA实现线索化与遍历

出错的地方:

  • 即使没有左右孩子,左右孩子也不为null,要用左右孩子的data判断是否存在左右孩子
  • while后面忘加大括号了
class BitThrNode
{public Object data;public BitThrNode lchild, rchild;public int ltag, rtag;public BitThrNode() {data = null;ltag = 0;rtag = 0;lchild = null;rchild = null;}public BitThrNode(Boolean b) {data = null;ltag = 0;rtag = 0;lchild = new BitThrNode();rchild = new BitThrNode();}
}public class BiThrTree {private static int pos;// 注意初始化个对象,不然在判断pre.lchild的时候会报空指针异常private static BitThrNode pre = new BitThrNode(Boolean.TRUE);public static void main(String[] args) {String string = "AB#D##C##";BitThrNode bitThrNode = new BitThrNode();pos = 0;create(bitThrNode,string);// 创造头结点BitThrNode head = new BitThrNode();// 头结点的常见需要在线索化的前面getHeadNode(bitThrNode, head);System.out.println("-----------线索二叉树----------");InThread(bitThrNode);head.ltag = 0;head.lchild = bitThrNode;head.rchild.rtag = 1;head.rchild.rchild = head;// 线索二叉树的遍历System.out.println("-----------线索二叉树遍历----------");InOrderTra(head);}private static void getHeadNode(BitThrNode thrNode, BitThrNode head) {if (thrNode.data != null){getHeadNode(thrNode.lchild, head);head.rchild = thrNode;getHeadNode(thrNode.rchild, head);}}private static void InOrderTra(BitThrNode head) {// 根结点BitThrNode p = head.lchild;// 为头结点,说明到头了while (p != head){// 一直向下找左孩子到底, 左孩子不可能是中序的最后一个点,所以这里不用判断// 又跑过头了,因此加了null判断while (p.ltag == 0 && p.data!=null) {// 测试用// System.out.println(p.data);// System.out.println(p.ltag);p = p.lchild;}// 打印了这个点System.out.println("data: " + p.data + " ltag: " + p.ltag+ " rtag: " + p.rtag);// 然后看此时有没有右孩子,有就进入右孩子,没有就通过后继指针跳跃// 在这里卡死了,最后一个的右孩子和head的右孩子在无限循环// 没加大括号坏事了,跟python搞混了while (p.rtag == 1 && p.rchild!=head) {p = p.rchild;System.out.println("data: " + p.data + " ltag: " + p.ltag+ " rtag: " + p.rtag);}// 现在有右孩子了,可以继续正常遍历p = p.rchild;// System.out.println("结尾是:" + p.data);}}private static void create(BitThrNode bitThrNode, String str){char ch = str.charAt(pos++);if (ch=='#')bitThrNode.data = null;else{   // 这边的创建是有问题的,即使不存在左右孩子,b.lchild和b.rchild也不会为null// 需要通过判断b.data来直到是不是没有结点bitThrNode.data = ch;bitThrNode.lchild = new BitThrNode();bitThrNode.rchild = new BitThrNode();// 前序遍历法,会顺着左孩子一直向下,当到头的时候,会处理右孩子// 处理完了之后在往上一层,处理上一层的右孩子create(bitThrNode.lchild, str);create(bitThrNode.rchild, str);}}private static void show(BitThrNode bitThrNode){if(bitThrNode.data != null){// 前序遍历System.out.println(bitThrNode.data);show(bitThrNode.lchild);show(bitThrNode.rchild);}}private static void InThread(BitThrNode bitThrNode){// 根据data来判断是否为空,而不是根据bitThrNode本身if (bitThrNode.data != null){// 中序遍历的方法来线索化InThread(bitThrNode.lchild);// 当前结点负责前驱的建立// 当该结点为第一个结点时,前驱为nullif (bitThrNode.lchild.data == null){bitThrNode.ltag = 1;bitThrNode.lchild = pre;}// 前一个结点建立后驱if (pre.rchild.data == null){pre.rtag = 1;pre.rchild = bitThrNode;}// 这块是没错的// System.out.println("data " + bitThrNode.data + " ltag " + bitThrNode.ltag// + " rtag " + bitThrNode.rtag);// System.out.println("pre data " + pre.data + " ltag " + pre.ltag//         + " rtag " + pre.rtag);pre = bitThrNode;InThread(bitThrNode.rchild);}}
}

树、森林与二叉树的转换

树转换为二叉树

步骤:

  1. 加线。在所有兄弟结点之间加一条连线(兄弟之间变成了父子关系)
  2. 去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线
  3. 层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。

森林转换为二叉树

森林由若干棵树组成,可以理解为森林中的每一棵树都是兄弟,可以按照兄弟的方法操作,步骤如下:

  1. 把每棵树转化为二叉树
  2. 把后面的二叉树的根结点作为前一个二叉树的右孩子,用线连接起来


二叉树转换为树

树转换为二叉树的逆过程步骤:

  1. 加线。如果某结点的左孩子结点存在,则将左孩子的n个右孩子结点都作为此结点的孩子,将该结点与这些右孩子结点用线连接起来
  2. 去线。删除原二叉树中所有结点与其右孩子结点的连线
  3. 层次调整。


二叉树转换为森林

看二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树,步骤如下:

  1. 从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除。。。知道所有右孩子连线都删除为止,得到分离的二叉树
  2. 再将每棵分离后的二叉树转换为树即可


树与森林的遍历

树的遍历

  1. 先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树
  2. 后根遍历,即先依次后根遍历每棵子树,然后再访问根结点

先根顺序为:ABEFCDG;后根顺序为:EFBCGDA。

森林的遍历

  1. 前序遍历:先访问第一棵树的根结点,再依次先根遍历根的每棵子树,再依次用同样方式遍历剩下的树构成的森林
  2. 后序遍历:先访问森林中的第一棵树,后根遍历的方式遍历每棵子树,再访问根结点,再依次同样方式遍历剩下的树构成的森林

前序遍历顺序:ABCDEFGHJI,和二叉树的前序遍历结果相同
后序遍历顺序:BCDAFEJHIG,和二叉树的中序遍历结果相同


赫夫曼树及其应用

赫夫曼树定义与原理

作用:提高传输效率。

路径长度:从树中的一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度。

树的路径长度:从树根到每一结点的路径长度之和。

二叉树a的树路径长度为20,二叉树b的树路径长度为16

赫夫曼树:让结点和路径长度相乘,所有的结点得到的这个值相加,结果为带权路径长度WPL,WPL最小的二叉树称做赫夫曼树。

二叉树a的WPL=315,二叉树b的WPL=220,这两个都不是赫夫曼树。

赫夫曼树构造

设置权重:A5,E10,B15,D30,C40

  1. 根据给定的结点的权重做一个集合F
  2. 在F中选取两棵根结点的权重最小的树作为左右子树(小的在左边),来构造一棵新的二叉树,二叉树的结点设置为左右子树的权重之和
  3. 在F中删除这两棵树,同时将新得到的二叉树加入到F中(变为 N 1 15 , B 15 , D 30 , C 40 N_115,B15,D30,C40 N1​15,B15,D30,C40)
  4. 重复2和3,知道F中只包含一棵树,这棵树就是赫夫曼树

赫夫曼编码

普通编码:BADCADFEED的原版二进制编码:

赫夫曼编码构造:根据出现的频率来构造赫夫曼树,如A27,B8,C15,D15,E30,F5。
之后再将左权值改为0,右权值改为1:

得到新的二进制编码:

结果对比

前缀编码:若要设计长短不等的编码,则必须是任意字符的编码都不是另一字符的编码的前缀,这种编码称做前缀编码。


思维导图

《大话数据结构》第六章 树相关推荐

  1. 数据结构思维 第六章 树的遍历

    第六章 树的遍历 原文:Chapter 6 Tree traversal 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 本章将介绍一个 Web 搜索引擎,我们将在本书其余部分开 ...

  2. 趣学数据结构--第六章:树

    趣学数据结构---->第六章:树 二叉树 线索二叉树 树的应用 二叉树的深度 二叉树的叶子数 二叉树的结点数 三元组创建二叉树 遍历序列还原树 哈夫曼树 二叉树 二叉树的创建(询问创建以及补空创 ...

  3. 《大话数据结构》样章试读

    <大话数据结构>样章试读 各位童鞋,<大话数据结构>从写作到出版,虽然经历了一些坎坷,但终于还是在今天正式在一些网店发售了.现在提供两章的完整版试读PDF文件,希望能给您有所 ...

  4. 数据结构第六章——图

    数据结构第六章--图 图的定义和术语 G = (V, E) V:顶点(数据元素的)有穷非空集合: E:边的有穷集合. 无向图:每条边都是无方向的. 有向图:每条边都是有方向的. 完全图:任意两个顶点都 ...

  5. 大话数据结构 第七章 图(二) 最小生成树、最短路径、拓扑排序、关键路径算法

    大话数据结构 第七章 图(二) 最小生成树.最短路径.拓扑排序.关键路径算法 最小生成树 定义 Prim算法 Kruskal算法 最短路径 Dijkstra算法 Floyd算法 拓扑排序 AOV网 拓 ...

  6. 中根遍历二叉查找树所得序列一定是有序序列_学习数据结构--第六章:查找(查找)

    第六章:查找 1.查找的基本概念 查找:在数据集合中寻找满足某种条件的数据元素的过程. 查找的结果 查找成功和查找失败 查找表:用于查找的数据集合,由同一种数据类型(或记录)的组成,可以是一个数组或链 ...

  7. 《大话数据结构》读书笔记-树

    写在前面:本文仅供个人学习使用.<大话数据结构>通俗易懂,适合整体做笔记输出,构建体系.并且文中很多图片来源于该书,如有侵权,请联系删除. 文章目录 6.2 树的定义 6.2.1 结点分类 ...

  8. 学习数据结构--第六章:查找(查找)

    第六章:查找 1.查找的基本概念 查找:在数据集合中寻找满足某种条件的数据元素的过程. 查找的结果 查找成功和查找失败 查找表:用于查找的数据集合,由同一种数据类型(或记录)的组成,可以是一个数组或链 ...

  9. 算法与数据结构 第四章 树与二叉树

    第四章树 一.选择题(20分) 1.在下述结论中,正确的是: (    ) ① 只有2个结点的树的度为1: ② 二叉树的度为2: ③ 二叉树的左右子树可任意交换: ④ 在最大堆(大顶堆)中,从根到任意 ...

最新文章

  1. Android中Intent传递对象的两种方法(Serializable,Parcelable)
  2. 基础知识(三)-网络
  3. curl 支持ws吗_curl 支持 http2
  4. optXXX方法,optBoolean
  5. 判断dialog是否显示_如何成为一流的仪表维修工之液位测量仪表故障检查判断思路...
  6. Spring PropertyPlaceholderConfigurer Usage - 使用系统变量替换spring配置文件中的变量
  7. java servlet异步_厉害了,Servlet3的异步处理机制
  8. java开发工具包jdk包括哪些
  9. 闲鱼如何利用端计算提升推荐场景的ctr
  10. 接口 对象 = new 实现类 与 实现类 对象= new 实现类
  11. -32767转化为二进制_谁说前端不需要懂二进制
  12. javaScript编码
  13. bci测试如何整改_电源动态响应测试,什么样的波形算合格?
  14. mysql时间转换格式
  15. 敏捷管理-PDCA循环(戴明环)
  16. 2022N1叉车司机题库及在线模拟考试
  17. 树莓派ONVIF推流做网络摄像头
  18. html 6位数支付密码,支付宝支付密码怎么设置长密码 支付宝支付密码取消6位数字密码设置长密码的方法...
  19. 【抓包】【Mac Charles】局域网无法抓包 手机热点来解决
  20. centos7 设置外部访问

热门文章

  1. 关于python的列表以下选项中描述错误的是_以下关于Python列表的描述中,错误的是______...
  2. 如何区分usb摄像头和uvc摄像头
  3. 赛普拉斯代理商与芯片ic是如何收购成功的
  4. 区块链是什么?一张图让你秒懂?
  5. .djvu 是什么文件阿?怎么把这个文件打开呢? 会的师傅帮帮忙吧!
  6. UniApp + JAVA连接百度云ocr进行身份证识别
  7. uniapp 上传视频获取视频封面图
  8. 成功解决将crx后缀文件加载到谷歌浏览器作为插件使用
  9. html 载入效果,HTML5 Loading动画特效集锦
  10. PYTHON从娃娃学起教程 教案 第二课计算机分类