数据结构 - 图之代码实现

书接上回,我们继续来聊聊图的遍历与实现。

1.png

01、遍历

在图的基本功能中有个很重要的功能——遍历,遍历顾名思义就是把图上所有的点都访问一遍,具体来说就是从一个连通的图中某一个点出发,沿着边访问所有的点,并且每个点只能访问一遍。

下面我们介绍两种常见的遍历方式:深度优先遍历(DFS)和广度优先遍历(BFS)。

1、深度优先遍历

如果我们把边当作路,深度优先遍历就是路一直往下走,直到没路了再返回走其他路。其实优点像树的先序遍历从根节点沿着子节点一直向下直到叶子节点再调头。

下面我们梳理一下深度优先遍历大致分为以下几个步骤:

(1)从图中任意一个点A出发,并访问点;

(2)找出点A的第一个未被访问的邻接点,并访问该点;

(3)以该点为新的点,重复步骤(2),直至新的邻接点没有未被访问的邻接点;

(4)返回前一个点并依次访问前一个点为未被访问的其他邻接点,并访问该点;

(5)重复步骤(3)和(4),直至所有点都被访问过;

2.jpg

如上图演示了从点A出发进行深度优先遍历过程,其中红色虚线表示前进路线,蓝色虚线表示回退路线。最后输出:A->B->E->F->C->G->D。

2、广度优先遍历

如果说深度优先遍历是找到一条路一直走到底,那么广度优先遍历就是先把所有的路走一步再说。其实优点像树的层次遍历从根节点出发先遍历其子节点然后再遍历其孙子节点直至遍历完所有节点。

下面我们梳理一下广度优先遍历大致分为以下几个步骤:

(1)从图中任意一点A出发,并访问点A;

(2)依次访问点A所有未被访问的邻接点,访问完邻接点后,然后按邻接点顺序把邻接点作为新的出发执行步骤(1);

(3)重复步骤(1)和(2)直至所有点都被访问到。

3.jpg

如上图演示了从点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();
}

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容