这个章节的内容主要是引用教材中的知识点
数组
关于数组这个章节主要是讲一下多维数组的基本概念以及推算一下地址计算公式,还有就是矩阵压缩的相关内容(一维数组便不陈述了)。
注意下面所有内容的数组下标都是从0开始计算的,如果有从1开始计算的请重新推演公式(推演办法是一致的)。
二维数组
一个形式化的数组可以描述为一个含有n个数据元素的一维数组,可以表示为线性表A=(a0,a1,a2,a3,an-1) 表中的元素个数为n。
那么二维数组是不是也可以描述为是一个m个一维数组的线性表,表示为A=(a00,a01,a02,a10,a12,a13) ,其表示形式如下:
二维数组在存储时分为行序存储和列序存储,不同的存储方式在内存中的表示方式不同,由于存储方式的不同导致元素在内存中的位置不同,不过计算机默认以行序的方式存储二维数组的数据元素。我们需要根据不同的存储方式计算数据元素的位置。
行序存储:行序存储时我们将二维数组拆分为一行一行的存储先存A[0][n],在存A[1][n],图形表示:
当数据元素是行序存储时的元素位置推算公式:
现在假设我们知道A[0][0]首元地址,如何计算A[i][j] (i<m,j<n)我们分析一下发现只要计算出i,j前面有几个数据元素然后在乘以每个数据元素的大小就知道了i,j的位置,我画了一个示意图能更好的帮助我们理解这个计算过程:
我们看上图中想要计算红色圈起来的元素位置,是不是只要知道他前面有几个数据数据元素就可以了,那么是不是只要计算出两个绿色矩形里面的数据元素有几个,矩形的面积等于长乘宽第一个绿色矩形的面积等于 (i-1)n 第二个矩形等于1(j-1) 综上所述得到如下公式:
解释一下:有一部分教程上面的计算i,j前面的元素公式是(i-1)*n+(j-1) 而这里没有减一应为这里的数组下标是从0开始计算的请注意。
假设我们知道i,j的地址如何推算首元地址(变形一下公式就可以了):、
再次假设,我们知道A[1][1]的元素位置如何求解A[i][j]元素的位置:
- 当我们知道A[1][1]的位置是不是就可以计算出A[0][0]首元元素的地址,通过首元元素的地址便可以计算出A[i][j]元素的位置。
还有就是当我们将二维数组转为一维数组时如何知道A[i][j]在一维数组中的下标(相对地址):其实解决问题的思路还是一样只要,应为数据是按照行序存储的只要计算出A[i][j]前面有几个数据元素便可以只到他在一维数组中的位置
列序存储:列序存储将二维数组一列一列的存储先存A[0][m],A[1][m]
列序存储的各个计算公式这里就不推导分析了,我直接给出公式(列序存储的分析方法和行序存储的分析方法一样)
计算A[i][j]的元素位置:
计算A[0][0]的元素位置:
知道A[1][1]计算A[i][j]的位置:
一维数组中的位置:
三维数组
三维数组是在二维数组的基础上再嵌套一个维度有r个二维数组组成的线性表,其表示形式:A[r][m][n] 我们可以吧三维数组想象成是一个魔方r表示的是正方体的层数,[m][n]表示 是二维数组即每层魔方有几个格子。图形表示如下:
魔方图的行序存储:魔方图的存储我们可以理解为他是一层一层的存而每一层又按照二维数组的行序的存储方式存储,这里我推演一个魔方图的A[r][m][n]的位置计算方式,和二维数组一样这里的魔方图的下标也是从0开始计算的请注意。
行序存储:前面我们提到过魔方图是由一层一层的二维数组组成的,当我们要计算某一层的 [i][j] 元素在内存中的位置是不是要计算出前面一层有几个元素(0层直接计算平面中有几个数据元素),然后在计算[i][j]所在层的前面有几个数据元素,此时就是比二维数组多了一个层数计算,公式如下:
三维数组的其他计算公式后面再补。
特殊矩阵压缩存储
特殊矩阵元素具有一定的分布规律,例如三角矩阵和带状矩阵。这几类矩阵的数据具有一定的分布规律。当我们对矩阵进行压缩存储,根据数据分布规律压缩入一维数组中,节省空间。
矩阵压缩的下标从1开始计算
三角矩阵压缩存储
三角矩阵分为上三角矩阵和下三角矩阵,上三角矩阵的下三角部分全为常数C或者0上,上三角部分为数据元素(下三角部分的数据是不使用的)。而下三角矩阵和上三角矩阵相反。
三角矩阵是如何压缩存储的?
我们将三角矩阵按照行序为主的序列一行一行的存储入一维数组中,最终得到的一维数组:
如何计算上(下)三角矩阵有几个数据元素:
我们观察三角矩阵的数据元素分布规律:1,2,3,4,5,n 我们发现三角矩阵的数据是一个等差数列,通过等差数列求和公式可以计算出三角矩阵的非常数C部分有几个元素
三角矩阵元素A[i][j]元素在内存中的地址位置计算方式和二维数组内存地址计算方式是一样的,计算A[i][j]前面有几个数据元素:
S(i-1) = 1+2+3+4+5+6+7+…+(i-1) = (i-1)(1+i-1) /2 = i(i-1)/2
当三角矩阵压缩为一维数组时如何计算出A[i][j]在一维数组中的位置?
分析:我们根据三角矩阵中每层数据个数的变化规律可以得到要计算第i行的前面有几个数据元素便要计算出前i-1行有几个数据元素,就是求前n-1项和。计算出i行前面有几个数据元素在计算出j列前面有几个数据元素相加便是A[i][j]在一维数组中的位置。
上三角的计算公式:
上三角矩阵的存储量和下三角矩阵的存储量是一致的都是n(n+1)/2,但是在一维数组中的位置需要重新计算,计算的思想和下三角矩阵一致,只不过公式需要重新推导。
我们观察上三角矩阵元素个数的变化趋势发现上三角矩阵和下三角的矩阵的元素分布规律是相反的,从而我们得到新的等差数列公式:
现在我分析一下A[i][j]的计算方法:由于上三角矩阵每行的元素个数构成一个公差为1的等差数列,那么我们只要计算出弟一行到第i-1行的数据元素个数加上j列前面有几个数据元素就是A[i][j]在一维数组中的位置。应为该三角矩阵是一个等差队列那么通过等差队列求和公式:
其中a1是三角矩阵中的第一行有n个元素,an是第i-1行有n-i+2个数据元素。得到行的数据元素计算公式还差列的数据元素计算公式,我们观察当i=2,j=4 时有2个元素,当i=3 j=4时有1个元素,当i=2 j=3时有一个元素,故可以得到公式 j列元素个数=j-i。根据上述分析可以得到如下分析公式:
有一点需要注意的是上述公式计算的三角矩阵下标是从1开始的,如果下标是从0开始计算请重新分析数据分布规律并且推算公式。
三角矩阵列序压缩存储:
三角矩阵的列序压缩存储所需的一维数组的存储空间和行序存储的空间大小是一样的(总的数据量不变)但是存储入一维后下标对应公式需要从新进行推算,但是推算的方法是一样的需要计算A[i][j]的前面有几个数据元素(一维数组默认从1开始存储0号位不用)
下三角矩阵A[i][j]的内存位置的计算:
S(j-1) = n+(n-1)+…+(n-j+2) = (j-1)(n+n-j+2) /2= (j-1)(2n-j+2)/2
通过上述公式可以计算出第j列前面有几个数据元素,那么只要计算出第i行前面有几个数据元素:
下三角矩阵A[i][j]对一维数组应的位置(借用前面S(j-1)的公式):
上三角矩阵A[i][j]的内存位置的计算:
第j列前面的每列的数据元素构成了一个等差数列公式如下:
第i行前有i-1个元素,可以得到如下计算公式:
上三角矩阵A[i][j]对应的一维数组的存储位置(借用S(j-1)的计算公式):
下三角矩阵压缩存储的代码片段(基于Java编写):
行序存储压缩:
//下三角矩阵行序压缩存储
public int[] RowCompression(int[][] array) {
int len = array.length * (array.length + 1) / 2;
int[] temp = new int[len + 1];
int index = 0;
for (int x = 0; x < array.length; x++) {
for (int y = 0; y <= x; y++) {
temp[++index] = array[x][y];
}
}
return temp;
}
列序存储压缩:
//下三角矩阵列序压缩存储
public int[] ColumnCompression(int[][] array) {
int len = array.length * (array.length + 1) / 2;
int[] temp = new int[len + 1];
int index = 0;
for (int x = 0; x < array.length; x++) {
for (int y = x; y < array.length; y++) {
temp[++index] = array[y][x];
}
}
return temp;
}
上三角矩阵压缩存储代码片段:
行序压缩存储:
public int[] RowCompression(int[][] array) {
int len = array.length * (array.length + 1) / 2;
int[] temp = new int[len + 1];
int index = 0;
for (int x = 0; x < array.length; x++) {
for (int y = x; y < array.length; y++) {
temp[++index] = array[x][y];
}
}
return temp;
}
列序压缩存储:
public int[] ColumnCompression(int[][] array) {
int len = array.length * (array.length + 1) / 2;
int[] temp = new int[len + 1];
int index = 0;
for (int x = 0; x < array.length; x++) {
for (int y = 0; y <= x; y++) {
temp[++index] = array[y][x];
}
}
return temp;
}
注:整个类的代码在笔记本的文件中。
对角矩阵压缩存储
对角矩阵(也叫带状矩阵)和三角矩阵一样元素在矩阵中分布具有规律,其非零元素都分布在主对角线和主对角线两侧,其他位置的数据元素都为零(对角线为奇数)一般有三对角线矩阵,5对角线矩阵,那么进行压缩存储的时候只要存储主对角线及上下两条对角线的数据即可。对角矩阵图如下:
注:一下我们所讨论的是三对角矩阵,矩阵下标从1开始计算
对角矩阵压缩后的空间计算:
我们观察上图发现对角矩阵除了第一行和最后一行有两个数据元素其他行都有三个数据元素,那么总的数据元素=2+2+(n-2)*3,我们在将上式进行变式,我们知道第一行和最后一行的数据个数是不是可以用总的减去第一行和第二行数据的差值加上最后一行和倒数第二行元素的差值,可以得到如下公式:
行序压缩存储:
行序压缩存储A[i][j]在内存中的位置计算:
由带状矩阵的特性我们可知第i行前面没行都有三个数据元素除了第一行有两个数据元素,可以推得计算i行前面的数据元素公式:3(i-1)-1。
j列前面的元素我们使用数学证明法计算:
-
A[2][3] --> 2
-
A[3][2] --> 0
-
A[3][4] --> 2
-
A[4][4] -->1
最终可以得到公式j列前面有:j-i+1个数据元素。
综上推导可以得到如下计算公式:
压缩存储后A[i][j]在一维数组中的位置:
推算方法和三角矩阵的一样,这里借助上面计算A[i][j]位置的公式,得到如下公式:
列序压缩存储:
列序压缩存储A[i][j]在内存中的位置计算:
列序和行序一样j列前每列都有3个数据元素在减去第一列差着的1个数据元素,得到计算公式:3(j-1)-1。
第i行前面有几个数据元素和行序推导是一样的得到计算公式:i-j+1
综上所述,得到推导公式:
压缩存储后A[i][j]在一维数组中的位置:
推算方法和上面的行序压缩一样,这里借用上面的A[i][j]的计算公式:
三对角矩阵行序压缩存储代码:
public int[] RowCompression(int[][] array){
int len = 3*array.length-2;
int index = 0;
int[] temp = new int[len+1];
//处理第0行
for(int z=0;z<2;z++){
temp[++index] = array[0][z];
}
//处理1 - n-2行的数据
for(int x=1;x<array.length-1;x++){
for(int y=x-1;y<=x+1;y++){
temp[++index] = array[x][y];
}
}
//处理第n-1行
for(int k=array.length-1;k<=array.length;k++){
temp[++index] = array[array.length-1][k-1];
}
return temp;
}
上述代码写得不是很好(很糟糕)第0行和n-1行的数据规律有点特殊暂时找不出来 只能分开处理,列序存储也是。
三对角矩阵列序压缩存储代码:
public int[] CloumCompression(int[][] array){
int len = 3*array.length-2;
int index = 0;
int[] temp = new int[len+1];
for(int z=0;z<2;z++){
temp[++index] = array[z][0];
}
for(int x=1;x<array.length-1;x++){
for(int y=x-1;y<=x+1;y++){
temp[++index] = array[y][x];
}
}
for(int k=array.length-1;k<=array.length;k++){
temp[++index] = array[k-1][array.length-1];
}
return temp;
}
稀疏矩阵压缩存储
稀疏矩阵是指在矩阵中大对数元素为零的矩阵,数学表示法为当矩阵中非零元素个数低于总元素个数的30%即为稀疏矩阵。
稀疏矩阵的数据分布没有规律,不像三角矩阵,对角矩阵元素分布有一定规律,稀疏矩阵只能用三元组表示法存储非零元素。使用三元组表示法存储数据元素在矩阵中的行值,列值和数据元素的值。
三元组结构:
稀疏矩阵压缩存储默使用行序进行压缩存储,将矩阵的没一行(由小到大)的全部非零元素的三元组按行号递增存放。
假设稀疏矩阵M如下:
转换后的三元组表如下:
一维数组下标 | 行号(Row) | 列号(Column) | 元素值 |
---|---|---|---|
1 | 1 | 2 | 1 |
2 | 1 | 3 | 5 |
3 | 2 | 1 | 3 |
4 | 3 | 3 | 7 |
5 | 4 | 4 | 4 |
6 | 5 | 2 | 2 |
7 | 5 | 5 | 8 |
稀疏矩阵使用三元组压缩后,一维数组的长度变为了8(0号位不适用),假设存储的是int类型变量在稀疏矩阵中需要254=100个字节存储,在一维数组中假设使用一个类进行封装每个封装类占 127 = 84个字节(还是节约了点的),最重要的是遍历矩阵可以在一个O(n)的时间复杂度遍历完,二遍历原矩阵需要O(n^2)的时间复杂度才能遍历完。
稀疏矩阵压缩算法(基于java编写):
//稀疏矩阵压缩 三元组类
public class SparseMatrixNode {
public int row;
public int column;
public int value;
public SparseMatrixNode(int row, int column, int value) {
this.row = row;
this.column = column;
this.value = value;
}
public void setColumn(int column) {
this.column = column;
}
public void setRow(int row) {
this.row = row;
}
public void setValue(int value) {
this.value = value;
}
public int getColumn() {
return column;
}
public int getRow() {
return row;
}
public int getValue() {
return value;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "row:" + this.row + " column:" + this.column + " value:" + value;
}
}
//三元组压缩主类
public class SparseMatrix {
// 稀疏矩阵
public int[][] sparseArray;
// 压缩后的三元组表
public SparseMatrixNode[] Tripe;
// 稀疏矩阵的行数
public int sparseArrayRow;
// 稀疏矩阵的列数
public int sparseArrayColumn;
// 稀疏矩阵的非0元素个数
public int sparseArrayNum;
/**
* 初始化数据
*
* @param sparseArray
*/
public SparseMatrix(int[][] sparseArray) {
this.sparseArray = sparseArray;
this.sparseArrayNum = this.getNonZero(sparseArray);
this.sparseArrayRow = sparseArray.length;
this.sparseArrayColumn = sparseArray[0].length;
}
/**
* 压缩稀疏矩阵
*
* @return
*/
public SparseMatrixNode[] Compression() {
SparseMatrixNode[] smn = new SparseMatrixNode[this.sparseArrayNum + 1];
int index = 0;
for (int x = 1; x < this.sparseArray.length; x++)
for (int y = 1; y < this.sparseArray.length; y++)
if (this.sparseArray[x][y] != 0)
smn[++index] = new SparseMatrixNode(x, y, this.sparseArray[x][y]);
return smn;
}
/**
* 获取元素中非0元素个数
*
* @param array
* @return
*/
private int getNonZero(int[][] array) {
int num = 0;
for (int x = 0; x < array.length; x++) {
for (int y = 0; y < array.length; y++) {
if (array[x][y] != 0)
num++;
}
}
return num;
}
}
稀疏矩阵转置算法:稀疏矩阵转置是指改变元素的位置,把位于(row,col)位置上的元素转置为(col,row)位置上(将行列位置 置换),置换后56的矩阵变为65。
算法一:直接在二维数组上面进行转置,循环遍历原二维数组将更改后的下标存储入新的二维数组中,存储完后将新的二维数组进行排序便可。
public int[][] TransMatrix() {
if (this.sparseArray == null || this.sparseArray.length <= 0)
return null;
int[][] temp = new int[this.sparseArrayColumn][this.sparseArrayRow];
for (int x = 1; x <= this.sparseArray.length; x++) {
for (int y = 1; y <= this.sparseArray[x].length; y++) {
temp[y][x] = this.sparseArray[x][y];
}
}
return temp;
}
算法一总的来说思路比较简单但是时间复杂度较大,转置所需O(n2)的时间加上排序所需的O(n2)的时间,在时间复杂度分析的加法法则中虽然最终的时间复杂度还是O(n^2)但是他还是低效的算法。
算法二:在转置稀疏矩阵中矩阵A的列变为矩阵B的行那么我们是不是可以由遍历矩阵改为遍历三元组,在三元组中从A组的第一列开始开始置换,然后是第二列当遍历完后得到的新的三元组表是不是就是转置过后而且是有序的三元组表。
public SparseMatrixNode[] TransposeTSMatrix() {
if (this.Tripe == null)
return null;
SparseMatrixNode[] temp = new SparseMatrixNode[this.Tripe.length];
int index = 0;
for (int x = 1; x < this.sparseArrayColumn; x++)
for (int y = 1; y < this.Tripe.length; y++)
if (this.Tripe[y].column == x)
// 如果是第一列
temp[++index] = new SparseMatrixNode(x, this.Tripe[y].row, this.Tripe[y].value);
return temp;
}
算法二:的时间复杂度相对与算法一时间复杂度要更好一些,O(n) = O(A.colTripe.length)该算法最好的时间复杂度是当mn矩阵中只有一个非零元素那么算法时间复杂度为O(A.col),算法最坏时间复杂度为mn矩阵中的非零元素为总数的30%,时间复杂度为O(n) = O(A.col(m*n)*0.3)
算法三:一次查询快速定位法,如果我们能够在扫描一次三元组表便能计算出装换后的三元组表位置那么时间复杂度是不是就能从乘积阶降为线性阶?
一维数组下标 | 行号(Row) | 列号(Column) | 元素值 |
---|---|---|---|
1 | 1 | 2 | 1 |
2 | 1 | 3 | 5 |
3 | 2 | 1 | 3 |
4 | 3 | 3 | 7 |
5 | 4 | 4 | 4 |
6 | 5 | 2 | 2 |
7 | 5 | 5 | 8 |
我们观察上面的三元组表当列为1的数据元素转化后行数还是1在一维数组中的第一个位置那就是1,那么第一列有几个数据元素在一维数组中就占几个元素空间,那么列为2的元素是在列为1的后面还是一样的列为2有几个元素那么在一维数组中就占空间,加上列1的元素就是列2的起始位置:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
三元组 | 3 | 1 | 2 | 5 | 6 |
我们观察上面的一维数组第一列有一个数据占用一个位置,第2列有2个占用2个位置,第三列有2个占用2个位置,我们在观察下面这个表:
col | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
num | 1 | 2 | 2 | 1 | 1 |
position | 1 | 2 | 4 | 6 | 7 |
表中的col是三元组表的列,num是每列中非零元素的个数,position是该列第一个非零元素的起始位置,当知道每列第一个元素的起始位置和有几个元素就能计算出该列最后一个元素的位置,下一列的起始位置就是上一列终止位置+1,循环计算便能得到每列每个数据元素在转换后表的位置,我们可以得到如下的抽象公式:
上面这个抽象计算的是每列第一个非零元素的第一个位置
算法代码如下:
public SparseMatrixNode[] TransposeTSMatrixPosition() {
if (this.Tripe == null)
return null;
SparseMatrixNode[] temp = new SparseMatrixNode[this.Tripe.length];
int[] num = new int[this.sparseArrayColumn];
int[] position = new int[this.sparseArrayColumn];
int k = 0;
// 计算每列中非零元素的个数
for (int x = 1; x <= this.Tripe.length; x++)
num[Tripe[x].getColumn()]++;
// 计算每列第一个非零元素的起始位置
position[1] = 1;
for (k = 2; k <= this.sparseArrayColumn; k++)
position[k] = position[k - 1] + num[k - 1];
// 扫描三元组表并且装置
for (int y = 1; y <= this.Tripe.length; y++) {
k = this.Tripe[y].getColumn();
int q = position[k];
temp[q] = new SparseMatrixNode(Tripe[y].getRow(), Tripe[y].getColumn(), Tripe[y].getValue());
position[k]++;
}
return temp;
Column();
int q = position[k];
temp[q] = new SparseMatrixNode(Tripe[y].getRow(), Tripe[y].getColumn(), Tripe[y].getValue());
position[k]++;
}
return temp;
}
算法分析:算法三去掉了双层for循环将时间复杂度从乘数阶降到了线性阶,上述代码的时间复杂度为 O(n) = O(Tirp.length)+O(sparseArrayColumn)+O(Tripe.length) ,根据时间复杂度分析的加法上述代码的时间复杂度为 O(n) = O(Tripe.length) ;算法三相对于算法二和算法一时间复杂度有了很多大的改进,但是这个改进是以消耗内存改变的。