前言:时隔四个月,这系列课程笔记总算有点时间整理更新到这里来了。
答:
1)加线:在所有结点(指的是兄弟间结点)之间加一条连线;
2)去线:对树中每个结点,只保留它与第一个孩子(从做到右数,第一个孩子)结点的连线,删除它与其他孩子之间的连线;
3)层次调整:以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明;
例子:
答:
1)把每棵树转为二叉树(方法如同1介绍);
2)第一棵树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来(将各个二叉树的根结点视为兄弟,从左到右连在一起,就形成了一棵二叉树)
答:
基本操作就是1、2的逆过程,步骤反过来做:
1)若结点x是其双亲y的左孩子,则把x的右孩子,右孩子的右孩子,…(总之就是右的右孩子),都与y用连线连接起来:
如例(x=B,y=A):
2)去除双亲到右孩子之间的连线:
3)调整位置:
判断一棵二叉树能够转换成一棵树还是森林,标准很简单,那就是看待转换的二叉树的根结点有木有右孩子,有的话就是森林,没有的话就是一棵树。
答:
树的遍历方式有两种:先根遍历、后根遍历
先根遍历:先访问树的根结点,然后再依次先根遍历根的每棵子树;
后根遍历:先依次遍历每棵子树,然后再访问根结点
例子:
森林的遍历也分为前序遍历和后序遍历,其实就是按照树的先根遍历和后根遍历依次访问森林的每一棵树
规律:树、森林的前序(先根)遍历和二叉树的前序遍历结果相同,树、森林的后序(后根)遍历和二叉树的中序遍历的结果相同
答:
1)赫夫曼树是首个实用的压缩编码方案;
2)在数据通信中,用二进制给每个字符进行编码不得不面对的一个问题是如何使电文总长最短且不产生二义性。根据字符出现频率,利用赫夫曼编码可以构造出一种不等长的二进制,使编码后的电文长度最短,且保证不产生二义性。
答:
1)将两个二叉树简化为叶子结点带权的二叉树。(树结点间的连线相关的树叫做权,weight)
2)结点的路径长度:从根结点到该结点的路径上的连接数,如左侧,A长度为1,B为2,C为3,D为3
3)树的路径长度:树中每个叶子结点的路径长度之和,如左侧,树总长度9
4)结点带权路径长度:结点的路径长度与结点权值的乘积,如左侧A,1x5;B,2x15;C,3x70;D,3x10
5)树的带权路径长度:WPL(weighted path length)是树中所有叶子结点的带权路径长度之和,如左侧计算得到275,而右侧同理计算可得210
6)WPL的值越小,说明构造出来的二叉树性能越优
答:
1)在森林中选出两棵根结点权值最小的二叉树;
2)合并两棵选出的二叉树,增加一个新结点作为新二叉树的根,权值为左右孩子权值之和
3)在已构建的树情况下,选择剩余中最小结点,继续构建树。
第一次合并(6=2+4)
第二次合并(11=5+6)
第三次合并(18=7+11)
答:
1)赫夫曼编码可以有效地压缩数据(通常可以节省20%~90%的空间,具体压缩率依赖于数据的特性,如字符重复的字数)
2)定长编码:像ASCII编码,约定使用8位来表示一个字符,如8个0表示一个A,10A就需要发送80个0
3)变长编码:单个编码的长度不一致,可以根据整体出现频率来调节,如约定一个0表示一个A,则10A,发送10个0
4)前缀码:所谓的前缀码,就是没有任何码字是其他码字的前缀
答:关键步骤:
1)build a priority queue:创建优先级的队列,字符在文本中出现的次数,次数越大权值越高;队列是从小到大排列的,出现次数越少就排越前,次数越多就排越后;
2)build a huffmanTree:根据上面的赫夫曼树构建方式进行代码构建赫夫曼树;
3)build a huffmanTable:构建赫夫曼表,存放字符;
4)encode:再次遍历文本,找到table,实现编码;
5)decode:解码
代码实现:
huffman.h
#pragma once
#ifndef _HUFFMAN_H //防止被重复定义;这种通用的命名方式为:_头文件名称_h
#define _HUFFMAN_H
typedef struct _htNode {
char symbol; //ASCII码符号
struct _htNode* left, * right; //左子树,右子树
}htNode;
typedef struct _htTree {
htNode* root;
}htTree;
typedef struct _hlNode {
char symbol; //存放字符
char* code; //索引
struct _hlNode* next; //连接下一个结点
}hlNode;
typedef struct _hlTable {
hlNode* first;
hlNode* last;
}hlTable;
htTree* buildTree(char* inputString);
hlTable* buildTable(htTree* huffmanTree);
void encode(hlTable* table, char* stringToEncode);
void decode(htTree* tree, char* stringToDecode);
#endif // !_HUFFMAN_H
queue.h
#pragma once
#ifndef _PQUEUE_H
#define _PQUEUE_H
#include"huffman.h"
#define TYPE htNode *
#define MAX_SZ 256
typedef struct _pQueueNode {
TYPE val;
unsigned int priority; //unsigned的使用,保证变量不会保存负数,优先级表示出现次数
struct _pQueueNode* next; //指向下一个结点
}pQueueNode;
typedef struct _pQueue {
unsigned int size;
pQueueNode* first; //指向队列的指针;也就是指向赫夫曼树头结点
}pQueue;
void initPQueue(pQueue** queue);
void addPQueue(pQueue** queue, TYPE val, unsigned int priority);
TYPE getPQueue(pQueue** queue);
#endif // !_PQUEUE_H
哈夫曼编码代码实现
#include
#include
#include
#include"queue.h"
#include"huffman.h"
void traverseTree(htNode* treeNode, hlTable** table, int k, char code[256]) {
if (treeNode->left == NULL && treeNode->right == NULL) {
//左右子节点都为空,表示该结点时叶子结点
code[k] = '\0';//结束字符串标记
hlNode* aux = (hlNode*)malloc(sizeof(hlNode));
aux->code = (char*)malloc(sizeof(char) * (strlen(code) + 1));
strcpy(aux->code, code);
aux->symbol = treeNode->symbol;
aux->next = NULL;
//这一组if-else就是链表存元素原理
if ((*table)->first == NULL) //如果是第一个链表第一个元素
//首末指针都指向插入的元素
{
(*table)->first = aux;
(*table)->last = aux;
}
else
{
(*table)->last->next = aux;//链表不为空,从后面插入链表
(*table)->last = aux;//链表的末尾指针后移
}
}
if (treeNode->left != NULL) {
//往左走,code[k]值取值0;否则往右走,为1
code[k] = '0';
traverseTree(treeNode->left, table, k + 1, code);
}
if (treeNode->right != NULL) {
code[k] = '1';
traverseTree(treeNode->right, table, k + 1, code);
}
//递归到达叶结点时,递归结束
}
hlTable* buildTable(htTree* huffmanTree) {
hlTable* table = (hlTable*)malloc(sizeof(hlTable));
table->first = NULL;
table->last = NULL;
char code[256];//存放字符
int k = 0; //每走一步就往下一级
traverseTree(huffmanTree->root, &table, k, code);//huffmanTree->root表示传入赫夫曼树根结点;
//table为待填充的索引的table;
//k表示目前处于树的第几层;
//code表示,添加字符结点,往左就加0,往右就加1
return table;
}
htTree* buildTree(char* inputString) {
int* probability = (int*)malloc(sizeof(int) * 256);//256个整型指针分别记录256个ASCII码出现次数【也就是将输入的字符串转为一个数组来理解】
//初始化
for (int i = 0; i < 256; i++) {
probability[i] = 0;
}
//统计待编码的字符串各个字符出现的次数
for (int j = 0; inputString[j] != '\0'; j++) {
probability[(unsigned char)inputString[j]]++;//统计输入字符出现的次数
}
//pQueue队列的头指针(头结点)
pQueue* huffmanQueue;
initPQueue(&huffmanQueue);
//填充队列
for (int k = 0; k < 256; k++) {
//队列生成【从小到大排列】
if (probability[k] != 0) {
htNode* aux = (htNode*)malloc(sizeof(htNode)); //定义一个赫夫曼树结点,并分配空间
//对定义的结点初始化
aux->left = NULL;
aux->right = NULL;
aux->symbol = (char)k;//出现的次数,也就是优先级
addPQueue(&huffmanQueue, aux, probability[k]);//huffmanQueue指针地址;aux为待插入的结点;probability[k]数组的k号元素
}
}//probability保存的出现次数,该循环已经将其使用了
free(probability);//释放传进来的指针所占的空间
//生成赫夫曼树
while (huffmanQueue->size!=1)//构建赫夫曼树
{
//将队列的前面两个权值相加,生成新结点的权值
int priority = huffmanQueue->first->priority;
priority += huffmanQueue->first->next->priority;
//新生成的结点,作为左或者右子树放好
htNode* left = getPQueue(&huffmanQueue);
htNode* right = getPQueue(&huffmanQueue);
htNode* newNode = (htNode*)malloc(sizeof(htNode));
newNode->left = left;
newNode->right = right;
addPQueue(&huffmanQueue, newNode, priority);
}
htTree* tree = (htTree*)malloc(sizeof(htTree));//定义根结点
tree->root = getPQueue(&huffmanQueue);//将队列中最后一个赋值给根结点(构建的赫夫曼树)
return tree;
}
void encode(hlTable* table, char* stringToEncode) {
hlNode* traversal;
printf("Encoding....\n\nInput string:\n%s\n\nEncode string:\n", stringToEncode);
for (int i = 0; stringToEncode[i] != '\0'; i++) {
traversal = table->first;
while (traversal->symbol!=stringToEncode[i])
{
traversal = traversal->next;
}
printf("%s", traversal->code);
}
printf("\n");
}
void decode(htTree* tree, char* stringToDecode) {
htNode* traversal = tree->root;
printf("\n\nDecoding....\n\nInput string:\n%s\n\nDecode string:\n", stringToDecode);
//对于要解码的字符串的每个“位”
//得到0,向左走
//得到1,向右走
for (int i = 0; stringToDecode[i] != '\0'; i++) {
if (traversal->left == NULL && traversal->right == NULL) {
printf("%c", traversal->symbol);
traversal = tree->root;
}
if (stringToDecode[i] == '0') {
traversal = traversal->left;
}
if (stringToDecode[i] == '1') {
traversal = traversal->right;
}
if (stringToDecode[i] != '0' && stringToDecode[i] != '1')
{
printf("The input string is not coded correctly!\n");
return;
}
}
if (traversal->left == NULL && traversal->right == NULL) {
printf("%c", traversal->symbol);
traversal = tree->root;
}
printf("\n");
}
注意:这里的代码使用的是c实现的,调试时候存在bug的,我先放上来,有大佬瞄出来的话,留言哈。
1)在线性表中,每个元素之间只有一个直接前驱和一个直接后继,在树形结构中,数据元素之间是层次关系,并且每一层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。
2)图的定义:
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中的顶点的集合,E是图G中边的集合
3)图形结构:图形结构的数据元素是多对多的关系
4)图定义注意地方:
4-1)线性表中,把数据元素叫做元素,树中叫结点,在图中,数据元素则被称为顶点(Vertex)
4-2)线性表可以没有数据元素,称为空表;树中可以没有结点,叫做空树;而图结构在国内大部分教材中强调顶点集合V要有穷非空(国外允许为顶点为0,图为空图)
4-3)线性表中,相邻的数据元素之间具有线性关系,树结构中,相邻两层的结点具有层次关系,而图结构中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集合可以是空的。
5)图的一些其他定义:
5-1)无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶(Vi,Vj)【Vi,Vj交换写也可以的,因为是无序的】来表示。
例子:
上图G1是一个无向图,G1={V1,E1},其中,V1={A,B,C,D},E1={(A,B),(B,C),(C,D),(D,A),(A,C)}
5-2)有向边:若从顶点Vi到Vj的边有方向,则称这条边为有向边,也称为弧(Arc),用有序偶
例子:
上图G2是一个有向图,G2={V2,E2},其中,V2={A,B,C,D},E2={,,
5-3)简单图:在图结构中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。
5-4)无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)/2条边
例子:
5-5)有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n*(n-1)条边。
例子:
5-6)稀疏图和稠密图:这里的稀疏和稠密是模糊的概念,都是相对而言的,通常认为边或弧数目小于n*logn (n是顶点的个数)的图称为稀疏图,反之称为稠密图。
5-7)有些图的边或弧带有与之相关的数,叫做权(weight),带权的图通常称为网(Network).
例子:
5-8)子图:
对于两个图G1=(V1,E1),G2=(V2,E2),如果V2是V1的子集,且E2是E1的子集,则称G2是G1的子图(Subgraph)。
例子,无向图和有向图:(右侧都是左侧的子图)
图的顶点与边之间的关系
1)无向图:
1-1)对于无向图G=(V,E),如果边(V1,V2)属于E,则称顶点V1和V2互为邻接点(Adjacent),即V1和V2相邻接。边(V1,V2)依附(incident)于顶点V1和V2,或者说边(V1,V2)与顶点V1和V2相关联。
1-2)顶点V的度(Degree)是和V相关联的边的数目,记做TD(V)。
例子:
无向图中,顶点A与B互为邻接点,边(A,B)依附顶点A与B上,顶点A的度为3.
2)有向图:
2-1)对于有向图G=(V,E),如果有
2-2)以顶点V为头的弧的数目称为V的入度(InDegree),记作ID(V),以V为尾的弧的数目称为初度(OutDegree),记作OD(V),因此顶点V的度为TD(V)=ID(V)+OD(V).
例子:
图中顶点A的入度为2,出度为1,所以其的度为3
3)无向图G=(V,E)中,从顶点V1到顶点V2,称为路径(Path).
4)如果G是有向图,则路径也是有向的。
例子:
图中红线表示的是顶点B到顶点D的两种路径,注意里面不存在顶点A到顶点B路径
5)路径的长度是路径上的边或弧的数目
6)第一个顶点到最后一个顶点相同的路径称为回路或环(Cycle)
7)序列中定点不重复出现的路径称为简单路径,除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路,称为简单回路或简单环
例子:
左图,从B到D所示的红色路径没有顶点重叠的,所以属于简单环;右图,从B到D所示的红色路径,点C重复了,所以不是简单环
答:
在无向图G中,如果顶点V1到顶点V2有路径,则称V1和V2是连通的,如果对于图中任意两个顶点Vi和Vj都是连通的,则称G是连通图(ConnectedGraph)
答:无向图中的极大连通子图称为连通分量。
注意:
1)首先是子图,并且子图是要连通的;
2)连通子图含有极大顶点数;【极大:对照图解析,子图包含了和顶点有关的所有边】
3)具有极大顶点数的连通子图包含依附于这些顶点的所有边
例子:
下方两图都是上图的子图,但是下左具有极大顶点数,所以是连通分量;而下右的极大定点数应该是A,B,C,D,4个,所以不是连通分量
1)在有向图G中,如果对于每一对Vi到Vj都存在路径,则称G是强连通图。
2)有向图中的极大强连通子图称为有向图的强连通分量。
例子:
左侧不是强连通图,如从D到C就没有路径了;右侧是强连通图。同时,右侧是左侧的极大强连通子图,也是左侧的强连通分量。
一个连通图的生成树是一个极小的连通子图,它包含图中全部的n个顶点,但只有n-1条边才构成一棵树。
例子:
下两图是通过删除上图的两条边【注意边不是随便删除的】,从而实现连通图的生成树
如果有一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一棵有向树。
例子:
左为普通有向图,中和右是有向树
1)图分为有向图和无向图;
2)无向图由顶点和边构成;有向图有顶点和弧(指的是有向的边)构成;弧分为弧尾和弧头,从弧尾指向弧头的;
3)图根据边或弧的多少,分为稀疏图和稠密图,一般根据n*logn来判断,只是相对概念;
4)如果任意两个顶点之间都存在边,则称为完全图;有向的,则称为有向完全图;无向的则称为无向完全图;
若无重复的边,到顶点,到自身,则称为简单图;图中顶点之间有连接点,则有依附概念;无向图顶点的边数目,称为度;
5)有向图的度分为入度【指向顶点】和出度【离开顶点】,两者之和才是度大小;
6)图上的边或弧带有权,则称为network网;两个顶点之间存在路径,则称两顶点是连通的;任意两个顶点存在路径的图,称之为连通图;连通图有向,则称之为强连通图;
7)图中有子图,若子图极大连通【子图中包含了和顶点有关的所有的边】,则称之为连通分量;连通分量有向,则称之为强连通分量;无向图可以构成一个生成树,该树特点是有n个顶点,具有n-1条边,同时边点需要连成一个整体;
8)有向图中,一个顶点入度为0,其他顶点入度为1,则称之为有向树;一个有向图,由若干个有向树构成森林。
1)对于线性表,是一对一关系,可以使用数组或者链表均可简单存放;树是一对多的关系,将数组和链表的特性结合在一起才能存放;但是,图是多对多关系,相对比较难进行存放;
2)树,因为任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系(内存物理位置是线性的,图的元素关系是平面的);
3)单纯使用多重链表可以实现树的存储,但是会导致极大的浪费,所以不宜采用。
1)考虑到图是由顶点和边或者弧两部分组成,合在一起比较困难,那就考虑采用两个结构分别存储;
2)顶点因为不区分大小、主次,所以用一个一维数组来存储;而边或弧由于是顶点与顶点之间的关系,一维数组难以满足需要,因此,考虑使用一个二维数组来存储;
3)图的邻接矩阵(Adjacency Matrix)存储方式是用来个数组来表示图。一个一维数组存储存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
例子:
那么,从上面的图可以看出,顶点数组为vertex[4]={V0,V1,V2,V3},边数组arc[4][4]为对称矩阵(由于是无向图)【对称矩阵:a[i][j]=a[j][i]】
4)根据邻接矩阵(属于对称矩阵),可以获取信息:
4-1)任意两顶点是否无边的表示很容易,1表示有边,0表示无边;
4-2)根据顶点Vi在在邻接矩阵中第i行或列的元素之和,就是该顶点的度;
4-3)求顶点Vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1,就是邻接点。
1)邻接矩阵,arc[i][j],i表示尾,j表示头;
对于有向图,可见顶点数组vertex[4]={V0,V1,V2,V3},弧数组arc[4][4]也是一个矩阵,但因为是有向图,所以这个矩阵并不对称,例如V1到V0有弧,得到arc[1][0]=1,而V0到V1没有弧,因此,arc[0][1]=0。
顶点的出度,等于行的各数值之和;入度,等于列的各数值之和。
1)边带有权的图称为网
2)存储:
这里的无穷符号,表示一个计算机允许的,大于所有边上的权值的值。
邻接矩阵,对应边数相对较少的图,会有极大的空间存储的浪费。
例子:
1)为了避免邻接矩阵可能带来的浪费,采用数组与链表结合方式进行存储,这种存储方式,在图中称之为邻接表(AdjacencyList)。
2)图的邻接表:
2-1)图的顶点用一个一维数组存储。当然,顶点也可以用单链表来存储,不过数组可以较容易读取顶点信息,更加方便;
2-2)图的每个顶点Vi的所有邻接点构成一个线性表,由于邻接点的个数不确定,所以我们选择单链表来存储。
1)对于有向图,邻接表也是类似的,把顶点当弧尾建立邻接表,很容易得到每个顶点的出度。
有向图邻接表例子:
2)有时为了便于确定顶点的入度或以顶点为弧头的弧,可以建立一个有向图的逆邻接表。
逆邻接表例子:
对于带权值的网图,可以在边表结点定义中再增加一个数据域来存储权值即可:
答:
1)将邻接表和逆邻接表结合起来就是十字链表(Orthogonal List)
2)顶点表结点结构:
firstin是第一个入边表指针;firstout是第一个出边表指针
3)重新定义边表结点结构(十字链表):
例子:
上面是出度角度编写,下面得红色表示入度方式编写
4)十字链表好处:
4-1)十字链表得好处就是因为把邻接表和逆邻接表整合一起,这样既容易找到以Vi为尾的弧,也容易找到以Vi为头的弧,因而容易求得顶点的出度和入度;
4-2)十字链表除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的,因此,在有向图的应用中,十字链表也是非常好的数据结构模型。
1)由于邻接表关注的是顶点,对于边的删除操作比较麻烦;所以有了邻接多重表。
2)邻接多重表结点结构:
其中iVex和jVex是与某条边依附的两个顶点在顶点表中的下标。iLink指向依附顶点iVex的下一条边,jLink指向依附顶点jVex的下一条边
也就是说,邻接多重表里边,边表存放的是一条边,而不是一个顶点。
邻接多重表例子:
1)边集数组是由两个一维数组构成,一个是存储顶点的信息,另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。
1)对于图的遍历方式,通常有两种:深度优先遍历、广度优先遍历
2)深度优先遍历(DepthFirstSearch),也被称为深度优先搜索,简称为DFS。【思想就是,挨个寻找,不放过任何一个死角】
3)深度优先遍历,约定右手原则:在没有碰到重复顶点的情况下,分叉路口始终是向右手边走,每路过一个顶点就做一个记号;
4)遍历时候,发现都是有记号的点,就退回上一个顶点,直到退回到一开始走的点时候,那么就表示遍历了图中每一个顶点。
5)这个遍历的过程,是一个递归的过程,并且类似一棵树的前序遍历【先根结点、左子树、右子树】过程。
6)例子:
红色表示碰到重复点的,蓝色线相连表示遍历过程的一棵树
后面设计的图存储结构代码都是没有的,但是都是可以根据前面学习的链表、数组、递归等等知识进行编写。太懒+太菜,所以直到课程学习过后四个多月都没有付之行动。
#########################
不积硅步,无以至千里
好记性不如烂笔头
感谢授课老师
截图权利归原作者所有