图的遍历、深度优先DFS、广度优先BFS

【数据结构】——图的遍历

系列文章目录

目录

【数据结构】——图的遍历

系列文章目录

前言

一、图的遍历

二、深度优先DFS

1.什么是深度优先

2.代码实现

三、

总结



前言

无论是什么程序都要和数据打交道,一个好的程序员会选择更优的数据结构来更好的解决问题,因此数据结构的重要性不言而喻。数据结构的学习本质上是让我们能见到很多前辈在解决一些要求时间和空间的难点问题上设计出的一系列解决方法,我们可以在今后借鉴这些方法,也可以根据这些方法在遇到具体的新问题时提出自己的解决方法。(所以各种定义等字眼就不用过度深究啦,每个人的表达方式不一样而已),在此以下的所有代码都是仅供参考,并不是唯一的答案,只要逻辑上能行的通,写出来的代码能达到相同的结果,并且在复杂度上差不多,就行了。


一、图的遍历

之前我们讲了用多种方法来实现图的存储,那么当图被创建出来后要怎么去遍历呢?我们知道像邻接矩阵之类的存储结构的核心是通过数组实现的,那么在遍历邻接矩阵的时候只需要用循环遍历数组一样遍历它就行了;对于像邻接表之类的存储结构的核心是使用链表实现的,那么在遍历的时候,我们可以用遍历链表一样的方法。

但是这样在实际运用中会很麻烦,那么有没有一种不用区分图的存储结构,都能实现图的遍历的结构呢?

解决这个问题之前,我们要清楚什么是图的遍历。简单而言就是从某一个顶点出发,按照某种搜索方式把图中顶点都访问一遍且仅访问一遍,这个过程就称之为图的遍历。

根据图的搜索思想不同,又分为了两种搜索算法:深度优先DFS和广度优先BFS

二、深度优先DFS

1.什么是深度优先

其实在树的学习中,我们已经接触了深度优先的思想,只不过那时候还没有系统的学习罢了,在树中,树的中序、后序遍历方法都是深度优先思想,那么回想一下树的遍历,是不是从根节点开始,一直往一个方向走去遍历?这和图中的深度优先查找是一样的。

图的遍历、深度优先DFS、广度优先BFS_第1张图片

 对于这个无向图,我们模仿树的遍历,假定从节点A为中心开始,沿一个方向持续遍历没有遍历过的节点,这里我们往左走,可以得到顶点的访问顺序是A-> B-> C-> D-> E-> F-> G-> H,即下图所示

图的遍历、深度优先DFS、广度优先BFS_第2张图片

 这个时候访问的最后一个节点 H 周围的所有邻接节点都被访问过了,但是可以发现还有一个节点 i 没有被访问,这个时候就要回溯,从节点 H 开始往回走,同时查找该节点有无没被访问过的邻接节点,当回溯到节点 D 的时候发现节点 I 还没有被访问过,于是访问节点 I 并再次回溯,最后发现所有节点都被访问过了,并且只被访问了一次,结束。

在遍历的过程中,我们用到了回溯,那么从代码上看,只有 递归 或者 栈 的思想才能实现。

2.代码实现

因为DFS适用于任何一种图的存储结构,所以这里我用邻接矩阵的方式创建图(主要是简单^_^)

//DFS深度优先算法遍历无向图
//以邻接矩阵为例创建图
#include 
#include 
#define MaxVertices 100
//邻接矩阵
typedef struct AdjanceMatrix
{
    //顶点集
    int Vertices[MaxVertices];
    //边集
    int Edge[MaxVertices][MaxVertices];
    //顶点数 边数
    int numV, numE;
}AdjMatrix;

void creategrahp(AdjMatrix* G)
{
    int n, e;//n代表顶点数 e代表边数
    int vi, vj;//vi vj代表边的两个顶点对
    printf("要输入的顶点数和边数\n");
    scanf_s("%d%d", &n, &e);
    G->numV = n;
    G->numE = e;
    //图的初始化
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            if (i == j)
            {
                //一个非带权的图 0代表没有边 1代表有边
                //边不指向自己 即对角线为0
                G->Edge[i][j] = 0;
            }
            else
            {
                //如果是带权的图 初始化为0或者为一个不可能的值
                G->Edge[i][j] = 65535;
            }
        }
    }
    //将顶点存入数组
    for (int i = 0; i < G->numV; i++)
    {
        printf("请输入第%d个节点的信息\n", i + 1);
        scanf_s("%d", &G->Vertices[i]);
    }
    //输入边的信息
    for (int i = 0; i < G->numE; i++)
    {
        //如果输入的是顶点的值 需要从顶点集中查找对应的下标 
        //如果是带权图 还要输入权的信息
        printf("请输入边的信息Vi,Vj\n");
        scanf_s("%d%d", &vi, &vj);
        G->Edge[vi - 1][vj - 1] = 1;
        //如果是带权图 等于权值
        //如果是有向图 就不对称
        //如果是无向图 矩阵对称
        G->Edge[vj - 1][vi - 1] = 1;
    }
}

//用DFS遍历无向图
//需要有个数组判断顶点是否被访问 1代表被访问
int Visit[MaxVertices] = { 0 };

//对于连通分量进行DFS
void DFStraverse(AdjMatrix* G, int i)
{
    printf("%d", G->Vertices[i]);//每递一次 输出没有被访问过的节点 
    //i是传进的初始顶点 j是与之邻接的顶点
    for (int j = 0; j < G->numV; j++)//递:循环去找该节点的所有邻接节点 跳出条件:直到某个节点找不到没被访问过的邻接点
    {
        //归:每一趟循环找没有被访问过的节点
        if (G->Edge[i][j] && !Visit[j])
        {
            Visit[j] = 1;//标记未访问的节点为已访问的
            DFStraverse(G, j);//递的过程
        }
    }
}

//对图进行深度优先查找
void DFS(AdjMatrix* G)
{
    for (int i = 0; i < G->numV; i++)
    {
        //如果这个节点没有被访问过
        if (!Visit[i])
        {
            Visit[i] = 1;
            DFStraverse(G, i);
        }
    }    
}

int main()
{
    AdjMatrix* g = (AdjMatrix*)malloc(sizeof(AdjMatrix));
    creategrahp(g);
    DFS(g);

    return 0;
}

步骤总结:

1、选择一个未被访问过的节点作为起始节点

2、搜索这个节点的所有邻接节点,访问没有访问过的节点并做标记

3、遍历某一个节点发现这个节点的所有邻接节点都被访问过

那么开始回溯遍历所有邻接点 找出剩下没有被访问过的节点

三、广度优先BFS

1.什么是广度优先

在上文中,我们用树的中序遍历类比了图中的深度优先,事实上,在树的学习中,我们同样也接触过了广度优先的思想——在树的层序遍历中,就是BFS的思想。

图的遍历、深度优先DFS、广度优先BFS_第3张图片

还是这样的一个图,还是假定从节点 A 出发,访问A及其所有未被访问的邻接点:

B F

然后再访问B和F所有未被访问的邻接点:

C I G E

然后再访问 C I G E 的所有未被访问的邻接点:

D H

至此所有节点都被访问过了,且仅访问了一次,结束。

广度优先的思想十分简单,在只要类比树中的层序遍历即可。那么回想一下,在树的层序遍历中,是不是用到了队列?那么在图的BFS中,也可以用队列记录哪些节点被遍历了,那么不在队列中的就是没有被遍历过的。

2.代码实现

//广度优先搜索
#include 
#include 
#include 
#include 
#define MaxVertices 100
//邻接矩阵
typedef struct AdjanceMatrix
{
    //顶点集
    int Vertices[MaxVertices];
    //边集
    int Edge[MaxVertices][MaxVertices];
    //顶点数 边数
    int numV, numE;
}AdjMatrix;

void creategrahp(AdjMatrix* G)
{
    int n, e;//n代表顶点数 e代表边数
    int vi, vj;//vi vj代表边的两个顶点对
    printf("要输入的顶点数和边数\n");
    scanf_s("%d%d", &n, &e);
    G->numV = n;
    G->numE = e;
    //图的初始化
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            if (i == j)
            {
                //一个非带权的图 0代表没有边 1代表有边
                //边不指向自己 即对角线为0
                G->Edge[i][j] = 0;
            }
            else
            {
                //如果是带权的图 初始化为0或者为一个不可能的值
                G->Edge[i][j] = 65535;
            }
        }
    }
    //将顶点存入数组
    for (int i = 0; i < G->numV; i++)
    {
        printf("请输入第%d个节点的信息\n", i + 1);
        scanf_s("%d", &G->Vertices[i]);
    }
    //输入边的信息
    for (int i = 0; i < G->numE; i++)
    {
        //如果输入的是顶点的值 需要从顶点集中查找对应的下标 
        //如果是带权图 还要输入权的信息
        printf("请输入边的信息Vi,Vj\n");
        scanf_s("%d%d", &vi, &vj);
        G->Edge[vi - 1][vj - 1] = 1;
        //如果是带权图 等于权值
        //如果是有向图 就不对称
        //如果是无向图 矩阵对称
        G->Edge[vj - 1][vi - 1] = 1;
    }
}

typedef int QDatatype;

typedef struct Queuenode//队列的节点
{
	struct Queuenode* next;
	QDatatype data;

}Qnode;

typedef struct Queue//指向队列的指针
{
	Qnode* head;
	Qnode* tail;
}PQ;

void Queue_Init(PQ* pq)
{
	assert(pq);

	pq->head = pq->tail = NULL;
}

void Queue_Destory(PQ* pq)
{
	assert(pq);

	Qnode* cur = pq->head;

	while (cur)
	{
		Qnode* next = cur->next;
		free(cur);
		cur = next;
	}

	pq->head = pq->tail = NULL;
}

void Queue_Push(PQ* pq, QDatatype x)
{
	assert(pq);

	Qnode* newnode = (Qnode*)malloc(sizeof(Qnode));
	if (newnode == NULL)
	{
		printf("Malloc Fail!\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;

	if (pq->head == NULL)
	{
		pq->head = pq->tail = newnode;
	}

	else
	{
		pq->tail->next = newnode;
		pq->tail = newnode;
	}
}

void Queue_pop(PQ* pq)
{
	assert(pq);
	assert(pq->head);

	if (pq->head->next == NULL)
	{
		free(pq->head);
		pq->head = pq->tail = NULL;
	}

	else
	{
		Qnode* next = pq->head->next;
		free(pq->head);
		pq->head = next;
	}
}

QDatatype Queue_Front(PQ* pq)
{
	assert(pq);
	assert(pq->head);

	return pq->head->data;
}

QDatatype Queue_Back(PQ* pq)
{
	assert(pq);

	return pq->tail->data;
}

int Queue_Size(PQ* pq)
{
	assert(pq);

	int size = 0;
	Qnode* cur = pq->head;
	while (cur = NULL)
	{
		size++;

		cur = cur->next;
	}
	return size;
}

bool Queue_Empty(PQ* pq)
{
	assert(pq);

	return pq->head == NULL;
}

//寻找二维数组中值所对应的下标
int LocateVex(AdjMatrix* G, int val)
{
	int i = 0;
	for (; i < G->numV; i++)
	{
		if (G->Vertices[i] == val)
		{
			break;
		}
	}
	return i;
}

//查找第一个与数组下标u的顶点之间有关的顶点的下标
int FirstAdjvex(AdjMatrix* G, int v)
{
	for (int i = 0; i < G->numV; i++)
	{
		if (G->Edge[v][i])
		{
			return i;
		}
	}
	return -1;
}

//从前一个访问位置w的下一个位置开始 查找与之有边的顶点
int NextAdjvex(AdjMatrix* G, int v, int w)
{
	for (int i = w + 1; i < G->numV; i++)
	{
		//遍历所有与顶点v连通的节点(邻接点)
		if (G->Edge[v][i])
		{
			return i;
		}
	}
	return -1;
}

int visit[20] = { 0 };
void BFSTraverse(AdjMatrix* G)
{
	PQ q;
	Queue_Init(&q);
	for (int i = 0; i < G->numV; i++)
	{
		//持续的去判断节点有没有被访问过 循环结束那么所有的点一定会遍历
		if (!visit[i])
		{
			visit[i] = 1;
			//输出已访问的节点
			printf("%d", G->Vertices[i]);
			Queue_Push(&q, G->Vertices[i]);
			if (!Queue_Empty(&q))
			{
				//只要队列不为空
				//把第一个元素出队 把相邻的顶点入队
				int u;//接收出队元素的值
				u = Queue_Front(&q);
				//出队
				Queue_pop(&q);
				u = LocateVex(G, u);
				//查找所有与数组下标u的顶点之间有关的顶点
				for (int w = FirstAdjvex(G, u); w >= 0; w = NextAdjvex(G, u, w))
				{
					if (!visit[w])
					{
						visit[w] = 1;
						printf("%d", G->Vertices[w]);
						Queue_Push(&q, G->Vertices[w]);
						Queue_pop(&q);
					}
				}
			}
		}
	}
}

int main()
{
    AdjMatrix* g = (AdjMatrix*)malloc(sizeof(AdjMatrix));
    creategrahp(g);
	BFSTraverse(g);

    return 0;
}

总结

DFS的优点:

1.不用开辟额外空间;

2.适合去搜索全部的解,包括如果求解的目标必须要走到深处(如树的叶子);

BFS的优点:

是找最短路径最优解;

缺点:需要持续的去判重;

你可能感兴趣的:(数据结构,C语言,算法,深度优先,广度优先,c语言,数据结构)