【C语言-多维数组】啃透C语言多维数组,三维四维也不过是层皮

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 212
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

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语言里,本质上就是“数组的数组的数组……”。每一维不过是在前一个基础上再做一次分组。你理解了这个递归的思维方式,多少维都难不倒你。

但话说回来,实际开发中超过三维的数组非常少见。绝大部分场景到二维就足够了,三维已经算是对数据进行了“立体”的组织。你把三维数组的三层循环玩得滚瓜烂熟,就足以应对大学课程和绝大多数编程场景了。

练到什么程度算过关? 拿到一个三维数组的需求,你能不假思索地写出三层循环来遍历它;知道怎么统计某个维度的数据;知道怎么提取某一层变成二维数组;知道多维数组在内存里是怎么排列的。做到这些,多维数组这块你就彻底拿下了。

还是那句话,打开编译器,把上面的例子自己敲一遍。把层数、行数、列数改一改,把三层循环里每个下标的变化打印出来看看。代码跑通了,脑子里的图景就清晰了。去吧,练起来。