C++ 练气期之二维数组与矩阵运算

1. 前言

C++中的一维数组可以存储线性结构的数据,二维数组可以存储平面结构的数据。如班上所有学生的各科目成绩就有二个维度,学生姓名维度和科目成绩维度。

这样的表格数据可以使用二维数组进行存储。

1.png

当需要存储更多维度的数据时,可以使用多维数组

二维数组和矩阵的关系:

有些教材上,把二维数组和矩阵当成一回事,其实,两者还是有区别的。

矩阵:

  • 矩阵(Matrix)是线性数学中的概念,是一个按照长方阵列排列的复数实数集合,最早用来描述方程组的系数常数信息。
  • 因为矩阵是数学上的一个概念,要求矩阵必须是数字类型的数据。
  • 使用矩阵时,会把它当成一个整体看待。

数组:

  • 数组(Array)是计算机中的一个概念。二维数组是数组中的一种结构形式。
  • 数组除了可以存储数字型数据,也能存储非数字型数据。
  • 数组中的数据总是被当成个体来对待。

当使用计算机解决数学中与矩阵有关的问题时,可以借助二维数组。所以说,二维数组是矩阵在计算机中的数字模型

下面将了解怎么创建二维数组以及如何使用二维数组解决与矩阵有关的问题。

2. 创建二维数组

二维数组一维数组创建方式是一样的,会有 2 种创建方案:

有关数组创建的细节,可以查阅与之相关的博文。

  • 静态创建:如下创建了一个 33 列的二维数组
int nums[3][3];
  • 动态创建:动态创建的数组本质是指向指针的指针。如下语句,说明数组中保存的是指针(指向一个一维数组的地址)。
int **nums=new int*[3];

无论是静态创建还是动态创建,都可以使用下标指针两种访问方式。

访问二维数组中的数据之前,先要了解二维数组的内存模型结构。二维数组可以认为是一维数组一维数组,第一个一维数组中的每一个存储单元格中都保存着一个一维数组的地址。

Tip:静态和动态创建的数组,两者在内存的存储位置不一样,但是模型结构是一样。

15.png
使用下标访问静态数组中的数据,可以先在行上移动,然后再在列上移动。

#include <iostream>
using namespace std;
int main(int argc, char** argv) {
    //静态创建数组
	int nums[5][5];
    //初始化第一个位置,也就是 0 行 0 列
    nums[0][0]=20;
	//访问第一个位置
	cout<<"第一行第一列数据:"<<nums[0][0]<<endl;
	//遍历整个数组
	cout<<"遍历所有数据:"<<endl;
	for(int i=0; i<5; i++) {
		for(int j=0; j<5; j++) {
             //先赋值(值仅用来测试)
			nums[i][j]=i*j;
            //输出
			cout<<nums[i][j]<<"\t";
		}
		cout<<endl;
	}
	return 0;
}

输出结果:

第一行第一列数据:20
遍历所有数据:
0       0       0       0       0
0       1       2       3       4
0       2       4       6       8
0       3       6       9       12
0       4       8       12      16

使用指针访问静态二维数组时。

#include <iostream>
using namespace std;
int main(int argc, char** argv) {
	int nums[5][5];
	//初始化第一行,第一列 
	**nums=20;
	//访问某一个指定位置的数据
	cout<<"第一行第一列数据:"<<**nums<<endl;
	//遍历整个数组
	cout<<"遍历所有数据:"<<endl;
	for(int i=0; i<5; i++) {
		for(int j=0; j<5; j++) {
            // nums+i 让行指针先移 ,确定行后, 再移动列指针。最终确定位置
			*(*(nums+i)+j)=i*j;
			cout<<*(*(nums+i)+j)<<"\t";
		}
		cout<<endl;
	}
	return 0;
}

动态数组创建后,系统只为第一个一维数组分配了存储空间,需要编码为一维数组的每一个存储单元格创建一个一维数组。其它的无论是下标或指针访问方式和静态数组一样。

#include <iostream>
using namespace std;
int main(int argc, char** argv) {
    //动态创建二维数组,说明一维数组中存储的是地址。
	int **nums = new int *[5];
    //为一维数组的每一个存储位置再创建一个一维数组(一维数组的一维数组)
	for (int i = 0; i < 5; i++) {
        //动态创建
		nums[i] = new int[5];
	}
	//下标、指针访问都和静态数组一样
	nums[0][0] = 5;
	**nums=20;
    //使用动态方案创建的数组需要显示删除
	for (int i = 0 ; i < 5; ++i) {
		//此处的[]不可省略
		delete [] nums[i];
	}
	return 0;
}

3. 矩阵的基本运算

二维数组可以模拟矩阵,计算机中可以使用二维数组解决与矩阵相关的运算。

用于矩阵运算操作时,把二维数组当成一个整体,所以,运算的结果也会是一个二维数组。

3.1 加法运算

现假设有 AB 2 个矩阵。矩阵加法运算遵循下面的运算规则:

  • AB矩阵对应位置的数据进行相加。
  • 结果是一个新的矩阵 C

2.png

矩阵之间进行加法运算时,需满足以下几个要求:

  • AB 2 个矩阵的维度数据类型必须是相同的。
  • AB 2 个矩阵相加后的结果是矩阵C。此 3 个矩阵满足: A+B=B+A(A+B)+C=A+(B+C)

编码实现:

  • 初始化矩阵
#include <iostream>
using namespace std;
//矩阵 A
int **num_a=new int*[3];
//矩阵 B
int **num_b=new int*[3];
//矩阵 C
int **num_c=new int*[3];
//初始化数组
void initArrays() {
	//构建二维数组
	for(int i=0; i<3; i++) {
        // A,B,C 都是 3 行 3 列数组
		num_a[i]=new int[3];
		num_b[i]=new int[3];
		num_c[i]=new int[3];
	}
	//初始化二维数组
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
            //数据仅用测试
			*(*(num_a+i)+j)=i*j+4;
			*(*(num_b+i)+j)=i*j+2;
		}
	}
}
//输出 A,B 中的数据
void outArrays() {
	//输出数据
	cout<<"数组 A:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
			cout<<*(*(num_a+i)+j)<<"\t";
		}
		cout<<endl;
	}
	//输出数据
	cout<<"数组B:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
			cout<<*(*(num_b+i)+j)<<"\t";
		}
		cout<<endl;
	}
}
  • 矩阵相加
//矩阵相加
void matrixAdd() {
	cout<<"矩阵相加:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
            //A和B 相同位置的数据相加,保存在C中
			*(*(num_c+i)+j)=*(*(num_a+i)+j)+*(*(num_b+i)+j);
			cout<<*(*(num_c+i)+j)<<"\t";
		}
		cout<<endl;
	}
}
  • 验证矩阵的 A+B=B+A(A+B)+C=A+(B+C)特性。
//验证 A+B 是否等于 B+A。根据加法的特性,这个必然成立
void validate() {
	cout<<"A+B=:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
            // A + B
			*(*(num_c+i)+j)=*(*(num_a+i)+j)+*(*(num_b+i)+j);
			cout<<*(*(num_c+i)+j)<<"\t";
		}
		cout<<endl;
	}
	cout<<"B+A=:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
            //B+A
			*(*(num_c+i)+j)=*(*(num_b+i)+j)+*(*(num_a+i)+j);
			cout<<*(*(num_c+i)+j)<<"\t";
		}
		cout<<endl;
	}
}
//验证 (A+B)+C 是否等于 A+(B+C)
void validate_() {
	//(A+B)+C
	cout<<"(A+B)+C=:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
			//因为:A + B=C 所以,(A+B)+C=C+C 
			cout<<num_c[i][j]+num_c[i][j]<<"\t";
		}
		cout<<endl;
	}
	//计算 B+C,且把结果保存在临时数组中
	int **tmp=new int*[3];
	for(int i=0; i<3; i++) {
		tmp[i]=new int[3];
		for(int j=0; j<3; j++) {
			//B+C
			tmp[i][j]=num_b[i][j]+num_c[i][j];
		}
		cout<<endl;
	}
	//再计算:A+(B+C)
	cout<<"A+(B+C)=:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
			//B+C
			cout<<num_a[i][j]+tmp[i][j]<<"\t";
		}
		cout<<endl;
	}
}
  • 测试:
int main(int argc, char** argv) {
	initArrays();
	outArrays();
	//矩阵相加
	matrixAdd();
	//A+B
	validate();
	//(A+B)+C=A+(B+C)
	validate_();
	return 0;
}

输出结果:

数组A:
4       4       4
4       5       6
4       6       8
数组B:
2       2       2
2       3       4
2       4       6
A+B=:
6       6       6
6       8       10
6       10      14
A+B=:
6       6       6
6       8       10
6       10      14
B+A=:
6       6       6
6       8       10
6       10      14
(A+B)+C=:
12      12      12
12      16      20
12      20      28
A+(B+C)=:
12      12      12
12      16      20
12      20      28

从上述结果中,可以看出 (A+B)+C 是等于 A+(B+C)

3.2 减法运算

矩阵相减矩阵相加一样,把A、B 2 个矩阵对应位置的数字相减,最终生成一个新矩阵C。且维度和数据类型需要保持相同

三者满足数学上的减法规律:

  • A-B=C

  • A=B+C

  • A-C=B

    如下所示:

3.png

编码实现:

  • 矩阵相减函数
//矩阵相减 
void matrixJian() {
	cout<<"A-B=:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
            //A B 相同位置数据相减,结果保存在 C中
			*(*(num_c+i)+j)=*(*(num_a+i)+j)-*(*(num_b+i)+j);
            //输出C
			cout<<*(*(num_c+i)+j)<<"\t";
		}
		cout<<endl;
	}
}
  • 验证A=C+B
//验证 A=B+C 
void validate01() {
	cout<<"B+C=:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
			int tmp=*(*(num_b+i)+j)+*(*(num_c+i)+j);
			cout<<tmp<<"\t";
		}
		cout<<endl;
	}
}
  • 测试代码。
int main(int argc, char** argv) {
	//初始化数组
    initArrays();
    //输出数组
	outArrays();
    //矩阵相减
	matrixJian();
    //验证A=B+C
	validate01();
    return 0;
}

输出结果:

数组A:
4       4       4
4       5       6
4       6       8
数组B:
2       2       2
2       3       4
2       4       6
A-B=:
2       2       2
2       2       2
2       2       2
B+C=:
4       4       4
4       5       6
4       6       8

3.3 数乘运算

数乘指让矩阵乘以一个数字。

数乘规则:让此数字和矩阵的每一个数字相乘,最终生成一个新的矩阵。如下图所示:

4.png

矩阵的数乘遵循如下的数学上的乘法运算规律。

  • a(bA)=b(aA)
  • a(bA)=(ab)A
  • (a+b)A=aA+bA
  • a(A+B)=aA+aB

小写 ab 代表 2 个乘数。大写 A、B代表 2 个矩阵。

编码实现数乘:

//矩阵相乘
void matrixmultiply(){
	cout<<"2XA=:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
			*(*(num_c+i)+j)=*(*(num_a+i)+j)*2;
			cout<<*(*(num_c+i)+j)<<"\t";
		}
		cout<<endl;
	}
} 

测试:

int main(int argc, char** argv) {
	//初始化数组
	initArrays();
    //输出数组
	outArrays();
    //矩阵相乘
	matrixmultiply();
}

输出结果:

数组A:
4       4       4
4       5       6
4       6       8
数组B:
2       2       2
2       3       4
2       4       6
2XA=:
8       8       8
8       10      12
8       12      16

矩阵的加减法和矩阵的数乘合称为矩阵的线性运算。

3.3 转置运算

把矩阵A的行和列互相交换所产生的矩阵称为A转置矩阵,这一过程称为矩阵的转置。转置用大写字母T表示。如下图所示:

6.png

矩阵的转置遵循以下的运算规律:

  • 转置后再转置,相当于没有转置。
  • 数乘后转置和数字乘以转置后的矩阵结果一样。
  • 矩阵相乘后转置和转置后再相乘的结果一样。

7.png

编码实现:

设有一矩阵为 m×n 阶(即 m 行 n 列),第 ij 列的元素是 a(i,j),需要将该矩阵转置为 n×m阶的矩阵,使其中元素满足 b(j,i)=a(i,j)

#include <iostream>
using namespace std;
//数组A为 3 行 2 列
int **num_a=new int*[3];
//数组A转置后的结果
int **num_b=new int*[2];
//输出
void outArrays() {
	//输出数据
	cout<<"数组A:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<2; j++) {
			cout<<*(*(num_a+i)+j)<<"\t";
		}
		cout<<endl;
	}
	//输出数据
	cout<<"数组B:"<<endl;
	for(int i=0; i<2; i++) {
		for(int j=0; j<3; j++) {
			cout<<*(*(num_b+i)+j)<<"\t";
		}
		cout<<endl;
	}
}

//初始化数组
void initArrays() {
	//构建  3 行 2 列的二维数组A
	for(int i=0; i<3; i++) {
		num_a[i]=new int[2];
	}
	//构建 2 行 3 列的二维数组B
	for(int i=0; i<2; i++) {
		num_b[i]=new int[3];
	}
	//初始化二维数组A
	for(int i=0; i<3; i++) {
		for(int j=0; j<2; j++) {
			*(*(num_a+i)+j)=i*(j+1)+4;
		}
	}
}
//转置数组A,转置后的数据保存在 B 中
void  matrixTranspose() {
	for(int i=0; i<3; i++) {
		for(int j=0; j<2; j++) {
			num_b[j][i]=num_a[i][j];
		}
	}
}
int main(int argc, char** argv) {
	//初始化数组
	initArrays();
	//转置A
	matrixTranspose();
	//输出
	outArrays(); 
	return 0;
}

输出结果:

数组A:
4       4
5       6
6       8
数组B(B是的转置矩阵):
4       5       6
4       6       8

如果矩阵A和其转置矩阵B相等,则称A为对称矩阵。

16.png

3.4 共轭运算

矩阵的共轭定义为:一个2×2复数矩阵的共轭(实部不变,虚部取负)如下所示:

8.png

C++内置有complex头文件,提供有计算复数的共轭函数:

#include <iostream>
#include <complex>
using namespace std;
int main() {
	complex<double> cs[2][2]= {{complex<double> (3,1),complex<double> (5,0)},
		{complex<double> (2,-2),complex<double> (0,1)}
	};
	complex<double> cs_[2][2] ;
	//原矩阵
	cout<<"原矩阵"<<endl;
	for(int i=0; i<2; i++) {
		for(int j=0; j<2; j++) {
			cout<<cs[i][j]<<"\t";
		}
		cout<<endl;
	}
	for(int i=0; i<2; i++) {
		for(int j=0; j<2; j++) {
			//对原矩阵中的复数进行共轭运算 
			cs_[i][j]=	conj(cs[i][j]);
		}
	}
    //输出原矩阵的共轭矩阵
	cout<<"原矩阵的共轭矩阵:"<<endl; 
	for(int i=0; i<2; i++) {
		for(int j=0; j<2; j++) {
			cout<<cs_[i][j]<<"\t";
		}
		cout<<endl;
	}
}

输出结果:

原矩阵
(3,1)   (5,0)
(2,-2)  (0,1)
原矩阵的共轭矩阵:
(3,-1)  (5,-0)
(2,2)   (0,-1)

3.5 共轭转置

共轭转置顾名思义,共轭后再转置。

矩阵的共轭转置定义为:9.png,也可以写为:10.png。或者写为11.png

一个2×2复数矩阵的共轭转置如下所示:

12.png

3.6 乘法运算

两个矩阵的乘法仅当第一个矩阵A的列数和另一个矩阵B的行数相等时才能运算。

如果m×n矩阵An×p的矩阵B相乘,它们的乘积C是一个m×p矩阵,它的一个元素:

13.png

并将此乘积记为:C=AB

14.png

矩阵的乘法满足以下运算规律:

  • 结合律:(AB)C=A(BC)

  • 左分配律:(A+B)C=AC+BC

  • 右分配律:C(A+B)=CA+CB

矩阵乘法不满足交换律。

编码实现:

#include <iostream>
using namespace std;
//数组A 为 3 行 2 列
int **num_a=new int*[3];
//数组 B为 2行3列 数组B的行数和A数组的列数相同
int **num_b=new int*[2];
//C 保存 A 乘以 B 后的结果, 3 行 3 列 
int **num_c=new int*[3];
//输出
void outArrays() {
	//输出数据 3 行 2 列
	cout<<"数组A:"<<endl;
	for(int i=0; i<3; i++) {
		for(int j=0; j<2; j++) {
			cout<<*(*(num_a+i)+j)<<"\t";
		}
		cout<<endl;
	}
	//输出数据
	cout<<"数组B:"<<endl;
	for(int i=0; i<2; i++) {
		for(int j=0; j<3; j++) {
			cout<<*(*(num_b+i)+j)<<"\t";
		}
		cout<<endl;
	}
}

//初始化数组
void initArrays() {
	for(int i=0; i<3; i++) {
        //构建  3 行 2 列的二维数组A
		num_a[i]=new int[2];
        //构建 3 行 3 列的二维数组C
		num_c[i]=new int[3];
	}
	//构建 2 行 3 列的二维数组B
	for(int i=0; i<2; i++) {
		num_b[i]=new int[3];
	}
	//初始化二维数组A
	for(int i=0; i<3; i++) {
		for(int j=0; j<2; j++) {
            //测试数据
			*(*(num_a+i)+j)=i*(j+1)+4;
		}
	}
    //初始化二维数组 B
	for(int i=0; i<2; i++) {
		for(int j=0; j<3; j++) {
            //测试数据
			*(*(num_b+i)+j)=i*(j+2)+3;
		}
	}
    //初始化二维数组 C
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
			num_c[i][j]=0;
		}
	}
}
//矩阵相乘 A X B
void matrixCheng() {
    // i 表示A的行号
	for(int i=0; i<3; i++) {
	   //C表示 C  的列号
        int c=0;
        // k 表示 B  的列号,有多少列,乘多少次
		for(int k=0; k<3; k++) {
            // A 的列数和 B 的行数(两者是相等的)
			for(int j=0; j<2; j++) {
                //A 第一行的数据乘以 B 每一列的数据
				num_c[i][c]+= num_a[i][j]*num_b[j][k];
			}
			c++;
		}
	}
	cout<<"AXB="<<endl;
    //输出 C 
	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++) {
			cout<<num_c[i][j]<<"\t";
		}
		cout<<endl;
	}
}

int main(int argc, char** argv) {
	//初始化数组
	initArrays();
	//输出
	outArrays();
	matrixCheng();
	return 0;
}

输出结果:

数组A:
4       4
5       6
6       8
数组B:
3       3       3
5       6       7
AXB=
32      36      40
45      51      57
58      66      74

4. 总结

站在数学角度,矩阵有很多特性,本文通过二维数组初窥矩阵相关问题。让大家对二维数组和矩阵有一个大致的理解。