Bài 9.1: Thao tác cơ bản trên mảng 2 chiều

Bài 9.1: Thao tác cơ bản trên mảng 2 chiều
Chào mừng bạn quay trở lại với series "Lặn sâu vào Cấu trúc dữ liệu và Giải thuật" cùng FullhouseDev! Nếu ở các bài trước chúng ta đã làm quen với mảng một chiều - một cách tuyệt vời để lưu trữ và truy cập các phần tử theo một danh sách tuyến tính, thì hôm nay, chúng ta sẽ nâng tầm lên một bước: khám phá thế giới phẳng của mảng 2 chiều!
Hãy tưởng tượng một bảng tính Excel, một bàn cờ vua, một bức ảnh kỹ thuật số, hay đơn giản là một ma trận toán học. Tất cả đều có chung một đặc điểm: chúng được tổ chức theo hàng và cột. Và chính mảng 2 chiều là cấu trúc dữ liệu hoàn hảo để mô hình hóa những thứ như vậy trong lập trình. Nó cho phép chúng ta lưu trữ dữ liệu không chỉ theo một chiều dài mà còn theo cả chiều rộng, tạo thành một lưới hoặc ma trận các phần tử.
Trong bài viết này, chúng ta sẽ cùng nhau đi qua những thao tác cơ bản nhất nhưng cũng quan trọng nhất khi làm việc với mảng 2 chiều trong ngôn ngữ C++. Từ việc khai báo "bảng" này rộng bao nhiêu, cao bao nhiêu, đến cách đặt dữ liệu vào từng "ô" và lấy dữ liệu từ đó ra sao.
Hãy cùng nhau làm chủ công cụ mạnh mẽ này nhé!
Mảng 2 Chiều là gì?
Đơn giản nhất, mảng 2 chiều có thể hiểu là mảng của các mảng. Mỗi phần tử của mảng chính lại là một mảng một chiều. Khi chúng ta truy cập một phần tử trong mảng 2 chiều, chúng ta cần cung cấp hai chỉ số: một chỉ số cho hàng (row) và một chỉ số cho cột (column) mà phần tử đó nằm ở đó.
Ví dụ, nếu mảng một chiều arr[5]
có 5 phần tử được đánh số từ arr[0]
đến arr[4]
, thì mảng 2 chiều matrix[3][4]
có thể được xem như có 3 "hàng", mỗi hàng là một mảng 4 phần tử. Các phần tử được truy cập bằng cú pháp matrix[chỉ_số_hàng][chỉ_số_cột]
.
Lưu ý quan trọng: Trong C++ (và nhiều ngôn ngữ khác), các chỉ số này bắt đầu từ 0. Do đó, với mảng matrix[3][4]
:
- Chỉ số hàng chạy từ 0 đến 2.
- Chỉ số cột chạy từ 0 đến 3.
Phần tử đầu tiên là matrix[0][0]
, phần tử ở hàng 1 cột 2 là matrix[1][2]
, và phần tử cuối cùng là matrix[2][3]
.
1. Khai Báo Mảng 2 Chiều
Trước khi sử dụng mảng, chúng ta cần khai báo nó để trình biên dịch biết kiểu dữ liệu của các phần tử và kích thước của mảng (số hàng và số cột).
Cú pháp cơ bản như sau:
kiểu_dữ_liệu tên_mảng[số_lượng_hàng][số_lượng_cột];
Trong đó:
kiểu_dữ_liệu
: Kiểu dữ liệu của các phần tử trong mảng (ví dụ:int
,float
,char
,...).tên_mảng
: Tên bạn đặt cho mảng (tuân theo quy tắc đặt tên biến).số_lượng_hàng
: Số lượng hàng của mảng. Phải là một hằng số hoặc biểu thức hằng tại thời điểm biên dịch.số_lượng_cột
: Số lượng cột của mảng. Phải là một hằng số hoặc biểu thức hằng tại thời điểm biên dịch.
Ví dụ:
#include <iostream>
int main() {
// Khai báo một mảng 2 chiều số nguyên có 3 hàng và 4 cột
int matrix[3][4];
// Khai báo một mảng 2 chiều ký tự có 5 hàng và 10 cột
char screen[5][10];
// Bạn có thể dùng hằng số để khai báo
const int ROWS = 2;
const int COLS = 3;
double data[ROWS][COLS];
std::cout << "Da khai bao thanh cong cac mang 2 chieu." << std::endl;
return 0;
}
Giải thích: Đoạn code trên chỉ thực hiện việc khai báo các mảng. Nó dành một vùng nhớ đủ lớn để chứa các phần tử, nhưng chưa gán giá trị cụ thể cho chúng (giá trị ban đầu có thể là rác).
2. Khởi Tạo Mảng 2 Chiều
Khởi tạo là quá trình gán giá trị ban đầu cho các phần tử của mảng ngay tại thời điểm khai báo. C++ cung cấp cú pháp khởi tạo khá trực quan sử dụng dấu ngoặc nhọn {}
.
Bạn có thể khởi tạo từng hàng một, hoặc khởi tạo toàn bộ mảng:
#include <iostream>
#include <iomanip> // De in cho dep hon
int main() {
// Khởi tạo mảng 2 chiều 3x4 với các giá trị cụ thể
int matrix[3][4] = {
{1, 2, 3, 4}, // Hang 0
{5, 6, 7, 8}, // Hang 1
{9, 10, 11, 12} // Hang 2
};
// Khởi tạo chỉ một phần: các phần tử còn lại sẽ được gán giá trị 0
int smaller_matrix[2][3] = {
{10, 20}, // Hang 0: cot 2 se la 0
{30} // Hang 1: cot 1 va 2 se la 0
};
// Khởi tạo tất cả các phần tử về 0
int zero_matrix[4][5] = {}; // Hoặc {{}, {}, {}, {}}
// In thử mảng 'matrix' để kiểm tra
std::cout << "Mang matrix:" << std::endl;
for (int i = 0; i < 3; ++i) { // Duyet qua cac hang
for (int j = 0; j < 4; ++j) { // Duyet qua cac cot
std::cout << std::setw(4) << matrix[i][j] << " "; // In phan tu va cach ra
}
std::cout << std::endl; // Xuong dong sau khi in het mot hang
}
std::cout << "\nMang smaller_matrix:" << std::endl;
for (int i = 0; i < 2; ++i) { // Duyet qua cac hang
for (int j = 0; j < 3; ++j) { // Duyet qua cac cot
std::cout << std::setw(4) << smaller_matrix[i][j] << " "; // In phan tu va cach ra
}
std::cout << std::endl; // Xuong dong sau khi in het mot hang
}
return 0;
}
Giải thích:
matrix[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
: Đây là cách khởi tạo đầy đủ. Mỗi cặp ngoặc nhọn bên trong{}
đại diện cho một hàng. Số lượng cặp ngoặc nhọn bên trong phải bằng số hàng đã khai báo.smaller_matrix[2][3] = {{10, 20}, {30}};
: Khai báo 2x3 nhưng chỉ cung cấp ít giá trị hơn. Các phần tử còn lại sẽ tự động được khởi tạo bằng 0.zero_matrix[4][5] = {};
: Cách khởi tạo tất cả các phần tử về 0 một cách nhanh chóng.- Vòng lặp
for
lồng nhau được sử dụng để duyệt qua từng phần tử của mảngmatrix
và in chúng ra.std::setw(4)
(cần<iomanip>
) giúp căn chỉnh đầu ra cho đẹp hơn.
3. Truy Cập và Gán Giá Trị cho Phần Tử
Đây là thao tác cốt lõi khi làm việc với mảng 2 chiều. Để truy cập hoặc thay đổi giá trị của một phần tử cụ thể, bạn sử dụng tên mảng kèm theo hai chỉ số nằm trong cặp ngoặc vuông []
. Chỉ số đầu tiên là chỉ số hàng, chỉ số thứ hai là chỉ số cột.
Cú pháp: tên_mảng[chỉ_số_hàng][chỉ_số_cột]
Ví dụ:
#include <iostream>
int main() {
const int ROWS = 3;
const int COLS = 4;
int matrix[ROWS][COLS]; // Khai bao
// Gan gia tri cho mot phan tu cu the
matrix[0][0] = 10; // Gan gia tri 10 cho phan tu o hang 0, cot 0
matrix[1][2] = 99; // Gan gia tri 99 cho phan tu o hang 1, cot 2
matrix[2][3] = -5; // Gan gia tri -5 cho phan tu o hang 2, cot 3
// Truc tiep gan gia tri trong khi khai bao (da noi o phan 2)
// int initial_matrix[2][2] = {{1, 2}, {3, 4}};
// Truy cap va in gia tri cua mot phan tu cu the
std::cout << "Gia tri tai matrix[0][0] la: " << matrix[0][0] << std::endl;
std::cout << "Gia tri tai matrix[1][2] la: " << matrix[1][2] << std::endl;
std::cout << "Gia tri tai matrix[2][3] la: " << matrix[2][3] << std::endl;
// Thu gan gia tri cho mot phan tu khac va in lai
matrix[0][0] = 50;
std::cout << "Gia tri moi tai matrix[0][0] la: " << matrix[0][0] << std::endl;
// Can THAN TRONG: Truy cap ngoai pham vi chi so se gay loi hoac hanh vi khong xac dinh!
// std::cout << matrix[3][0] << std::endl; // Lỗi! Chi so hang chi den 2
// std::cout << matrix[0][4] << std::endl; // Lỗi! Chi so cot chi den 3
return 0;
}
Giải thích:
- Chúng ta sử dụng
matrix[i][j]
để tham chiếu đến phần tử ở hàngi
, cộtj
. - Khi dùng ở vế trái của phép gán (
=
), chúng ta thay đổi giá trị của phần tử đó. - Khi dùng ở vế phải hoặc trong các biểu thức, chúng ta lấy giá trị hiện tại của phần tử đó.
- Lưu ý quan trọng: C++ không tự động kiểm tra xem chỉ số bạn sử dụng có nằm trong phạm vi hợp lệ của mảng hay không. Truy cập ra ngoài phạm vi (ví dụ: hàng 3 khi chỉ có 3 hàng được đánh số 0, 1, 2) là một lỗi lập trình nghiêm trọng có thể gây ra sự cố chương trình hoặc các kết quả không mong muốn.
4. Nhập Dữ Liệu Từ Bàn Phím vào Mảng 2 Chiều
Để nhập dữ liệu cho toàn bộ mảng từ người dùng (qua console), chúng ta thường sử dụng các vòng lặp lồng nhau. Vòng lặp ngoài duyệt qua các hàng, và vòng lặp trong duyệt qua các cột của mỗi hàng.
Ví dụ:
#include <iostream>
int main() {
const int ROWS = 2; // So hang
const int COLS = 3; // So cot
int matrix[ROWS][COLS];
std::cout << "Nhap cac phan tu cho mang " << ROWS << "x" << COLS << ":" << std::endl;
// Vong lap de nhap du lieu
for (int i = 0; i < ROWS; ++i) { // Duyet qua tung hang (i tu 0 den ROWS-1)
std::cout << "Nhap du lieu cho hang " << i << ":" << std::endl;
for (int j = 0; j < COLS; ++j) { // Duyet qua tung cot trong hang hien tai (j tu 0 den COLS-1)
std::cout << " Nhap phan tu tai [" << i << "][" << j << "]: ";
std::cin >> matrix[i][j]; // Doc gia tri tu ban phim va gan vao phan tu
}
}
std::cout << "\nMang vua nhap la:" << std::endl;
// Vong lap de in mang (de kiem tra)
for (int i = 0; i < ROWS; ++i) {
for (int j = 0; j < COLS; ++j) {
std::cout << matrix[i][j] << "\t"; // In phan tu va cach ra bang tab
}
std::cout << std::endl; // Xuong dong sau moi hang
}
return 0;
}
Giải thích:
- Hai hằng số
ROWS
vàCOLS
được định nghĩa để dễ quản lý kích thước mảng. - Vòng lặp ngoài
for (int i = 0; i < ROWS; ++i)
chạy từi = 0
đếnROWS - 1
, tương ứng với các chỉ số hàng. - Vòng lặp trong
for (int j = 0; j < COLS; ++j)
chạy từj = 0
đếnCOLS - 1
, tương ứng với các chỉ số cột trong hàng hiện tại. - Tại mỗi bước của vòng lặp lồng nhau, chúng ta sử dụng
std::cin >> matrix[i][j];
để đọc một giá trị từ bàn phím và gán vào phần tử tại vị trí[i][j]
. - Sau khi nhập xong, chúng ta sử dụng một cặp vòng lặp lồng nhau khác để in mảng ra màn hình, giúp kiểm tra xem dữ liệu đã được nhập đúng chưa.
5. Xuất (In) Dữ Liệu Từ Mảng 2 Chiều
Để hiển thị nội dung của mảng 2 chiều lên màn hình, chúng ta cũng sử dụng các vòng lặp lồng nhau tương tự như khi nhập liệu, nhưng thay vì dùng cin
, chúng ta dùng cout
.
Quan trọng là phải định dạng đầu ra sao cho dễ nhìn, thường là in mỗi hàng trên một dòng riêng biệt.
Ví dụ:
#include <iostream>
#include <iomanip> // Canh le
int main() {
const int ROWS = 3;
const int COLS = 5;
int matrix[ROWS][COLS] = {
{10, 15, 20, 25, 30},
{5, 10, 15, 20, 25},
{1, 2, 3, 4, 5}
};
std::cout << "Noi dung cua mang " << ROWS << "x" << COLS << ":" << std::endl;
// Vong lap de in mang
for (int i = 0; i < ROWS; ++i) { // Duyet qua tung hang
for (int j = 0; j < COLS; ++j) { // Duyet qua tung cot trong hang hien tai
// In phan tu tai [i][j], dung setw(4) de canh le va in kem khoang trang
std::cout << std::setw(4) << matrix[i][j] << " ";
}
std::cout << std::endl; // Xuong dong sau khi in het mot hang
}
return 0;
}
Giải thích:
- Cặp vòng lặp lồng nhau duyệt qua tất cả các phần tử từ
matrix[0][0]
đếnmatrix[ROWS-1][COLS-1]
. std::cout << std::setw(4) << matrix[i][j] << " ";
in giá trị của phần tử hiện tại.std::setw(4)
đảm bảo rằng mỗi số sẽ chiếm ít nhất 4 ký tự (padding bằng khoảng trắng ở phía trước), giúp các cột thẳng hàng nhau." "
thêm một khoảng trống sau mỗi số để chúng không dính vào nhau.std::cout << std::endl;
được gọi sau vòng lặp trong kết thúc (tức là sau khi in hết tất cả các cột của một hàng). Lệnh này đưa con trỏ xuống dòng tiếp theo, chuẩn bị in hàng mới.
6. Duyệt (Traversal) và Xử Lý Cơ Bản
Duyệt mảng 2 chiều đơn giản là việc "ghé thăm" từng phần tử của nó. Thao tác này là nền tảng cho mọi xử lý dữ liệu phức tạp hơn trên mảng 2 chiều, như tìm kiếm, tính tổng, tìm giá trị lớn nhất/nhỏ nhất, sắp xếp, v.v.
Kỹ thuật duyệt cơ bản nhất vẫn là sử dụng các vòng lặp for
lồng nhau mà chúng ta đã thấy ở phần nhập và xuất. Vòng lặp ngoài cho hàng, vòng lặp trong cho cột (hoặc ngược lại, tùy bài toán, nhưng hàng trước cột sau là phổ biến nhất).
Ví dụ: Tính tổng tất cả các phần tử trong mảng
#include <iostream>
int main() {
const int ROWS = 3;
const int COLS = 3;
int matrix[ROWS][COLS] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
int total_sum = 0; // Bien luu tong
// Duyet qua tung phan tu va cong vao tong
for (int i = 0; i < ROWS; ++i) { // Duyet hang
for (int j = 0; j < COLS; ++j) { // Duyet cot
total_sum += matrix[i][j]; // Cong gia tri phan tu hien tai vao tong
}
}
std::cout << "Tong tat ca cac phan tu trong mang la: " << total_sum << std::endl; // Ket qua la 45
return 0;
}
Giải thích:
- Chúng ta khởi tạo một biến
total_sum
bằng 0 để lưu kết quả. - Vòng lặp lồng nhau duyệt qua mọi phần tử của mảng.
- Trong thân vòng lặp trong,
total_sum += matrix[i][j];
thực hiện việc cộng giá trị của phần tửmatrix[i][j]
vào biếntotal_sum
. - Sau khi hai vòng lặp kết thúc,
total_sum
sẽ chứa tổng của tất cả các phần tử.
Ví dụ: Tìm giá trị lớn nhất trong mảng
#include <iostream>
#include <limits> // De lay gia tri nho nhat cua int
int main() {
const int ROWS = 4;
const int COLS = 2;
int matrix[ROWS][COLS] = {
{10, 5},
{25, 12},
{8, 30},
{15, 20}
};
// Khoi tao gia tri max ban dau rat nho (hoac dung phan tu dau tien)
int max_value = std::numeric_limits<int>::min(); // Gia tri int nho nhat co the
// Duyet qua tung phan tu de tim max
for (int i = 0; i < ROWS; ++i) { // Duyet hang
for (int j = 0; j < COLS; ++j) { // Duyet cot
if (matrix[i][j] > max_value) {
max_value = matrix[i][j]; // Cap nhat neu tim thay gia tri lon hon
}
}
}
std::cout << "Gia tri lon nhat trong mang la: " << max_value << std::endl; // Ket qua la 30
return 0;
}
Giải thích:
- Biến
max_value
được khởi tạo bằng giá trị rất nhỏ (hoặc bạn có thể gán nó bằngmatrix[0][0]
nếu mảng chắc chắn không rỗng).std::numeric_limits<int>::min()
là một cách an toàn để lấy giá trị nhỏ nhất của kiểuint
. - Vòng lặp lồng nhau duyệt qua từng phần tử.
if (matrix[i][j] > max_value)
: Kiểm tra xem phần tử hiện tại có lớn hơn giá trị lớn nhất đã tìm thấy từ trước đến nay không.- Nếu có,
max_value = matrix[i][j];
cập nhậtmax_value
. - Sau khi duyệt hết mảng,
max_value
sẽ chứa giá trị lớn nhất. (Bạn làm tương tự vớimin_value
bằng cách khởi tạo nó với giá trị rất lớn và dùng phép so sánh<
).
Những Điều Cần Lưu Ý Thêm
- Bộ nhớ: Mảng 2 chiều được lưu trữ trong bộ nhớ máy tính theo cách "làm phẳng" (row-major order theo mặc định trong C++). Tức là, toàn bộ hàng 0 được lưu trữ, sau đó đến toàn bộ hàng 1, và cứ thế tiếp tục. Điều này có thể quan trọng trong một số trường hợp tối ưu hiệu năng, nhưng với các thao tác cơ bản thì bạn không cần quá lo lắng.
- Kích thước động: Mảng 2 chiều khai báo theo cú pháp
kiểu tên[hàng][cột];
yêu cầu kích thước (số hàng và số cột) phải biết tại thời điểm biên dịch. Nếu bạn cần mảng có kích thước có thể thay đổi lúc chạy chương trình, bạn cần sử dụng cấp phát động (ví dụ: con trỏ của con trỏ,std::vector<std::vector>>
, hoặcstd::unique_ptr<std::unique_ptr<...>>
trong C++ hiện đại). Đây là một chủ đề nâng cao hơn sẽ được đề cập sau này. - Truyền mảng 2 chiều vào hàm: Khi truyền mảng 2 chiều vào hàm trong C++, bạn phải chỉ định số lượng cột. Số lượng hàng có thể bỏ qua. Ví dụ:
void processMatrix(int arr[][10], int numRows)
. Hoặc sử dụngstd::vector<std::vector>>
để dễ dàng hơn.
Bài tập ví dụ:
Nhân 2 ma trận.
Cho ma trận A cỡ nxm, ma trận B cỡ mxp. Hãy tính ma trận tích của A và B.
Input Format
Dòng đầu tiên là 3 số n, m, p. 1≤n,m,p≤50; Các phần tử trong ma trận là số dương không quá 100000;
Constraints
.
Output Format
In ra ma trận tích của A và B
Ví dụ:
Dữ liệu vào
3 4 5
42 18 35 1
20 25 29 9
13 15 6 46
2 8 5 1 10
5 9 9 3 5
6 6 2 8 2
2 6 3 8 7
Dữ liệu ra
386 714 445 384 587
357 613 410 399 446
229 551 350 474 539
Đây là hướng dẫn giải bài toán Nhân 2 ma trận bằng C++ một cách ngắn gọn:
- Đọc đầu vào: Đọc ba số nguyên n, m, p lần lượt là kích thước của ma trận A (nxm) và B (mxp).
- Khai báo ma trận: Khai báo ba ma trận 2 chiều:
- Ma trận A kích thước n x m.
- Ma trận B kích thước m x p.
- Ma trận kết quả C kích thước n x p.
- Lưu ý: Do các phần tử và tổng có thể lớn, nên sử dụng kiểu dữ liệu
long long
cho các ma trận để tránh tràn số.
- Đọc dữ liệu cho A và B: Dùng các vòng lặp lồng nhau để đọc nm phần tử cho ma trận A và mp phần tử cho ma trận B từ input.
- Tính ma trận tích C: Dùng ba vòng lặp lồng nhau để tính từng phần tử
C[i][j]
của ma trận kết quả C:- Vòng lặp ngoài cùng chạy theo chỉ số hàng
i
của ma trận C, từ 0 đến n-1. - Vòng lặp thứ hai chạy theo chỉ số cột
j
của ma trận C, từ 0 đến p-1. - Bên trong hai vòng lặp trên, để tính
C[i][j]
, khởi tạo một biếnsum
bằng 0 (kiểulong long
). - Vòng lặp thứ ba chạy theo chỉ số
k
từ 0 đến m-1. Trong vòng lặp này, cộng dồnA[i][k] * B[k][j]
vào biếnsum
. - Sau khi vòng lặp thứ ba kết thúc, giá trị của
sum
chính làC[i][j]
. GánC[i][j] = sum;
.
- Vòng lặp ngoài cùng chạy theo chỉ số hàng
- In kết quả: Dùng hai vòng lặp lồng nhau để in các phần tử của ma trận C:
- Vòng lặp ngoài cùng chạy theo chỉ số hàng
i
từ 0 đến n-1. - Vòng lặp bên trong chạy theo chỉ số cột
j
từ 0 đến p-1. - In phần tử
C[i][j]
. In thêm một khoảng trắng sau mỗi phần tử (trừ phần tử cuối cùng của mỗi hàng). - Sau khi kết thúc vòng lặp bên trong (hết một hàng), in một ký tự xuống dòng để chuyển sang hàng tiếp theo.
- Vòng lặp ngoài cùng chạy theo chỉ số hàng
Comments