数据结构基础

本文将介绍常用的数据结构。慢慢完善。

先进后出

队列

先进先出

二叉树

树形结构,每个节点最多只能有两个子分支

遍历等等

  • 由顶点的有穷非空集合和顶点之间的边的集合组成
  • 图中和边有关的数叫做权,带权的图称作 网
  • 无向图中,与顶点 V 相关联的边的数目称作 V 的
  • 有向图中,以顶点 V 为起点的弧的数目叫做 V 的 入度;以顶点 V 为终点的弧的数目叫做 V 的 出度

存储结构

邻接矩阵

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

设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:

看一个实例,下图左就是一个无向图。

从上面可以看出,无向图的边数组是一个对称矩阵。所谓对称矩阵就是n阶矩阵的元满足aij = aji。即从矩阵的左上角到右下角的主对角线为轴,右上角的元和左下角相对应的元全都是相等的。

从这个矩阵中,很容易知道图中的信息。

(1)要判断任意两顶点是否有边无边就很容易了;

(2)要知道某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;

(3)求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点;

而有向图讲究入度和出度,顶点vi的入度为1,正好是第i列各数之和。顶点vi的出度为2,即第i行的各数之和。

若图G是网图,有n个顶点,则邻接矩阵是一个n*n的方阵,定义为

这里的wij表示 (vi,vj) 上的权值。无穷大表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值。下面左图就是一个有向网图,右图就是它的邻接矩阵。

邻接表

邻接矩阵是不错的一种图存储结构,但是,对于边数相对顶点较少的图,这种结构存在对存储空间的极大浪费。因此,找到一种数组与链表相结合的存储方法称为邻接表。

邻接表的处理方法是这样的:
(1)图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过,数组可以较容易的读取顶点的信息,更加方便。
(2)图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以,用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。
例如,下图就是一个无向图的邻接表的结构。

从图中可以看出,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。

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

十字链表

对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才知道,反之,逆邻接表解决了入度却不了解出度情况。下面介绍的这种有向图的存储方法:十字链表,就是把邻接表和逆邻接表结合起来的。

重新定义顶点表结点结构,如下所示。

其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点,firstout表示出边表头指针,指向该顶点的出边表中的第一个结点。

重新定义边表结构,如下所示。

其中,tailvex是指弧起点在顶点表的下表,headvex是指弧终点在顶点表的下标,headlink是指入边表指针域,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边。如果是网,还可以增加一个weight域来存储权值。

比如下图,顶点依然是存入一个一维数组,实线箭头指针的图示完全与邻接表相同。就以顶点v0来说,firstout指向的是出边表中的第一个结点v3。所以,v0边表结点hearvex = 3,而tailvex其实就是当前顶点v0的下标0,由于v0只有一个出边顶点,所有headlink和taillink都是空的。

重点需要解释虚线箭头的含义。它其实就是此图的逆邻接表的表示。对于v0来说,它有两个顶点v1和v2的入边。因此的firstin指向顶点v1的边表结点中headvex为0的结点,如上图圆圈1。接着由入边结点的headlink指向下一个入边顶点v2,如上图圆圈2。对于顶点v1,它有一个入边顶点v2,所以它的firstin指向顶点v2的边表结点中headvex为1的结点,如上图圆圈3。

十字链表的好处就是因为把邻接表和逆邻接表整合在一起,这样既容易找到以v为尾的弧,也容易找到以v为头的弧,因而比较容易求得顶点的出度和入度。

而且除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的,因此,在有向图应用中,十字链表是非常好的数据结构模型。

这里就介绍以上三种存储结构,除了第三种存储结构外,其他的两种存储结构比较简单。

遍历

图的遍历和树的遍历类似,希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫图的遍历。

深度优先遍历

深度优先遍历也叫深度优先搜索(Depth First Search)。它的遍历规则:不断地沿着顶点的深度方向遍历。顶点的深度方向是指它的邻接点方向。

具体点,给定一图G=<V,E>,用visited[i]表示顶点i的访问情况,则初始情况下所有的visited[i]都为false。假设从顶点V0开始遍历,则下一个遍历的顶点是V0的第一个邻接点Vi,接着遍历Vi的第一个邻接点Vj,……直到所有的顶点都被访问过。

所谓的第一个是指在某种存储结构中(邻接矩阵、邻接表),所有邻接点中存储位置最近的,通常指的是下标最小的。在遍历的过程中有两种情况经常出现

  1. 某个顶点的邻接点都已被访问过的情况,此时需回溯已访问过的顶点。

  2. 图不连通,所有的已访问过的顶点都已回溯完了,仍找不出未被访问的顶点。此时需从下标0开始检测visited[i],找到未被访问的顶点i,从i开始新一轮的深度搜索。

看一个例子

从V0开始遍历

遍历分析:V0有两个邻接点V1和V2,选择下标最小的V1遍历。接着从V1开始深度遍历,V1只有邻接点V3,也就是没有选的:遍历V3。接着从V3开始遍历,V3只有邻接点V0,而V0已经被遍历过。此时出现了上面提到的情况一,开始回溯V1,V1无未被遍历的邻接点,接着回溯V0,V0有一个未被遍历的邻接点V2,新的一轮深度遍历从V2开始。V2无邻接点,且无法回溯。此时出现了情况二,检测visited[i],只有V4了。深度遍历完成。看到回溯,应该可以想到需要使用栈。

遍历序列是
V0->V1->V3->V2->V4。

广度优先遍历

广度优先遍历也叫广度优先搜索(Breadth First Search)。它的遍历规则:

  1. 先访问完当前顶点的所有邻接点。(应该看得出广度的意思)
  2. 先访问顶点的邻接点先于后访问顶点的邻接点被访问。

具体点,给定一图G=<V,E>,用visited[i]表示顶点i的访问情况,则初始情况下所有的visited[i]都为false。假设从顶点V0开始遍历,且顶点V0的邻接点下表从小到大有Vi、Vj…Vk。按规则1,接着应遍历Vi、Vj和Vk。再按规则2,接下来应遍历Vi的所有邻接点,之后是Vj的所有邻接点,…,最后是Vk的所有邻接点。接下来就是递归的过程…

在广度遍历的过程中,会出现图不连通的情况,此时也需按上述情况二来进行:测试visited[i]…。在上述过程中,可以看出需要用到队列。

举个例子,还是同样一幅图:

从V0开始遍历

遍历分析:V0有两个邻接点V1和V2,于是按序遍历V1、V2。V1先于V2被访问,于是V1的邻接点应先于V2的邻接点被访问,那就是接着访问V3。V2无邻接点,只能看V3的邻接点了,而V0已被访问过了。此时需检测visited[i],只有V4了。广度遍历完毕。

遍历序列是
V0->V1->V2->V3->V4。

参考文章数据结构之图(存储结构、遍历)

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×

keyboard_arrow_up 回到顶端