圖 - Java 實現

圖的鄰接鏈表實現

public class LinkedGraph<T> {
    // 頂點的訪問狀態
    private static final int UNDISCOVERED = 0;
    private static final int DISCOVERED = 1;
    private static final int VISITED = 2;

    // 頂點類
    private static class Vertex<T> {
        T data;
        int inDegree, outDegree;    // 入度、出度
        int status;                 // 訪問狀態
        
        // 求最短路徑時有用
        int distance;               // 從某個起點到當前頂點的最短路徑的距離
        							// 或在最小生成樹中,當前頂點到父頂點的最小距離
        							
        int from;                   // 最短路徑上,通往當前頂點的前一個頂點
        							// 或在最小生成樹中,當前頂點的父頂點

        Vertex(T data) {
            this.data = data;
            from = -1;
        }
    }

    // 鏈表節點類
    private static class LinkedNode {
        int index;          // 鄰居頂點的索引
        int weight;         // 邊的權重

        LinkedNode(int index, int weight) {
            this.index = index;
            this.weight = weight;
        }
    }

    // 鏈表頭部
    private static class LinkedHead<T> {
        Vertex<T> vertex;               // 頂點
        List<LinkedNode> neighbors;     // 頂點的鄰居

        LinkedHead(Vertex<T> vertex) {
            this.vertex = vertex;
            neighbors = new LinkedList<>();
        }
    }

    private List<LinkedHead<T>> heads;  // 所有鏈表的頭

    public LinkedGraph() {
        heads = new ArrayList<>();
    }


    // 插入頂點
    public void insertVertex(T data) {
        Vertex<T> vertex = new Vertex<>(data);
        LinkedHead<T> head = new LinkedHead<>(vertex);
        heads.add(head);
    }

    // 刪除頂點
    public void removeVertex(int index) {
        // 更新頂點的度-所有出邊
        List<LinkedNode> neighbors = getNeighbors(index);
        for (int i = 0; i < neighbors.size(); i++) {
            getVertex(neighbors.get(i).index).inDegree--;
        }

        heads.remove(index);

        // 更新頂點的度-所有入邊
        for (int i = 0; i < heads.size(); i++) {
            neighbors = getNeighbors(i);
            Iterator<LinkedNode> iterator = neighbors.iterator();
            while (iterator.hasNext()) {
                LinkedNode node = iterator.next();
                if (node.index == index) {
                    iterator.remove();
                    getVertex(i).outDegree--;
                    break;
                }
            }
        }
    }

    // 插入邊
    public void insertEdge(int from, int to, int weight) {
        LinkedNode node = new LinkedNode(to, weight);
        getNeighbors(from).add(node);

        // 更新頂點的度
        getVertex(from).outDegree++;
        getVertex(to).inDegree++;
    }

	// 是否存在指定的邊
    public boolean exists(int from, int to) {
        List<LinkedNode> neighbors = getNeighbors(from);
        for (int i = 0; i < neighbors.size(); i++) {
            if (neighbors.get(i).index == to) {
                return true;
            }
        }
        return false;
    }
    
    // 刪除邊
    public void removeEdge(int from, int to) {
        if (!exists(from, to)) {
            return;
        }

        // 更新頂點的度
        getVertex(from).outDegree--;
        getVertex(to).inDegree--;

        // 刪除鄰居
        List<LinkedNode> neighbors = getNeighbors(from);
        Iterator<LinkedNode> iterator = neighbors.iterator();
        while (iterator.hasNext()) {
            LinkedNode node = iterator.next();
            if (node.index == to) {
                iterator.remove();
                break;
            }
        }
    }
    
    // 獲取頂點 vertex 的鄰居
    private List<LinkedNode> getNeighbors(int vertex) {
        return heads.get(vertex).neighbors;
    }

    // 獲取 index 處的頂點
    private Vertex<T> getVertex(int index) {
        return heads.get(index).vertex;
    }

    ...
}

廣度優先搜索

始自圖中頂點 s 的BFS搜索,將首先訪問頂點 s;再依次訪問 s 所有尚未被訪問到的鄰居;再按後者被訪問的先後次序,逐個訪問它們的鄰居; …;如此不斷。

BFS搜索總體僅需 O(n + e) 時間。(n 爲頂點數,e 爲邊數)

// 廣度優先搜索
public void breadthFirstSearch(int startVertex) {
    reset();		// 重置頂點的訪問狀態
    int vertex = startVertex;
    int n = heads.size();

    do {
        if (getVertex(vertex).status == UNDISCOVERED) {
            breadthFirstSearchHelper(vertex);
        }
        vertex = ++vertex % n;			// 確保所有頂點都被訪問過
    } while (vertex != startVertex);
}

// 重置頂點的訪問狀態
private void reset() {
    for (int i = 0; i < heads.size(); i++) {
        heads.get(i).vertex.status = UNDISCOVERED;
    }
}

// startVertex:搜索過程的起始頂點
private void breadthFirstSearchHelper(int startVertex) {
    Queue<Integer> queue = new ArrayDeque<>();
    
    // 起點入隊列
    getVertex(startVertex).status = DISCOVERED;
    queue.offer(startVertex);

    while (!queue.isEmpty()) {
        int vertex = queue.poll();

        // 將未被發現的鄰居入隊列
        List<LinkedNode> neighbors = getNeighbors(vertex);
        for (int i = 0; i < neighbors.size(); i++) {
            int neighbor = neighbors.get(i).index;
            if (getVertex(neighbor).status == UNDISCOVERED) {
                getVertex(neighbor).status = DISCOVERED;
                queue.offer(neighbor);
            }
        }

        // 對頂點執行操作...
        System.out.println(getVertex(vertex).data);

        // 當前節點訪問完畢
        getVertex(vertex).status = VISITED;
    }
}

深度優先搜索

以頂點 s 爲基點的DFS搜索,將首先訪問頂點 s;再從 s 所有尚未訪問到的鄰居中任取其一, 並以之爲基點, 遞歸地執行DFS搜索。如果 s 所有的鄰居都已經被訪問過了,則回溯至 s 的上一個頂點,然後繼續 DFS 搜索。

深度優先搜索算法也可在 O(n + e) 時間內完成。

// 深度優先搜索
public void depthFirstSearch(int startVertex) {
    reset();		// 重置頂點的訪問狀態
    int vertex = startVertex;
    int n = heads.size();

    do {
        if (getVertex(vertex).status == UNDISCOVERED) {
            depthFirstSearchHelper(vertex);
        }
        vertex = ++vertex % n;			// 確保所有頂點都被訪問過
    } while (vertex != startVertex);
}

private void depthFirstSearchHelper(int startVertex) {
    ArrayDeque<Integer> stack = new ArrayDeque<>();
    int vertex = startVertex;
    boolean moved = true;           // 是否移動過

    do {
        // 如果沒有向前移動,則說明無頂點可繼續深入,此時應該回溯
        if (!moved) {
        	getVertex(vertex).status = VISITED;
        	
            // 如果棧不爲空,則回溯上一個頂點
            if (!stack.isEmpty()) {
                vertex = stack.pop();
            } else {
                break;
            }
        }
        // 已向前移動,當前頂點是一個未被訪問的新頂點,則訪問之
        else {
            getVertex(vertex).status = DISCOVERED;

            // 對頂點操作...
            System.out.println(getVertex(vertex).data);

            // 在這一輪中,當前還沒有向前移動
            moved = false;
        }

        // 有未被訪問的鄰居,則將當前頂點入棧,然後深入下一個頂點
        List<LinkedNode> neighbors = getNeighbors(vertex);
        for (int i = 0; i < neighbors.size(); i++) {
            int neighbor = neighbors.get(i).index;
            if (getVertex(neighbor).status == UNDISCOVERED) {
                stack.push(vertex);
                vertex = neighbor;
                moved = true;
                break;
            }
        }
    } while (true);
}

拓撲排序

有向無環圖的拓撲排序必然存在;反之亦然。

對於有向無環圖 G,只要將入度爲 0 的頂點 m(及其關聯邊) 從圖 G 中取出, 則剩餘的 G’ 依然是有向無環圖,故其拓撲排序也必然存在。 從遞歸的角度看, 一旦得到了 G’ 的拓撲排序,只需將 m 作爲最大頂點插入,即可得到 G 的拓撲排序。

拓撲排序算法也可在 O(n + e) 時間內完成。

// 拓撲排序
public void topologicalSort() {
    List<Integer> vertices = topologicalSortHelper();
    
    // 可以進行拓撲排序
    if (vertices != null) {
        for (int i = 0; i < vertices.size(); i++) {
            // 對頂點執行操作 ...
            System.out.println(getVertex(vertices.get(i)).data);
        }
    }
    // 非 DAG,不可進行拓撲排序
    else {
        System.out.println("NOT DAG!!");
    }
}

private List<Integer> topologicalSortHelper() {
    resetDegree();		// 重置頂點的度
    Queue<Integer> queue = new ArrayDeque<>();
    List<Integer> list = new ArrayList<>(heads.size());		// 保存排序結果

    // 入度爲 0 的頂點入隊列
    for (int i = 0; i < heads.size(); i++) {
        if (heads.get(i).vertex.inDegree == 0) {
            queue.offer(i);
        }
    }

    while (!queue.isEmpty()) {
    	// 從圖中移除入度爲 0 的頂點
        int vertex = queue.poll();
        list.add(vertex);

        // 更新鄰居的入度,入度爲 0 的頂點入隊列
        List<LinkedNode> neighbors = getNeighbors(vertex);
        for (int i = 0; i < neighbors.size(); i++) {
            int neighbor = neighbors.get(i).index;
            if (--getVertex(neighbor).inDegree == 0) {
                queue.offer(neighbor);
            }
        }
    }

    if (list.size() == heads.size()) {
        return list;
    }
    return null;        // 非 DAG
}

// 重新設置頂點的度
private void resetDegree() {
    for (int i = 0; i < heads.size(); i++) {
        List<LinkedNode> neighbors = getNeighbors(i);

        // 出度
        getVertex(i).outDegree = neighbors.size();

        // 入度
        for (int j = 0; j < neighbors.size(); j++) {
            getVertex(neighbors.get(j).index).inDegree++;
        }
    }
}

單源最短路徑:Dijkstra

以頂點 s 爲起點,設 dvd_v 爲頂點 v 到頂點 s 的最短距離。

  1. 初始時。每個頂點都還是處於未被發現的狀態,且它們到 s 的距離都爲 \infty ,除了 s 到 s 的距離爲 0。

  2. Dijkstra 算法從所有未被發現的頂點中選擇一個頂點 v,該頂點在所有未被發現的頂點中具有最小的 dvd_v ,同時將該頂點標記爲已發現的。

  3. 對頂點 v 的所有鄰居頂點 w,將 dwd_w 設爲 min{dw,dv+cv,w}\min\{d_w, d_v + c_{v,w}\} ,其中 cv,wc_{v,w} 表示 v 到 w 的邊的權重。

  4. 繼續步驟 2,直到所有頂點都已被發現。

時間複雜度爲 O(n2)O(n^2)

// 最短路徑:從 from 到 to 頂點
public ArrayDeque<Integer> shortestPath(int from, int to) {
    dijkstra(from);
    
    // 保存最短的路徑:逆序保存
    ArrayDeque<Integer> stack = new ArrayDeque<>();

    int current = to;

    while (current != -1) {
        stack.push(current);
        current = getVertex(current).from;
    }

    return stack;
}

// Dijkstra 算法
private void dijkstra(int startVertex) {
    // 初始化
    for (int i = 0; i < heads.size(); i++) {
        Vertex<T> vertex = getVertex(i);
        vertex.distance = Integer.MAX_VALUE;
        vertex.status = UNDISCOVERED;
    }

    Vertex<T> v = getVertex(startVertex);
    v.distance = 0;
    int undiscoverd = heads.size();		// 未被發現的頂點的數量

    while (undiscoverd > 0) {
        // 找到距離最短的、還未被發現的頂點 v
        int smallestIndex = -1;
        int smallestDistance = Integer.MAX_VALUE;
        for (int i = 0; i < heads.size(); i++) {
            Vertex<T> vertex = getVertex(i);
            if (vertex.status == UNDISCOVERED && vertex.distance < smallestDistance) {
                smallestDistance = vertex.distance;
                smallestIndex = i;
            }
        }

        // 發現之
        v = getVertex(smallestIndex);
        v.status = DISCOVERED;
        undiscoverd--;

        // 更新頂點 v 的鄰居的距離
        List<LinkedNode> neighbors = getNeighbors(smallestIndex);
        for (int i = 0; i < neighbors.size(); i++) {
            LinkedNode neighbor = neighbors.get(i);
            Vertex<T> w = getVertex(neighbor.index);
            if (w.status == UNDISCOVERED) {
                int costOfv2w = neighbor.weight;
                if (v.distance + costOfv2w < w.distance) {
                    w.distance = v.distance + costOfv2w;
                    w.from = smallestIndex;
                }
            }
        }
    }
}

最小生成樹:Prim

連通圖 G 的某一無環連通子圖 T 若覆蓋 G 中所有的頂點,則稱作 G 的一棵支撐樹或生成樹(spanning tree) 。

Prim 算法的大致過程是,每次選擇一條邊 (u, v),其中,頂點 u 在樹上,頂點 v 不在樹上,且 (u, v) 邊的權在所有候選者中是最小的。然後,將 (u, v) 添加到樹上。

以頂點 s 爲起點,設 dvd_v 爲頂點 v 連接到已知頂點(已在樹上)的最短邊的權重。

  1. 初始時。每個頂點都還是處於未被發現的狀態(即不在樹上),且它們到已知頂點的距離都爲 \infty ,除了 s 到 s 的距離爲 0。

  2. Prim 算法從所有未被發現的頂點中選擇一個頂點 v,該頂點在所有未被發現的頂點中具有最小的 dvd_v ,同時將該頂點標記爲已發現的。

  3. 對頂點 v 的所有鄰居頂點 w,將 dwd_w 設爲 min{dw,cv,w}\min\{d_w, c_{v,w}\} ,其中 cv,wc_{v,w} 表示 v 到 w 的邊的權重。

  4. 繼續步驟 2,直到所有頂點都已被發現。

時間複雜度爲 O(n2)O(n^2)

public void minimumSpanningTreeByPrim(int startVertex) {
    prim(startVertex);

	// 打印每個頂點的父頂點
    for (int i = 0; i < heads.size(); i++) {
    	Vertex<T> vertex = getVertex(i);
        System.out.println(vertex.data + " parent: " + getVertex(vertex.from).data);
    }
}

// Prim:和 Dijkstra 的代碼非常相似,只是 distance  表示的意思不同,且在更新 distance 部分不同
private void prim(int startVertex) {
    // 初始化
    for (int i = 0; i < heads.size(); i++) {
        Vertex<T> vertex = getVertex(i);
        vertex.distance = Integer.MAX_VALUE;
        vertex.status = UNDISCOVERED;
    }

    Vertex<T> v = getVertex(startVertex);
    v.distance = 0;
    int undiscoverd = heads.size();

    while (undiscoverd > 0) {

        // 找到邊最短的、還未被發現的頂點 v
        int smallestIndex = -1;
        int smallestDistance = Integer.MAX_VALUE;
        for (int i = 0; i < heads.size(); i++) {
            Vertex<T> vertex = getVertex(i);
            if (vertex.status == UNDISCOVERED && vertex.distance < smallestDistance) {
                smallestDistance = vertex.distance;
                smallestIndex = i;
            }
        }

        // 發現之
        v = getVertex(smallestIndex);
        v.status = DISCOVERED;
        undiscoverd--;

        // 更新頂點 v 的鄰居的距離
        List<LinkedNode> neighbors = getNeighbors(smallestIndex);
        for (int i = 0; i < neighbors.size(); i++) {
            LinkedNode neighbor = neighbors.get(i);
            Vertex<T> w = getVertex(neighbor.index);
            if (w.status == UNDISCOVERED) {
                int costOfv2w = neighbor.weight;
                if (costOfv2w < w.distance) {
                    w.distance = costOfv2w;
                    w.from = smallestIndex;
                }
            }
        }
    }
}

最小生成樹:Kruskal

Kruskal 算法的大致過程是:每次選擇具有最小權重的邊,並且當所選的邊不產生環時,纔將其添加到樹上。

具體而言,就是對於候選邊 (u, v),如果 u、v 在同一個集合中,則放棄該邊(因爲兩者已經連通,再添加該邊將產生環)。如果 u、v 不在同一集合中,則接受該邊,併合並 u、v 所在的兩個集合。

時間複雜度爲 O(eloge)O(e\log e)

// 表示圖的邊(作爲內部類)
private static class Edge implements Comparable<Edge> {
    int u, v;       // 邊 (u, v)
    int weight;     // 邊的權重

    Edge(int u, int v, int weight) {
        this.u = u;
        this.v = v;
        this.weight = weight;
    }

	// 比較邊時有用
    public int compareTo(Edge other) {
        return weight - other.weight;
    }
}
public void minimumSpanningTreeByKruskal() {
    // 獲取所有的邊 (u, v)
    List<Edge> edges = new ArrayList<>();

    for (int u = 0; u < heads.size(); u++) {
        List<LinkedNode> neighbors = getNeighbors(u);
        for (int i = 0; i < neighbors.size(); i++) {
            int v = neighbors.get(i).index;
            int weight = neighbors.get(i).weight;
            edges.add(new Edge(u, v, weight));
        }
    }

    List<Edge> tree = kruskal(edges);

    for (int i = 0; i < tree.size(); i++) {
        System.out.println(getVertex(tree.get(i).u).data + " - " + getVertex(tree.get(i).v).data);
    }
}
// 不相交集合
// union-find 算法
private static class DisjointSet {
	// 當 sets[i] >= 0 時,即表示 i 的父親
	// 當 sets[i] < 0 時,即表示 i 爲根,且該樹高度爲 -sets[i]
    private int[] sets;
    
	// 初始時有 numElements 個集合
    DisjointSet(int numElements) {
        sets = new int[numElements];
        for (int i = 0; i < numElements; i++) {
            sets[i] = -1;		// 樹高爲 1
        }
    }

    // 合併兩個不相交集合
    // root1 和 root2 分別是兩個集合所形成的樹的根
    // -sets[root1] 表示 root1 集合所形成的樹的高度
    // 誰高誰就是新根,同高則增加合併後的樹的高
    void union(int root1, int root2) {
        if (sets[root2] < sets[root1]) {		// root2 高
            sets[root1] = root2;				// 以 root2 爲根
        } else {
            if (sets[root1] == sets[root2]) {	// 同高
                sets[root1]--;					// 增加樹高
            }
            sets[root2] = root1;
        }
    }

    // 找到 x 所在集合的根
    int find(int x) {
        // 向上追尋
        while (sets[x] >= 0) {
            x = sets[x];
        }
        return x;
    }
}

在這裏插入圖片描述

// Kruskal 算法
private List<Edge> kruskal(List<Edge> edges) {
    int numVertices = heads.size();
    DisjointSet disjSet = new DisjointSet(numVertices);		// 初始化 numVertices 個不相交集合,一個頂點即爲一個集合
    PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(edges);		// 將所有邊放到堆中
    List<Edge> mininumSpanningTree = new ArrayList<>();		// 保存結果

    while (mininumSpanningTree.size() != numVertices - 1) {
        Edge edge = priorityQueue.poll();	// 獲取、移除最小的邊
        int rootu = disjSet.find(edge.u);	// 找到集合的根
        int rootv = disjSet.find(edge.v);
        if (rootu != rootv) {				// u、v 不屬於同一個集合
            mininumSpanningTree.add(edge);	// 接受該邊,成爲生成樹的一條邊
            disjSet.union(rootu, rootv);	// 合併兩個集合
        }
    }

    return mininumSpanningTree;
}
發佈了118 篇原創文章 · 獲贊 18 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章