1.思维导图
(学艺不精见谅)
2.具体内容
(1).图的定义及相关概念
- 图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其 中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
注意:线性表中可以没有元素,称为空表。树中可以没有结点,叫做空树。但是在图中不允许 没有顶点,可以没有边。
- 无向边:若顶点Vi和Vj之间的边没有方向,称这条边为无向边(Edge),用
(Vi,Vj)
来表示。 - 无向图(Undirected graphs):图中任意两个顶点的边都是无向边。
- 有向边:若从顶点Vi到Vj的边有方向,称这条边为有向边,也称为弧(Arc),用
<Vi, Vj>
来表示,其中Vi称为弧尾(Tail),Vj称为弧头(Head)。 - 有向图(Directed graphs):图中任意两个顶点的边都是有向边。
- 简单图:不存在自环(顶点到其自身的边)和重边(完全相同的边)的图
- 无向完全图:无向图中,任意两个顶点之间都存在边。
- 有向完全图:有向图中,任意两个顶点之间都存在方向相反的两条弧。
- 稀疏图;有很少条边或弧的图称为稀疏图,反之称为稠密图。
- 权:表示从图中一个顶点到另一个顶点的距离或耗费。
- 网:带有权重的图
- 度:与特定顶点相连接的边数;
- 出度、入度:有向图中的概念,出度表示以此顶点为起点的边的数目,入度表示以此顶点为终点的边的数目;
- 环:第一个顶点和最后一个顶点相同的路径;
- 简单环:除去第一个顶点和最后一个顶点后没有重复顶点的环;
- 连通图:任意两个顶点都相互连通的图;
- 极大连通子图:包含竟可能多的顶点(必须是连通的),即找不到另外一个顶点,使得此顶点能够连接到此极大连通子图的任意一个顶点;
- 连通分量:极大连通子图的数量;
- 强连通图:此为有向图的概念,表示任意两个顶点a,b,使得a能够连接到b,b也能连接到a 的图;
- 生成树:n个顶点,n-1条边,并且保证n个顶点相互连通(不存在环);
- 最小生成树:此生成树的边的权重之和是所有生成树中最小的;
- AOV网:在有向图中若以顶点表示活动,有向边表示活动之间的先后关系
- AOE网:在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间
(2)图的存储结构
2.1、邻接矩阵
定义:图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称邻接矩阵)存储图中的边或弧的信息。
无向图由于边不区分方向,所以其邻接矩阵是一个对称矩阵。邻接矩阵中的0表示边不存在,主对角线全为0表示图中不存在自环。
在带权有向图的邻接矩阵中,数字表示权值weight,「无穷」表示弧不存在。由于权值可能为0,所以不能像在无向图的邻接矩阵中那样使用0来表示弧不存在。
void CreateMGraph(MGraph* G)
{
int i, j, k, w;
printf("please input number of vertex and edge:\n");
scanf("%d,%d", &G->numVertexes, &G->numEdges); //输入顶点数和边数
getchar(); //可以获取回车符
for (i = 0; i < G->numVertexes; i++) //读入顶点信息,建立顶点表
scanf("%c", &G->vers[i]);
getchar(); //可以获取回车符
for (i = 0; i < G->numVertexes;i++)
for (j = 0; j < G->numVertexes;j++)
G->arc[i][j] = INFINITY; //邻接矩阵初始化
for (k = 0; k < G->numEdges;k++) //读入numEdges条边,建立邻接矩阵
{
printf("input edge(vi,vj) row(i),col(j),weight(w):\n");
scanf("%d,%d,%d", &i, &j, &w); //输入边(vi,vj),以及上面的权值
getchar(); //可以获取回车符
G->arc[i][j] = w;
G->arc[j][i] = G->arc[i][j]; //因为是无向图,所有是对称矩阵
}
}
优缺点:
- 优点:结构简单,操作方便
- 缺点:对于稀疏图,这种实现方式将浪费大量的空间。
2.2、邻接表
邻接表是一种将数组与链表相结合的存储方法。其具体实现为:将图中顶点用一个一维数组存储,每个顶点Vi的所有邻接点用一个单链表来存储。这种方式和树结构中孩子表示法一样。如图:
有向图的邻接表是以顶点为弧尾来存储边表的,这样很容易求一个顶点的出度(顶点对应单链表的长度),但若求一个顶点的入度,则需遍历整个图才行。这时可以建立一个有向图的逆邻接表即对每个顶点v都建立一个弧头尾v的单链表。如上图所示。
void CreateALGraph(GraphAdjList* G)
{
int i, j ,k,w;
EdgeNode *e;
printf("please input number of vertex and edge:\n");
scanf("%d,%d",&G->numVertexes,&G->numEdges); //输入顶点数和边数
getchar(); //可以获取回车符
for (i = 0; i < G->numVertexes;i++) //输入顶点信息
{
scanf("%c", &G->adjList[i].data); //输入顶点信息
G->adjList[i].firstedge = NULL; //将边表置为空
}
getchar(); //可以获取回车符
for (k = 0; k < G->numEdges;k++)
{
printf("input edge(vi,vj) vertexs series and the weight:\n");
scanf("%d,%d,%d", &i, &j,&w);
getchar();
//由于是无向图,对称矩阵,当我们设置边以后,需要在两个地方设置结点
e = (EdgeNode *)malloc(sizeof(EdgeNode));
//使用头插法将数据插入(主要是头插法方便),我们插入不需要考虑顺序,因为链表结点都是与数组顶点相连接的
e->adjvex = j;
e->next = G->adjList[i].firstedge;
e->weight = w;
G->adjList[i].firstedge = e;
e = (EdgeNode*)malloc(sizeof(EdgeNode));
e->adjvex = i;
e->next = G->adjList[j].firstedge;
e->weight = w;
G->adjList[j].firstedge = e;
}
}
2.3、十字链表
十字链表(Orthogonal List)是将邻接表和逆邻接表相结合的存储方法,它解决了邻接表(或逆邻接表)的缺陷,即求入度(或出度)时必须遍历整个图。
十字链表的结构如下:
图中:
- firstIn表示入边表(即是逆邻接表中的单链表)头指针,firstOut表示出边表(即是邻接表中的单链表)头指针,data表示顶点数据。
- tailVex表示边的起点在顶点数组中的下标,tailNext值出边表指针域,指向起点相同的下一条边。
- headVex表示边的终点在顶点数组中的下标,headNext指入边表指针域,指向终点相同的下一条边。
(3)图的遍历
3.1、深度优先遍历
遍历思想:基本思想:首先从图中某个顶点v0出发,访问此顶点,然后依次从v相邻的顶点出发深度优先遍历,直至图中所有与v路径相通的顶点都被访问了;若此时尚有顶点未被访问,则从中选一个顶点作为起始点,重复上述过程,直到所有的顶点都被访问。
深度优先遍历用递归实现比较简单,只需用一个递归方法来遍历所有顶点,在访问某一个顶点时:
- 将它标为已访问
- 递归的访问它的所有未被标记过的邻接点
代码实现:
//深度优先遍历
int visitedDFS[MAXV] = { 0 }; //全局数组,记录是否遍历
void DFS(ListGraph* LG, int v) {
EdgeNode* p;
visitedDFS[v] = 1; //记录已访问,置 1
printf("%2d", v); //输出顶点编号
p = LG->adjList[v].firstEdge; //p 指向顶点 v 的第一个邻接点
while (p != NULL) {
if (visitedDFS[p->adjVer] == 0 && p->weight != INF) {
//如果 p->adjVer 没被访问,递归访问它
DFS(LG, p->adjVer);
}
p = p->nextEdge; //p 指向顶点 v 的下一个邻接点
}
}
3.2、广度优先遍历
遍历思想:
(1)从图中的某个初始点 v0 出发,首先访问初始点 v0。
(2)接着访问该顶点的所有未访问过的邻接点 v01 v02 v03 ……v0n。
(3)然后再选择 v01 v02 v03 ……v0n,访问它们的未被访问的邻接点,v010 v011 v012……v01n。
(4)直到所有与初始顶点 v 联通的顶点都被访问。
代码实现:
//广度优先遍历
void BFS(ListGraph* LG, int v) {
int ver; //定义出队顶点
EdgeNode* p;
SqQueue* sq; //定义指针
initQueue(sq); //初始化队列
int visitedBFS[MAXV] = { 0 }; //初始化访问标记数组
enQueue(sq, v); //初始点进队
printf("%2d", v);
visitedBFS[v] = 1; //打印并标记要出队顶点
while (!emptyQueue(sq)) { //队为空结束循环
ver = deQueue(sq, v); //出队,并得到出队信息
p = LG->adjList[ver].firstEdge; //指向出队的第一个邻接点
while (p != NULL) { //查找 ver 的所有邻接点
if (visitedBFS[p->adjVer] == 0 && p->weight != INF) { //如果没被访问
printf("%2d", p->adjVer); //打印该顶点信息
visitedBFS[p->adjVer] = 1; //置已访问状态
enQueue(sq, p->adjVer); //该顶点进队
}
p = p->nextEdge; //找下一个邻接点
}
}
printf("\n");
}
(4)最小生成树
图的生成树是它的一棵含有所有顶点的无环连通子图。一棵加权图的最小生成树(MST)是它的一棵权值(所有边的权值之和)最小的生成树。
通常来说,要解决最小生成树问题,通常采用两种算法:Prim算法和Kruskal算法。先假设要求一个连通无向图G=(V, E)的最小生成树T,且以其中的一个顶点V1为T的根结点。下面就分别对这两种算法进行介绍。
4.1Prim算法
Prim算法构建最小生成树的过程是:先构建一棵只包含根结点V1的树A,然后每次在连接树A结点和图G中树A以外的结点的所有边中,选取一条权重最小的边加入树A,直至树A覆盖图G中的所有结点。
从顶点0开始,首先将顶点0加入到树中(标记),顶点0和其它点的横切边(这里即为顶点0的邻接边)加入优先队列,将权值最小的横切边出队,加入生成树中。此时相当于也向树中添加了一个顶点2,接着将集合(顶点1,2组成)和另一个集合(除1,2的顶点组成)间的横切边加入到优先队列中,如此这般,直到队列为空。
代码实现:
#define Maximum 1000
#define Biggest 100000000
typedef struct EdgeListNode{
int adjId;
int weight;
EdgeListNode* next;
};
typedef struct VertexListNode{
int data;
EdgeListNode* firstadj;
};
typedef struct GraphAdjList{
int vertexnumber;
int edgenumber;
VertexListNode vertextlist[Maximum];
};
typedef struct MiniTreeEdge {
int s;
int e;
int weight;
MiniTreeEdge *next;
};
typedef struct MiniTree { //最小生成树
MiniTreeEdge *head; //指向最小生成树的根节点
int vertextnumber;
};
void MiniSpanTree_Prim(GraphAdjList g, MiniTree tree, int start_node) {
tree.head = NULL;
int *distance = (int*)malloc(sizeof(int) * g.vertexnumber + 2);
int *miniadj = (int*)malloc(sizeof(int) * g.vertexnumber + 2);
int i, j, k, lastnode, thisnode;
lastnode = start_node;
for(i=1; i<=g.vertexnumber; i++) {
distance[i] = Biggest;
miniadj[i] = i;
}
distance[start_node] = 0;
tree.vertextnumber = 1;
while(tree.vertextnumber < g.vertexnumber) {
EdgeListNode *temp = g.vertextlist[lastnode].firstadj;
while(temp != NULL) {
j = temp->adjId;
if(distance[j] && distance[j]>temp->weight) {
distance[j] = temp->weight;
miniadj[j] = lastnode;
}
temp = temp->next;
}
k = Biggest;
for(i=1; i<=g.vertexnumber; i++) {
if(distance[i] && k>distance[i]) {
k = distance[i];
thisnode = i;
}
}
MiniTreeEdge *temp1 = (MiniTreeEdge*)malloc(sizeof(MiniTreeEdge));
temp1->e = thisnode; //新加入的结点
temp1->s = miniadj[thisnode]; //最小生成树中与新加入结点相连的结点
temp1->weight = k; //新加入的边的权重
temp1->next = NULL;
temp1->next = tree.head;
tree.head = temp1;
distance[thisnode] = 0;
lastnode = thisnode;
tree.vertextnumber++;
}
//打印最小生成树
MiniTreeEdge *e = tree.head;
while(e != NULL) {
cout<<e->s<<" -> "<<e->e<<" : "<<e->weight<<endl;
e = e->next;
}
}
4.2、Kruskal算法
假设现在要求无向连通图G=(V, E)的最小生成树T,Kruskal算法的思想是令T的初始状态为|V|个结点而无边的非连通图,T中的每个顶点自成一个连通分量。接着,每次从图G中所有两个端点落在不同连通分量的边中,选取权重最小的那条,将该边加入T中,如此往复,直至T中所有顶点都在同一个连通分量上。
关键:(1)在生成最小生成树前,要对图中的所有边进行排序;
(2)如何判断一条边的两个端点是否落在不同的连通分量上:
#define Maximum 1000
#define Biggest 100000000
typedef struct EdgeListNode{
int adjId;
int weight;
EdgeListNode* next;
};
typedef struct VertexListNode{
int data;
EdgeListNode* firstadj;
};
typedef struct GraphAdjList{
int vertexnumber;
int edgenumber;
VertexListNode vertextlist[Maximum];
};
typedef struct MiniTreeEdge {
int s;
int e;
int weight;
MiniTreeEdge *next;
};
typedef struct MiniTree {
MiniTreeEdge *head;
int edgenumber;
};
typedef struct EdgeArrayData {
int l;
int r;
int weight;
};
bool compare(EdgeArrayData a, EdgeArrayData b) {
return a.weight < b.weight;
}
int find_parent(int node, int *parent) {
while(parent[node] != node) {
node = parent[node];
}
return node;
}
void MiniSpanTree_Kruskal(GraphAdjList g, MiniTree *tree) {
int i, j, k, edge_index, *parent;
MiniTreeEdge *e;
EdgeArrayData *edge = (EdgeArrayData*)malloc(sizeof(EdgeArrayData)*(g.edgenumber+2));
parent = (int*)malloc(sizeof(int)*(g.vertexnumber+2));
tree = (MiniTree*)malloc(sizeof(MiniTree));
EdgeListNode *v;
//将图中的每条边存储在edge里
edge_index = 0;
for(i=1; i<=g.vertexnumber; i++) {
v = g.vertextlist[i].firstadj;
parent[i] = i;
while(v != NULL) {
if(v->adjId > i) { //为了避免将一条边存两次
edge[edge_index].l = i;
edge[edge_index].r = v->adjId;
edge[edge_index].weight = v->weight;
edge_index++;
}
v = v->next;
}
}
sort(edge, edge+edge_index, compare); //将边按权重从小到大排序
tree->edgenumber = 0;
tree->head = NULL;
for(i=0; i<edge_index; i++) {
j = find_parent(edge[i].l, parent);
k = find_parent(edge[i].r, parent);
if(j != k) {
parent[j] = k;
e = (MiniTreeEdge*)malloc(sizeof(MiniTreeEdge));
e->s = edge[i].l;
e->e = edge[i].r;
e->weight = edge[i].weight;
e->next = tree->head;
tree->head = e;
tree->edgenumber++;
}
if(tree->edgenumber == g.vertexnumber - 1) {
break;
}
}
MiniTreeEdge *ee = tree->head;
while(ee != NULL) {
cout<<ee->s<<" -> "<<ee->e<<" : "<<ee->weight<<endl;
ee = ee->next;
}
}
(5)最短路径
最短路径指两顶点之间经过的边上权值之和最少的路径,并且称路径上的第一个顶点为源点,最后一个顶点为终点。
5.1、Dijkstra算法
求单源最短路径,即求一个顶点到任意顶点的最短路径,其时间复杂度为O(n*n)
算法实现:
void Dijkstra(AMGraph g,int dist[],int path[],int v0){
int n=g.vexnum,v;
int set[n];//set数组用于记录该顶点是否归并
//第一步:初始化
for(int i=0;i<n;i++){
set[i]=0;
dist[i]=g.arcs[v0][i];
if(dist[i]<MaxInt){//若距离小于MaxInt说明两点之间有路可通
path[i]=v0;//则更新路径i的前驱为v
}else{
path[i]=-1; //表示这两点之间没有边
}
}
set[v0]=1;//将初始顶点并入
path[v0]=-1;//初始顶点没有前驱
//第二步
for(int i=1;i<n;i++){//共n-1个顶点
int min=MaxInt;
//第二步:从i=1开始依次选一个距离顶点的最近顶点
for(int j=0;j<n;j++){
if(set[j]==0&&dist[j]<min){
v=j;
min=dist[j];
}
}
//将顶点并入
set[v]=1;
//第三步:在将新结点并入后,其初始顶点v0到各顶点的距离将会发生变化,所以需要更新dist[]数组
for(int j=0;j<n;j++){
if(set[j]==0&&dist[v]+g.arcs[v][j]<dist[j]){
dist[j]=dist[v]+g.arcs[v][j];
path[j]=v;
}
}
}
Dijkstra算法的局限性:图中边的权重必须为正,但可以是有环图。时间复杂度为O(elogn),空间复杂度O(n)
5.2、 Floyd算法
算法描述:
a.从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
b.对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。
算法实现:
void Floyd(AMGraph g,int path[][MaxVexNum]){
int n=g.vexnum;
int A[n][n];
//第一步:初始化path[][]和A[][]数组
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
A[i][j]=g.arcs[i][j];
path[i][j]=-1;
}
}
//第二步:三重循环,寻找最短路径
for(int v=0;v<n;v++){//第一层是代表中间结点
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(A[i][j]>A[i][v]+A[v][j]){
A[i][j]=A[i][v]+A[v][j];
path[i][j]=v;
}
}
}
}
}
(6)拓扑排序
在一个有向图中,对所有的节点进行排序,要求没有一个节点指向它前面的节点。
先统计所有节点的入度,对于入度为0的节点就可以分离出来,然后把这个节点指向的节点的入度减一。
一直做改操作,直到所有的节点都被分离出来。
如果最后不存在入度为0的节点,那就说明有环,不存在拓扑排序,也就是很多题目的无解的情况。
代码实现:
//b[]为每个点的入度
for(i=1;i<=n;i++){
for(j=1;j<=n;j++){
if(b[j]==0){ //找到一个入度为0的点
ans=j;
vis[cnt++]=j;
b[j]--;
break;
}
}
for(j=1;j<=n;j++)
if(a[ans][j]) b[j]--; //与入度为0的点相连的点的入度减一
}
printf("%d",vis[0]);
for(i=1;i<cnt;i++) printf(" %d",vis[i]);
printf("\n");
(7)关键路径
- 关键路径:AOE-网中,从起点到终点最长的路径的长度(长度指的是路径上边的权重和)
7.1、AOV网介绍
AOV网:
顶点活动(Activity On Vertex,AOV)网是指用顶点表示活动,而用边集表示活动间优先关系的有向图。例如图10-57的先导课程示意图就是AOV网,其中图的顶点表示各项课程,也就是“活动”;有向边表示课程的先导关系,也就是“活动间的优先关系”。显然,图中不应当存在有向环,否则会让优先关系出现逻辑错误。
7.2、AOE网介绍
我们在学习拓扑排序的时候,已经接触了什么是AOV-网,AOV-网是优先考虑顶点的思路,而我们也同样可以优先考虑边,这个就是AOE-网的思路。
若在带权的有向无环图中,以顶点表示事件,以有向边表示活动,边上的权值表示活动的开销(如该活动持续的时间),则此带权的有向无环图称为AOE网。记住AOE-网只是比AOV-网多了一个边的权重,而且AOV-网一般是设计一个庞大的工程各个子工程实施的先后顺序,而我们的AOE-网就是不仅仅关系整个工程中各个子工程的实施的先后顺序,同时也关系整个工程完成最短时间。
因此,通常在AOE网中列出完成预定工程计划所需要进行的活动,每个活动计划完成的时间,要发生哪些事件以及这些事件与活动之间的关系,从而可以确定该项工程是否可行,估算工程完成的时间以及确定哪些活动是影响工程进度的关键。
AOE-网还有一个特点就是:只有一个起点(入度为0的顶点)和一个终点(出度为0的顶点),并且AOE-网有两个待研究的问题:
1.完成整个工程需要的时间
2.哪些活动是影响工程进度的关键
7.3求关键路径算法设计:
关键路径算法是一种典型的动态规划法,设图G=(V, E)是个AOE网,结点编号为1,2,...,n,其中结点1与n 分别为始点和终点,ak=<i, j>∈E是G的一个活动。算法关键是确定活动的最早发生时间ve[k]和最晚发生时间vl[k],进而获取顶点的最早开始时间e[k]和最晚开始时间l[k]。
代码设计:
//拓扑序列
stack<int>topOrder;
//拓扑排序,顺便求ve数组
bool topologicalSort()
{
queue<int>q;
for(int i=0;i<n;i++)
if(inDegree[i]==0)
q.push(i);
while(!q.empty())
{
int u=q.front();
q.pop();
topOrder.push(u);//将u加入拓扑序列
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i].v;//u的i号后继结点编号为v
inDegree[v]--;
if(inpegree[v]==0)
q.push(v);
//用ve[u]来更新u的所有后继结点
if(ve[u]+G[u][i].w> ve[v])
ve[v]=ve[u]+G[u][i].w;
}
}
if(toporder.size()== n)
return true;
else
return false;
}
(8)关于图的疑难问题
1.题目:
现在你总共有 n 门课需要选,记为 0 到 n-1。 在选修某些课程之前需要一些先修课程。例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1] 给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。 可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
示例 1:
输入: 2, [[1,0]]
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
123
示例 2:
输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
2.解题思路:
拓扑排序
- 从 DAG 图中找出所有入度为0的顶点,放入队列。
- 每次从队列取出一个结点,从图中删除该顶点以及所有以它为起点的有向边。
- 每删除一条有向边,该边的终结点的入度-1,如果入度为0,将终结点加入队列。
- 重复以上步骤,直到当前图中不存在无前驱的顶点。
3.代码实现:
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
/**
* 使用拓扑排序来完成
*/
public class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
// 先处理极端情况
if (numCourses <= 0) {
return new int[0];
}
// 邻接表表示
HashSet<Integer>[] graph = new HashSet[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new HashSet<>();
}
// 入度表
int[] inDegree = new int[numCourses];
// 遍历 prerequisites 的时候,把 邻接表 和 入度表 都填上
for (int[] p : prerequisites) {
graph[p[1]].add(p[0]);
inDegree[p[0]]++;
}
LinkedList<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.addLast(i);
}
}
ArrayList<Integer> res = new ArrayList<>();
while (!queue.isEmpty()) {
// 当前入度为 0 的结点
Integer inDegreeNode = queue.removeFirst();
// 加入结果集中
res.add(inDegreeNode);
// 下面从图中删去
// 得到所有的后继课程,接下来把它们的入度全部减去 1
HashSet<Integer> nextCourses = graph[inDegreeNode];
for (Integer nextCourse : nextCourses) {
inDegree[nextCourse]--;
// 马上检测该结点的入度是否为 0,如果为 0,马上加入队列
if (inDegree[nextCourse] == 0) {
queue.addLast(nextCourse);
}
}
}
// 如果结果集中的数量不等于结点的数量,就不能完成课程任务,这一点是拓扑排序的结论
int resLen = res.size();
if (resLen == numCourses) {
int[] ret = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
ret[i] = res.get(i);
}
return ret;
} else {
return new int[0];
}
}
}
1