图的概念
引用百科的描述:
在数学的分支图论中,图(Graph)用于表示物件与物件之间的关系,是图论的基本研究对象。一张图由一些小圆点(称为顶点或结点)和连结这些圆点的直线或曲线(称为边)组成。英国数学家西尔维斯特在 1878 年首次提出“图”这一名词。
由上可知,我们学习图的数据结构,也就是在学习图论中的一部分内容。所以当你遇到不理解的内容时, 建议去看一看图论的相关教材,也许能解决你的困惑。
通常我们用一个代表有序对的二元组表达式: 来表示一个图结构,其中 表示顶点集,表示边集:
边集由所有无序顶点对构成(换句话说,边连接了顶点对)。对于一个边 {x,y},顶点 x,y 被称作是边的端点,边则被称为连接了此两个点。
顶点间的关系(边)
在顶点集合所包含的若干个顶点之间,可能存在着某种两两关系,如果某两个点之间确实存在这种关系的话,我们就在这两个点之间连边,这样就得到了边集的一个成员,也就是一条边。如果对应到社交网络中,把顶点看做是用户,如果这两个用户被连上了一条边,就表示他们之间存在好友关系。
无向图
用边来表示好友关系的话,像聊天软件里面的好友就可以看做是双向关注的社交网络,毕竟只有添加了好友才可以聊天,删除了就不能聊天了。这种两个点之间用一条无向的边连接起来的图叫做无向图。
有向图
像短视频里面的主播,我们可以给他点关注双击666,这样的关系就不能用无向图来表示了,因为我们关注了主播,主播不一定会关注我们,如果主播没有关注我们,那么这样的关系就是单向的,用一个有向边来连接两个顶点。如果我们跟主播互相关注了,这样可以看做是无向图的好友关系,但是我们也可以使用两条有向边来表示好友关系。所以在研究图的数据结构的时候,我们一般研究有向图,因为它可以用来表示无向图。
稀疏图和稠密图
规定:当 时,叫做稀疏图,也就是这时图里面的边很少,反之就叫做稠密图,比如用户之间都相互关注,这时就是稠密图。
顶点的度
图中某个顶点的度是通过某个顶点有多少条边算出来的,比如:某个顶点有 3 条边,那么这个顶点的度就是 3。
出度和入度
在有向图中我们会看到有的有向边是指向某个顶点的,也就是以某个顶点为终点。而有的有向边是从这个顶点出发指向其它顶点的,也就是以某个顶点为起点。
这样以某个顶点为终点的有向边的数量我们叫该顶点的入度,以某个顶点为起点的有向边的数量我们叫顶点的出度。
不难看出,在有向图中,一个顶点的度等于出度和入度之和。
图的存储结构
在研究图的数据结构之前我们还有一些先修的知识点要学习,图是按照什么方式来存储的。
邻接矩阵
邻接矩阵是用来存储图的顶点之间的关系的,通过邻接矩阵可以很方便地反映出这种关系。
定义:若图 的顶点有 个,分别标为:,那么构造出一个 阶的矩阵 ,让它符合这样的条件:若 ,,否则。
通过一个例子来理解一下,假设有这样一个图:
根据邻接矩阵的定义,可以构造出如下矩阵:
这里解释一下这个矩阵是如何构造出来的:
可以对照着这个图表理解:
- 当 i = 1, j = 1 时,明显 自己没有边,于是矩阵的
- 当 i = 1, j = 2 时, 有指向 的边,于是矩阵的
- 当 i = 1, j = 3 时, 有指向 的边,于是矩阵的
- 当 i = 1, j = 4 时, 没有有指向 的边,于是矩阵的
- 当 i = 2, j = 1 时, 没有有指向 的边,于是矩阵的
- 当 i = 2, j = 2 时, 自己没有边,于是矩阵的
- 当 i = 2, j = 3 时, 没有有指向 的边,于是矩阵的
- 当 i = 2, j = 4 时, 没有有指向 的边,于是矩阵的
...按照这种方法就可以把矩阵构造出来了
可以看到,矩阵中的每一行,从上到下分别表示每个顶点()与其它顶点的出度的关系。如果与某个顶点有出度关系,就在某个顶点处标上 1。
矩阵中的每一列,从左到右分别表示每个顶点()与其它顶点的入度的关系。如果与某个顶点有入度关系,就在某个顶点处标上 1。其实我们把出度的关系做好以后,入度的关系就自动标好了。
邻接表
邻接表也是用来描述图的顶点之间间的关系的一种工具。如果用文字法描述的话就是这样的:
有这样一个无向图,它的邻接表就描述为:a 邻接于 b,c;b 邻接于 a,c;c 邻接于 a,b。
如果是有向图,则邻接表表示的是各顶点的出度的关系,相反逆邻接表就表示各顶点入度的关系了。
还是以邻接矩阵那个图作为例子,可以得到如下邻接表的描述:
- 邻接于
- 无邻接
- 邻接于
- 邻接于
如果用链表来表示的话就是这样的:
v1->v2->v3
v2
v3->v4
v4->v2
这些链表的表头结点一般会存储在一个数组中,方便访问顶点。
邻接表的好处是很容易就能知道某一个顶点和哪些顶点相连接。
邻接矩阵和邻接表都可以用来存储图,它们各有好坏,比如对于有 n 个顶点的图,邻接矩阵总是需要 的存储空间,当边数很少的时候,会造成空间浪费。
所以,如果是稀疏图,那么一般选用邻接表来存储,如果是稠密图就选用邻接矩阵来存储。
用邻接矩阵构造图
理论学习完后,咱们来实现一下如何用邻接矩阵存储图。
/**
* 图的结构
* @param {*} length 邻接矩阵的阶数,也就是图的顶点数
*/
function Graph(length) {
this.length = length;
// 初始化存矩阵的二维数组
this.matrix = [];
for (let i = 0; i < length; i++) {
// 这里也可以不用把类数组转成数组
this.matrix[i] = Array.from(new Int8Array(length));
}
}
定义好图的结构后,咱们来手动创建一个邻接矩阵,以邻接矩阵的图为例。
/**
* 插入邻接矩阵数据
* @param {*} graph 图
* @param {*} a 0-有向图,1-无向图
* @param {*} i 顶点,相当于矩阵下标
* @param {*} j 顶点,相当于矩阵下标
* @returns
*/
function insert(graph, a, i, j) {
if (i < 0 || i >= graph.n || j < 0 || j >= graph.n) {
return;
}
// 有向图就只需要一条边
if (a === 0) {
graph.matrix[i][j] = 1;
} else {
// 无向图有两条边,对称的
graph.matrix[i][j] = 1;
graph.matrix[j][i] = 1;
}
}
/**
* 输出邻接矩阵
* @param {*} graph 图
*/
function output(graph) {
let str = "";
for (let i = 0; i < graph.length; i++) {
for (let j = 0; j < graph.length; j++) {
str += graph.matrix[i][j] + " ";
}
str += "\n";
}
console.log(str);
}
const graph = new Graph(4);
// 手动创建邻接矩阵
insert(graph, 0, 0, 1);
insert(graph, 0, 0, 2);
insert(graph, 0, 2, 3);
insert(graph, 0, 3, 1);
output(graph);
console.log(graph);
这样就完成了邻接矩阵的创建和使用。
用邻接表够造图
要构造邻接表,我们需要一个用于保存顶点的链表:
/**
* 保存顶点的链表结点
* @param {*} vertex 顶点
*/
function Node(vertex) {
this.vertex = vertex;
this.next = null;
}
然后需要邻接表的结构,邻接表里面有两个属性,一个是图的顶点数,一个是用于保存边的关系的数组,数组里面保存的是以各个顶点为头结点的链表:
/**
* 图的邻接表结构
* @param {*} length 长度
*/
function GraphList(length) {
this.length = length;
this.edges = [];
for (let i = 0; i < length; i++) {
this.edges[i] = null;
}
}
接下来就要创建邻接表了:
/**
* 按倒序插入结点(结点的顺序如何没有关系)
* @param {*} head 头结点
* @param {*} index 顶点
* @returns 倒序的链表
*/
function insertNode(head, index) {
const node = new Node(index);
node.next = head;
head = node;
return head;
}
/**
* 创建邻接表
* @param {*} graph 图
* @param {*} a 0-有向边,1-无向边
* @param {*} i 顶点
* @param {*} j 顶点
* @returns
*/
function insertGraph(graph, a, i, j) {
if (i < 0 || i >= graph.length || j < 0 || j >= graph.length) {
return;
}
if (a === 0) {
graph.edges[i] = insertNode(graph.edges[i], j);
} else {
graph.edges[i] = insertNode(graph.edges[i], j);
graph.edges[j] = insertNode(graph.edges[j], i);
}
}
遍历输出邻接表
function outputGraph(graph) {
let str = "";
for (let i = 0; i < graph.length; i++) {
str += i + ':';
for (let j = graph.edges[i]; j !== null; j = j.next) {
str += j.vertex + ' ';
}
str += '\n';
}
console.log(str);
}
const graph = new GraphList(4);
// 手动创建邻接表来做测试
insertGraph(graph, 0, 0, 1);
insertGraph(graph, 0, 0, 2);
insertGraph(graph, 0, 2, 3);
insertGraph(graph, 0, 3, 1);
console.log(graph);
outputGraph(graph);