文章目录
本学习笔记是根据《算法零基础100讲》进行的每讲总结,目标是采用刻意练习的方法对个人的学习和思考进行总结和归纳,方便复习和进一步深入理解和掌握内化。
每一讲的笔记内容大致分为三个部分:
- 设问,通过这一讲的学习应该理解什么内容?
- 题目,在学习完每讲内容后对题目先进行独立的思考和分析,记录下来;
- 题解报告,通过个人思考和学习其他优秀解题方案,记录个人认为较为优秀的解答并进行画图理解和内化吸收,并分析如何优化自己的思考;
- 知识点笔记,将本讲内容的知识点进行汇总和扩展,方便日后查阅复习;
- 解题模板,尽量将所学代码进行通用化处理。
另外:
笔记中会尽量用图示的方式将所学内容进行可视化,方便记忆和内化,以及帮助读者进行理解。
笔记至少会参考5篇以上的参考资料,并在文尾列出。
第三讲:矩阵
一、设问
- 在C语言中如何表示一个矩阵?
- 如何在C语言中将矩阵作为参数输入函数?
- 如何在C语言中将矩阵作为函数的输出?
- 矩阵在计算机中有哪些应用领域?
- 矩阵的基本运算有哪些?分别如何用C语言实现?
二、题目
最富有客户的资产总量
计算矩阵每行数值的和,再取最大值
二进制矩阵中的特殊位置
想办法找到值为1的[i][j],然后确定i行其他均为0,j列其他均为0;
想过用^和为1的方式,但是这会导致1的个数为奇数就会被判定正确
或者使用计算行和和列和的方式去减少判断次数,但是似乎不会有太大增益效果
最后还是只能一一遍历,只要实现该功能即可。
翻转图像
这道题需要开辟新的矩阵空间进行返回,因此操作空间较大,按照功能需求编写代码即可。
旋转图像
这道题卡了很久,因为不能开辟新空间而只能原理进行旋转,但旋转也不是两个数之间的相互交换,这一点让我有点难以转过弯来。
转置矩阵
本来以为可以直接使用上一题中的主轴翻转代码,但发现有坑。
将一维数组转变成二维数组
正常思路,主要考察对于一维数组和二维数组索引概念的理解。
判断矩阵经轮转后是否一致
只能想到模拟旋转并对比判断。
二维网格迁移
按照规律赋值即可,只需要知道如何将二维数组返回。
三、题解报告
最富有客户的资产总量
循环将每一行的数据都累加,与当前的max进行比对,更新最大的数据返回即可。
关于数组的题目每个参数的含义:
- 参数int **是输入的数组,名字叫什么随意
- 参数int xxSize是行数,名字一般就是数组名+Size
- 参数int *xxColSize是每行的列数,因为每行列数不一定相同,因此使用一个与行数大小相同的数组进行传入
- 如果需要输出的是一维数组或者二维数组,还会有int *returnSize和int **returnColSize,用于传出行数和列数
int maximumWealth(int** accounts, int accountsSize, int* accountsColSize){
int max = 0;
int customSize = accountsSize;
int bankSize = *accountsColSize;
int sum = 0;
for (int custom = 0; custom < customSize; ++custom) {
sum = 0;
for (int bank = 0; bank < bankSize; ++bank) {
sum += accounts[custom][bank];
}
max = max > sum? max : sum;
}
return max;
}
二进制矩阵中的特殊位置
找到值为1的位置,然后判断其所在行列其他值是否都为0
由于输入参数为m行n列方阵,因此只需要获取xxColSize[0]即可知道列数。
bool check(int **mat, int m, int n, int x, int y){
for(int i = 0; i < m; i ++){
if(i != x && mat[i][y] == 1) return false;
}
for(int i = 0; i < n; i ++){
if(i != y && mat[x][i] == 1) return false;
}
return true;
}
int numSpecial(int** mat, int matSize, int* matColSize){
int res = 0;
int m = matSize, n = matColSize[0];
for(int i = 0; i < m; i ++){
for(int j = 0; j < n; j ++){
if(mat[i][j] == 1 && check(mat, m, n, i, j)){
res++;
}
}
}
return res;
}
翻转图像
找到相应的关系进行赋值即可。
int** flipAndInvertImage(int** image, int imageSize, int* imageColSize, int* returnSize, int** returnColumnSizes){
int **newImage = (int **)malloc(sizeof(int*) * imageSize);
*returnColumnSizes = (int *)malloc(sizeof(int) * imageSize);
*returnSize = imageSize;
int col;
for (int i = 0; i < imageSize; ++i) {
col = imageColSize[i];
newImage[i] = (int*)malloc(sizeof(int) * col);
(*returnColumnSizes)[i] = col;
for(int j = 0; j < col; ++j) {
newImage[i][j] = 1 - image[i][col - 1 - j];
}
}
return newImage;
}
旋转矩阵
由于题目需要在原地进行旋转,因此需要分析矩阵旋转的特征
画图分析得出如上结论,其中的row和col为了方便分析,指的是行列索引的最大值,即行数和列数分别 - 1之后的值。
分析利用此结论进行旋转换位置的可行性:
我们大致可以看出这个旋转不仅仅是两个数之间的事情,需要继续分析
按照得出的结论进行转换,可以得知四个数可形成旋转的闭环,且当i,j正好为中间的数时,如:mat[1][1] = mat[1][2 - 1], 也符合情况,但实际上它并不需要参与旋转,因此可以写代码,利用一个临时变量,将这四个数进行旋转。
最后需要注意的是,由于旋转是每4个数一组,因此需要考虑如何使每个数都只旋转一次,最简单的方式是进行行列值/2,如图所示
只考虑 (row+1)/2 和 col/2,或者 row/2 和(col + 1)/2(此处的row和col是行和列数)即可。
另外还有一种解题方案是两次翻转,每次更换两个数之间的值
void rotate(int** matrix, int matrixSize, int* matrixColSize){
int n = matrixColSize[0] - 1;
for (int i = 0; i < matrixSize / 2; ++i) {
for (int j = 0; j < (n + 2) / 2; ++j) {
int tmp = matrix[i][j];
matrix[i][j] = matrix[n - j][i];
matrix[n - j][i] = matrix[n - i][n - j];
matrix[n - i][n - j]= matrix[j][n - i];
matrix[j][n - i] = tmp;
}
}
}
void rotate(int** matrix, int matrixSize, int* matrixColSize){
int m = matrixColSize[0] - 1;
int n = matrixSize - 1;
///< 水平翻转
for (int i = 0; i < (n + 1) / 2; ++i) {
for (int j = 0; j <= m; ++j) {
matrix[i][j] ^= matrix[n - i][j];
matrix[n - i][j] ^= matrix[i][j];
matrix[i][j] ^= matrix[n - i][j];
}
}
///< 主轴翻转
for (int i = 0; i <= n; ++i) {
for (int j = 0; j < i; ++j) {
matrix[i][j] ^= matrix[j][i];
matrix[j][i] ^= matrix[i][j];
matrix[i][j] ^= matrix[j][i];
}
}
}
转置矩阵
这里要注意一个坑,就是转置不完全等同于主轴翻转,行列转置还会导致行列发生变化,因此不能像上一题一样处理,必须开辟新的数组空间。
int** transpose(int** matrix, int matrixSize, int* matrixColSize, int* returnSize, int** returnColumnSizes){
int n = matrixColSize[0];
int m = matrixSize;
int **res = (int **)malloc(sizeof(int *) * n);
*returnColumnSizes = (int *)malloc(sizeof(int) * n);
*returnSize = matrixColSize[0];
for (int i = 0; i < n; ++i) {
(*returnColumnSizes)[i] = m;
res[i] = (int *)malloc(sizeof(int) * m);
for (int j = 0; j < m; ++j) {
//printf("%d %d\n", n, m);
res[i][j] = matrix[j][i];
}
}
return res;
}
将一维矩阵转变为二维矩阵
先判断是否可以构建二维数组,然后根据索引关系进行赋值。
int** construct2DArray(int* original, int originalSize, int m, int n, int* returnSize, int** returnColumnSizes){
int **res = (int **)malloc(sizeof(int *) * m);
*returnColumnSizes = (int *)malloc(sizeof(int) * m);
*returnSize = 0;
*returnColumnSizes[0] = 0;
if (originalSize != m * n) {
return res;
}
*returnSize = m;
for (int i = 0; i < m; ++i) {
res[i] = (int *)malloc(sizeof(int) * n);
(*returnColumnSizes)[i] = n;
for (int j = 0; j < n; ++j) {
res[i][j] = original[i * n + j];
}
}
return res;
}
判断矩阵经轮转后是否一致
常规思路,如果行列数不一致一定错误;
尝试最多四次旋转,如果和target完全一致则返回true,否则返回false。
void rotate(int** matrix, int matrixSize, int* matrixColSize){
int n = matrixColSize[0] - 1;
for (int i = 0; i < matrixSize / 2; ++i) {
for (int j = 0; j < (n + 2) / 2; ++j) {
int tmp = matrix[i][j];
matrix[i][j] = matrix[n - j][i];
matrix[n - j][i] = matrix[n - i][n - j];
matrix[n - i][n - j]= matrix[j][n - i];
matrix[j][n - i] = tmp;
}
}
}
bool matrixEq(int **mat, int **target, int n, int m) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (mat[i][j] != target[i][j]) {
return false;
}
}
}
return true;
}
bool findRotation(int** mat, int matSize, int* matColSize, int** target, int targetSize, int* targetColSize){
int n = matSize;
int m = matColSize[0];
if (n != targetSize || m != targetColSize[0]) {
return false;
}
for (int i = 0; i < 4; ++i) {
rotate(mat, matSize, matColSize);
if (matrixEq(mat, target, n, m)) {
return true;
}
}
return false;
}
二维网格迁移
按规律赋值。
需要特别注意输入矩阵为1x1时的情况,另外需要仔细考虑好赋值的规律要如何用代码进行表示。
由于输入的二维数组往往是malloc出来的,无法像局部变量一样直接用地址+偏移量(i * n + m)获得第[i][j]的数据,因此规律描述比想象中麻烦。
int** shiftGrid(int** grid, int gridSize, int* gridColSize, int k, int* returnSize, int** returnColumnSizes){
int **res = (int *)malloc(sizeof(int *) * gridSize);
int m = gridSize;
int n = gridColSize[0];
int size = n * m - 1;
*returnSize = m;
*returnColumnSizes = (int *)malloc(sizeof(int) * m);
int row = (size - k)/m;
int column = (size - k)%m;
int idx = 0;
if (size == 0) {
k = 0;
}
k = k % (size + 1);
for (int i = 0; i < m; ++i) {
res[i] = (int *)malloc(sizeof(int) * n);
(*returnColumnSizes)[i] = n;
for (int j = 0; j < n; ++j) {
idx = i * n + j;
if (idx < k) {
row = (size - k + idx + 1)/n;
column = (size - k + idx + 1)%n;;
} else {
row = (idx - k) / n;
column = (idx - k) % n;
}
printf("%d %d\n", row, idx);
res[i][j] = grid[row][column];
}
}
return res;
}
四、知识点笔记
-
矩阵相当于C语言中的二维数组, A n × m A_{n\times m} An×m 表示n行m列矩阵,在C语言中可表示为
matrix[n][m]
,其中matrix[i][j]
表示第i行j列的元素; -
“如果不熟悉线性代数的概念,要去学习自然科学,现在看来就和文盲差不多。”
“按照现行的国际标准,线性代数是通过公理化来表述的,它是第二代数学模型” -
C中的二维数组如果是malloc的话,代表着是一维数组名的一维数组,需要malloc了一维数组指针之后,再循环对一维数组指针进行malloc。
int **createMatrix(int n, int m, int* returnSize, int **returnColumnSizes) { int **matrix = (int **)malloc(sizeof(int *) * n); *returnColumnSizes = (int *)malloc(sizeof(int) * n); *returnSize = m; for (int i = 0; i < n; ++i) { matrix[i] = (int *)malloc(sizeof(int) * m); (*returnColumnSizes)[i] = m; for (int j = 0; j < m; ++j) { matrix[i][j] = 0; } } return matrix; }
-
矩阵是数的排列,其运算包括标量运算和点积运算:标量运算是与常量的运算(与矩阵中每个值进行+ - * /);点积运算是指矩阵乘法,较为复杂。可以查看矩阵乘法器,矩阵乘法必须第一个矩阵的列数与第二个矩阵的行数相等,并不可互换。
-
矩阵中没有除法的概念,只有乘上第二个矩阵的逆矩阵,工程师用类似的计算(当然是用大得多的矩阵)来设计楼宇。类似的计算也应用在很多其他的领域,例如在电玩和电脑动画制作里用来显示三维物体。同时也是解线性方程组的方法。
-
线性代数是向量计算的基础,很多重要的数学模型都要用到向量计算。矩阵的本质就是线性方程式,两者是一一对应关系。矩阵的最初目的,只是为线性方程组提供一个简写形式。
-
在线性空间中,当你选定一组基之后,不仅可以用一个向量来描述空间中的任何一个对象,而且可以用矩阵来描述该空间中的任何一个运动(变换)。而使某个对象发生对应运动的方法,就是用代表那个运动的矩阵,乘以代表那个对象的向量。
简而言之,在线性空间中选定基之后,向量刻画对象,矩阵刻画对象的运动,用矩阵与向量的乘法施加运动。
矩阵的本质是运动的描述。
《算法零基础100讲》介绍
该栏目是CSDN@英雄哪里出来大佬的万人千题计划第一阶段,其目标是让想要学会算法的人抱团成长,能通过这个项目稳步成长,扎扎实实掌握算法,最终用算法改变世界!
大佬将算法学习的路线总结如下,并分别根据五步路线总结写出了相应的教学内容,关于目前的第一阶段算法入门的详细的介绍可以看博文《万人千题:第一阶段,算法零基础抱团打卡》学习路线指引
100讲的学习大纲如下:
参考资料
此处列出除文中链接之外的参考资料