图的存储结构
十字链表(针对有向图)
由于简单的拉链法对有向图太不友好(统计入度需要扫描全表),所以引入十字链表。
十字链表由数组+链表组成,数组存放节点对象,链表存放边对象。
这样的优势是,顺着出边表可以找到所有以该节点为出点的边,顺着入边表可以找到所有以该节点为入点的边。
/**
* 十字链表
* 针对有向图
*/
public static class AcrossLinkedList{
//数组部分
NodeElement[] elements = new NodeElement[10];
/**
* 用于描述节点
*/
public static class NodeElement{
//以该节点为出点的边的链表的头节点
EdgeElement outerEdgeElement;
//以该节点为入点的边的链表的头节点
EdgeElement innerEdgeElement;
//该节点中存储的数据
Object data;
public NodeElement(EdgeElement outerEdgeElement, EdgeElement innerEdgeElement, Object data) {
this.outerEdgeElement = outerEdgeElement;
this.innerEdgeElement = innerEdgeElement;
this.data = data;
}
}
/**
* 用于描述边
*/
public static class EdgeElement{
//从哪个节点出
NodeElement outerNode;
//到哪个节点入
NodeElement innerElement;
//出链表中下一个边节点:即下一个出节点相同的边
EdgeElement outerNextEdge;
//入链表中下一个边节点:即下一个入节点相同的边
EdgeElement innerNextEdge;
public EdgeElement(NodeElement outerNode, NodeElement innerElement, EdgeElement outerNextEdge, EdgeElement innerNextEdge) {
this.outerNode = outerNode;
this.innerElement = innerElement;
this.outerNextEdge = outerNextEdge;
this.innerNextEdge = innerNextEdge;
}
}
public static void main(String[] args) {
AcrossLinkedList acrossLinkedList = new AcrossLinkedList();
NodeElement a = new NodeElement(null, null, "A");
NodeElement b = new NodeElement(null, null, "B");
NodeElement c = new NodeElement(null, null, "C");
NodeElement d = new NodeElement(null, null, "D");
EdgeElement bc = new EdgeElement(b, c, null, null);
EdgeElement ac = new EdgeElement(a, c, null, bc);
EdgeElement cd = new EdgeElement(c, d, null, null);
EdgeElement cb = new EdgeElement(c, b, cd, null);
EdgeElement da = new EdgeElement(d, a, null, null);
EdgeElement ab = new EdgeElement(a, b, ac, cb);
a.outerEdgeElement=ab;
b.outerEdgeElement=bc;
c.outerEdgeElement=cb;
d.outerEdgeElement=da;
a.innerEdgeElement=da;
b.innerEdgeElement=ab;
c.innerEdgeElement=ac;
d.innerEdgeElement=cd;
acrossLinkedList.elements[0] = a;
acrossLinkedList.elements[1] = b;
acrossLinkedList.elements[2] = c;
acrossLinkedList.elements[3] = d;
System.out.println(acrossLinkedList.elements);
}
}
邻接多重表(针对无向图)
同样,如果用简单的拉链法表示无向图,如果要删除某条边,则需要删除两处(比如删除ab边,就得删除ab节点和ba节点)。
邻接多重表中,为了使删除方便,希望所有依附于同一节点的边都在一个链表里,同时不论ab还是ba,只用一个节点表示。
public static class multiAjacent{
/**
* 用于描述节点
*/
public static class MaNode{
//用于存放数据
Object data;
//用于指向边
MaEdge maEdge;
public MaNode(Object data, MaEdge maEdge) {
this.data = data;
this.maEdge = maEdge;
}
}
/**
* 用于描述边
*/
public static class MaEdge{
MaNode from;
MaNode to;
//下一条依附于from节点的边
MaEdge belongFromEdge;
//下一条依附于to节点的边
MaEdge belongToEdge;
public MaEdge(MaNode from, MaNode to, MaEdge belongFromEdge, MaEdge belongToEdge) {
this.from = from;
this.to = to;
this.belongFromEdge = belongFromEdge;
this.belongToEdge = belongToEdge;
}
}
}
图的遍历
从图中某一顶点出发,遍历访问其他所有顶点,且使得每一个顶点只被访问一次
广度优先搜索 类似于树的层序遍历,设有向图如图, 则BFS算法流程如下:
- 申请一个访问状态队列A,A[0]=A节点是否被访问过,A[1]表示B节点是否被访问过,依次类推
- 申请一个辅助队列B,用于存放待处理节点
- A节点入辅助队列
- 若辅助队列不为空,则处理辅助队列中的节点(此时队列中只有A节点),A节点出队,修改访问状态队列中A的被访问状态为true,遍历从A出去的弧,把被这些弧的另外一个节点入辅助队列
- 循环步骤4,直到访问状态队列中的所有节点访问状态均为true为止
代码为:
public static void BFS(AcrossLinkedList acrossLinkedList){
//申请一个节点被访问状态序列
Map<String,Boolean> visited = new HashMap<>(4);
visited.put("A",false);
visited.put("B",false);
visited.put("C",false);
visited.put("D",false);
//申请一个存放待处理节点的队列
Queue<NodeElement> assistanceQueue = new LinkedList<>();
//循环直到所有节点访问状态均为true为止
for (NodeElement element : acrossLinkedList.elements) {
assistanceQueue.offer(element);
while (!assistanceQueue.isEmpty()){ //当队列不为空
//顺序弹出队列中的节点
NodeElement poll = assistanceQueue.poll();
//若已经访问过,则跳过
if (visited.get(poll.data)){
continue;
}
System.out.println(poll.data); // 输出
visited.put(poll.data.toString(),true);//将访问状态改为true
EdgeElement edge = poll.outerEdgeElement;
while (edge!=null){
//将边的另一头入队
assistanceQueue.offer(edge.innerElement);
edge = edge.outerNextEdge;
}
}
}
}
}
知识点:
- 最坏情况下,空间复杂度为o(|v|),即辅助队列满的情况
- 时间复杂度=每个节点入队一次的复杂度+每条边处理一次的复杂度=o(|v|+|E|),相比于邻接矩阵o(|v|2)快了很多
- 邻接链表产生的广度优先生成树是不唯一的,邻接矩阵是唯一的
深度优先搜索
类似于树的先序遍历,先根,然后递归左孩子,再递归右孩子,设待遍历的无向图如图
其搜索流程是先<0,1> -> <1,2> -> <2,3> -> <3,4> 尽可能深的搜索
/**
* 多重邻接表
*/
public static class MultiAdjacent{
//数组部分
MaNode[] elements = new MaNode[4];
/**
* 用于描述节点
*/
public static class MaNode{
//用于存放数据
Object data;
//用于指向边
MaEdge maEdge;
public MaNode(Object data, MaEdge maEdge) {
this.data = data;
this.maEdge = maEdge;
}
}
/**
* 用于描述边
*/
public static class MaEdge{
MaNode from;
MaNode to;
//下一条依附于from节点的边
MaEdge belongFromEdge;
//下一条依附于to节点的边
MaEdge belongToEdge;
public MaEdge(MaNode from, MaNode to, MaEdge belongFromEdge, MaEdge belongToEdge) {
this.from = from;
this.to = to;
this.belongFromEdge = belongFromEdge;
this.belongToEdge = belongToEdge;
}
}
public static void main(String[] args) {
MultiAdjacent multiAdjacent = new MultiAdjacent();
MaNode a = new MaNode("A", null);
MaNode b = new MaNode("B", null);
MaNode c = new MaNode("C", null);
MaNode d = new MaNode("D", null);
MaEdge ab = new MaEdge(a, b, null, null);
MaEdge ac = new MaEdge(a, c, ab, null);
MaEdge ad = new MaEdge(a, d, ac, null);
MaEdge bc = new MaEdge(b, c, null, ac);
MaEdge cd = new MaEdge(c, d, null, ad);
a.maEdge = ad;
b.maEdge = bc;
c.maEdge = cd;
d.maEdge = null;
multiAdjacent.elements[0] = a;
multiAdjacent.elements[1] = b;
multiAdjacent.elements[2] = c;
multiAdjacent.elements[3] = d;
//申请一个节点被访问状态序列
Map<String,Boolean> visited = new HashMap<>(4);
visited.put("A",false);
visited.put("B",false);
visited.put("C",false);
visited.put("D",false);
for (MaNode element : multiAdjacent.elements) {
DFS(element,visited);
}
}
/**
* 类似于树的先序遍历,访问根节点,递归处理belongFromEdge,再递归处理belongToEdge
*/
public static void DFS(MaNode root,Map<String,Boolean> visited){
//递归出口
if (root==null){
return;
}
//若已经访问过,则跳过
if (visited.get(root.data)){
return;
}
//否则,先访问根节点
System.out.println(root.data);
visited.put(root.data.toString(),true);
MaEdge maEdge = root.maEdge;
if (maEdge!=null){
//递归“左子树” belongFromEdge
if (maEdge.belongFromEdge != null){
DFS(maEdge.belongFromEdge.to,visited);
}
//递归“右子树” belongToEdge
if (maEdge.belongToEdge != null){
DFS(maEdge.belongToEdge.from,visited);
}
}
}
}
结果:
知识点:
- 时间复杂度同样是o(|v|+|e|)
- 空间复杂度即递归栈的大小,最好的情况类似于平衡二叉树o(log2|v|),最坏情况o(|v|)
- 邻接矩阵的深度优先生成树是唯一的,多重邻接表的深度优先生成树是不唯一的