1. Mảng răng cưa khác gì mảng hai chiều
Cuối cùng cũng tới chủ đề mà nhiều người gọi là "mảng của mảng" hoặc "mảng răng cưa" — tiếng Anh là jagged arrays. Khác với mảng hai chiều, mảng răng cưa cho phép lưu các cột có độ dài khác nhau. Nó giống như bạn có một khu nhà, mỗi tòa nhà có số căn hộ riêng — một tòa có 5 căn, tòa khác 20 căn, tòa nữa chỉ có 1 căn.
Mảng răng cưa là mảng mà mỗi phần tử của nó lại là một mảng khác. Các mảng con (còn gọi là "subarray") này có thể có độ dài khác nhau.
Điểm khác biệt chính:
- Với mảng hai chiều, mỗi "dòng" (và mỗi "cột") đều có số phần tử bằng nhau. Ví dụ: int[,] grid = new int[3, 5]; — luôn luôn có 3 dòng, mỗi dòng 5 phần tử.
- Với mảng răng cưa, mỗi dòng có thể dài khác nhau! Ví dụ: int[][] jagged = new int[3][]; — rồi sau đó bạn tự khởi tạo từng dòng (subarray) theo ý mình.
Nhìn trực quan thì như này:
| Mảng hai chiều | Mảng răng cưa | |
|---|---|---|
| Số phần tử | Cố định (ví dụ, 3x5) | Có thể khác nhau giữa các dòng |
| Chỉ số | |
|
| Linh hoạt | Thấp | Cao |
| Ứng dụng | Bảng, toán học | Dữ liệu không đều: danh sách sinh viên với số điểm khác nhau, tam giác |
Hình dung: so sánh mảng hai chiều và mảng răng cưa
Mảng hai chiều (3x3):
┌───┬───┬───┐
│ 1 │ 2 │ 3 │
├───┼───┼───┤
│ 4 │ 5 │ 6 │
├───┼───┼───┤
│ 7 │ 8 │ 9 │
└───┴───┴───┘
Mảng răng cưa (độ dài khác nhau):
┌───┬───┐
│ 1 │ 2 │
├───┼───┼───┬───┐
│ 3 │ 4 │ 5 │ 6 │
├───┼───┴───┴───┘
│ 7 │
└───┘
2. Cú pháp khai báo và khởi tạo mảng răng cưa
Khai báo mảng răng cưa cũng không có gì ghê gớm đâu! Đừng sợ dấu ngoặc vuông kép nhé:
int[][] jaggedArray = new int[3][];
Nghĩa là bạn có một mảng gồm 3 phần tử, mỗi phần tử lại là một mảng int. Nhưng lúc này các mảng con bên trong chưa được tạo đâu! Để hiểu rõ hơn, cùng phân tích kỹ nhé.
Khởi tạo mảng răng cưa từng bước
Bước 1 — tạo mảng chính (bên ngoài):
int[][] jaggedArray = new int[3][];
Bây giờ bạn có 3 "dòng", nhưng tất cả đều đang là null.
Bước 2 — tạo và gán các mảng con (subarray):
Ví dụ, dòng đầu dài 2, dòng hai dài 4, dòng ba dài 3:
jaggedArray[0] = new int[2]; // 2 phần tử ở dòng đầu
jaggedArray[1] = new int[4]; // 4 phần tử ở dòng hai
jaggedArray[2] = new int[3]; // 3 phần tử ở dòng ba
Bước 3 — gán giá trị:
Các mảng con này là mảng bình thường thôi! Ví dụ:
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;
Khởi tạo nhanh mảng răng cưa
Bạn có thể tạo và gán giá trị cho mảng răng cưa ngay nếu biết trước giá trị:
int[][] jaggedArray = new int[][]
{
new int[] { 1, 2 },
new int[] { 3, 4, 5, 6 },
new int[] { 7, 8, 9 }
};
Hoặc ngắn hơn, bỏ qua kiểu của mảng con:
int[][] jaggedArray = {
new[] { 1, 2 },
new[] { 3, 4, 5, 6 },
new[] { 7, 8, 9 }
};
3. Lặp và làm việc với mảng răng cưa
Lặp qua mảng răng cưa cũng không khó hơn mảng hai chiều đâu, chỉ là vòng ngoài chạy qua các dòng, vòng trong chạy qua từng phần tử của dòng (có thể dài khác nhau):
for (int i = 0; i < jaggedArray.Length; i++)
{
Console.WriteLine($"Dòng {i}:");
for (int j = 0; j < jaggedArray[i].Length; j++)
{
Console.Write($"{jaggedArray[i][j]} ");
}
Console.WriteLine();
}
Kết quả trên màn hình:
Dòng 0:
1 2
Dòng 1:
3 4 5 6
Dòng 2:
7 8 9
Có thể dùng foreach cho đỡ phải nghĩ về chỉ số:
foreach (int[] row in jaggedArray)
{
foreach (int value in row)
{
Console.Write($"{value} ");
}
Console.WriteLine();
}
4. Cấu trúc của mảng của mảng
Giờ bạn sẽ biết mảng của mảng thực sự hoạt động thế nào. Sẵn sàng chưa?
Với mảng bình thường thì "biến-mảng lưu trữ tham chiếu tới container chứa các phần tử mảng". Còn với mảng răng cưa thì hơi hack não hơn: biến-mảng-của-mảng lưu trữ tham chiếu tới container chứa các tham chiếu tới các mảng một chiều. Nhìn hình sẽ dễ hiểu hơn là giải thích trăm lần:
Bên trái là "biến-mảng-của-mảng", lưu trữ tham chiếu tới "đối tượng-container của mảng". Ở giữa là "đối tượng-container của mảng", trong các ô của nó là tham chiếu tới các mảng một chiều — các dòng của mảng răng cưa. Và bên phải bạn thấy bốn mảng một chiều — các dòng của mảng răng cưa.
Đó là cách mảng răng cưa thực sự hoạt động. Cách này cho C# developer vài lợi thế:
Thứ nhất, vì "container của container" lưu trữ tham chiếu tới "mảng-dòng", bạn có thể đổi chỗ các dòng rất nhanh và dễ. Để truy cập "container của container", chỉ cần một chỉ số thay vì hai. Ví dụ:int[][] data = new int[2][];
data[0] = new int[5]; // dòng đầu — mảng 5 phần tử
data[1] = new int[5]; // dòng hai — mảng 5 phần tử
int[] row1 = data[0];
int[] row2 = data[1];
Với code này bạn có thể đổi chỗ hai dòng:
// Ma trận dữ liệu quan trọng
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;
Nếu bạn truy cập một ô của mảng hai chiều mà chỉ ghi một chỉ số sau tên mảng, bạn sẽ truy cập vào container của container, trong đó lưu các tham chiếu tới các mảng một chiều.
5. Các trường hợp dùng mảng răng cưa
Khi nào mảng răng cưa tiện hơn mảng hai chiều?
- Nếu bạn lưu cho mỗi user số lượng dữ liệu khác nhau: điểm từng môn, mua hàng, comment v.v.
- Nếu dữ liệu của bạn có cấu trúc tam giác hoặc bậc thang (ví dụ, để in hình tháp, tam giác Pascal, v.v.).
- Nếu muốn tiết kiệm bộ nhớ: mảng hai chiều thì mọi dòng đều cố định, còn mảng răng cưa thì chỉ tạo đúng số phần tử cần thiết.
Ví dụ thực tế: quản lý điểm sinh viên
Cùng mở rộng project học tập nhé! Mỗi sinh viên có thể có số điểm khác nhau cho từng môn. Ví dụ, có bạn làm nhiều bài hơn, có bạn ít hơn. Mảng răng cưa là chuẩn luôn.
Giả sử có ba sinh viên, đây là điểm của họ cho các bài toán:
| Sinh viên | Điểm |
|---|---|
| 0 | 5, 4 |
| 1 | 3, 4, 4 |
| 2 | 5 |
Khai báo mảng như sau:
int[][] studentMarks = new int[3][];
studentMarks[0] = new int[] { 5, 4 }; // Sinh viên đầu - 2 điểm
studentMarks[1] = new int[] { 3, 4, 4 }; // Sinh viên hai - 3 điểm
studentMarks[2] = new int[] { 5 }; // Sinh viên ba - 1 điểm
In điểm của từng sinh viên:
for (int i = 0; i < studentMarks.Length; i++)
{
Console.Write($"Sinh viên {i}: ");
for (int j = 0; j < studentMarks[i].Length; j++)
{
Console.Write(studentMarks[i][j] + " ");
}
Console.WriteLine();
}
Dùng mảng răng cưa với kiểu khác
Mảng răng cưa có thể là mảng của bất cứ thứ gì: string, mảng của mảng khác (sâu hơn nữa!), thậm chí là object tự định nghĩa.
Ví dụ: mảng string
string[][] groups = new string[][]
{
new string[] { "Ivan", "Pyotr" },
new string[] { "Maria", "Aleksei", "Sergey" },
new string[] { "Vasilisa" }
};
6. Đặc điểm và lỗi thường gặp
Mảng răng cưa rất linh hoạt, nhưng cũng có nhiều bẫy.
- Nếu bạn chưa khởi tạo một mảng con nào đó (jaggedArray[1] = ...), truy cập vào nó sẽ bị NullReferenceException. Đừng quên khởi tạo từng mảng con nhé!
- Không phải dòng nào (subarray) cũng dài như nhau. Nếu dùng chỉ số cố định ở chiều thứ hai, có thể bị vượt quá giới hạn.
- Đừng nhầm với mảng hai chiều! Cách truy cập là array[i][j], không phải array[i, j].
GO TO FULL VERSION