圖論(3):Guava中Graph使用入門及原理介紹

關于guava中數據結構的基本情況官方介紹請先查看上一篇wiki文檔翻譯:圖論(2):Guava中Graph模塊(wiki翻譯),這一節我們使用具體的示例圖來測試各個接口的功能并以此查看對應源碼的具體實現。

由于Graph模塊中絕大部分的具體實現類都是private類型的(僅對本包可見)而僅暴露相關的interface(如:Graph、Network等),因此在實際使用過程中,我們并不會接觸到太多的具體實現類。這里為了理清Guava的實現原理,打算在用例中順帶梳理一下其實現。

測試用例準備

節點定義: {N1=1, N2=2, N3=3, N4=4}.
邊集定義: {E11="1-1", E11_A="1-1a", E12="1-2", E12_A="1-2a", E12_B = "1-2b", E21 = "2-1", E13 = "1-3", E31 = "3-1", E34 = "3-4", E44 = "4-4", E42 = "4-2"}.(邊名稱上數字表示其起點和終點)
預期節點數: NODE_COUNT=20.
預期邊數: EDGE_COUNT=20.

Graph功能介紹

特性
a)頂點唯一; b)支持有向邊和無向邊; c)邊只能通過兩個頂點隱式定義; d)不支持并行邊。

示例圖如下:


Graph測試用例圖
  1. 使用對應構建類GraphBuilder來構建Graph實例:
MutableGraph<Integer> graph1 = GraphBuilder.directed() //指定為有向圖
    .nodeOrder(ElementOrder.<Integer>insertion()) //節點按插入順序輸出
    //(還可以取值無序unordered()、節點類型的自然順序natural())
    .expectedNodeCount(NODE_COUNT) //預期節點數
    .allowsSelfLoops(true) //允許自環
    .build();
Log.d(TAG, "initlized graph1: " + graph1);

輸出:

initlized graph1:isDirected: true, allowsSelfLoops: true, nodes: [], edges: []

Builder中并沒有包含復雜的構造邏輯,而只是簡單設置了幾個全局屬性而已(如輸出所示:是否允許自環、是否有向等);build()接口為最終的構建接口,返回一個MutableGraph接口類型的返回值,此處返回的是其實現子類ConfigurableMutableGraph,內部通過一個ConfigurableMutableValueGraph實例來實現(所有的方法都調用該實例的方法實現)的,因為ValueGraph包含了Graph的全部功能,可以猜測到設計者也因此復用了同一套實現方案(ConfigurableMutableValueGraph)。

注:使用Builder類構建的實例都是Mutable類型的,表示這個Graph可以增刪節點和邊,與之對應的是Immutable類型,一般通過copyOf()的靜態函數實現,表示該類型是不可變類型(不能增加/刪除節點和邊)。

  1. 增加節點以及連接邊:
//插入邊(默認會將節點加入graph中)
graph1.putEdge(N2, N3);
graph1.putEdge(N1, N3);
graph1.putEdge(N1, N2);
graph1.putEdge(N2, N2);
graph1.addNode(N4);

//返回圖中所有的節點(順序依賴nodeOrder)
Set<Integer> nodes = graph1.nodes(); 
Log.d(TAG, "graph1 nodes count:" + nodes.size() + ", nodes value:" 
      + format(nodes));

//返回圖中所有的邊集合
Set<EndpointPair<Integer>> edges = graph1.edges();
Log.d(TAG, "graph1 edge count:" + edges.size() + ", edges value:" 
      + format(edges));

輸出:

graph1 nodes count:4, nodes value:{2,3,1,4} //按節點的插入先后順序
graph1 edge count:4, edges value:{<2 -> 2>,<2 -> 3>,<1 -> 2>,<1 -> 3>}

示例中的修改接口addNode()putEdge(),以及removeNode()removeEdge(),最終操作的數據結構是ConfigurableMutableValueGraph中的屬性nodeConnections,它是一個Map<N, GraphConnections>類型,保存每一個節點的連接關系(它的前趨有哪些節點、后繼有哪些節點、連接邊的權值是什么),其具體實現子類是DirectedGraphConnections(有向圖)或UndirectedGraphConnections(無向圖)。

注:此處節點的順序如果指定為無序unordered()或者自然順序natural()時將會影響節點的輸出順序:

//無序:節點的輸出順序
nodes value:{3,4,1,2}

//自然順序:節點輸出順序
nodes value:{1,2,3,4}

另外,Graph(ValueGraph)要求節點在圖中唯一(作為Map<N, GraphConnections>Key值),因此如果添加重復的節點會自動覆蓋已有的同名節點。

由于節點和邊在添加進來時就已經按照其邏輯關系保存在GraphConnections中了,因此下面示例在求其前趨、后繼、鄰接點、出度、入度等操作時,只要查詢該數據結構就能獲取相關信息了,下面為添加邏輯:

對于無向圖的UndirectedGraphConnections而言,由于不需要區分前趨和后繼,因此只要將其指定為鄰節點即可。如下源碼所示:

//UndirectedGraphConnections
  public void addPredecessor(N node, V value) { //添加前趨
    V unused = addSuccessor(node, value); //直接調用了添加后繼方法
  }

  public V addSuccessor(N node, V value) { //添加后繼
    return adjacentNodeValues.put(node, value); //直接將node和value的映射關系添加到Map中
  }

對于有向圖DirectedGraphConnections而言,則情況復雜一點,關鍵點在于在同一個Map中如何區分相關節點是前趨還是后繼。
代碼中是這樣定義的:

// Every value in this map must either be an instance of PredAndSucc with a successorValue of
// type V, PRED (representing predecessor), or an instance of type V (representing successor).
private final Map<N, Object> adjacentNodeValues;

其意思是:Map中的value值要么是一個PredAndSucc(這里表示既是前趨也是后繼)、要么是PRED(僅僅是前趨節點)、要么是V類型的實例(僅僅是后繼節點)。

添加前趨

  public void addPredecessor(N node, V unused) {
    Object previousValue = adjacentNodeValues.put(node, PRED); //首先直接添加PRED,標識是前趨
    if (previousValue == null) { //表示是首次添加該節點
      checkPositive(++predecessorCount); //前趨+1
    } else if (previousValue instanceof PredAndSucc) {
      // Restore previous PredAndSucc object.
      adjacentNodeValues.put(node, previousValue); //表示該節點已經包含了前趨和后繼關系,則恢復原來的值
    } else if (previousValue != PRED) { // successor //到這里已經有一個具體的V類型的值了,表示已經添加了后繼
      // Do NOT use method parameter value 'unused'. In directed graphs, successors store the value.
      adjacentNodeValues.put(node, new PredAndSucc(previousValue)); //則構造一個既是前趨也是后繼的數據
      checkPositive(++predecessorCount); //前趨+1
    }
  }

添加后繼

  public V addSuccessor(N node, V value) {
    Object previousValue = adjacentNodeValues.put(node, value); //首先直接添加value,表示是一個后繼
    if (previousValue == null) { //首次添加
      checkPositive(++successorCount); //后繼+1
      return null;
    } else if (previousValue instanceof PredAndSucc) { //已經加入過,且標識為既是前趨也是后繼
      adjacentNodeValues.put(node, new PredAndSucc(value)); //覆蓋其舊值
      return (V) ((PredAndSucc) previousValue).successorValue; //返回舊值
    } else if (previousValue == PRED) { //已經加入為前趨節點
      adjacentNodeValues.put(node, new PredAndSucc(value)); //則構造一個既是前趨也是后繼的數據
      checkPositive(++successorCount); //后繼+1
      return null;
    } else { // successor //已經是value類型的值,
      return (V) previousValue; //已經在第一行覆蓋了,返回其舊值即可。
    }
  }

判斷當前節點是前趨還是后繼方法:

  private static boolean isPredecessor(@Nullable Object value) {
    //是PRED或者是PredAndSucc類型就表示該節點是前趨節點(在添加時就是按這個規則加入的)
    return (value == PRED) || (value instanceof PredAndSucc);
  }

  private static boolean isSuccessor(@Nullable Object value) {
    //要么是具體的value類型值,要么是`PredAndSucc`類型,則表示是后繼節點
    return (value != PRED) && (value != null);
  }

刪除前趨節點和刪除后繼節點也按上述類型規則執行,此處省略其實現。

3.獲取節點的前趨列表:

Set<Integer> predecessors = graph1.predecessors(N2); //獲取N2的前趨
Log.d(TAG, "graph1 node:" + N2 + " predecessors:" + format(predecessors));

輸出:

graph1 node:2 predecessors:{1,2}

注:對于允許自環的圖allowsSelfLoops(true)中,一條自環邊在有向圖中既是前趨也是后繼,既是入度也是出度。

  1. 獲取節點的后繼列表:
graph1.putEdge(N2, N4); //圖上面示例圖中紅色邊所示,動態增加了一條邊
Set<Integer> successors = graph1.successors(N2); //獲取N2的后繼
Log.d(TAG, "add edge of (" + N2 + "->" + N4 + ") after graph1 node:" 
      + N2 + " successors:" + format(successors));

輸出:

add edge of (2->4) after graph1 node:2 successors:{4,2,3} //如圖所示
  1. 獲取節點的鄰接點列表(包括前趨和后繼):
Set<Integer> adjacents = graph1.adjacentNodes(N2); //獲取N2的鄰接點
Log.d(TAG, "graph1 node: " + N2 + ", adjacents: " + format(adjacents));

輸出:

graph1 node: 2, adjacents: {4,1,2,3}
  1. 獲取節點的度(入度和出度):
Log.d(TAG, "graph1 node: " + N2 + ", degree: " + graph1.degree(N2)
    + ", indegree: " + graph1.inDegree(N2) 
    + ", outdegree: " + graph1.outDegree(N2)); //N2的度、入度、出度

輸出:

graph1 node: 2, degree: 5, indegree: 2, outdegree: 3 //自環既是入度也是出度
  1. 判斷頂點連通性(是否有直連邊):
final boolean connecting23 = graph1.hasEdgeConnecting(N2, N3); //N2&N3是否連通
final boolean connecting14 = graph1.hasEdgeConnecting(N1, N4); //N1&N4是否連通
Log.d(TAG, "graph1 node " + N2 + " & " + N3 + " connecting: " + connecting23
    + ", node " + N1 + " & " + N4 + " connecting: " + connecting14);

輸出:

graph1 node 2 & 3 connecting: true, node 1 & 4 connecting: false //N1&N4之間無邊

判斷連通性,只需要后面那個節點是否是前一個節點的后繼即可,AbstractBaseGraph中實現:

  public boolean hasEdgeConnecting(N nodeU, N nodeV) {
    checkNotNull(nodeU);
    checkNotNull(nodeV);
    return nodes().contains(nodeU) && successors(nodeU).contains(nodeV);
  }
  1. 轉換成不可變graph(Immutable**類型)
ImmutableGraph<Integer> immutableGraph = ImmutableGraph.copyOf(graph1);
nodes = immutableGraph.nodes(); //返回圖中所有的節點(順序依賴nodeOrder)
Log.d(TAG, "immutable graph nodes count:" + nodes.size() 
      + ", nodes value:" + format(nodes));

輸出:

immutable graph nodes count:4, nodes value:{2,3,1,4} //同被拷貝圖順序

ImmutableGraph的實現方式實際上就是將Mutable**類型的數據原樣復制到沒有實現Mutable**接口的類ForwardingGraph中。

  1. 判斷是否存在環(第一個頂點和最后一個頂點相同的路徑稱為)
final boolean cycle = Graphs.hasCycle(graph1);
Log.d(TAG, "graph1 has cycle: " + cycle);

輸出:

graph1 has cycle: true //因為N2節點存在一條自環,如果去掉則不存在環
  1. 獲取僅包含指定節點的生成子圖:
Set<Integer> subNodes = new HashSet<>();
subNodes.add(N1);
subNodes.add(N2); //獲取只包含N1和N2的生成子圖
MutableGraph<Integer> subgraph = Graphs.inducedSubgraph(graph1, subNodes);
Log.d(TAG, "subgraph: " + subgraph);

輸出:

subgraph: isDirected: true, allowsSelfLoops: true, nodes: [1, 2], 
edges: [<1 -> 2>, <2 -> 2>]
  1. 獲取節點的可到達列表(獲取能訪問到的節點結合,不單指直連邊):
Set<Integer> reachNodes = Graphs.reachableNodes(graph1, N2); //N2的可達列表
Log.d(TAG, "graph1 node: " + N2 + ", reachNodes: " + format(reachNodes));

輸出:

graph1 node: 2, reachNodes: {2,4,3} //N2不存在能訪問到N1的邊

這里是通過從起始點N開始進行BFS遍歷的結果。

  1. 獲取圖的傳遞閉包(如果節點A的可達列表reachableNodes(A)包含節點B,則在節點A和節點B之間增加一條直連邊),具體參考有向圖的傳遞閉包概念。
Graph<Integer> graph2 = Graphs.transitiveClosure(graph1);
Log.d(TAG, "transitiveClosure graph2: " + graph2);

輸出:

transitiveClosure graph2: isDirected: true, allowsSelfLoops: true, 
nodes: [2, 4, 3, 1], edges: [<2 -> 4>, <2 -> 3>, <2 -> 2>, <4 -> 4>, 
<3 -> 3>, <1 -> 4>, <1 -> 1>, <1 -> 3>, <1 -> 2>]
  1. 獲取有向圖的的反轉圖:
Graph<Integer> graph3 = Graphs.transpose(graph1);
Log.d(TAG, "transpose graph3: " + graph3);

輸出:

transpose graph3: isDirected: true, allowsSelfLoops: true, 
nodes: [2, 3, 1, 4], edges: [<2 -> 1>, <2 -> 2>, <3 -> 1>, <3 -> 2>, <4 -> 2>]
  1. 圖的遍歷
//深度優先-后序
Iterable<Integer> dfs = Traverser.forGraph(graph1).depthFirstPostOrder(N1); 
Log.d(TAG, "dfs traverser: " + format(dfs));

//深度優先-前序
Iterable<Integer> dfsPre =Traverser.forGraph(graph1).depthFirstPreOrder(N1); 
Log.d(TAG, "dfs pre traverser: " + format(dfsPre));

//廣度優先
Iterable<Integer> bfs =Traverser.forGraph(graph1).breadthFirst(N1);
Log.d(TAG, "bfs traverser: " + format(bfs));

輸出:

dfs traverser: {4,3,2,1} //深度優先-后序
dfs pre traverser: {1,2,4,3} //深度優先-前序
bfs traverser: {1,2,3,4} //廣度優先
  1. 刪除節點(對應的連接邊也將刪除)
    刪除節點,或者刪除邊時,需要將對應的連接關系也要刪除,因此又涉及到了關系結構GraphConnections,此處也重點分析一下:
//ConfigurableMutableValueGraph

  //刪除節點
  public boolean removeNode(N node) {
    checkNotNull(node, "node");

    GraphConnections<N, V> connections = nodeConnections.get(node);
    if (connections == null) { //查看是否有鄰接點
      return false; 
    }

    //先刪除自環(簡單,因為不涉及其他節點)
    if (allowsSelfLoops()) {
      // Remove self-loop (if any) first, so we don't get CME while removing incident edges.
      if (connections.removeSuccessor(node) != null) { //刪除它的后繼,不為null表示存在此環
        connections.removePredecessor(node); //再次刪除其前趨,存放的數據不一樣
        --edgeCount; //邊數-1
      }
    }

    //遍歷其后繼節點列表,并分別刪除它的前趨關系
    for (N successor : connections.successors()) {
      nodeConnections.getWithoutCaching(successor).removePredecessor(node);
      --edgeCount;
    }

    //因為在無向圖中不區分前趨和后繼,因此這里只有是有向圖時才需要刪除后繼關系
    if (isDirected()) { // In undirected graphs, the successor and predecessor sets are equal.
      for (N predecessor : connections.predecessors()) { //類似刪除前趨
        checkState(nodeConnections.getWithoutCaching(predecessor).removeSuccessor(node) != null);
        --edgeCount;
      }
    }
    nodeConnections.remove(node); //連接關系中徹底刪除該節點
    checkNonNegative(edgeCount);
    return true;
  }


  //刪除邊
  public V removeEdge(N nodeU, N nodeV) {
    checkNotNull(nodeU, "nodeU");
    checkNotNull(nodeV, "nodeV");

    //分別獲取節點U和V的連接結構
    GraphConnections<N, V> connectionsU = nodeConnections.get(nodeU);
    GraphConnections<N, V> connectionsV = nodeConnections.get(nodeV);
    if (connectionsU == null || connectionsV == null) { //校驗其連通性,有一個為null,表示結構已不成立
      return null;
    }

    //刪除U的后繼
    V previousValue = connectionsU.removeSuccessor(nodeV);
    if (previousValue != null) { //不為null表示有連接關系
      connectionsV.removePredecessor(nodeU); //則還需要刪除節點V到U的前趨關系
      checkNonNegative(--edgeCount);
    }
    return previousValue;
  }

刪除節點:

graph1.removeNode(N2); //刪除一個節點N2
edges = graph1.edges();
Log.d(TAG, "graph1 remove node of (" + N2 +  ") after graph1 edge 
count:" + edges.size() + ", edges value:" + format(edges));

輸出:

graph1 remove node of (2) after graph1 edge count:1, edges value:{<1 -> 3>}
  1. 構建類Builderfrom()接口只能復制其屬性值,而并不會復制相應的節點和邊:
//build of from()僅僅復制其屬性,節點和邊不會復制過來
MutableGraph<Integer> graph4 = GraphBuilder.from(graph1).build(); 
Set<EndpointPair<Integer>> edges4 = graph4.edges();
Log.d(TAG, "graph4 edge count:" + edges4.size() 
+ ", edges value:" + format(edges4));

輸出:

graph4 edge count:0, edges value:{}

ValueGraph

ValueGraph由于和Graph是一套實現方案,都是實現類ConfigurableMutableValueGraph來操作的,因此這里不再詳細描述其實現。

特性
a)頂點必須唯一,邊可以不唯一; b)支持有向和無向邊; c)邊的定義支持權值指定; d)不支持并行邊.

示例圖如下:

ValueGraph測試用例圖

注:ValueGraph支持Graph的全部功能,因此下面僅介紹其差異功能:

  1. 使用對應構建類ValueGraphBuilder來構建ValueGraph實例:
//節點類型為Integer,邊類型為String
MutableValueGraph<Integer, String> graph1 = ValueGraphBuilder.directed() //有向
    .allowsSelfLoops(true) //允許自環
    .expectedNodeCount(NODE_COUNT) //期望節點數
    .nodeOrder(ElementOrder.<Integer>insertion()) //節點順序
    .build();

Log.d(TAG, "initlized graph1: " + graph1);

輸出:

initlized graph1:isDirected: true,allowsSelfLoops:true,nodes:[], edges:{}
  1. 增加頂點和邊
graph1.putEdgeValue(N3, N1, E31);
graph1.putEdgeValue(N3, N4, E34);
graph1.putEdgeValue(N4, N4, E44);
graph1.putEdgeValue(N1, N1, E11);
graph1.putEdgeValue(N1, N2, E12);
graph1.putEdgeValue(N2, N1, E21);
graph1.putEdgeValue(N1, N3, E13);

Log.d(TAG, "put edges after graph1: " + graph1);

//返回圖中所有的節點(順序依賴nodeOrder)
Set<Integer> nodes = graph1.nodes(); 
Log.d(TAG, "graph1 nodes count:" + nodes.size() + ", nodes value:" 
+ format(nodes));

輸出:

put edges after graph1: isDirected: true, allowsSelfLoops: true, 
nodes: [3, 1, 4, 2], edges: {<3 -> 4>=3-4, <3 -> 1>=3-1, 
<1 -> 1>=1-1, <1 -> 2>=1-2, <1 -> 3>=1-3, <4 -> 4>=4-4, <2 -> 1>=2-1}

//節點順序
graph1 nodes count:4, nodes value:{3,1,4,2}
  1. 獲取兩個節點之間的連接邊:
    其內部實現為:
  public V edgeValueOrDefault(N nodeU, N nodeV, @Nullable V defaultValue) {
    checkNotNull(nodeU);
    checkNotNull(nodeV);
    //得到節點U的連接連接結構
    GraphConnections<N, V> connectionsU = nodeConnections.get(nodeU);
    V value = (connectionsU == null) ? null : connectionsU.value(nodeV); //拿到其連接值
    return value == null ? defaultValue : value;
  }

獲取連接邊:

String edge = graph1.edgeValueOrDefault(N1, N2, "@null");
Log.d(TAG, "graph1 node " + N1 + " & " + N2 + " edge: " + edge);

輸出:

graph1 node 1 & 2 edge: 1-2
  1. asGraph()轉換為Graph視圖
    asGraph()實際上是重新new了一個AbstractGraph,只是它的接口實現是調用了Graph本身的接口,因此如果修改asGraph()返回的視圖的數據,其變化也會反映在Graph本身上,反之亦然。
Graph<Integer> graph5 = graph1.asGraph();
Log.d(TAG, "asGraph:" + graph5);

輸出:

asGraph:isDirected: true, allowsSelfLoops: true, nodes: [3, 1, 4], 
edges: [<3 -> 4>, <3 -> 1>, <1 -> 1>, <1 -> 3>, <4 -> 4>]  //注意此處不包含邊信息

Network

由于NetworkGraph以及ValueGraph有很大的不同性,最大的不同點是Network中允許并行邊(即兩個節點間可以有多條同向邊,如:節點A和節點B可以有兩條同向邊:A->B: a-b-1,a-b-2),這就導致了前面介紹的使用節點作為Mapkey的數據結構GraphConnections的邏輯走不下去了,因為節點不唯一了。使得設計者重新設計了另一套使用邊為Key的機制來實現節點的連接關系。

為什么需要Network加入到Guava中?通過上面描述的不同點可知,Graph(ValueGraph)是通過頂點來定義邊的,即兩個頂點只能有一條(有向)邊連接;但是在某些圖中,在兩個頂點之間需要有多條邊連接的場景出現。比如,兩個機場(對應兩個節點)之間存在多趟固定航班,而這里用ValueGraph是無法描述的。

Network相對于Graphe(ValueGraph),由于功能的差異,其接口也發生了些變化,增加了如下特殊接口:

Set<E> inEdges(N node); //node的入度邊集合(不同于predecessors(),它返回的入度節點非邊)

Set<E> outEdges(N node); //node的出度邊集合(不同于successors(),它返回的是出度節點非邊)

EndpointPair<N> incidentNodes(E edge); //邊對應的兩個端點

Set<E> adjacentEdges(E edge); //邊的鄰接邊

Set<E> edgesConnecting(N nodeU, N nodeV); //兩個節點的連接邊集

其主要區別在于NetworkConnections的實現不同:

取消了前趨和后繼的操作接口,因為操作節點已不能滿足Network的場景:
void addPredecessor(N node, V value);
void removePredecessor(N node);
V addSuccessor(N node, V value);
V removeSuccessor(N node);

增加了對出度邊和入度邊的操作接口:
void addInEdge(E edge, N node, boolean isSelfLoop);
void addOutEdge(E edge, N node);
N removeInEdge(E edge, boolean isSelfLoop);
N removeOutEdge(E edge);

重點查看DirectedMultiNetworkConnections(使用allowsParallelEdges(true)構建Network實例時使用)的源碼,其內部相對于GraphConnections已經將出度邊和入度邊集合分開存放了:

AbstractDirectedNetworkConnections --是NetworkConnections的子類

/** Keys are edges incoming to the origin node, values are the source node. */
protected final Map<E, N> inEdgeMap; //入度邊集合,邊是key

/** Keys are edges outgoing from the origin node, values are the target node. */
protected final Map<E, N> outEdgeMap; //出度邊集合,邊是key

//addInEdge(), addOutEdge, removeInEdge(), removeOutEdge()操作的對象都是上述兩個Map



DirectedMultiNetworkConnections --是AbstractDirectedNetworkConnections的子類

//注意,下面的前趨集合和后繼集合使用了Multiset,因為前趨和后繼允許有并行邊,存在多個相同節點

//內部緩存了節點對應的前趨節點集
private transient Reference<Multiset<N>> predecessorsReference;

//內部緩存了節點對應的后繼節點集
private transient Reference<Multiset<N>> successorsReference;

//這里使用緩存的目的是減少inEdgeMap.values()的遍歷操作,在函數predecessors()中可以直接返回predecessorsReference。
private Multiset<N> predecessorsMultiset() {
    Multiset<N> predecessors = getReference(predecessorsReference);
    if (predecessors == null) {
        //前趨就是節點入度邊Map的value集合,可重復
        predecessors = HashMultiset.create(inEdgeMap.values()); 
        predecessorsReference = new SoftReference<>(predecessors);
    }
    return predecessors;
}
private Multiset<N> successorsMultiset() {
  //...同上
}

特性
a)邊必須唯一(因為兩個相同頂點間可以同時存在多條邊); b)支持有向和無向邊; c)邊的定義支持權值指定; d)支持并行邊.

示例圖如下:


NetWork測試用例圖

注:Network也支持Graph的全部功能,因此下面僅介紹其差異功能:

  1. 使用對應構建類NetworkBuilder來構建Network實例:
MutableNetwork<Integer, String> network1 = NetworkBuilder.directed() //有向網
    .allowsParallelEdges(true) //允許并行邊
    .allowsSelfLoops(true) //允許自環
    .nodeOrder(ElementOrder.<Integer>insertion()) //節點順序
    .edgeOrder(ElementOrder.<String>insertion()) //邊順序
    .expectedNodeCount(NODE_COUNT) //期望節點數
    .expectedEdgeCount(EDGE_COUNT) //期望邊數
    .build();


network1.addEdge(N1, N3, E13);
network1.addEdge(N3, N1, E31);
network1.addEdge(N3, N4, E34);
network1.addEdge(N4, N4, E44);
network1.addEdge(N1, N1, E11);
network1.addEdge(N1, N1, E11_A);
network1.addEdge(N1, N2, E12);
network1.addEdge(N1, N2, E12_A);
network1.addEdge(N1, N2, E12_B);
network1.addEdge(N2, N1, E21);

Log.d(TAG, "add edges after network1: " + network1);

輸出:

add edges after network1: isDirected: true, allowsParallelEdges: true, 
allowsSelfLoops: true, nodes: [1, 3, 4, 2], edges: {1-3=<1 -> 3>, 
3-1=<3 -> 1>, 3-4=<3 -> 4>, 4-4=<4 -> 4>, 1-1=<1 -> 1>, 
1-1a=<1 -> 1>, 1-2=<1 -> 2>, 1-2a=<1 -> 2>, 1-2b=<1 -> 2>, 2-1=<2 -> 1>}

注:NetworkGraph不同的是,它可以通過一條邊來定義兩個頂點,如輸出所示。

  1. 獲取邊的鄰接邊(即邊對應兩個端點的鄰接邊集合)
Set<String> adjacentEdges = network1.adjacentEdges(E12_A);
Log.d(TAG, "network1 edge: " + E12_A + ", adjacentEdges: " 
+ format(adjacentEdges));

輸出:

network1 edge: 1-2a, adjacentEdges: {1-1,1-1a,2-1,3-1,1-2b,1-2,1-3,4-2}
  1. 獲取兩個頂點之間的邊集(network中由于頂點間存在平行邊,因此兩個頂點之間存在多條邊):
Set<String> networkEdges = network1.edgesConnecting(N1, N2);
Log.d(TAG, "network1 node " + N1 + " & " + N2 + " edges: " 
+ format(networkEdges));

輸出:

network1 node 1 & 2 edges: {1-2a,1-2b,1-2} //存在3條邊
  1. 返回兩個頂點之間的一條邊(如果該兩個頂點間存在多條邊則會拋出異常):
String edge = network1.edgeConnectingOrNull(N1, N3);//如果是N1和N2則會拋異常
Log.d(TAG, "network1 node " + N1 + " & " + N3 + " edge: " + edge);

輸出:

network1 node 1 & 3 edge: 1-3
  1. 獲取節點的鄰接邊(所有包含該節點的邊集合)
Set<String> incidentEdges = network1.incidentEdges(N1);
Log.d(TAG, "network1 node " + N1 + " incidents: " + format(incidentEdges));

輸出:

network1 node 1 incidents: {1-1,1-1a,2-1,3-1,1-2a,1-2b,1-2,1-3}
  1. 獲取邊的鄰接點(邊對應的兩個頂點)
EndpointPair<Integer> incidentNodes =  network1.incidentNodes(E12_A);
Log.d(TAG, "network1 edge " + E12_A + " incidentNodes: " + incidentNodes);

輸出:

network1 edge 1-2a incidentNodes: <1 -> 2>

示例代碼中組裝Iterable的元素信息的函數如下:

    private static <T> String format(Iterable<T> iterable) {
        StringBuilder builder = new StringBuilder();
        builder.append("{");
        for (T obj : iterable) {
            builder.append(obj).append(",");
        }
        if (builder.length()  > 1) {
            builder.deleteCharAt(builder.length() - 1);
        }
        builder.append("}");
        return builder.toString();
    }

最后

Guava中的Graph的基本API用法及原理到這里基本介紹完了。后續,我們將使用上面的這些基本接口來實現《數據結構》中關于圖的具體算法應用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容