C语言在线编程环境:https://sky315.com:8080/c/。
很多同学学二维数组的时候,老想着“数组里面套数组”,这么理解也行,但容易把自己绕进去。更直观的理解方式:二维数组就是一个矩阵,有行号有列号,通过两个下标来锁定一个元素。 行决定在哪一排,列决定在这一排的第几个位置。
下面我把二维数组从头到尾捋一遍,每个知识点配上经典例题,你挨个啃下来,二维数组这关就彻底过了。
一、二维数组长啥样?先搞清楚定义和存储
定义格式:类型 数组名[行数][列数];
比如int a[3][4];,意思是一个3行4列的表格,总共12个int类型的格子。
访问的时候用a[行下标][列下标],行和列都是从0开始数的。所以a[0][0]是第一个格子,a[2][3]是最后一行最后一列。最大行下标是行数减1,最大列下标是列数减1,越界了就翻车。
初始化:
int a[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
这样最清楚,每一行用一对花括号包起来。
也可以偷懒:
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
编译器会按行顺序往里填,填满为止。
还能只初始化一部分:
int a[3][4] = { {1, 2}, {5, 6, 7}, {9} };
没填到的位置自动补0。第一行只有两个数,第三和第四个补0;第二行给了三个,第四个补0;第三行只给了一个,后面三个补0。
如果你不初始化,局部二维数组里面的值就是乱七八糟的垃圾数据。
行数可以省略不写,但列数必须写:
int a[][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
编译器根据列数自动算出来有几行:12除以4等于3行。但列数绝对不能省。
内存里到底怎么存的? 这一点特别关键。二维数组在内存里不是表格形状,它还是一行接一行连续存储的。先存完第0行的所有列,再存第1行,再存第2行,就这么一溜排下去。这叫“行优先存储”。
所以a[1][2]在内存里的位置,就是起始地址加上(1 * 列数 + 2) * 每个元素大小。你理解了这个,就知道为啥按行遍历比按列遍历效率高——按行遍历的时候内存是连续访问的,CPU缓存友好;按列遍历的时候跳来跳去,效率就差一些。
二、二维数组的基本操作,就是双层循环打天下
操作二维数组,99%的情况就是两层for循环套在一起。外层管行,内层管列。
按行遍历(最常用) :
for (int i = 0; i < 行数; i++) { for (int j = 0; j < 列数; j++) { // 处理 a[i][j] } }
外层i走一行,内层j走完这一行的所有列,然后i换下一行。
按列遍历:把循环顺序换一下就行。
for (int j = 0; j < 列数; j++) { for (int i = 0; i < 行数; i++) { // 处理 a[i][j] } }
外层固定列,内层遍历所有行。这种访问方式在内存里是跳着走的,效率不如按行遍历。
输入和输出,就是把遍历里的处理换成scanf或printf:
// 输入 for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { scanf("%d", &a[i][j]); } } // 输出(带格式,每行末尾换行) for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { printf("%5d", a[i][j]); } printf("\n"); }
输出的时候加个换行,打印出来就是个表格的形状,看着舒服。
三、经典例题挨个讲,每个都是硬菜
下面这些题是二维数组的“必修课”,你挨个啃下来,二维数组的操作就没什么能难住你了。
例1:求所有元素的和、平均值
最基础的操作,就是遍历累加。
int a[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int sum = 0; for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { sum += a[i][j]; } } printf("总和=%d,平均值=%.2f\n", sum, sum / 12.0);
注意平均值那里除以12.0而不是12,否则整数除法会把小数截掉。你要把总元素个数算对,行数乘以列数。
例2:求每行、每列的和
这题比上面多一层——不仅要遍历,还得分别统计每行每列。
int a[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int rowSum[3] = {0}; int colSum[4] = {0}; for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { rowSum[i] += a[i][j]; colSum[j] += a[i][j]; } } // 输出行和 for (int i = 0; i < 3; i++) { printf("第%d行和=%d\n", i, rowSum[i]); } // 输出列和 for (int j = 0; j < 4; j++) { printf("第%d列和=%d\n", j, colSum[j]); }
关键点:rowSum[i]跟着行走,colSum[j]跟着列走。同一个a[i][j]既要加到行的累计里,又要加到列的累计里。
例3:求主对角线元素之和(方阵)
方阵就是行数和列数相等的矩阵。主对角线就是从左上到右下的那条线,a[0][0]、a[1][1]、a[2][2]……条件是行下标等于列下标。
int a[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; int sum = 0; for (int i = 0; i < 3; i++) { sum += a[i][i]; } printf("主对角线之和=%d\n", sum); // 1+5+9=15
就一个循环搞定,因为行和列同步增长。
副对角线呢?从右上到左下,条件是行下标 + 列下标 = 行数 - 1。比如3×3矩阵,a[0][2]、a[1][1]、a[2][0]。
for (int i = 0; i < 3; i++) { sum += a[i][2 - i]; // 3-1=2 }
如果矩阵是偶数阶,主对角线和副对角线没有重叠元素。如果是奇数阶,正中间那个元素被算了两次(比如3×3的a[1][1]),你要根据需求决定要不要减掉一次。
例4:矩阵转置
转置就是把行和列互换——原来的第i行第j列变成第j行第i列。3行4列转置后变成4行3列。
int a[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int b[4][3]; // 转置后的矩阵 for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { b[j][i] = a[i][j]; } } // 输出转置后的矩阵 for (int i = 0; i < 4; i++) { for (int j = 0; j < 3; j++) { printf("%5d", b[i][j]); } printf("\n"); }
转置的核心就一句话:b[列][行] = a[行][列]。如果是对同一个方阵原地转置,那就只需要遍历对角线一侧的元素,跟对角线的另一半交换。
// 方阵原地转置,只需要遍历右上角 for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { int temp = a[i][j]; a[i][j] = a[j][i]; a[j][i] = temp; } }
注意内层j从i+1开始,这样只交换一次,不会换回来。
例5:找出矩阵中的最大值及其位置
思路跟一维数组找最大值一样,只是多了一个维度。记住位置的时候要把行和列都记住。
int a[3][4] = { {1, 5, 3, 9}, {7, 2, 8, 4}, {6, 0, 11, 10} }; int max = a[0][0]; int maxRow = 0, maxCol = 0; for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { if (a[i][j] > max) { max = a[i][j]; maxRow = i; maxCol = j; } } } printf("最大值=%d,位置=(%d, %d)\n", max, maxRow, maxCol);
如果把>换成<,就是找最小值。
例6:矩阵乘法——这才是二维数组的硬核应用
两个矩阵相乘,规则是:A是m×n,B是n×p,结果C是m×p。C[i][j]等于A的第i行和B的第j列对应元素相乘再求和。
概念有点抽象,写代码就清楚了。
int A[2][3] = { {1, 2, 3}, {4, 5, 6} }; int B[3][2] = { {7, 8}, {9, 10}, {11, 12} }; int C[2][2] = {0}; // 结果矩阵 for (int i = 0; i < 2; i++) { // A的行 for (int j = 0; j < 2; j++) { // B的列 for (int k = 0; k < 3; k++) { // A的列 = B的行 C[i][j] += A[i][k] * B[k][j]; } } } // 输出结果 for (int i = 0; i < 2; i++) { for (int j = 0; j < 2; j++) { printf("%5d", C[i][j]); } printf("\n"); }
三层循环,最里层的k是“累加”的那一层。注意C的维度是A的行数乘以B的列数。这个三重循环是矩阵乘法的标准写法,你背下来也好、理解了再写也好,反正得能写出来。
例7:螺旋遍历矩阵——稍微带点技巧
这个题挺有意思:给定一个矩阵,按顺时针螺旋顺序打印所有元素。比如3×3矩阵从外到内转圈。
int a[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int top = 0, bottom = 2, left = 0, right = 3; while (top <= bottom && left <= right) { // 从左到右打印上边 for (int j = left; j <= right; j++) { printf("%d ", a[top][j]); } top++; // 从上到下打印右边 for (int i = top; i <= bottom; i++) { printf("%d ", a[i][right]); } right--; // 从右到左打印下边 if (top <= bottom) { for (int j = right; j >= left; j--) { printf("%d ", a[bottom][j]); } bottom--; } // 从下到上打印左边 if (left <= right) { for (int i = bottom; i >= top; i--) { printf("%d ", a[i][left]); } left++; } }
思路就是一圈一圈往里剥,四个方向的边界不断收缩。每打印完一条边,对应的边界就往里缩一格。这个题训练的是你对二维数组边界的精准控制,笔试面试里经常出现变种。
例8:杨辉三角——二维数组的动态填充
杨辉三角长这样:
1 1 1 1 2 1 1 3 3 1 1 4 6 4 1
规律是每一行的第一个和最后一个都是1,中间每个数是上一行同列和前一列之和。
int n = 6; int a[6][6] = {0}; for (int i = 0; i < n; i++) { a[i][0] = 1; // 每行第一个是1 a[i][i] = 1; // 每行最后一个是1 for (int j = 1; j < i; j++) { a[i][j] = a[i-1][j-1] + a[i-1][j]; } } // 打印 for (int i = 0; i < n; i++) { for (int j = 0; j <= i; j++) { printf("%5d", a[i][j]); } printf("\n"); }
这个题的精髓在于:你只填充了矩阵的“下三角”部分,每行长度不一样,越来越长。内层循环j < i意味着第i行有i+1个有效元素(从0到i)。等你把递推公式转换成数组下标的时候,二维数组就用得游刃有余了。
四、几个要命的坑,提前知道省得流泪
坑1:行列下标搞反了
a[3][4]是3行4列,最大下标是a[2][3]。有些人一紧张写成a[3][4]去访问,直接越界。记住,定义的时候写的是长度,访问的时候最大下标是长度减1。
坑2:初始化时内层花括号忘写了
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};这样写没问题,但可读性差。更常见的是int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};,你如果不小心写成int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};,结果一样,但别人读你代码的时候会骂你。
坑3:二维数组作为函数参数时,列数必须指明
void func(int arr[][4], int row) { // 必须写列数,行数可以空着 }
编译器要知道每一行有多长才能正确计算偏移量。你写成int arr[][]编译直接报错。列数在定义和传参的时候都要写清楚。
坑4:按列遍历效率低
前面说过了,二维数组在内存里是行优先存储的。按行遍历的时候,内存地址是连续的,CPU缓存命中率高。按列遍历的时候,每访问一个元素都要跳到下一行的同一列,地址跳跃大,效率低。数据量小的时候感觉不出来,数据量大了差异明显。养成按行遍历的习惯。
五、说两句掏心窝的话
二维数组就是“表格思维”,行和列两个维度构成了一个平面。你把这个平面的遍历、查找、统计、变换都练熟了,以后碰到三维数组、多维数组,道理是一样的——无非就是再多套几层循环。
练到什么程度算过关? 拿到一个二维数组的需求,你脑子里能立刻反应过来:用几层循环、外层管什么内层管什么、边界条件是什么、要不要额外开一个数组存结果。把上面那八个例题自己敲一遍,改改数据跑一跑,把每一种操作都写熟练了,二维数组这关你就彻底拿下了。
还是那句话,别光看,打开编译器。二维数组的调试最好用,你把每一步的中间结果打印出来看看,行和列的变换一目了然。动手吧,代码敲多了自然就通了。