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

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

一、图的基本概念

1.1 多对多的复杂关系

  现实中人与人之间关系非常复杂,比如我认识的朋友,可能他们之间也互相认识,这不是简单的一对一、一对多,研究人际关系很自然会考虑多对多的情况。图是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。

定义:图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

  在图中需要注意的是:

  (1)线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(Vertex)

  (2)线性表可以没有元素,称为空表;树中可以没有节点,称为空树;但是,在图中不允许没有顶点(有穷非空性)。

  (3)线性表中的各元素是线性关系,树中的各元素是层次关系,而图中各顶点的关系是用边来表示(边集可以为空)。

1.2 纷繁冗多的术语

  图的基本术语有很多,本文只挑选几个特别重要的来说明,其余的请阅读相关教材。

  (1)无向图

  如果图中任意两个顶点之间的边都是无向边(简而言之就是没有方向的边),则称该图为无向图(Undirected graphs)。

  (2)有向图

  如果图中任意两个顶点之间的边都是有向边(简而言之就是有方向的边),则称该图为有向图(Directed graphs)。

  (3)完全图

  ①无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。(含有n个顶点的无向完全图有(n×(n-1))/2条边)如下图所示:

  ②有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。(含有n个顶点的有向完全图有n×(n-1)条边)如下图所示:

PS:当一个图接近完全图时,则称它为稠密图(Dense Graph),而当一个图含有较少的边时,则称它为稀疏图(Spare Graph)。

  (4)顶点的度

  顶点Vi的度(Degree)是指在图中与Vi相关联的边的条数。对于有向图来说,有入度(In-degree)和出度(Out-degree)之分,有向图顶点的度等于该顶点的入度和出度之和。

  (5)邻接

  ①若无向图中的两个顶点V1和V2存在一条边(V1,V2),则称顶点V1和V2邻接(Adjacent);

  ②若有向图中存在一条边<V3,V2>,则称顶点V3与顶点V2邻接,且是V3邻接到V2或V2邻接直V3

PS:无向图中的边使用小括号“()”表示,而有向图中的边使用尖括号“<>”表示。

  (6)路径

  在无向图中,若从顶点Vi出发有一组边可到达顶点Vj,则称顶点Vi到顶点Vj的顶点序列为从顶点Vi到顶点Vj的路径(Path)。

  (7)连通

  若从Vi到Vj有路径可通,则称顶点Vi和顶点Vj是连通(Connected)的。

  (8)权

  有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight)。

二、图的存储结构

图的存储结构除了要存储图中的各个顶点本身的信息之外,还要存储顶点与顶点之间的关系,因此,图的结构也比较复杂。常用的图的存储结构有邻接矩阵和邻接表等。

2.1 邻接矩阵表示法

  图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。

  (1)无向图:

  我们可以设置两个数组,顶点数组为vertex[4]={v0,v1,v2,v3},边数组arc[4][4]为上图右边这样的一个矩阵。对于矩阵的主对角线的值,即arc[0][0]、arc[1][1]、arc[2][2]、arc[3][3],全为0是因为不存在顶点的边。

  (2)有向图:

  我们再来看一个有向图样例,如下图所示的左边。顶点数组为vertex[4]={v0,v1,v2,v3},弧数组arc[4][4]为下图右边这样的一个矩阵。主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称,比如由v1到v0有弧,得到arc[1][0]=1,而v到v没有弧,因此arc[0][1]=0。

不足:由于存在n个顶点的图需要n*n个数组元素进行存储,当图为稀疏图时,使用邻接矩阵存储方法将会出现大量0元素,这会造成极大的空间浪费。这时,可以考虑使用邻接表表示法来存储图中的数据。

2.2 邻接表表示法

  首先,回忆我们在线性表时谈到,顺序存储结构就存在预先分配内存可能造成存储空间浪费的问题,于是引出了链式存储的结构。同样的,我们也可以考虑对边或弧使用链式存储的方式来避免空间浪费的问题。

  邻接表由表头节点表节点两部分组成,图中每个顶点均对应一个存储在数组中的表头节点。如果这个表头节点所对应的顶点存在邻接节点,则把邻接节点依次存放于表头节点所指向的单向链表中。

  (1)无向图:下图所示的就是一个无向图的邻接表结构。

  从上图中我们知道,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。例如:v1顶点与v0、v2互为邻接点,则在v1的边表中,adjvex分别为v0的0和v2的2。

PS:对于无向图来说,使用邻接表进行存储也会出现数据冗余的现象。例如上图中,顶点V0所指向的链表中存在一个指向顶点V3的同事,顶点V3所指向的链表中也会存在一个指向V0的顶点。

  (2)有向图:若是有向图,邻接表结构是类似的,但要注意的是有向图由于有方向的。因此,有向图的邻接表分为出边表和入边表(又称逆邻接表),出边表的表节点存放的是从表头节点出发的有向边所指的尾节点;入边表的表节点存放的则是指向表头节点的某个顶点,如下图所示。

  (3)带权图:对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可,如下图所示。

三、图的模拟实现

PS:由于邻接矩阵容易造成空间资源的浪费,因此这里只考虑使用邻接表来实现。

3.1 总体设计结构

  (1)链表节点定义

  ①表头节点Vertex

        /// <summary>
        /// 嵌套类:存放于数组中的表头节点
        /// </summary>
        /// <typeparam name="TValue"></typeparam>
        protected class Vertex<TValue>
        {
            public TValue data;     // 数据
            public Node firstEdge;  // 邻接点链表头指针
            public bool isVisited;  // 访问标志:遍历时使用

            public Vertex()
            {
                this.data = default(TValue);
            }

            public Vertex(TValue value)
            {
                this.data = value;
            }
        }

  ②表节点Node

        /// <summary>
        /// 嵌套类:链表中的表节点
        /// </summary>
        protected class Node
        {
            public Vertex<T> adjvex;    // 邻接点域
            public Node next;           // 下一个邻接点指针域

            public Node()
            {
                this.adjvex = null;
            }

            public Node(Vertex<T> value)
            {
                this.adjvex = value;
            }
        }

  (2)邻接表总体定义

    public class MyAdjacencyList<T> where T : class
    {
        private List<Vertex<T>> items;  // 图的顶点集合

        public MyAdjacencyList()
            : this(10)
        {
        }

        public MyAdjacencyList(int capacity)
        {
            this.items = new List<Vertex<T>>(capacity);
        }

        #region 基本方法:为图中添加顶点、添加有向与无向边
        #endregion

        #region 辅助方法:图中是否包含某个元素、查找指定顶点、初始化visited标志
        #endregion

        #region 嵌套类:表头节点与表节点定义
        #endregion
    }

  首先,我们使用了一个动态集合List来代替数组存储Vertex的集合,默认容量为10,且不需要数组存储空间不够的情况,简化了操作。其次,我们要定义一些基本方法,如添加顶点、添加边。还要定义一些辅助方法,如判断是否包含某个元素等(详见完整代码文件)。最后,我们再实现图的一些遍历算法,如深度优先遍历与广度优先遍历(本篇不作介绍,下一篇再介绍)。

3.2 基本方法实现

  (1)添加一个顶点

        /// <summary>
        /// 添加一个顶点
        /// </summary>
        /// <param name="item">顶点元素data</param>
        public void AddVertex(T item)
        {
            if (Contains(item))
            {
                throw new ArgumentException("添加了重复的顶点!");
            }

            Vertex<T> newVertex = new Vertex<T>(item);
            items.Add(newVertex);
        }

  就是往集合里边加入新元素;

  (2)添加一条边

  这里需要分为两种情况,一种是添加无向图的边,这时无向图的两个顶点都需要记录边的信息。另一种则是添加有向图的边,这时只需要一条记录;

  ①无向图

        /// <summary>
        /// 添加一条无向边
        /// </summary>
        /// <param name="from">头顶点data</param>
        /// <param name="to">尾顶点data</param>
        /// <param name="weight">权值</param>
        public void AddEdge(T from, T to)
        {
            Vertex<T> fromVertex = Find(from);
            if (fromVertex == null)
            {
                throw new ArgumentException("头顶点不存在!");
            }

            Vertex<T> toVertex = Find(to);
            if (toVertex == null)
            {
                throw new ArgumentException("尾顶点不存在!");
            }

            // 无向图的两个顶点都需要记录边的信息
            AddDirectedEdge(fromVertex, toVertex);
            AddDirectedEdge(toVertex, fromVertex);
        }

  这里可以看到这两句代码,对应的两个顶点都记录了边的信息。

    // 无向图的两个顶点都需要记录边的信息
    AddDirectedEdge(fromVertex, toVertex);
    AddDirectedEdge(toVertex, fromVertex);

  ②有向图

        /// <summary>
        /// 添加一条有向边
        /// </summary>
        /// <param name="from">头结点data</param>
        /// <param name="to">尾节点data</param>
        public void AddDirectedEdge(T from, T to)
        {
            Vertex<T> fromVertex = Find(from);
            if (fromVertex == null)
            {
                throw new ArgumentException("头顶点不存在!");
            }

            Vertex<T> toVertex = Find(to);
            if (toVertex == null)
            {
                throw new ArgumentException("尾顶点不存在!");
            }

            AddDirectedEdge(fromVertex, toVertex);
        }

  ③如何添加边

  在实现中,无论是无线图还是有向图都是添加的有向边,只不过无向图是添加了两条有向边:

        /// <summary>
        /// 添加一条有向边
        /// </summary>
        /// <param name="fromVertex">头顶点</param>
        /// <param name="toVertex">尾顶点</param>
        private void AddDirectedEdge(Vertex<T> fromVertex, Vertex<T> toVertex)
        {
            if (fromVertex.firstEdge == null)
            {
                fromVertex.firstEdge = new Node(toVertex);
            }
            else
            {
                Node temp = null;
                Node node = fromVertex.firstEdge;

                do
                {
                    // 检查是否添加了重复边
                    if (node.adjvex.data.Equals(toVertex.data))
                    {
                        throw new ArgumentException("添加了重复的边!");
                    }
                    temp = node;
                    node = node.next;
                } while (node != null);

                Node newNode = new Node(toVertex);
                temp.next = newNode;
            }
        }

  (3)打印每个顶点及其邻接点的信息

        /// <summary>
        /// 打印打印每个顶点和它的邻接点
        /// </summary>
        /// <param name="isDirectedGraph">是否是有向图</param>
        public string GetGraphInfo(bool isDirectedGraph = false)
        {
            StringBuilder sb = new StringBuilder();
            foreach (Vertex<T> v in items)
            {
                sb.Append(v.data.ToString() + ":");
                if (v.firstEdge != null)
                {
                    Node temp = v.firstEdge;
                    while (temp != null)
                    {
                        if (isDirectedGraph)
                        {
                            sb.Append(v.data.ToString() + "→" + temp.adjvex.data.ToString() + " ");
                        }
                        else
                        {
                            sb.Append(temp.adjvex.data.ToString());
                        }
                        temp = temp.next;
                    }
                }
                sb.Append("\r\n");
            }

            return sb.ToString();
        }

  这里判断了是否是有向图,如果是有向图则显示A→B的形式,如果是无向图则显示A:B的形式。

3.3 基本功能测试

  这里我们对基本功能做一下测试,分为无向图和有向图,首先插入顶点及对应边,然后打印顶点及其邻接表的信息,要构造的无向图与有向图如上面两张图所示,测试代码如下所示:

        static void MyAdjacencyListTest()
        {
            Console.WriteLine("------------无向图------------");
            MyAdjacencyList<string> adjList = new MyAdjacencyList<string>();
            // 添加顶点
            adjList.AddVertex("A");
            adjList.AddVertex("B");
            adjList.AddVertex("C");
            adjList.AddVertex("D");
            //adjList.AddVertex("D"); // 会报异常:添加了重复的节点
            // 添加无向边
            adjList.AddEdge("A", "B");
            adjList.AddEdge("A", "C");
            adjList.AddEdge("A", "D");
            adjList.AddEdge("B", "D");
            //adjList.AddEdge("B", "D"); // 会报异常:添加了重复的边

            Console.Write(adjList.GetGraphInfo());

            Console.WriteLine("------------有向图------------");
            MyAdjacencyList<string> dirAdjList = new MyAdjacencyList<string>();
            // 添加顶点
            dirAdjList.AddVertex("A");
            dirAdjList.AddVertex("B");
            dirAdjList.AddVertex("C");
            dirAdjList.AddVertex("D");
            // 添加有向边
            dirAdjList.AddDirectedEdge("A", "B");
            dirAdjList.AddDirectedEdge("A", "C");
            dirAdjList.AddDirectedEdge("A", "D");
            dirAdjList.AddDirectedEdge("B", "D");

            Console.Write(dirAdjList.GetGraphInfo(true));
        }

  运行结果如下图所示:

附件下载

  本篇实现的图的邻接表结构:code.datastructure.graph

参考资料

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

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

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

作者:周旭龙

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

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

时间: 2024-08-24 23:37:26

数据结构基础温故-5.图(上)的相关文章

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

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

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

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

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

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

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

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

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

前面所讨论的线性表元素之间都是一对一的关系,今天我们所看到的结构各元素之间却是一对多的关系.树在计算机中有着广泛的应用,甚至在计算机的日常使用中,也可以看到树形结构的身影,如下图所示的Windows资源管理器和应用程序的菜单都属于树形结构.树形结构是一种典型的非线性结构,除了用于表示相邻关系外,还可以表示层次关系.本文重点讨论树与二叉树的基本结构和遍历算法等内容. 一.好大一棵树,绿色的祝福 1.1 树的基本概念 Defination:树(Tree)是 n(n≥0)个结点的有限集.n=0时,该树

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

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

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

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

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

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

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

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