【C语言-二维数组】啃透C语言二维数组,表格数据就这么拿捏了

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

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缓存命中率高。按列遍历的时候,每访问一个元素都要跳到下一行的同一列,地址跳跃大,效率低。数据量小的时候感觉不出来,数据量大了差异明显。养成按行遍历的习惯。

五、说两句掏心窝的话

二维数组就是“表格思维”,行和列两个维度构成了一个平面。你把这个平面的遍历、查找、统计、变换都练熟了,以后碰到三维数组、多维数组,道理是一样的——无非就是再多套几层循环。

练到什么程度算过关? 拿到一个二维数组的需求,你脑子里能立刻反应过来:用几层循环、外层管什么内层管什么、边界条件是什么、要不要额外开一个数组存结果。把上面那八个例题自己敲一遍,改改数据跑一跑,把每一种操作都写熟练了,二维数组这关你就彻底拿下了。

还是那句话,别光看,打开编译器。二维数组的调试最好用,你把每一步的中间结果打印出来看看,行和列的变换一目了然。动手吧,代码敲多了自然就通了。