一,树
-
树(Tree)的定义:树是n个结点的有限集
- 当n=0时,称为空树;
- 当n>0时,树由一个根结点和若干棵子树组成,每棵子树的根结点都是根结点的后继。
可见树是一个递归定义
- 树的几个基本概念:
- 结点的度:结点拥有的子树个数;
- 树的度:树中所有结点中最大的度数;
- 叶子结点:度为0的结点;
- 结点的层次:从根结点开始,根结点为第一层,根结点的子树为第二层,以此类推;
- 树的深度:树中结点的最大层次数;
- 有序树:树中结点的各子树从左到右是有次序的,不能互换;
- 无序树:树中结点的各子树从左到右没有次序,可以互换;
- 森林:m(m≥0)棵互不相交的树的集合。(树把根结点删了就是森林),树一定是森林,森林不一定是树;
二,二叉树(Binary Tree)
-
二叉树的定义:二叉树是n个结点的有限集合,该集合或者为空集,或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
-
二叉树和树的区别:
- 树中结点的度没有限制,二叉树的结点度最多为2;
- 树中结点的左右子树没有次序,二叉树的左右子树有次序,二叉树必须分左右子树;
-
二叉树的基本操作:
CreateBiTree(BiTree *T) //创建二叉树 DestroyBiTree(BiTree *T) //销毁二叉树 PreOrderTraverse(BiTree T) //先序遍历 InOrderTraverse(BiTree T) //中序遍历 PostOrderTraverse(BiTree T) //后序遍历 LevelOrderTraverse(BiTree T) //层序遍历
-
二叉树的性质:
- 在二叉树的第i层上至多有2^(i-1)个结点(i≥1);
- 深度为k的二叉树至多有2^k-1个结点(k≥1);
- 对任何一棵二叉树T,如果其叶子结点数为n0,度为2的结点数为n2,则n0=n2+1;
-
两种特殊形式的二叉树
- 满二叉树:深度为k且有2^k-1个结点的二叉树;
- 完全二叉树:深度为k,有n个结点的二叉树,当且仅当每个结点都与深度为k的满二叉树中编号为1~n的结点一一对应;
性质:
-
二叉树的顺序存储结构
- 实现:按照满二叉树的结点层次编号,依次存放二叉树中的数据元素
- 缺点:
-
二叉树的链式存储结构
- 二叉链表存储结构
typedef struct BiTNode{ TElemType data; struct BiTNode *lchild, *rchild; }BiTNode, *BiTree;
- 三叉链表存储结构
typedef struct TriTNode{ TElemType data; struct TriTNode *lchild, *rchild, *parent; }TriTNode, *TriTree;
-
二叉树的遍历(先左后右)
-
先序遍历(DLR):先访问根结点,再先序遍历左子树,最后先序遍历右子树;
先序遍历算法:
void PreOrderTraverse(BiTree T){ if(T!=NULL){ visit(T);//可修改此项为别的操作 PreOrderTraverse(T->lchild); PreOrderTraverse(T->rchild); } }
-
中序遍历(LDR):先中序遍历左子树,再访问根结点,最后中序遍历右子树;
中序遍历算法:
void InOrderTraverse(BiTree T){ if(T!=NULL){ InOrderTraverse(T->lchild); visit(T);//可修改此项为别的操作 InOrderTraverse(T->rchild); } }
-
后序遍历(LRD):先后序遍历左子树,再后序遍历右子树,最后访问根结点;
后序遍历算法:
void PostOrderTraverse(BiTree T){ if(T!=NULL){ PostOrderTraverse(T->lchild); PostOrderTraverse(T->rchild); visit(T);//可修改此项为别的操作 } }
-
层次遍历:从根结点开始,按从上到下、从左到右的顺序访问每一个结点;
算法设计思路:
- 根结点入队;
- 队头结点出队并访问;
- 若该结点有左孩子,则将左孩子入队;
- 若该结点有右孩子,则将右孩子入队;
- 重复2~4步,直到队列为空。
队列类型定义:
typedef struct{ BiTNode data[MAXSIZE]; int front,rear;//队头和队尾指针 }SqQueue;
层次遍历算法:
// 二叉树的层序遍历 void LevelOrderTraverse(BiTree T){ // 初始化队列 InitQueue(Q); // 定义指针p BiTree p; // 将根节点入队 EnQueue(Q,T); // 当队列不为空时,循环执行 while(!IsEmpty(Q)){ // 出队,将出队的节点赋值给p DeQueue(Q,p); // 访问节点p visit(p); // 如果节点p的左孩子不为空,将左孩子入队 if(p->lchild!=NULL) EnQueue(Q,p->lchild); // 如果节点p的右孩子不为空,将右孩子入队 if(p->rchild!=NULL) EnQueue(Q,p->rchild); } }
-
由二叉树的先序和中序遍历可以确认唯一二叉树
由二叉树的后序和中序遍历可以确认唯一二叉树
-
遍历算法的时间复杂度为O(n),空间复杂度为O(n)
-
遍历二叉树的非递归算法
- 中序遍历的非递归算法
void InOrderTraverse(BiTree T){ InitStack(S); p=T; while(p||!IsEmpty(S)){ if(p){ Push(S,p); p=p->lchild; } else{ Pop(S,p); visit(p); p=p->rchild; } } }
-
-
二叉树的建立(遍历算法的应用)
- 先序遍历建立二叉树的二叉链表
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); } }
- 复制二叉树
// 递归复制二叉树 void Copy(BiTree T1,BiTree &T2){ // 如果T1为空,则T2也为空 if(T1==NULL){ T2=NULL; return; } else{ // 为T2分配空间 T2=(BiTree)malloc(sizeof(BiTNode)); // 如果分配失败,则退出程序 if(!T2) exit(OVERFLOW); // 复制T1的值到T2 T2->data=T1->data; // 递归复制T1的左子树到T2的左子树 Copy(T1->lchild,T2->lchild); // 递归复制T1的右子树到T2的右子树 Copy(T1->rchild,T2->rchild); } }
- 计算二叉树深度
// 计算二叉树的深度 int Depth(BiTree T){ // 如果二叉树为空,则深度为0 if(T==NULL) return 0; else{ // 递归计算左子树的深度 int m=Depth(T->lchild); // 递归计算右子树的深度 int n=Depth(T->rchild); // 返回左子树和右子树深度的较大值加1 if(m>n) return m+1; else return n+1; } }
- 计算二叉树结点总数
// 计算二叉树的结点总数 int NodeCount(BiTree T){ // 如果二叉树为空,则结点数为0 if(T==NULL) return 0; else // 递归计算左子树的结点数加右子树的结点数加1 return NodeCount(T->lchild)+NodeCount(T->rchild)+1; }
- 计算二叉树叶子结点数
// 计算二叉树的叶子结点数 int LeafCount(BiTree T){ // 如果二叉树为空,则叶子结点数为0 if(T==NULL) return 0; // 如果二叉树是叶子结点,则叶子结点数为1 if(T->lchild==NULL&&T->rchild==NULL) return 1; else // 递归计算左子树的叶子结点数加右子树的叶子结点数 return LeafCount(T->lchild)+LeafCount(T->rchild); }
-
线索二叉树(英文为 Threaded Binary Tree)
- 实现:利用二叉链表中的空指针域,如果左孩子为空,则左指针指向其前驱,如果右孩子为空,则右指针指向其后继。为了区分左右指针是指向孩子还是前驱或后继,需要增加两个标志位itag和rtag并且约定
itag=0表示指向左孩子,itag=1表示指向前驱,rtag=0表示指向右孩子,rtag=1表示指向后继。 - 类型定义:
typedef struct ThreadNode{ TElemType data; struct ThreadNode *lchild,*rchild; int ltag,rtag; }ThreadNode,*ThreadTree;
- 线索二叉树的建立
- 增设一个头结点
- 实现:利用二叉链表中的空指针域,如果左孩子为空,则左指针指向其前驱,如果右孩子为空,则右指针指向其后继。为了区分左右指针是指向孩子还是前驱或后继,需要增加两个标志位itag和rtag并且约定
-
树和森林
-
树是n(n>=2)个结点的有限集合,其中有一个结点称为根结点,其余结点可分为m(m>=0)个互不相交的有限集合T0,T1,T2,…,Tm-1,其中每个集合Ti又是一棵树,称为根结点为Ti的子树。
-
森林是m(m>=0)棵互不相交的树的集合。
-
树的存储结构
- 双亲表示法
特点:找双亲容易,找孩子难
类型描述:
typedef struct{ TElemType data; int parent; }PTNode;
树结构:
// 定义一个结构体PTree,包含两个成员变量nodes和r,以及一个整型变量n typedef struct{ // 定义一个PTNode类型的数组nodes,大小为MAXSIZE PTNode nodes[MAXSIZE]; // 定义一个整型变量r,表示根节点的位置,n为结点个数 int r,n; }PTree;
-
孩子表示法
特点:找孩子容易,找双亲难
类型描述:- 孩子结点结构:
typedef struct CTNode{ int child; struct CTNode *next; }CTNode;
- 双亲结点结构:
typedef struct{ TElemType data; CTNode *firstchild; }CTBox;
- 树结构
typedef struct{ CTBox nodes[MAXSIZE]; int n,r;//结点数和根结点位置 }CTree;
-
孩子兄弟表示法(二叉链表表示法)
- 特点:找孩子和找双亲都容易
- 实现:用二叉链表作为树的存储结构,链表中的每个结点由三个域组成,data域存放结点的数据信息,firstchild域存放该结点的第一个孩子结点的指针,nextsibling域存放该结点的下一个兄弟结点的指针。
- 类型描述
typedef struct CSNode{ TElemType data; struct CSNode *firstchild,*nextsibling; }CSNode,*CSTree;
- 双亲表示法
-
树和二叉树的转换
-
将树转变为二叉树的步骤:
- 加线:在所有兄弟结点之间加一条线。
- 抹线:树中每个结点只保留它与第一个孩子结点之间的线,删除它与其他孩子结点之间的线。
- 旋转:以树的根结点为轴心,将整棵树顺时针旋转45度,使之结构层次分明。
总结口诀:树变二叉树,兄弟相连留长子
- 树变二叉树例子:
-
将二叉树转变为树的步骤:
-
二叉树变树例子:
-
-
森林与二叉树的转换
- 森林变二叉树:
- 将森林中的每棵树分别转换为二叉树。
- 将每棵树的根结点作为兄弟连在一起。
- 以第一棵树根结点作为二叉树的根,再以根结点为轴心,将整个二叉树顺时针旋转45度。
总结口诀:森林变二叉树,树变二叉根相连
- 森林变二叉树:
-
二叉树与森林的转换
-
二叉树变森林:
-
-
树的遍历
- 先根遍历
- 先访问根结点,再依次访问根结点的每棵子树
- 后根遍历
- 先依次访问根结点的每棵子树,再访问根结点
- 层次遍历
- 从根结点开始,自上而下,自左至右逐层遍历树中结点
- 先根遍历
-
森林的遍历
- 先序遍历
- 若森林非空,则
- 访问森林中第一棵树的根结点
- 先序遍历第一棵树中根结点的子树森林
- 先序遍历除去第一棵树之后剩余的树构成的森林
- 若森林非空,则
- 中序遍历
- 若森林非空,则中序遍历森林中第一棵树中根结点的子树森林
- 访问第一棵树的根结点
- 中序遍历其他树构成的森林
- 先序遍历
-
-
哈夫曼树
-
基本概念
- 判断树:用于描述分类过程的二叉树
- 路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径
- 路径长度:路径上的分支数目称为路径长度
树的路径长度:从树根到每一结点的路径长度之和
但是路径长度最短的树不一定是完全二叉树-
权:将树中结点赋予一个有某种意义的数值,该数值称为该结点的权
-
权值:结点的权值
-
带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和
-
总结:
-
哈夫曼树的构造
- 步骤:
例子1:
解读:
- 7,5,2,4全为根构成一个森林
- 选权值最小的2,4作为左右结点,根结点为二者之和,构成一个二叉树
- 原来的森林中删除2,4,将2中的二叉树放入森林中
- 重复2中的操作,在7,5,6中选择权值最小的5,6作为左右子树,根结点权值为11,构成一个二叉树,并且在森林中删除5,6,将生成的二叉树放入森林中
- 重复2和3的操作,知道森林中只剩下一个二叉树(或者只剩下一个根结点)
注释:最初的森林经过n-1次合并,最后会剩下一个根结点,所以哈夫曼树的构造需要n-1次合并,每次合并会产生一个结点,所以到最后一步。哈夫曼树会有2n-1个结点
例子2:
- 实现代码:
#include <stdio.h> #include <stdlib.h> // 定义哈夫曼树节点 typedef struct HTNode { int weight; // 节点权重 int lch, rch, parent; // 左子节点、右子节点、父节点 } HTNode, *HuffmanTree; // 选择两个权值最小的节点 // 选择权值最小的两个结点 void Select(HuffmanTree HT, int n, int *s1, int *s2) { // 初始化最小权值结点 int min1 = 0, min2 = 1; // 如果第一个结点的权值大于第二个结点的权值,则交换 if (HT[min1].weight > HT[min2].weight) { min1 = 1, min2 = 0; }//min1为最小权值,min2为次小权值 // 遍历剩余的结点 for (int i = 2; i < n; i++) { // 如果当前结点的权值小于最小权值结点的权值,则更新最小权值结点 if (HT[i].weight < HT[min1].weight) { min2 = min1; min1 = i; } else if (HT[i].weight < HT[min2].weight) { // 如果当前结点的权值小于次小权值结点的权值,则更新次小权值结点 min2 = i; } } // 将最小权值结点和次小权值结点的下标赋值给s1和s2 *s1 = min1; *s2 = min2; } // 构建哈夫曼树 void buildHuffmanTree(HuffmanTree HT, int n) { if (n <= 1) return; // 如果只有一个节点,直接返回 int m = 2 * n - 1; HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode));//0号单元未使用HT[m]表示根结点,数组加上0号单元就是2n个单位 for (int i = 1; i <= m; i++) { HT[i].lch = 0; HT[i].rch = 0; HT[i].parent = 0; }//初始化2n-1个结点 for (int i = 1; i <= n; i++) { scanf("%d", &HT[i].weight); } // 初始化结束,下面开始建立哈夫曼树 for (int i = n + 1; i <= m; i++) {//i在n+1和2n-1之间取值 int s1, s2; Select(HT, i - 1, &s1, &s2);//在HT[k](1<=k<=i-1)中选择两个权值最小的结点s1,s2 HT[s1].parent = i; HT[s2].parent = i; HT[i].lch = s1; HT[i].rch = s2; HT[i].weight = HT[s1].weight + HT[s2].weight; } } // 打印哈夫曼编码 void printHuffmanCodes(HuffmanTree HT, int n) { // ... // 省略具体的打印编码过程 } int main() { HuffmanTree HT; int n; printf("请输入哈夫曼树中节点的数量:"); scanf("%d", &n); buildHuffmanTree(HT, n); printHuffmanCodes(HT, n); return 0; }
-
-
哈夫曼编码
- 例子:
哈夫曼编码代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { int weight; int parent, lchild, rchild; } HuffmanTree; typedef char** HuffmanCode; void CreatHuffmanCode(HuffmanTree* HT, HuffmanCode* HC, int n) { // 从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中 // 分配n个字符编码的头指针矢量 *HC = (char**)malloc((n + 1) * sizeof(char*)); // 分配n+1个字符编码的头指针矢量,因为数组索引从1开始 char* cd = (char*)malloc(n); // 分配临时存放编码的动态数组空间 cd[n - 1] = '\0'; // 编码结束符,假设用'^'表示编码结束,编码为0——n-1 for (int i = 1; i <= n; ++i) { // 逐个字符求哈夫曼编码 int start = n - 1; // 初始化编码的起始位置 int c = i; // 当前字符的索引 int f = HT[i].parent; // 当前字符的父节点索引 // 从叶子结点开始向上回溯,直到根结点 while (f != 0) { // 回溯一次,start向前指一个位置 --start; if (HT[f].lchild == c) { cd[start] = '0'; // 结点c是f的左孩子,则生成代码0 } else { cd[start] = '1'; // 结点c是f的右孩子,则生成代码1 } // 继续向上回溯 c = f; f = HT[f].parent; } // 为第i个字符编码分配空间 (*HC)[i] = (char*)malloc((n - start) * sizeof(char)); // 分配空间,长度为编码的实际长度 // 将求得的编码从临时空间cd复制到HC的当前行中,复制了n-start个字符 strncpy((*HC)[i], &cd[start], n - start); (*HC)[i][n - start] = '\0'; // 确保字符串以null结尾 } free(cd); // 释放临时空间 } int main() { // 示例:初始化HuffmanTree和调用函数 HuffmanTree HT[100]; // 假设最大字符数为100 // 初始化HT和n int n = 5; // 假设n为5 // 调用函数 HuffmanCode HC; CreatHuffmanCode(HT, &HC, n); // 打印编码 for (int i = 1; i <= n; ++i) { printf("Character %d: %s\n", i, HC[i]); free(HC[i]); // 释放每个字符编码的内存 } free(HC); // 释放HC的内存 return 0; }
- 例子:
-
文件的解码与编码
-
编码:
-
解码:
-