【数据结构】图

一.图的定义

1.定义:

图G由顶点集V和关系集E组成,记为G=(V,E)

注:图可以没有边,但不能没有顶点。

2.图的分类:

图的顶点数为n,边数为e

有向图:$0\leqslant e \leqslant n(n-1)$

无向图:$0\leqslant e\leqslant \frac{n(n-1)}{2}$

若:$e=n(n-1)$,则为有向完全图。

3.图的基本概念:

(1)权:与图的边或弧相关的数

(2)网:边或弧上带有权值的图

(3)顶点的度TD(V)

对于有向图来说:TD(V)=ID(V)+OD(V)

(4)生成树

设无向图G是含有n个顶点的连通图,则图G的生成树是含有n个顶点,且只有n-1条边的连通子图

二.图的存储

1.邻接矩阵

1.1

设图G=(V,E)有n个顶点,e条边。

则G的邻接矩阵为n阶方阵A,其中A满足:

$ A[i,j]= \begin{cases} 1 & if (v_i,v_j) or <v_i,v_j> \in E \\ 0 & if (v_i,v_j) or <v_i,v_j> \notin E \\ \end{cases} $

1.2 邻接矩阵的特点:

(1)判定两个顶点$v_i,v_j$之间是否关联,只需要判断$a_{ij}$是否为1

(2)容易求顶点的度

对于无向图来说:

$TD(v_i)=\sum_{j=1}^{n}{a_{ij}}$$=\sum_{i=1}^{n}{a_{ji}}$

对于有向图来说:

$TD(v_i)=OD(v_i)+ID(v_i)=\sum_{j=1}^{n}a_{ij}+\sum_{i=1}^{n}a_{ji}$

1.3 带权图的邻接矩阵

$ A[i,j]= \begin{cases} w_{ij} &if (v_i,v_j)or <v_i,v_j>\in E (i\not\neq j)\\ \infty & if (v_i,v_j)or <v_i,v_j>\notin E (i\not\neq j)\\ 0 & (i=j) \\ \end{cases} $

1.4 不带权的无向图的代码实现

template 
class MGraph{
private:
    T vertex[MAX_SIZE];//每个顶点中的信息
    int arc[MAX_SIZE][MAX_SIZE];//存储邻接矩阵的二维数组
    int n,e;//图的顶点数和边数
public:
    MGraph(int n,int e,T v[]);
    
};
template 
MGraph::MGraph(int n,int e,T v[]){
    int vi,vj;
    this->n=n;
    this->e=e;
    for(int i=0;i>vi>>vj;
        arc[vi][vj]=1;
        arc[vj][vi]=1;
    }
}

2.邻接表 

(1)结构数组(存储图中的顶点)

vertex firstedge

(2)邻接点域(每个顶点的邻接点信息)

adjvex next

2.1无向图邻接表的特点:

1)n个顶点,e条边的无向图,需要n个表头结点,2e个链表结点。

2)顶点$v_i$的度$TD(v_i)$=对应firstedge中结点数

2.2有向图邻接表的特点:

1)n个顶点,e条边的有向图,需要n个表头结点,e个链表结点。

2)顶点$v_i$的出度=对应firstedge中结点数;即:求某个顶点的出度容易,求入度难。

2.3有向图的逆邻接表的特点:

1)n个顶点,e条边的有向图,需要n个表头结点,e个链表结点。

2)顶点$v_i$的入度=对应firstedge中结点数;即:求某个顶点的入度容易,求出

度难。

2.4 有向图的无权邻接表代码实现:

struct vnode{
    int pos;
    struct node *next;
};
template 
struct vertexNode{
    T vertex;
    struct vnode *firstedge;
};

template 
class AlGraph{
private:
    struct vertexNode  vertex[MAX_SIZE];
    int n;
    int e;
public:
    AlGraph(int n,int e,T v[]);
    ~AlGraph();
};
template 
AlGraph::AlGraph(int n,int e,T v[]){
    int vi,vj;
    struct vnode *p;
    this->e=e;
    this->n=n;
    for(int i=0;i>vi>>vj;
        p=new struct vnode;
        p->pos=vj;
        p->next=vertex[i].firstedge->next;
        vertex[i].firstedge=p;
    }
}
template 
AlGraph::~AlGraph(){
    struct vnode *p;
    for(int i=0;inext;
                delete p;
                p=vertex[i].firstedge;
            }
        }
    }
}

2.5 十字链表 (orthogonal list)

由于邻接表求出度容易,而逆邻接表求入度容易,可以将其改造为十字链表,将邻接表和逆邻接表结合起来。

1)顶点结点:

data firstin firstout

data:存放顶点的有关信息

firstin:指向以该顶点为弧头的第一个弧结点

firstout:指向以该顶点为弧尾的第一个弧结点

2)弧结点:

tailvex headvex hlink tlink

tailvex:指示该弧的弧尾顶点

headvex:指示该弧的弧头顶点

hlink:指向弧头相同的下一条弧

tlink:指向弧尾相同的下一条弧

通过hlink将弧头相同的弧连在一个链表上

通过tlink将弧尾相同的弧连在一个链表上

3)十字链表的特点:

顶点结点数等于顶点数

弧结点数等于弧的条数 

4)求顶点$V_i$入度和出度:

出度:从顶点的firstin出发,沿着弧结点的hlink所经过的弧结点数

入度:从顶点的firstout出发,沿着弧结点的tlink所经过的弧结点数

#include 
using namespace std;

const int MAX_SIZE=10;
//弧结点
struct arc{
    int tailvex;
    int headvex;
    struct arc *hlink;
    struct arc *tlink;
};
//顶点结点
struct vertex{
    int data;
    struct arc *firstin,*firstout;
};

class OrthGraph{
private:
    struct vertex v[MAX_SIZE];
    int vertexNum;
    int arcNum;
public:
    OrthGraph(int n,int e);
    ~OrthGraph();
};
OrthGraph::OrthGraph(int n,int e){
    vertexNum=n;
    arcNum=e;
    int vi,vj;
    struct arc *p;
    for(int i=0;i>vi>>vj;
        p=new struct arc;
        p->headvex=vi;
        p->tailvex=vj;
        p->hlink=NULL;
        p->tlink=NULL;
        if(v[vi].firstin==NULL){
            p->hlink=v[vi].firstin;
            v[vi].firstin=p;
        }
        else{
            struct arc *q=v[vi].firstin;
            while(q->hlink){
                q=q->hlink;
            }
            q->hlink=p;
        }
        if(v[vj].firstout==NULL){
            p->tlink=v[vj].firstout;
            v[vj].firstout=p;
        }
        else{
            struct arc *q=v[vj].firstout;
            while(q->tlink){
                q=q->tlink;
            }
            q->tlink=p;
        }
    }
}
OrthGraph::~OrthGraph(){
    struct arc *p,*q;
    for(int i=0;ihlink;
            delete p;
            p=v[i].firstin;
        }
        p=v[i].firstout;
        while(p){
            v[i].firstout=p->tlink;
            delete p;
            p=v[i].firstout;
        }
    }
}

3.图的邻接矩阵和邻接表的比较 

(1)一个图的邻接矩阵表示是唯一的,邻接表不唯一。

(2)邻接表(逆邻接表)的空间复杂度为S(n,e)=O(n+e)。

(3)稀疏图选取邻接表;稠密图选择邻接矩阵。

(4)判断边,邻接矩阵比邻接表容易;求边数,邻接矩阵的时间复杂度为O($n^2$),邻接表中的复杂度为O(n+e)

(5)求有向图顶点的度,邻接矩阵更加方便;因为,邻接表求出度容易,而逆邻接表求入度容易。

三.图的遍历

1.定义:

从图中某个顶点出发,沿路径使图中每个顶点被访问且仅被访问一次的过程。

  • 深度优先搜索
  • 广度优先搜索

2.深度优先搜索DFS(depth-first-search)

 2.1 深度优先搜索遍历图的过程为:

1)访问指定的某顶点v,将v作为当前顶点;

2)访问当前顶点未被访问过的邻接点,并将该邻接点作为当前顶点;

3)重复2),直到当前顶点的所有邻接点都被访问过;

4)沿搜索路径回退,退到尚有邻接点未被访问过的某结点,将该结点作为当前结点,重复2),直到所有顶点都被访问完为止。

2.2 深度优先遍历的递归算法代码实现:

template 
void MGraph::DFS(int start){
    static bool visted[n]={false};
    int w;
    visted[start]=true;
    for(int i=0;i

2.3 深度优先遍历的非递归算法代码实现:

template 
void MGraph::DFS_non_recursion(int start){
    int w,i;
    stack s;
    static bool visited[n]={false};
    
    //源点压栈
    s.push(start);
    
    while(!s.empty()){
        w=s.top();//弹栈
        s.pop();
        visited[w]=true;//访问过
        cout<

3.广度优先遍历BFS(breadth- first-search)

3.1 广度优先遍历图的过程为:

1)首先访问指定顶点$v_0$,将$v_0$作为当前顶点;

2)访问当前顶点的所有未访问过的邻接点,并依次将访问的这些邻接点作为当前顶点;

3)重复2,直到所有顶点被访问为止。

3.2 广度优先遍历图的代码实现:

template 
void MGraph::BFS(int start){
    int v,w,i,count=0;
    queue s;
    static bool visited[n]={false};
    
    v=start;
    visited[v]=true;
    cout<

四.最小生成树

 1.引例:

N台计算机之间建立通讯网:

  • n台计算机中的任意两台能通过网络进行通讯。
  • 使总的通讯距离最短

2.最小生成树(Minimal Spanning Tree):

 1)找到一个生成树

2)确保生成树中边的权值和最小

3.MST性质:

假设G=(V,E)是一个无向连通网,U是顶点集V的一个非空子集。若(u,v)是一条具有最小权值的边,其中u属于U,v属于V-U,则必存在一棵包含边(u,v)的最小生成树。

 4.Prim算法

基本思想:设G=(V,E)是具有n个顶点的连通网,T=(U,TE)是G的最小生成树,T的初始状态为U={$u_0 $},TE={},重复执行下述操作:找一条代价最小的边(u,v)并入合集TE,同时v并入U,直至U=V。

5.Prim算法的代码实现

注:

如何将顶点加入到集合U中?

shortEdge[i].lowcost=0;

template 
void MGraph::Prim(int start){
    struct edge shortEdge[n];
    int i,j,k;
    
    shortEdge[start].lowcost=0;//起点放入集合U中
    
    //初始化shortEdge数组
    for(i=0;i0&&shortEdge[j].lowcost==-1){//新加入的顶点有边并且j表示的顶点集合U到达不了
                shortEdge[j].adjvex=k;
                shortEdge[j].lowcost=arc[k][j];
            }
            else if(arc[k][j]>0&&shortEdge[j].lowcost>0&&arc[k][j]>shortEdge[j].lowcost){
                shortEdge[j].lowcost=arc[k][j];
                shortEdge[j].adjvex=k;
            }
        }
    }
}

template 
int MGraph::minEdge(struct edge shortEdge[]){
    int i,j,min;
    for(i=0;i0){
            min=i;
            break;
        }
    }
    for(i=0;i0&&shortEdge[i].lowcost
void MGraph::outputMST(int k,struct edge shortEdge[]){
    cout<<"("<

伪代码:

1.根据源点对shortEdge数组进行初始化。

2.进入循环

3.找到shortEdge数组中权值最小的边对应的顶点

4.将该顶点加入到集合U中 

5.由于集合U中新加入了顶点,则调整shortEdge数组

6.时间复杂度分析:

        外重循环是将每一个顶点加入到集合U中,内层循环是根据新加入的顶点来调整shortEdge数组的值。

        所以时间复杂度为$O(n^2)$

7.Kruskal算法:

逐步给生成树T中添加不和T中的边构成回路的当前最小代价边。

思想:

1.置生成树T的初始状态为T=$(V,\varnothing)$

2.当T中边数

从E中选取代价最小的边(v,u)

若:(v,u)与已有边集中的边没有形成回路,则将边(v,u)并入到T的边集中

否则,舍去该边,选择下一条代价最小的边

 问题的关键在于:

如何判断是否形成回路\rightarrow如何判别被考察边的两个顶点是否位于两个连通分量\rightarrow所属的树是否有相同的根节点

Kruskal算法实质上是使生成树以一种随意的方式生长,初始时,每个顶点构成一颗生成树。然后,每生长一次,就将两棵树合并,到最后合并成一棵树。

因此,可以设置一个数组parent[n],元素parent[i]表示顶点i的双亲结点,初始时,parent[i]=-1,表示顶点i没有双亲,即该结点是所在生成树的根节点;对于边(u,v),设vex1和vex2分别表示两个顶点所在树的根结点,如果vex1!=vex2,则顶点u和v必位于不同的连通分量,令parent[vex2]=vex1,实现将两棵树合并。

struct edge{//V和U-V之间的边
    int adjvex;
    int lowcost;
};

struct EdgeType{
    int from,to;
    int weight;
};

struct EdgeGraph{
    int vertex[MAX_SIZE];
    struct EdgeType edge[MAX_SIZE];
    int vertexNum,edgeNum;
};

void outPutMST(EdgeType edge){
    cout<<"("<-1){
        t=parent[t];
    }
    return t;
}

void Kruskal(struct EdgeGraph G){
    //为了确保新加入边不会形成回路,则其两个顶点位于不同的连通分支中,那么问题转化为
    //如何确定其位于不同的连通分支
    //答案是,用一个parent数组来记录一下根结点
    int i,num=0,vex1,vex2;
    int parent[G.vertexNum];
    
    for(i=0;i

 8.Kruskal与Prim算法的比较

Prim算法:

1)以连通为主

2)选择保证连通的代价最小的邻接边(n-1)次

3)时间复杂度与边无关  $O(n^2)$

4)适合求边稠密的最小生成树

Kruskal算法:

1)以最小代价边为主

2)添加不形成回路的当前最小代价边

3)算法时间复杂度与边有关 $O(elog_2{e})$

4)适合求边稀疏的最小生成树

五.最短路径问题

1.最短路径问题的描述:

在有向图中,寻找从源点到其余各个顶点或者每一对顶点之间的最短带权路径的运算

2.Dijkstra算法(单源点最短路径问题):

主要思想:

给定源点v,需要获取v到其余顶点的最短路径。

需要一个辅助数组dist,记录源点到其他顶点之间的距离。

依次加入顶点,每加入一个顶点,考察源点到其他顶点之间的距离是否缩短,如果缩短,更新dist和path数组。

1)由于需要频繁读取边的信息,所以采用邻接矩阵的存储方式

2)使用dist数组,这个数组用于记录从源点到每个终点的最短路径长度,初始值用arc[v][i]初始化,自己到自己记为0,到达不了记为$\infty$,其余记为权值。

3)path数组用于记录路径,初始值全部用源点来初始化,含义为从某个顶点到该顶点。如果新加入的顶点使距离变短,则更新path数组。path[i]存放i的直接前驱

4)s数组存放源点和已生成的终点,其初态为只有一个源点v。s[i]=1表示i顶点被存放进s数组中。

伪代码:

1.初始化数组dist、path和s;

2.while(s中的元素个数

        2.1 在dist[n]中求最小值,其下标为k;

        2.2 输出dist[i]和path[j];

        2.3 修改数组dist和path;

        2.4 将顶点vk添加到数组s中

3.Floyd算法(多源点最短路径问题):

 如何求每一对顶点之间的最短路径?

每次以一个顶点为源点,调用Dijkstra算法n次 时间复杂度为$O(n^3)$

另一种求解思路:Floyd算法

基本思想:

1)用邻接矩阵来存储边。

2)外层循环:依次加入顶点,考察加入顶点后,对于路径长度是否有变化。

3)加入一个顶点,考察n个顶点

4)对于每一个顶点来说,需要考察其与n个顶点之间的距离,需要修改距离,则更新arc[][]数组,并且更新路径数组

算法时间复杂度为:$O(n^3)$

六.AOV网与拓扑排序

1.AOV网

在一个表示工程的有向图中,用顶点表示活动,用表示活动之间的优先关系。这样的有向图为顶点表示活动的网,我们称为AOV网(Activity On Vertex Network)

2.AOV网的特点:

(1)AOV网中的弧表示活动之间存在的某种制约关系

(2)AOV网中不能出现回路

3.拓扑排序:

拓扑序列:设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列$v_1,v_2,...,v_n$称为一个拓扑序列,当且仅当满足下列条件:若从顶点$v_i$$v_j$有一条路径,则在顶点序列中顶点$v_i$必在$v_j$之前。

拓扑排序:对一个有向图构造拓扑序列的过程 

注:

1)拓扑排序是一种对非线性结构的有向图进行线性化的重要手段。

2)可以检查有向图中是否存在回路。

3)AOV网可以解决如下问题:

  • 判定工程的可行性。如果有回路,工程无法结束。
  • 确定各项活动在整个工程中执行的先后顺序。 

4.拓扑排序的基本思想:

(1)从AOV网中选择一个没有前驱的顶点并且输出;

(2)从AOV网中删除该顶点,并且删去所有以该顶点为弧头的的弧;

(3)重复上述两步,直到全部顶点都被输出,或AOV网中不存在没有前驱的顶点。

注:

1)没有前驱的顶点即:入度为0的顶点

2)删除以它为弧头的弧即:将该结点所有的直接后继结点的入度-1

由此:

选取存储结构时:

  • 容易得到各顶点的入度
  • 容易寻找到该顶点的直接后继

使用邻接表作为AOV网的存储结构,并在头结点中增加一个存放顶点入度的域indegree

我们还需要一个栈来存储所有没有前驱的顶点

5.拓扑排序的伪代码

1.栈S初始化,累加器count初始化;

2.扫描顶点表,将没有前驱的顶点压栈;

3.当栈非空时循环:

        3.1  保存弹栈元素,输出 ,累加器加一;

        3.2 将顶点的各个邻接点的入度减一;

        3.3 将新的入度为0的顶点入栈;

4.如果count

七.AOE网与关键路径

1.AOE网的定义(Activity On Edge NetWork):

        在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动边上的权值表示活动的持续时间,称这样的有向图叫做边表示活动的网,简称AOE网。AOE网中没有入边的顶点称为始点(或源点),没有出边的顶点称为终点(或汇点)。

2.AOE网的性质:

1.只有在某顶点所代表的事件发生后,从该顶点出发的各活动才能开始;

2.只有在进入某顶点的各活动都结束,该顶点所代表的事件才能发生。

3.AOE网可以解决的问题:

1)可以知道完成整个工程至少需要多少时间

2)为缩短完成工程所需的时间,应当加快哪些活动,即:找出哪些活动是影响整个工程进展的关键。

4.关键路径与关键活动:

1)关键路径:

在AOE网中,从始点到终点具有最大路径长度(该路径上各个活动所持续的时间之和)的路径称为关键路径。

2)关键活动:

关键路径上的活动称为关键活动

5.术语:

1)事件的最早发生时间ve[k]

从始点开始到顶点$v_k$的最大路径长度,这个长度决定了所有从顶点$v_k$发出的活动能够开工的最早时间。

\left\{ \begin{aligned} &ve[1]=0\\ &ve[k]=max\{ve[j]+len<v_j,v_k>\}(<v_j,v_k>\in p[k])\\ \end{aligned} \right.

p[k]表示所有到达$v_k$的有向边的集合。

2)事件的最迟发生时间vl[k]

vl[k]是指在不推迟整个工期的前提下,事件$v_k$允许的最晚发生时间

$ \left\{ \begin{aligned} &vl[n]=ve[n] \\ &vl[k]=min\{vl[j]-len<v_k,v_j>\} (<v_k,v_j>\in s[k])\\ \end{aligned} \right. $

从后往前推算结点的vl值

3)活动的最早开始时间ee[i]

若活动$a_i$是由弧$<v_k,v_j>$表示,则活动$a_i$的最早开始时间等于事件$v_k$的最早发生时间

即:ee[i]=ve[k]

4)活动的最晚开始时间el[i]

若活动$a_i$是由弧$<v_k,v_j>$表示,则活动$a_i$的最晚开始时间要保证事件$v_j$的最迟发生时间不拖后

即:$el[i]=vl[j]-len<v_k,v_j>$

6.关键活动的确定

最后计算各个活动的时间余量el[k]-ee[k],时间余量为0即为关键活动 

7.关键路径算法的伪代码

1)从源点$v_0$出发,令ve[0]=0,按拓扑排序求其余各顶点的最早发生时间ve[i];

2)如果得到的拓扑排序中顶点个数小于AOE网中的顶点数,则说明网中存在环,不能求关键路径,算法终止;否则执行步骤3;

3)从终点v_{n-1}出发,令vl[n-1]=ve[n-1],按照逆序拓扑排序求其余各顶点的最迟发生时间vl[i];

4)根据各顶点的ve和vl值,求每条有向边的最早开始时间ee[i]和最迟开始时间el[i];

5)若某条有向边$a_i $满足条件ee[i]=el[i],则$a_i $为关键活动。

8.缩短工程时间

1)适当提高对应关键路径上的关键活动的速度,保证不改变AOE网的关键路径的前提下。

2)同时提高AOE网上的关键路径上关键活动的速度。

9.关键路径的代码实现

八.贪心算法

1.贪心算法的定义:

贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解

2.贪心算法的步骤:

1.建立数学模型来描述问题;

2.把求解的问题分成若干个子问题;

3.对每一个子问题求解,得到子问题的局部最优解;

4.把子问题的局部最优解合成原来问题的解。

3.贪心算法的基本要素:

1)贪心选择性质:

整体最优解可以通过一系列局部最优的选择得到

2)最优子结构性质:

子问题的最优解包含在原问题的最优解中

4.贪心算法的应用——活动安排问题:

你可能感兴趣的:(数据结构)