图
一.图的基本概念
1.什么是图?
图是由顶点集合和顶点之间的关系构成的一种数据结构.
在现实世界中应用极其广泛,比如人与人之间的朋友关系,地图中各个地点之间的关系
简单的图示例:
2.顶点和边
图的节点称为顶点,图中节点与节点之间的关系称之为边
3.有向图和无向图
在图中如果存在节点x、y,从节点x出发可以到达节点y,但是从节点y出发不能到达节点x,称这样的图为有向图。
如果一个图中任意相连的节点,从x可以到达y,从y也可以到达x,称这样的图为无向图
4.有向完全图和无向完全图
完全图:如果从图中的任何一个节点出发都能到达其它节点,称这样的图为完全图
有向完全图:
在有向图中任意两个节点之间有且仅有方向相反的边,这样的图称之为有向完全图
无向完全图:
在无向图中任意节点间都有一条边,这样的图称之为无向完全图
5.邻接顶点
同一条边上的两个顶点称之为对方的邻接顶点
6.顶点的度
顶点的度指的是和顶点相关联的边的条数
对于有向图存在入度和出度之分,入度指的是指向是以当前顶点为终点的边的条数,出度指的是以当前顶点为源点的边的条数,有向图的度等于入度和出度之和;
无向图的度等于入度,也等于出度
7.路径
路径指的是从一个节点到另一个节点的边
8.权
和图的边相关的参数
9.路径长度
对于不带权的图,一条路径的路径长度指的是这条路径上的边的条数
对于带权的图,一条路径的路径长度指的是这条路径上的权值之和
10.简单路径和回路
如果一个路径中的所有节点都不重复,称这样的路径为简单路径
如果一条路径上的节点有重复,也就是说从某个节点出发沿着路径仍然可以到达该节点,说明这条路径有回路
11.子图
一个图的一部分
12.连通图和强连通图
连通图:
在一个图中,如果任意一对节点之间都有路径,称这个图是连通图
强连通图:
如果在一个有向图中,任意一对节点之间存在方向相反的路径,称这样的图是强连通图
13.生成树
一个图中最小连通子图称之为这个图的生成树,有n个顶点的连通图的生成树有n个顶点和n- 1条边
二.图的存储结构
1.邻接矩阵法
将节点数据存放在一个数组中,将节点之间的权值用二维矩阵表示
代码实现:
//邻接矩阵表示图
namespace LinkedMatrix
{
/*
V:节点类型
W:权值类型
IsDirect:图是否为有向图,默认为false表示无向图
*/
template<class V, class W, bool IsDirect = false>
class Graph
{
public:
Graph(V* nodes, int sz)
:_nodes(nodes, nodes + sz)
{
_edges.resize(sz);
for (int i = 0; i < sz; ++i)
{
_edges[i].resize(sz, 0);
}
}
//添加一条边
void addEdge(const V& srcNode, const V& desNode, const W& weight)
{
size_t srcIdx = getIndex(srcNode);
size_t desIdx = getIndex(desNode);
_edges[srcIdx][desIdx] = weight;
//无向图需要添加另一条边
if (!IsDirect)
{
_edges[desIdx][srcIdx] = weight;
}
}
//删除一条边
void removeEdge(const V& srcNode, const V& desNode)
{
size_t srcIdx = getIndex(srcNode);
size_t desIdx = getIndex(desNode);
_edges[srcIdx][desIdx] = 0;
//无向图的话两条边都要删除
if (!IsDirect)
{
_edges[desIdx][srcIdx] = 0;
}
}
//获取节点的度
size_t getDegree(const V& nodeVal)
{
size_t nodeIdx = getIndex(nodeVal);
size_t degree = 0;
size_t len = _nodes.size();
//获取出度
for (size_t i = 0; i < len; ++i)
{
if (_edges[nodeIdx][i] != 0)
{
++degree;
}
}
//如果是有向图,获取入度
if (IsDirect)
{
for (size_t i = 0; i < len; ++i)
{
if (_edges[i][nodeIdx] != 0)
{
++degree;
}
}
}
return degree;
}
//打印图
void PrintGraph()
{
int len = _nodes.size();
cout << "-----------图的邻接矩阵表示-----------\n\n";
//打印第一行内容
cout << " \t";
for (int i = 0; i < len; ++i)
{
cout << _nodes[i] << "\t";
}
cout << endl;
for (int i = 0; i < len; ++i)
{
cout << _nodes[i] << "\t";
for (int j = 0; j < len; ++j)
{
cout << _edges[i][j] << "\t";
}
cout << endl;
}
}
private:
vector<V> _nodes; //节点集合
vector<vector<W>> _edges; //边的集合,表示从当前节点出发能直接到达节点的边
static const size_t npos = -1; //size_t的最大值,在成员函数中有用到
//在节点集合找节点对应下标,如果找不到返回size_t的最大值
size_t getIndex(const V& nodeVal)
{
for (size_t i = 0; i < _nodes.size(); ++i)
{
if (_nodes[i] == nodeVal)
{
return i;
}
}
return npos;
}
};
}
2.邻接表法
将数据节点存放在数组中,在另外一个数组中存放从每个节点出发的边,这些边之间用链表相连
代码实现
//邻接表表示图
namespace LinkedTable
{
//邻接表中的一条边
template<class W>
struct LinkEdge
{
//src:源点下标, des:终点下标 weight:源点到终点的路径对应的权值
LinkEdge(size_t src, size_t des, const W& weight)
:_src(src)
, _des(des)
, _weight(weight)
, _next(nullptr)
{}
size_t _src; //边的起点下标
size_t _des; //边的终点下标
W _weight; //边的权值
LinkEdge* _next; //下一条边,这个属性的设置是为了保证一种链式结构的实现
};
/*
V:节点数据类型
W:权值数据类型
IsDirect表示是否为有向图,默认false表示无向图
*/
template<class V, class W, bool IsDirect = false>
class Graph
{
typedef LinkEdge<W> LinkEdge;
public:
Graph(V* nodes, size_t sz)
:_nodes(nodes, nodes + sz)
{
_edges.resize(sz, nullptr);
}
//添加一条边
void AddEdge(const V& srcNode, const V& desNode, const W& weight)
{
size_t srcIdx = getIndex(srcNode);
size_t desIdx = getIndex(desNode);
AddEdgeHelper(srcIdx, desIdx, weight);
//无向图需要再添加一条边
if (!IsDirect)
{
AddEdgeHelper(desIdx, srcIdx, weight);
}
}
//删除一条边
void removeEdge(const V& srcNode, const V& desNode)
{
size_t srcIdx = getIndex(srcNode);
size_t desIdx = getIndex(desNode);
removeEdgeHelper(srcIdx, desIdx);
if (!IsDirect)
{
removeEdgeHelper(desIdx, srcIdx);
}
}
//获取节点的度
size_t getDegree(const V& nodeVal)
{
size_t idx = getIndex(nodeVal);
size_t degree = 0;
LinkEdge* curNode = _edges[idx];
//获取出度
while (curNode)
{
++degree;
curNode = curNode->_next;
}
//有向图需要获取入度
if (IsDirect)
{
for (size_t i = 0; i < _edges.size(); ++i)
{
LinkEdge* node = _edges[i];
while (node)
{
if (node->_des == idx)
{
++degree;
}
node = node->_next;
}
}
}
return degree;
}
//打印图
void PrintGraph()
{
cout << "--------邻接表法创建图如下------------" << endl;
for (size_t i = 0; i < _nodes.size(); ++i)
{
cout << _nodes[i] << ": ";
LinkEdge* curNode = _edges[i];
while (curNode)
{
cout << "(" << _nodes[curNode->_src] << "--->" << _nodes[curNode->_des] << " : " << curNode->_weight << ");";
curNode = curNode->_next;
}
cout << endl;
}
}
//从某个节点出发进行图的广度优先遍历
void BFS(const V& nodeVal)
{
queue<V> q;
vector<bool> visited(_nodes.size(), false); //标记数组,标记当前节点是否访问过
q.push(nodeVal);
BFSHelper(q, visited);
//处理非连通图
for (size_t i = 0; i < _nodes.size(); ++i)
{
if (!visited[i])
{
q.push(_nodes[i]);
BFSHelper(q, visited);
}
}
}
//图的深度优先搜索
void DFS()
{
size_t len = _nodes.size();
for (size_t i = 0; i < len; ++i)
{
DFSHelper(i);
cout << endl;
}
}
private:
vector<V> _nodes; //节点集合
vector<LinkEdge*> _edges; //节点对应的边的集合,从当前下标对应节点出发的边用一个链表连接起来
static const size_t npos = -1; //size_t对应的最大值,用于类中的成员函数
//找节点在节点数组中对应的下标,找不到返回npos
size_t getIndex(const V& nodeVal)
{
for (size_t i = 0; i < _nodes.size(); ++i)
{
if (_nodes[i] == nodeVal)
{
return i;
}
}
return npos;
}
//添加边的辅助函数
void AddEdgeHelper(size_t srcIdx, size_t desIdx, const W& weight)
{
LinkEdge* head = _edges[srcIdx];
LinkEdge* newEdge = new LinkEdge(srcIdx, desIdx, weight);
//头插
newEdge->_next = head;
_edges[srcIdx] = newEdge;
}
//删除边的辅助函数
//删除从srcIdx到desIdx的这条边
void removeEdgeHelper(size_t srcIdx, size_t desIdx)
{
LinkEdge* curNode = _edges[srcIdx];
LinkEdge* prev = nullptr;
while (curNode)
{
if (curNode->_des == desIdx)
{
if (prev == nullptr)
{
_edges[srcIdx] = nullptr;
}
else
{
prev->_next = curNode->_next;
}
delete curNode;
break;
}
prev = curNode;
curNode = curNode->_next;
}
}
//图的广度优先遍历辅助函数
void BFSHelper(queue<V>& q, vector<bool>& visited)
{
while (!q.empty())
{
V front = q.front();
q.pop();
size_t index = getIndex(front);
if (!visited[index])
{
cout << front << ": (";
LinkEdge* curNode = _edges[index];
while (curNode)
{
cout << _nodes[curNode->_src] << "-->" << _nodes[curNode->_des] << ";";
curNode = curNode->_next;
}
cout << ")" << endl;
visited[index] = true;
}
}
}
//深度优先搜索辅助函数
void DFSHelper(size_t index)
{
cout << _nodes[index] << "(";
LinkEdge* curNode = _edges[index];
while (curNode)
{
cout << _nodes[curNode->_src] << "--->" << _nodes[curNode->_des] << ";";
curNode = curNode->_next;
}
cout << ")";
}
};
}
三.图的两种遍历方式
这两种遍历方式的实现在邻接表表示法中都有实现
1.深度优先
一条路走到黑
2.广度优先
一次性处理一批数据