导读
大家好,很高兴又和大家见面啦!!!
数组相信大家都不陌生了,在C语言中我们就开始接触数组相关的知识点了,在学习指针时,我们进一步探讨了数组和指针的关系,等到我们开始学习线性表时我们才知道,原来数组这种存储方式为线性存储,并且它本身就是一种数据结构——顺序表。
经过这么长时间的学习,不得不说数组在计算机的知识体系中,还是处于一个比较重要的位置的。
今天的内容中,我们同样是要学习数组。和前面的学习不同,今天我们要学习的内容是如何将一个矩阵中的数据存放进数组。在今天的内容中,我们会详细的探讨一下数组和矩阵之间的关系。
一、数组与矩阵
在介绍数组和矩阵前,我们需要先复习一下数组的相关知识点;
1.1 数组
数组是由n(n>=1)个相同类型的数据元素构成的有限序列,每个数据元素称为一个数组元素,每个元素在n个线性关系中的序号称为该元素的下标,下标的取值范围称为数组的维界。
在之前的学习中,我们只知道数组有三要素:数组名、数组大小以及数组元素的数据类型。现在我们又新学了一个概念,那就是数组下标的取值范围的专业术语是维界。这些知识点因为都是大家比较熟悉的,我就不再展开赘述;
1.2 数组与线性表
在学习线性表时,我们介绍的第一种线性表就是顺序表,当时我们有提到过数组就是用来描述线性表的顺序存储结构的。因此数组可以看做是线性表的一种推广。
在学习数组时我们有学过,数组是有一维数组、二维数组甚至是多维数组的,而一维数组我们就可以将其视为是一个线性表,二维数组则可以看做其元素是定长线性表的一种线性表,以此类推。
数组这种结构在创建时是需要在内存中申请一块连续的存储空间的,因此它一旦被定义,其维数和维界则不可再被修改。因此,对于数组而言,除了进行初始化和销毁外,数组只会有存取元素和修改元素的操作。
1.3 数组的存储结构
数组中的元素在存储时在内存中是进行从低地址到高地址的顺序进行连续存放的,因此在知道每个元素所占空间大小后,我们就可以通过数组首元素地址与第k个元素的下标来获取该元素在内存空间中的地址。
假设首元素地址为LOC(arr[0])
,数组元素所占空间大小为sizeof(Elemtype)
,下面我们分别来看一下一维数组和二维数组在内存中的存储以及如何获取数组中的各个元素的地址;
- 一维数组
arr[m]
在内存中的存储如下所示:
如果用L来表示元素类型所占空间大小,则存储结构关系式可以写成:
L
O
C
(
a
r
r
[
i
]
)
=
L
O
C
(
a
r
r
[
0
]
)
+
i
×
L
(
0
<
=
i
<
m
)
LOC(arr[i]) = LOC(arr[0]) + i × L(0<=i<m)
LOC(arr[i])=LOC(arr[0])+i×L(0<=i<m)
- 二维数组
arr[m][n]
在内存中的存储如下所示:
在这个二维数组中,它是由m
个一维数组组成,在每个一维数组中都有n
个元素,二维数组中的元素的地址之间的关系为:
- 同一个一维数组中相邻两个元素之间的地址相差
1 × sizeof(Elemtype)
; - 每个一维数组从首元素到尾元素之间的地址相差
(n - 1) × sizeof(Elemtype)
; - 两个相邻的数组之间的地址相差
n × sizeof(Elemtype)
对于第arr[i][j]
这个元素而言,我们在计算它的地址时的思路应该是:
- 先计算一维数组之间的地址差值:下标为
i
这个一维数组arr[i]
,它与二维数组中的首元素arr[0]
这个数组之间的地址相差(i × n) × sizeof(Elemtype)
; - 再计算一维数组中元素之间的地址差值:在
arr[i]
这个一维数组中,元素arr[i][j]
与首元素arr[i][0]
之间又相差j × sizeof(Elemtype)
个地址; - 最后计算该元素的地址:
arr[i][j]
这个元素在内存中的地址为 首元素地址 + 一维数组之间的地址差值 + 一维数组中元素之间的地址差值 首元素地址 + 一维数组之间的地址差值 + 一维数组中元素之间的地址差值 首元素地址+一维数组之间的地址差值+一维数组中元素之间的地址差值也就是LOC(arr[0]) + (i × n) × sizeof(Elemtype) + j × sizeof(Elemtype)
,最后合并同类项可以得到LOC(arr[0]) + (i × n + j) × sizeof(Elemtype)
;
如果用L来表示元素类型所占空间大小,则存储结构关系式可以写成:
L
O
C
(
a
r
r
[
i
]
[
j
]
)
=
L
O
C
(
a
r
r
[
0
]
)
+
(
i
×
n
+
j
)
×
L
(
0
<
=
i
<
m
,
0
<
=
j
<
n
)
LOC(arr[i][j]) = LOC(arr[0]) + (i × n + j) × L(0<=i<m,0<=j<n)
LOC(arr[i][j])=LOC(arr[0])+(i×n+j)×L(0<=i<m,0<=j<n)
1.4 矩阵在数组中的存储
由 m × n m×n m×n个数 a i j ( i = 1 , 2 , 3 , … … , m ; j = 1 , 2 , 3 , … … , n ) a_{ij}(i=1,2,3,……,m;j=1,2,3,……,n) aij(i=1,2,3,……,m;j=1,2,3,……,n)排列成的m行n列的矩形表格称为m×n矩阵,简记为A或 ( a i j ) m × n ( i = 1 , 2 , 3 , … … , m ; j = 1 , 2 , 3 , … … , n ) (a_{ij})_{m×n}(i=1,2,3,……,m;j=1,2,3,……,n) (aij)m×n(i=1,2,3,……,m;j=1,2,3,……,n),当 m = n m=n m=n时,称A为n阶方阵。
在二维数组中,由于它是数组元素为定长线性表的线性表,在逻辑上我们同样也可以将其看做是一个矩阵。比如一个二维数组arr[m][n]
我们在逻辑上就可以将其看做是一个
m
×
n
m×n
m×n的矩阵,如下图所示:
如果我们要将m×n的矩阵中的所有元素存入到这样一个二维数组的话,那我们就有两种存储方式:
- 按照行优先进行存储:以先行后列的存储方式,将行号较小的元素优先进行存储,行号相同则将列号较小的元素进行优先存储;
- 按照列优先进行存储:以先列后行的存储方式,将列号较小的元素优先进行存储,列号相同则将行号较小的元素进行优先存储;
这两种存储方式在逻辑上和实际内存中的存储上都是有所差异的,下面我们就来分别看一下这两种存储方式;
1.4.1 行优先存储
当我们将m×n的矩阵A通过行优先将矩阵中的各个元素放入二维数组arr[m][n]
时,矩阵中的元素
a
i
j
(
i
=
1
,
2
,
3
,
…
…
,
m
;
j
=
1
,
2
,
3
,
…
…
,
n
)
a_{ij}(i=1,2,3,……,m;j=1,2,3,……,n)
aij(i=1,2,3,……,m;j=1,2,3,……,n)所对应的数组元素就为arr[i-1][j-1]
。可以看到矩阵元素的下标与所对应的数组元素的下标相差1,这是因为在矩阵中,下标是从1开始计数,而在数组中下标是从0开始计数。
如果用L来表示数组中元素所占空间大小,则矩阵中的元素
a
i
j
(
i
=
1
,
2
,
3
,
…
…
,
m
;
j
=
1
,
2
,
3
,
…
…
,
n
)
a_{ij}(i=1,2,3,……,m;j=1,2,3,……,n)
aij(i=1,2,3,……,m;j=1,2,3,……,n)在内存中的地址为:
L
O
C
(
a
i
j
)
=
L
O
C
(
a
r
r
[
0
]
)
+
[
(
i
−
1
)
×
n
+
(
j
−
1
)
]
×
L
LOC(a_{ij}) = LOC(arr[0]) + [(i - 1) × n + (j - 1)] × L
LOC(aij)=LOC(arr[0])+[(i−1)×n+(j−1)]×L
矩阵元素在内存中的存储形式如下所示:
1.4.2 列优先存储
相比于行优先,我们在进行列优先存储时,我们可以把m×n的矩阵A看做是一个n×m的矩阵B,此时我们如果将其存放进一个与之对应的二维数组arr2[n][m]
的话,实际上我们也是在进行行优先存储,只不过相比于矩阵A此时的矩阵B的行号就是A的列号。矩阵B中的元素
b
j
i
(
j
=
1
,
2
,
3
,
…
…
,
n
;
i
=
1
,
2
,
3
,
…
…
,
m
)
b_{ji}(j=1,2,3,……,n;i=1,2,3,……,m)
bji(j=1,2,3,……,n;i=1,2,3,……,m)所对应的数组元素为arr2[j-1][i-1]
。
如果用L来表示数组中元素所占空间大小,则矩阵中的元素
b
j
i
(
j
=
1
,
2
,
3
,
…
…
,
n
;
i
=
1
,
2
,
3
,
…
…
,
m
)
b_{ji}(j=1,2,3,……,n;i=1,2,3,……,m)
bji(j=1,2,3,……,n;i=1,2,3,……,m)在内存中的地址为:
L
O
C
(
a
i
j
)
=
L
O
C
(
a
r
r
[
0
]
)
+
[
(
j
−
1
)
×
m
+
(
i
−
1
)
]
×
L
LOC(a_{ij}) = LOC(arr[0]) + [(j - 1) × m + (i - 1)] × L
LOC(aij)=LOC(arr[0])+[(j−1)×m+(i−1)]×L
矩阵元素在内存中的存储形式如下所示:
二、特殊矩阵及其压缩存储
前面的介绍主要是说明普通的m×n的矩阵在内存中的一个存储方式。当我们需要存储普通矩阵的数据时,我们就可以借助与其元素个数相同的二维矩阵来进行相应的存储,根据自己的具体需求来选择行优先还是列优先的存储方式。
但是当我们在存储矩阵时,并不是矩阵中的所有数据都需要进行存储。对于一些特殊的矩阵,我们在进行存储时,可以采用更高效的存储方式——压缩存储。
- 特殊矩阵
特殊矩阵指的是具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一定规律性的矩阵。常见的特殊矩阵有对称矩阵、上(下)三角矩阵、对角矩阵、稀疏矩阵等。
- 压缩存储
压缩存储指的是为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的是为了节省空间。
- 特殊矩阵的压缩存储方法
在存储特殊矩阵时,我们可以通过找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的、值相同的多个矩阵元素压缩存储到一个存储空间中。
了解了什么是特殊矩阵和压缩存储后,下面我们就来看一下几种常见的特殊矩阵及其存储方式;
三、对称矩阵及其存储
3.1 方阵与对称矩阵
在n阶方阵中,元素可以划分为3个部分:上三角区、主对角线和下三角区,如下所示:
对一个n阶方阵A中的任意一个元素 a i j a_{ij} aij都有 a i j = a j i ( 1 < = i , j < = n ) a_{ij}=a_{ji}(1<=i,j<=n) aij=aji(1<=i,j<=n),则称其为对称矩阵。
3.2 对称矩阵的存储
在n阶对称矩阵中,因为上三角区和下三角区对应的元素都相同,若仍采用二维数组来存放矩阵中的元素,则会浪费几乎一半的空间。为了减少对空间的消耗,我们可以将n阶对称矩阵A存放在一个一维数组B[n(n+1)/2]
中,即元素
A
i
j
A_{ij}
Aij存放在B[k]中。比如只存放主对角线和下三角区的元素。
那我们这样存放后如何来查找上三角区对应的元素呢?
对于上三角区各个元素的查找,这里我们就可以利用对称矩阵的特性——n阶方阵A中的任意一个元素 a i j a_{ij} aij都有 a i j = a j i ( 1 < = i , j < = n ) a_{ij}=a_{ji}(1<=i,j<=n) aij=aji(1<=i,j<=n)来进行查找。
也就是我在数组中存放的是 A 10 A_{10} A10那我要查找 A 01 A_{01} A01的话我只需要访问存放 A 10 A_{10} A10的数组元素B[k]
即可。
这就是对称矩阵的压缩存储——将两个值相同的元素存入通一个空间内,将原先需要 n 2 n^2 n2的空间大小压缩成 n ( n + 1 ) / 2 n(n+1)/2 n(n+1)/2的空间大小。
相比与使用二维数组存放的方式,这种压缩存储方式,可以让我们节省整个下/上三角区的空间大小,也就是 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2的空间大小。
3.3 压缩存储的手动实现
当我们在实现压缩存储时,我们会面临两个问题:
- 矩阵中的元素如何进行存放
- 如何在一维数组
B[n(n+1)/2]
中找到矩阵元素 A i j A_{ij} Aij
在前面我们介绍了两种矩阵的存放方式——行优先存储和列优先存储。
对于不同类型的矩阵在二维数组中进行行优先和列优先的存储会有些许的不同:
- m×n的矩阵
A
A
A在进行行优先和列优先存储时有两点不同:
- 二维数组的不同——进行行优先存储时,对应的二维数组为
B[m][n]
;而在进行列优先存储时,对应的二维数组为B[n][m]
; - 矩阵元素在二维数组中存放的位置不同——进行行优先存储时,数组元素
B[0][1]
存放的是矩阵元素 A 01 A_{01} A01;而进行列优先存储时,数组元素B[0][1]
存放的是矩阵元素 A 10 A_{10} A10;
- 二维数组的不同——进行行优先存储时,对应的二维数组为
- 对于n阶方阵
A
A
A而言,两种存储方式需要的二维数组都是
B[n][n]
,但是还是有一点不同:- 矩阵元素在二维数组中存放的位置不同——进行行优先存储时,数组元素
B[0][1]
存放的是矩阵元素 A 01 A_{01} A01;而进行列优先存储时,数组元素B[0][1]
存放的是矩阵元素 A 10 A_{10} A10;
- 矩阵元素在二维数组中存放的位置不同——进行行优先存储时,数组元素
- 对于n阶对称矩阵
A
A
A而言,在二维数组
B[n][n]
中不管是哪一种存储方式,存储的结果都是相同的;
当我们对n阶对称矩阵进行压缩存储时,这里以下三角区和主对角线元素为例,实际需要存储的元素如下图所示:
要进行压缩存储的元素从
a
11
a_{11}
a11开始,到
a
n
n
a_{nn}
ann结束,在矩阵中的排列就是一个直角三角形。
从行来看这个三角形的话我们不难发现,每一行的元素个数与对应行号相等:
第一行有一个元素;
第二行有两个元素;
……
第n行有n个元素;
而从列来看的话每一列的元素个数与对应的列号刚好相反:
第一行有n个元素;
第二行有n-1个元素;
……
第n行有1个元素;
不管是行也好,还是列也好,相邻两行或者相邻两列之间的元素个数相差1,根据等差数列的求和公式我们可以很容易的算出,我们实际需要存储的元素有
n
×
(
n
+
1
)
/
2
n×(n+1)/2
n×(n+1)/2个元素,如果想要存放这些元素,那么我们所需要的一维数组就应该是 Elemtype arr[n(n+1)/2]
,这里为了方便说明,我们就使用数组B[n(n+1)/2]
来说明。
可见在这种情况下行优先与列优先的存储方式的选择就决定了矩阵元素
A
i
j
A_{ij}
Aij在一维数组B[n(n+1)/2]
中存放的位置。下面我们就来分别探讨矩阵元素
A
i
j
A_{ij}
Aij在进行不同的存储方式时与所对应的数组元素B[k]之间的关系;
3.3.1 行优先存储
如果采用行优先的方式来存放这些元素,那元素在内存中的位置如下所示:
从上图中我们可以看到,按照行优先存储时,每行之间都是顺序存储的,每一行的元素之间也都是顺序存储的。在数组中,数组下标表示的是当前元素之前的元素个数,根据数组下标的这个特性,我们不难得出一个结论——我们在寻找矩阵元素
A
i
j
A_{ij}
Aij所对应的数组元素,实际上就是在计算该元素之前的元素个数。
如何计算元素个数相信是大家比较关心的一个问题,为了方便大家更好的理解,我们可以把每一行的元素个数给列出来,如下所示:
行号 | 元素个数 |
---|---|
1 | 1 |
2 | 2 |
3 | 3 |
…… | …… |
i-1 | i-1 |
i | i |
i+1 | i+1 |
…… | …… |
n-2 | n-2 |
n-1 | n-1 |
n | n |
从上表中我们不难得出一个结论——1到n行的元素个数刚好是1到n的一个公差为1的等差数列。
因此,当我们想计算第 i i i 行的元素总个数时,我们只需要将从第 1 1 1 行到第 i i i 行的所有元素个数都给加起来。第 1 1 1 行到第 i i i 行的元素个数总和我们可以通过等差数列求和公式得到: S i = ( 1 + i ) × i / 2 S_i=(1+i)×i/2 Si=(1+i)×i/2
对于同一行而言,它的每一列只存在一个元素,因此从 1 1 1 列到第j列共有 j j j 个元素。也就是: S j = j S_j=j Sj=j
当我们要计算第 i + 1 i+1 i+1行的第 j j j 列元素的元素个数时,实际上就是计算从第 1 1 1 行到第i行的元素总个数 S i S_i Si以及第 i + 1 i+1 i+1行的第 j j j 列个元素 S j S_j Sj的总和,那我们就不难得到从元素 A 11 A_{11} A11到元素 A ( i + 1 ) j A_{(i+1)j} A(i+1)j的元素总个数: S ( i + 1 ) j = S i + S j = ( 1 + i ) × i / 2 + j S_{(i+1)j}=S_i+S_j=(1+i)×i/2+j S(i+1)j=Si+Sj=(1+i)×i/2+j
而对于数组而言,由于数组下标是记录的该元素之前的元素个数,那也就是说我想要计算 A ( i + 1 ) j A_{(i+1)j} A(i+1)j的数组下标,实际上也就是计算从元素 A 11 A_{11} A11到元素 A ( i + 1 ) ( j − 1 ) A_{(i+1)(j-1)} A(i+1)(j−1)的元素总个数。因此我们可以很容易的得到其数组下标k的值: k = S ( i + 1 ) ( j − 1 ) = ( 1 + i ) × i / 2 + j − 1 k=S_{(i+1)(j-1)}=(1+i)×i/2+j-1 k=S(i+1)(j−1)=(1+i)×i/2+j−1
当我们要查找元素 A i j A_{ij} Aij所对应的数组下标k,实际上就是在计算从第 1 1 1 行到第 i − 1 i-1 i−1行的元素总个数 S i − 1 S_{i-1} Si−1和第 i i i 行第 j − 1 j-1 j−1列的元素总个数 S j S_j Sj的和 S ( i − 1 ) ( j − 1 ) S_{(i-1)(j-1)} S(i−1)(j−1)。通过前面推导出来的公式,我们不难得到其对应的数组下标k的值: k = S ( i − 1 ) ( j − 1 ) = i × ( i − 1 ) / 2 + j − 1 k=S_{(i-1)(j-1)}=i×(i-1)/2+j-1 k=S(i−1)(j−1)=i×(i−1)/2+j−1
3.3.2 列优先存储
如果采用列优先的存储方式,那元素在内存中的位置如下所示:
在列优先存储中,由于下三角区的元素个数与列号并不是一一对应的,因此我们在计算元素总数时我们需要计算每列的元素个数以及总列数,如下表所示:
列号 | 元素个数 |
---|---|
1 | n |
2 | n-1 |
3 | n-2 |
…… | …… |
j-1 | n-j+2 |
j | n-j+1 |
j+1 | n-j |
…… | …… |
n-2 | 3 |
n-1 | 2 |
n | 1 |
从表中不难看出列优先存储在列数上是一个公差为1的递增的等差数列,而每列的元素个数则是一个公差为1的递减的等差数列。根据等差数列的求和公式 (首项 + 尾项) × 项数 ÷ 2 (首项+尾项)×项数÷2 (首项+尾项)×项数÷2可知,公式中的首项为第 1 1 1 列的元素个数,也就是 n n n;项数为对应的列号,比如要求 j j j 列的元素总和,那对应的项数就为 j j j ;尾项则是第j列的元素个数 n − j + 1 n-j+1 n−j+1;
因此从第
1
1
1 列到第
j
j
j 列的元素总个数
S
j
S_j
Sj为:
S
j
=
(
n
+
n
−
j
+
1
)
×
j
/
2
=
(
2
n
−
j
+
1
)
j
/
2
S_j=(n+n-j+1)×j/2=(2n-j+1)j/2
Sj=(n+n−j+1)×j/2=(2n−j+1)j/2
由于每一列的元素个数是一个递减的等差数列,因此,对于不同列来说,它们在同一行所有的元素个数是不相同的,如下所示:
列号 | 从第1行到第i行的元素个数 |
---|---|
1 | i |
2 | i-1 |
3 | i-2 |
…… | …… |
j-1 | i-j+2 |
j | i-j+1 |
j+1 | i-j |
…… | …… |
n-2 | i-n+3 |
n-1 | i-n+2 |
n | i-n+1 |
因此,第
j
j
j 列的第
i
i
i 行元素所对应的元素总个数
S
i
S_i
Si为:
S
i
=
i
−
j
+
1
S_i=i-j+1
Si=i−j+1
那么对于下三角区和主对角线的这些元素通过列优先进行存储时,从
a
11
a_{11}
a11到
a
i
j
a_{ij}
aij所对应的元素总个数
S
i
j
S_{ij}
Sij实际上是从第
1
1
1 列到
j
−
1
j-1
j−1列的元素总个数加上第
j
j
j列第
i
i
i 行的元素总个数:
S
i
j
=
S
j
−
1
+
S
i
=
(
2
n
−
j
+
2
)
(
j
−
1
)
/
2
+
i
−
j
+
1
S_{ij}=S_{j-1}+S_i=(2n-j+2)(j-1)/2+i-j+1
Sij=Sj−1+Si=(2n−j+2)(j−1)/2+i−j+1
在数组中,由于数组下标比元素总个数少1,因此元素
a
i
j
a_{ij}
aij所对应的数组下标k为:
k
=
S
i
j
−
1
=
(
2
n
−
j
+
2
)
(
j
−
1
)
/
2
+
i
−
j
+
1
−
1
=
(
2
n
−
j
+
2
)
(
j
−
1
)
/
2
+
i
−
j
k=S_{ij}-1=(2n-j+2)(j-1)/2+i-j+1-1=(2n-j+2)(j-1)/2+i-j
k=Sij−1=(2n−j+2)(j−1)/2+i−j+1−1=(2n−j+2)(j−1)/2+i−j
3.3.3 易错点
- 矩阵元素下标的取值范围
这里我们需要注意的是前面我们介绍的不管是行优先存储还是列优先存储对于n阶对称矩阵A而言,其所对应的元素 A i j A_{ij} Aij下标 i 、 j i、j i、j 的取值范围是 ( 1 < = i , j < = n ) (1<=i,j<=n) (1<=i,j<=n);而对于数组
B[n(n+1)/2]
而言,其数组元素B[k]
所对应的数组下标 k k k 的取值范围是 ( 0 < = k < = n ( n + 1 ) / 2 − 1 ) (0<=k<=n(n+1)/2-1) (0<=k<=n(n+1)/2−1)。
可以看到在数组中元素总个数与下标之间的差值为1,因此我们在将矩阵元素下标转换为所对应的数组元素下标时,所求得的元素总数 S i j S_{ij} Sij在 − 1 -1 −1后得到的结果才是对应的数组元素下标k。
但是并不是所有遇到的矩阵下标的取值范围都是从1开始的,比如有的n阶对称矩阵的下标取值范围是 ( 0 < = i , j < = n − 1 ) (0<=i,j<=n-1) (0<=i,j<=n−1),在这种情况下矩阵元素下标与对应的数组元素下标是一一没有差值了,此时算出来的 a i j a_{ij} aij的元素个数就是其所对应的数组下标。
因此数组下标与矩阵元素对应的下标之间的关系需要根据矩阵元素下标的取值范围来进行确定。
- 存储区域
在前面的介绍中,我们都是以下三角区和主对角线为例子来探讨在不同的存储形式中矩阵元素下标与数组元素下标之间的关系。因此我们得到的结论如下所示:
在n阶对称矩阵A中,其矩阵元素 A i j A_{ij} Aij下标 i 、 j i、j i、j 的取值范围是 ( 1 < = i , j < = n ) (1<=i,j<=n) (1<=i,j<=n)
存放矩阵元素的数组B[n(n+1)/2]
,其数组元素B[k]
所对应的数组下标 k k k 的取值范围是
( 0 < = k < = n ( n + 1 ) / 2 − 1 ) (0<=k<=n(n+1)/2-1) (0<=k<=n(n+1)/2−1)
通过行优先存储时,矩阵元素下标与数组元素下标之间的关系为:
k = S ( i − 1 ) ( j − 1 ) = i × ( i − 1 ) / 2 + j − 1 k=S_{(i-1)(j-1)}=i×(i-1)/2+j-1 k=S(i−1)(j−1)=i×(i−1)/2+j−1
通过列优先存储时,矩阵元素下标与数组元素下标之间的关系为:
k = S i j − 1 = ( 2 n − j + 2 ) ( j − 1 ) / 2 + i − j + 1 − 1 = ( 2 n − j + 2 ) ( j − 1 ) / 2 + i − j k=S_{ij}-1=(2n-j+2)(j-1)/2+i-j+1-1=(2n-j+2)(j-1)/2+i-j k=Sij−1=(2n−j+2)(j−1)/2+i−j+1−1=(2n−j+2)(j−1)/2+i−j
但是,当我们存储的矩阵区域为主对角线与上三角区时,我们不难发现,此时的矩阵元素下标与下三角区的元素下标刚好相反,为 A j i A_{ji} Aji,因此矩阵元素下标与数组下标之间的关系式也是稍有差异的,这里我就不多加赘述了。
四、三角矩阵及其存储
介绍完了n阶对称矩阵的压缩存储,下面我们再来看一下什么是三角矩阵以及其对应的压缩存储。
4.1 三角矩阵
三角矩阵和对称矩阵一样,也是一种特殊的n阶方阵。在三角矩阵中,矩阵的下三角区和上三角区这两块区域中其中一块区域都为通一个常量,这里我们还是以下三角区和主对角线为例,如下所示:
向这种只有主对角线与下/上三角区中存放元素,上/下三角区中存放同一常量的特殊n阶方阵,我们将其称为三角矩阵。
4.2 三角矩阵的存储
在三角矩阵中,如果我们以二维数组来存放矩阵中的所有元素,对于存放常量的区域来说,其实也是一种空间的浪费。
按照压缩存储的思想,我们在存放三角矩阵中的元素时,我们实际上可以和对称矩阵一样通过一维数组存放下/上三角区加上主对角线的全部元素,再通过额外的一个空间来存放上/下三角区中存放的常量。
因此,相比于对称矩阵而言,三角矩阵在存储时所使用的数组为B[n(n+1)/2+1]
,这多出来的一个区域是用来存放常量的区域。
在对称矩阵中我们已经很好的分析了如何对下三角区和主对角线的矩阵元素进行存储,这里我就不再展开赘述。下面我们直接给结论,这里以下三角区和主对角线为例,上三角区存放同一常量:
在n阶三角矩阵A中,其矩阵元素 A i j A_{ij} Aij下标 i 、 j i、j i、j 的取值范围是 ( 1 < = j < = i < = n ) (1<=j<=i<=n) (1<=j<=i<=n)
存放常量的矩阵元素 A i j A_{ij} Aij下标 i 、 j i、j i、j 的取值范围是 ( 1 < = i < j < = n ) (1<=i<j<=n) (1<=i<j<=n)
存放矩阵元素的数组B[n(n+1)/2+1]
,其数组元素B[k]
所对应的数组下标 k k k 的取值范围是
( 0 < = k < = n ( n + 1 ) / 2 ) (0<=k<=n(n+1)/2) (0<=k<=n(n+1)/2)
存放常量的数组元素B[k]
所对应的数组下标 k k k为:
k = n ( n + 1 ) / 2 k=n(n+1)/2 k=n(n+1)/2
通过行优先存储时,矩阵元素下标与数组元素下标之间的关系为:
k = S ( i − 1 ) ( j − 1 ) = i × ( i − 1 ) / 2 + j − 1 k=S_{(i-1)(j-1)}=i×(i-1)/2+j-1 k=S(i−1)(j−1)=i×(i−1)/2+j−1
通过列优先存储时,矩阵元素下标与数组元素下标之间的关系为:
k = S i j − 1 = ( 2 n − j + 2 ) ( j − 1 ) / 2 + i − j + 1 − 1 = ( 2 n − j + 2 ) ( j − 1 ) / 2 + i − j k=S_{ij}-1=(2n-j+2)(j-1)/2+i-j+1-1=(2n-j+2)(j-1)/2+i-j k=Sij−1=(2n−j+2)(j−1)/2+i−j+1−1=(2n−j+2)(j−1)/2+i−j
五、三对角矩阵及其存储
介绍完了对称矩阵和三角矩阵,接下来我们来看另一种特殊的n阶方阵——三对角矩阵。
5.1 对角矩阵
对角矩阵时一个除主对角线之外的其它元素全为0的矩阵。对角线上的元素可以为0或者其他值,如下所示:
对角线上元素相等的对角矩阵称为数量矩阵;对角线上元素全为1的对角矩阵称为单位矩阵;对角矩阵也称为带状矩阵。
5.2 三对角矩阵
在n阶方阵A中,当任意一个元素 A i j A_{ij} Aij的下标满足 ∣ i − j ∣ > 1 |i-j|>1 ∣i−j∣>1时,有 A i j = 0 ( 1 < = i , j < = n ) A_{ij}=0(1<=i,j<=n) Aij=0(1<=i,j<=n),则该n阶方阵称为三对角矩阵,如下所示:
不难看出,除了主对角线以及左右两相邻的对角线上的元素外,其他区域都为0,因此三对角矩阵也是可以采用压缩存储的。
5.3 三对角矩阵的存储
对于三对角矩阵而言,根据压缩存储的思想,对于值为0的元素不分配存储空间,因此我们主要存储的就是主对角线和左右两相邻对角线上的元素。
在n阶方阵中,三对角矩阵除了第1行和第n行只有两个元素外,其余行都有3个元素,因此我们很容易得到需要存储的元素个数为 3 n − 2 3n-2 3n−2。
三对角矩阵在存储时,我们同样可以选择行优先存储和列优先存储两种方式。
5.3.1 行优先存储
当我们通过行优先来存储n阶三对角矩阵A时,矩阵中的元素
A
i
j
(
1
<
=
i
,
j
<
=
n
)
A_{ij}(1<=i,j<=n)
Aij(1<=i,j<=n)与存储矩阵元素的数组B[3n-2]
所对应的数组元素B[k]
0
<
=
k
<
=
3
n
−
3
0<=k<=3n-3
0<=k<=3n−3之间的关系如下所示:
在计算从第
1
1
1 行到第
i
i
i 行的元素个数时,我们以主对角线的元素为起点与终点,这样我们能够很容易的得到元素的总个数为
3
i
−
2
3i-2
3i−2,对应的数组下标则是
k
=
3
i
−
3
k=3i-3
k=3i−3;
通过主对角线上的元素来推算左右两侧的元素,我们不难得到
- 位于主对角线左侧的元素总个数为 3 i − 3 3i-3 3i−3,对应的数组下标则是 k = 3 i − 4 k=3i-4 k=3i−4;
- 位于主对角线右侧的元素总个数为 3 i − 1 3i-1 3i−1,对应的数组下标则是 k = 3 i − 2 k=3i-2 k=3i−2;
5.3.2 列优先存储
当我们通过列优先来存储n阶三对角矩阵A时,矩阵中的元素
A
i
j
(
1
<
=
i
,
j
<
=
n
)
A_{ij}(1<=i,j<=n)
Aij(1<=i,j<=n)与存储矩阵元素的数组B[3n-2]
所对应的数组元素B[k]
0
<
=
k
<
=
3
n
−
3
0<=k<=3n-3
0<=k<=3n−3之间的关系如下所示:
通过观察我们不难得到,对于三对角矩阵不管是行优先还是列优先,我们在计算矩阵元素对应的数组下标时,其实没有区别。从函数的角度来看行优先以行号 i i i 为自变量,数组下标 k k k 为因变量,列优先以列号 j j j 为自变量,数组下标 k k k 为因变量,它们所对应的法则是一致的。对于函数而言两个行数的定义域一致,对应法则一致,那么这两个函数就是同一个函数。
因此在计算从第
1
1
1 行到第
j
j
j 列的元素个数时,我们以主对角线的元素为起点与终点,这样我们能够很容易的得到元素的总个数为
3
j
−
2
3j-2
3j−2,对应的数组下标则是
k
=
3
j
−
3
k=3j-3
k=3j−3;
通过主对角线上的元素来推算左右两侧的元素,我们不难得到
- 位于主对角线上方的元素总个数为 3 j − 3 3j-3 3j−3,对应的数组下标则是 k = 3 j − 4 k=3j-4 k=3j−4;
- 位于主对角线下方的元素总个数为 3 j − 1 3j-1 3j−1,对应的数组下标则是 k = 3 j − 2 k=3j-2 k=3j−2;
5.3.3 思维拓展
刚才我们通过一种比较巧妙的方式介绍了在n阶三对角矩阵的行优先存储和列优先存储这两种存储方式中矩阵元素下标 i , j ( 1 < = i , j < = n ) i,j(1<=i,j<=n) i,j(1<=i,j<=n)与数组元素下标 k ( 0 < = k < = 3 n − 3 ) k(0<=k<=3n-3) k(0<=k<=3n−3)之间的关系。
现在我们要介绍的一种新的方式——从函数的角度来获取在这两种存储方式下的矩阵元素下标 i , j ( 1 < = i , j < = n ) i,j(1<=i,j<=n) i,j(1<=i,j<=n)与数组元素下标 k ( 0 < = k < = 3 n − 3 ) k(0<=k<=3n-3) k(0<=k<=3n−3)之间的关系。大家可能不太理解,没关系,我们一起往下看;
前面我们也提到过在三对角矩阵中,不管是行优先还是列优先,实际上它们都是同一个函数,因此我们不妨假设矩阵元素下标 i , j ( 1 < = i , j < = n ) i,j(1<=i,j<=n) i,j(1<=i,j<=n)与数组元素下标 k ( 0 < = k < = 3 n − 3 ) k(0<=k<=3n-3) k(0<=k<=3n−3)之间的关系式为: a i + b j + c = k ( a , b , c 为实数 ) ai+bj+c=k(a,b,c为实数) ai+bj+c=k(a,b,c为实数)函数的定义域为 1 < = i , j < = n 1<=i,j<=n 1<=i,j<=n,函数的值域为 0 < = k < = 3 n − 3 0<=k<=3n-3 0<=k<=3n−3。
对于这个二元一次方程的求解,我们可以通过赋值法来求出自变量的系数 a , b a,b a,b与常数 c c c ,这里我们以行优先为例,矩阵元素下标与数组元素下标如下所示:
i | j | k |
---|---|---|
1 | 1 | 0 |
1 | 2 | 1 |
2 | 1 | 2 |
2 | 2 | 3 |
…… | …… | …… |
n-1 | n | 3n-5 |
n | n-1 | 3n-4 |
n | n | 3n-3 |
下面我们将前四个元素中任意3个元素的值带入到表达式,不难得到结果: a = 2 , b = 1 , c = − 3 a=2,b=1,c=-3 a=2,b=1,c=−3
因此矩阵元素下标与数组元素下标之间的关系式为:
2
i
+
j
−
3
=
k
(
1
<
=
i
,
j
<
=
n
)
2i+j-3=k(1<=i,j<=n)
2i+j−3=k(1<=i,j<=n)
之后我们可以将最后3个元素中的任意一个带入进行验算,可以证明我们求出来的表达式正是矩阵元素下标与数组元素下标之间的关系式。
同样,如果我们以列优先为例来进行求解,我们会得到表达式
2
j
+
i
−
3
=
k
(
1
<
=
i
,
j
<
=
n
)
2j+i-3=k(1<=i,j<=n)
2j+i−3=k(1<=i,j<=n)
表达式的求解过程我就不多加赘述了。
根据三对角矩阵行列下标之间的关系式
∣
i
−
j
∣
<
=
1
|i-j|<=1
∣i−j∣<=1,我们以
i
−
j
=
1
i-j=1
i−j=1为例,还可以得到行下标与数组元素下标之间的关系式
i
=
(
k
+
1
)
/
3
+
1
,
(
0
<
=
k
<
=
3
n
−
3
)
i=(k+1)/3+1,(0<=k<=3n-3)
i=(k+1)/3+1,(0<=k<=3n−3)
表达式结果向下取整。如当
k
=
6
k=6
k=6时,
i
=
10
/
3
≈
3
i=10/3≈3
i=10/3≈3。
由表达式
2
i
+
j
−
3
=
k
(
1
<
=
i
,
j
<
=
n
)
2i+j-3=k(1<=i,j<=n)
2i+j−3=k(1<=i,j<=n)我们可以得到表达式
j
=
k
−
2
i
+
3
j=k-2i+3
j=k−2i+3。接下来我们将刚刚求出的
i
i
i 值与已知的
k
k
k 值带入,就能得到
j
=
3
j=3
j=3。也就是在行优先存储中B[6]
所对应的矩阵元素为
A
33
A_{33}
A33。
对于该结果我们还可以根据前面介绍的行优先存储中矩阵元素行下标与数组元素下标之间的关系式来进行验算。由矩阵元素的下标可知,该元素是位于主对角线上的元素,因此我们可以根据表达式 k = 3 i − 3 ( 1 < = i < = n ) k=3i-3(1<=i<=n) k=3i−3(1<=i<=n)求出 k = 3 × 3 − 3 = 6 k=3×3-3=6 k=3×3−3=6 。可以看到矩阵元素 A 33 A_{33} A33就是数组中下标为6的元素。
六、稀疏矩阵及其存储
在n阶矩阵中还有一类特殊矩阵——稀疏矩阵。下面我们来看看什么是稀疏矩阵;
6.1 稀疏矩阵
在n阶矩阵中,矩阵元素的总个数为
s
s
s,非零元素个数为
t
t
t,当
s
>
>
t
s>>t
s>>t时,我们称该矩阵为稀疏矩阵。如下图所示:
向这种非零元素的个数远小于矩阵元素的个数且元素分布散乱的矩阵我们就可以将其称为稀疏矩阵。稀疏矩阵即可以是n阶方阵也可以是m×n阶矩阵。
6.2 稀疏矩阵的存储
由于稀疏矩阵中的非零元素的分布是散乱的,因此我们无法只对非零元素的值进行存储,为了能够准确的找到元素在矩阵中的位置,我们有两种存储的思路——三元组存储与十字链表存储。
6.2.1 三元组存储
所谓的三元组存储其实就是我们在存储非零元素时将非零元素的行标、列标与元素值一同存储起来。以上图列举的4阶稀疏矩阵为例,我们对非零元素进行三元组存储的方法如下所示:
i | j | v |
---|---|---|
1 | 2 | 1 |
1 | 4 | 1 |
2 | 3 | 1 |
3 | 1 | 1 |
4 | 4 | 1 |
在三元组存储中因为我们是借助行标与列标来对元素进行定位的,因此不管元素的值是否相同,我们都必须分配相应的空间来记录它的行标、列标与元素值。
为了访问三元组中的成员,我们需要借助相应的数据结构来进行存放,比如通过顺序表或者链表。当我们想要访问某一个元素时,不管是顺序表还是链表,我们需要通过对应的行标和列标来进行定位,因此,在三元组存储中我们无法做到随机存取。
6.2.2 十字链表法
对于稀疏矩阵的第二种存储方式十字链表法,我们可以理解为通过两个指针数组分别记录矩阵的行与列,记录行的指针数组的指针指向的是同行对应的下一个非零元素,记录列的指针数组的指针指向的是同列对应的下一个非零元素,如下所示:
可以看到稀疏矩阵的十字链表法是通过行指针数组和列指针数组来模拟矩阵的行标和列标,通过行指针和列指针来指向矩阵中的非零元素。十字链表法会在后面的学习中详细介绍,这里我就不再继续展开,大家通过上图对该存储方法留一个印象就行。
对于稀疏矩阵的压缩存储,我们需要重点记忆它的两种存储方法——三元组存储和十字链表存储。
结语
在今天的内容中我们首先介绍了数组与矩阵的关系,当我们要将一个m×n矩阵通过二维数组存放在内存中时,我们有两种存储方式——行优先存储和列优先存储。
当我们将m×n的矩阵A通过行优先将矩阵中的各个元素放入二维数组arr[m][n]
时,如果用L来表示数组中元素所占空间大小,则矩阵中的元素
a
i
j
(
i
=
1
,
2
,
3
,
…
…
,
m
;
j
=
1
,
2
,
3
,
…
…
,
n
)
a_{ij}(i=1,2,3,……,m;j=1,2,3,……,n)
aij(i=1,2,3,……,m;j=1,2,3,……,n)在内存中的地址为:
L
O
C
(
a
i
j
)
=
L
O
C
(
a
r
r
[
0
]
)
+
[
(
i
−
1
)
×
n
+
(
j
−
1
)
]
×
L
LOC(a_{ij}) = LOC(arr[0]) + [(i - 1) × n + (j - 1)] × L
LOC(aij)=LOC(arr[0])+[(i−1)×n+(j−1)]×L
当我们将m×n的矩阵A通过列优先将矩阵中的各个元素放入二维数组arr[n][m]
时如果用L来表示数组中元素所占空间大小,则矩阵中的元素
a
i
j
(
j
=
1
,
2
,
3
,
…
…
,
n
;
i
=
1
,
2
,
3
,
…
…
,
m
)
a_{ij}(j=1,2,3,……,n;i=1,2,3,……,m)
aij(j=1,2,3,……,n;i=1,2,3,……,m)在内存中的地址为:
L
O
C
(
a
i
j
)
=
L
O
C
(
a
r
r
[
0
]
)
+
[
(
j
−
1
)
×
m
+
(
i
−
1
)
]
×
L
LOC(a_{ij}) = LOC(arr[0]) + [(j - 1) × m + (i - 1)] × L
LOC(aij)=LOC(arr[0])+[(j−1)×m+(i−1)]×L
之后我们介绍了什么是压缩存储——为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的是为了节省空间。
并介绍了4种矩阵的压缩存储;
- 对称矩阵的压缩存储:
- 通过行优先存储时矩阵元素下标与数组元素下标的关系为 k = S ( i − 1 ) ( j − 1 ) = i × ( i − 1 ) / 2 + j − 1 ( 1 < = j < = i < = n ) k=S_{(i-1)(j-1)}=i×(i-1)/2+j-1(1<=j<=i<=n) k=S(i−1)(j−1)=i×(i−1)/2+j−1(1<=j<=i<=n)
- 通过列优先存储时矩阵元素下标与数组元素下标的关系为 k = S i j − 1 = ( 2 n − j + 2 ) ( j − 1 ) / 2 + i − j ( 1 < = j < = i < = n ) k=S_{ij}-1=(2n-j+2)(j-1)/2+i-j(1<=j<=i<=n) k=Sij−1=(2n−j+2)(j−1)/2+i−j(1<=j<=i<=n)
- 三角矩阵的压缩存储:
- 通过行优先存储时矩阵元素下标与数组元素下标的关系为 k = S ( i − 1 ) ( j − 1 ) = i × ( i − 1 ) / 2 + j − 1 ( 1 < = j < = i < = n ) k=S_{(i-1)(j-1)}=i×(i-1)/2+j-1(1<=j<=i<=n) k=S(i−1)(j−1)=i×(i−1)/2+j−1(1<=j<=i<=n)
- 通过列优先存储时矩阵元素下标与数组元素下标的关系为 k = S i j − 1 = ( 2 n − j + 2 ) ( j − 1 ) / 2 + i − j ( 1 < = j < = i < = n ) k=S_{ij}-1=(2n-j+2)(j-1)/2+i-j(1<=j<=i<=n) k=Sij−1=(2n−j+2)(j−1)/2+i−j(1<=j<=i<=n)
- 存放常量元素的矩阵元素下标与数组元素下标的关系为 k = n ( n + 1 ) / 2 ( 1 < = i < j < = n ) k=n(n+1)/2(1<=i<j<=n) k=n(n+1)/2(1<=i<j<=n)
- 三对角矩阵的压缩存储:
- 以主对角线为起点与终点的矩阵元素下标与数组元素下标之间的关系式为 k = 3 i − 3 ( 1 < = i = j < = n ) k=3i-3(1<=i=j<=n) k=3i−3(1<=i=j<=n)
- 以函数的角度计算的矩阵元素下标与数组元素下标之间的关系式为 2 i + j − 3 = k ( 1 < = i , j < = n , ∣ i − j ∣ < = 1 ) 2i+j-3=k(1<=i,j<=n,|i-j|<=1) 2i+j−3=k(1<=i,j<=n,∣i−j∣<=1)
- 稀疏矩阵的压缩存储:
- 稀疏矩阵有两种压缩存储的方式——三元组存储和十字链表法存储。
到这里咱们今天的内容就全部结束了,希望今天的内容能够帮助大家进一步理解特殊矩阵的压缩存储的方式。如果各位朋友喜欢博主的内容,可以点赞、收藏加评论来支持一下博主,当然也可以转发给身边需要的朋友。最后感谢各位的支持,咱们下一篇再见!!!