学習目標:グラフは私たちが学ぶ最も複雑なデータ構造なので、それを理解しなければなりません。
学習課題:
コードの説明:
- 深さ優先では再帰を使用します。
- 幅優先スタックは実際にはバイナリ ツリーのトラバースと非常によく似ており、キューもそこからコピーされて変更されます。
勉強の時間:
2022.6.5
1 つの完全なコード
#include <stdio.h>
#include <malloc.h>
#define MAXSIZE 10
/*这个数组是用来记录某位置是否被访问过了,为什么要记录呢,
因为图不像二叉树那样,由上往下只能通过双亲到达儿子,并且每一个
结点只有一个双亲,图的话,可以很有可能可以通过多个结点访问到你。
所以是否被访问过了我们是很有必要记录的。这种在各个函数都会用
到,并且随时会更改的变量设置为全局变量很舒服*/
int* visited;
/*这里我们用的是 n×n的布尔矩阵来表示每两个点之间的连通性
第n行就代表第n个元素分别与其他元素(包括自己)是否连通*/
typedef struct graph
{
int** connections;
int numNodes;
}*graphPtr;
/*这里是我们在广度遍历要用到的循环队列的相关操作*/
typedef struct graphQueue
{
int *node;
int front;
int rear;
}*graphQueuePtr;
/**
* @brief 队列初始化
*
* @return
*/
graphQueuePtr queueInit()
{
graphQueuePtr resultQueue = (graphQueuePtr)malloc(sizeof(struct graphQueue));
resultQueue->node = (int*)malloc(MAXSIZE * sizeof(int));
resultQueue->front = 0;
resultQueue->rear = 0;
return resultQueue;
}
/**
* @brief 入队
*
* @param paraData
* @param paraQueue
*/
void enqueue(graphQueuePtr paraQueue,int paraData)
{
if((paraQueue->rear + 1) % MAXSIZE == (paraQueue->front) % MAXSIZE)
{
printf("队列满了,无法入队!\n");
return ;
}
paraQueue->node[(paraQueue->rear) % MAXSIZE] = paraData;
paraQueue->rear = (paraQueue->rear + 1) % MAXSIZE;
}
/**
* @brief 出队
*
* @param paraQueue
*
* @return
*/
int dequeue(graphQueuePtr paraQueue)
{
if(paraQueue->front == paraQueue->rear)
{
printf("队列为空,无法出队!\n");
return 0;
}
int temp = paraQueue->node[(paraQueue->front) % MAXSIZE];
paraQueue->front = (paraQueue->front + 1) % MAXSIZE;
return temp;
}
/**
* @brief 这里就是把记录是否访问的数组初始化一下
*
* @param paraGraph
*/
void visitedInit(graphPtr paraGraph)
{
int i;
visited = (int*)malloc(paraGraph->numNodes * sizeof(int));
for(i = 0;i < paraGraph->numNodes;i ++)
{
visited[i] = 0;
}
}
/**
* @brief 传入一个动态二维数组和元素个数,然后初始化的时候将其拷贝到图结构里面就行了其实这个初始化就是动态分配空间和拷贝
*
* @param paraConnections
* @param paraNumNodes
*
* @return
*/
graphPtr graphInit(int **paraConnections,int paraNumNodes)
{
int i,j;
graphPtr resultGraph = (graphPtr)malloc(sizeof(struct graph));
resultGraph->numNodes = paraNumNodes;
//这里如何给二维数组动态分配空间如果不明白可以参考我以前的二维数组文章
resultGraph->connections = (int**)malloc(resultGraph->numNodes * sizeof(int*));
for(i = 0;i < resultGraph->numNodes;i ++)
{
resultGraph->connections[i] = (int*)malloc(resultGraph->numNodes * sizeof(int));
}
for(i = 0;i < resultGraph->numNodes;i ++)
for(j = 0;j < resultGraph->numNodes;j ++)
{
resultGraph->connections[i][j] = paraConnections[i][j];
}
return resultGraph;
}
/**
* @brief 在我看到这个深度遍历的代码时候,我就想到了我前两天写的N皇后问题,两者其实有很多相似之处,即每一个遍历的元素都要走完一个for循环,如果满足条件,就先递归到下一个里面去,但是等他们走完了,终究还是要回到这里把剩下的for循环走完
*
* @param paraGraph
* @param paraNum
*/
void depthTranverse(graphPtr paraGraph,int paraNum)
{
int i;
visited[paraNum] = 1;
printf("%d ",paraNum);
for(i = 0;i < paraGraph->numNodes;i ++)
{
/*先得检查是否被访问过了,然后看两者是否连通
都满足的话就可以递归了*/
if(visited[i] == 0 && paraGraph->connections[paraNum][i] == 1)
{
depthTranverse(paraGraph,i);
}
}
}
/**
* @brief 这个广度遍历和我们二叉树的层次遍历是很相似的,只多了一个检查是否被访问过,用队列来实现是比较简单的
*
* @param paraGraph
* @param paraNum
*/
void wideTranverse(graphPtr paraGraph,int paraNum)
{
int i;
int temp;
//建立队列
graphQueuePtr tempQueue = queueInit();
visited[paraNum] = 1;
//入队的时候就打印出来
enqueue(tempQueue,paraNum);
printf("%d ",paraNum);
//循环结束条件就是队列为空
while(tempQueue->front != tempQueue->rear)
{
temp = dequeue(tempQueue);
//出队一个元素就要入队与他连通且没有被访问过的元素
for(i = 0;i < paraGraph->numNodes;i ++)
{
if(visited[i] == 0 && paraGraph->connections[temp][i] == 1)
{
visited[i] = 1;
enqueue(tempQueue,i);
printf("%d ",i);
}
}
}
}
/**
* @brief 测试类
*/
void testGraphTranverse()
{
int i, j;
int myGraph[5][5] =
{
{0, 1, 0, 1, 0},
{1, 0, 1, 0, 1},
{0, 1, 0, 1, 1},
{1, 0, 1, 0, 0},
{0, 1, 1, 0, 0}
};
int** tempPtr;
printf("正在准备数据\n");
tempPtr = (int**)malloc(5 * sizeof(int*));
for (i = 0; i < 5; i ++)
{
tempPtr[i] = (int*)malloc(5 * sizeof(int));
}
for (i = 0; i < 5; i ++)
{
for (j = 0; j < 5; j ++)
{
tempPtr[i][j] = myGraph[i][j];
}
}
printf("数据准备完毕\n");
graphPtr tempGraphPtr = graphInit(tempPtr,5);
printf("节点个数为 %d \n", tempGraphPtr -> numNodes);
printf("图已被初始化\n");
printf("深度优先访问:\n");
visitedInit(tempGraphPtr);
depthTranverse(tempGraphPtr, 4);
printf("\n广度优先访问:\r\n");
visitedInit(tempGraphPtr);
wideTranverse(tempGraphPtr, 4);
}
int main()
{
testGraphTranverse();
return 0;
}
2 テスト結果
正在准备数据
数据准备完毕
节点个数为 5
图已被初始化
深度优先访问:
4 1 0 3 2
广度优先访问:
4 1 2 0 3
トラバースの 3 つの類似点と相違点
1. 同じ点: これらはすべてグラフの横断であり、すべて 1 行ずつ表示されます。4 行目以降、出力値は値 1 の点の列です。
2. 相違点:
2.1 グラフの深さ優先探索は、バイナリ ツリーの事前順序トラバーサル アルゴリズムに似ています。
その考え方: 初期状態がグラフ内のすべての頂点が未訪問であると仮定し、特定の頂点 v から開始して、最初に頂点を訪問し、次に未訪問の隣接点から開始して、深さ優先検索でグラフを横断する。 v を含むパスを持つグラフ内のすべての頂点が訪問されます。この時点でまだ訪問していない他の頂点がある場合は、まだ訪問していない別の頂点を開始点として選択し、グラフ内のすべての頂点が訪問されるまで上記のプロセスを繰り返します。
明らかに、深さ優先検索は再帰的なプロセスです。
深さ優先トラバースの特徴は、開始点を選択してからトラバースすることで、前進できる場合は前進します。選択した点に接続されているすべての頂点を通過するまで、これを繰り返します。
2.2 グラフの幅優先探索は、バイナリ ツリーの階層トラバーサル アルゴリズムに似ています。
その考え方: まず開始頂点 v から開始し、頂点 v を訪問してキューに入れます。頂点 v はキューから出て、v に隣接する未訪問の頂点 w1、w2、...、wi を訪問します。順番にキューに入ります; その後、頂点 w1 がキューから出て、w1 に隣接する未訪問の頂点を順番に訪問し、それらをキューに入れます; 次に、頂点 w2 がキューから出て、隣接する未訪問の頂点を訪問しますグラフ内のすべての頂点が訪問されるまで、順番に w2 にアクセスし、それをキューに入れます。
2.3 図の簡単な説明
深さ優先検索 (行が最初) では、値 1 の列を走査し、その列にジャンプして走査します。
幅優先検索 (最初の列) では、値 1 で次の列にジャンプする前に列全体を走査します。
int myGraph[5][5] =
{
{0, 1, 0, 1, 0},
{1, 0, 1, 0, 1},
{0, 1, 0, 1, 1},
{1, 0, 1, 0, 0},
{0, 1, 1, 0, 0}
};