最近在看数据结构(C语言描述)
一切开始于一个用C语言实现两个矩阵相乘的问题。
A×B=C
其中A表示一个m×n的矩阵,B代表一个n×r的矩阵(能相乘的矩阵,左矩阵的列必须要等于有矩阵的行)。那么得到的C理应是一个m×r的矩阵。
一、用二维数组来实现
其实最和矩阵结构相似的莫过于二维数组了。
假设我要写一个方法,带3个参数,3个参数都分别是一个二维数组。分别代表A矩阵,B矩阵和C矩阵。代码可以写成下面的样子:
#include <stdio.h>
int main(){
void MatrixMult(int [2][4], int [4][3], int [2][3]);
int A[2][4] = {{1,0,3,-1},{2,1,0,2}};
int B[4][3] = {{4,1,0},{-1,1,3},{2,0,1},{1,3,4}};
int C[2][3];
MatrixMult(A,B,C);
for(int i=0; i<2; i++){
for(int j=0; j<3; j++){
printf("%d\t",C[i][j]);
}
printf("\n");
}
return 1;
}
void MatrixMult(int A[2][4], int B[4][3], int C[2][3]){
int s = 0;
for(int i=0; i<2; i++){
for(int j=0; j<3; j++){
s = 0;
for(int k=0; k<4; k++){
s += A[i][k]*B[k][j];
}
C[i][j] = s;
}
}
}
代码执行结果:
9 -2 -1
9 9 11上述代码有些不理想的地方,其一就在于MatrixMult这个矩阵相乘的方法的定义上。它只能计算一个2×4的矩阵乘以一个4×3的矩阵,并将结果存到一个2×3的矩阵中。由于使用数组就必须指定数组长度,二维数组即便不需要指定一维长度,也要指定二维,也就是说上述代码中的方法定义,简化数组维数指定之后可以写成这样:
#include <stdio.h>
int main(){
void MatrixMult(int [][4], int [][3], int [][3],int );
int A[2][4] = {{1,0,3,-1},{2,1,0,2}};
int B[4][3] = {{4,1,0},{-1,1,3},{2,0,1},{1,3,4}};
int C[2][3];
MatrixMult(A,B,C,2);
for(int i=0; i<2; i++){
for(int j=0; j<3; j++){
printf("%d\t",C[i][j]);
}
printf("\n");
}
return 1;
}
void MatrixMult(int A[][4], int B[][3], int C[][3],int n){
//相比较多了一个参数n是因为在省略了数组的一维长度之后,变无从得知数组A有几行了,所以要通过参数传进来。
int s = 0;
for(int i=0; i<n; i++){
for(int j=0; j<3; j++){
s = 0;
for(int k=0; k<4; k++){
s += A[i][k]*B[k][j];
}
C[i][j] = s;
}
}
}
但仍是不能适应任意大小的两个矩阵相乘。
也许你会说使用二维数组指针,但可惜的是问题同样存在。请看如下代码便知:
#include <stdio.h>
int main(){
void MatrixMult(int (*)[4], int (*)[3], int (*)[3],int);
int A[2][4] = {{1,0,3,-1},{2,1,0,2}};
int B[4][3] = {{4,1,0},{-1,1,3},{2,0,1},{1,3,4}};
int C[2][3];
MatrixMult(A,B,C,2);
for(int i=0; i<2; i++){
for(int j=0; j<3; j++){
printf("%d\t",C[i][j]);
}
printf("\n");
}
return 1;
}
void MatrixMult(int (*A)[4], int (*B)[3], int (*C)[3],int n){
int s = 0;
int left = 0;
int right = 0;
for(int i=0; i<n; i++){
for(int j=0; j<3; j++){
s = 0;
for(int k=0; k<4; k++){
left = *(*(A+i)+k);
right = *(*(B+k)+j);
s += left * right;
}
*(*(C+i)+j) = s;
}
}
}
可以看到,在使用二维指针的时候,同样需要指定第二维的长度,否则程序也无法编译运行。
于是想到构造一个矩阵的自定义类Matrix。想法付诸实践就写了如下的代码:
Matrix.htypedef int MatrixCellType;
/************************************************************************/
/* 矩阵元素 */
/************************************************************************/
struct MatrixCell{
int i;//元素横坐标,从0开始
int j;//元素纵坐标,从0开始
MatrixCellType val;//元素值
};
struct Matrix{
int row;
int column;
MatrixCell *pt;//指向Matrix首个元素的指针
};
/************************************************************************/
/* 通过一个二维数组构建一个矩阵
MatrixCellType*:二维数组的第一个元素的指针
int:二维数组的行
int:二维数组的列
Matrix*:一个指向矩阵的指针,构建好矩阵之后该指针将指向该矩阵*/
/************************************************************************/
void buildMatrix(MatrixCellType* ,int, int, Matrix*);
/************************************************************************/
/* 打印矩阵 */
/************************************************************************/
void printMatrix(Matrix* );
/************************************************************************/
/* 给矩阵某个位置设置元素
Matrix*:矩阵
MatrixCell*:要设置的元素*/
/************************************************************************/
void setMatrixCell(Matrix* , MatrixCell* );
/************************************************************************/
/* 设置一个矩阵某个位置的元素的设置值 */
/************************************************************************/
void setMatrixCellVal(Matrix* , int , int , MatrixCellType );
/************************************************************************/
/* 获取一个矩阵元素 */
/************************************************************************/
void getMatrixCell(Matrix* , int , int, MatrixCell* );
/************************************************************************/
/* 获取一个矩阵元素的值 */
/************************************************************************/
MatrixCellType getMatrixCellVal(Matrix* , int , int );
/************************************************************************/
/* 矩阵乘法 */
/************************************************************************/
void MatrixMult(Matrix* , Matrix* , Matrix* );
Matrix.cpp
#include <stdio.h>
#include <stdlib.h>
#include "Matrix.h"
#define ROW_A 3//第1个矩阵的行数
#define COL_A_ROW_B 2//第1个矩阵的列数,也是第2个矩阵的行数。因为这是满足矩阵相乘的条件。即第1个矩阵的列=第2个矩阵的行
#define COL_B 2//第2个矩阵的列数
int main(){
/*MatrixCellType A_arr[ROW_A][COL_A_ROW_B]={{1,2,3},{2,1,4},{3,4,1}};
MatrixCellType B_arr[COL_A_ROW_B][COL_B]={{5,6,7},{6,5,8},{7,8,5}};*/
MatrixCellType A_arr[ROW_A][COL_A_ROW_B]={{1,2},{4,5},{3,6}};
MatrixCellType B_arr[COL_A_ROW_B][COL_B]={{3,4},{1,2}};
MatrixCellType* pt_A = &A_arr[0][0];
MatrixCellType* pt_B = &B_arr[0][0];
Matrix* A = (Matrix*)malloc(sizeof(Matrix));
buildMatrix(pt_A,ROW_A,COL_A_ROW_B,A);
printf("Matrix A:\n");
printMatrix(A);
Matrix* B = (Matrix*)malloc(sizeof(Matrix));
buildMatrix(pt_B,COL_A_ROW_B,COL_B,B);
printf("Matrix B:\n");
printMatrix(B);
Matrix* C = (Matrix*)malloc(sizeof(Matrix));
buildMatrix(NULL,ROW_A,COL_B,C);
MatrixMult(A,B,C);
printf("Matrix C:\n");
printMatrix(C);
return 1;
}
void buildMatrix(MatrixCellType* pt,int row, int column, Matrix* M){
M->row = row;
M->column = column;
//没有办法根据形参来定义这样的一个数组MatrixCell cell_arr[row][column];
//所以只能使用下面的动态内存分配的办法,来申请一块内存地址,用来存放row*column个元素,并将M->pt的指针指向第一个元素的地址
int size = sizeof(MatrixCell)*row*column;
M->pt = (MatrixCell *)malloc(sizeof(MatrixCell)*row*column);
if (pt != NULL){
for(int i=0; i<row; i++){
for(int j=0; j<column; j++){
(M->pt+column*i+j)->i = i;
(M->pt+column*i+j)->j = j;
(M->pt+column*i+j)->val = *(pt+column*i+j);
}
}
}
}
void printMatrix(Matrix* M){
for(int i=0; i<M->row; i++){
for(int j=0; j<M->column; j++){
MatrixCell* cell = M->pt+i*M->column+j;
printf("%d\t", cell->val);
}
printf("\n");
}
}
void setMatrixCell(Matrix* M, MatrixCell* cell){
if(M->pt == NULL){
M->pt=(MatrixCell*)malloc(sizeof(MatrixCell));
}
*(M->pt+(cell->i * M->column + cell->j)) = *cell;
}
void setMatrixCellVal(Matrix* M, int i, int j, MatrixCellType val){
MatrixCell* cell = (MatrixCell*)malloc(sizeof(MatrixCell));
//getMatrixCell(M,i,j,cell);
cell->i = i;
cell->j = j;
cell->val = val;
setMatrixCell(M,cell);
}
void getMatrixCell(Matrix* M, int i, int j, MatrixCell* cell){
*cell = *(M->pt + i*M->column + j);
}
MatrixCellType getMatrixCellVal(Matrix* M, int i, int j){
MatrixCell* cell = (MatrixCell*)malloc(sizeof(MatrixCell));
getMatrixCell(M, i, j,cell);
return cell->val;
}
void MatrixMult(Matrix* A, Matrix* B, Matrix* C){
MatrixCellType s = 0;
C->row = A->row;
C->column = B->column;
for(int i=0; i<A->row; i++){
for(int j=0; j<B->column; j++){
s = 0;
for(int k=0; k<B->row; k++){
MatrixCellType A_val = getMatrixCellVal(A,i,k);
MatrixCellType B_val = getMatrixCellVal(B,k,j);
//printf("cell_A.val = %d , cell_B.val = %d,\t",cell_A->val,cell_B->val);
s += A_val * B_val;
}
//printf("[%d,%d]=%d\n",i,j,s);
setMatrixCellVal(C,i,j,s);
}
}
}
程序执行的结果是:
上述代码中的MatrixMult方法的三个参数是三个Matrix的自定义类型,该方法可是适应任意大小矩阵的相乘。只需要在调用的地方根据实际的需要构建好具体大小的矩阵即可。
下面来谈谈这个Matrix的几个问题:
1、 Matrix是如何构造的?
Matrix是通过
void buildMatrix(MatrixCellType* pt,int row, int column, Matrix* M)
这个方法类构造的。
第1个参数是一个矩阵元素类型指针,通过它来获取初始化矩阵的各个元素,因此它可以是指向一维数组的,也可以是指向二维数组的,但数组的元素个数必须是row×column个。
第2个参数指明了需要构造的矩阵的行数。
第3个参数指明了需要构造的矩阵的列数。
第4个参数是一个指向Matrix的指针。该指针将指向构造好的矩阵。
2、 为什么选择一维数组指针(即普通的指针)存取Matrix元素,而不是二维数组指针?
使用二维数组指针势必需要指定其中的第二维的长度,因此反而使得它收到了某种限制。使用一维数组指针同样可以达到存取数据的目的。而且在某些特殊矩阵,如对称矩阵,稀疏矩阵的存储上,采用一维数组指针可以减少内存的开销。
3、 再来看看看这个构造Matrix的方法内的一些细节。
void buildMatrix(MatrixCellType* pt,int row, int column, Matrix* M){
M->row = row;
M->column = column;
//没有办法根据形参来定义这样的一个数组MatrixCell cell_arr[row][column];
//所以只能使用下面的动态内存分配的办法,来申请一块内存地址,用来存放row*column个元素,并将M->pt的指针指向第一个元素的地址
int size = sizeof(MatrixCell)*row*column;
M->pt = (MatrixCell *)malloc(sizeof(MatrixCell)*row*column);
if (pt != NULL){
for(int i=0; i<row; i++){
for(int j=0; j<column; j++){
(M->pt+column*i+j)->i = i;
(M->pt+column*i+j)->j = j;
(M->pt+column*i+j)->val = *(pt+column*i+j);
}
}
}
}
这里主要谈一谈为Matrix中*pt指针分配内存的问题。即下面这句代码:
M->pt = (MatrixCell *)malloc(sizeof(MatrixCell)*row*column);
刚开始的时候我错误的写成了
M->pt = (MatrixCell *)malloc(sizeof(MatrixCell));
确实一般的为某一种类型申请内存确实是如上面的这句代码一样去写。但在这个Matrix构造的方法内这样确实有问题的。大家都知道指针只是执行一个地址,对于一个数组的指针,一般是指向开头的第一个元素。这里的Matrix->pt这个指针是指向构造的矩阵中的第一个元素的。如果将Matrix->pt+1则指向第2个元素,依次类推。矩阵中的每一个元素需要一块内存地址来存放(注意是每一个元素)。而
M->pt = (MatrixCell *)malloc(sizeof(MatrixCell));
这句代码只是申请了一块元素的内存,并将其指向了指针Matrix->pt。那么问题就来了矩阵中其他的元素存哪?显然这是错误的。所以在使用malloc申请内存的时候应该是申请rowcolumn块内存,并将第一块的地址赋给指针Matrix->pt。即下面的代码才正确:
M->pt = (MatrixCell *)malloc(sizeof(MatrixCell)*row*column);
再来谈谈Matrix.cpp中几个实现方法中的细节问题,从而归纳一下指针的使用方式:
1、首先来看下 getMatrixCell方法。该方法是获取矩阵 M中的第 i行的第 j个元素,并将参数指针 cell指向获取的值。并且该方法有在 getMatrixCellVal中被调用。void getMatrixCell(Matrix* M, int i, int j, MatrixCell* cell){
*cell = *(M->pt + i*M->column + j);
}
MatrixCellType getMatrixCellVal(Matrix* M, int i, int j){
MatrixCell* cell = (MatrixCell*)malloc(sizeof(MatrixCell));
getMatrixCell(M, i, j,cell);
return cell->val;
}
由于C语言编程不是很熟悉,刚开始的时候将getMatrixCell方法改写成如下的形式:
void getMatrixCell(Matrix* M, int i, int j, MatrixCell* cell){
cell = (M->pt + i*M->column + j);
}
但实际执行程序会发现,程序得到的结果并非是预期的结果。
非预期结果:
经过研究发现这样的做法是不对的。函数形参中的指针是实参指针的一个copy,它和实参是指向同一块内存地址的,但确是两个不同的变量,通过改变形参指针所指向的内存地址的值,相应的实参指针所指的内存地址的值也改变了(因为他们指向同一块地址)。但是如果在函数内,将另一个变量的地址赋给形参指针(即改变形参指针的指向),那么实参是不会有改变的,因为实参和形参是完全独立的两个变量,在你改变形参指针所指向的地址前,他们唯一的关联就是都指向同一个地址,一旦你将实参指向了别的地址,那么实参指针和形参指针就完完全全没有任何关系了。永远不要在函数内改变形参指针的指向(即地址),而要操作地址做指向的内存的值。关于这一点的详细介绍请见另一篇文章《指针参数传值的真相》。
2、 再来说一说矩阵相乘的逻辑问题,用如下代码中的3个for循环实现两个矩阵的相乘,确实还让我费了一会时间,可能是久不写代码,有点生疏了,所以这里也顺便将思路写一写。
两个矩阵相乘就是依次那左矩阵的行乘以右矩阵的列,得到的就是新矩阵在该行该列的元素。为讨论方便,设A和B两个矩阵如下。
如果C=AB且设C为
根据线性代数中矩阵的乘法法则。
总结一下就是
于是可以通过3个变量的3个for循环来实现。第一个变量i循环A矩阵的行数次,第二个变量循环B矩阵的列数次,第三个变量k循环A的列或B的行数次。一个简单的实现如下:
void MatrixMult(int A[2][4], int B[4][3], int C[2][3]){
int s = 0;
for(int i=0; i<2; i++){
for(int j=0; j<3; j++){
s = 0;
for(int k=0; k<4; k++){
s += A[i][k]*B[k][j];
}
C[i][j] = s;
}
}
}
再来谈谈矩阵元素的存取问题。
矩阵是一张二维表,有横纵两个坐标唯一确定一个元素。而通常我们使用一维的数组来存取,或是通过指针的方式来操作一段连续的内存来存储。
那么由此有几个细节需要注意:
1、 由于C语言中数组的下标是从0开始的,所以Matrix[0][0]是指的第1行第1列的元素,Matrix[0][1]是指的第1行第2列的元素,Matrix[m][n]是指的第m+1行第n+1列的元素。
2、 如果使用一个数组来存取矩阵。假设用数组是Arr[len]来存取矩阵Matrix[m×n]。其中len=m×n,我们用Matrix[i][j]来表示矩阵第i+1行第j+1列的元素,因而i和j的取值范围是0≤i≤m,0≤j≤n。一个很自然的一个问题是:矩阵元素Matrix[i][j],在数组Arr中的下标是多少?要计算这个下标请记住一点:请计算该元素在数组中的下标就是计算在矩阵中该元素的前面多少个元素。分析:矩阵Matrix[i][j]是表示第i+1行第j+1列的元素。(1)在该元素所在行之前还有i行元素,计算一下这i行元素一共是i×n个(每行元素是n个,即列数)。(2)该元素是所在行的第j+1一个元素。所以连同第一个元素到我们关注的这个Matrix[i][j]元素在内,一共是i×n+j+1个元素。这些元素按照直线排成一行,一次存储在数组Arr中,所以第i×n+j+1个元素(即元素Matrix[i][j])的下标就是i×n+j,而这个值i×n+j就是第i×n+j+1个元素(即元素Matrix[i][j])之前的元素的个数。