数据结构基础温故-5.图(下):最短路径

图的最重要的应用之一就是在交通运输和通信网络中寻找最短路径。例如在交通网络中经常会遇到这样的问题:两地之间是否有公路可通;在有多条公路可通的情况下,哪一条路径是最短的等等。这就是带权图中求最短路径的问题,此时路径的长度不再是路径上边的数目总和,而是路径上的边所带权值的和。带权图分为无向带权图和有向带权图,但如果从A地到B地有一条公路,A地和B地的海拔高度不同,由于上坡和下坡的车速不同,那么边<A,B>和边<B,A>上表示行驶时间的权值也不同。考虑到交通网络中的这种有向性,本篇也只讨论有向带权图的最短路径。一般习惯将路径的开始顶点成为源点,路径的最后一个顶点成为终点。

一、单源点最短路径

  单源点最短路径是指给定一个出发点(源点)和一个有向图,求出源点到其他各顶点之间的最短路径。对于下图左边所示的有向图G,设顶点0为源点,则其到其他各顶点的最短路径如下图右边所示。

  从上图中可以看出,从源点0到终点4一共有4条路径:

  (1)0→4      路径长度:90

  (2)0→3→4     路径长度:90

  (3)0→1→2→4    路径长度:70

  (4)0→3→2→4    路径长度:60

  可以看出,源点0到终点4的最短路径为第(4)条路径。为了求出最短路径,前人们已经做很多工作,为我们奉献了两个重要的算法:Dijkstra(迪杰斯特拉)和Floyd(弗洛伊德)算法,下面我们就来看看这两个算法。

二、Dijkstra算法

2.1 算法思想

  Dijkstra在对最短路径的求解方式做了大量的观察之后,首先提出了按路径长度递增产生与顶点之间的路径最短的算法,以他的名字称之为Dijkstra算法。

Dijkstra算法的基本思想是:将图中顶点的集合分为两组S和U,并按最短路径长度的递增次序依次将集合U中的顶点加入到S中,在加入的过程中,总保持从原点v到S中各顶点的最短路径长度不大于从原点v到U中任何顶点的最短路径长度。

  Dijkstra算法采用邻接矩阵存储图的信息并计算源点到图中其余顶点的最短路径,如下图所示。

2.2 算法实现

  (1)代码实现

        static void Dijkstra(int[,] cost, int v)
        {
            int n = cost.GetLength(1); // 计算顶点个数
            int[] s = new int[n];      // 集合S
            int[] dist = new int[n];   // 结果集
            int[] path = new int[n];   // 路径集

            for (int i = 0; i < n; i++)
            {
                // 初始化结果集
                dist[i] = cost[v, i];
                // 初始化路径集
                if (cost[v, i] > 0)
                {
                    // 如果源点与顶点存在边
                    path[i] = v;
                }
                else
                {
                    // 如果源点与顶点不存在边
                    path[i] = -1;
                }
            }

            s[v] = 1;   // 将源点加入集合S
            path[v] = 0;

            for (int i = 0; i < n; i++)
            {
                int u = 0;  // 指示剩余顶点在dist集合中的最小值的索引号
                int minDis = int.MaxValue; // 指示剩余顶点在dist集合中的最小值大小

                // 01.计算dist集合中的最小值
                for (int j = 0; j < n; j++)
                {
                    if (s[j] == 0 && dist[j] > 0 && dist[j] < minDis)
                    {
                        u = j;
                        minDis = dist[j];
                    }
                }

                s[u] = 1; // 将抽出的顶点放入集合S中

                // 02.计算源点经过顶点u到其余顶点的距离
                for (int j = 0; j < n; j++)
                {
                    // 如果顶点不在集合S中
                    if (s[j] == 0)
                    {
                        // 加入的顶点如与其余顶点存在边,并且重新计算的值小于原值
                        if (cost[u, j] > 0 && (dist[j] == 0 || dist[u] + cost[u, j] < dist[j]))
                        {
                            // 计算更小的值代替原值
                            dist[j] = dist[u] + cost[u, j];
                            path[j] = u;
                        }
                    }
                }
            }

            // 打印源点到各顶点的路径及距离
            for (int i = 0; i < n; i++)
            {
                if (s[i] == 1)
                {
                    Console.Write("从{0}到{1}的最短路径为:", v, i);
                    Console.Write(v + "→");
                    // 使用递归获取指定顶点在路径上的前一顶点
                    GetPath(path, i, v);
                    Console.Write(i + Environment.NewLine + "SUM:");
                    Console.WriteLine("路径长度为:{0}", dist[i]);
                }
            }
        }

        static void GetPath(int[] path, int i, int v)
        {
            int k = path[i];
            if (k == v)
            {
                return;
            }

            GetPath(path, k, v);
            Console.Write(k + "→");
        }

  这里经历了三个步骤,第一步是构造集合U,第二步是寻找最短路径,第三步是重新计算替换原有路径。

  (2)基本测试

  这里要构造的有向带权图如下图所示:

        static void DijkstraTest()
        {
            int[,] cost = new int[5, 5];
            // 初始化邻接矩阵
            cost[0, 1] = 10;
            cost[0, 3] = 30;
            cost[0, 4] = 90;
            cost[1, 2] = 50;
            cost[2, 4] = 10;
            cost[3, 2] = 20;
            cost[3, 4] = 60;
            // 使用Dijkstra算法计算最短路径
            Dijkstra(cost, 0);
        }

  运行结果如下图所示:

  从图中可以看出,从源点0到终点4的最短路径为:0→3→2→4,该路径长度为60。

三、Floyd算法

3.1 算法思想

  Floyd(弗洛伊德)算法提出了另外一种用于计算有向图中所有顶点间的最短路径,这种算法成为Floyd算法,它的形式较Dijkstra算法更为简单。

  Floyd算法仍然使用邻接矩阵存储的图,同时定义了一个二维数组A,其中每一个分量A[i,j]是顶点i到顶点j的最短路径长度。另外还使用了另一个二维数组path来保存最短路径信息。Floyd算法的基本思想如下:

(1)初始时,对图中任意两个顶点Vi和Vj,如果从Vi到Vj存在边,则从Vi到Vj存在一条长度为cost[i,j]的路径,但该路径不一定是最短路径。初始化时,A[i,j]=cost[i,j]。

(2)在图中任意两个顶点Vi和Vj之间加入顶点Vk,如果Vi经Vk到达Vj的路径存在并更短,则用A[i,k]+A[k,j]的值代替原来的A[i,j]值。

(3)重复步骤(2),直到将所有顶点作为中间点依次加入集合中,并通过迭代公式不断修正A[i,j]的值,最终获得任意顶点的最短路径长度。

3.2 算法实现

  (1)代码实现

        static void Floyd(int[,] cost, int v)
        {
            int n = cost.GetLength(1);  // 获取顶点个数
            int[,] A = new int[n, n];   // 存放最短路径长度
            int[,] path = new int[n, n];// 存放最短路径信息

            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    // 辅助数组A和path的初始化
                    A[i, j] = cost[i, j];
                    path[i, j] = -1;
                }
            }

            // Flyod算法核心代码部分
            for (int k = 0; k < n; k++)
            {
                for (int i = 0; i < n; i++)
                {
                    for (int j = 0; j < n; j++)
                    {
                        // 如果存在中间顶点K的路径
                        if (i != j && A[i, k] != 0 && A[k, j] != 0)
                        {
                            // 如果加入中间顶点k后的路径更短
                            if (A[i, j] == 0 || A[i, j] > A[i, k] + A[k, j])
                            {
                                // 使用新路径代替原路径
                                A[i, j] = A[i, k] + A[k, j];
                                path[i, j] = k;
                            }
                        }
                    }
                }
            }

            // 打印最短路径及路径长度
            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    if (A[i, j] == 0)
                    {
                        if (i != j)
                        {
                            Console.WriteLine("从{0}到{1}没有路径!", i, j);
                        }
                    }
                    else
                    {
                        Console.Write("从{0}到{1}的路径为:", i, j);
                        Console.Write(i + "→");
                        // 使用递归获取指定顶点的路径
                        GetPath(path, i, j);
                        Console.Write(j + "     ");
                        Console.WriteLine("路径长度为:{0}", A[i, j]);
                    }
                }
                Console.WriteLine();
            }
        }

        static void GetPath(int[,] path, int i, int j)
        {
            int k = path[i, j];
            if (k == -1)
            {
                return;
            }

            GetPath(path, i, k);
            Console.Write(k + "→");
            GetPath(path, k, j);
        }

  (2)基本测试

        static void FloydTest()
        {
            int[,] cost = new int[5, 5];
            // 初始化邻接矩阵
            cost[0, 1] = 10;
            cost[0, 3] = 30;
            cost[0, 4] = 90;
            cost[1, 2] = 50;
            cost[2, 4] = 10;
            cost[3, 2] = 20;
            cost[3, 4] = 60;
            // 使用Flyod算法计算最短路径
            Floyd(cost, 0);
        }

  运行结果如下图所示:

参考资料

(1)程杰,《大话数据结构》

(2)陈广,《数据结构(C#语言描述)》

(3)段恩泽,《数据结构(C#语言版)》

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

时间: 2024-08-27 22:45:35

数据结构基础温故-5.图(下):最短路径的相关文章

数据结构基础温故-5.图(中):最小生成树算法

图的“多对多”特性使得图在结构设计和算法实现上较为困难,这时就需要根据具体应用将图转换为不同的树来简化问题的求解. 一.生成树与最小生成树 1.1 生成树 对于一个无向图,含有连通图全部顶点的一个极小连通子图成为生成树(Spanning Tree).其本质就是从连通图任一顶点出发进行遍历操作所经过的边,再加上所有顶点构成的子图. 采用深度优先遍历获得的生成树称为深度优先生成树(DFS生成树),采用广度优先遍历获得的生成树称为广度优先生成树(BFS生成树).如下图所示,无向图的DFS生成树和BFS

数据结构基础温故-5.图(上)

前面几篇已经介绍了线性表和树两类数据结构,线性表中的元素是“一对一”的关系,树中的元素是“一对多”的关系,本章所述的图结构中的元素则是“多对多”的关系.图(Graph)是一种复杂的非线性结构,在图结构中,每个元素都可以有零个或多个前驱,也可以有零个或多个后继,也就是说,元素之间的关系是任意的.现实生活中的很多事物都可以抽象为图,例如世界各地接入Internet的计算机通过网线连接在一起,各个城市和城市之间的铁轨等等. 一.图的基本概念 1.1 多对多的复杂关系 现实中人与人之间关系非常复杂,比如

数据结构基础温故-6.查找(上):基本查找与树表查找

只要你打开电脑,就会涉及到查找技术.如炒股软件中查股票信息.硬盘文件中找照片.在光盘中搜DVD,甚至玩游戏时在内存中查找攻击力.魅力值等数据修改用来作弊等,都要涉及到查找.当然,在互联网上查找信息就更加是家常便饭.查找是计算机应用中最常用的操作之一,也是许多程序中最耗时的一部分,查找方法的优劣对于系统的运行效率影响极大.因此,本篇讨论一些查找方法. 一.顺序查找 1.1 基本思想 顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后

数据结构基础温故-1.线性表(下)

在上一篇中,我们了解了单链表与双链表,本次将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list). 一.循环链表基础 1.1 循环链表节点结构 循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p.next是否为空,现在则是p.next不等于头结点,则循环未结束. 1.2 循环链表的O(1)访问时间 在单链表中,有了头结点,我们可以在O(1)时间访问到第一个节点,但如果要访

数据结构基础温故-6.查找(下):哈希表

哈希(散列)技术既是一种存储方法,也是一种查找方法.然而它与线性表.树.图等结构不同的是,前面几种结构,数据元素之间都存在某种逻辑关系,可以用连线图示表示出来,而哈希技术的记录之间不存在什么逻辑关系,它只与关键字有关联.因此,哈希主要是面向查找的存储结构.哈希技术最适合的求解问题是查找与给定值相等的记录. 一.基本概念及原理 1.1 哈希定义的引入 这里首先看一个场景:在大多数情况下,数组中的索引并不具有实际的意义,它仅仅表示一个元素在数组中的位置而已,当需要查找某个元素时,往往会使用有实际意义

数据结构基础温故-1.线性表(上)

开篇:线性表是最简单也是在编程当中使用最多的一种数据结构.例如,英文字母表(A,B,C,D...,Z)就是一个线性表,表中的每一个英文字母都是一个数据元素:又如,成绩单也是一个线性表,表中的每一行是一个数据元素,每个数据元素又由学号.姓名.成绩等数据项组成.顺序表和链表作为线性表的两种重要的存在形式,它们是堆栈.队列.树.图等数据结构的实现基础. 一.线性表基础 1.1 线性表的基本定义 线性表:零个或多个数据元素的有限序列.线性表中的元素在位置上是有序的,类似于储户去银行排队取钱,人们依次排着

数据结构基础温故-4.树与二叉树(中)

在上一篇中,我们了解了树的基本概念以及二叉树的基本特点和代码实现,还用递归的方式对二叉树的三种遍历算法进行了代码实现.但是,由于递归需要系统堆栈,所以空间消耗要比非递归代码要大很多.而且,如果递归深度太大,可能系统撑不住.因此,我们使用非递归(这里主要是循环,循环方法比递归方法快, 因为循环避免了一系列函数调用和返回中所涉及到的参数传递和返回值的额外开销)来重新实现一遍各种遍历算法,再对二叉树的另外一种特殊的遍历—层次遍历进行实现,最后再了解一下特殊的二叉树—二叉查找树. 一.递归与循环的区别及

数据结构基础温故-1.线性表(中)

在上一篇中,我们学习了线性表最基础的表现形式-顺序表,但是其存在一定缺点:必须占用一整块事先分配好的存储空间,在插入和删除操作上需要移动大量元素(即操作不方便),于是不受固定存储空间限制并且可以进行比较快捷地插入和删除操作的链表横空出世,所以我们就来复习一下链表. 一.单链表基础 1.1 单链表的节点结构 在链表中,每个节点由两部分组成:数据域和指针域. 1.2 单链表的总体结构 链表就是由N个节点链接而成的线性表,如果其中每个节点只包含一个指针域那么就称为单链表,如果含有两个指针域那么就称为双

数据结构基础温故-7.排序

排序(Sorting)是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为按关键字“有序”的记录序列.如何进行排序,特别是高效率地进行排序时计算机工作者学习和研究的重要课题之一.排序有内部排序和外部排序之分,若整个排序过程不需要访问外存便能完成,则称此类排序为内部排序,反之则为外部排序.本篇主要介绍插入排序.交换排序.选择排序和归并排序这几种内部排序方法. 首先,我们今天的目标就是编写一个SortingHelper类,它是一个提供了多种排序方法的帮助类,后面我们的目标就是实现其中