图的描述和抽象——动态聚集、关联和依赖的应用实例
一、 问题描述
在数据结构和图论中,我们经常会接触到“图”这种数据结构。“图”在很多程序中都有着极其重要的作用。因此,在计算机当中描述一个图,并将其显示出来是我们经常碰到的一个问题。在这里,我们将用面向对象的思想实现图的描述及其抽象。注意,我们描述的图是有向的(无向图可以当成有向图的一种特殊情况),而且我们允许两点之间存在多条线。比如,我们应该可以描述下面这样的这一种图(图1):
| |
(图1)(深色为起点,淡色为终点) | (图2) |
二、 问题分析
从上面的问题描述中可以看出,我们主要是解决两个问题。一个是描述图,一个是显示图。显然对于一个图来说,它应该是没有位置信息的,我们用一个BasicGraph类来描述这样的一种图,它描述纯粹的图的数据结构。而我们用另一个PositionGraph类来添加位置的信息,该类可以不必理会图的数据结构是如果组织的,它只需要知道点和线的位置信息。因此,这两个类有图2所示的继承关系。
(一)用动态聚集方法描述图
对于一个图来说,它动态聚集了点和线。我们用BasicPoint类来描述点,用BasicLine类来描述线。这两个类与BasicGraph位于同一个层次,它们都没有位置信息。
现在我们对一个图引入了三个类:BasicPoint,BasicLine和BasicGraph。这三个类具有如下的关系:
1. 一条线有两个端点,一个为起点,一个为始点。
2. 一个点可能与多条边相连。将与某一端点相连的线分成两类:一类是以该点为起点的线;另一类是以该点为终点的线。
3. 一个图由一组点与一组线组成。
由此,我们可以得到这三个类的关系图如图3,图4和图5所示:
|
| ||||
(图3) |
其中,图3说明了一个图是由一组点和一组线动态聚集而成。图并不知道点和线的关系,点和线的关系是散布在点和线之间的,点和线之间更详细的关系见图4和图5。从图中我们可以看出,图知道它所有的点和所有的线;点知道以它为起点的所有线,也知道以它为终点的所有线;线知道它的起点和终点。
(二)用继承方法描述有位置信息的图
让一个图在屏幕中显示出来,我们不仅需要知道图的拓扑结构,而且还需要知道点和线的位置信息。显然上面的三个类已经足以描述图的拓扑结构了。接下来,我们必须在已有的类中加入位置的信息。从而得到三个包含位置信息的类:PositionPoint,PositionLine和PositionGraph。这三个类分别继承于上面的BasicPoint,BasicLine和BasicGraph。
对于点,我们使用setPosition方法添加点的位置信息,给点定位;对于线,我们使用setGradualPoint方法设置渐近点,这样,通过起点、终点以及渐近点,我们可以画出一条曲线出来。
(三)用关联方法在画板上显示有位置信息的图
我们的目的是能够在多个画板上显示图,因此我们使用简化的MVC模式。在这个简化的MVC模式里面,我们只有M(Model)和V(View),而不考虑C(Controller)。我们这里的M就是一个PositionGraph类,而V是一种实现了Visuable接口的类,Visuable接口定义了一个方法repaint,使得每一次PositionGraph改变之后,可以调用V中的repaint函数,重新画图。
显然,PositionGraph也可以动态聚集多个实现了Visuable接口的类,以更在多个地方显示图。而单个实现了Visuable接口的类将通过关联的方法,访问PositionGraph的数据结构,并将其显示出来。我们这里给出了Visuable接口的一个实现类DrawingPanel。它们的关系见图6所示,这里,一个DrawingPanel只能画一个PositionGraph:
| |
(图6) | (图7) |
(四)用依赖方法将一个无位置图转变成有位置图
通常,我们只关心图的拓扑结构,而并不关系它的位置。而我们最后又希望可以在程序中直接显示出来。因此,我们需要一个助手类,它可以根据一个BasicGraph,一个画板(DrawingPanel,提供高度和宽度信息),而自动的定位点以及线的位置,然后自动产生一个PositionGraph。这就是类PosGraphHelper的作用了。关于刚刚提及的几个类的关系,见图7所示。
对一个无位置的图的点和线进行定位是很困难的,我们只是给出一种较简单的实现。如果有更好的算法,我们只需要更新PosGraphHelper类即可。
三、 动态聚集的实现
动态聚集可以多种方法实现,Java2中给出了Collections框架,并提供了多个类可以让我们实现动态聚集。基本上它们分为四类:哈希表,可变长数组,平衡树和链表。Vector类是属于可变长数组;而TreeMap和HashMap属于哈希表,TreeMap比HashMap多了一个功能,可以对关键字进行排序。
在BasicGraph描述的图当中,我们给每一个点一个唯一的ID,每条线也有唯一的ID。我们可能经常需要通过ID来访问图当中的点和线,因此,使用哈希表来保存图当中的点和线是最好的选择。经常我们给点的ID,可能是:1、2、3┉,或者P1、P2、P3┉。因此,点最好是可以按ID的次序进行排列的。因此,在BasicGraph当中我们使用如下的语句来动态聚集点和线:
TreeMap points;
HashMap lines;
一条线包含两个端点的信息,因此,BasicLine只需要包含如下的信息即可:
BasicPoint startPoint,endPoint;
而一个点会记录所有与它相邻的线,这些线分成两类,一类以该点为起点,一类以该点为终点。在BasicPoint中这些线使用下面的方面聚集起来:
Vector linesAsStartPoint;
Vector linesAsEndPoint;
现在的问题,是必须在BasicGraph中添加或删除点和线时自动维护这些信息。下面给出添加线的顺序图如下:
在这里,其它类必须对BasicGraph操作以实现对图的修改,首先,如果要添加一条线时,我们必须给出起点的ID(sid)和始点的ID(eid),还要给出所添加线的ID(id)。当以这些参数调用addLine时,BasicGraph通过sid和eid从哈希表取出起点(sp)和终点(ep),用以新生成一个BasicLine的实例。在新生成一个BasicLine实例时,它会调用起点sp的addLineAsStartPoint方法,将线自身注册为起点sp的一条邻接线;同时,它会调用终点ep的addLineAsEndPoint方法,将线自身注册为终点ep的一条邻接线。最后,BasicGraph会调用自身的addLine方法,将新生成的一个BasicLine实例注册为自身的一条线。具体的程序见源码。
四、 关联的实现
在一个DrawingPanel类中,关联了一个有位置的图,即其拥有PositionGraph类的一个实例。当调用DrawingPanel类的setPosGraph方法时,DrawingPanely就初始化PositionGraph的这个实例。通过访问PositionGraph类的成员,DrawingPanely首先画点,接着画线,从而画出整个图。
一个图可以聚集多个视图,当只有一个视图且它就是一个DrawingPanel类的实例时,我们也可以看成从PositionGraph类可以关联到DrawingPanel。这样的一种关联的意义在于,PositionGraph一知道它自己的拓扑结构变的时候,它就可以调用PositionGraph的repaint方法进行重画。
五、 依赖的实现
在PosGraphHelper中,我们提供了几个方法,可以实现从一个BasicGraph类生成一个PositionGraph的方法,或者自动生成一个完全图等等。比如,getPositionGraph可以根据DrawingPanel的大小,将BasicGraph转成一个PositionGraph。其源码如下:
public static PositionGraph getPositionGraph(DrawingPanel panel,BasicGraph bgraph){
PositionGraph pgraph = new PositionGraph();
reshape(panel,bgraph,pgraph);
return pgraph;
}
其中的reshape方法可以实现对点和线的重新定位,而最后生成PositionGraph,返回给调用者。Reshape方法还很不完善,有待进一步修改。
六、 用到的类列表
到目前,我们已经解说了使用到的大部分类。现在把所有的类列出来,如下表所示:
类名 | 作用 |
BasicPoint | 实现基本点,基本点只有点线关系,没有位置信息 |
BasicLine | 实现基本线,基本线只有点线关系,没有位置信息 |
BasicGraph | 实现拓扑图,没有位置信息 |
PositionPoint | BasicPoint的子类,添加位置信息 |
PositionLine | BasicLine的子类,添加位置信息 |
PositionGraph | BasicGraph的子类,实现一类有位置的图 |
Visuable | 接口,定义一类可以重画(repaint)的类 |
DrawingPanel | 画板,实现Visuable接口,关联一个PositionGraph对象,实现画图的功能 |
PosGraphHelper | 助手类,用于产生一些特殊的PositionGraph,或者从BasicGraph转换而来 |
Frame1 | 测试类,用于测试上面的其它类 |
我们把前面九个类放在包DynAggregate当中。以和其它无关的类区别开来。
七、 实例
我们产生两个图,一个是无向图中的完全图,一个是有向图中的完全图(不包括点自身到自身的线)。这两个图是分别通过:
1. PosGraphHelper.produceFullGraph(5)
2. PosGraphHelper.produceFullGraph2(5)
生成的。它们产生的结果如下:
| |
有向图的完全图 | 无向图中的完全图 |
八、 部分源码(只包括一小部分)
1.BasicPoint源码
class BasicPoint{
protected String point_id; //唯一ID
protected Vector linesAsStartPoint; //保存所有以该点为起点的线
protected Vector linesAsEndPoint; //保存所有以该点为终点的线
public BasicPoint(String id){
point_id = id;
linesAsStartPoint = new Vector();
linesAsEndPoint = new Vector();
}
public void addLineAsStartPoint(BasicLine line){ //把线注册为以该点为起点的线
linesAsStartPoint.add(line);
}
public void addLineAsEndPoint(BasicLine line){ //把线注册为以该点为终点的线
linesAsEndPoint.add(line);
}
}
2.BasicLine源码
class BasicLine{
protected BasicPoint startPoint,endPoint;
protected String line_id;
protected int powerValue = 0;
public BasicLine(BasicPoint start,BasicPoint end,String id){ //线总由两个点组成
startPoint = start;
endPoint = end;
start.addLineAsStartPoint(this); //把该线与起点关联
end.addLineAsEndPoint(this); //把该线与终点关联
line_id = id;
}
public void resetStartPoint(BasicPoint start){ //有时可能需要改线的起点
startPoint.removeLineAsStartPoint(this);
start.addLineAsStartPoint(this);
startPoint = start;
}
public void resetEndPoint(BasicPoint end){ //有时可能需要改线的终点
endPoint.removeLineAsEndPoint(this);
end.addLineAsEndPoint(this);
endPoint = end;
}
}
3.BasicGraph源码
public class BasicGraph{
protected TreeMap points;
protected HashMap lines;
public BasicGraph() { //初始化
points = new TreeMap();
lines = new HashMap();
}
public Vector getPoints(){ //得到图的所有点
return new Vector(points.values());
}
public Vector getLines(){ //得到图的所有线
return new Vector(lines.values());
}
public BasicPoint addPoint(String id){ //对图加点
return addPoint(new BasicPoint(id));
}
public BasicPoint addPoint(BasicPoint point){ //对图加点
if(point != null) points.put(point.getID(),point);
return point;
}
public BasicLine addLine(String startID,String endID,String id){ //对图加线
Object start = points.get(startID), end = points.get(endID);
if(start == null) start = addPoint(startID);
if(end == null) end = addPoint(endID);
return addLine(new BasicLine((BasicPoint)start,(BasicPoint)end,id));
}
public BasicLine addLine(BasicLine line){ //对图加线
if(line != null) lines.put(line.getID(),line);
return line;
}
}
4.PositionPoint源码
public class PositionPoint extends BasicPoint{
private Point position;
public PositionPoint(String id){
this(id,new Point(0,0));
}
public PositionPoint(String id,Point pos){
super(id);
position = pos;
}
void setPosition(Point pos){ //设置点的位置
position = pos;
}
}
5.PositionLine源码
public class PositionLine extends BasicLine{
private Point gradualPoint = null; //线的位置已由起点和终点决定,但还可有渐近点
public PositionLine(PositionPoint startPoint,PositionPoint endPoint,String id){
super(startPoint,endPoint,id);
}
public void setGradualPoint(Point point){
gradualPoint = point;
}
public Point getGradualPoint(){
return gradualPoint;
}
}
6.PositionGraph源码
public class PositionGraph extends BasicGraph{
private Vector viewers = null; //可以注册多个视图
public PositionGraph() {
super();
viewers = new Vector();
}
public BasicPoint addPoint(String id) { //重载BasicPoint,以成生成PositionPoint点
PositionPoint pp = (PositionPoint)addPoint(new PositionPoint(id));
return pp;
}
public BasicPoint addPoint(BasicPoint point){ //重载,实现自动重画功能
PositionPoint pp = (PositionPoint)super.addPoint(point);
repaint();
return pp;
}
//重载BasicLine,以成生PositionLine线
public BasicLine addLine(String startID,String endID,String id){
Object start = points.get(startID), end = points.get(endID);
if(start == null || end == null) return null;
PositionLine pl =
(PositionLine)addLine(new PositionLine((PositionPoint)start,(PositionPoint)end,id));
return pl;
}
public BasicLine addLine(BasicLine line){ //重载,实现自动重画功能
PositionLine pl = (PositionLine)super.addLine(line);
repaint();
return pl;
}
public void addViewer(Visuable viewer){ //添加视图
viewers.add(viewer);
viewer.repaint();
}
public void repaint(){ //所有视图重画
for(int i = 0; i < viewers.size(); i++){
((Visuable)viewers.elementAt(i)).repaint();
}
}
}