线性存储元素时,元素的关系也同时确定了。而非线性数据结构就不同了,需要同时考虑存储数据元素和数据元素的关系。
由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在存储区中的物理位置来表示元素之间的关系,即图没有顺序存储结构,但可以借助二维表来表示元素之间的关系,一般称为邻接矩阵。另一方面,由于图的任意两个顶点间都可能存在关系,因此,用链式存储表示图是很自然的事,其中有代表性的链式存储结构称为邻接表(Adjacency List)。
在邻接表中,对图中每个顶点vi建立一个单链表,把与vi相邻接的顶点放在这个链表中。邻接表中每个单链表的第一个结点存放有关顶点的信息, 把这一结点看成链表的表头,其余结点存放有关边的信息。这样邻接表便由两部分组成:表头结点表和边表。
![d459322406c56dbbfcfbe8aa2a582dda.png](https://i-blog.csdnimg.cn/blog_migrate/706e6335e66575e7d372ca2eaa950e1c.jpeg)
如上图的邻接表和逆邻接表:
![33eebc1930ffe26bf062364dd8fe766b.png](https://i-blog.csdnimg.cn/blog_migrate/7ddfea96339bd2dc720783e9145af7be.jpeg)
需要注意的是,图的邻接表表示并不唯一,这是因为在每个顶点对应的单链表中,各边结点的链接次序可以是任意的,取决于邻接表的算法以及边的输入次序。
用邻接表表示图一般可以描述为:
typedef struct ArcNode{// 单链表中的结点的类型int adjvex;// 该边指向的顶点在顺序表中的位置struct ArcNode *next;// 下一条边 //OtherInfo info; // 和边相关的信息,如权值}ArcNode;typedef struct VNode{// 顶点类型int data;// 顶点中的数据信息ArcNode *firstarc;// 指向单链表,即指向第一条边}VNode;
图的邻接存储:顶点表+n个链表(顶点元素及链接到相邻顶点的指针、下一个相邻点及指针……);
有向图存储全部信息同时还需要一个逆邻接表;
带权图的邻接表则需要结点元素多一个权值域;
图遍历背后的关键思想是在我们第一次访问每个顶点时对其进行标记(一般使用一个布尔数组),并跟踪我们尚未完全探索的内容(一般使用一个栈或队列结构)。尽管面包屑或解开的线被用来标记童话迷宫中参观过的地方,但我们将依赖布尔标志或枚举类型来进行。
每个顶点将以三种状态之一存在:
未发现-顶点处于原始状态。
已发现-顶点已找到,但尚未检查其所有入射边。
已处理–访问所有关联边后的顶点。
显然,一个顶点只有在我们发现它之后才能被处理,所以每个顶点的状态在遍历过程中从未发现到发现再到处理。
我们还必须维护一个包含我们已经发现但尚未完全处理的顶点的结构。最初,只有一个起始顶点被认为是被发现的。为了完全探索一个顶点v,我们必须计算每一条离开v的边。如果一条边转到一个未发现的顶点x,我们标记x 为“已发现”并将其添加到要做的工作列表中。我们要忽略处理过的顶点的边,因为进一步的探索不会告诉我们关于图的任何新的东西。我们还可以忽略到已发现但未处理的顶点的任何边,因为目标已位于要处理的顶点列表中。
BFS(Depth First Search--DFS)和DFS(Breadth First Search--BFS)结果的区别在于它们探索顶点的顺序。此顺序完全取决于用于存储已发现但未处理的顶点的容器数据结构。
Stack–通过将顶点存储在后进先出(last-in,first-out,LIFO)堆栈中,我们通过沿着路径一直前行、访问新邻居(如果有的话)和仅当我们被以前发现的顶点包围时才回退来探索新的顶点。因此,我们的探索很快偏离了起点,定义了深度优先搜索。
一旦发现一个顶点,它就被放置在栈(显式或隐式栈)中。由于我们按后入先出的顺序处理这些顶点,所以最新的顶点将首先展开,这些顶点正是最远离根的顶点。
队列-通过将顶点存储在先进先出(FIFO)队列中,我们首先探索最古老的未探索顶点。因此,我们的探索从起始顶点缓慢地向外辐射,定义了广度优先搜索。
一旦发现一个顶点,它就被放置在队列中。由于我们按先入先出的顺序处理这些顶点,所以最旧的顶点将首先展开,这些顶点正是最接近根的顶点。
深度优先搜索有一个简洁的递归实现,它消除了显式使用堆栈的需要。
我们需要能够对每个入口和出口分别采取行动。
深度优先搜索的另一个重要特性是它将无向图的边划分为两类:tree edges和back edges。tree edges发现新的顶点,并且是在parent关系中编码的顶点。back edges是那些其另一个端点是被展开顶点的ancestor的边,因此它们指向树中。
![483fd1c0a5900a6778530e2e4ddb957f.png](https://i-blog.csdnimg.cn/blog_migrate/ef124fc75234e18bda75fe5ff757a23d.jpeg)
1 深度优先遍历
优先向深度探索,一直走到头才回头到路径的上一个相邻顶点,直到回溯到最开始顶点。
对于邻接点,先孩子…后上一辈的兄弟,后进先出(栈隐式或显式辅助)。
![b25f43aaf66fea40e00460891522cddb.png](https://i-blog.csdnimg.cn/blog_migrate/c06e6d8842064165bb44627febc48c7c.jpeg)
如上图的深度优先遍历的顺序:0 1 2 3 4 5 6 8 9 7。
如果使用递归,则相当于使用了一个隐式的栈数据结构(编译器对函数递归调用的压栈和回归的出栈操作)。
如果使用迭代法,则需要显式使用一个栈数据结构。
2 广度优先遍历
广度优先遍历,也就是从某一个顶点开始,优先访问全部的相邻顶点,按层次辐射,直到全部顶点访问完。
对于邻接点,先兄弟后孩子,先进先出(队列辅助)。
如上图使用广度优先遍历的顺序:0 1 3 2 4 5 6 7 8 9。
广度优先遍历需显式使用一个队列的数据结构。广度优先搜索是一种分层的搜索过程,不像深度优先遍历那样有往回退的情况。因此,广度优先遍历不能递归实现,可以使用先进先出的队列来实现。
深度搜索与广度搜索的控制结构和产生系统很相似,唯一的区别在于对扩展节点选取上。由于其保留了所有的前继节点,所以在产生后继节点时可以去掉一部分重复的节点,从而提高了搜索效率。这两种算法每次都扩展一个节点的所有子节点,而不同的是,深度搜索下一次扩展的是本次扩展出来的子节点中的一个,而广度搜索扩展的则是本次扩展的节点的兄弟节点。也就是说,广度优先搜索会优先考虑最早被发现的顶点,也就是说离起点越近的顶点优先级越高。深度优先搜索会优先考虑最后被发现的顶点。
在20世纪50年代,广度优先搜索最早由Edward F. Moor在研究迷宫路径问题时发现,深度优先搜索在人工智能方面获得了广泛应用。
需要注意的是,广度优先搜索是以起始点为中心,一层一层向外层扩展遍历图的顶点,因此无法考虑到边的权值,只适合求边权值相等的图的单源最短路径。
// 图的邻接表存储,以及图的深度优先遍历(DST)和广度优先遍历(BST)#include "stdio.h"#include "malloc.h"#define MAX_VERTEX_NUM 10 // 表示创建和遍历的图的顶点数typedef struct ArcNode{ // 单链表中的结点的类型 int adjvex; // 该边指向的顶点在顺序表中的位置(下标) struct ArcNode *next; // 下一条边 //OtherInfo info; // 和边相关的信息,如权值}ArcNode;typedef struct VNode{ // 顶点类型 int data; // 顶点中的数据信息 ArcNode *firstarc; // 指向单链表,即指向第一条边}VNode;typedef struct QNode{ int data; // 链队列结点中的数据域 struct QNode *next; // 链队列结点中的指针域}QNode , *QueuePtr;typedef struct{ QueuePtr front; // 队头指针 QueuePtr rear; // 队尾指针}LinkQueue;int visited[MAX_VERTEX_NUM]={0};/* 0-----1 | /| | 2 | | / | | / 4 5---8 |/ / / 3 7 6--9 */void CreatGraph(int n, VNode G[]);int FirstAdj(VNode G[],int v) ; // 返回下标为v的顶点的第一邻接点在数组中的下标int NextAdj(VNode G[],int v, int w); // 返回下标为v的顶点的邻接点中下标为w的邻接点的下标void DFS(VNode G[],int v) // 从下标为v的顶点开始DFS{ printf("%d ",G[v]); // 访问当前顶点,打印出该顶点中的数据信息 visited[v] = 1; // 将顶点v对应的访问标记置1 int w = FirstAdj(G,v); // 找到顶点v的第一个邻接点,如果无邻接点,返回-1 while(w != -1){ if(visited[w] == 0) // 该顶点未被访问 DFS(G,w); // 递归地进行深度优先搜索 w = NextAdj(G,v,w); // 找到顶点v的邻接点为w的下一个邻接点,如果无邻接点,返回-1 }}void Travel_DFS(VNode G[], int n) // 非连能图各子图的DFS{ for(int i=0;inext = NULL; p->adjvex = e; if(G[i].firstarc == NULL) G[i].firstarc = p; // i结点的第一条边 else q->next = p; // 下一条边 q = p; scanf("%d",&e); } }}int FirstAdj(VNode G[],int v) // 返回下标为v的顶点的第一邻接点在数组中的下标{ if(G[v].firstarc != NULL) { return (G[v].firstarc)->adjvex; } return -1;}int NextAdj(VNode G[],int v, int w){ // 返回下标为v的顶点的邻接中下标为w // 的下一个邻接点在数组中的下标 ArcNode *p = G[v].firstarc; while( p!= NULL){ if(p->adjvex == w && p->next != NULL) { return p->next->adjvex; } p = p->next; } return -1;}void initQueue(LinkQueue *q) // 初始化一个空队列{ q->front = q->rear = (QueuePtr)malloc(sizeof(QNode)); // 创建一个头结点,队头队尾指针指向该结点 if(!q->front) return; // 创建头结点失败 q->front->next = NULL; // 头结点指针域置NULL}void EnQueue(LinkQueue *q, int e){ QueuePtr p; p = (QueuePtr)malloc(sizeof(QNode)); // 创建一个队列元素结点 if(p==NULL)return; // 创建元素结点失败 p->data = e; // 将数据e存放到队列结点的data域中 p->next = NULL; // 指针域置NULL q->rear ->next = p; // 从队尾插入结点 q->rear = p; // 修改队尾指针}void DeQueue(LinkQueue *q, int *e){ // 如果队列q不为空,删除q的队头元素,用e返回其值 QueuePtr p; if(q->front == q->rear) return; // 队列为空,返回 p = q->front->next; // p指向队列的第一个元素 *e = p->data; // 将队首元素的数据赋值给e返回 q->front->next = p->next; // 删除头结点 if(q->rear == p) q->rear = q->front; // 如果此时队列为空,则修改队尾指针 free(p);}int emptyQ(LinkQueue q) { if (q.rear == q.front) { return 1; } return 0;}
无论那种搜索,都是通过对一个线性表进行处理,只不过是先处理头部还是尾部的问题罢了。处理头部优先的时候,也就是先加入的先探索,就是广度优先了,因为,头部的都是兄弟节点;而尾部的则是深度优先,因为放入尾部的都是刚刚生产出来的节点,后加入的先探索——也就是所谓一条路走到死。 同理可以联想到启发式搜索。启发式搜索就是先以你自定义的优先级处理,然后再以广度为优先级处理。 所以,归根结底,所谓的搜索,就是一种定义了优先级的枚举。
回溯算法一般使用深度优先搜索策略,在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。
回溯法以深度优先的方式搜索解空间树T,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树T。分支限界法因为不回溯,所以只是找出满足约束条件的一个解,从当前活结点表中选择一个最有利的结点作为扩展结点,使搜索朝着解空间树上有最优解的分支推进。
附:邻接表与邻接矩阵的DFS、BFS比较
![292576fbc0c66fbfcc93e980a125ec11.png](https://i-blog.csdnimg.cn/blog_migrate/d5ff5c8082c0c44bc97da8b0cbb8655b.jpeg)
附DFS、BFS(邻接矩阵)代码:
#include #include typedef int VertexType; // 顶点类型应由用户定义typedef int EdgeType; // 边上的权值类型应由用户定义#define MAXSIZE 15 // 存储空间初始分配量#define MAXEDGE 15#define MAXVEX 10#define INFINITY 65535/* 0-----1 | /| | 2 | | / | | / 4 5---8 |/ / / 3 7 6--9 */int arc[MAXVEX][MAXVEX]={{0,1,0,1,0,0,0,0,0,0},{1,0,1,0,1,0,0,0,0,0},{0,1,0,1,1,0,0,0,0,0},{1,0,1,0,0,0,0,0,0,0},{0,1,1,0,0,0,0,0,0,0},{0,0,0,0,0,0,1,1,1,0},{0,0,0,0,0,1,0,0,1,1},{0,0,0,0,1,0,0,0,0,0},{0,0,0,0,0,1,1,0,0,0},{0,0,0,0,0,0,1,0,0,0}};typedef struct{ VertexType vexs[MAXVEX]; // 顶点表 EdgeType arc[MAXVEX][MAXVEX]; // 邻接矩阵,可看作边表 int numVertexes, numEdges; // 图中当前的顶点数和边数 }MGraph;typedef struct // 循环队列顺序存储结构{ int data[MAXSIZE]; int front; // 头指针 int rear; // 尾指针,若队列不空,指向队列尾元素的下一个位置}Queue;void CreateMGraph(MGraph *G){ int i,j; G->numEdges=15; G->numVertexes=10;for(i=0; inumVertexes; i++) // 建立顶点表G->vexs[i] = i; for (i = 0; i < G->numVertexes; i++) // 初始化图 { for (int j = 0; j < G->numVertexes; j++) { G->arc[i][j]=arc[i][j]; } }} bool visited[MAXVEX]; // 访问标志的数组void DFS(MGraph G, int i) // 邻接矩阵的深度优先递归算法{ visited[i] = true; printf("%d ", G.vexs[i]); // 打印顶点,也可以其它操作 for(int j=0; jfront=0; Q->rear=0; return true;}bool QueueEmpty(Queue Q){ if(Q.front==Q.rear) // 队列空的标志 return true; else return false;}bool EnQueue(Queue *Q,int e){ if ((Q->rear+1)%MAXSIZE == Q->front) // 队列满的判断 return false; Q->data[Q->rear]=e; // 将元素e赋值给队尾 Q->rear=(Q->rear+1)%MAXSIZE; // rear指针向后移一位置, // 若到最后则转到数组头部 return true;}bool DeQueue(Queue *Q,int *e) // 删除Q中队头元素,用e返回其值{ if (Q->front == Q->rear) // 队列空的判断 return false; *e=Q->data[Q->front]; // 将队头元素赋值给e Q->front=(Q->front+1)%MAXSIZE; // front指针向后移一位置, // 若到最后则转到数组头部 return true;}
-End-