前面两章我们讲解了数据结构中的线性结构--线性表、栈和队列,这章开始以及下一章我们将讲解非线性结构树和图。
什么是树呢?树很好地反应了一种层次结构,例如下图,这就是一种树形结构,它有很多结点组成,最上面的实验楼课程结点称为树的根,结点拥有的直接子节点数称为结点的度,度为0的结点称为叶子,例如C语言、评估课这些结点,而树的度是所有结点的度中的最大值,这颗树的度就是3,一个结点的直接子结点称为它的孩子,项目课结点的孩子就是制作Markdown预览器结点,相应地项目课结点就是制作Markdown预览器结点的双亲,相同双亲的孩子结点互称为兄弟,例如C语言结点和Linux入门结点,一个结点的祖先是从根到该结点所经过的所有结点,C语言结点的祖先就是基础课和实验楼课程结点,一个结点下的所有结点称为该结点的子孙,例如实验楼课程下的所有结点都是它的子孙。树有层次之分,根记为第一层,依次类推,例如这棵树的最大层次就是3,也称为该树的深度,双亲在同一层的结点互称为堂兄弟,例如Linux入门结点和制作Markdown预览器结点。
上面介绍了树,接下来我们介绍一种很常用的树结构--二叉树,它的特点是一个结点的直接子节点最多只能有两个,并且有左右之分。在二叉树中有种常见的称为完全二叉树的结构,它的特点是除最后一层外每一层的结点数为2i-1,最后一层的结点数若不满足2i-1,那么最后一层的结点是自左向右排列的,如下图。
二叉树也有顺序存储结构和链式存储结构两种,这里我们就讲下链式存储结构的代码实现(主要操作):
#include
#include
#define TRUE 1
#define FALSE 0
#define OVERFLOW -2
#define OK 1
#define ERROR 0
typedef int Status;
typedef int TElemType;
/*
* 存储结构
*/
typedef struct BiTNode
{
TElemType data; //数据
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
/*
* 创建二叉树,输入0表示创建空树
*/
Status CreateBiTree(BiTree *T)
{
TElemType e;
scanf("%d", &e);
if (e == 0)
{
*T = NULL;
}
else
{
*T = (BiTree) malloc(sizeof(BiTNode));
if (!T)
{
exit(OVERFLOW);
}
(*T)->data = e;
CreateBiTree(&(*T)->lchild); //创建左子树
CreateBiTree(&(*T)->rchild); //创建右子树
}
return OK;
}
/*
* 访问元素
*/
void visit(TElemType e)
{
printf("%d ", e);
}
/*
* 先序遍历二叉树:指先访问根,然后访问孩子的遍历方式
*/
Status PreOrderTraverse(BiTree T, void (*visit)(TElemType))
{
if (T)
{
visit(T->data);
PreOrderTraverse(T->lchild, visit);
PreOrderTraverse(T->rchild, visit);
}
}
/*
* 中序遍历二叉树:指先访问左(右)孩子,然后访问根,最后访问右(左)孩子的遍历方式
*/
Status InOrderTraverse(BiTree T, void (*visit)(TElemType))
{
if (T)
{
InOrderTraverse(T->lchild, visit);
visit(T->data);
InOrderTraverse(T->rchild, visit);
}
}
/*
* 后序遍历二叉树:指先访问孩子,然后访问根的遍历方式
*/
Status PostOrderTraverse(BiTree T, void (*visit)(TElemType))
{
if (T)
{
PostOrderTraverse(T->lchild, visit);
PostOrderTraverse(T->rchild, visit);
visit(T->data);
}
}
int main()
{
BiTree T;
printf("创建树,输入0为空树:\n");
CreateBiTree(&T);
printf("先序遍历:");
PreOrderTraverse(T, *visit);
printf("\n中序遍历:");
InOrderTraverse(T, *visit);
printf("\n后序遍历:");
PostOrderTraverse(T, *visit);
printf("\n");
return 0;
}
上面我们讲了二叉树的一些主要操作,其实它的操作远不止这些,例如你可以试试把遍历改为非递归实现、求树的深度等等一些操作。除了上面实现的基本二叉树之外,还有一种线索二叉树,它其实就是用结点空的指针域来指向它的前驱或者后继结点,不浪费空的指针域,如果你想深入了解,可以查查资料。
堆是一种经过排序的完全二叉树,其中任一非叶子节点的值均不大于(或不小于)其左孩子和右孩子节点的值。
最大堆和最小堆是二叉堆的两种形式。
最大堆:根结点的键值是所有堆结点键值中最大者。
最小堆:根结点的键值是所有堆结点键值中最小者。
而最大-最小堆集结了最大堆和最小堆的优点,这也是其名字的由来。
最大-最小堆是最大层和最小层交替出现的二叉树,即最大层结点的儿子属于最小层,最小层结点的儿子属于最大层。
以最大(小)层结点为根结点的子树保有最大(小)堆性质:根结点的键值为该子树结点键值中最大(小)项。
最小堆
最大堆
二叉排序树又称二叉查找树,亦称二叉搜索树,如下图所示,它主要用于查找。 它或者是一棵空树;或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
平衡二叉树又被称为AVL树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树,如下图,由它可以生成平衡二叉搜索树,查找效率会更高。构造与调整方法平衡二叉树的常用算法有红黑树、AVL、Treap等。最小二叉平衡树的节点的公式如下F(n)=F(n-1)+F(n-2)+1这个类似于一个递归的数列,可以参考Fibonacci数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。
哈夫曼树也称最优二叉树,它是带权路径长度最小的二叉树。下面我就通过一个例子让大家快速地明白,相信大家都看过抗日电视剧,打仗的时候,前线要与后方指挥部取得联系通常都会使用电报,那么电报编码后的长度当然是越短越好,但同时翻译电报时又不能造成歧义,这时候就可以使用哈夫曼树来编码,那么怎么实现呢?
哈夫曼树的构造步骤如下:
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…、wn看成是有n 棵树的集合(每棵树仅有一个结点);
(2) 在集合中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从集合中删除选取的两棵树,并将新树加入集合;
(4)重复(2)、(3)步,直到集合中只剩一棵树为止,该树即为所求得的哈夫曼树。
比如需要发送“goodgoodstudy”,我们先计算每个字母出现的次数即权值,g:2、o:4、d:3、s:1、t:1、u:1、y:1,然后通过哈夫曼树的构造规则构造出哈夫曼树,如下图。
通过构造哈夫曼树我们就能得到每个字母的编码,g:010、o:00、d:10、s:0110、t:0111、u:110、y:111,这就能使编码总长度最小,此种编码就是著名的哈夫曼编码。
本章我们讲了非线性结构树、二叉树以及哈夫曼树(最优二叉树),树结构体现的是一种层次结构,二叉树结点的直接子节点最多只能有两个,可以解决表达式求值等问题。堆是一种经过排序的完全二叉树,其中任一非叶子节点的值均不大于(或不小于)其左孩子和右孩子节点的值,堆有最大堆、最小堆和最大-最小堆。二叉搜索树和平衡二叉树主要用于查找,还有B-树和B+树也是用于查找,它们主要应用于文件系统。哈夫曼树是一种带权路径长度最小的树,哈夫曼编码就是由它而得名
前面已经讲了几种线性结构和树形结构,本章来讲解比它们更为复杂的图结构。在线性结构中,元素之间是一种线性关系,只有一个直接前驱和一个直接后继,而树结构体现的是一种层次关系,在图中每个元素之间都可能是有关联的。
下面就通过一个例子来让大家快速地知道什么是图,如下图所示,G1是有向图,G2是无向图,每个数据元素称为顶点,在有向图中,从V1到V3称为一条弧,V3到V1为另一条弧,V1称为弧尾,V3称为弧头,在无向图中,从V1到V3称为一条边。有n个顶点,1/2n(n-1)条边的无向图称为完全图,有n(n-1)条弧有向图称为有向完全图,有很少条边或图称为稀疏图,反之称为稠密图。在G2无向图中,类似V3与V1、V2和V4之间有边的互称为邻接点,与顶点相关联的边数称为顶点的度,例如V3顶点的度为3,而在G1有向图中,顶点的度是顶点的出度和入度之和,以顶点为头的弧的数目称为入度,为尾的弧的数目称为出度,例如V1顶点的出度为2,入度为1,它的度为1+2=3。从一个顶点到另一个顶点的顶点序列称为路径,在有向图中,路径是有方向的,路径上边或弧的数目称为路径的长度,如果一条路径中的起始顶点跟结束结点相同,那么称这个路径为环或回路,不出现重复顶点的路径称为简单路径。无向图中,如果一个顶点到另一个顶点有路径,那么它们就是连通的,如果图中的任意两个顶点都是连通的,那么这个图就是连通图,无向图中的极大连通子图称为连通分量,如果是有向图中的任意一对顶点都有路径,那么这个就是强连通图,相应的它的极大连通子图就称为强连通分量。一个连通图的一个极小连通子图,它包含所有顶点,但足以构成一棵树的n-1条边,加一条边必定会形成环,这个就称为*生成树。
表示图通常有四种方法--数组表示法、邻接表、十字链表和邻接多重表。邻接表是图的一种链式存储结构,十字链表是有向图的另一种链式存储结构,邻接多重表是无向图的另一种链式存储结构。这里主要讲解一下邻接表的表示和实现,邻接表中有两种结点,一种是头结点,另一种是表结点,头结点中存储一个顶点的数据和指向链表中第一个结点,表结点中存储当前顶点在图中的位置和指向下一条边或弧的结点,表头结点用链式或顺序结构方式存储,如下图所示就是上图G2无向图的邻接表表示。
通常图的遍历有两种:深度优先搜索和广度优先搜索。
深度优先搜索是树的先根遍历的推广,它的基本思想是:从图G的某个顶点v0出发,访问v0,然后选择一个与v0相邻且没被访问过的顶点vi访问,再从vi出发选择一个与vi相邻且未被访问的顶点vj进行访问,依次继续。如果当前被访问过的顶点的所有邻接顶点都已被访问,则退回到已被访问的顶点序列中最后一个拥有未被访问的相邻顶点的顶点w,从w出发按同样的方法向前遍历,直到图中所有顶点都被访问。
广度优先搜索是树的按层次遍历的推广,它的基本思想是:首先访问初始点vi,并将其标记为已访问过,接着访问vi的所有未被访问过的邻接点vi1,vi2,…, vin,并均标记已访问过,然后再按照vi1,vi2,…, vin的次序,访问每一个顶点的所有未被访问过的邻接点,并均标记为已访问过,依次类推,直到图中所有和初始点vi有路径相通的顶点都被访问过为止。
如下图
深度优先搜索:0->1->3->7->4->2->5->6
广度优先搜索:0->1->2->3->4->5->6->7
下面是邻接表的创建和图的遍历的代码实现:
#include
#include
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define OVERFLOW -2
#define MAX_NUM 20
typedef int Status;
typedef int QElemType;
typedef char VexType;
/*
* 邻接表存储结构
*/
typedef struct EdgeNode
{
int adjvex; //顶点的位置
struct EdgeNode *next; //指向下一条边的指针
}EdgeNode, *EdgeLink;
typedef struct VexNode
{
VexType data; //顶点数据
EdgeNode *firstEdge; //指向第一条依附该顶点的边的指针
}VexNode, AdjList[MAX_NUM];
typedef struct
{
AdjList adjList;
int vexNum, edgeNum; //顶点数和边数
}ALGraph;
/*
* 队列存储结构(用于图的遍历)
*/
typedef struct QNode
{
QElemType data; //结点数据
struct QNode *next; //指向下一个结点
}QNode, *QueuePtr;
typedef struct
{
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
/*
* 初始化队列
*/
Status InitQueue(LinkQueue *Q)
{
Q->front = Q->rear = (QueuePtr) malloc(sizeof(QNode));
if (!Q->front)
{
exit(OVERFLOW);
}
Q->front->next = NULL;
return OK;
}
/*
* 判断队列是否为空
*/
Status IsEmpty(LinkQueue Q)
{
if (Q.front->next == NULL)
{
return TRUE;
}
else
{
return FALSE;
}
}
/*
* 入队
*/
Status EnQueue(LinkQueue *Q, QElemType e)
{
QueuePtr p = (QueuePtr) malloc(sizeof(QNode));
if (!p)
{
exit(OVERFLOW);
}
p->data = e;
p->next = NULL;
Q->rear->next = p;
Q->rear = p;
return OK;
}
/*
* 出队
*/
Status DeQueue(LinkQueue *Q, QElemType *e)
{
QueuePtr p;
if (Q->front == Q->rear)
{
return ERROR;
}
p = Q->front->next;
*e = p->data;
Q->front->next = p->next;
if (Q->rear == p)
{
Q->rear = Q->front;
}
free(p);
return OK;
}
/*
* 创建图
*/
Status CreateGraph(ALGraph *G)
{
int i, j, k;
EdgeLink e;
printf("请输入顶点数目和边数:\n");
scanf("%d", &G->vexNum);
scanf("%d", &G->edgeNum);
getchar();
printf("请输入各顶点的数据:\n");
for (i = 0; i < G->vexNum; i++)
{
scanf("%c",&G->adjList[i].data);
if (G->adjList[i].data == '\n')
{
i--;
continue;
}
G->adjList[i].firstEdge = NULL;
}
printf("请依次输入边(Vi,Vj)的顶点序号:\n");
for (k = 0; k < G->edgeNum; k++)
{
scanf("%d", &i);
scanf("%d", &j);
e = (EdgeLink) malloc(sizeof(EdgeNode));
e->adjvex = j;
e->next = G->adjList[i].firstEdge;
G->adjList[i].firstEdge = e;
e = (EdgeLink) malloc(sizeof(EdgeNode));
e->adjvex = i;
e->next = G->adjList[j].firstEdge;
G->adjList[j].firstEdge = e;
}
return OK;
}
int visited[MAX_NUM]; //用于记录遍历状态
/*
* 递归从第i个结点深度优先遍历图
*/
void DFS(ALGraph G, int i)
{
EdgeLink p;
visited[i] = TRUE;
printf("%c ", G.adjList[i].data);
p = G.adjList[i].firstEdge;
while (p)
{
if (!visited[p->adjvex])
{
DFS(G, p->adjvex);
}
p = p->next;
}
}
/*
* 深度优先遍历
*/
Status DFSTraverse(ALGraph G)
{
int i;
for (i = 0; i < MAX_NUM; i++)
{
visited[i] = FALSE;
}
for (i = 0; i < G.vexNum; i++)
{
if (!visited[i])
{
DFS(G, i);
}
}
return OK;
}
/*
* 广度优先遍历
*/
Status BFSTraverse(ALGraph G)
{
int i;
EdgeLink p;
LinkQueue Q;
InitQueue(&Q);
for (i = 0; i < MAX_NUM; i++)
{
visited[i] = FALSE;
}
for (i = 0; i < G.vexNum; i++)
{
if (!visited[i])
{
visited[i] = TRUE;
printf("%c ", G.adjList[i].data);
EnQueue(&Q, i);
while (!IsEmpty(Q))
{
DeQueue(&Q, &i);
p = G.adjList[i].firstEdge;
while (p)
{
if (!visited[p->adjvex])
{
visited[p->adjvex] = TRUE;
printf("%c ", G.adjList[p->adjvex].data);
EnQueue(&Q, p->adjvex);
}
p = p->next;
}
}
}
}
return OK;
}
int main()
{
ALGraph G;
CreateGraph(&G);
printf("深度优先遍历:");
DFSTraverse(G);
printf("\n广度优先遍历:");
BFSTraverse(G);
printf("\n");
}
一个有n个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有n个结点,并且有保持图连通的最少的边。最小生成树可以用kruskal(克鲁斯卡尔)算法或Prim(普里姆)算法求出。
应用:例如要在n个城市之间铺设光缆,主要目标是要使这n个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。
参考自百度百科。
拓扑排序简单地说,就是在有向图中,想访问一个顶点需要先访问它的所有前驱顶点。它的执行步骤为:
如上图,它的拓扑序列就为:
Linux基础入门->Vim编辑器->Git Community Book->HTML基础入门->SQL基础课程->MySQL参考手册中文版->Python编程语言->Python Flask Web框架->Flask开发轻博客
最短路径问题是图论研究中的一个经典算法问题,旨在寻找图(由结点和路径组成的)中两结点之间的最短路径。Dijkstra(迪杰斯特拉)算法是典型的最短路径路由算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。Dijkstra算法能得出最短路径的最优解,但由于它遍历计算的节点很多,所以效率低。
其采用的是贪心法的算法策略,大概过程为先创建两个表,OPEN和CLOSE表,OPEN表保存所有已生成而未考察的节点,CLOSED表中记录已访问过的节点,然后:
1. 访问路网中距离起始点最近且没有被检查过的点,把这个点放入OPEN组中等待检查。
2. 从OPEN表中找出距起始点最近的点,找出这个点的所有子节点,把这个点放到CLOSE表中。
3. 遍历考察这个点的子节点。求出这些子节点距起始点的距离值,放子节点到OPEN表中。
4. 重复第2和第3步,直到OPEN表为空,或找到目标点。