開啓圖結構:圖的創建和遍歷

資源和文章同步更新至微信公衆號:算法工程師之路

今天我們來聊一聊圖結構,雖然在面試中圖結構用的不多,但是我真的覺得圖結構可以綜合很多知識點,以及STL中容器的使用,並且需要很強大的邏輯性!是一個鍛鍊腦子的東西,並且Coding起來非常之爽~~

圖的建立

在這裏插入圖片描述
我們使用算法來模擬圖結構之前,需要首先搞清楚圖結構都需要什麼元素!一般來說我們將一張圖定義爲G=(V, E),其中集合V表示頂點(nodes),而集合E表示某一對頂點之間的關係,叫做邊,如果這種關係是單向的,那麼形成的圖爲有向圖,反之如果是雙向的,那麼形成的圖就是無向圖,如果一個圖中頂點的關係既有單向,又存在雙向,那麼叫做混合圖!

頂點類

對於一個頂點而言,我們需要定義什麼呢?主要有以下幾個屬性:

  • 頂點的值value
  • 頂點的入度in(也就是指向該頂點的邊數)
  • 頂點的出度out(也就是從該頂點出發的邊數)
  • to節點的集合nexts(有向圖時,指向的節點爲to節點,當前節點爲from節點)
  • 從該節點出發邊的集合edges

然後頂點的類定義如下:
使用list的原因是因爲list相比vector在中間操作數據更加快速!

class Node{
public:
    int value;
    int in;
    int out;
    list<Node*> nexts;   // 當前節點爲from,to的節點的集合
    list<Edge*> edges;

    Node(int value){
        this->value = value;
        in = 0;
        out = 0;
    }
};

邊類

對於邊的定義就很簡單了,確定一個邊只要知道其從哪個頂點來,到那個頂點去就好了,還有如果是帶權圖,每個邊都有一個權重屬性!因此對於邊類來說,其屬性很簡單,如下:

  • 權重weight
  • from節點
  • to節點

邊類的定義如下:

class Edge{
public:
    int weight;
    Node* from;
    Node* to;

    Edge(int weight, Node* from, Node* to){
        this->weight = weight;
        this->from = from;
        this->to = to;
    }
};

圖類

由於上面也說了,一張圖其實質就是一個點的集合+一個邊的集合,並且這些元素都是無序的,因此爲了更加便捷的訪問,所以我們在這裏都是用基於哈希函數的無序容器結構來儲存!
注意:如果使用自定義類型,需要重寫哈希函數,請參考原來的文章:
如何使用哈希容器來操作自定義類型
圖類的定義如下:

class Graph{
public:
    unordered_map<int, Node*> nodes;
    unordered_set<Edge*> edges;
};

當我們準備好了這些類之後,我們就可以建立整個圖了,我們使用鄰接矩陣的形式,只需要輸入一個邊的權重、from節點的值和to節點的值就可以創建兩個節點和一條邊,然後添加入整個圖中!

建立過程中,一定要注意,當加入一條邊時,我們一定要將from節點的出度+1,to節點的入度+1,然後將to節點添加到from節點的nexts中,並將這個邊添加到from節點的邊集edges。

class GraphGenerator{
// 三列分別是權重,from, to
public:
    Graph createGraph(vector<vector<int>> matrix, int rows, int cols){
        Graph graph;
        for(int i=0;i < rows;i ++){
            int weight = matrix[i][0];
            int from = matrix[i][1];
            int to = matrix[i][2];
            if(graph.nodes.find(from) == graph.nodes.end()){
                graph.nodes[from] = new Node(from);
            }
            if(graph.nodes.find(to) == graph.nodes.end()){
                graph.nodes[to] = new Node(to);
            }
            Node* fromNode = graph.nodes.find(from)->second;
            Node* toNode = graph.nodes.find(to)->second;

            Edge* newEdge = new Edge(weight, fromNode, toNode);
            // 新增一條邊,則to節點的入度增加
            toNode->in++;
            // from節點的出度增加
            fromNode->out++;
            fromNode->nexts.push_back(toNode);
            fromNode->edges.push_back(newEdge);
            graph.edges.insert(newEdge);
        }
        return graph;
    }
};

那麼我們如何創建一個有向圖和無向圖呢?由於我們的edge是有指向的,從from節點到to節點,假設有向圖的邊爲1->3,那麼我們可以用有向圖的方式創建無向圖,只不過多了一個描述,則爲1->3, 3->1。例如下面這個無向圖,我們可以這樣創建:
在這裏插入圖片描述

vector<vector<int>> matrix = {{6, 1, 2}, {5, 1, 4}, {2, 6, 4},
                                {6, 6, 5}, {3, 2, 5}, {1, 3, 1},
                                {5, 3, 2}, {6, 3, 5}, {4, 3, 6}, {5, 3, 4},  
                                // 只到這裏爲一個有向圖,from和to再反過來一遍就是無向的了
                                {6, 2, 1}, {5, 4, 1}, {2, 4, 6},
                                {6, 5, 6}, {3, 5, 2}, {1, 1, 3},
                                {5, 2, 3}, {6, 5, 3}, {4, 6, 3}, {5, 4, 3}};

BFS(廣度優先遍歷)

廣度優先遍歷算法,從一個節點開始,優先打印其所有的下一節點,然後再打印其下一節點的下一節點,這時候就需要我們標記這個節點是否被打印過,避免重複打印!因此我們使用unordered_set用來儲存訪問過的節點,並使用隊列結構來儲存將要打印的節點,接着在打印一個節點的同時要把其所有下一節點且未訪問過的壓入隊列中!

// 廣度優先遍歷圖節點
void bfs(Node* node){
    if(node == nullptr){
        return;
    }
    queue<Node*> que;
    unordered_set<Node*> set;   // 用來標示是否訪問過
    Node* help;
    que.push(node);
    set.insert(node);
    while(!que.empty()){
        help = que.front();
        que.pop();
        cout << help->value << " ";
        // 使用出隊的當前節點來找
        for(auto node: help->nexts){
            if(set.find(node) == set.end()){
                que.push(node);
                set.insert(node);
            }
        }
    }
    cout << endl;
}

DFS(深度優先遍歷)

深度優先遍歷算法同樣也需要一個容器用來標記是否被訪問過,但是與BFS不同的是其使用的是棧結構,原因是對於DFS來說是從一個點一直遍歷到最後節點,然後還要返回到上一節點判斷,如果其nexts中的節點都標記訪問過了,那麼就再向上回溯,如果有沒有訪問過的節點,那麼就訪問,一直重複這個過程!而棧結構可以維護我們的訪問節點順序,便於回溯!

// 深度優先遍歷圖節點
void dfs(Node* node){
    stack<Node*> sta;
    unordered_set<Node*> set;   // 用來標示是否被訪問過
    Node* help;   // 輔助節點
    // 入棧的同時並打印信息
    sta.push(node);
    set.insert(node);
    cout << node->value << " ";
    while(!sta.empty()){
        help = sta.top();
        sta.pop();
        // 對出棧的元素判斷,如果被打印過,則不會被壓棧,說明訪問過了
        for(auto node: help->nexts){
            if(set.find(node) == set.end()){
                cout << node->value << " ";
                sta.push(help);  // 將訪問的節點入棧,由於當前節點可能還有的分支沒有訪問
                sta.push(node);   
                set.insert(node);
                break;  // 每次只訪問某個節點的一個分支,一直深入下去訪問
            }
        }
    }
    cout << endl;
}

資源分享

以上完整代碼文件(C++版),文件名爲:圖的創建和遍歷,請關注我的個人公衆號 (算法工程師之路),回覆"左神算法基礎CPP"即可獲得,並實時更新!希望大家多多支持哦~

公衆號簡介:分享算法工程師必備技能,談談那些有深度有意思的算法,主要範圍:C++數據結構與算法/深度學習(CV),立志成爲Offer收割機!堅持分享算法題目和解題思路(Day By Day)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章