有向图(2.图的基础知识及效率分析)

开始是一些关于图的基础知识,后面一部分是对于上一贴有向图基于邻接表的C++实现效率的分析。

 

有向图:

 

        在数学上,一个图(Graph)是表示物件与物件之间的关系的方法,是图论的基本研究对象。一个图看起来是由一些小圆点(称为顶点或结点)和连结这些圆点的直线或曲线(称为边)组成的。如果给图的每条边规定一个方向,那么得到的图称为有向图,其边也称为有向边。在有向图中,与一个节点相关联的边有出边和入边之分,而与一个有向边关联的两个点也有始点和终点之分。相反,边没有方向的图称为无向图。

 

 

图又有各种变体,包括简单图/多重图;有向图/无向图等,但大体上有以下两种定义方式。

 

 

 

二元组的定义

 

  图G是一个二元组(V,E),其中V称为顶点集,E称为边集。它们亦可写成V(G)和E(G)。 E的元素是一个二元组数对,用(x,y)表示,其中。

 

三元组的定义

 

  一个图,是指一个三元组(V,E,I),其中V称为顶集(Vertices set),E称为边集(Edges set),EG不相交;I称为关联函数,IE中的每一个元素映射到。如果那么称边e连接顶点u,v,而u,v则称作e的端点,u,v此时关于e相邻。同时,若两条边i,j有一个公共顶点u,则称i,j关于u相邻。

 

 

 

 

 

存储结构:

 

        一、邻接矩阵法

 

        用一个二维数组表示图中的边顶点间的相邻关系,用一个顺序表来存储顶点信息。可以单纯用0、1来表示两个顶点是否相邻,也可以用数值表示两个顶点间边的权值。

 

       邻接矩阵在查找任意一条边及边上的权时,很方便。也能很快捷的查找任意顶点的邻接点,确定任意顶点的度。但是在顶点数目巨大,而边稀疏的时候,存储空间利用率低。查找的时间度<= O(n),  (n为顶点数)

 

       现在我做的项目就是这样的情况,所以不使用邻接矩阵来存储数据。

 

 

 

       二、邻接表

 

       对图中每个顶点建立一个邻接关系的单链表--顶点邻接表。第i个单链表中的节点表示依附于顶点vi的边(对于有向图是以顶点vi为尾的弧)。即所有从顶点vi出发的边所指向的节点都应在第i个单链表中。

 

邻接表便于找任一顶点的出边及出边邻接点、出度。平均查找时间复杂度为O(e/n)。

      但是如果想要查找一条具体的边,则不如邻接矩阵方便。因为必须要遍历顶点(弧尾)的链表,才能知道是否存在所求边。

      同样,如果要查找任一顶点的入边和入邻接点、入度则需要遍历所有的单链表才能得出最终结果。针对这种要求,可以建立逆邻接表。即为顶点vi建立入边表。入边表中每个结点对应一条指向vi的边的弧尾。
      不过,同时建立邻接表和逆邻接表会占用双倍的空间,因为每个结点都需要使用两次。

 

      三、十字链表

      十字链表其实是将邻接表和逆邻接表结合为一个表,缩减了需要的存储空间。

      如图:

从图中可以看出,对于链表结点,有4个需要注意的值。若有边v1->v2,边起点是弧尾v1在顶点存放顺序表中的位置,边终点是弧头v2在顶点存放顺序表中的位置。下一入边指针指向边终点同为v2的另一条邻接边,下一出边指针指向边起点同为v1的另一条邻接边。

 

    边集数组及多重邻接表的表示方法暂时不讨论。

 

代码实现及效率分析:

        在上一贴中实现了有向图基于邻接表的C++实现,下一贴将给出基于十字链表的实现。这里还是主要说一点关于基于邻接表代码的实现及效率问题。

 

       可以看出,使用两个struct(class),一个表示边一个表示顶点,内容跟上面邻接表图几乎一样。所有的顶点都会存放到一个vector容器中,方便管理,定位元素位置等等。主要操作还是针对struct中数据。

 

       在刚完成时,我做测试,加入10W条边需要20分钟。开始以为在已有链表中插入新边很费时间。比如插入v1->v2,如果不存在边,则在v1的邻接表起始点插入这条边。但如果已经有v1->v2边存在,那么新的边会插在已有的第一条v1->v2边后面。这样的实现方式一定会去遍历邻接表,尤其是不存在需要插入的边,会遍历顶点的整个邻接表。

 

       修改插入边的方式,即不管需要插入的边是否存在,都会放在顶点邻接表的开始位置,这样提高了插入速度。不过重新运行,发现时间仍然没有很大的变化。然后发现,相对于5000个顶点,10W条边真不算很多,每个顶点分摊下来平均也就20条边。遍历一次有20个节点的邻接表花不了太多时间。这样的话,时耗大部分就不是浪费在链表遍历上了。如果图中每个顶点的邻接表很大,这样修改是有用的。但是这样插入的边是没有顺序可言的,如果你想找出所有v1->v2的边,必须遍历整个v1的邻接链表。而按照我初始的实现方式,不一定需要遍历完整个链表就可完成要求。

 

       加入时间函数后,跟踪insertAEdge()函数每部分。发现相对于定位顶点v1,v2的位置来说,遍历链表的时间几乎可以不计。插入一条边v1->v2时,首先需要定位顶点v1,v2看看他们是否是vector中已有的数据。不是的话说明顶点是不存在的。这份代码中绝大部分时间就花在了搜索中。最坏情况下,搜索一个顶点竟然需要0.008秒左右。这样的时耗真是伤不起啊。代码中完成搜索v1,v2功能的是下面语句:

      int v1 = getVertexIndex(vertexName1);      //V1,V2是返回的顶点v1,v2在容器中的索引
int v2 = getVertexIndex(vertexName2);

getVertexIndex函数实现:

 

int getVertexIndex(IN const vertexNametype vertexName)
{
for (int i = 0; i < m_vertexArray.size(); i++)
{
if (vertexName == m_vertexArray.at(i).vertexName)
{
return i;
}
}
return -1;
}

     想想觉得每次循环都调用m_vertexArray.size()和m_vertexArray.at(i)肯定是会花时间的,于是用一个临时变量来存储m_vertexArray.size()的值,避免多次调用。然后不用容器的at()函数,而直接使用下标去提升效率。修改后的定位v1,v2代码如下:

 

int v1 = -1;
int v2 = -1;
int size = m_vertexArray.size();

for(int i = 0; i != size ; i++)
{
if( v1 != -1 && v2 != -1 )
{
break;
}
if( v1 == -1 && vertexName1 == m_vertexArray[i].vertexName )
{
v1 = i;
}
if( v2 == -1 && vertexName2 == m_vertexArray[i].vertexName )
v2 = i;
}

 

 

再次运行程序,发现时间从20分钟缩短到4分钟,虽然进步很大,可是离理想要求还是差太远。好嘛,只能想办法缩短查找v1,v2的时间。目前来说,我对搜索算法研究还不多,知道2分搜索法效率比较高(囧...)。而我们搜索的关键字是字串,可以进行比较,所以,动手吧!

 

 

 

       首先需要将顶点排序,这样才能使用2分搜索法。由于存放在容器中的元素是一个类类型,我懒得去写函数对比它们,所以在开始的时候直接对字串数组排序,然后按这个顺序将数据压入容器中,再采用2分搜索去查找v1,v2的索引。

 

 

 

       最后运行程序,时间控制在10秒内,大概8秒左右。可能还有方法能提高效率,但目前的速度已经比较符合要求,就暂时这样吧。

 

      

 

       这些修改细节在下一贴的十字链表实现中都可以看到。

 

 

 

       必须吐槽一下,当我刚发布完这个文章,突然发现2分算法写得有一点点问题,用了一种最慢的递归调用方式。修改成函数内部循环后,其实现在运行时间是4秒左右。。。

 

 

 

 

 

 

转载于:https://www.cnblogs.com/SadGeminids/archive/2011/11/09/2243544.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值