链表、队列和图实现BFS和DFS算法(C+造轮子+详细代码注释)
1、队列的链式存储结构
队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表。头指针指向队头节点,尾指针指向队尾节点,即单链表的最后一个节点(与顺序表存储结构不同)。
队列链式存储类型描述:
struct Node{
int data;
struct Node* next;
};
typedef struct Node iNode;
struct Queue{
iNode * front, *rear;
};
typedef struct Queue iQueue;
链式队列基本操作:
//初始化队列
void initQueue(iQueue * queue);
//判断是否为空
bool isQEmpty(const iQueue * queue);
//元素入队
void enQueue(iQueue * queue, int adddata);
//元素出队
bool deQueue(iQueue * queue, int * popdata);
//显示队列
void showQueue(const iQueue *queue);
2、图的邻接表法存储结构
当一个图为稀疏图时,使用邻接矩阵显然要浪费大量的存储空间,而图的邻接表法结合了顺序存储和链式存储方法,大大减少不必要的空间的浪费。所谓邻接表,是指对图G
中的每个顶点Vi
建立一个单链表,第i
个单链表的结点表示依附于顶点Vi
的边表(对于有向图则成为出边表)。边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种节点:顶点表结点和边表结点。图的邻接表存储结构定义:
#define MaxVertexNum 20
struct Node{
int data;
struct Node* next;
};
typedef struct Node iNode;
struct Vertex{
bool visittag ;
iNode * head ;
};
typedef struct Vertex iVertex ;
struct Graph{
iVertex graphVertex[MaxVertexNum];
int vexnum;
};
typedef struct Graph iGraph ;
图的邻接表存储方法有以下特点:
- 若
G
为无向图,则所需的存储空间为O(|V| + 2|E|)
;若G
为有向图,所需的存储空间为O(|V| + |E|)
。前者的倍数2是由于无向图中,每条边在邻接表中出现了两次。 - 对于稀疏图,采用邻接表表示将极大的节省存储空间。
- 在邻接表中,给定一节点,能很容易地找出它的所有临边,因为只需要读取它的邻接表。在邻接矩阵中,相同的操作则需要扫描一行,花费的时间为O(n)。但是,若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以迅速查到,而在邻接表中则需要在相应结点对应的边表中查找另一个结点,效率较低。
- 在有向图的邻接表表示中,求一个给定顶点的出度,只需计算其邻接表中的结点个书;但求其顶点的入度则需要遍历全部的邻接表。因此也有人采用逆邻接表的存储方式来加速求解给定顶点的入度。
- 图的邻接表表示并不唯一,因为在每个顶点对应的单链表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序。
图的一些基本操作
//从文件读边的信息 建立邻接表结构的图
void initGraph(const char * filename, iGraph* graph);
//显示邻接表
void showGraph(const iGraph * graph);
//根据将图的结点的相邻节点,以读入顺序方向存储
void reverseGraph(iGraph* graph);
//释放图分配的的空间
void freeGraph(iGraph * graph);
3、线性表的链式表示:
线性表的链时存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的元素外,还需要存放一个指向其后继的指针单链表结构中data为数据域,存放数据元素;next为指针域,存放其后继结点的地址。
单链表中结点类型的描述如下:
struct Node{
int data;
struct Node* next;
};
链表的基本操作:
//显示链表元素
void showList(iNode * head);
//采用递归反向输出列表元素&&另一种方法:列表元素入栈然后出栈
void showBackListDigui(iGraph * graph, iNode * head);
//将单链表逆置(辅助空间复杂度为O(1))
iNode * reversList(iNode * head);
//释放链表空间
void freeList(iNode * head);
4、BFS和DFS算法
算法描述
广度优先搜索(Breadth-First-Search, BFS)类似于二叉树的层序遍历算法。基本思想是:首先访问起始节点 v ,接着由 v 出发,依次访问 w1,w2,…, wi 的所有未被访问过的邻接顶点;在从这些访问过的顶点出发,访问他们所有未被访问过的邻接顶点,直至途中所有顶点都被访问。若此时图中尚有结点未被访问,则另选图中一个未被访问结点作为始点,重复上述过程,直至图中所有结点都被访问到为止。Dijistra单元最短路径算法和Prim最小生成树算法也应用了类似的思想。广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批顶点,不想深度优先搜索那样存在回退的情况,因此他不是一个递归算法。为了实现逐层的访问必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点。
深度优先搜索(Depth-First-Search,DFS) 与广度优先搜索不同,DFS类似于树的先序遍历。这种搜索算法遵循的搜索策略是尽可能深的搜索一个图。其基本思想:首先访问图中某一起始顶点 v ,然后由 v 出发,访问与 v 邻接且未被访问的任一顶点 w1 ,再访问与 w1 邻接且未被访问的任一顶点 w2 … ,重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若他还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直到图中所有顶点均被访问过谓之。
//深度优先搜索
void GraphDFS(iGraph * graph, int start);
//广度优先搜索
void GraphBFS(iGraph * graph, iQueue *queue, int start);
//访问顶点
void visitVertext(iGraph * graph, int n);
性能分析
BFS性能分析:无论是邻接表还是邻接矩阵的存储方式,BFS算法都要借助一个辅助队列 Q ,n 个顶点均需入队一次,在最坏的情况下,空间复杂度为 O(|V|) 。采用邻接表存储方式时,每个顶点均需搜索一次(或入队一次),故时间复杂度为 O(|V|) ,在搜索任一顶点的邻接点时,每条边至少访问一次,故时间复杂度为 O(|E|),算法总的时间复杂度为 O(|V| + |E|)。采用邻接矩阵存储方式时,查找每个顶点的邻接点所需的时间为 O(|V|) ,故算法总的时间复杂度为O(|V|^2)。
DFS性能分析:DFS算法是一个递归算法,需要借助一个递归工作栈,故其空间复杂度为 O(|V|) 。遍历图的过程实际上是对每个顶点查找其邻接点的过程,其耗费的时间取决于所用的存储结构。采用邻接矩阵存储方式时,查找每个顶点的邻接点所需的时间为 O(|V|) ,故算法总的时间复杂度为O(|V|^2)。采用邻接表存储方式时,查找所有顶点的邻接点所需的时间为 O(|E|) , 访问顶点所需的时间为 O(|V|),此时算法总的时间复杂度为 O(|V| + |E|)。
完整的程序如下:
#include<stdio.h>
#include<stdlib.h>
#include<stddef.h>
#define bool _Bool
#define true 1
#define false 0
#define nullptr NULL
#define MaxVertexNum 4040 //图的最多的顶点个数
/* ———————————————————Graph、Queue的结构—————————————————— */
//链表、队列的结点结构,保存图的相邻节点信息结构
struct Node{
int data;
struct Node* next;
};
typedef struct Node iNode;
//队列的基本结构
struct Queue{
//头指针和尾指针
iNode * front, *rear;
};
typedef struct Queue iQueue;
//保存图顶点信息的结构
struct Vertex{
//bool值表示当前结点是否已被遍历
bool visittag ;
//存有相邻节点信息链表的头结点
iNode * head ;
};
typedef struct Vertex iVertex ;
//图的基本结构
struct Graph{
//分配MaxVertexNum个iVertex的连续空间,存储图顶点
iVertex graphVertex[MaxVertexNum];
//图总结点个数
int vexnum;
};
typedef struct Graph iGraph ;
/* ———————————————————所有实现的函数—————————————————— */
//具体的函数功能可以查看函数具体时实现前面的说明
void showList(iNode * head);
void showBackListDigui(iGraph * graph, iNode * head);
iNode * reversList(iNode * head);
void freeList(iNode * head);
void initGraph(const char * filename, iGraph* graph);
void showGraph(const iGraph * graph);
void reverseGraph(iGraph* graph);
void freeGraph(iGraph * graph);
void initQueue(iQueue * queue);
bool isQEmpty(const iQueue * queue);
void enQueue(iQueue * queue, int adddata);
bool deQueue(iQueue * queue, int * popdata);
void showQueue(const iQueue *queue);
void freeQueue(iQueue * queue);
void addListToQueue(const iGraph * graph, iQueue* queue,iNode* head);
void GraphDFS(iGraph * graph, int start);
void GraphBFS(iGraph * graph, iQueue *queue, int start);
void visitVertext(iGraph * graph, int n);
/* ———————————————————main函数—————————————————— */
int main() {
//保存图信息的文件
const char * filename = "/home/wang/projects/workspace/main/data-bfs-shiyan.txt";
// const char * filename = "/home/wang/projects/workspace/main/data-bfs-test";
//初始化一个图grapg
iGraph *graph = (iGraph *)malloc(sizeof(iGraph));
initGraph(filename,graph);
//初始化一个队列queue
iQueue * queue = (iQueue *)malloc(sizeof(iQueue));
initQueue(queue);
//打印图的邻接表结构信息
showGraph(graph);
//将图的结点的邻接表信息反转
reverseGraph(graph);
showGraph(graph);
//深度遍历
printf("DFS........\n");
GraphDFS(graph,1);
// //广度遍历
// printf("BFS............\n");
// GraphBFS(graph,queue,1);
//释放图和队列分配的空间
freeGraph(graph); freeQueue(queue);
}
/* ———————————————————BFS和DFS算法的实现—————————————————— */
/*递归实现图的深度遍历
*假设图所有节点都是连通的
*深度优先算法DFS*/
void GraphDFS(iGraph* graph, int start)
{
//如果graph的第start个顶点未被遍历,则visit该顶点,并递归遍历该顶点的相邻节点
if(!graph->graphVertex[start].visittag){
//visit第start个顶点
visitVertext(graph,start);
//通过递归对graph的第start个顶点的相邻节点进行深度遍历;
iNode * currnode = graph->graphVertex[start].head;
while(currnode){
GraphDFS(graph,currnode->data);
currnode=currnode->next;
}
}
}
/*增加队列空间实现图的广度遍历
* 假设图所有节点都是连通的
*广度优先算法BFS*/
void GraphBFS(iGraph* graph, iQueue * queue, int start)
{
//visit初始顶点start
visitVertext(graph,start);
//初始顶点start入队
enQueue(queue,start);
//依次弹出队列中的元素并遍历,知道队列为空
while(!isQEmpty(queue)){
int popdata;
//popdata保存下一个出队的顶点
deQueue(queue,&popdata);
//遍历当前顶点的相邻顶点点
iNode * curr = graph->graphVertex[popdata].head;
while(curr){
/*如果的出队的顶点popdata的相邻顶点curr->data的visit tag为false,表示该顶点未被遍历
* 则visit该顶点curr->data,并将该顶点放入队列queue中*/
if(!graph->graphVertex[curr->data].visittag){
//visit顶点curr->data
visitVertext(graph,curr->data);
//将顶对入队queue
enQueue(queue,curr->data);
}
curr = curr->next;
}
}
printf("\n");
}
/* 实现链表节点的反转
*用到3个iNode指针nodea, nodeb, head*/
iNode * reversList(iNode* lhead)
{
//列表没有或只有一个元素时不需要处理
if(!lhead || !lhead->next)
return lhead;
//列表元素反转需要用到3个指针
iNode *head = lhead;
iNode *nodea = head->next;
iNode * nodeb = head->next;
head->next = nullptr;
//反转的辅助空间复杂度为O(1)
while(nodea){
nodeb = nodea->next;
nodea->next = head;
head = nodea;
nodea = nodeb;
}
return head;
}
/*visit graph的节点n,
*修改结点n的标记值
* 输出显示节点值*/
void visitVertext(iGraph * graph ,int nVertex)
{
//将顶点的标记值标记为true
graph->graphVertex[nVertex].visittag = true;
printf("%d ",nVertex);
}
/* ———————————————————Graph的相关函数的实现—————————————————— */
/*从文本中读取图结点间边的信息并建图
*以邻接表的形式保存图信息*/
void initGraph(const char* filename, iGraph* graph)
{
//初始化Graph的空间和值
graph->vexnum = MaxVertexNum;
int i =0 ;
while(i < graph->vexnum){
graph->graphVertex[i].head = nullptr;
graph->graphVertex[i++].visittag = false;
}
//从文件中读取图的边的信息,并构建邻接矩阵
int vernum, adjnum;
FILE * filep = fopen(filename, "r");
if(filep){
while(fscanf(filep, "%d %d" ,&vernum,&adjnum)!= EOF){
iNode * addadjnode = (iNode *)malloc(sizeof(iNode));
if(addadjnode){
addadjnode->data = adjnum;
addadjnode->next = graph->graphVertex[vernum].head;
graph->graphVertex[vernum].head = addadjnode;
}
}
graph->vexnum = vernum ;
}
else printf("file erro");
fclose(filep);
}
/* 将图所有Vetex的相邻节点值的列表反转 */
void reverseGraph(iGraph* graph)
{
int i = 0;
while(i < graph->vexnum){
graph->graphVertex[i].head = reversList(graph->graphVertex[i].head);
i++;
}
}
/* ———————————————————Queue的相关函数的实现—————————————————— */
/* 初始化队列queue */
void initQueue(iQueue* queue)
{
iNode * ihead = (iNode *) malloc (sizeof(iNode));
ihead->data = rand()%10;
ihead->next = nullptr;
queue->front = ihead;
queue->rear = ihead;
}
/* 队列非空时弹出front指向的节点
* 将节点data传给popdata
*释放队列弹出的值的空间*/
bool deQueue(iQueue* queue, int* popdata)
{
//出队时先判断当前队列是否为空
if(isQEmpty(queue))
return false;
//popdata记录出队值
iNode * qhead = queue->front->next;
*popdata = qhead->data;
//队列头指针后移并释放空间
queue->front ->next = qhead->next;
if (qhead == queue->rear)
queue->rear = queue->front;
free(qhead);
return true;
}
/* 将adddata入队列queue
* 创建节点插入到队尾*/
void enQueue(iQueue* queue, int adddata)
{
//创建新结点保存入队数据
iNode * addnode = (iNode *)malloc(sizeof(iNode));
addnode->next = nullptr;
addnode->data = adddata;
//将入队结点插入到队列的尾指针后面
queue->rear->next = addnode;
//更新尾指针
queue->rear = addnode;
}
/* 判断队列queue是否为空
*空->true 非空->false*/
bool isQEmpty(const iQueue* queue)
{
//三目运算符判断队列是否为空
return (queue->front==queue->rear)?true:false;
}
/* ———————————————————List的相关函数的实现—————————————————— */
/* 递归算法 反向输出链表中每个节点的值*/
void showBackListDigui(iGraph * graph,iNode* head)
{
//如若当前非最后一个元素,继续递归
if(head->next)
showBackListDigui(graph,head->next);
if(head) visitVertext(graph,head->data);
}
/* 没什么大的作用,把图的某个节点的所有相邻节点放到队列中*/
void addListToQueue(const iGraph * graph, iQueue* queue, iNode* head)
{
showList(head);
iNode * lhead = head;
while(lhead){
if(!graph->graphVertex[lhead->data].visittag)
enQueue(queue,lhead->data);
lhead = lhead->next;
}
showQueue(queue);
}
/* ———————————————List、Queue、Graph信息显示和空间释放——————————————— */
/* 打印list元素的值 */
void showList(iNode* head)
{
iNode * currnode = head;
while(currnode){
printf("%d ",currnode->data);
currnode = currnode->next;
}
printf("\n");
}
/*显示当前Queue的值
*queue 为空显示 queue empty
* 非空传入头节点,作为链表输出元素值*/
void showQueue(const iQueue* queue)
{
if(isQEmpty(queue))
printf("queue empty\n");
else {
showList(queue->front->next);
}
}
/* 打印构建的图的值 */
void showGraph(const iGraph* graph)
{
int i = 0;
printf("graph vertex %d\n",graph->vexnum);
while(i < graph->vexnum){
printf("vertex %d:",i);
showList(graph->graphVertex[i].head);
i++;
}
}
/* 释放List的分配的空间*/
void freeList(iNode * head){
// printf("free list\n") ;
iNode * curnode ;
while(head){
curnode = head;
head = head->next ;
free(curnode);
}
}
/* 释放Queue分配的空间 */
void freeQueue(iQueue * queue){
iNode *head ;
printf("free queue\n");
freeList(queue->front);
free(queue);
}
/* 释放Graph分配的空间 */
void freeGraph(iGraph* graph)
{
printf("free graph\n");
int i = 0;
while(i < graph->vexnum){
freeList(graph->graphVertex[i].head);
i++;
}
free(graph);
}
程序运行结果:
测试数据:
0 1
0 4
0 7
0 2
0 3
0 6
1 0
1 2
1 3
2 1
2 3
2 7
3 1
3 2
4 0
4 5
4 6
5 4
6 4
7 0
7 2