阅读本文大概需要6分钟

我没骗你们,这个图的确不是你想象中的图。你想看什么图我还不知道? 不过,我的图却是大千世界,继续以通俗易懂的方式给你提升一波内功。不信你继续往下读。

图是一种与树有些相像的数据结构,实际上,从数学意义上说,树是图的一种。然而在计算机程序设计中,图的应用方式与树不同。

我前面一些文章中讨论的数据结构都有一个框架,这个框架都是由相应的算法设定的。比如说,二叉树是那样一个形状,就是因为那样的形状使它更容易搜索数据和插入新数据,树的边表示了从一个节点到另一个节点的快捷方式。而图通常也有一个固定的形状,这是由物理或抽象的问题所决定的。比如说,图中节点表示城市,而边表示城市间的航班线,这些都是固定的。即,图的形状取决于真实世界的具体情况。在图中,我们称节点为顶点。

在大多数情况下,顶点表示某个真实世界的对象,这个对象必须用数据项来描述。例如顶点代表城市,那么它需要存储城市名字、海拔高度、地理位置等相关信息。因此通常用一个顶点类的对象来表示一个顶点。这里我们仅仅在顶点中存储了一个字母来标识顶点,同时还有一个标志位,用来判断该顶点有没有被访问过。

class
 
Vertex
 
{
 
//顶点类
    
public
 
char
 label
;
  
//label: eg.'A'
    
public
 
boolean
 wasVisited
;
    
public
 
Vertex
(
char
 lab
)
 
{
        label 
=
 lab
;
        wasVisited 
=
 
false
;
    
}
}

顶点对象能放在数组(这里用vertexList数组)中,然后用下标指示,也可以放在链表或其他数据结构中,但不论使用什么数据结构,存储只为了使用方便,这与边如何连接点没有关系。

前面提到的树的表示中,大多数情况下都是每个节点包含它的子节点的引用,也可以用数组表示树,数组中的节点位置决定了它和其他节点的关系,比如堆。然而,图并不像树那样拥有几种固定的结构,因为图的每个顶点可以与任意多个顶点连接。为了模拟这种自由形式的组织结构,需要用一种不同的方法表示边,一般用两种方式:邻接矩阵和邻接表。

邻接矩阵是一个二维数组,里面的数据表示两点间是否存在边。如果图有N个顶点,那么邻接矩阵就是N*N的数组,如下表所示:1表示有边,0表示没有边,也可以用true和false来表示。

邻接表是一个链表数组(或者链表的链表),每个单独的链表表示了有哪些顶点与当前顶点邻接,如下表所示:A邻接顶点有B、C和D。

下面讨论下图中的搜索。在图中实现的基本操作之一就是搜索从一个定到可以到达其他哪些顶点,或者找所有当前顶点可到达的顶点。有两种常用的方法可用来搜索图:深度优先搜索(DFS)和广度优先搜索(BFS),它们最终都会到达所有的连通顶点。深度优先搜索通过栈来实现,而广度优先搜索通过队列来实现。具体的见下面的程序:

public
 
class
 
Graph
 
{
    
private
 
final
 
int
 MAX_VERTS 
=
 
20
;
    
private
 
Vertex
 vertexArray
[];
   
//存储顶点的数组
    
private
 
int
 adjMat
[][];
 
//存储是否有边界的矩阵数组, 0表示没有边界,1表示有边界
    
private
 
int
 nVerts
;
 
//顶点个数
    
private
 
StackX
 stack
;
   
//深度搜索时用来临时存储的栈
    
private
 
QueueX
 queue
;
   
//广度搜索时用来临时存储的队列
    
public
 
Graph
()
 
{
        vertexArray 
=
 
new
 
Vertex
[
MAX_VERTS
];
        adjMat 
=
 
new
 
int
[
MAX_VERTS
][
MAX_VERTS
];
        nVerts 
=
 
0
;
        
for
(
int
 i 
=
 
0
;
 i 
<
 MAX_VERTS
;
 i
++)
 
{
            
for
(
int
 j 
=
 
0
;
 j 
<
 MAX_VERTS
;
 j
++)
 
{
                adjMat
[
i
][
j
]
 
=
 
0
;
            
}
        
}
        stack 
=
 
new
 
StackX
();
        queue 
=
 
new
 
QueueX
();
    
}
    
public
 
void
 addVertex
(
char
 lab
)
 
{
        vertexArray
[
nVerts
++]
 
=
 
new
 
Vertex
(
lab
);
    
}
    
public
 
void
 addEdge
(
int
 start
,
 
int
 end
)
 
{
        adjMat
[
start
][
end
]
 
=
 
1
;
        adjMat
[
start
][
end
]
 
=
 
1
;
    
}
    
public
 
void
 displayVertex
(
int
 v
)
 
{
        
System
.
out
.
print
(
vertexArray
[
v
].
label
);
    
}
    
/*
     * 深度优先搜索算法:做四件事
     * 1. 用peek()方法检查栈顶的顶点
     * 2. 试图找到这个顶点还未访问的邻节点
     * 3. 如果没有找到,出栈
     * 4. 如果找到这样的顶点,访问这个顶点,并把它放入栈
     * 深度优先算法类似于从树的跟逐个沿不同路径访问到不同的叶节点
     */
    
public
 
void
 depthFirstSearch
()
 
{
        
//begin at vertex 0
        vertexArray
[
0
].
wasVisited 
=
 
true
;
 
//make it
        displayVertex
(
0
);
        stack
.
push
(
0
);
        
while
(!
stack
.
isEmpty
())
 
{
            
//get an unvisited vertex adjacent to stack top
            
int
 v 
=
 getAdjUnvisitedVertex
(
stack
.
peek
());
            
if
(
v 
==
 
-
1
)
 
{
   
//if no such vertex
                stack
.
pop
();
            
}
            
else
 
{
  
//if it exists
                vertexArray
[
v
].
wasVisited 
=
 
true
;
                displayVertex
(
v
);
                stack
.
push
(
v
);
            
}
        
}
        
//stack is empty, so we're done
        
for
(
int
 i 
=
 
0
;
 i 
<
 nVerts
;
 i
++)
 
{
            vertexArray
[
i
].
wasVisited 
=
 
false
;
        
}
    
}
    
//returns an unvisited vertex adj to v
        
public
 
int
 getAdjUnvisitedVertex
(
int
 v
)
 
{
            
for
(
int
 i 
=
 
0
;
 i 
<
 nVerts
;
 i
++)
 
{
                
if
(
adjMat
[
v
][
i
]
 
==
 
1
 
&&
 vertexArray
[
i
].
wasVisited 
==
 
false
)
 
{
//v和i之间有边,且i没被访问过
                    
return
 i
;
                
}
            
}
            
return
 
-
1
;
        
}
    
/*
     * 广度优先搜索算法:做四件事
     * 1. 用remove()方法检查栈顶的顶点
     * 2. 试图找到这个顶点还未访问的邻节点
     * 3. 如果没有找到,该顶点出列
     * 4. 如果找到这样的顶点,访问这个顶点,并把它放入队列中
     * 深度优先算法中,好像表现的要尽快远离起始点,在广度优先算法中,要尽可能靠近起始点。
     * 它首先访问其实顶点的所有邻节点,然后再访问较远的区域。这种搜索不能用栈,而要用队列来实现。
     * 广度优先算法类似于从树的跟逐层往下访问直到底层
     */
    
public
 
void
 breadthFirstSearch
()
 
{
        vertexArray
[
0
].
wasVisited 
=
 
true
;
        displayVertex
(
0
);
        queue
.
insert
(
0
);
        
int
 v2
;
        
while
(!
queue
.
isEmpty
())
 
{
            
int
 v1 
=
 queue
.
remove
();
            
//until it has no unvisited neighbors
            
while
((
v2 
=
 getAdjUnvisitedVertex
(
v1
))
 
!=
 
-
1
)
 
{
                vertexArray
[
v2
].
wasVisited 
=
 
true
;
                displayVertex
(
v2
);
                queue
.
insert
(
v2
);
            
}
        
}
        
for
(
int
 i 
=
 
0
;
 i 
<
 nVerts
;
 i
++)
 
{
            vertexArray
[
i
].
wasVisited 
=
 
false
;
        
}
    
}
}

其中StackX和QueueX类的代码如下:

public
 
class
 
QueueX
 
{
    
private
 
final
 
int
 SIZE 
=
 
20
;
    
private
 
int
[]
 queArray
;
    
private
 
int
 front
;
    
private
 
int
 rear
;
    
public
 
QueueX
()
 
{
        queArray 
=
 
new
 
int
[
SIZE
];
        front 
=
 
0
;
 
        rear 
=
 
-
1
;
    
}
    
public
 
void
 insert
(
int
 j
)
 
{
        
if
(
rear 
==
 SIZE
-
1
)
 
{
            rear 
=
 
-
1
;
        
}
        queArray
[++
rear
]
 
=
 j
;
    
}
    
public
 
int
 remove
()
 
{
        
int
 temp 
=
 queArray
[
front
++];
        
if
(
front 
==
 SIZE
)
 
{
            front 
=
 
0
;
        
}
        
return
 temp
;
    
}
    
public
 
boolean
 isEmpty
()
 
{
        
return
 
(
rear
+
1
 
==
 front 
||
 front
+
SIZE
-
1
 
==
 rear
);
    
}
}

public
 
class
 
StackX
 
{
    
private
 
final
 
int
 SIZE 
=
 
20
;
    
private
 
int
[]
 stack
;
    
private
 
int
 top
;
    
public
 
StackX
()
 
{
        stack 
=
 
new
 
int
[
SIZE
];
        top 
=
 
-
1
;
    
}
    
public
 
void
 push
(
int
 j
)
 
{
        stack
[++
top
]
 
=
 j
;
    
}
    
public
 
int
 pop
()
 
{
        
return
 stack
[
top
--];
    
}
    
public
 
int
 peek
()
 
{
        
return
 stack
[
top
];
    
}
    
public
 
boolean
 isEmpty
()
 
{
        
return
 
(
top 
==
 
-
1
);
    
}
}

图中还有个最小生成树的概念,所谓最小生成树,就是用最少的边连接所有的顶点。对于给定的一组顶点,可能又很多种最小生成树,但是最小生成树边E的数量总是比顶点V的数量小1,即E=V-1。寻找最小生成树不需要关心边的长度,并不需要找到一条最短路径,而是要找最少数量的边(最小路径在带权图中讨论)。

创建最小生成树的算法与搜索算法几乎是相同的,它同样可以基于广度优先搜索和深度优先搜索,这里使用深度优先搜索。在执行深度优先搜索的过程中,如果记录走过的边,就可以创建一棵最小生成树,可能会感到有点奇怪。见下面的程序(是上面Graph类中的一个方法,加到Graph类中即可):

public
 
void
 minSpanningTree
()
 
{
    vertexArray
[
0
].
wasVisited 
=
 
true
;
    stack
.
push
(
0
);
    
while
(!
stack
.
isEmpty
())
 
{
        
int
 currentVertex 
=
 stack
.
peek
();
        
int
 v 
=
 getAdjUnvisitedVertex
(
currentVertex
);
        
if
(
v 
==
 
-
1
)
 
{
            stack
.
pop
();
        
}
        
else
 
{
            vertexArray
[
v
].
wasVisited 
=
 
true
;
            stack
.
push
(
v
);
            displayVertex
(
currentVertex
);
 
//from currentV
            displayVertex
(
v
);
 
//to v
            
System
.
out
.
print
(
" "
);
        
}
    
}
    
//stack is empty, so we're done
    
for
(
int
 j 
=
 
0
;
 j 
<
 nVerts
;
 j
++)
 
{
        vertexArray
[
j
].
wasVisited 
=
 
false
;
    
}
}

好嘞,我的图就说这么多嘞,文章建议收藏,在等公交、吃饭排队的时候可以拿出来读一读,提升一波内功。利用碎片化时间来学习。

END

这里不仅有技术,还有段子,有感悟,有资源。来吧,还等什么呢~

更多相关阅读:

如果让你手写个栈和队列,你还会写吗? 开发了那么多项目,你能自己手写个健壮的链表出来吗? 下次面试若再被问到二叉树,希望你能对答如流! 面试还在被红-黑树虐?看完这篇轻松搞定面试官 2-3-4树是如何解决二叉树中非平衡问题的? 读完这篇,希望你能真正理解什么是哈希表