C语言在线编程环境:https://sky315.com:8080/c/。
现实里三维数组已经足够应付绝大多数场景了,比如图像处理里的RGB三通道、游戏里的三维坐标网格、时间序列的立体数据。你把三维彻底搞明白,四维不过是在这个逻辑上再套一层壳。
所以今天咱们的精力就集中在三维数组上,把它彻底啃透。后面即便真遇到四维,你自己也能举一反三。
一、三维数组长啥样?想象成一摞表格
二维数组是行和列,三维数组就是层、行、列。先确定在哪一层,再确定在这一层的哪一行,最后确定这一行的哪一列。
定义格式:类型 数组名[层数][行数][列数];
比如int a[3][4][5];,意思是有3层,每层是4行5列的表格,总共3×4×5=60个int元素。
访问的时候用三个下标:a[层][行][列]。每一维都从0开始数。a[0][0][0]是第一个,a[2][3][4]是最后那个。
内存里怎么存的? 还是老规矩,连续存储。先把第0层的所有元素按行存完,再存第1层,再存第2层。层里面的顺序跟二维数组一样,按行优先。所以a[1][2][3]在内存里的位置就是起始地址加上(1 * 行数 * 列数 + 2 * 列数 + 3) * 每个元素大小。
计算偏移量的公式其实就是一层一层剥洋葱:
-
先数前面有几层,每层有“行数×列数”个元素
-
再数这一层前面有几行,每行有“列数”个元素
-
最后加上这一行前面有几列
你把这个公式理解透了,不管多少维都能算出来。
初始化:
最直观的写法,一层一层地写:
int a[2][3][4] = { { // 第0层 {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }, { // 第1层 {13, 14, 15, 16}, {17, 18, 19, 20}, {21, 22, 23, 24} } };
是不是跟二维数组一模一样?就是外面再多套一层花括号而已。
也可以“扁平化”初始化,把60个数按顺序写在一对花括号里,编译器会自动按层→行→列的顺序填进去。但没人这么干,因为可读性太差了。三维数组初始化,该写的花括号别省,否则你自己过两天回来看代码都懵。
二、三维数组的基本操作,就是三层循环套一起
操作三维数组,核心就是三层循环嵌套。外层管层,中层管行,内层管列。
遍历全部元素:
for (int i = 0; i < 层数; i++) { for (int j = 0; j < 行数; j++) { for (int k = 0; k < 列数; k++) { // 处理 a[i][j][k] } } }
顺序是固定的:先固定层,再遍历这一层的所有行,每一行再遍历所有列。跑完一层再进下一层。
输入输出,就是把处理换成scanf或printf:
// 输入 for (int i = 0; i < depth; i++) { printf("第%d层:\n", i); for (int j = 0; j < row; j++) { for (int k = 0; k < col; k++) { scanf("%d", &a[i][j][k]); } } } // 输出(每一层打印一个表格) for (int i = 0; i < depth; i++) { printf("第%d层:\n", i); for (int j = 0; j < row; j++) { for (int k = 0; k < col; k++) { printf("%5d", a[i][j][k]); } printf("\n"); } printf("\n"); }
打印出来的效果就是一层一个表格,一层一层地展示。
三、典型例题挨个讲,三层循环是主角
三维数组的题目其实没有什么高深算法,核心就是锻炼你对多层循环下标的精准控制。下面这几道题你挨个练,三维数组的感觉就出来了。
例1:三维数组所有元素求和
最基础的遍历累加,没任何花头。
int a[2][3][4] = { {{1,2,3,4},{5,6,7,8},{9,10,11,12}}, {{13,14,15,16},{17,18,19,20},{21,22,23,24}} }; int sum = 0; for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { for (int k = 0; k < 4; k++) { sum += a[i][j][k]; } } } printf("总和=%d\n", sum); // 1加到24 = 300
这道题就是让你熟悉三层循环的写法。层、行、列三个变量你分清楚了,代码自然就出来了。
例2:求每层的平均值
这题要求你在层的维度上做统计——遍历的时候把同一层的所有元素加起来,层变了就重新开始。
int a[2][3][4] = {...}; int layerSum[2] = {0}; int layerCount = 3 * 4; // 每层有12个元素 for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { for (int k = 0; k < 4; k++) { layerSum[i] += a[i][j][k]; } } printf("第%d层平均值=%.2f\n", i, layerSum[i] / (float)layerCount); }
关键点:layerSum的下标是i,跟着层走。你在内存脑海里想一下,i不变的时候,j和k把所有元素过一遍,都累加到layerSum[i]里。等i变成1,换到下一层。
如果你要统计每一行的和,那就是rowSum的下标跟着j走,同时i和k在变化。要统计每一列的和,那就是colSum的下标跟着k走,i和j在变。逻辑是一样的,就看你把哪个维度固定下来作为统计维度。
例3:图像“灰度化”——三维数组的典型应用
这个例子挺实战的。假设有一张2×3的小图片,每个像素用RGB三个值表示(红绿蓝),存成三维数组img[高][宽][3]。灰度化的公式是:灰度 = 0.299×R + 0.587×G + 0.114×B。
int img[2][3][3] = { { // 第0行 {255, 0, 0}, // 纯红 {0, 255, 0}, // 纯绿 {0, 0, 255} // 纯蓝 }, { // 第1行 {255, 255, 0}, // 黄 {255, 0, 255}, // 品红 {0, 255, 255} // 青 } }; int gray[2][3]; // 灰度图,只有高和宽,没有颜色通道 for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { int r = img[i][j][0]; int g = img[i][j][1]; int b = img[i][j][2]; gray[i][j] = (int)(0.299 * r + 0.587 * g + 0.114 * b); } } // 输出灰度值 for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { printf("%4d", gray[i][j]); } printf("\n"); }
这个例子让你体会到三维数组在图像处理里的实际用途。第三维固定是3,表示RGB三个通道。你操作的时候,前两维定位到哪个像素,第三维取通道值。
例4:三维数组的“切片”操作
切片就是从三维数组里取出一层,变成二维数组。比如取第1层:
int a[3][4][5]; int slice[4][5]; // 取出第1层 int layer = 1; for (int j = 0; j < 4; j++) { for (int k = 0; k < 5; k++) { slice[j][k] = a[layer][j][k]; } }
反过来,你也可以把二维数组“塞”进三维数组的某一层。这种操作在数据处理里很常见,你把三维数据一层一层地提取出来分析,分析完再放回去。
例5:三维数组的“体素”遍历——找最大值及位置
三维里找最大值,不仅要记录值,还要记录它所在的层、行、列。
int a[2][3][4] = {...}; int max = a[0][0][0]; int max_i = 0, max_j = 0, max_k = 0; for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { for (int k = 0; k < 4; k++) { if (a[i][j][k] > max) { max = a[i][j][k]; max_i = i; max_j = j; max_k = k; } } } } printf("最大值=%d,位置=(层%d, 行%d, 列%d)\n", max, max_i, max_j, max_k);
跟二维找最大值一模一样,就是多了一个维度记录位置。
例6:三维数组的“旋转”思路——按层做矩阵转置
三维数组本身没有“旋转”一说,但你可以对每一层做矩阵转置。
int a[2][3][4]; // 2层,每层3行4列 int b[2][4][3]; // 转置后每层变成4行3列 for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { for (int k = 0; k < 4; k++) { b[i][k][j] = a[i][j][k]; } } }
这就是把二维数组转置的操作,在每一层上独立执行了一遍。三维数组的很多操作其实就是“对每一层做二维操作”,你把它拆开看就不复杂了。
四、四维数组?基本上用不着,但懂了三维就能推
理论上C语言支持任意维度的数组,int a[2][3][4][5];就是四维。但说真的,除了极少数算法题和科学计算,你基本碰不到。
如果你真碰到了四维,怎么理解? 把三维想象成一个“魔方块”,四维就是一排魔方块排在一起。访问的时候需要四个下标,循环遍历的时候就是四层循环套在一起。
初始化就是一层一层套花括号,你能套到天荒地老。但记住一个原则:只有第一维的大小可以省略,其他维的大小必须写清楚。这个规则对所有维度都适用。
你平时写代码,最多用到三维就完全够用了。不要为了炫技去搞四维五维,代码可读性比什么都重要。
五、几个必踩的坑,提前知道不吃亏
坑1:维度搞反了
int a[3][4][5];是3层、4行、5列。你访问的时候写成a[3][4][5]就溢出了,最大是a[2][3][4]。定义里写的是长度,访问用的是下标,长度=最大下标+1。定义和访问是两个概念,别混了。
坑2:循环顺序写乱套了
三层循环,你如果写成外层列、中层行、内层层,也能遍历所有元素,但逻辑别扭而且效率低。标准写法就是外层层、中层行、内层列,跟数组定义的顺序一致,这样最不容易出错。
坑3:多维数组作为函数参数时,除第一维外都得写
void func(int arr[][4][5], int depth) { // 行数4和列数5必须写,层数可以空着 }
为什么?编译器要根据除第一维外的所有维度来计算地址偏移。你少写一个,编译器就不知道该怎么定位元素了。
坑4:内存占用超乎想象
三维数组的大小是层×行×列×每个元素大小。你要是写个int a[100][100][100];,那就是100万个数,每个int占4字节,总共4MB,还行。但要写成double a[500][500][500];,那就是1.25亿个数,占了1GB内存,直接爆掉。定义多维数组之前,先拿计算器按一下总共有多少个元素。
六、最后说几句实在话
多维数组在C语言里,本质上就是“数组的数组的数组……”。每一维不过是在前一个基础上再做一次分组。你理解了这个递归的思维方式,多少维都难不倒你。
但话说回来,实际开发中超过三维的数组非常少见。绝大部分场景到二维就足够了,三维已经算是对数据进行了“立体”的组织。你把三维数组的三层循环玩得滚瓜烂熟,就足以应对大学课程和绝大多数编程场景了。
练到什么程度算过关? 拿到一个三维数组的需求,你能不假思索地写出三层循环来遍历它;知道怎么统计某个维度的数据;知道怎么提取某一层变成二维数组;知道多维数组在内存里是怎么排列的。做到这些,多维数组这块你就彻底拿下了。
还是那句话,打开编译器,把上面的例子自己敲一遍。把层数、行数、列数改一改,把三层循环里每个下标的变化打印出来看看。代码跑通了,脑子里的图景就清晰了。去吧,练起来。