图->连通性->有向图的强连通分量

 文字描述

  有向图强连通分量的定义:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。

  用深度优先搜索求有向图的强连通分量的方法如下并假设有向图的存储结构为十字链表。

  1 在有向图G上,从某个定点出发沿以该顶点为尾的弧进行深度优先遍历;并按其所有邻接点的搜索都完成(即退出DFS函数)的顺序将顶点排列起来。此时需对之前的深度优先遍历算法(见图遍历)作两处修改: (a)在进入DSFTraverse函数时首先进行计数变量的初始化,即在入口处加上count=0;(b)在退出DFS函数之前将完成搜索的顶点号记录在另一个辅助数组finished[vexnum]中,即在DFS函数结束之前加上finished[count++]=v;

  2 在有向图G上,从最后完成搜索的顶点(即finished[count-1])出发,沿着以该顶点为头的弧作逆向的深度优先搜索遍历,若遍历不能访问到有向图中所有顶点,则从余下的顶点中最后完搜索的那个顶点出发,继续作逆向的深度优先搜索便利。依次类推。

  由此,每一次调用DFS作逆向深度优先遍历所访问到的顶点集便是有向图G中一个强连通分量的顶点集。

示意图

见本章后面的代码运行Example01, Example02, Example03, Example04。

算法分析

求有向图的强连通分量的算法时间复杂度和深度优先搜索遍历相同。

代码实现

 

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #define    MAX_VERTEX_NUM    20
  5 #define    DEBUG
  6 #ifdef DEBUG
  7 #include <stdarg.h>
  8 #define LOG(args...) _log_(__FILE__, __FUNCTION__, __LINE__, ##args);
  9 void _log_(const char *file, const char *function, int line, const char * format, ...)
 10 {
 11     char buf[1024] = {0};
 12     va_list list;
 13     va_start(list, format);
 14     sprintf(buf, "[%s,%s,%d]", file, function, line);
 15     vsprintf(buf+strlen(buf), format, list);
 16     sprintf(buf+strlen(buf), "
");
 17     va_end(list);
 18     printf(buf);
 19 }
 20 #else
 21 #define LOG
 22 #endif // DEBUG
 23 
 24 //////////////////////////////////////////////////////////////
 25 // 十字链表作为图的存储结构
 26 //////////////////////////////////////////////////////////////
 27 typedef enum {DG, DN, UDG, UDN} GraphKind;
 28 typedef char InfoType;
 29 typedef char VertexType;
 30 typedef struct ArcBox{
 31     int tailvex, headvex;
 32     struct ArcBox *hlink, *tlink;
 33     InfoType *info;
 34 }ArcBox;
 35 typedef struct VexNode{
 36     VertexType data;
 37     ArcBox *firstin, *firstout;
 38 }VexNode;
 39 typedef struct{
 40     VexNode xlist[MAX_VERTEX_NUM];
 41     int vexnum, arcnum;
 42     GraphKind kind;
 43 }OLGraph;
 44 
 45 //////////////////////////////////////////////////////////////
 46 // 若G中存在顶点u,则返回该顶点在图中位置;否则返回-1。
 47 //////////////////////////////////////////////////////////////
 48 int LocateVex(OLGraph G, VertexType v)
 49 {
 50     int i = 0;
 51     for(i=0; i<G.vexnum; i++)
 52     {
 53         if(G.xlist[i].data == v)
 54             return i;
 55     }
 56     return -1;
 57 }
 58 
 59 //////////////////////////////////////////////////////////////
 60 // 若G中存在顶点位置loc存在,则返回其顶点名称
 61 //////////////////////////////////////////////////////////////
 62 VertexType LocateVInfo(OLGraph G, int loc)
 63 {
 64     return G.xlist[loc].data;
 65 }
 66 
 67 
 68 //////////////////////////////////////////////////////////////
 69 // 以十字链表作为图的存储结构创建有向图
 70 //////////////////////////////////////////////////////////////
 71 int CreateDG(OLGraph *G)
 72 {
 73     printf("
以十字链表作为图的存储结构创建有向图:
");
 74     int i = 0, j = 0, k = 0, IncInfo = 0;
 75     int v1 = 0, v2 = 0;
 76     char tmp[10] = {0};
 77     ArcBox *p = NULL;
 78     printf("输入顶点数,弧数,其他信息标志位: ");
 79     scanf("%d,%d,%d", &G->vexnum, &G->arcnum, &IncInfo);
 80     for(i=0; i<G->vexnum; i++)
 81     {
 82         //输入顶点值
 83         printf("输入第%d个顶点: ", i+1);
 84         memset(tmp, 0, sizeof(tmp));
 85         scanf("%s", tmp);
 86         //初始化指针
 87         G->xlist[i].data = tmp[0];
 88         G->xlist[i].firstin = NULL;
 89         G->xlist[i].firstout = NULL;
 90     }
 91     //输入各弧并构造十字链表
 92     for(k=0; k<G->arcnum; k++)
 93     {
 94         printf("输入第%d条弧(顶点1, 顶点2): ", k+1);
 95         memset(tmp, 0, sizeof(tmp));
 96         scanf("%s", tmp);
 97         sscanf(tmp, "%c,%c", &v1, &v2);
 98         i = LocateVex(*G, v1);
 99         j = LocateVex(*G, v2);
100         //对弧结点赋值
101         p = (ArcBox *) malloc(sizeof(ArcBox));
102         p->tailvex = i;
103         p->headvex = j;
104         p->hlink = G->xlist[j].firstin;
105         p->tlink = G->xlist[i].firstout;
106         p->info = NULL;
107         //完成在入弧和出弧链头的插入
108         G->xlist[j].firstin = p;
109         G->xlist[i].firstout = p;
110         //若弧有相关的信息,则输入
111         if(IncInfo)
112         {
113             //Input(p->info);
114         }
115     }
116     return 0;
117 }
118 
119 int CreateGraph(OLGraph *G)
120 {
121     printf("输入图类型: +有向图(0), -有向网(1), -无向图(2), -无向网(3): ");
122     scanf("%d", &G->kind);
123     switch(G->kind){
124         case DG:
125             return CreateDG(G);
126         case DN:
127         case UDN:
128         case UDG:
129         default:
130             printf("not support!
");
131             return -1;
132     }
133     return 0;
134 }
135 
136 //////////////////////////////////////////////////////////////
137 // 打印图结点
138 //////////////////////////////////////////////////////////////
139 void printG(OLGraph G)
140 {
141     printf("
打印图结点
");
142     if(G.kind == DG){
143         printf("类型:有向图;顶点数 %d, 弧数 %d
", G.vexnum, G.arcnum);
144     }else if(G.kind == DN){
145         printf("类型:有向网;顶点数 %d, 弧数 %d
", G.vexnum, G.arcnum);
146     }else if(G.kind == UDG){
147         printf("类型:无向图;顶点数 %d, 弧数 %d
", G.vexnum, G.arcnum);
148     }else if(G.kind == UDN){
149         printf("类型:无向网;顶点数 %d, 弧数 %d
", G.vexnum, G.arcnum);
150     }
151     int i = 0;
152     ArcBox *fi = NULL;
153     ArcBox *fo = NULL;
154     for(i=0; i<G.vexnum; i++)
155     {
156         printf("%c: ", G.xlist[i].data);
157         fi = G.xlist[i].firstin;
158         fo = G.xlist[i].firstout;
159         printf("{hlink=");
160         while(fi)
161         {
162             printf("(%d,%c)->(%d,%c); ", fi->tailvex, LocateVInfo(G, fi->tailvex), fi->headvex, LocateVInfo(G, fi->headvex));
163             fi = fi->hlink;
164         }
165         printf("}        {tlink=");
166         while(fo)
167         {
168             printf("(%d,%c)->(%d,%c); ", fo->tailvex, LocateVInfo(G, fo->tailvex), fo->headvex, LocateVInfo(G, fo->headvex));
169             fo = fo->tlink;
170         }
171         printf("}
");
172     }
173     return ;
174 }
175 
176 
177 
178 //////////////////////////////////////////////////////////////
179 // 用双向遍历的方法求有向图的强连通分量
180 //////////////////////////////////////////////////////////////
181 int GVisited[MAX_VERTEX_NUM] = {0};
182 void (*visitfun)(OLGraph G, int v);
183 
184 //记录退出DFS函数之前完成搜索的顶点号
185 int GFinished[MAX_VERTEX_NUM] = {0};
186 int GCount = 0;
187 
188 #define D_OUT 0
189 #define D_IN  1
190 typedef enum DIRECTION
191 {
192     IN,
193     OUT
194 }DIRECTION;
195 
196 //////////////////////////////////////////////////////////////
197 // 打印结点在图中的位置及结点信息
198 //////////////////////////////////////////////////////////////
199 void printvex(OLGraph G, int v)
200 {
201     printf("[%d]:%c	", v, G.xlist[v].data);
202     return ;
203 }
204 
205 //////////////////////////////////////////////////////////////
206 // direction is IN: 返回图G中以顶点位置为V的顶点为入度的第一个结点
207 // direction is OUT: 返回图G中以顶点位置为V的顶点为出度的第一个结点
208 //////////////////////////////////////////////////////////////
209 int FirstAdjVex(OLGraph G, int v, int direction)
210 {
211     if(direction == IN){
212         return (G.xlist[v].firstin)?G.xlist[v].firstin->tailvex:-1;
213     }else if(direction == OUT){
214         return (G.xlist[v].firstout)?G.xlist[v].firstout->headvex:-1;
215     }else{
216         return -1;
217     }
218 }
219 
220 //////////////////////////////////////////////////////////////
221 // direction is IN: G中位置w是以v为入度的结点, 返回下一个以v为入度的结点
222 // direction is OUT: G中位置w是以v为出度的结点, 返回下一个以v为出度的结点
223 //////////////////////////////////////////////////////////////
224 int NextAdjVex(OLGraph G, int v, int w, int direction)
225 {
226     int ret = -1;
227     if(direction == IN){
228         ArcBox *fi = NULL;
229         fi = G.xlist[v].firstin;
230         while(fi)
231         {
232             if(fi->tailvex == w)
233                 break;
234             fi = fi->hlink;
235         }
236         if(fi && (fi=fi->hlink)){
237             ret = fi->tailvex;
238         }else{
239             ret = -1;
240         }
241     }else if(direction == OUT){
242         ArcBox *fo = NULL;
243         fo = G.xlist[v].firstout;
244         while(fo)
245         {
246             if(fo->headvex == w)
247                 break;
248             fo = fo->tlink;
249         }
250         if(fo && (fo=fo->tlink)){
251             ret = fo->headvex;
252         }else{
253             ret = -1;
254         }
255     }
256     return ret;
257 }
258 //////////////////////////////////////////////////////////////
259 // direction is IN :从第v个顶点出发, 沿着以v为入度的顶点的方式, 递归地深度优先遍历图G
260 // direction is OUT :从第v个顶点出发, 沿着以v为出度的顶点的方式, 递归地深度优先遍历图G
261 //////////////////////////////////////////////////////////////
262 void DFS(OLGraph G, int v, int direction)
263 {
264     GVisited[v] = 1;
265     printvex(G, v);
266     int w = 0;
267     for(w=FirstAdjVex(G,v,direction); w>=0; w=NextAdjVex(G, v, w, direction))
268     {
269         if(!GVisited[w]){
270             DFS(G, w, direction);
271         }
272     }
273     GFinished[GCount++] = v;
274 }
275 
276 //////////////////////////////////////////////////////////////
277 // 1 从第一个出发,对图G作深度优先遍历并记录下退出DFS函数的顺序
278 // 2 从最后一个退出DFS函数的顶点出发, 反方向对图G作深度优先遍历并求出图的强连通分量
279 //////////////////////////////////////////////////////////////
280 void DFSTraverse(OLGraph G, void (*visitfun)(OLGraph G, int v))
281 {
282     visitfun = printvex;
283     int v = 0;
284     int index = 0;
285 
286     printf("
深度优先搜索(以第一个顶点为尾的弧进行深度优先搜索遍历):
");
287     //访问标志数组初始化
288     for(v=0; v<G.vexnum; v++)
289     {
290         GVisited[v] = 0;
291         GFinished[v] = 0;
292     }
293     GCount = 0;
294 
295     for(v=0; v<G.vexnum; v++)
296     {
297         if(!GVisited[v]){
298             DFS(G, v, OUT);
299         }
300     }
301 
302     printf("
退出DFS函数的次序location和顶点位置index为:
");
303     for(v=0; v<GCount; v++)
304     {
305         printf("[location:%d, index:%d] ", v, GFinished[v]);
306     }
307     printf("
");
308 
309     printf("深度优先搜索(从最后完成搜索的顶点出发,沿着以该顶点为头的弧作逆向的深度优先搜索遍历):");
310     //访问标志数组初始化
311     for(v=0; v<G.vexnum; v++)
312     {
313         GVisited[v] = 0;
314     }
315     for(v=0; v<GCount; v++)
316     {
317         index = GFinished[GCount-1-v];
318         if(!GVisited[index]){
319             printf("
强连通分量:");
320             DFS(G, index, IN);
321         }
322     }
323     printf("
");
324 }
325 
326 
327 int main(int argc, char *argv[])
328 {
329     OLGraph G;
330     if(CreateGraph(&G) > -1){
331         printG(G);
332     }
333     DFSTraverse(G, printvex);
334 }
有向图(十字链表存储)的强连通分量

代码运行

Example01

 

[jennifer@localhost Data.Structure]$ ./a.out 
输入图类型: +有向图(0), -有向网(1), -无向图(2), -无向网(3): 0

以十字链表作为图的存储结构创建有向图:
输入顶点数,弧数,其他信息标志位: 4,7,0
输入第1个顶点: a
输入第2个顶点: b
输入第3个顶点: c
输入第4个顶点: d
输入第1条弧(顶点1, 顶点2): a,b
输入第2条弧(顶点1, 顶点2): a,c
输入第3条弧(顶点1, 顶点2): c,a
输入第4条弧(顶点1, 顶点2): c,d
输入第5条弧(顶点1, 顶点2): d,c
输入第6条弧(顶点1, 顶点2): d,a
输入第7条弧(顶点1, 顶点2): d,b

打印图结点
类型:有向图;顶点数 4, 弧数 7
a: {hlink=(3,d)->(0,a); (2,c)->(0,a); }        {tlink=(0,a)->(2,c); (0,a)->(1,b); }
b: {hlink=(3,d)->(1,b); (0,a)->(1,b); }        {tlink=}
c: {hlink=(3,d)->(2,c); (0,a)->(2,c); }        {tlink=(2,c)->(3,d); (2,c)->(0,a); }
d: {hlink=(2,c)->(3,d); }        {tlink=(3,d)->(1,b); (3,d)->(0,a); (3,d)->(2,c); }

深度优先搜索(以第一个顶点为尾的弧进行深度优先搜索遍历):
[0]:a    [2]:c    [3]:d    [1]:b    
退出DFS函数的次序location和顶点位置index为:
[location:0, index:1] [location:1, index:3] [location:2, index:2] [location:3, index:0] 
深度优先搜索(从最后完成搜索的顶点出发,沿着以该顶点为头的弧作逆向的深度优先搜索遍历):
强连通分量:[0]:a    [3]:d    [2]:c    
强连通分量:[1]:b    
[jennifer@localhost Data.Structure]$ 
Example01

Example02

 

[jennifer@localhost blogs]$ ./a.out 
输入图类型: +有向图(0), -有向网(1), -无向图(2), -无向网(3): 0

以十字链表作为图的存储结构创建有向图:
输入顶点数,弧数,其他信息标志位: 6,8,0
输入第1个顶点: 1
输入第2个顶点: 2
输入第3个顶点: 3
输入第4个顶点: 4
输入第5个顶点: 5
输入第6个顶点: 6
输入第1条弧(顶点1, 顶点2): 1,2
输入第2条弧(顶点1, 顶点2): 1,3
输入第3条弧(顶点1, 顶点2): 2,4
输入第4条弧(顶点1, 顶点2): 3,4
输入第5条弧(顶点1, 顶点2): 3,5
输入第6条弧(顶点1, 顶点2): 4,1
输入第7条弧(顶点1, 顶点2): 4,7
输入第8条弧(顶点1, 顶点2): 5,6

打印图结点
类型:有向图;顶点数 6, 弧数 8
1: {hlink=(3,4)->(0,1); }        {tlink=(0,1)->(2,3); (0,1)->(1,2); }
2: {hlink=(0,1)->(1,2); }        {tlink=(1,2)->(3,4); }
3: {hlink=(0,1)->(2,3); }        {tlink=(2,3)->(4,5); (2,3)->(3,4); }
4: {hlink=(2,3)->(3,4); (1,2)->(3,4); }        {tlink=(3,4)->(-1,<); (3,4)->(0,1); }
5: {hlink=(2,3)->(4,5); }        {tlink=(4,5)->(5,6); }
6: {hlink=(4,5)->(5,6); }        {tlink=}

深度优先搜索(以第一个顶点为尾的弧进行深度优先搜索遍历):
[0]:1    [2]:3    [4]:5    [5]:6    [3]:4    [1]:2    
退出DFS函数的次序location和顶点位置index为:
[location:0, index:5] [location:1, index:4] [location:2, index:3] [location:3, index:2] [location:4, index:1] [location:5, index:0] 
深度优先搜索(从最后完成搜索的顶点出发,沿着以该顶点为头的弧作逆向的深度优先搜索遍历):
强连通分量:[0]:1    [3]:4    [2]:3    [1]:2    
强连通分量:[4]:5    
强连通分量:[5]:6    
[jennifer@localhost blogs]$ 
Example02

Example03

[jennifer@localhost blogs]$ ./a.out 
输入图类型: +有向图(0), -有向网(1), -无向图(2), -无向网(3): 0

以十字链表作为图的存储结构创建有向图:
输入顶点数,弧数,其他信息标志位: 8,13,0
输入第1个顶点: a
输入第2个顶点: b
输入第3个顶点: c
输入第4个顶点: d
输入第5个顶点: e
输入第6个顶点: f
输入第7个顶点: g
输入第8个顶点: h
输入第1条弧(顶点1, 顶点2): a,b
输入第2条弧(顶点1, 顶点2): b,c
输入第3条弧(顶点1, 顶点2): d,c
输入第4条弧(顶点1, 顶点2): c,d
输入第5条弧(顶点1, 顶点2): e,a
输入第6条弧(顶点1, 顶点2): b,e
输入第7条弧(顶点1, 顶点2): b,f
输入第8条弧(顶点1, 顶点2): c,g
输入第9条弧(顶点1, 顶点2): h,d
输入第10条弧(顶点1, 顶点2): e,f
输入第11条弧(顶点1, 顶点2): g,f
输入第12条弧(顶点1, 顶点2): f,g
输入第13条弧(顶点1, 顶点2): h,g

打印图结点
类型:有向图;顶点数 8, 弧数 13
a: {hlink=(4,e)->(0,a); }        {tlink=(0,a)->(1,b); }
b: {hlink=(0,a)->(1,b); }        {tlink=(1,b)->(5,f); (1,b)->(4,e); (1,b)->(2,c); }
c: {hlink=(3,d)->(2,c); (1,b)->(2,c); }        {tlink=(2,c)->(6,g); (2,c)->(3,d); }
d: {hlink=(7,h)->(3,d); (2,c)->(3,d); }        {tlink=(3,d)->(2,c); }
e: {hlink=(1,b)->(4,e); }        {tlink=(4,e)->(5,f); (4,e)->(0,a); }
f: {hlink=(6,g)->(5,f); (4,e)->(5,f); (1,b)->(5,f); }        {tlink=(5,f)->(6,g); }
g: {hlink=(7,h)->(6,g); (5,f)->(6,g); (2,c)->(6,g); }        {tlink=(6,g)->(5,f); }
h: {hlink=}        {tlink=(7,h)->(6,g); (7,h)->(3,d); }

深度优先搜索(以第一个顶点为尾的弧进行深度优先搜索遍历):
[0]:a    [1]:b    [5]:f    [6]:g    [4]:e    [2]:c    [3]:d    [7]:h    
退出DFS函数的次序location和顶点位置index为:
[location:0, index:6] [location:1, index:5] [location:2, index:4] [location:3, index:3] [location:4, index:2] [location:5, index:1] [location:6, index:0] [location:7, index:7] 
深度优先搜索(从最后完成搜索的顶点出发,沿着以该顶点为头的弧作逆向的深度优先搜索遍历):
强连通分量:[7]:h    
强连通分量:[0]:a    [4]:e    [1]:b    
强连通分量:[2]:c    [3]:d    
强连通分量:[5]:f    [6]:g    
[jennifer@localhost blogs]$ 
Example03

Example04

[jennifer@localhost blogs]$ ./a.out 
输入图类型: +有向图(0), -有向网(1), -无向图(2), -无向网(3): 0

以十字链表作为图的存储结构创建有向图:
输入顶点数,弧数,其他信息标志位: 10,15
输入第1个顶点: ^C
[jennifer@localhost blogs]$ ./a.out 
输入图类型: +有向图(0), -有向网(1), -无向图(2), -无向网(3): 0

以十字链表作为图的存储结构创建有向图:
输入顶点数,弧数,其他信息标志位: 10,15,0
输入第1个顶点: 0
输入第2个顶点: 1
输入第3个顶点: 2
输入第4个顶点: 3
输入第5个顶点: 4
输入第6个顶点: 5
输入第7个顶点: 6
输入第8个顶点: 7
输入第9个顶点: 8
输入第10个顶点: 9
输入第1条弧(顶点1, 顶点2): 9,2
输入第2条弧(顶点1, 顶点2): 7,9
输入第3条弧(顶点1, 顶点2): 2,7
输入第4条弧(顶点1, 顶点2): 2,1
输入第5条弧(顶点1, 顶点2): 2,4
输入第6条弧(顶点1, 顶点2): 7,4
输入第7条弧(顶点1, 顶点2): 1,8
输入第8条弧(顶点1, 顶点2): 0,1
输入第9条弧(顶点1, 顶点2): 1,0
输入第10条弧(顶点1, 顶点2): 0,4
输入第11条弧(顶点1, 顶点2): 8,5
输入第12条弧(顶点1, 顶点2): 5,0
输入第13条弧(顶点1, 顶点2): 4,3
输入第14条弧(顶点1, 顶点2): 3,4
输入第15条弧(顶点1, 顶点2): 5,6

打印图结点
类型:有向图;顶点数 10, 弧数 15
0: {hlink=(5,5)->(0,0); (1,1)->(0,0); }        {tlink=(0,0)->(4,4); (0,0)->(1,1); }
1: {hlink=(0,0)->(1,1); (2,2)->(1,1); }        {tlink=(1,1)->(0,0); (1,1)->(8,8); }
2: {hlink=(9,9)->(2,2); }        {tlink=(2,2)->(4,4); (2,2)->(1,1); (2,2)->(7,7); }
3: {hlink=(4,4)->(3,3); }        {tlink=(3,3)->(4,4); }
4: {hlink=(3,3)->(4,4); (0,0)->(4,4); (7,7)->(4,4); (2,2)->(4,4); }        {tlink=(4,4)->(3,3); }
5: {hlink=(8,8)->(5,5); }        {tlink=(5,5)->(6,6); (5,5)->(0,0); }
6: {hlink=(5,5)->(6,6); }        {tlink=}
7: {hlink=(2,2)->(7,7); }        {tlink=(7,7)->(4,4); (7,7)->(9,9); }
8: {hlink=(1,1)->(8,8); }        {tlink=(8,8)->(5,5); }
9: {hlink=(7,7)->(9,9); }        {tlink=(9,9)->(2,2); }

深度优先搜索(以第一个顶点为尾的弧进行深度优先搜索遍历):
[0]:0    [4]:4    [3]:3    [1]:1    [8]:8    [5]:5    [6]:6    [2]:2    [7]:7    [9]:9    
退出DFS函数的次序location和顶点位置index为:
[location:0, index:3] [location:1, index:4] [location:2, index:6] [location:3, index:5] [location:4, index:8] [location:5, index:1] [location:6, index:0] [location:7, index:9] [location:8, index:7] [location:9, index:2] 
深度优先搜索(从最后完成搜索的顶点出发,沿着以该顶点为头的弧作逆向的深度优先搜索遍历):
强连通分量:[2]:2    [9]:9    [7]:7    
强连通分量:[0]:0    [5]:5    [8]:8    [1]:1    
强连通分量:[6]:6    
强连通分量:[4]:4    [3]:3    
[jennifer@localhost blogs]$ 
Example04
原文地址:https://www.cnblogs.com/aimmiao/p/10088601.html