《数据结构、算法与应用 —— C++语言描述》学习笔记 — 图
一、基本概念
1、顶点和边
图是一个用线或边连接在一起的定点或节点的集合。严格地说,图是有限集V和E的有序对,即 G = ( V , E ) G=(V,E) G=(V,E),其中V的元素称为顶点,E的元素称为边。
每一条边连接两个不同的顶点,而且用元组(i,j)来表示,其中 i 和 j 是边所连接的两个顶点。带方向的边是有向边,不带方向的边叫无向边。当且仅当(i,j)是图的边,我们称 i 和 j 是邻接的。
2、方向和权
如果图中所有边都是无向边,那么该图叫做无向图;如果所有的边都是有向边,则该图叫做有向图。在图的一些应用中。我们可能要为每条边赋予一个表示成本的值,我们称之为权。这时的图称为加权有向图和加权无向图。
对于有向图来说,有向边(i,j)是关联顶点 j 而关联于顶点 i。顶点 i 邻接至顶点 j,顶点 j 邻接于顶点 i。
3、路径
一个顶点序列 P = i 1 , i 2 , ⋅ ⋅ ⋅ , i k P=i_1,i_2,···,i_k P=i1,i2,⋅⋅⋅,ik是图或有向图 G = ( V , E ) G=(V,E) G=(V,E)的一条从 i 1 i_1 i1到 i k i_k ik的路径,当且仅当对于每个 j ( 1 ≤ j < k 1 \le j<k 1≤j<k),边( i j , i j + 1 i_j,i_{j+1} ij,ij+1)都在E中。
一条路径如果除第一个和最后一个顶点外,其余所有顶点均不同,那么该路径被称为一条简单路径。
图或有向图的每一条边都有长度,一条路径的长度是该路径所有边的长度之和。
对于图 G = ( V , E ) G=(V,E) G=(V,E),我们说G是连通的,当且仅当G的每一对顶点之间都有一条路径。
如果H的顶点和边的集合分别是G的顶点和边的集合的子集,那么称图H是图G的子图。一条起点和终点相同的简单路径称为环路。没有环路的连通无向图是一棵树。一个G的子图,如果包含G的所有顶点,且是一棵树,则称为G的生成树。
二、特性
在一个无向图中,于一个顶点 i 相关联的边数称为该顶点的度。
特性① 设
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)是一个无向图,令
n
=
∣
V
∣
,
e
=
∣
E
∣
n=|V|,e=|E|
n=∣V∣,e=∣E∣,则
(1)
∑
i
=
1
n
d
i
=
2
e
\sum_{i=1}^nd_i=2e
∑i=1ndi=2e
(2)
0
≤
e
≤
n
(
n
−
1
)
2
0\le e \le \frac{n(n-1)}{2}
0≤e≤2n(n−1)
一个具有 n 个顶点和 n ( n − 1 ) 2 {n(n-1)}{2} n(n−1)2条边的无向图是一个完全图。
设G是一个有向图。顶点 i 的入度 d i i n d_i^{in} diin是指关联至该顶点的边数。顶点 i 的出度 d i o u t d_i^{out} diout是指关联于该顶点的边数。
特性② 设
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)是一个有向图,令
n
=
∣
V
∣
,
e
=
∣
E
∣
n=|V|,e=|E|
n=∣V∣,e=∣E∣,则
(1)
∑
i
=
1
n
d
i
i
n
=
∑
i
=
1
n
d
i
o
u
t
=
e
\sum_{i=1}^nd_i^{in}=\sum_{i=1}^nd_i^{out}=e
∑i=1ndiin=∑i=1ndiout=e
(2)
0
≤
e
≤
n
(
n
−
1
)
0\le e \le n(n-1)
0≤e≤n(n−1)
三、抽象数据类型
1、边及迭代器
// Edge.h
#pragma once
template<class T>
class Edge
{
public:
virtual T vertex1() const = 0;
virtual T vertex2() const = 0;
virtual int weight() const = 0;
};
// VertexIterator.h
#pragma once
template <class T>
class VertexIterator
{
public:
virtual int next() = 0;
};
2、图接口
#pragma once
#include "Edge.h"
#include "VertexIterator.h"
template <class T>
class Graph
{
public:
virtual ~Graph() {}
virtual int numberOfVertices() const = 0;
virtual int numberOfEdges() const = 0;
virtual bool existsEdge(int, int) const = 0;
virtual void insertEdge(Edge<T>* edge) = 0;
virtual void eraseEdge(int, int) = 0;
virtual int degree(int) const = 0;
virtual int inDegree(int) const = 0;
virtual int outDegree(int) const = 0;
virtual bool directed() const = 0;
virtual bool weighted() const = 0;
virtual VertexIterator<T>* iterator(int) = 0;
};
四、无权图的描述
1、邻接矩阵
一个顶点图 G = ( V , E ) G=(V,E) G=(V,E)的邻接矩阵是一个 n ∗ n n*n n∗n矩阵(假设是A),其中的每个元素是0或1。假设V={1,2,3,···,n}。如果G是一个无向图,那么其中的元素定义如下: A ( i , j ) = { 1 如 果 ( i , j ) ∈ E 或 ( j , i ) ∈ E 0 其 他 A(i,j)=\left\{\begin{aligned}1 & \quad 如果(i,j)∈E或(j,i)∈E\\ 0 & \quad 其他 \end{aligned}\right. A(i,j)={10如果(i,j)∈E或(j,i)∈E其他如果G是有向图,那么 A ( i , j ) = { 1 如 果 ( i , j ) ∈ E 0 其 他 A(i,j)=\left\{\begin{aligned}1 & \quad 如果(i,j)∈E\\ 0 & \quad 其他 \end{aligned}\right. A(i,j)={10如果(i,j)∈E其他
从以上两个公式,我们不难得出以下结论:
① 无向图的邻接矩阵是对角线对称的,且
A
(
i
,
i
)
=
0
,
1
≤
i
≤
n
A(i,i) = 0,1 \le i \le n
A(i,i)=0,1≤i≤n。
② 对于n顶点的无向图,有
∑
1
n
A
(
i
,
j
)
=
∑
1
n
A
(
j
,
i
)
=
d
i
\sum_1^nA(i,j)=\sum_1^nA(j,i)=d_i
∑1nA(i,j)=∑1nA(j,i)=di。
③ 对于n顶点的有向图,有
∑
1
n
A
(
i
,
j
)
=
d
i
o
u
t
\sum_1^nA(i,j)=d_i^{out}
∑1nA(i,j)=diout和
∑
1
n
A
(
j
,
i
)
=
d
i
i
n
\sum_1^nA(j,i)=d_i^{in}
∑1nA(j,i)=diin,其中
1
≤
i
≤
n
1 \le i \le n
1≤i≤n。
2、邻接链表
一个顶点 i 的邻接表 是一个线性表,它包含所有邻接于顶点 i 的顶点。在一个图的邻接表描述中,图的每一个顶点都有一个邻接表。当邻接表用链表表示时,就是邻接链表。
我们可以使用类型为链表的数组 aList 来描述所有邻接表。aList[i].firstNode 指向顶点 i 的邻接表的第一个顶点。如果 x 指向链表 aList[i] 的一个顶点,那么(i,x->element)是图的一条边。
一个指针和一个整数各需要4字节的存储空间,因此用邻接链表描述一个n顶点图需要
8
(
n
+
1
)
8(n+1)
8(n+1)字节存储
n
+
1
n+1
n+1个firstNode指针和aList链表的listSize域,需要
4
∗
2
∗
m
4*2*m
4∗2∗m字节存储 m 个链表节点,每个链表节点的两个域 next 和 element 各需4字节,其中对无向图,m=2e,对有向图,m=e,其中 e 是边数。
当 e 远远小于 n 2 n^2 n2 时,邻接链表比邻接矩阵需要更少的空间。例如,一个 e = n e=n e=n 的有向图,用邻接链表描述需要 16 n + 8 16n+8 16n+8 字节,用压缩的邻接矩阵描述需要 n 2 n^2 n2 字节。因此,当 e = n ≥ 17 e=n \ge 17 e=n≥17时,邻接链表描述所需空间更少。
3、临界数组
在邻接数组中,每一个邻接表用一个数组线性表而非链表来描述。我们可以选择使用二维数组实现,也可以使用指针数组实现。
五、加权图的描述
将无向图的描述进行简单扩充就可得到加权图的描述。用成本邻接矩阵C描述加权图。如果C(i,j)表示边(i,j)的权,那么它的使用方法和邻接矩阵的使用方法一样。在这种描述方法中,需要给不存在的边指定一个值,一般是一个很大的值。在实现代码中,我们要求用户用noEdge表示这个值。
如果链表的元素有两个域 vertex 和 weight,就可以从相应的无权图的邻接链表得到加权图的邻接链表。
六、类关系
我们可以通过继承实现图的邻接矩阵和邻接链表表示,其继承关系如下:
七、抽象结构
1、图
#pragma once
#include "Edge.h"
#include "VertexIterator.h"
template <class T>
class Graph
{
public:
virtual ~Graph() {}
virtual int numberOfVertices() const = 0;
virtual int numberOfEdges() const = 0;
virtual bool existsEdge(int, int) const = 0;
virtual void insertEdge(Edge<T>* edge) = 0;
virtual void eraseEdge(int, int) = 0;
virtual int degree(int) const = 0;
virtual int inDegree(int) const = 0;
virtual int outDegree(int) const = 0;
virtual bool directed() const = 0;
virtual bool weighted() const = 0;
virtual VertexIterator<T>* iterator(int) = 0;
};
2、边
#pragma once
template<class T>
class Edge
{
public:
virtual ~Edge() {}
virtual int vertex1() const = 0;
virtual int vertex2() const = 0;
virtual T weight() const = 0;
};
3、顶点迭代器
#pragma once
template <class T>
class VertexIterator
{
public:
virtual ~VertexIterator() {}
virtual int next() = 0;
virtual int next(T& weight) = 0;
};
八、加权有向图的邻接矩阵实现
1、声明
#pragma once
#include "Graph.h"
#include <stdexcept>
#include <algorithm>
#include <iostream>
template <class T>
class adjacencyWDGraph : public Graph<T>
{
public:
class adjacencyWDIterator : public VertexIterator<T>
{
public:
adjacencyWDIterator(T* row, T noEdge, int vertexNum);
virtual ~adjacencyWDIterator() {}
virtual int next() override;
virtual int next(T& weight) override;
protected:
T* row;
T noEdge;
int vertexNum;
int currentVertex;
};
adjacencyWDGraph(int vertexNum = 0, T noEdge = T());
adjacencyWDGraph(const adjacencyWDGraph& other);
adjacencyWDGraph(adjacencyWDGraph&& other);
virtual ~adjacencyWDGraph() override;
adjacencyWDGraph& operator=(const adjacencyWDGraph& other);
adjacencyWDGraph& operator=(adjacencyWDGraph&& other);
virtual int numberOfVertices() const override;
virtual int numberOfEdges() const override;
virtual bool existsEdge(int, int) const override;
virtual void insertEdge(Edge<T>* edge) override;
virtual void eraseEdge(int, int) override;
virtual int degree(int) const override;
virtual int inDegree(int) const override;
virtual int outDegree(int) const override;
virtual bool directed() const override;
virtual bool weighted() const override;
virtual VertexIterator<T>* iterator(int) override;
protected:
int vertexNum;
int edgeNum;
T** elements;
T noEdge;
bool checkEdge(int row, int column) const;
void checkVertex(int vertex) const;
private:
void makeCopyAndSwap(const adjacencyWDGraph& other);
adjacencyWDGraph makeCopy(const adjacencyWDGraph& other);
void swap(adjacencyWDGraph& other);
void make2DArray(T**& arr, int row, int column);
};
2、构造析构
template<class T>
inline adjacencyWDGraph<T>::adjacencyWDGraph(int vertexNum, T noEdge)
{
if (vertexNum <= 0) {
throw std::invalid_argument("vertexNum must be > 0");
}
this->vertexNum = vertexNum;
edgeNum = 0;
this->noEdge = noEdge;
make2DArray(this->elements, vertexNum, vertexNum);
std::for_each(elements, elements + vertexNum, [vertexNum, noEdge](T* arr) {std::fill(arr, arr + vertexNum, noEdge); });
}
template<class T>
inline adjacencyWDGraph<T>::~adjacencyWDGraph()
{
std::for_each(elements, elements + vertexNum, [](T* arr) {delete arr; });
delete elements;
}
不难看出,构造函数的时间复杂度为 O(n)。
3、拷贝控制
template<class T>
inline adjacencyWDGraph<T> adjacencyWDGraph<T>::makeCopy(const adjacencyWDGraph& other)
{
adjacencyWDGraph returnGraph;
returnGraph.vertexNum = other.vertexNum;
returnGraph.edgeNum = other.edgeNum;
make2DArray(this->elements, returnGraph.vertexNum, returnGraph.vertexNum);
for (int i = 0; i < other.vertexNum; ++i)
{
copy(other.elements[i], other.elements[i] + other.vertexNum, returnGraph.elements[i]);
}
return returnGraph;
}
template<class T>
inline void adjacencyWDGraph<T>::make2DArray(T**& arr, int row, int column)
{
arr = new T * [row];
std::for_each(arr, arr + row, [&column](T*& arr) {arr = new T[column]; });
}
4、边界检查
我们需要保证插入或删除的边不是自连边,且顶点符合边界要求:
template<class T>
inline bool adjacencyWDGraph<T>::checkEdge(int row, int column) const
{
if (row < 0 || row >= vertexNum || column < 0 || column >= vertexNum || row == column)
{
return false;
}
return true;
}
template<class T>
inline void adjacencyWDGraph<T>::checkVertex(int vertex) const
{
if (vertex < 0 || vertex >= vertexNum)
{
throw std::invalid_argument("illegal vertex index");
}
}
5、迭代器
由于使用的是有向图,因此这里提供的迭代器访问只需按行顺序遍历:
template<class T>
inline adjacencyWDGraph<T>::adjacencyWDIterator::adjacencyWDIterator(T* row, T noEdge, int vertexNum):
row(row),
noEdge(noEdge),
vertexNum(vertexNum),
currentVertex(0)
{
}
template<class T>
inline int adjacencyWDGraph<T>::adjacencyWDIterator::next()
{
T weight;
return next(weight);
}
template<class T>
inline int adjacencyWDGraph<T>::adjacencyWDIterator::next(T& weight)
{
for (int i = currentVertex + 1; i < vertexNum; ++i)
{
if (row[i] != noEdge)
{
currentVertex = i;
weight = row[i];
return i;
}
}
currentVertex = vertexNum + 1;
return -1;
}
6、边声明
#pragma once
#include "Edge.h"
template<class T>
class WEdge : public Edge<T>
{
public:
WEdge(int v1, int v2, T weight);
~WEdge() {}
virtual int vertex1() const override;
virtual int vertex2() const override;
virtual T weight() const override;
protected:
int v1;
int v2;
T w;
};
template<class T>
inline WEdge<T>::WEdge(int v1, int v2, T weight):
v1(v1),
v2(v2),
w(weight)
{
}
template<class T>
inline int WEdge<T>::vertex1() const
{
return v1;
}
template<class T>
inline int WEdge<T>::vertex2() const
{
return v2;
}
template<class T>
inline T WEdge<T>::weight() const
{
return w;
}
7、边操作
template<class T>
inline void adjacencyWDGraph<T>::insertEdge(Edge<T>* edge)
{
auto vertex1 = edge->vertex1();
auto vertex2 = edge->vertex2();
if (!checkEdge(vertex1, vertex2) || vertex1 == vertex2)
{
throw std::invalid_argument("the edge doesn't exist");
}
if (elements[vertex1][vertex2] == noEdge)
{
edgeNum++;
}
elements[vertex1][vertex2] = edge->weight();
}
template<class T>
inline void adjacencyWDGraph<T>::eraseEdge(int row, int column)
{
if (checkEdge(row, column) && elements[row][column] != noEdge)
{
elements[row][column] = noEdge;
edgeNum--;
}
}
8、入度出度
template<class T>
inline int adjacencyWDGraph<T>::inDegree(int vertex) const
{
checkVertex(vertex);
int count = 0;
std::for_each(elements, elements + vertexNum,
[&count, vertex, this](T* arr)
{
if (arr[vertex] != noEdge)
{
count++;
}
});
return count;
}
template<class T>
inline int adjacencyWDGraph<T>::outDegree(int vertex) const
{
checkVertex(vertex);
int count = 0;
std::for_each(elements[vertex], elements[vertex] + vertexNum,
[&count, this](T element)
{
if (element != noEdge)
{
count++;
}
});
return count;
}
9、其余接口
template<class T>
inline int adjacencyWDGraph<T>::numberOfVertices() const
{
return vertexNum;
}
template<class T>
inline int adjacencyWDGraph<T>::numberOfEdges() const
{
return edgeNum;
}
template<class T>
inline int adjacencyWDGraph<T>::degree(int) const
{
throw std::runtime_error("no such method");
return 0;
}
template<class T>
inline bool adjacencyWDGraph<T>::directed() const
{
return true;
}
template<class T>
inline bool adjacencyWDGraph<T>::weighted() const
{
return true;
}
template<class T>
inline VertexIterator<T>* adjacencyWDGraph<T>::iterator(int vertex)
{
checkVertex(vertex);
return new adjacencyWDIterator(elements[vertex], noEdge, vertexNum);
}
九、加权无向图
加权无向图通过派生于加权有向图实现。我们需要重写 insertEdge、eraseEdge 以实现对称边的插入和删除;除此之外,directed 方法应该直接返回 false,degree 方法也需要被定义。