//我还可以再肝!!!//写不完了哭哭
图结构是一种比树结构更复杂的非线性结构,任意两项之间都有可能有关系。
图的逻辑结构
定义
顶点:图中的数据元素
图:由顶点的有穷非空集合和顶点之间边的集合组成。
无向边:两个顶点之间没有方向。(𝑣𝑖,𝑣𝑗)
有向边:两个顶点之间有方向。有序偶对表示。<𝑣𝑖,𝑣𝑗>,𝑣𝑖弧尾,𝑣𝑗弧头
目录
无向图:任意两点之间的边都是无向边。
有向图:任意两点之间的边都是有向边。
权:对边赋予的有意义的数值量。
带权图(网图):边上带权的图。
基本术语
1.邻接:对于任意两个顶点存在边,则这两个顶点互为邻接点。
入度:顶点为弧头的弧的个数。 (进入该顶点)
出度:顶点为弧尾的弧的个数。(由该顶点出去)
出度 = 入度 =边的条数
3.无向完全图:任意两点都有边。
有向完全图:任意两点都有方向相反的两条弧。
4.稀疏图:边很少。
稠密图:边很多。
5.路径:一个顶点到另一个顶点的一个顶点序列。
路径长度:路径上边的数目。
回路:第一个顶点=最后一个顶点的路径。
6.简单路径:顶点不重复出现的路径。
简单回路:除第一个和最后一个顶点外,其余顶点不重复出现的回路。
7.子图:顶点属于原来的顶点集且边属于原来边集的图。 一个图可以有多个子图。
8.连通:无向图中两点之间存在路径,则这两点连通。
连通图:任意两个顶点之间均有路径。
连通分量:非连通图的极大连通子图。
9.强连通图:有向图中,任意两个顶点之间均有路径。
强连通分量:非强连通图的极大强连通子图。
图的抽象数据类型定义:
顶点的有穷集合和顶点之间边的集合、图的建立、图的销毁、深度优先遍历、广度优先遍历。
图的遍历
从图中某顶点出发,对图中所有顶点访问一次且仅访问一次。
图中没有确定的起始顶点,可以从图中任一顶点出发,从0开始对顶点编号。
在遍历的过程中区分顶点是否已经被访问:设置一个访问标志数组visited[n],初始值都为0表示没有被访问,若某顶点i被访问,那么visited[i]=1。
深度优先遍历
递归的方式
类似于树的前序遍历。
1.访问顶点v
2.从v中未被访问的邻接点选取一个顶点w,再从w出发进行深度优先遍历
3.重复1 2
广度优先遍历
类似于树的层序遍历。
1.访问顶点v
2.依次访问v的各个未被访问的邻接点w,x...
3.分别从w,x...出发依次访问他们未被访问的邻接点,直到图中与顶点v路径相通的点都被访问到。
图的存储结构
邻接矩阵&邻接表
邻接矩阵
(数组表示法)
用一个一维数组存储图中顶点
用一个二维数组存储图中的边(顶点之间的邻接关系) =邻接矩阵
n个顶点,邻接矩阵时nxn的方阵。
无向图的邻接矩阵一定是对称矩阵。
1.对于无向图,顶点i的度=邻接矩阵第i行非0元素的个数。
对于有向图,顶点i的出度=邻接矩阵中第i行非0元素的个数
入度=邻接矩阵中第i列非0元素的个数
2.判断两个顶点是否有边,只需测试邻接矩阵中相应位置元素是否为1
3.查找顶点i的所有邻接点,扫描邻接矩阵的第i行,若有非0元素,则找出邻接点即可。
#include<iostream>
using namespace std;
const int MaxSize = 10; //图中最多的顶点个数
int visited[MaxSize] = { 0 }; //visited初始化
template <typename T>
class MGragh
{
T vertex[MaxSize]; //存放顶点的数组
int edge[MaxSize][MaxSize]; //存放边的数组
int vertexNum, edgeNum; //顶点数 边数
public:
MGragh(T a[], int n, int e);//构造函数
~MGragh() { }; //析构函数,邻接矩阵是静态存储非配,会自动释放
void DFTraverse(int v);//深度优先
void BFTraverse(int v);//广度优先
};
template<typename T>
MGragh<T>::MGragh(T a[], int n, int e)
{
int i, j, k;
vertexNum = n;
edgeNum = e;
for (int i = 0; i < vertexNum; i++) //存储顶点
vertex[i] = a[i];
for (i = 0; i < vertexNum; i++) //初始化邻接矩阵
for (j = 0; j < vertexNum; j++)
edge[i][j] = 0;
for (k = 0; k < edgeNum; k++) //依次输入每一条边
{
cout << "请输入第" << k + 1 << "条边";
cin >> i >> j;
edge[i][j] = 1; //这里是无向图的创建
edge[j][i] = 1;
}
}
template <typename T>
void MGragh<T>::DFTraverse(int v) //深度优先遍历
{
cout << vertex[v];
visited[v] = 1;
for (int j = 0; j < vertexNum; j++)
{
if (edge[v][j] == 1 && visited[j] == 0)
DFTraverse(j);
}
}
template <typename T>
void MGragh<T>::BFTraverse(int v) //广度优先遍历
{
int w, j, Q[MaxSize];
int front = -1;
int rear = -1;
cout << vertex[v];
visited[v] = 1;
Q[++rear] = v;
while (front != rear)
{
w = Q[++front];
for (j = 0; j < vertexNum; j++)
{
if (edge[w][j] == 1 && visited[j] == 0)
{
cout << vertex[j];
visited[j] = 1;
Q[++rear] = j;
}
}
}
}
int main()
{
int i;
char ch[] = { 'A','B','C','D','E' };
MGragh<char>MG(ch, 5, 6);
for (i = 0; i < MaxSize; i++)
visited[i] = 0;
cout << "深度优先遍历序列:";
MG.DFTraverse(0);
for (i = 0; i < MaxSize; i++)
visited[i] = 0;
cout << "\n广度优先遍历序列:";
MG.BFTraverse(0);
return 0;
}
邻接表
顺序存储+链接存储。
类似于树的孩子表示法。
边表:对于每个顶点v,将v的所有邻接点链成一个单链表。
方便所有边表的头指针进行存取操作,采用顺序存储。
顶点表:存储边的表头指针和存储顶点的数组构成邻接表的表头数组。
邻接表中方两种结点结构:顶点表结点&边表结点
对于网图,边表结点增设info域存储边上的信息。
#include<iostream>
using namespace std;
const int MaxSize = 10;
int visited[MaxSize] = { 0 };
struct EdgeNode //边表结点
{
int adjvex; //邻接点域
EdgeNode* next;
};
template<typename T>
struct VertexNode //顶点表结点
{
T vertex;
EdgeNode* firstEdge;
};
template<typename T>
class ALGraph
{
VertexNode<T>adjlist[MaxSize]; //存放顶点表的数组
int vertexNum, edgeNum; //图的顶点数和边数
public:
ALGraph(T a[], int n, int e); //建立n个顶点e条边的图
~ALGraph(); //析构函数
void DFTraverse(int v);
void BFTraverse(int v);
};
template<typename T>
ALGraph<T>::ALGraph(T a[], int n, int e)
{
EdgeNode* s = nullptr;
vertexNum = n;
edgeNum = e;
for (int i = 0; i < vertexNum; i++) //初始化
{
adjlist[i].vertex = a[i];
adjlist[i].firstEdge = nullptr;
}
for (int i = 0; i < edgeNum; i++)
{
cout << "输入边所依附的两个顶点编号:";
int j, k;
cin >> j >> k;
s = new EdgeNode; //生产边表结点s
s->adjvex = k;
s->next = adjlist[j].firstEdge; //将结点s插入表头
adjlist[j].firstEdge = s;
}
}
template<typename T>
ALGraph<T>::~ALGraph()
{
EdgeNode* p = nullptr, * q = nullptr;
for (int i = 0; i < vertexNum; i++)
{
p = q = adjlist[i].firstEdge;
while (p != nullptr)
{
p = p->next;
delete q;
q = p;
}
}
}
template<typename T>
void ALGraph<T>::DFTraverse(int v)
{
int j;
EdgeNode* p = nullptr;
cout << adjlist[v].vertex;
visited[v] = 1;
p = adjlist[v].firstEdge;
while (p!=nullptr)
{
j = p->adjvex;
if (visited[j] == 0)
DFTraverse(j);
p = p->next;
}
}
template<typename T>
void ALGraph<T>::BFTraverse(int v)
{
int w, j, Q[MaxSize]; //采用顺序队列
int front = -1, rear = -1;
EdgeNode* p = nullptr;
cout << adjlist[v].vertex;
visited[v] = 1;
Q[++rear] = v;
while (front != rear)
{
w = Q[++front];
p = adjlist[w].firstEdge;
while (p != nullptr)
{
j = p->adjvex;
if (visited[j] == 0)
{
cout << adjlist[j].vertex;
visited[j] = 1;
Q[++rear] = j;
}
p = p->next;
}
}
}
int main()
{
char ch[] = "ABCDEF";
ALGraph<char>ALG(ch,5,6);
for (int i = 0; i < MaxSize; i++)
visited[i] = 0;
cout << "深度优先遍历的序列是:";
ALG.DFTraverse(0);
for (int i = 0; i < MaxSize; i++)
visited[i] = 0;
cout << "广度优先遍历的序列是:";
ALG.BFTraverse(0);
//ALG.TopSort();
return 0;
}
邻接矩阵vs邻接表
1.空间性能比较
邻接矩阵空间代价O(n2),邻接表空间代价O(n+e)。
邻接矩阵存储所有可能的边,邻接表仅存储实际出现在图中的边。
邻接矩阵不需要指针的结构性开销。
图越稠密,邻接矩阵的空间效率更高。
2.时间性能比较
访问某顶点的所有邻接点:
邻接矩阵O(e/n)
邻接表O(n)
3.唯一性比较
邻接矩阵表示是唯一的;
邻接表表示不是唯一的,取决定边的输入次序以及结点在边表的插入算法。
4.对应关系
邻接表中顶点i的边表对应邻接矩阵的第i行。
整个邻接表可以看作邻接矩阵的带行指针的链接存储。
最小生成树
连通图的生成树:包含图中全部顶点的极小连通子图。
具有n个顶点的生成树有且仅有n-1条边。
最小生成树:代价最小的生成树。
Prim算法
时间复杂度:O(n2),与网中边数无关,适用于求稠密网中的最小生成树。
Kruskal算法
还是上面的图
不能有回路!如果加入最短边产生回路则舍弃
Kruskal算法基于边进行操作,可以采用边集数组存储。
连通分量的顶点所在的集合,由于涉及集合的查找和合并等操作,可以采用并查集来实现。
并查集:将集合中的元素组织成树的形式。
时间复杂度:O(elog2e),与边数有关,适用于求稀疏网的最小生成树。