数组,相信大家对这个名词不陌生,他在高级语言中都已经物理实现为数组数据类型。数组是一个特殊的线性表,特殊在元素受多个线性关系约束。因为数组的特殊性,所以其定义的操作非常少。
接下来我将讲一下数组的一些知识以及利用数组实现一种特殊的数据结构——矩阵。
(我结合我们老师的PPT给大家讲解,其中会夹杂一下我本人对某些问题的理解和看法,说的不对的地方,还请大家不吝赐教。)
目录:
一、数组的基本知识
1.数组的逻辑结构
2.数组上定义的操作
3.数组ADT
4.数组的存储方式
5.数组操作的实现
二、矩阵及特殊矩阵
1.矩阵ADT定义
2.矩阵ADT实现
3.特殊矩阵的存储及操作的实现
3.1 对称矩阵、上三角矩阵、下三角矩阵
3.2 三对角矩阵
3.3 稀疏矩阵
一、数组的基本知识
数组(Array),简单说就是数据类型相同的一组数据元素的有序集合(当然,在Python中不是,Python中的数组被称为列表(list),可以同时包含多种数据类型的元素)。元素之间可以具有多个线性关系。元素在集合中的位置是由元素在每一个线性关系上的位置共同决定的。
数组中元素的特点是参与多个(>=1)线性关系,在每个关系上都有前驱、后继。
数组中元素的下标表示元素在某个关系上所处的位置;数组的维数表示元素参与线性关系的个数。
1.数组的逻辑结构
以二维数组为例,二维数组的逻辑结构如下:
其中,集合D代表数组中的元素,c1...d1、c2...d2表示元素的下标,一般为1到n;集合R代表数组中的线性关系,R中的元素个数表示数组中每个元素参与的线性关系的个数。
2.数组上定义的操作
由于数组中的元素受多个线性关系的约束,正所谓“牵一发而动全身”,因此一般只对数组做静态数据处理,不进行诸如插入、删除这样的操作!(Python中的列表可以进行插入删除操作,有专门的函数,比如list.insert()、list.remove()等)
(1)初始化:为数组分配存储空间;
(2)元素的存取:已知下标访问元素。
Python中的列表(数组)不用初始化,只需要如下一行代码:
list = []
就可以定义一个理论上无限大的列表(数组)。
3. 数组ADT
由于数组这种线性数据结构的特殊性(多个线性关系约束元素),其上定义的操作很简单,所以数组ADT实现一般都采用顺序存储结构。几乎在所有语言中,都已经把数组ADT物理实现了,即我们可以直接使用数组数据类型来求解问题。
4.数组的存储结构
存储方式:顺序存储,即用地址连续的一段存储空间一次存放数组的各个元素。
数组的存储方式,使其产生了两个问题:
需要开辟多大的空间?
如何把元素“依次”存放到空间中去而且把关系也表示出来?
显然,一位数组很简单,按照线性关系顺序存放就可以了,即,第一个元素存放在空间的第一个位置,第二个元素存放在空间的第二个位置,.......。关系通过物理相邻表示出来。空间的开辟也很简单,按照数组中元素的个数开辟合适的空间即可。
二维数组时,元素如何存放进去?即第i个空间存放哪个元素?此时,逻辑上元素的位置是由行和列位置共同决定的,即按行计算出的元素位置和按列计算出的元素位置是不一样的。
一般地,多维数组中,数组元素的位置是由是由数据元素在各个关系上的位置确定的,即下标值,那么,哪个关系先,哪个关系后?
通常,把线性关系正向或反向顺序排列,例如对二维数组,两个线性关系,顺序有“行—列”或“列—行”。分别称为行主序和列主序。
R1→R2 →R3 → …… → Rk-1→ Rk
Rk→Rk-1 →Rk-2 → …… → R2→ R1
关系的优先次序约定好后,所有元素就可以唯一的排出一个次序。
假设k维数组,其各维的界为: ( c1..d1, c2..d2, ... ..., ck..dk) ,则数组的元素个数
n=(d1-c1+1)*(d2-c2+1)...*(dk-ck+1)
假设每个数据元素占用l空间,则占用空间为:
Size=n*l==(d1-c1+1)*(d2-c2+1)...*(dk-ck+1)*l
然后,元素按照线性关系的“顺序”就有一个排列顺序,从第1~n个。则序号为i的元素存放到第i个空间去。
例如,二维数组A[1...4,1...5]
空间大小=4*5*l=20l
元素的存储:
行主序和列主序,通俗点理解,就是谁先不变,比如行主序,就是行号先不变,改变列号;列主序就是列号先不变,改变行号。
元素依次存放,关系是用物理上相邻来表示的。那么如何表示出多个线性关系呢?即逻辑关系和物理地址应如何对应?
一维数组:逻辑与物理完全一致。
A[c1...d1]
行主序:LOC(a[i]) = LOC(a[c1])+(i-c1)*l
列主序:LOC(a[i]) = LOC(a[c1])+(i-c1)*l
二维数组:逻辑上(i,j)与一个物理位置对应。
A[c1...d1,c2...d2]
行主序:LOC(a[i,j])=LOC(a[c1,c2])+(d2-c2+1)*(i-c1)*l+(j-c2)*l(先按行填满)
列主序:LOC(a[i,j])=LOC(a[c1,c2])+(d1-c1+1)*(j-c2)*l+(i-c1)*l(先按列填满)
数组存储的特点:随机存取,即存取任何元素花费的时间相同。
数组存储的实现:根据数组说明,得到数组的元素个数,及元素类型(根据类型可计算出每个元素所占用的物理空间大小),然后,分配连续空间,空间的首地址存储在数组名中。
5.数组操作的实现
已知要访问的元素的下标,则根据存储方式可以计算出该元素的存储地址(根据前面的公式),于是可以存取该元素。
二、矩阵及特殊矩阵
1.矩阵ADT定义
2.矩阵ADT实现
存储结构:二维数组
操作实现:
用C++写的一般矩阵ADT完整的操作实现,我这人有点强迫症,写代码必须要写能运行的,而且要写的比较友好,所以代码可能有点长,大家可以挑对自己有用的代码看。下面的代码很完整,大家可以直接贴到IDE里运行,看一看效果。(PS:C++库函数的丰富程度是真的不如Python,一个矩阵相乘,Python一行代码就能实现,C++要写大半天。。。)
#include <iostream>
#include <malloc.h>
using namespace std;
class NormalMatrix{
public:
int **Matrix;
int Row,Col;//行数和列数
public:
NormalMatrix(int row,int col);//构造函数
~NormalMatrix(){
for(int i=0;i<Row;i++){delete Matrix[i];}
delete Matrix;
}//析构函数
void Tran();//矩阵转置
void Mult();//矩阵乘法
void Sum();//矩阵加法
void Less();//矩阵减法
void Input();//矩阵输入
void PrintOut();//矩阵输出
};
void NormalMatrix::Mult(){
int row,col;
int temp;
cout<<"请输入另外一个矩阵的行数和列数:"<<endl;
cin>>row>>col;
if(row!=Col||col!=Row){
cout<<"列数或行数不相同,无法相乘!"<<endl;
}
else{
//定义两个矩阵,一个存放另一个矩阵,另一个存放两矩阵相乘后的矩阵
int **multMatrix,**mt;
//给两个矩阵开辟空间
multMatrix=(int **) malloc(Col*sizeof(int *)) ;
mt=(int **) malloc(Col*sizeof(int *)) ;
for(int i=0; i<Col; i++){
multMatrix[i] = (int *)malloc(Row * sizeof(int ));
mt[i] = (int *)malloc(Row * sizeof(int ));
}
//输入另一个矩阵的元素
for(int i=0;i<Col;i++){
for(int j=0;j<Row;j++){
cout<<"请输入第"<<i+1<<"行第"<<j+1<<"个元素"<<endl;
cin>>mt[i][j];
}
}
//相乘
for(int i=0;i<Row;i++){
for(int j=0;j<Row;j++){
multMatrix[i][j] = 0;
for(int k=0;k<Col;k++){
multMatrix[i][j] = multMatrix[i][j] + Matrix[i][k]*mt[k][j];
}
}
}
//输出相乘后的结果
cout<<"相乘后,结果为:"<<endl;
for(int i=0;i<Col;i++){
for(int j=0;j<Row;j++){
cout<<" "<<multMatrix[i][j];
}
cout<<endl;
}
}
}
void NormalMatrix::Sum(){
//矩阵加法
int row,col;
int temp;
cout<<"请输入另外一个矩阵的行数和列数:"<<endl;
cin>>row>>col;
if(row!=Row||col!=Col){
cout<<"行数或列数不相同,无法相加!"<<endl;
}
else{
//定义一个矩阵存放两矩阵相加后的矩阵
int **sumMatrix;
//为矩阵开辟空间
sumMatrix=(int **) malloc(Row*sizeof(int *)) ;
for(int i=0; i<Col; i++){
sumMatrix[i] = (int *)malloc(Col * sizeof(int ));
}
//输入另一个矩阵的元素
for(int i=0;i<Row;i++){
for(int j=0;j<Col;j++){
cout<<"请输入第"<<i+1<<"行第"<<j+1<<"个元素"<<endl;
cin>>temp;
//直接相加
sumMatrix[i][j] = Matrix[i][j]+temp;
}
}
//输出对应矩阵
cout<<"加法完成!相加后:"<<endl;
for(int i=0;i<Row;i++){
for(int j=0;j<Col;j++){
cout<<" "<<sumMatrix[i][j];
}
cout<<endl;
}
}
};
void NormalMatrix::Less(){
//矩阵减法
int row,col;
int temp;
cout<<"请输入另外一个矩阵的行数和列数:"<<endl;
cin>>row>>col;
if(row!=Row||col!=Col){
cout<<"行数或列数不相同,无法相加!"<<endl;
}
else{
int **lessMatrix;
lessMatrix=(int **) malloc(Row*sizeof(int *)) ;
for(int i=0; i<Col; i++){
lessMatrix[i] = (int *)malloc(Col * sizeof(int ));
}
//输入另一个矩阵的元素
for(int i=0;i<Row;i++){
for(int j=0;j<Col;j++){
cout<<"请输入第"<<i+1<<"行第"<<j+1<<"个元素"<<endl;
cin>>temp;
lessMatrix[i][j] = Matrix[i][j]-temp;
}
}
cout<<"减法完成!相减后:"<<endl;
for(int i=0;i<Row;i++){
for(int j=0;j<Col;j++){
cout<<" "<<lessMatrix[i][j];
}
cout<<endl;
}
}
};
void NormalMatrix::Tran(){
//矩阵转置函数
//将转置后的矩阵单独存到一个二维数组中
int **tranMatrix;
tranMatrix=(int **) malloc(Col*sizeof(int *)) ;
for(int i=0; i<Col; i++){
tranMatrix[i] = (int *)malloc(Row * sizeof(int ));
}
//转置操作
for(int i=0;i<Col;i++){
for(int j=0;j<Row;j++){
tranMatrix[i][j] = Matrix[j][i];
}
}
Matrix = tranMatrix;
cout<<"转置完成!"<<endl;
};
void NormalMatrix::Input(){
//矩阵输入函数
cout<<"矩阵行数:"<<Row<<endl;
cout<<"矩阵列数:"<<Col<<endl;
for(int i=0;i<Row;i++){
for(int j=0;j<Col;j++){
cout<<"请输入第"<<i+1<<"行第"<<j+1<<"个元素"<<endl;
cin>>Matrix[i][j];
}
}
cout<<"输入完成!"<<endl;
};
void NormalMatrix::PrintOut(){
//矩阵输出函数
for(int i=0;i<Row;i++){
for(int j=0;j<Col;j++){
cout<<" "<<Matrix[i][j];
}
cout<<endl;
}
cout<<"输出完成!"<<endl;
};
NormalMatrix::NormalMatrix(int row,int col){
Row = row;
Col = col;
Matrix=(int **) malloc(Row*sizeof(int *)) ;
for(int i=0; i<Row; i++){
Matrix[i] = (int *)malloc(Col * sizeof(int ));
}
};
int main()
{
NormalMatrix nm(3,3);
//矩阵输入
nm.Input();
nm.PrintOut();
//矩阵加法
nm.Sum();
//矩阵减法
nm.Less();
//矩阵转置
nm.Tran();
//矩阵乘法
nm.Mult();
return 0;
}
3.特殊矩阵的存储及操作的实现
对于一个矩阵结构采用二维数组来存储是非常恰当的,但在某些情况下,比如一些常见的特殊矩阵,如三角矩阵、对称矩阵、带状矩阵、稀疏矩阵等,从解决存储空间的角度考虑,这种存储是不太合适的。因此,我们采用矩阵压缩存储的方法来存储特殊矩阵。(当然,存储简单了,操作就复杂了。)
3.1对称矩阵、上三角矩阵、下三角矩阵
这类矩阵,真正需要存储的元素有n*(n+1)/2个。
n+(n-1)+(n-2)+…+2+1=n*(n+1)/2(从1到n的加法,不会真有人不会算吧,哈哈)
因此只需要开辟n*(n+1)/2个连续空间就可以存放这样的矩阵。
压缩存储方式:对下三角或上三角矩阵元素排成一个顺序,排在第i的元素子在第i个位置。
显然,也有按行和按列两种顺序。
以下三角对称矩阵(因为对称,所以只存储下三角的元素,上三角的元素可按对称矩阵的特点得出)行主序为例,可以推导出其物理地址的计算公式:
对于下三角矩阵a中的任何一个元素aij,当i>=j时,元素位于下三角部分,因此要在空间中存放,其存储位置为:
LOC(aij)=1+2+3+…+i + j =(i+1)*i/2+j i>=j
而对于下三角矩阵a中的任何一个元素aij,当i<j时,元素位于上三角部分,没有在空间中存放,但其对称元素aji存放了,因此,也可以计算出其存储位置(就是其对称位置的元素的存储位置):
LOC(aij)= LOC(aji)=1+2+3+…+j + i =(j+1)*j/2+i i<j
当然,对于下三角矩阵,如果其上三角的所有元素都是0的话,即i<j时,aij=0,那么也就没有必要计算它的存储位置了。
类似,对于上三角矩阵,也可以采用相同的存储策略来存储(也分行主序和列主序)。
对于上三角矩阵中的任何以一个元素aij,假设采用行主序,其存储地址计算公式如下:
LOC(aij)= n +(n-1)+(n-2)+ ... +(n-i+1)+ j-i =(2*n-i-1) * i / 2 + j i≤j
LOC(aij)= (2*n-j-1) * j / 2 + i i≥j
相反,对于存储空间中的一个元素bk,它唯一对应下三角矩阵中的一个元素aij。
若已知某矩阵元素位于连续存储空间的第 k个位置(k≥0), 可寻找满足 i (i + 1) / 2 <= k < (i + 1)*(i + 2) / 2 的 i, 此即为该元素的行号。 j = k - i * (i + 1) / 2 此即为该元素的列号。
3.2 三对角矩阵
三对角矩阵中除主对角线及在及在主对角线上下最邻近的两条对角线上的元素外,所有其他元素均为0,总共有3*n-2个元素。
对于三对角矩阵,如果采用一般的存储方式(使用二维数组),需要n^2个存储单元,有n^2-3*n+2个存放0.空间效率低。
压缩存储方式:思想同上,即用连续存储空间只存放非零元素,即开辟3*n-2个连续空间单元。
对于三对角矩阵中的任何一个元素aij, 当0<=i<=n-1,i-1<=j<=j+1时,在存储空间中有存放,存储位置可以计算出来:
LOC(aij)=3*i-1 + j-i+1 =2*i+j
相反,若已知三对角矩阵中某元素aij在空间中的存储位置为k,也可以推出其在原三对角矩阵中的逻辑位置:
i = (k+1)/3
j = k - 2*i
3.3 稀疏矩阵
稀疏矩阵:零元素比较多,而且分布不均匀(没有规律)。
设矩阵A中有s个非零元素,若s远远小于矩阵元素的总数 (s<<m*n),则称A为稀疏矩阵。
令e=s/(m*n),称e为矩阵的稀疏因子。
有人认为e<=0.05时称之为稀疏矩阵。
显然,用一般的存储方式(二位数组存储)会浪费大量的存储空间,用上述对称矩阵和带状矩阵的存储方式也不行,因为稀疏矩阵非零元素的分布是不规律的,位置不确定,所以,要存储稀疏矩阵,不仅要存储非零元素的值,还要存储非零元素的位置。
我们采用一个三元组(i,j,v)来表示每个非零元素。于是一个稀疏矩阵就是这样一些三元组的集合,考虑它们在矩阵中的位置,稀疏矩阵就可以抽象为一个以三元组为数据元素的线性表(线性关系可以是行或列)。
例如有稀疏矩阵:
0 0 1 0 2
0 0 0 0 0
1 0 0 0 0
0 0 1 0 -1
可以抽象为线性表:
按行: ( (1,3,1), (1,5 2), (3,1,1), (4,3,1), (4,5,-1) )
按列: ( (3,1,1), (1,3,1), (4,3,1), (1,5,2), (4,5,-1) )
和线性表ADT一样,稀疏矩阵也有顺序和链式存储两种存储方式。
三元组顺序存储:
(1)三元组定义:
typedef 矩阵元素类型 ElemType;
struct Triple //三元组定义,即线性表元素类型
{
int row, col; //非零元素行号/列号
ElemType value; //非零元素的值
void operator = (Triple & R) //赋值
{ row = R.row; col = R.col; value = R.value; }
};
(2)三元组类定义:
class SparseMatrix //稀疏矩阵三元组类定义
{
public:
SparseMatrix (int Rw = drows, int Cl = dcols, int Tm = dterms);//构造函数
void Input();//输入
void Output();//输出
void Transpose(SparseMatrix& b); //转置
void Add (SparseMatrix& a, SparseMatrix& b); //a = a+b
void Multiply (SparseMatrix& a, SparseMatrix& b); //a = a*b
public:
int Rows, Cols, Terms; //矩阵的行、列,非零元素数
Triple *smArray; //三元组表,顺序存储
};
(3)操作实现:
记得我们老师讲特殊矩阵的时候,着重讲了稀疏矩阵,练习题大部分也都是稀疏矩阵的,想来这个矩阵应该是比较重要的,所以我这里简单写一下稀疏矩阵各个操作的实现。(因为C++写起来有点麻烦,所以用Python写了; 因为自身水平不够,所以没写乘法)
#三元组类定义
class Triple:
def __init__(self,r=-1,c=-1,v=0):
self.row = r #行号
self.col = c #列号
self.value = v #值
#稀疏矩阵三元组类定义
class SparseMatrix:
def __init__(self,row,col,terms):
self.Rows = row #矩阵行数
self.Cols = col #矩阵列数
self.Terms = terms #矩阵非零元素个数
self.smArray = [] #存储非零元素三元组
#输入
def Input(self):
for i in range(self.Terms):
triple = Triple()
print("请输入第"+str(i+1)+"个非零元素的行数:",end='')
triple.row = int(input())
print("请输入第"+str(i+1)+"个非零元素的列数:",end='')
triple.col = int(input())
print("请输入第"+str(i+1)+"个非零元素的元素值:",end='')
triple.value = int(input())
self.smArray.append(triple)
#输出
def Output(self):
for i in range(self.Terms):
print(str(self.smArray[i].row)+" "+str(self.smArray[i].col)+" "+str(self.smArray[i].value))
def Tran(self):
for i in range(self.Terms):
temp = self.smArray[i].row
self.smArray[i].row = self.smArray[i].col
self.smArray[i].col = temp
print("转置完成")
#加法
def Add(self,smb):
for i in range(self.Terms):
for j in smb.smArray:
if self.smArray[i].row==j.row and self.smArray[i].col==j.col:
self.smArray[i].value += j.value
smb.smArray.remove(j)
smb.Terms -= 1
self.smArray.extend(smb.smArray)
#减法
def Less(self,smb):
for i in range(self.Terms):
for j in smb.smArray:
if self.smArray[i].row==j.row and self.smArray[i].col==j.col:
self.smArray[i].value -= j.value
smb.smArray.remove(j)
smb.Terms -= 1
for i in smb.smArray:
i.value = -i.value
smb.Output()
self.smArray += smb.smArray