1. 锯齿数组和二维数组有啥不一样
我们终于聊到一个很多人叫做“数组的数组”或者“锯齿数组”的话题——英文里叫jagged arrays。和二维数组不一样,锯齿数组可以存储长度不一样的列。就像你有一堆楼,每栋楼的房子数量都不一样——有的楼5个房子,有的20个,还有的就1个。
锯齿数组其实就是一个数组,它的每个元素本身又是一个数组。而且这些内部的数组(也叫“子数组”)长度可以完全不一样。
最主要的区别:
- 在二维数组里,每一“行”(还有每一“列”)的元素数量都是一样的。比如:int[,] grid = new int[3, 5];——永远是3行,每行5个元素。
- 在锯齿数组里,每一行长度都可以不一样!比如:int[][] jagged = new int[3][];——然后你可以自己给每一行(子数组)单独初始化。
直观上长这样:
| 二维数组 | 锯齿数组 | |
|---|---|---|
| 元素数量 | 严格固定(比如3x5) | 每行可以不一样 |
| 索引方式 | |
|
| 灵活性 | 低 | 高 |
| 应用场景 | 表格、数学 | 不规则数据: 比如学生成绩数量不一样、三角形啥的 |
可视化:对比二维数组和锯齿数组
二维数组 (3x3):
┌───┬───┬───┐
│ 1 │ 2 │ 3 │
├───┼───┼───┤
│ 4 │ 5 │ 6 │
├───┼───┼───┤
│ 7 │ 8 │ 9 │
└───┴───┴───┘
锯齿数组 (长度不一样):
┌───┬───┐
│ 1 │ 2 │
├───┼───┼───┬───┐
│ 3 │ 4 │ 5 │ 6 │
├───┼───┴───┴───┘
│ 7 │
└───┘
2. 锯齿数组的声明和初始化语法
声明锯齿数组其实没啥可怕的!别被两个中括号吓到:
int[][] jaggedArray = new int[3][];
这意思是我们有一个3个元素的数组,每个元素本身又是一个int数组。但现在内部的数组还没创建!为了更好理解,我们来详细拆一下。
锯齿数组的逐步初始化
第1步——创建主(外部)数组:
int[][] jaggedArray = new int[3][];
现在我们有3个“行”,但它们现在都是null。
第2步——创建和填充内部数组(子数组):
比如,第一行长度2,第二行4,第三行3:
jaggedArray[0] = new int[2]; // 第一行2个元素
jaggedArray[1] = new int[4]; // 第二行4个元素
jaggedArray[2] = new int[3]; // 第三行3个元素
第3步——填充值:
内部数组就是普通数组!比如:
jaggedArray[0][0] = 1;
jaggedArray[0][1] = 2;
jaggedArray[1][0] = 3;
jaggedArray[1][1] = 4;
jaggedArray[1][2] = 5;
jaggedArray[1][3] = 6;
jaggedArray[2][0] = 7;
jaggedArray[2][1] = 8;
jaggedArray[2][2] = 9;
锯齿数组的简洁初始化
如果你提前知道值,也可以直接创建并填充锯齿数组:
int[][] jaggedArray = new int[][]
{
new int[] { 1, 2 },
new int[] { 3, 4, 5, 6 },
new int[] { 7, 8, 9 }
};
或者更短一点,省略内部数组的类型:
int[][] jaggedArray = {
new[] { 1, 2 },
new[] { 3, 4, 5, 6 },
new[] { 7, 8, 9 }
};
3. 遍历和操作锯齿数组
遍历锯齿数组其实不比二维数组难,只不过外层循环是行,内层循环是每行的元素(长度可能不一样):
for (int i = 0; i < jaggedArray.Length; i++)
{
Console.WriteLine($"行 {i}:");
for (int j = 0; j < jaggedArray[i].Length; j++)
{
Console.Write($"{jaggedArray[i][j]} ");
}
Console.WriteLine();
}
屏幕上的结果:
行 0:
1 2
行 1:
3 4 5 6
行 2:
7 8 9
你也可以用foreach,不用管索引:
foreach (int[] row in jaggedArray)
{
foreach (int value in row)
{
Console.Write($"{value} ");
}
Console.WriteLine();
}
4. 数组的数组的内部结构
现在你要知道,数组的数组到底是怎么实现的。准备好了吗?
如果是普通数组,“数组变量存的是指向元素容器的引用”。但锯齿数组就有点炸裂:数组的数组变量存的是指向一个容器的引用,这个容器里存着一堆一维数组的引用。看图比说一百遍都清楚:
左边是“数组的数组变量”,它存着“数组容器对象”的引用。中间是“数组容器对象”,每个格子里存着一维数组的引用——也就是锯齿数组的行。右边你看到四个一维数组——就是我们锯齿数组的行。
这就是锯齿数组的真实结构。这种方式给C#程序员带来几个好处:
首先,因为“容器的容器”存的是“行数组”的引用,我们可以很快很方便地交换行。要访问“容器的容器”,只要写一个索引,不用两个。比如:int[][] data = new int[2][];
data[0] = new int[5]; // 第一行——5个元素的数组
data[1] = new int[5]; // 第二行——5个元素的数组
int[] row1 = data[0];
int[] row2 = data[1];
用这种代码可以交换行:
// 重要的数据矩阵
int[][] matrix = {
new int[] {1, 2, 3, 4, 5},
new int[] {5, 4, 3, 2, 1}
};
int[] tmp = matrix[0];
matrix[0] = matrix[1];
matrix[1] = tmp;
如果你访问二维数组的某个格子,但只写一个索引,那你其实访问的是“容器的容器”,它里面存着普通一维数组的引用。
5. 锯齿数组的常见应用场景
什么时候锯齿数组比二维数组更香?
- 如果你要给每个用户存不一样数量的数据:比如每门课的成绩、购物记录、评论啥的。
- 如果你的数据本身就是三角形、阶梯形结构(比如输出金字塔、帕斯卡三角形啥的)。
- 如果你想省内存:二维数组每行都固定长度,锯齿数组只分配需要的元素。
生活中的例子:学生成绩管理器
来扩展一下我们的学习项目!假设每个学生每门课的成绩数量都不一样。比如有的人交作业多,有的人少。锯齿数组就很适合。
假设我们有三个学生,他们的数学作业成绩如下:
| 学生 | 成绩 |
|---|---|
| 0 | 5, 4 |
| 1 | 3, 4, 4 |
| 2 | 5 |
声明这样的数组:
int[][] studentMarks = new int[3][];
studentMarks[0] = new int[] { 5, 4 }; // 第一个学生——2个成绩
studentMarks[1] = new int[] { 3, 4, 4 }; // 第二个学生——3个成绩
studentMarks[2] = new int[] { 5 }; // 第三个学生——1个成绩
输出每个学生的成绩:
for (int i = 0; i < studentMarks.Length; i++)
{
Console.Write($"学生 {i}: ");
for (int j = 0; j < studentMarks[i].Length; j++)
{
Console.Write(studentMarks[i][j] + " ");
}
Console.WriteLine();
}
锯齿数组和其他类型一起用
锯齿数组可以是任何类型的数组:字符串、数组的数组(更深!),甚至你自定义的对象。
例子:字符串数组
string[][] groups = new string[][]
{
new string[] { "伊万", "彼得" },
new string[] { "玛丽亚", "阿列克谢", "谢尔盖" },
new string[] { "瓦西丽萨" }
};
6. 特点和常见错误
锯齿数组很灵活,但也有不少坑。
- 如果你没初始化某个内部数组(jaggedArray[1] = ...),访问它会直接NullReferenceException。记得每个内部数组都要初始化!
- 不是所有行(子数组)长度都一样。如果你用固定的第二维索引,可能会越界。
- 别和二维数组搞混!索引方式是array[i][j],不是array[i, j]。
GO TO FULL VERSION