資料結構 - 圖之程式碼實現

IT规划师發表於2024-11-04

書接上回,我們繼續來聊聊圖的遍歷與實現。

01、遍歷

在圖的基本功能中有個很重要的功能——遍歷,遍歷顧名思義就是把圖上所有的點都訪問一遍,具體來說就是從一個連通的圖中某一個點出發,沿著邊訪問所有的點,並且每個點只能訪問一遍。

下面我們介紹兩種常見的遍歷方式:深度優先遍歷(DFS)和廣度優先遍歷(BFS)。

1、深度優先遍歷

如果我們把邊當作路,深度優先遍歷就是路一直往下走,直到沒路了再返回走其他路。其實優點像樹的先序遍歷從根節點沿著子節點一直向下直到葉子節點再調頭。

下面我們梳理一下深度優先遍歷大致分為以下幾個步驟:

(1)從圖中任意一個點A出發,並訪問點;

(2)找出點A的第一個未被訪問的鄰接點,並訪問該點;

(3)以該點為新的點,重複步驟(2),直至新的鄰接點沒有未被訪問的鄰接點;

(4)返回前一個點並依次訪問前一個點為未被訪問的其他鄰接點,並訪問該點;

(5)重複步驟(3)和(4),直至所有點都被訪問過;

如上圖演示了從點A出發進行深度優先遍歷過程,其中紅色虛線表示前進路線,藍色虛線表示回退路線。最後輸出:A->B->E->F->C->G->D。

2、廣度優先遍歷

如果說深度優先遍歷是找到一條路一直走到底,那麼廣度優先遍歷就是先把所有的路走一步再說。其實優點像樹的層次遍歷從根節點出發先遍歷其子節點然後再遍歷其孫子節點直至遍歷完所有節點。

下面我們梳理一下廣度優先遍歷大致分為以下幾個步驟:

(1)從圖中任意一點A出發,並訪問點A;

(2)依次訪問點A所有未被訪問的鄰接點,訪問完鄰接點後,然後按鄰接點順序把鄰接點作為新的出發執行步驟(1);

(3)重複步驟(1)和(2)直至所有點都被訪問到。

如上圖演示了從點A出發進行廣度優先遍歷過程,其中紅色虛線表示前進路線。最後輸出:A->B->C->D->E->F->G。

02、實現(鄰接矩陣)

下面我們就以鄰接矩陣的儲存方式實現一個無向圖。

1、定義

根據圖的定義,我們需要定義點集合、邊集合兩個私有變數用於儲存核心資料,為了操作訪問我們再定義點數量和邊數量兩個私有變數,程式碼如下:

//點集合
private T[] _vertexArray { get; set; }
//邊集合
private int[,] _edgeArray { get; set; }
//點數量
private int _vertexCount;
//邊數量
private int _edgeCount { get; set; }

2、初始化 Init

此方法主要是初始化上面定義的私有變數,同時確定點集合大小,具體程式碼如下:

//初始化
public MyselfGraphArray<T> Init(int length)
{
    //初始化指定長度點集合
    _vertexArray = new T[length];
    //初始化指定長度邊集合
    _edgeArray = new int[length, length];
    //初始化點數量
    _vertexCount = 0;
    //初始化邊數量
    _edgeCount = 0;
    return this;
}

3、獲取點數量 VertexCount

我們可以透過點數量私有變數快速獲取圖的點數量,程式碼如下:

//返回點數量
public int VertexCount
{
    get
    {
        return _vertexCount;
    }
}

4、獲取邊數量 EdgeCount

我們可以透過邊數量私有變數快速獲取圖的點數量,程式碼如下:

//返回邊數量
public int EdgeCount
{
    get
    {
        return _edgeCount;
    }
}

5、獲取點索引 GetVertexIndex

該方法是透過點元素獲取其索引值,具體程式碼如下:

//返回指定點元素的索引   
public int GetVertexIndex(T vertex)
{
    if (vertex == null)
    {
        return -1;
    }
    //根據值查詢索引
    return Array.IndexOf(_vertexArray, vertex);
}

6、獲取點元素 GetVertexByIndex

該方法透過點索引獲取點元素,具體程式碼如下:

//返回指定點索引的元素
public T GetVertexByIndex(int index)
{
    //如果索引非法則報錯
    if (index < 0 || index > _vertexArray.Length - 1)
    {
        throw new InvalidOperationException("索引錯誤");
    }
    return _vertexArray[index];
}

7、插入點 InsertVertex

插入點元素時,我們需要先透過點元素獲取其索引,如果索引已存在或者點集合已經滿了則直接返回,否則新增點元素同時更新點數量,具體程式碼如下:

//插入點
public void InsertVertex(T vertex)
{
    //獲取點索引
    var index = GetVertexIndex(vertex);
    //如果索引大於-1說明點已存在,則直接返回
    if (index > -1)
    {
        return;
    }
    //如果點集合已滿,則直接返回
    if (_vertexCount == _vertexArray.Length)
    {
        return;
    }
    //新增點元素,並且更新點數量
    _vertexArray[_vertexCount++] = vertex;
}

8、插入邊 InsertEdge

插入邊時可以同時指定邊的權值。我們首先需要把兩個點元素轉換為點索引,同時驗證索引,驗證不透過則直接返回。否則開始新增邊,因為無向圖的特性,所以需要新增兩點索引相反的邊。同時更新邊數量,具體程式碼如下:

//插入邊
public void InsertEdge(T vertex1, T vertex2, int weight)
{
    //根據點元素獲取點索引
    var vertexIndex1 = GetVertexIndex(vertex1);
    //如果索引等於-1說明點不存在,則直接返回
    if (vertexIndex1 == -1)
    {
        return;
    }
    //根據點元素獲取點索引
    var vertexIndex2 = GetVertexIndex(vertex2);
    //如果索引等於-1說明點不存在,則直接返回
    if (vertexIndex2 == -1)
    {
        return;
    }
    //更新兩點關係,即邊資訊
    _edgeArray[vertexIndex1, vertexIndex2] = weight;
    //用於無向圖,對於有向圖則刪除此句子
    _edgeArray[vertexIndex2, vertexIndex1] = weight;
    //更新邊數量
    _edgeCount++;
}

9、獲取邊權值 GetWeight

該方法可以獲取邊的權值,權值可以根據需要在插入邊方法中設定,需要對輸入的點進行驗證,如果點不存在則報錯,具體程式碼如下:

//返回兩點之間邊的權值
public int GetWeight(T vertex1, T vertex2)
{
    //根據點元素獲取點索引
    var vertexIndex1 = GetVertexIndex(vertex1);
    //如果索引等於-1說明點不存在
    if (vertexIndex1 == -1)
    {
        //如果未找到點則報錯
        throw new KeyNotFoundException($"點不存在");
    }
    //根據點元素獲取點索引
    var vertexIndex2 = GetVertexIndex(vertex2);
    //如果索引等於-1說明點不存在
    if (vertexIndex2 == -1)
    {
        //如果未找到點則報錯
        throw new KeyNotFoundException($"點不存在");
    }
    return _edgeArray[vertexIndex1, vertexIndex2];
}

10、深度優先遍歷 DFS

深度優先遍歷正常有兩種實現方法,一種是使用遞迴呼叫,一種是使用棧結構實現,下面我們使用遞迴的方式來實現。

因為我們需要保證每個點只會被訪問一次,因此需要定義一個陣列用來記錄元素已經被訪問過。我們這裡是以無向圖為例,因為無向圖的對稱性,索引我們選用一維陣列即可滿足記錄被訪問元素,而如果是有向圖我們則需要使用二維陣列記錄被訪問元素。

具體程式碼如下:

//深度優先遍歷
public void DFS(T startVertex)
{
    //根據點元素獲取點索引
    var startVertexIndex = GetVertexIndex(startVertex);
    //如果索引等於-1說明點不存在
    if (startVertexIndex == -1)
    {
        //如果未找到點則報錯
        throw new KeyNotFoundException($"點不存在");
    }
    //定義已訪問標記陣列
    //因為無向圖對稱特性因此一維陣列即可
    //如果是有向圖則需要定義二維陣列
    var visited = new bool[_vertexCount];
    DFSUtil(startVertexIndex, visited);
    Console.WriteLine();
}
//深度優先遍歷
private void DFSUtil(int index, bool[] visited)
{
    //標記當前元素已訪問過
    visited[index] = true;
    //列印點
    Console.Write(_vertexArray[index] + " ");
    //遍歷查詢與當前元素相鄰的元素
    for (var i = 0; i < _vertexCount; i++)
    {
        //如果是相鄰的元素,並且元素未被訪問過
        if (_edgeArray[index, i] == 1 && !visited[i])
        {
            //則遞迴呼叫自身方法
            DFSUtil(i, visited);
        }
    }
}

11、廣度優先遍歷 BFS

廣度優先遍歷可以藉助佇列來實現。首先把起始點新增入佇列,然後把點出佇列,同時把該點的所有鄰接點新增入佇列,迴圈往復,一直到把所有元素處理完為止。

//廣度優先遍歷
public void BFS(T startVertex)
{
    //根據點元素獲取點索引
    var startVertexIndex = GetVertexIndex(startVertex);
    //如果索引等於-1說明點不存在
    if (startVertexIndex == -1)
    {
        //如果未找到點則報錯
        throw new KeyNotFoundException($"點不存在");
    }
    //定義已訪問標記陣列
    //因為無向圖對稱特性因此一維陣列即可
    //如果是有向圖則需要定義二維陣列
    var visited = new bool[_vertexCount];
    //使用佇列實現廣度優先遍歷
    var queue = new Queue<int>();
    //將起點入隊
    queue.Enqueue(startVertexIndex);
    //標記起點為已訪問
    visited[startVertexIndex] = true;
    //遍歷佇列
    while (queue.Count > 0)
    {
        //出隊點
        var vertexIndex = queue.Dequeue();
        //列印點
        Console.Write(_vertexArray[vertexIndex] + " ");
        //遍歷查詢與當前元素相鄰的元素
        for (var i = 0; i < _vertexCount; i++)
        {
            //如果是相鄰的元素,並且元素未被訪問過
            if (_edgeArray[vertexIndex, i] == 1 && !visited[i])
            {
                //則將相鄰元素索引入隊
                queue.Enqueue(i);
                //並標記為已訪問
                visited[i] = true;
            }
        }
    }
    Console.WriteLine();
}

:測試方法程式碼以及示例原始碼都已經上傳至程式碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner

相關文章