挑战408——数据结构(24)——图的存储与矩阵压缩
图的存储方式
在实践中,存储图最常见的策略是:
- 将每个节点的连接存储在邻接列表中。
- 将整个图形的连接存储在邻接矩阵中。
用邻接链表来表示图之间的关系
在图中表示连接的最简单方法是在每个节点的数据结构中存储与其连接的节点的列表。该结构称为邻接列表。 例如,在航空公司图表中
每一个节点连接的相邻节点构成一个邻接表,就像这样:
现在我们来讲将上图的内容抽象化:
假设我们现在有个有向图:
根据定义,我们将每个节点的连接存储在邻接列表中。对于节点 1 2 3 4 5,有以下连接表(比如与节点1相连的节点有2和5,他们之间先后顺序可以是任意的):
当图为有向图的时候,按箭头的方向给出连接表即可:
使用邻接矩阵表示节点间的关系
虽然临接表提供了一种表示图形中连接的便捷方式,但是当操作需要搜索与节点关联的节点时,它的效率可能会低下。 例如,如果使用邻接列表表示,则确定是两个节点是否相互连接需要O(D)时间,其中D表示节点之间的度。 如果图中的节点都具有少量邻居,则搜索该列表的成本很小。 但是,如果图中的节点往往具有大量邻居,则此成本变得很高。
如果效率成为一个很关心的问题,那么我们可以通过在称为邻接矩阵的二维数组中表示节点之间的花费(即权重),该数组显示哪些节点已连接。 航空公司图的邻接矩阵如下所示:
通常空白部分我们用∝表示,意思是两个节点之间没有直接关联。
特别的,当图为无向图是,邻接矩阵是对称的(如上图所示),因为每个相邻节点之间有相互的关系。(这个时候,如果矩阵很大,可以采取压缩矩阵的方式将矩阵压缩。矩阵的压缩后面会提到)
一般的,如果无向图中两个节点相互连接,那么我们就记为1,否则记为0,。这个时候我们就可以得出只有0 1 的矩阵。
下面来加班一些具体的实例:
上图中,第一行表示的是,对于节点1,它与自己本身不相连,记为0,与节点2相连记为1。与节点3不相连记为0,与节点4相连记为1,与节点5不相连记为0.由此得出矩阵的第一行应该是0 1 0 1 0.其他行以此类推。
对于有向图,可以按上述的逻辑进行推算:
还有一种图,叫做带权图。如下:
所谓权,就是节点之间相互到达需要的花费。这时候我们的邻接矩阵记录的就是权的大小。当节点之间的距离需要逆向而行或者非直接连接的时候,这是我们表示距离不可达,即无穷。比如第一行,节点1到自己本身没有箭头可指,因此是无穷。而节点1到节点2需要花费5.而节点1到节点三不是直接相连的,也记为无穷。由此得出邻接矩阵每行的数据。
要使用邻接矩阵,必须将每个节点与其索引号相关联,该索引号指定该表中与该节点对应的列或行号。 作为图的具体结构的一部分,实现需要为图中的每个节点分配一行和一列的二维数组。 数组的元素是布尔值。 如果matrix [start] [finish]中的条目为真,则图中就有表示 start→finish的节点间的关系。
两种表示方式的空间复杂度
就执行时间而言,使用邻接矩阵比使用邻接列表快得多。 但另一方面,矩阵需要O(N^2)存储空间(如果不压缩),其中N是节点的数量。 对于大多数图形,邻接列表表示在空间方面往往更有效。 在邻接列表表示中,每个节点都有一个连接列表,在最坏的情况下,它的长度将是Dmax,其中Dmax是图中任一节点的最大度数,即从单个节点出发的最大连接数。 因此,邻接列表的空间成本是O(N×Dmax)。 如果大多数节点彼此连接,则Dmax将相对接近N,这意味着表示连接的成本对于两种方法是相近的的。 另一方面,如果图形包含许多节点但相对较少的互连(称为稀疏矩阵),则邻接列表表示可以节省相当大的空间。
除此之外,图的存储方式还有邻接多重表和十字链表法。
特点:
- 由上述可以看出,通常我们可以将边的类型定义为0或者1的枚举类型。
- 无向图的邻接矩阵一定是对称矩阵(且唯一),对于规模较大的矩阵可以进行压缩。
- 当图的边数较多的时候,适合采用邻接矩阵表示。反之当边数较少的时候适合采用邻接链表。
矩阵压缩
我们刚刚提到 ,对于规模较大的对称矩阵可以考虑进行压缩。那么什么是压缩呢?
压缩存储,是指为多个值相同的元素只分配一个存储空间,并且对于0元素不分配空间。其目的就是要节省存储空间。我们通常可以对一些特殊的矩阵进行压缩。下面着重介绍三种(只考虑方阵)。
对称矩阵
定义:对于一个n阶方阵A[1…n][1…n],中的任意一个元素Aij,满足:
Aij = Aji //其中 1 <=1,且j < = n
那么就称为对称矩阵。通常可以将元素分成3大部分:上三角区,下三角区,对角线。:
图中,A(n,1) = A(1,n), A(n,2) = A(2,n)
因此我们可以考虑直接用一维数组来存储一半的数据即可,另外一半的数据可以由之前的一半交换下标所得。假设我们新建一个数组B来存储:
其中:
1+2+3+4+....(i-1) \\定位数组所在的行
(j -1)\\定位数组所在的列
因此得出如下关系(两个式子实际上是i,j 位置互换):
三角矩阵
这种矩阵的特点是,上三角区的所有元素均为同一常量。其存储思想与对称矩阵类似。先存完下三角和主对角线的元素后,再存对角线上方的常量一次。
-
下三角矩阵
对于下三角,存储方式完全与对称矩阵的下三角相同:
对于下三角,由于存的都是常数,第一行有一个元素,第二行有两个元素,第三行有3个元素,那么第n行的n个元素(存常数),他所在的位置就是一个等差数列求和的过程。:
因此其在内存中的存储为:
也就说,常数项只占用一个存储位置。 -
上三角矩阵
按刚刚的方法可以很快得出位置关系:
从而得出存储图:
三对角矩阵
对于n阶方阵A中的任一元素a(i,j),如果| i - j | >1(即行-列的绝对值大于1)的时候,有 a(i,j)= 0,那么矩阵A就被称为三对角矩阵。在三对角矩阵中,所有的主要元素都集中在以主对角线为中心的3条对角线中,其他区域的数值均为0.
采用压缩存储,其原理为,先将3条对角线上的元素,以按行优先方式存储在一维数组B中,且B0为a(1,1)。那么存储结构为:
因此,A中三条对角线的主元素A(i,j)的下标为:
k = 2i + j -3
反推,已知元素位置为k,那么其下标为:
i = [(k+1) /3 +1] //中括号的意思是向下取整
j = k - 2i +3 //这里是求主元素下标公式的逆推
稀疏矩阵
这个很容易理解,就不用什么官方定义了,就是矩阵中,非零元素非常少的矩阵。(比如一个100 x 100的矩阵中,只有不到100个非零元素)。这种矩阵就是稀疏矩阵。
对于这种矩阵的存储也很好办,只要我们存储非零元素就好了。如果将行,列,值三者组成一个三元组,那么三元组的格式就是(行,列,值)。将其存在数组下标就很好办:
注意,这里我们是指定了特定的地方存储,那么矩阵压缩存储后,就失去了随机存储的特性。
按行存储和按列存储
按行存储
这个也不看具体的公式定义,先上图:
上图是个二维数组,一个矩阵。按行存储,意思就是先把行从左往右读,每读到一个就存起来:
这样就可以得出一个结论,元素a(i,j)所在的位置为:
i x (h +1) +j //h表示列的最大值,从0开始算
//i x (h +1) 可以定位到a(i,j)开始的位置a (i)
//再加上所在的列,可以计算出具体的位置。
用LOC(a(0,0))表示数组在内存中表示的物理位置,L表示每个元素的存储空间,那么a(i,j)的物理位置为(从0开始算):
按列存储
原理同上,放图自己分析: