数据结构 – 图基础 基本概念+存储
KEY
- 图的基本概念
- 图的存储及基本操作
- 邻接矩阵法
- 邻接表法
- 图的遍历
- 深度优先搜索
- 广度优先搜索
- 图的应用(下一个复习笔记再写了)
一、图的基本概念
网状逻辑结构
- 网状逻辑结构的特点是:
- 结点的特征一致(顶点)
- 结点可以和多个的其它结点关联(边)
- 图结构是一种动态的,非线性的,可描述结构网状特性的数据结构。这种结构是按结点元素的关联映射关系把信息联系起来的数据组织形式。
回顾一下
数据结构定义了一组按某些关系结合在一起的数据元素。 数据类型不仅定义了一组带结构的数据元素,而且还在其上定义了一组操作。
线性结构反映结点间的逻辑关系是一对一的,非线性结构反映结点间 的逻辑关系是多对多的。
数据逻辑结构是指数据元素之间的逻辑关系的整体
图的术语 ---- 纲要
图可以用 G = (V, E) 来表示, 每个图都包括一个顶点(vertices) V, 和一个边集合(edges) E, 其中E中的每条边都是 V 中某一对顶点之间的连接.
总结 复习的时候看一下有没有不知道的,有就好好看看
- 顶点的总数|V|
- 边的总数|E|
- 稀疏图、密集图(稠密图)和完全图
- 顶点的度,入度和出度
- 路径、路径长度、简单路径,回路、简单回路
- 子图
- 连通图、连通分量
- 强连通图与强连通分量
- 无环图,有向无环图
- 自由树
- 二分图
图的术语 ---- 详细
-
一条边所连接的两个顶点是相邻的(adjacent), 称为 邻接点(neighbors)。
-
连接一对邻接点u、v的边被称为与顶点u、v相关联的边。 每条边都可能附有值或权。
-
有向边:有序顶点对<u,v> <v1,v2>和<v2,v1>不是一条边
-
无向边:无序顶点对(u,v) (v1,v2)和(v2,v1)是一条边
-
包括所有可能边的图称为完全图
-
完全图若有n个顶点的无向图有n(n-1)/2条边,则此图为完全无向图。有n个顶点的有向图有n(n-1)条边,则此图为完全有向图。
-
子图:设有两个图 G=(V,E) 和 G’=(V’,E’)。若 V ′ ⊆ V V'\subseteq V V′⊆V且 E ′ ⊆ E E'\subseteq E E′⊆E,则称图G’是图G的子图
例如下图:
-
顶点的度: 一个顶点v的度是与它相关联的边的条数。记作TD(v)。在有向图中,顶点的度等于该顶点的入度与出度之和。顶点v的入度是以v为终点的有向边的条数;顶点v的出度是以v为始点的有向边的条数。
-
路径长度: 非带权图的路径长度是指此路径上边的条数。带权图的路径长度是指路径上各边的权之和。
-
简单路径:若路径上各顶点v1,v2,…,vm均不互相重复,则称这样的路径为简单路径。
-
回路:若路径上第一个顶点v1与最后一个顶点vm重合,则称这样的路径为回路或环。
-
连通图与连通分量 :在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与v2是连通的。如果图中任意一对顶点都是连通的,则称此图是连通图。非连通图的极大连通子图叫做连通分量
-
强连通图与强连通分量:在有向图中,若对于每一对顶点vi和vj,都存在一条从vi到vj和从vj到vi的路径,则称此图是强连通图。非强连通图的极大强连通子图叫做强连通分量。
完全有向图一定是强连通图
但强连通图不一定是完全有向图。因为有路径 ≠ \ne =有边,可以通过别的结点到达
-
自由树: 不带简单回路的连通无向图
-
支撑树(spanning tree)一个连通图的生成树是它的极小连通子图,在n个顶点的情形下,有n-1条边。但有向图则可能得到它的由若干有向树组成的生成森林。
-
二分图又称作二部图: 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。
二、图的ADT
数据对象D:图是由一个顶点集V和一个弧集E构成的数据结构。Graph = (V , E )顶点的数据域和边的权可用于存储数据元素
数据关系R:VR={<v,w>| v,w∈V且P(v,w) ∈E} <v,w>表示从v 到w 的一条弧,并称v为弧头,w 为弧尾。谓词P(v,w) 定义了弧<v,w>的意义或信息。
基本操作:
-
结构构造/销毁性操作
-
引用型操作
获取图顶点/边,查找,遍历
-
加工型操作
插入/删除 图的顶点/边
class Graph
{
private:
void operator=(constGraph&) {}
Graph(constGraph&) {}
public:
// return the number of vertices(顶点) and edges(边)
virtual int n() =0; // # of vertices
virtual int e() =0; // # of edges
// Return index of first, next neighbor
virtual int first(int) =0;
virtual int next(int, int) =0;
//store new edge
virtual void setEdge(int, int, int) =0;
// Delete edge defined by two vertices
virtual void delEdge(int, int) =0;
// Weight of edge connecting two vertices
virtual intweight(int, int) =0;
virtual intgetMark(int) =0;
virtual void setMark(int, int) =0;
};
三、图的实现
3.1 邻接矩阵
-
在图的邻接矩阵表示中,有一个记录各个顶点信息的顶点表,还有一个表示各个顶点之间关系的邻接矩阵。
-
设图A=(V,E)是一个有n个顶点的图,则图的邻接矩阵是一个二维数组A.edge[n][n],定义:
A . E d g e [ i , j ] = { 1 , 如果<i,j> ∈ E 或者(i,j) ∈ E 0 , 不存在边 A.Edge[i,j]= \begin{cases} 1, & \text{如果<i,j> $\in$ E 或者(i,j) $\in$E } \\[5ex] 0, & \text{不存在边} \end{cases} A.Edge[i,j]=⎩⎪⎪⎨⎪⎪⎧1,0,如果<i,j> ∈ E 或者(i,j) ∈E 不存在边
-
无向图的邻接矩阵是对称的,有向图的邻接矩阵可能是不对称的。
3.1.1 特点分析
- 相邻矩阵需要存储所有可能的边,不管这条边是否实际存在
- 没有结构性开销,但是存在空间浪费
- 相邻矩阵的空间代价只与顶点的个数有关,为Θ( ∣ V ∣ 2 |V|^2 ∣V∣2),图越密集,其空间效率就越高
- 基于相邻矩阵的图算法,必须查看它的所有可能的边,导致总时间代价为Θ( ∣ V ∣ 2 |V|^2 ∣V∣2),所以相邻矩阵适合密集图的存储
3.1.2 具体实现
#ifndef GRAPHM_H
#define GRAPHM_H
#include <Graph.h>
class Graphm : public Graph
{
public:
Graphm(int numVert);
~Graphm();
int n() ; // # of vertices
int e() ; // # of edges
// Return index of first, next neighbor
int first(int) ;
int next(int, int) ;
//store new edge
void setEdge(int, int, int) ;
// Delete edge defined by two vertices
void delEdge(int, int) ;
// Weight of edge connecting two vertices
int weight(int, int) ;
bool getMark(int) ;
void setMark(int, int) ;
void print ();
bool legalNum(int i);
private:
int **matrix;//邻接矩阵,记录边
int numVertex,numEdge;//顶点个数 边的条数
bool *mark;//用于后面广度/深度优先遍历时记录某个结点是否被遍历过
};
#endif // GRAPHM_H
#include "Graphm.h"
#include <iostream>
#define UNVISITED false
#include <assert.h>
using namespace std;
Graphm::Graphm(int numVert)
{
int i;
numVertex=numVert;
numEdge=0;
mark = new bool[numVertex];
matrix=(int **)new int*[numVertex];
for (i = 0; i<numVertex; i++)
{
mark[i]=UNVISITED;
matrix[i]=new int[numVertex];
}
for(i=0; i<numVertex; i++)
for(int j=0; j<numVertex; j++)
matrix[i][j]=0;
}
Graphm::~Graphm()
{
delete []mark;
for(int i=0; i<numVertex; i++)
delete []matrix[i];
delete []matrix;
//dtor
}
int Graphm::e()
{
return numEdge;
}
int Graphm::n()
{
return numVertex;
}
int Graphm::first(int v)
{
for(int i=0; i<numVertex; i++)
{
if(matrix[v][i]!=0) return i;
}
return numVertex;
}
//find v's next neighbor after w
int Graphm::next(int v,int w)
{
for(int i=w+1; i<numVertex; i++)
if(matrix[v][i]!=0) return i;
return numVertex;
}
void Graphm::setEdge(int b,int e,int w)
{
assert((legalNum(b))&&(legalNum(e)));
assert(w>0);
if(matrix[b][e]==0) numEdge++;
matrix[b][e]=w;
}
void Graphm::delEdge(int b,int e)
{
if(matrix[b][e]!=0) numEdge--;
matrix[b][e]=0;
}
bool Graphm::getMark(int i)
{
return mark[i];
}
void Graphm::setMark(int i,int w)
{
assert(legalNum(i));
mark[i]=w;
}
void Graphm::print()
{
for(int i=0; i<numVertex; i++)
{
for(int j=0; j<numVertex; j++)
cout<<matrix[i][j];
cout << endl;
}
}
int Graphm::weight(int b,int e)
{
return matrix[b][e];
}
bool Graphm::legalNum(int i)
{
if((i>=0)&&(i<numVertex)) return true;
return false;
}
3.2 邻接表
3.2.1 概述
邻接表(Adjacency List)
-
无向图的邻接表把同一个顶点发出的边链接在同一个边链表中,链表的每一个结点代表一条边,叫做边结点,结点中保存有与该边相关联的另一顶点的顶点下标dest和指向同一链表中下一个边结点的指针link
有些类似于树的子节点表示法,但是稍有不同的是,树的子节点表示法有顺序,最左侧的表示该节点最左孩子。
-
有向图的邻接表和逆邻接表
在有向图的邻接表中,第i个边链表链接的边都是顶点i发出的边。也叫做出边表。图1
在有向图的逆邻接表中,第i个边链表链接的边都是进入顶点i的边。也叫做入边表。图2
3.2.2 特点
-
带权图的边结点中保存该边上的权值cost。
-
顶点i的边链表的表头指针adj在顶点表的下标为i的顶点记录中,该记录还保存了该顶点的其它信息。
-
在邻接表的边链表中,各个边结点的链入顺序任意,视边结点输入次序而定。
-
设图中有n个顶点,e条边,则用邻接表表示无向图时,需要n个顶点结点,2e个边结点;用邻接表表示有向图时,若不考虑逆邻接表,只需n个顶点结点,e个边结点。
3.2.3 具体实现
Graphl.h
#ifndef GRAPHL_H
#define GRAPHL_H
#include <iostream>
#include <Graph.h>
using namespace std;
struct arcNode
{
int adjvert;
arcNode *next;
int weight;
arcNode(int adjvert,arcNode* next,int weight){this->next=next;this->adjvert=adjvert;this->weight=weight;};
arcNode(){next=NULL;adjvert=-1;weight=-1;};
};
struct hNode
{
arcNode *firstArc;
hNode(){firstArc=NULL;}
};
class Graphl : public Graph
{
public:
Graphl(int numVert);
~Graphl();
int n() ; // # of vertices
int e() ; // # of edges
// Return index of first, next neighbor
int first(int) ;
int next(int, int) ;
//store new edge
void setEdge(int, int, int) ;
// Delete edge defined by two vertices
void delEdge(int, int) ;
// Weight of edge connecting two vertices
int weight(int, int) ;
bool getMark(int) ;
void setMark(int, int) ;
void print ();
bool legalNum(int i);
private:
hNode * vertices;
int numVertex,numEdge;
bool * mark ;
};
#endif // GRAPHL_H
Graphl.cpp
#include "Graphl.h"
#include <iostream>
#define UNVISITED false
#include <assert.h>
using namespace std;
Graphl::Graphl(int numVert)
{
int i;
numVertex=numVert;
numEdge=0;
mark = new bool[numVertex];
vertices=new hNode[numVertex];
for (i = 0; i<numVertex; i++)
mark[i]=UNVISITED;
}
Graphl::~Graphl()
{
delete []mark;
delete vertices;
vertices=NULL;
//dtor
}
int Graphl::e()
{
return numEdge;
}
int Graphl::n()
{
return numVertex;
}
int Graphl::first(int v)
{
assert(legalNum(v));
int n=numVertex;
arcNode *temp;
temp=vertices[v].firstArc;
while(temp)
{
if((temp->adjvert) < n) n=temp->adjvert;
temp=temp->next;
}
return n;
}
//find v's next neighbor after w
int Graphl::next(int v,int w)
{
assert(legalNum(v));
int n=numVertex;
arcNode *temp;
temp=vertices[v].firstArc;
while(temp)
{
if(((temp->adjvert)>w )&&((temp->adjvert) < n)) n=temp->adjvert;
temp=temp->next;
}
return n;
}
void Graphl::setEdge(int b,int e,int w)
{
assert((legalNum(b))&&(legalNum(e)));
assert(w>0);
arcNode *temp;
temp=vertices[b].firstArc;
while(temp)
{
if((temp->adjvert)==e )
{
temp->weight=w;
return;
}
temp=temp->next;
}
if(!temp) numEdge++;
arcNode *temp2=new arcNode(e,vertices[b].firstArc,w);
vertices[b].firstArc=temp2;
}
void Graphl::delEdge(int b,int e)
{
arcNode *temp,*prev;
prev=vertices[b].firstArc;
temp=prev->next;
if(!temp)
{
if(prev->adjvert==e) vertices[b].firstArc=NULL;
delete prev;
numEdge--;
return;
}
while(temp)
{
if((temp->adjvert)==e )
{
prev->next=temp->next;
delete temp;
numEdge--;
return;
}
prev=temp;
temp=temp->next;
}
cout<<"delete not found"<<endl;
return;
}
bool Graphl::getMark(int i)
{
return mark[i];
}
void Graphl::setMark(int i,int w)
{
assert(legalNum(i));
mark[i]=w;
}
void Graphl::print()
{
arcNode *temp;
for(int i=0; i<numVertex; i++)
{
cout<<i<<": ";
temp=vertices[i].firstArc;
while(temp)
{
cout<<"->"<<temp->adjvert<<"("<<temp->weight<<")";
temp=temp->next;
}
cout<<endl;
}
}
int Graphl::weight(int b,int e)
{
assert((legalNum(b))&&(legalNum(e)));
arcNode *temp;
temp=vertices[b].firstArc;
while(temp)
{
if((temp->adjvert)==e )
{
return temp->weight;
}
temp=temp->next;
}
return numVertex;
}
bool Graphl::legalNum(int i)
{
if((i>=0)&&(i<numVertex)) return true;
return false;
}
四、图的遍历
4.0 问题描述
从图中某个顶点出发游历图,访遍图中其余顶点,并且使图中的每个顶点仅被访问一次的过程。
4.1 深度优先(DFS)
4.1.1 算法思想
从图中某个顶点V0 出发,访问此顶 点,然后依次从V0的各个未被访问的邻接 点出发深度优先搜索遍历图,直至图中所 有和V0有路径相通的顶点都被访问到。
4.1.2 算法描述
在图的搜索过程中,每当访问某个顶点V时,DFS就递归地访问它的所有未被访问的相邻的顶点。同样,DFS把所有从顶点V发出的边压栈。从栈顶弹出一条边,根据这条边找到顶点V的一个相邻顶点,这个顶点就是下一个要访问的顶点,对这个顶点重复对顶点V的操作。结果就沿着图中的一个分支一直处理下去,完成这个分支后再回溯处理下一个分支,以此类推。
4.1.3 算法实现
// 1.递归
void dfs(Graph *g,int start)
{
g->setMark(start,VISITED);
cout<<start<<endl;
for(int w=g->first(start);w<g->n();w=g->next(start,w))
if(!(g->getMark(w))) dfs(g,w);
}
//2 非递归
void dfs2(Graph *g,int start)
{
cout<<endl<<"dfs2"<<endl;
stack<int> s;
s.push(start);
int temp,son;
while(!s.empty())
{
temp=s.top();
if(g->legalNum(temp)&& (g->getMark(temp)==false))cout<<temp<<endl;
g->setMark(temp,VISITED);
son=g->first(temp);
if(g->legalNum(son)&& (g->getMark(son)==false))
s.push(son);
else if(g->legalNum(son)==false)
s.pop();
else
{
while(g->legalNum(son))
{
if(!(g->getMark(son)))
{
s.push(son);
break;
}
son=g->next(temp,son);
}
if(!g->legalNum(son)) s.pop();
}
}
cout<<endl;
}
因为每个顶点要访问一次,每条边在有向图中最多访问一次,在无向图中最多访问两次; C o s t : θ ( ∣ V ∣ + ∣ E ∣ ) Cost:\theta(|V|+|E|) Cost:θ(∣V∣+∣E∣)
4.2 广度优先(BFS)
4.2.1 算法思想
从图中的某个顶点V0出发,并在访问此 顶点之后依次访问V0的所有未被访问过的 邻接点,之后按这些顶点被访问的先后次 序依次访问它们的邻接点,直至图中所有 和V0有路径相通的顶点都被访问到。
4.2.2 算法描述
- 首先创建一个队列;若图非空,将顶点V0 放入队列,并设置顶点V0已访问;
- 从队列取出一个顶点,并依次访问该顶点 的所有邻接点,如果该邻接点未被访问, 则将该邻接点放入队列,并设置其已访问;
- 若队列非空,继续第2步,直至队列为空, 则遍历过程结束。
4.2.3 算法实现
void BFS(Graph *p,int start)
{
queue<int> q;
q.push(start);
int one,i;
p->setMark(start,VISITED);
while(!q.empty())
{
one=q.front();
q.pop();
cout<<one;
for(i=p->first(one);i<p->n();i=p->next(one,i))
{
if(!(p->getMark(i))) {q.push(i);p->setMark(i,VISITED);}
}
}
}
C o s t : θ ( ∣ V ∣ + ∣ E ∣ ) Cost:\theta(|V|+|E|) Cost:θ(∣V∣+∣E∣)