文章目录
图的遍历:必须依赖访问数组visited[n]
从某一顶点出发,访问图中其余所有顶点,且每个顶点只访问一次。
树的遍历中,由于只有一个根结点,每个结点只有一个父亲,又没有环存在,树的这种结构保证了前序中序后序和层次遍历都只会把每个结点访问一次,而不会重复访问。
但是图由于本身结构的复杂性,从任意一个结点开始访问,也许又回到自己了但是并没访问完所有结点,很容易存在重复访问且不自知,访问完了都不知道自己已经访问了所有结点。所以图的访问必须要依赖一个访问数组,visited[n],对已经访问过的结点做标记。
一般有两种常用的遍历次序:深度优先和广度优先。
不同的遍历方案的不同之处就在于遍历不同结点时的访问次序。
深度优先 DFS:递归过程;等价于某棵树的前序遍历(其实说先根遍历更准确,因为不是二叉树,这树不唯一)
depth first search
原理
深度优先的规则是:从某一个点出发,一直沿着右手边前进(右手同行准则),依次访问直到回到出发结点;然后依次回退,找倒数第二条出路(之前一直是沿着右手边倒数第一条路)的点,然后能前行就前行,不能前行就回退。
举例,下图:
A出发,右手同行,到B,一路右手同行,直到A,发现A走过了,于是回退到F,走F的右手边倒数第二条路到G,G右手通行到H,H右手通行E已经访问,退回H,走倒数第二条路到D,发现还是访问过,H已经没有其他路,所以回退到G,G按顺序查看了FDB发现都访问了,于是回退到F,F按顺序查看了AGE,都访问了于是回退到E,E按序一看FHD都访问了,回退到D,D按序看EHG都访问了,于是看右手边倒数第四条路的I,没访问,于是去访问,此时visited数组已经全部设置为1了,所以遍历完毕,退出。
通过上面这段详细的描述,你大概也发现了,有很多回退的动作,这种回退用递归实现起来是最简单最完美的,用函数的多个不同版本记录了每一层的信息,回退就是某一层函数执行完毕回到上一层递归调用。如果不用递归,要把到底是从哪个节点来的信息存储下来,才能准确回退到上一步。所以DFS一般会用递归实现,如果遇到很大的图,则可以用迭代版本实现因为递归太多层,很吃空间。
如果把上面深度优先搜索的路径画成一棵树,则如下:
可以看到DFS的过程很像这棵树的先根遍历,但是又有点区别,区别有3:
- 一是这里每个根结点的孩子可能在前面出现过了,一个结点可能作为多个结点的孩子在这棵树中出现多次。
- 二是这里有一个访问数组,如果某个结点的最左边孩子已经被访问,则依次找右边的孩子访问。
- 三是这里的树不是二叉树,而一般前序遍历是针对二叉树而言的,如果不是二叉树,会说先根遍历(所以我个人觉得其实说先根遍历更准确,因为不是二叉树)。
从A开始,一路访问最左边的孩子,直到F,最左边孩子已经被访问过,所以找左边第二个孩子G,然后G的左边俩孩子都访问了,于是找第三个孩子H,H的俩孩子都访问了,于是回退到H,G,F,E,可以看到,这个先根遍历树的回退过程和DFS都是完全一样的。
除了那三点没办法必须要有的区别之外,图的DFS就完全等价于这棵树的先根遍历。当然,这棵树不是固定的,可以根据不同的初始访问点而得到不同的树。
代码
邻接矩阵存储结构, O ( n 2 ) O(n^2) O(n2)
typedef int Boolean;
Boolean visited[MAX];//全局数组
/*深度优先遍历*/
void DFSTraverse(MGraph * G)
{
int i;
for (i=0;i<G->numV;++i)
visited[i] = false;//所有结点初始化为未访问
for (i=0;i<G->numV;++i)
if (!visited[i])
DFS(G, i);//DFS的递归算法的实现函数
}
/*DFS的递归算法的实现函数*/
void DFS(MGraph * G, int i)
{
printf("%d", G->vers[i]);//打印结点数据,可改为其他操作
visited[i] = true;
//对i的邻点递归调用DFS
int j;
for (j = 0; j < G->numV; ++j)
if (G->arc[i][j]==1 && !visited[j])
DFS(G, j);
}
递归果然牛逼,我其实觉得DFS的执行过程真的描述起来还是略费劲的,但是递归就适合执行这种任务,真心牛逼,代码是那么地简洁有力,让人觉得充满了智慧,回味无穷。精妙无比。
if (G->arc[i][j]==1 && !visited[j])
对每个i都要执行n次,所以一共是
n
2
n^2
n2次。
邻接表存储结构, O ( n + e ) O(n+e) O(n+e)
DFSTraverse函数是一样的
/*DFS的递归算法的实现函数,邻接表*/
void DFS(AdjListGraph * G, int i)
{
printf("%d", G->vers[i]);//打印结点数据,可改为其他操作
visited[i] = true;
//对i的邻点递归调用DFS
int j;
ENode * e = G->adjlist[i].firstEdge;
while (e)
{
if (!visited[e->adjvex])
DFS(G, e->adjvex);
e = e->nextEdge;
}
}
在DFSTraverse中,每一个顶点调用一次DFS,而在所有n个从DFSTraverse指向的DFS调用中,while循环总共执行的次数为e,即总的边数。所以时间复杂度是O(n+e)
广度优先 BFS:等价于某棵树的层次遍历,使用队列实现
原理
breadth first search
很牛逼很神奇!把一个图稍微改改画法,就看起来像一棵树啦!并且一切逻辑关系都没变(所以任意的连通图都可以看作是一棵树!你只要按这种方法把层次画出来就好啦,当然这树也不唯一,因为任意结点都可以做为根结点)。
BFS的思想很适合用这个树的层次遍历来描述。
从任意一个点,这里假设是A,开始,先依次访问A的所有孩子,然后再依次访问A的所有孩子的所有孩子,再·····中文描述起来不太好听懂,但是却能发现,这个描述刚好完美对应了树的层次遍历!!!第一层是根结点,第二层是根结点的所有孩子,第三层是第二层的所有孩子······
代码,用队列辅助实现
由于BFS是把图转换为一棵树,然后对这棵树进行层次遍历,所以BFS代码的本质也是树的层次遍历,要用一个队列来辅助实现,并且没有像DFS那样使用递归。
但是虽然BFS把图的遍历转换为一棵树的层次遍历,本来是不用担心重复访问和访问不完全的问题的,不需要visited标记数组,但是图不一定是连通图,如果一个图有多个连通分量,那么只有通过visited数组才可以判断是否全图都遍历完了。而下面的代码也保证了全图的所有连通分量都会被遍历到。
邻接矩阵, O ( n 2 ) O(n^2) O(n2)
太牛了!!!我是写不出来的
/*邻接矩阵的广度优先遍历算法,邻接矩阵*/
void BFSTraverse(MGraph * G)
{
Queue Q;
InitQueue(&Q);//初始化一个空队列
int i, j;
for (i=0;i<G->numV;++i)
visited[i] = false;
for (i=0;i<G->numV;++i)//保证图的所有连通分量都被访问到
{
if (!visited[i])
{
visited[i] = true;
printf("%c ", G->vexs[i]);//操作,可换为其他
EnQueue(&Q, i);//队列用于保证访问次序,因为入队后会把他的邻点(孩子)入队在当前层次的所有结点后面
while (!QueueEmpty(Q))
{
DeQueue(&Q, &i);
//依次找i的所有邻点
for (j=0;j<G->numV;++j)
{
//如果邻点j没被访问就访问并入队
if (G->arc[i][j] == 1 && !visited[j])
{
EnQueue(&Q, j);
visited[j] = true;
printf("%c ", G->vexs[j]);//操作
}
}
}
}
}
}
邻接表, O ( n + e ) O(n+e) O(n+e)
void BFSTraverse(AdjListGraph * G)
{
int i, j;
Queue Q;
InitQueue(&Q);
ENode * e;
for (i=0;i<G->numV;++i)
visited[i] = false;
for (i=0;i<G->numV;++i)
{
if (!visited[i])
{
EnQueue(&Q, i);
visited[i] = true;
printf("%c ", G->adjlist[i]);
while (!QueueEmpty(Q))
{
DeQueue(&Q, &i);
e = G->adjlist[i].firstEdge;
while (e)
{
if (!visited[e->adjvex])
{
visited[e->adjvex] = true;
printf(····);//操作
EnQueue(&Q, e->adjvex);
}
e = e->nextEdge;
}
}
}
}
}
BFS的通用模板
不需要知道当前遍历层数
链接:https://www.nowcoder.com/questionTerminal/91b69814117f4e8097390d107d2efbe0?answerType=1&f=discussion
来源:牛客网
void bfs() {
vis[] = {0}; // or set
queue<int> pq(start_val);
while (!pq.empty()) {
int cur = pq.front(); pq.pop();
for (遍历cur所有的相邻节点nex) {
if (nex节点有效 && vis[nex]==0){
vis[nex] = 1;
pq.push(nex)
}
} // end for
} // end while
}
真的通用,任何用法bfs的问题目实际都是基于这个模板
需要知道当前遍历层数
链接:https://www.nowcoder.com/questionTerminal/91b69814117f4e8097390d107d2efbe0?answerType=1&f=discussion
来源:牛客网
void bfs() {
int level = 0;
vis[] = {0}; // or set
queue<int> pq(original_val);
while (!pq.empty()) {
int sz = pq.size();
while (sz--) {
int cur = pq.front(); pq.pop();
for (遍历cur所有的相邻节点nex) {
if (nex节点有效 && vis[nex] == 0) {
vis[nex] = 1;
pq.push(nex)
}
} // end for
} // end inner while
level++;
} // end outer while
}
DFS VS BFS
- 时间复杂度相同,只是顶点的访问顺序不同。在全图的遍历上没有优劣之分,我们只是根据需要来选择。
- 但是对于顶点和边很多的图,遍历时间很长,但是遍历目的是为了找到某种条件的顶点,那么选DFS还是BFS就要根据情况实际分析了。
- 深度和广度本就是矛盾的,深度和广度在这里实现了图的遍历,但是实际上他们可以上升到方法论的高度。你可以博览群书不求甚解,也可以深钻细研鞭辟入里,很有哲学思想的味道。可见算法之精妙,抽象地概括起来,充满了哲学思想,品味去穷。
DFS适合用递归实现,原理理解困难一下,但是代码特别精简美丽。而BFS很巧妙,理解起来特别简单,因为他就是转化为树的层次遍历。但是BFS的代码稍微复杂一些。