用邻接表实现了一个无向图,在实现时,包含了添加和删除顶点,添加和删除边,size方法(顶点个数),isEmpty方法,广度和深度优先迭代器
1,成员变量,构造方法,数组扩展
private VNode[] VNodes; //将顶点放在数组中
private int nodeCount; //顶点个数,也表示下一个可用的顶点数组下标
private int edgeCount; //边个数
//int kind; //图种类标识
publicUnDirectedGraph() //构造一个空图
{
VNodes = newVNode[5];
nodeCount = 0;
edgeCount = 0;
}
private void expand() //数组扩展
{
VNode[] larger = new VNode[VNodes.length * 2];
for(int i = 0;i < VNodes.length;i++)
{
larger[i] = VNodes[i];
larger[i].setFirst(VNodes[i].getFirst()); //注意还要把边链表拉起来
}
VNodes = larger;
}
当顶点数组不够用时,要扩展顶点数组,这个跟之前的差别在于将VNodes[i]赋给lager[i],后,还要把它的边链表拉过来,见注释
2,建图(顶点,边的添加删除方法)
1)添加一个顶点容易,直接在数组里添加一个元素即可(而且是添加到最后一个下标,所以很easy)
2)删除一个顶点要先删除跟这个顶点有关联的全部边,然后将这个顶点从数组里移除,为了保持数组的连续性,还要把数组里在该顶点之后的顶点前移,而且还要把边链表拉过来(之前我也就考虑到了这一步,写这个文章的时候忽然发现还有错,当后面的顶点都往前移动了,顶点在数组里的位置就变了,那么数组里所有顶点的边链表里的边信息(边的2个端点)就要变)
3)添加一条边,在无向图里,添加一条从VNodes[i]到VNodes[i]的边和反过来添加是一样的,要在两个顶点的边链表里分别添加
4)删除一条边,同上,要在2个顶点的边链表里同时删除
从上面的分析可以看出,删除顶点的时候要调用删除边的方法,也就是说删除顶点包含了删除边。所以先来实现边的方法,再来实现顶点的方法
边的表示:
package Graph;
public class Edge {
private int start,end; //边指向两个节点的位置(如果用数组存放节点,就是下标,如果用链存放下标,就是位置索引)
private Edge next; //边指向的下一条边
private int len; //边的信息(长度)
public Edge(int start,int end) //用起始顶点和结束顶点构造一条边
{
this.start= start;
this.end =end;
next = null;
}
public Edge(int start,int end,intlen) //用起始顶点和结束顶点构造一条边
{
this.start= start;
this.end =end;
this.len =len;
next = null;
}
//set get省略
}
用了2种方法来构造边,一种要长度,一种不要长度。
1)public void addEdge(int start,int end,intlen) 在指定2个顶点之间添加指定长度的边
public void addEdge(intstart,int end,intlen){ //在两个指定下标的节点之间添加一条边
if(start< 0 || start >= size() || end < 0 || end >= size() || start == end)
{
System.out.println(“节点选取非法!!!”);
return;
}
if(hasEdge(start,end))
{
System.out.println(“这两点之间已经存在一条边!!!”);
return;
}
//写一个支持方法将某个边添加到一个顶点的边链表中,减少了许多注释中的重复代码
Edge edge = newEdge(start,end,len);
addEdgeToVNodeList(VNodes[start],edge);//将边添加到某个顶点边链表中的方法
edge = newEdge(end,start,len);
addEdgeToVNodeList(VNodes[end],edge);
edgeCount++;
}
其中用到了2个支持方法,hasEdge(int start,int end)判断2个顶点之间是否有边,这个也可以当公有方法用;private void addEdgeToVNodeList(VNode node,Edge edge) 将条边添加到一个顶点的边链表中去的方法(前提是这个边肯定是这个顶点的边链表中的边),注意要调用2次这个方法
下面看一下这2个支持方法:
public boolean hasEdge(int start,int end):
public boolean hasEdge(intstart,int end){ //判断2个顶点之间是否存在边
if(VNodes[start].getFirst()== null)
return false;
else
{
Edge temp =VNodes[start].getFirst();
while(temp!= null)
{
if(temp.getEnd()== end)
return true;
else temp= temp.getNext();
}
}
return false;
}
private void addEdgeToVNodeList(VNode node,Edge edge):
private void addEdgeToVNodeList(VNode node,Edge edge){//将一条边添加到某个节点的边链表中
if(node.getFirst()== null) //将边添加到start顶点的边链表中去
node.setFirst(edge);
else{
Edge temp = node.getFirst();
while(temp.getNext()!= null)
temp =temp.getNext(); //最后一条边
temp.setNext(edge);
}
}
2)public void removeEdge(int start,intend) //删除两个指定下标顶点之间的边
public void removeEdge(intstart,int end){ //删除两个指定下标顶点之间的边
if(start< 0 || start >= size() || end < 0 || end >= size() || start == end)
{
System.out.println(“节点选取非法!!!”);
return;
}
if(!hasEdge(start,end))
{
System.out.println(“所删除的两点之间不存在边!!!”);
return;
}
//存在边得时候分别从两个顶点的边链表中删除即可
Edge edge = newEdge(start,end); //不需要知道长度信息
removeEdgeFromVNodeList(VNodes[start],edge);
edge = newEdge(end,start);
removeEdgeFromVNodeList(VNodes[end],edge);
edgeCount–;
}
同样需要从2个端点的边链表中同时删除那条边,也将从某个端点的边链表中删除某个边写为一个支持方法,减少重复代码:
private void removeEdgeFromVNodeList(VNode node,Edge edge):
//从某个顶点的边链表中删除某个边的操作(顶点的边链表中存在这条边的时候才会调用,所以不用再讨论存不存在)
private void removeEdgeFromVNodeList(VNode node,Edgeedge){
Edge temp = node.getFirst();
if(temp.getEnd()== edge.getEnd()) //末尾相等即可,同一顶点的边链表中的边得起始点都相同
node.setFirst(temp.getNext());
else //如果首条边不是要删的边
{
Edge preTemp = temp;
temp = temp.getNext();
while(temp.getEnd()!= edge.getEnd())
{
preTemp = temp;
temp = temp.getNext();
}
preTemp.setNext(temp.getNext());
}
}
3)public void addVNode(Object element) //添加一个顶点
最简单了,
public void addVNode(Object element){ //添加顶点
VNode node = newVNode(element);
if(size()== VNodes.length)
expand();
VNodes[nodeCount] = node;
nodeCount++;
//VNodes[nodeCount-1].setFirst(null);//可以省略,默认null
}
4)public Object removeVNode(int position) //删除指定下标处的顶点
先要删除关联边,还要保持数组连续性,更麻烦的是为了保持数组连续性,顶点的下标也变了,所有顶点的边链表中的边的起始和结束端点的信息有的要变(与移动过的那些顶点相关的边)。刚发现的一个错误,我还没有实现,待会补上~
//从图中删除一个顶点
public ObjectremoveVNode(int position){//删除指定下标处的顶点(注意先将相关联边删除再将顶点删除)
if(isEmpty())
{
System.out.println(“图为空!!!”);
return null;
}
if(position< 0 || position >= size())
{
System.out.println(“下标非法!!!”);
return null;
}
Object result = VNodes[position];
for(int i = 0; i < size();i++) //1,删除每一条与它相关联的边
if(i !=position && hasEdge(i,position))
removeEdge(i,position);
for(int i = 0;i < size();i++) //2,调整由于将要移动顶点导致的每个顶点边链表的边信息
if(i !=position)
{
Edge temp =VNodes[i].getFirst();
while(temp!= null)
{
if(temp.getStart()> position) //与要移动的顶点关联的边都要调整
temp.setStart(temp.getStart()-1);
if(temp.getEnd() > position)
temp.setEnd(temp.getEnd()-1);
temp =temp.getNext();
}
}
for(int i = position;i < size()-1;i++) //3,然后直接移除顶点,移动数组保持数组的连续性即可
{
VNodes[i] = VNodes[i+1];
VNodes[i].setFirst(VNodes[i+1].getFirst());
}
VNodes[nodeCount-1] = null;//最后一位置空
nodeCount–;
returnresult;
}
刚开始的时候没有考虑第二步,添加一个for循环遍历顶点数组,在移动顶点元素之前将所有顶点的边链表信息更改后,再直接移动即可,更改方法为,边链表中的所有边的信息中,无论是start还是end,只要大于被删的position(说明待会这个点要往前移动一个),就重新设置start or end为递减1的值。
3,图的遍历
1)广度优先遍历
用队列实现,先将第一个点放入遍历队列,并置为已访问过的。然后开始循环做下列事情:只要遍历队列不空,就将队列中的元素出队,并将它置为已访问过的,然后将它的未被访问过的邻接顶点入队。
可以用一个结果队列用来实现迭代器,将每次出队的顶点依次放入结果队列,返回结果队列的迭代器。
下列方法实现了从某个顶点开始,将这个顶点所在的连通区域按照广度优先顺序放入结果队列:
//按照广度规则从position开始将position所在连通分量顶点进队
private void BFSorder(intposition,LinkedQueue queue){
LinkedQueue tempQueue = new LinkedQueue();//遍历队列
tempQueue.enqueue(VNodes[position]);//队列中存放顶点类型
VNodes[position].setVisited(true);
while(!tempQueue.isEmpty())
{
VNode node = (VNode)tempQueue.dequeue();//tempQueue的元素一次出队放入Queue即可
queue.enqueue(node);
Edge temp = node.getFirst();
while(temp!= null) //将没有被访问过的关联顶点放入tempQueue
{
int index= temp.getEnd();//相关联的顶点的下标
VNode linkNode = VNodes[index];
if(linkNode.getVisited()== false) //如果没有被访问过
{
tempQueue.enqueue(VNodes[index]);
VNodes[index].setVisited(true);
}
temp = temp.getNext();
}
}
}
图有可能有几个连通域,对于上述方法,仅能将起始点所在连通域的顶点全部遍历到,所以将上述方法独立出来,再写两个主方法来调用,就可以分别实现仅遍历一个连通域的广度优先遍历和整个图的广度优先遍历
只有一个连通域,或者只遍历第一个顶点所在连通域的广度优先:直接调用即可
public Iterator SingleBFS(intposition){ //只遍历position所在连通域的BFS
LinkedQueue queue = new LinkedQueue();//放结果的队列
BFSorder(position,queue);//将position所在联通分量的顶点进队
returnqueue.iterator();
}
遍历整个图的广度优先遍历:可能有多个连通域,只需要遍历完一个后检查顶点数组是否被全部遍历到,如果没有,就从没有遍历到的顶点继续调用广度优先遍历,循环即可。
public Iterator GraphBFS(intposition){ //可以遍历多个联通分量的BFS
LinkedQueue queue = new LinkedQueue();
BFSorder(position,queue);//将position所在联通分量的顶点进队
for(int i = 0;i < size();i++)
if(VNodes[i].getVisited()== false)
BFSorder(i,queue);
returnqueue.iterator();
}
2)深度优先遍历
深度优先遍历用栈实现,开始将起始顶点压栈,并置为已访问过的。然后循环的做下列事情:只要栈不空,就出栈,然后将出栈元素的所有未被访问过的邻接顶点入栈,循环直到栈空。
如果要实现一个迭代器,同样用一个结果队列收集每次出栈的顶点,返回队列的迭代器即可。
可以发现,深度和广度的实现唯一的差别就在于一个用栈一个用队列,其他在逻辑上都是相同的,这样导致深度优先实现起来跟上面的代码几乎完全一样,只需要遍历队列改成遍历栈即可。
直接贴出全部代码:
//深度优先遍历图
publicIterator GraphDFS(int position){ //可以遍历多个连通分量的DFS
LinkedQueue queue = new LinkedQueue();//放结果的队列
DFSorder(position,queue);
for(int i = 0 ;i < size();i++)
if(VNodes[i].getVisited()== false)
DFSorder(i,queue);
returnqueue.iterator();
}
publicIterator SingleDFS(int position){ //只遍历position所在连通域的DFS
LinkedQueue queue = new LinkedQueue();//放结果的队列
DFSorder(position,queue);
returnqueue.iterator();
}
//按照深度优先规则将position所在连通域顶点进队
public void DFSorder(intposition,LinkedQueue queue){
LinkedStack tempStack = new LinkedStack();//遍历栈
tempStack.push(VNodes[position]);//栈中同一存放顶点类型
VNodes[position].setVisited(true);
while(!tempStack.isEmpty()) //每个顶点出栈进入队列后要将其未被访问的关联顶点入栈
{
VNode node = (VNode)tempStack.pop();//tempStack的元素依次出栈放入Queue即可
queue.enqueue(node);
Edge temp = node.getFirst();
while(temp!= null) //将没有被访问过的关联顶点放入tempStack
{
int index= temp.getEnd();//相关联的顶点的下标
VNode linkNode = VNodes[index];
if(linkNode.getVisited()== false) //如果没有被访问过
{
tempStack.push(VNodes[index]);
VNodes[index].setVisited(true);
}
temp = temp.getNext();
}
}
}
3)还有一点需要注意,每遍历完一次,顶点是否被访问的属性就改变了,为了清除访问记录,写一个恢复的方法很有必要。
public void clearVisited(){ //清除访问记录
for(int i = 0;i < size();i++)
VNodes[i].setVisited(false);
}
4,清单及方法正确性的测试
上面基本上已经把方法都贴出来了,还是把整个代码也贴出来吧:
UnDirectedGraph
构造了下面的图:
1)刚开始的时候,遍历结果如下:
广度优先遍历为:
0 2 3 1 5 7 9 4 8 6
深度优先遍历为:
0 3 9 6 8 5 1 4 7 2
多联通分量的广度优先遍历为:
0 2 3 1 5 7 9 4 8 6
多联通分量的深度优先遍历为:
0 3 9 6 8 5 1 4 7 2
2)g.removeEdge(0, 3);删除0到3之间的边后:
广度优先遍历为:
0 2 1 5 4 9 3 6 7 8
深度优先遍历为:
0 2 5 9 6 8 7 4 3 1
多联通分量的广度优先遍历为:
0 2 1 5 4 9 3 6 7 8
多联通分量的深度优先遍历为:
0 2 5 9 6 8 7 4 3 1
3) g.removeVNode(9);删除顶点9后:
广度优先遍历为:
0 2 3 1 5 7 4 8 6
深度优先遍历为:
0 3 7 8 6 2 5 1 4
多联通分量的广度优先遍历为:
0 2 3 1 5 7 4 8 6
多联通分量的深度优先遍历为:
0 3 7 8 6 2 5 1 4
4)多连通分量,略
总结:
主要实现了1)图的建立,更改(增删顶点和边) 2)图的遍历
图的广度和深度优先搜索区别就在于用队列还是用栈,广度优先是横向扩展,因此每次顶点出队的时候将其邻接顶点入队,这样保证先进先出,而深度是深向扩展,每次顶点出栈时,要将其邻接顶点入栈,这样保证先进后出,只有将某一个顶点深向完后才返回到同一级的下一个顶点。
注意深度优先总是走到离当前结点最远的地方才返回,只需要看深度即可,在有环的时候不要产生误解,例如上述1)中 0 3 9 6 8 它并不会到达7,8是这条线上的最大深度。