数据结构:树与二叉树

树是由 $n$ $(n>0)$ 个结点的有限集合。如果 $n=0$,称为空树;如果 $n>0$,则

  1. 有且仅有一个根结点,它没有直接前驱。
  2. 当 $n>1$,除根结点以外的其他结点划分为 $m$ $(m>0)$ 个互不相交的有限集 $T_1, T_2, \cdots , T_m$,其中每个集合本身又是一棵树,并且称为根的子树。

由树的性质可知,树的结构具有递归性。

树的基本概念

基本概念 含义
结点 一个数据元素及其指向结点的分支
结点拥有的子树个数
叶结点 度为 $0$ 的节点
子女 结点子树的根
兄弟 同一个结点的子女
祖先 根结点到该结点路径上的所有结点
子孙 某结点为根结点的子树上的任意结点
以根结点为第一层,根结点的子女为第二层,以此类推
树的深度 树中结点的最大层数
有序树 树中结点的子树从左向右有序
森林 $m$ $(m \geqslant 2)$ 棵互不相交的树

二叉树

二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根结点加上互不相交的左右子树。

二叉树具有以下性质:

  1. 在二叉树的第 $i$ $(i \geqslant 1)$ 层上至多有 $2^{i-1}$ 个结点。
  2. 深度为 $k$ $(k \geqslant 1)$ 的二叉树至多有 $2^{k-1}$ 个结点。
  3. 对任意一个二叉树 $T$,如果其叶结点数为 $n_0$,度为 $2$ 的结点数为 $n_2$,则 $n_0 = n_2 + 1$。
  4. 拥有 $n$ $(n \geqslant 0)$ 个结点的完全二叉树的深度为 $\left[ \log_{2}(n)\right] + 1$

二叉树的存储

  1. 顺序存储
  2. 链式存储
1/* ----- 二叉树的链式表示 ----- */
2typedef char TreeData;
3
4typedef struct node {
5    TreeData data;
6    struct node *leftChild; *rightChild;
7};
8
9typedef BinTreeNode *BinTree;

二叉树的操作

遍历

按某种次序访问树中所有结点,并且每个结点仅访问一次的操作。

1// 访问结点
2void visit(BinTreeNode *T) {
3    if (T != NULL) {
4        printf(T->data);
5    }
6}

遍历分为广度优先遍历和深度优先遍历两种。

遍历的方式 性质 具体内容
层序遍历 广度优先 逐层从左至右,从上到下访问
先序遍历 深度优先 先访问根结点,再访问左子树,最后访问右子树
中序遍历 深度优先 先访问左子树,再访问根结点,最后访问右子树
后序遍历 深度优先 先访问左子树,再访问右子树,最后访问根结点
 1// 先序遍历
 2void PreOrder(BinTreeNode *T) {
 3    if (T != NULL) {
 4        visit(T->data);
 5        PreOrder(T->leftChild);
 6        PreOrder(T->rightChild);
 7    }
 8}
 9
10// 中序遍历
11void InOrder(BinTreeNode *T) {
12    if (T != NULL) {
13        PreOrder(T->leftChild);
14        visit(T->data);
15        PreOrder(T->rightChild);
16    }
17}
18
19// 后序遍历
20void PostOrder(BinTreeNode *T) {
21    if (T != NULL) {
22        PreOrder(T->leftChild);
23        PreOrder(T->rightChild);
24        visit(T->data);
25    }
26}

获取二叉树的相关信息

二叉树的操作函数基本都是以递归作为执行的核心逻辑。

 1// 按前序建立二叉树
 2void CreateBiTree(BiTree* &T) {
 3    scanf(&ch);
 4    if (ch == '@') {
 5        T = NULL;
 6    }
 7    else {
 8        T = (BiTree*)malloc(sizeof(BiTree));
 9        T->data = ch;
10        CreateBiTree(T->leftChild);
11        CreateBiTree(T->rightChild);
12    }
13}
14
15// 计算二叉树结点个数
16int cnt = 0;
17void NodeCount(BiTree* &T) {
18    if (T != NULL) {
19        cnt ++;
20        Count(T->leftChild);
21        Count(T->rightChild);
22    }
23}
24
25// 求二叉树叶结点个数
26int LeafCount(BiTree* &T) {
27    if (!T) {
28        return 0;
29    }
30    else if (!T->leftChild && !T->rightChild) {
31        return 1;
32    }
33    else {
34        return LeafCount(T->leftChild) + LeafCount(T->rightChild);
35    }
36}
37
38// 求二叉树高度
39int Height(BiTree* &T) {
40    if (T == NULL) {
41        return 0;
42    }
43    else {
44        int m = Height(T->leftChild);
45        int n = Height(T->rightChild);
46        return (m>n) ? m+1 : n+1;
47    }
48}

二叉树的重构

重构:由二叉树的若干遍历序列,复现二叉树的原本结构。

已知遍历 结论
先序 + 中序 可重构
后序 + 中序 可重构
先序 + 后序 不可重构
 1// 重构二叉树
 2void BuildTree(BinTreeNode* &T, ElemType PreStart, ElemType PreEnd, 
 3                ElemType InStart, ElemType InEnd) {
 4    // 递归终点
 5    if (PreStart > PreEnd || InStart > InEnd) {
 6        return;
 7    }
 8
 9    T = new BinTreeNode();
10    // 当前子树的根结点
11    T->data = PreSeq[PreStart];
12    T->leftChild = NULL;
13    T->rightChild = NULL;
14
15    // 寻找中序遍历的根结点位置
16    int root = find(T->data, InSeq, InStart, InEnd);
17    // 计算左子树结点数目
18    int nL = root - InStart;
19    
20    // 递归构建左子树
21    BuildTree(T->leftChild, PreStart+1, PreStart+nL, InStart, root-1);
22    // 递归构建右子树
23    BuildTree(T->rightChild, PreStart+nL+1, PreEnd, root+1, InEnd);
24}

线索二叉树

线索二叉树中,除根节点外,每个结点都有自己的前驱和后继。

用二叉链表表示的二叉树中,$n$ 个结点的二叉树有 $n+1$个空链域,可利用这些空链域存储结点的前驱或后继。

线索二叉树的结点结构

1// PointerTag 为标志
2// Link = 0 代表指针,Thread = 1 代表线索
3typedef enum {Link, Thread} PointerTag
4
5typedef struct BiThrNode {
6    ElemType data;
7    struct BiThrNode *lchild, *rchild;
8    PointerTag LTag, RTag;
9};

中序线索二叉树

 1/* ----- 建立中序线索二叉树 ----- */
 2BiThrTree *pre;
 3
 4bool InOrderThreading(BiThrTree* &Thrt, BiThrTree T) {
 5    if(!Thrt = (BiThrTree*)malloc(sizeof(BiThrNode))) {
 6        // 分配空间失败
 7        return false;
 8    }
 9    Thrt->LTag = Link;
10    Thrt->RTag = Thread;
11    Thrt->rchild = Thrt;    // 头结点右指针回指
12
13    if(!T) {
14        Thrt->lchild = Thrt // 二叉树为空,头指针也回指
15    }
16    else {
17        Thrt->lchild = T;
18        pre = Thrt;         // pre指向最后一个访问过的结点
19        InThreading(T);
20        pre->RTag = Thread; // pre指向中序遍历的最后一个结点
21        pre->rchild = Thrt; // 最后一个结点的后继为头结点
22        Thrt->rchild = pre; // 头结点的右指针指向中序遍历的最后一个结点
23    }
24    return true;
25}
26
27void InThreading(BiThrTree* p) {
28    if(!p) {
29        InThreading(p->lchild);
30
31        if(!p->lchild) {
32            // 建立前驱线索
33            p->LTag = Thread;
34            p->lchild = pre;
35        }
36        if(!p->rchild) {
37            // 建立后继线索
38            pre->RTag = Thread;
39            pre->rchild = p;
40        }
41        // 保持pre为下一结点的前驱
42        pre = p;
43
44        InThreading(p->rchild);
45    }
46}

树与森林

树的存储结构

双亲表示

以一组连续空间存储树的结点,同时在结点中附设一个指针,存放双亲结点在链表中的位置。该方法利用每个结点只有一个双亲的特点,可以很方便地求出结点的双亲,但不方便得到结点的孩子。

树的双亲表示

 1/* ----- 树的双亲表示 ----- */
 2#define MAXSIZE 100
 3typedef int TreeData;
 4
 5typedef struct {
 6    TreeData data;
 7    int parent;
 8} TreeNode;
 9
10typedef TreeNode Tree[MAXSIZE];
孩子表示(多重链表)

将每个结点的孩子作为链表的结点链接在该结点之后,再将所有头结点组成一个线性表。

树的孩子链表表示

 1/* ----- 树的孩子链表表示 ----- */
 2// 孩子链表
 3typedef struct CTNode {
 4    int child;
 5    struct CTNode *next;
 6} *ChildPtr;
 7s
 8// 孩子链表的头结点
 9typedef struct {
10    TElemType data;
11    ChildPtr FirstChild;
12} CTBox;
13
14// 孩子链表头结点的线性表
15typedef struct {
16    CTBox nodes[MAX_TREE_SIZE];
17    int n;      // 结点数
18    int r;      // 根的位置
19} CTree;

左子女-右兄弟表示(二叉链表)

任何一棵和树对应的二叉树,其右子树一定为空。

1/* ----- 树的二叉链表表示 ----- */
2typedef char TreeData;
3
4typedef struct node {
5    TreeData data;
6    struct node *firstChild, *nextSibling;
7} TreeNode;
8
9typedef TreeNode *Tree;

树的二叉链表表示

使用二叉链表可以方便地实现森林与二叉树的转换。

树的遍历

遍历方法 操作 对应二叉链表的遍历
先根次序遍历 当树非空时,先访问根结点,再依次先跟遍历根的各棵子树 前序遍历
后根次序遍历 当树非空时,先依次后根遍历根的各棵子树,再访问根结点 中序遍历

霍夫曼树

路径长度与霍夫曼树的定义

概念 内容
路径长度 连接两结点的路径上的分支数,记为 $PL$
外部路径长度 各叶结点到根结点的路径长度之和,记为 $EPL$
内部路径长度 各非叶结点到根结点的路径长度之和,记为 $IPL$
带权路径长度 各叶结点所带的权值 $w_i$ 与该结点到根结点的路径长度 $l_i$ 的乘积之和,记为 $WPL$

容易知道,$PL=EPL+IPL$

霍夫曼树就是带权路径长度最小的二叉树,也称作最优二叉树。

在霍夫曼树中,权值大的结点离根结点越近。

 1/* ----- 霍夫曼树的定义 -----*/
 2const int n = 20;    // 叶结点数,初始给定的权值数量
 3const int m = 2*n-1; // 结点数,所需的存储数量
 4
 5typedef struct {
 6    float weight;   // 权值
 7    int parent, leftChild, rightChild;
 8} HTNode;
 9
10typedef HTNode HuffmanTree[m];

构造霍夫曼树

由给定的 $n$ 个权值 $\{w_0,w_1,w_2,\cdots,w_{n-1}\}$,构造具有 $n$ 棵二叉树的森林 $F=\{T_0,T_1,T_2,\cdots,T_{n-1}\}$,其中每棵二叉树 $T_i$ 只有一个带权值的根结点,其左右子树均为空。

步骤:

  1. 在 $F$ 中选取两棵根结点权值最小的二叉树,作为左、右子树构造一棵新的二叉树。令新的二叉树根结点的权值为其左右子树上根结点的权值之和。
  2. 在 $F$ 中删去这两棵二叉树。
  3. 把新的二叉树加入 $F$。
 1/* ----- 构造霍夫曼树 ------ */
 2void CreatHuffmanTree(HuffmanTree T[], float fr[]) {
 3    // 将各边的权值加入数组中
 4    for(int i = 0; i<n; i++) {
 5        T[i].weight = fr[i];
 6    }
 7    // 初始化各子树
 8    for(int i = 0; i<m; i++) {
 9        T[i].parent = -1;
10        T[i].leftChild = -1;
11        T[i].rightChild = -1;
12    }
13
14    for (int i = 0; i<m; i++) {
15        int min1 = min2 = MAXNUM;
16        int pos1, pos2;
17        for(int j = 0; j<i; j++) {
18            if(T[j].parent == -1) {
19                // 从所有根结点中查找权值最小的两个
20                if (T[j].weight < min1) {
21                    pos2 = pos1;
22                    min2 = min1;
23                    pos1 = j;
24                    min1 = T[j].weight;
25                }
26                else if(T[j].weight < min2) {
27                    pos2 = j;
28                    min2 = T[j].weight;
29                }
30            }
31        }
32        T[i].leftChild = pos1;      // 左子树为第一小的结点
33        T[i].rightChild = pos2;     // 右子树为第二小的结点
34        T[i].weight = T[pos1].weight + T[pos2].weight;
35        T[pos1].parent = T[pos2].parent = i;
36    }
37}

霍夫曼编码

霍夫曼编码主要用于实现数据压缩。

若按照各个字符出现概率的不同而给予不同长度的编码,可以减少总编码长度。总编码长度正好等于霍夫曼树的带权路径长度。

霍夫曼编码是一种无前缀编码(都由叶结点组成,路径不会重复)。解码时不会混淆。

设给出一段报文:

$$CAST CAST SAT AT A TASA$$

字符集合是{C, A, S, T},各个字符出现的频度(次数)为 W = {2, 7, 4, 5}

按照各字符出现概率P = {2, 7, 4, 5}为各叶结点上的权值建立霍夫曼树。

霍夫曼编码

左分支赋值0,右分支赋值1,可得霍夫曼编码:A:0 T:10 C:110 S:111

Licensed under CC BY-NC-SA 4.0
网站总访客数:Loading

使用 Hugo 构建
主题 StackJimmy 设计