数据结构图

图:它是一个顶点V集和边集构成的数据结构

图的存储结构:

邻接矩阵:

一维数组存储顶点集合,矩阵存储边集是否存在或者网图的权值(网和图的区别在于边有没有权重,权重可能存在物理意义比如,修建铁路的成本,或者步行到达的时间等等)。

补充几个相关的知识点:

第一个无向图:就是没有方向的图,一条边存在,那么能从一个顶点沿着边到另一个顶点,同时也能反向。无向图的邻接矩阵是对称矩阵,只需要存储上三角形矩阵即可。

第二个有向图:就是有方向的图,只能沿着箭头方向走,有向图的临街矩阵不对称,需要存储整个矩阵

第三个度:对于无向图来说,无向图的某行或列的非零元素个数,就是该顶点的度。对于有向图来说,一个顶点包括入度和出度,入度是指向该顶点的度,出度是指向其他顶点的度。

第四个邻接矩阵的优点和缺点:点与点之间的边是否存在很好查找,但是很难计算有多少条边,需要遍历整个矩阵才能看到,所以我们在设计数据结构的时候加上一个整形存储边数

#define MAX_VARTEX_NUM 20    //最大顶点数初始化为20
typedef char VartexType;    //顶点使用字符型存储
typedef int VRType;        //边的权重是整形,所以要创建整形二维数组邻接矩阵
typedef struct{
    VertexType vexs[MAX_VARTEX_NUM];
    VRType edges[MAX_VARTEX_NUM][MAX_VARTEX_NUM];
    int vexnum,arcnum;    //顶点数 边数

}MGraph;

邻接表:

邻接表分为两个部分,顶点邻接和边链表,就是顶点和顶点之间是顺序表,一个顶点里面存了一个链表的指针,那个链表里存储的是和该顶点相连的顶点

#define MAX_VERTEX_NUM 20    //最大顶点个数
typedef struct ArcNode{
    int adjvex;        //链表存储的节点位置
    ArcNode* nextarc;

}ArcNode;

typedef strcut VNode{
    VertexType    data;    //如果是有向图,那么链表长度就是data的出度
    ArcNode    *firstarc;    //链表头节点

}VNode,AdjList[MAX_VERTEX_NUM];    //顺序表部分
typedef struct{
    AdjList adjlist;    //顺序表
    int vexnum,arcnum;    //顶点数 边数
}ALGraph;

再补充两种存储方式,因为不常用,就不单独列一个标题了,一个是逆邻接表,邻接表的链表长度代表出度,那么逆邻接表的链表长度代表入度,要怎么实现都随意,只要满足我刚才说的就是逆邻接表,还有一个是十字链表,这个自己百度吧,我就提一嘴,虽然当初我上数据结构课的时候老师很认真的让我们写这个结构,但是我确实不常用,他就是把邻接表和逆邻接表合体了,叫做十字链表

图的遍历:

DFS深度优先遍历:

        深度优先遍历还是上图讲解比较明显,能力有限,我直接上画图画一个吧,凑活看吧

 dfs结果不唯一,根据你的存储结构,邻接表的存储顺序都有关

以下几种结果全都是dfs

第一种:a-b-c-e-d-z-f-h-i

第二种:a-b-c-e-d-z-i-f-h

第三种:a-b-c-e-z-f-h-i-d

第四种:a-b-c-e-z-i-f-h-d

太多种可能性了我就随便列了几个,就是沿着路径一直钻,钻不动了回到上一个节点,看有没有没钻过的节点,有就换一个接着钻,没有就继续回溯,就是DFS,至于代码根本不重要,理解了思路,代码不就是时间问题,本着复习和负责任的态度,我还是会上代码,并对代码进行注释和解析的,还有就是希望看这段代码之前能会递归,不要求非要写的出来,但至少得看得懂怎么递归调用自身

void DFSTraverse(Graph G)
{
    for(v=0;v<G.vexnum;v++)
    {
        visted[v]=False;    //visted数组是一个整形数组 false代表未被遍历,True就代表被遍历过
    }

    for(v=0;v<G.vexnum;v++)
    {
        if(!visted[v])DFS(G,v);
    }

}

void DFS(Graph G,int v)
{
    visted[v]=True;
    /*我省略了一个函数,这个东西有些教科书上写的是Visit(v),可以是任何功能,你要遍历,修改,增                                                                                                           加然后输出,只要是你想要对数据进行的操作都可以*/

    for(w=FirstAdjVex(G,v);w>=0;w=NextAdjVex(G,v,w))
        if(!visted[w])DFS(G,w);
}

for(w=FirstAdjVex(G,v);w>=0;w=NextAdjVex(G,v,w))
        if(!visted[w])DFS(G,w);

这个for循环我重点讲,因为他有两个函数,一个是FirstAdjVex(G,v),这个的功能就是把这个图的v节点的头结点返回,NextAdjVex(G,v,w)这个的功能是把v的下一个节点用w给我接收回来

至于递归的开始条件为什么要递归需要你们自己悟,悟不懂的就回去看之前给的dfs结果按代码的思路跑一遍,
会写递归的一看就懂。

这个代码没有用到回溯是因为递归栈会自己回溯。,函数的调用本身就是一种栈结构。

当然你如果不会用递归,就得自己写个栈,然后判断回溯之类的,我没试过,不过只要看过我之前的博客栈的讲解基本都不会有问题

时间复杂度我就不讲了,根据存储结构不同是不一样的,具体问题具体分析,这又不难,相信大家都可以分析

BFS广度遍历:

还是上图说好一点,手画的,凑活看吧

 a-b-f-c-d-z-h-e-i 跟树的层序遍历有一点点像,当着 BFS也不唯一,还是看存储方式或邻接表的存储顺序,DFS用的栈,那么与之对应的BFS就用的队列去实现,她的步骤就是先把当前节点入队,然后读取队头,然后打印出队,然后把与他相关的节点入队,然后接着读取队头,打印出队,重复之前的操作直到队列为空,BFS也不难我觉得大家可以根据我的诉说凭空想象,而且BFS和DFS代码很像,但我还是列一下,讲一下。

void BFSTraverse(Graph G)//这个函数是BFS的开端
{
    for(v=0;v<G.vexnum;v++)
    {
        visted[v]=0;
    }

    for(v=0;v<G.vexnum;v++)
    {

        if(!visted[v])BFS(G,v);
    }

}

void BFS(Graph G,int v)//这个函数是顶点入队,访问出队再入队的过程
{
    InitQueue(Q);
    visted[v]=true;
    //此处我省略了一个遍历函数,这个的功能自己定义可以是打印,修改,等等

    while(!EmptyQueue(Q))
    {
        DeQueue(Q,u);
        for(w=FistAdjVex(Q,u);w>=0;w=NextAdjVex(Q,u))
        {
            if(!visted[w])
            {
                visted[w]=True;
                //省略了遍历的步骤

                EnQueue(Q,w);
            }
        }

    }



}

最小生成树:

        先解释两个概念,连通图是指,从图中的任何一个顶点都能走到另外一个顶点就是连通图。生成树是指某连通图G的一个生成树,它包括G的所有顶点和n-1条边,生成树只要去掉任意一条边必然是不连通的。最小生成图是指,带权连通图的最小权值之和生成树,它在具体生活中的应用比如修路,怎么修最省钱,或者去旅游,怎么转最节省时间。

        有两种思路的解决办法,一种学名叫Prim 一种学名叫Kruskal。

Prim算法:

假设G={V,E}是连通图,其最小生成树T=(U,Et),Et是最小生成树中变得集合
两个集合U 和 Et 树的一个顶点u0放进U了集合U里面,每次选择与u集合中某顶点相连的最小的且
两个集合不重复的点,直到所有的点都被选完为止
void Prim(G,T)
{
    T=∅;
    U={w} //在集合u中加入任意一个顶点
    
    while((V-U)!=∅)
    {
        找到边(u,v)v∉U,且是u,v中最短的边
        T=T∪{(u,v)} 把这个边加入到边的集合T中
        U=U∪v 把顶点加入到顶点集
    }

}

Prim算法的时间复杂度为:O\left ( v^{2} \right ),不依赖于边,它主要在于点,所以适合求解边稠密的图的最小生成树

Kruskal算法:

卡尔斯算法的和普里姆算法的区别在于他是在选边,选n-1最短的边加入最小生成树,同时保证这n-1条边不形成回路就够了,代码我就不加了,一听都懂,这里主要讲一下怎么判断是否形成回路的方法,叫做拓扑排序

拓扑排序:

他的算法太简单,简单的我都不想列代码,懂逻辑,就一会用,就是选一个没有前驱的顶点把这个顶点输出就是拓扑排序的第一个结果,然后把这个顶点相连的边去掉,再找没有前驱的顶点,再把这个定点输出就是拓扑排序的第二个结果,然后再去掉相关的边,知道网被清空,当没有不存在前驱的顶点就是成环了,你想想一个环里的每个节点是不是都有前驱,这就是判断形成回路的方法。

最短路径:

其实最短路径很简单,简单的我还是不想列代码,懂逻辑,就是写起来会麻烦一点,肯定能写出来,代码我就不列了,没意思还不好理解,我直接用画图板给大家画个图,然后上个数组你们肯定就看明白了

 图中是一到五五个顶点,这是一个有向网。从第一个顶点出发 到其它每个顶点的最短路径数组变化如下,仔细看看怎么变的,你就懂什么是最短路径了 ,F代表无穷大,不相连第一行是四个顶点的序号)

2        3       4        5

10      F        5        F(更新与第一个节点相连最近的节点是4)1-4距离为5

8        14        5        7(更新与第四个节点相连的最近节点第五个 因为第一与第四相连,如果四与其他相连且比原来的距离短,那么就用这个距离替换,不比他短就不替换)1-4-5        距离为7

8         13        7        5(更新与第五个节点相连的节点 因为不存在所以不变,因为8比9小所以选第二个节点)1-4-2        距离为8

8        9        7        5(更新与第二个节点相连的节点 因为不存在所以不变)只剩下第三个节点没有算1-4-2-3离为9

 FLOYD:

我简单提一句,你现在可能学不明白,你可以试着去学动态规划,那个要学明白了,你floyd就很轻松,但是floyd本身说简单也简单,难点在于这种动态规划这种思路,他就是用一个二维矩阵a[i][j]

去存储顶点i到j的距离,i除了直接到j可以从i 到 w 到w 到k,遍历与ij相关的w看呢能不能利用矩阵和已知更新距离ij最终最后一列就是ij的距离,当然可嫩有多重结果,只不过最后一行肯定是一种。具体问题具体分析吧。我说他难,难在你是怎么想到的用矩阵这么存的,那个动态规划找到问题的思路是怎么做的,那个才是难点。但是对于刚学数据结构的人是想不到这一层,也就是后来学了算法,我才对FLOYD有了更深的理解。

关键路径:

关键路径这个问题很简单,你知道两个概念就完事儿了,最早开始时间和最迟发生时间。因为所有的时间走完了肯定有一套步骤耗时最或者是最长之一其余的那些短的就可以晚点发生,但是最长的不能晚,因为她一晚了,整个工期就被无理由拖长了。不能晚的就是关键路径,换句话说关键路径的最早开始时间和最迟发生时间是一样的,关键路径有几条有什么性质也不重要,重要的是你知道非关键路径的减少不会影响工期就足够了,关键路径的时间减少很容易出问题,因为他可能不再是关键路径了,而且关键路径也不唯一,并不能说加快一条路径上的关键活动就可以缩短工期。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天开心7788665544

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值