Như chúng ta đã tìm hiểu, khi chạy một chương trình C++, CPU bắt đầu thực thi các câu lệnh tại
điểm trên cùng của hàm main, thực hiện lần lượt các câu lệnh từ trên xuống dưới, và kết thúc tại
điểm dưới cùng của hàm main. Chuỗi các câu lệnh được CPU thực thi gọi là program's path. Phần
lớn các chương trình mà bạn từng thấy được thực thi theo dạng straight-line (tuần tự từ trên xuống
dưới). Tuy nhiên, trong một số trường hợp, đây không phải là điều chúng ta muốn.
Ví dụ nếu chúng ta yêu cầu người dùng đưa ra một lựa chọn, và người dùng nhập vào lựa chọn
không phù hợp, chúng ta nên yêu cầu người dùng đưa ra một lựa chọn khác. Với cấu trúc chương
trình dạng straight-line, điều này là bất khả thi.
Một trường hợp khác, chúng ta muốn chương trình thực hiện lặp đi lặp lại một công việc nào đó với
số lần thực hiện chưa biết trước. Ví dụ chúng ta muốn in ra điểm số của một trò chơi trên màn hình
cho đến khi trò chơi kết thúc, chúng ta không thể biết chính xác thời điểm kết thúc trò chơi là khi nào.
Do đó, ngôn ngữ C++ cung cấp các cấu trúc điều khiển (control flow statements) nó cho phép lập
trình viên thay đổi hướng đi của chương trình. Có một số dạng cấu trúc điều khiển khác nhau và
mình sẽ giới thiệu sơ lược để các bạn có sự hình dung ban đầu
123 trang |
Chia sẻ: huyhoang44 | Lượt xem: 826 | Lượt tải: 0
Bạn đang xem trước 20 trang tài liệu Kĩ thuật lập trình - Phần 1: C ++ cơ bản, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
n con đường, để xác định
được vị trí của 1 cái nhà, chúng ta cần biết địa chỉ của nhà cần tìm.
Địa chỉ ô nhớ đầu tiên được đánh số 0, và địa chỉ cuối cùng tương đương với số ô nhớ có trên thiết
bị đó.
1.png?raw=true929x485
Giả sử biến var được khai báo bằng kiểu dữ liệu int32_t, và hệ điều hành tìm được vùng nhớ trống
đủ 4 bytes để cung cấp cho biến var tại vị trí 125 đến 128, biến var sau khi được cấp phát vùng nhớ
sẽ có địa chỉ 125 (là địa chỉ của ô nhớ đầu tiên mà biến nắm giữ).
Ở hình trên chỉ là minh họa cho việc cấp phát vùng nhớ cho biến có kích thước 4 bytes. Trên thực tế,
địa chỉ của ô nhớ được cấp phát cho biến trong chương trình của chúng ta sẽ có giá trị rất lớn do các
chương trình đang chạy trong hệ điều hành của chúng ta đã chiếm giữ trước đó.
Ở trên đây là kết quả của một chương trình mà mình viết. Mình đã tạo ra hai biến kiểu số
nguyên int32_t có vùng nhớ nằm cạnh nhau, và mình thực hiện in ra địa chỉ của 2 biến đó.
Như các bạn thấy, biến đầu tiên có địa chỉ 14324216 thì biến tiếp theo sẽ có địa chỉ cách biến đầu
tiên 4 bytes (là 14324220). Ở các bài học sau, bạn sẽ biết cách cấp phát những vùng nhớ liên tiếp
nhau cho biến.
Làm thế nào để lấy được địa chỉ của biến trong ngôn ngữ C++?
Ví dụ ta khai báo biến có tên var với kiểu dữ liệu bất kì mà bạn đã được học. Để lấy ra địa chỉ của
biến var này, chúng ta đặt toán tử & trước tên của biến.
int32_t var;
cout << "Address of var: " << &var << endl;
Toán tử & được gọi là toán tử tham chiếu (address-of operator). Đoạn chương trình trên sẽ tìm đến
chính xác địa chỉ mà biến var đang nắm giữ và in địa chỉ đó ra màn hình. Các bạn cùng xem kết quả
bên dưới:
Thử chạy lại chương trình một lần nữa:
Chúng ta thấy qua 2 lần chạy chương trình thì địa chỉ của biến này có 2 vị trí khác nhau. Đồng nghĩa
với việc chọn vị trí vùng nhớ để cấp phát cho biến hoàn toàn được thực thi tự động bởi hệ điều hành.
Địa chỉ của biến được định dạng theo hệ cơ số 16 chứ không phải hệ thập phân như chúng ta
thường thấy.
Tham chiếu (Reference)
Một tham chiếu (reference) trong ngôn ngữ C++ cũng là một kiểu dữ liệu cơ bản, nó hoạt động như
một tên giả của biến nó tham chiếu đến.
1) Cách khai báo 1 tham chiếu (reference)
Đặt toán tử & giữa kiểu dữ liệu và tên biến trong khi khai báo biến sẽ tạo thành một tham chiếu.
int32_t & var_reference; //use to refer to another int32_t variable
Khi viết đến đây, compiler sẽ báo lỗi tại dòng khai báo tham chiếu, vì 1 tham chiếu cần có giá trị khởi
tạo là tên biến mà nó sẽ tham chiếu đến.
Một biến tham chiếu chỉ có thể tham chiếu đến một biến khác có cùng kiểu dữ liệu.
2) Thực hiện tham chiếu đến biến khác:
int32_t var = 10;
int32_t & var_reference = var;
3) Thử in ra giá trị của 2 biến var và var_reference:
cout << "Value of var: " << var << endl;
cout << "Value of var_reference: " << var_reference << endl;
Kết quả cho thấy giá trị của biến var_reference hoàn toàn giống với biến var ban đầu.
Điều gì đã xảy ra? Chúng ta cùng làm thêm 1 bước nữa trước khi đi vào kết luận.
4) In ra địa chỉ của 2 biến var và var_reference:
cout << "Address of var: " << &var << endl;
cout << "Address of var_reference: " << &var_reference << endl;
Và đây là kết quả chương trình:
Kết quả cho thấy giá trị của tham chiếu var_reference và địa chỉ của var_reference hoàn toàn giống
với biến var ban đầu. Vậy nó có phải là một bản sao của biến var? Hoàn toàn không phải nhé các
bạn.
Về mặt ngữ nghĩa của dòng lệnh
int32_t & var_reference = var;
Toán tử & không mang ý nghĩa "địa chỉ của", mà nó có nghĩa "tham chiếu đến".
Khi thực hiện tham chiếu từ biến var_reference đến biến var, biến var_reference sẽ kiểm soát vùng
nhớ có địa chỉ là địa chỉ của biến var.
Lúc này, biến var và biến var_reference vẫn là 2 tên biến khác nhau, nhưng chúng có cùng địa chỉ.
Điều này có nghĩa khi chúng ta thực hiện thay đổi giá trị cho biến var_reference, giá trị của
biến var cũng thay đổi và ngược lại.
int32_t var = 10;
int32_t & var_reference = var;
cout << "Value of var: " << var << endl;
cout << "Value of var_reference: " << var_reference << endl;
var++; //Increase value of var
var_reference++; //Increase value of var_reference
cout << endl << "========================================" << endl << endl;
cout << "New value of var: " << var << endl;
cout << "New value of var_reference: " << var_reference << endl;
Một số lưu ý khi sử dụng tham chiếu
1) Các bạn không thể khởi tạo tham chiếu không phải hằng số bằng một biến hằng số.
Đoạn code sau sẽ báo lỗi:
const int32_t var = 10;
int32_t & ref = var;
Vì biến tham chiếu ref có thể thay đổi giá trị bên trong vùng nhớ, nhưng lúc này, var là hằng số nên
giá trị vùng nhớ không được phép thay đổi. Điều này dẫn đến xung đột nên compiler ngăn chặn
chúng ta biên dịch chương trình.
2) Nhưng chúng ta có thể tham chiếu một biến tham chiếu hằng số đến một hằng số.
const int32_t var = 10;
const int32_t & ref = var;
3) Hoặc chúng ta có thể tham chiếu một biến tham chiếu hằng số đến một biến bình
thường.
int32_t var = 10;
const int32_t & ref = var;
4) Chúng ta không có thể thực hiện nhiều lần tham chiếu đến nhiều biến khác nhau.
#include
using namespace std;
int main() {
int32_t i_value1 = 10;
int32_t i_value2 = 20;
cout << "Address of i_value1: " << &i_value1 << endl;
int32_t & ref = i_value1;
cout << "Address of ref: " << &ref << endl;
cout << "Value of ref:" << ref << endl;
ref = i_value2;
cout << "Address of ref: " << &ref << endl;
cout << "Value of ref:" << ref << endl;
cout << "Value of i_value1:" << i_value1 << endl;
}
Đây là kết quả của chương trình này:
Như các bạn thấy, địa chỉ của ref không bị thay đổi, nghĩa là phép gán thứ hai chỉ là phép gán giá trị
thông thường, chứ không phải tham chiếu.
Lưu ý: Biến tham chiếu chỉ có thể tham chiếu một lần duy nhất ngay khi khai báo và khởi tạo.
Chúng ta không thể tham chiếu đến biến có địa chỉ khác sau khi đã khởi tạo.
Tổng kết
Việc hiểu được địa chỉ của biến khá là quan trọng. Sau này khi học đến phần con trỏ trong C++, mình
sẽ còn nhắc lại khái niệm này.
Tiếp tục với bài học ngày hôm nay, chúng ta sẽ cùng tìm hiểu về một cách tổ chức dữ liệu cơ bản
trong thiết bị lưu trữ tạm thời của máy tính giúp khắc phục một số nhược điểm của việc sử dụng các
biến thông thường.
Đặt vấn đề
Giảng viên cần tìm ra điểm số cao nhất của bài kiểm tra môn lập trình cơ sở. Giả sử lớp học có 30
sinh viên có số thứ tự 1 đến 30.
Công việc của những lập trình viên chúng ta là giúp giảng viên này chỉ ra số thứ tự của sinh viên có
điểm kiểm tra cao nhất, và điểm cao nhất đó là bao nhiêu bằng cách viết chương trình ngôn ngữ C++
trên máy tính để tiết kiệm thời gian suy nghĩ.
Tìm hướng giải quyết
Với yêu cầu như trên, chúng ta cần 30 biến để lưu lại điểm của 30 sinh viên.
int32_t score_of_student1;
int32_t score_of_student2;
//....
int32_t score_of_student30;
Vậy là chúng ta cần tới 30 dòng lệnh khai báo 30 biến, chưa kể mất thời gian viết thêm 30 dòng lệnh
nhập dữ liệu vào là điểm của từng sinh viên, sau đó chúng ta còn phải tìm điểm cao nhất.
Một vấn đề khác nãy sinh: Sau khi tìm ra điểm số cao nhất từ 30 biến trên, làm thế nào chúng ta biết
điểm số đó là của sinh viên có số thứ tự nào trong khi 30 biến này được cấp phát hoàn toàn tách biệt
nhau (không theo 1 thứ tự nhất định)?
Rất may mắn cho chúng ta khi ngôn ngữ C/C++ đưa ra cho chúng ta một khái niệm về tổ chức dữ
liệu liên tiếp nhau trên thiết bị cung cấp bộ nhớ. Chúng ta có thể gọi là Mảng một chiều (Array).
Mảng một chiều (Array)
Mảng một chiều (array) là một dãy các phần tử có cùng kiểu dữ liệu được đặt liên tiếp nhau trong
một vùng nhớ, chúng ta có thể ngay lập tức truy xuất đến một phần tử của dãy đó thông qua chỉ số
của mỗi phần tử.
Như hình trên, giả sử mình khai báo mảng một chiều có 3 phần tử kiểu int32_t, mỗi phần tử sẽ có
kích thước 4 bytes.
Mình lấy ví dụ hệ điều hành tìm thấy vùng nhớ trống đủ chổ chứa 3 phần tử của mảng tại địa chỉ 108,
thì phần tử đầu tiên a1 sẽ có địa chỉ là địa chỉ ô nhớ đầu tiên mà hệ điều hành cấp phát (là 108). Khi
PHẦN 5: KIỂU DỮ LIỆU MẢNG
5.0 Mảng một chiều
đó, phần tử thứ 2 sẽ có địa chỉ là địa chỉ của phần tử thứ nhất cộng thêm 4 (4 là kích thước kiểu dữ
liệu int32_t), tương tự cho phần tử thứ 3.
Với kiểu tổ chức dữ liệu này, chúng ta chỉ cần quan tâm đến 2 điều:
• Địa chỉ ô nhớ đầu tiên trong mảng.
• Số phần tử của mảng.
Từ đó, chúng ta có thể truy xuất đến toàn bộ phần tử trong mảng.
Khai báo mảng một chiều
Chúng ta có nhiều cách để khai báo mảng một chiều khác nhau:
• Khai báo nhưng không khởi tạo các phần tử:
[];
Với cách khai báo này, chúng ta cần ghi rõ cho compiler biết số lượng phần tử mà bạn cần sử dụng
đặt trong cặp dấu ngoặc vuông. Ví dụ:
int32_t age_of_students[30];
Mình vừa tạo ra một mảng dữ liệu kiểu int32_t để lưu trữ số tuổi của 30 sinh viên trong 1 lớp học.
Vì mình chưa khởi tạo giá trị cụ thể cho 30 phần tử trong mảng, nên khi truy xuất đến giá trị của từng
phần tử, chúng ta có thể nhận được giá trị khởi tạo mặc định của kiểu int32_t là 0 hoặc giá trị rác (tùy
vào compiler).
• Khai báo và khởi tạo giá trị cho mỗi phần tử:
[] = { , , ... };
Với cách khai báo này, chúng ta không cần thiết xác định trước số phần tử của mảng. Compiler sẽ
xác định số phần tử thông qua số lượng giá trị mà bạn khởi tạo.
char my_string[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
Mình vừa khai báo một mảng phần tử với kiểu kí tự (Chúng ta sẽ đi sâu hơn về chuỗi kí tự trong
những bài học sau), compiler nhìn vào số lượng kí tự mình khởi tạo và cấp phát 6 ô nhớ liên tục
nhau trên vùng nhớ còn trống.
Với kiểu kí tự, mỗi phần tử chỉ chiếm 1 byte, nên chúng ta có 6 bytes liên tiếp nhau để chứa được
chuỗi kí tự trên.
Truy xuất đến các phần tử trong mảng một chiều
Sau khi biết cách khai báo mảng một chiều (array), điều tiếp theo chúng ta cần quan tâm là làm thế
nào để truy xuất đến một phần tử trong mảng.
Mỗi phần tử trong mảng sẽ đi kèm với một chỉ số cho biết vị trí của phần tử có khoảng cách bao
nhiêu so với phần tử đầu tiên của mảng. Phần tử đầu tiên của mảng mang chỉ số 0, phần tử cuối
cùng của mảng có N phần tử sẽ có chỉ số (N - 1).
Cú pháp truy xuất phần tử trong mảng một chiều:
[index];
Trong đó, index là một số nguyên đại diện cho chỉ số của phần tử trong mảng một chiều.
Ví dụ với một mảng một chiều kiểu int32_t có 5 phần tử được khai báo như sau:
int32_t values[] = { 2, 4, 6, 8, 10 };
Khi đó, các phần tử trong mảng lần lượt là:
values[0]; //2
values[1]; //4
values[2]; //6
values[3]; //8
values[4]; //10
Giải thích cho việc tại sao chỉ số của mảng một chiều trong C/C++ bắt đầu từ 0:
Mỗi phần tử trong mảng sẽ đi kèm với một chỉ số cho biết vị trí của phần tử có khoảng cách bao
nhiêu so với phần tử đầu tiên của mảng.
Sau khi khai báo mảng một chiều, địa chỉ của mảng ứng với địa chỉ của phần tử đầu tiên trong mảng.
Vị trí của các phần tử sẽ được tính dựa trên công thức:
index = (address_of_current_element - address_of_the_first_element) / sizeof(data_type);
Lấy lại ví dụ mảng có 3 phần tử kiểu int32_t như trong mục Mảng một chiều (Array)
Cho rằng địa chỉ của mảng a (cũng là địa chỉ của phần tử a1) là 108. Vậy chỉ số của phần tử đầu tiên a1 là:
index_of_a1 = (address_of_a1 - address_of_the_first_element) / sizeof(int32_t);
index_of_a1 = (108 - 108) / 4 = 0;
Như vậy, phần tử đầu tiên của mảng có chỉ số là 0.
Đây chỉ là phần mình làm rõ cho các bạn tại sao chỉ số của mảng một chiều trong C/C++ bắt đầu từ 0
và kết thúc tại (số_phần_tử - 1). Các bạn không cần quan tâm đến việc tính toán chỉ số của mỗi phần
tử mà compiler sẽ làm giúp bạn.
In ra giá trị của tất cả phần tử trong mảng
Để quản lý mảng một chiều, chúng ta cần biết:
• Địa chỉ phần tử đầu tiên của mảng. (Có thể có được thông qua [0])
• Số lượng phần tử của mảng.
Mình sẽ thực hiện một phương pháp tổng quát để lấy ra số lượng phần tử của mảng:
= sizeof() / sizeof();
Chúng ta sử dụng toán tử sizeof, truyền vào tên của mảng chúng ta sẽ nhận được giá trị là tổng kích
thước bộ nhớ sử dụng cho mảng, chia cho kích thước của một phần tử của mảng chúng ta sẽ có
được số lượng phần tử. Ví dụ:
double d_values[] = { 2.08, 1.32, 6, 4.1, 12, 999.99 };
int32_t num_of_elements = sizeof(d_values) / sizeof(double);
//another way
num_of_elements = sizeof(d_values) / sizeof(d_values[0]);
cout << "Number of elements = " << num_of_elements << endl;
Kết quả chương trình sẽ cho ta thấy mảng có 6 phần tử:
Như cách thông thường, chúng ta thường định nghĩa trước số lượng phần tử tối đa mà mảng một
chiều có thể chứa như sau:
#define ARRAY_SIZE 100
//........
float f_values[ARRAY_SIZE];
Lúc này, chúng ta chỉ cần sử dụng ARRAY_SIZE như là số lượng phần tử của mảng. Nhưng cách này
có thể là hao tốn bộ nhớ khi số lượng phần tử thực sự cần sử dụng không đạt đến con số ARRAY_SIZE.
Vì thế, mình thường tính số phần tử của mảng theo cách tổng quát mà mình trình bày ở trên.
Điều gì xảy ra nếu chúng ta truy xuất mảng bằng chỉ số lớn hơn số lượng phần tử?
Các bạn thử chạy đoạn chương trình sau:
int32_t arr[] = { 1, 2, 3, 4, 5 }; //create an array with 5 elements
cout << arr[100] << endl;
Ở lần chạy đầu tiên của đoạn chương trình trên, máy mình cho ra kết quả:
Thử chạy lại chương trình nhiều lần khác nhau, các bạn sẽ thấy được nhiều giá trị khác nhau. Những
giá trị này ở đâu ra?
Đó chính là những giá trị thuộc vùng nhớ mà chương trình khác đang quản lý.
Có thể sau khi các chương trình khác sử dụng vùng nhớ đó và trả lại cho hệ điều hành quản lý, giá trị của ô
nhớ vẫn còn giữ nguyên, nên khi truy cập mảng với chỉ số vượt quá số lượng phần tử tối đa, chúng ta nhận
được những giá trị không có ý nghĩa.
Trường hợp xấu hơn có thể xảy ra là khi các chương trình khác đang sử dụng vùng nhớ mà bạn truy
cập đến, Visual studio sẽ đưa ra cảnh báo về việc xung đột vùng nhớ và cho dừng chương trình của
bạn.
Vì thế việc quản lý số lượng phần tử của mảng là rất quan trọng.
Nhập dữ liệu cho mảng một chiều (Array input)
Giả sử chúng ta có mảng một chiều dùng để chứa 10 số nguyên (có chỉ số từ 0 đến 9). Để nhập dữ
liệu cho từng phần tử trong mảng này, chúng ta có thể sử dụng đối tượng cin trong thư
viện iostream mà các bạn đã được học.
cin >> [index];
Trong đó, index là chỉ số của phần tử của mảng mà chúng ta cần nhập giá trị từ bàn phím và đưa
vào phần tử.
int32_t arr[10];
for(int32_t index = 0; index <= 9; index++) {
cin >> arr[index];
}
Mình vừa sử dụng vòng lặp for (vì mình biết được số lượng phần tử của mảng nên mình biết cần lặp
bao nhiêu lần), trong vòng lặp for này, mình sử dụng biến index và cho nó di chuyển từ giá trị 0 đến
9 tương ứng với từng chỉ số của các phần tử trong mảng. Với mỗi giá trị index được gán, mình thực
hiện nhập dữ liệu từ bàn phím bằng đối tượng cin cho phần tử arr[index].
Một cách tổng quát hơn để nhập dữ liệu cho mảng một chiều
Ở ví dụ trên, mình cho mảng số nguyên có số lượng phần tử cố định là 10. Đối với mảng một chiều
có số lượng phần tử khác nhau thì ta làm thế nào?
Việc đầu tiên chúng ta cần làm là tìm ra số lượng phần tử của mảng. Ví dụ:
int32_t i_values[100];
int32_t num_of_elements = sizeof(i_values) / sizeof(int32_t);
for(int32_t index = 0; index <= (num_of_elements - 1); index++) {
cin >> i_values[index];
}
Với cách này, chúng ta có thể không cần quan tâm đến số lượng phần tử hiện tại của mảng, mà
mình để compiler tính giúp mình.
Mình cho biến index chạy từ 0 đến (num_of_elements - 1) vì như mình đã nói ở trên, mảng một chiều
có chỉ số bắt đầu từ 0 đến số_lượng_phần_tử trừ đi 1.
Đưa ra nhắc nhở khi nhập dữ liệu cho mảng
Chúng ta nên thông báo cho người dùng biết là chúng ta đang nhập dữ liệu cho phần tử nào trong
mảng.
int32_t i_values[100];
int32_t num_of_elements = sizeof(i_values) / sizeof(int32_t);
for(int32_t index = 0; index <= (num_of_elements - 1); index++) {
cout << "Value of element " << index << ": ";
cin >> i_values[index];
}
Như vậy, người dùng sẽ tránh được việc nhập nhầm thứ tự dữ liệu cho các phần tử trong mảng.
Ngoài việc dùng đối tượng cin, chúng ta cũng có thể gán trực tiếp giá trị cho các phần tử trong mảng
thông qua toán tử gán.
int32_t i_values[100];
int32_t num_of_elements = sizeof(i_values) / sizeof(int32_t);
for(int32_t index = 0; index <= (num_of_elements - 1); index++) {
i_values[index] = index + 1;
}
Nhập dữ liệu cho ô nhớ có chỉ số vượt quá số lượng phần tử
Cũng tương tự như việc bạn truy xuất đến phần tử với chỉ số vượt ngoài tầm số lượng phần tử trong
mảng, Visual studio sẽ đưa ra cảnh báo xung đột vùng nhớ và dừng chương trình.
Tổng kết
Cùng nhìn lại vấn đề mình đặt ra ngay từ đầu bài học, mảng một chiều đã giúp chúng ta tiết kiệm thời
gian hơn khi mà chỉ với 1 dòng lệnh khai báo mảng một chiều, chúng ta có thể quản lý 30 vùng nhớ
liên tiếp nhau dùng để lưu trữ điểm của cả 30 sinh viên. Chúng ta cũng có thể biết được điểm số nào
là của sinh viên nào thông qua chỉ số của mảng đó.
Mảng một chiều đã khắc phục nhiều nhược điểm của việc khai báo các biến đơn lẻ. Tuy nhiên, nó
cũng có một số nhược điểm riêng như việc dư thừa vùng nhớ khi không dùng hết số lượng ô nhớ đã
cấp phát, hoặc số lượng phần tử được yêu cầu quá lớn nên hệ điều hành không đủ khả năng cấp
phát. Chúng ta sẽ tìm cách giải quyết những vấn đề này trong những bài học sau.
Bài tập cơ bản
Với yêu cầu đặt ra ban đầu, giảng viên cần biết điểm số cao nhất của 30 sinh viên trong lớp, đồng
thời muốn biết điểm cao nhất là của sinh viên có số thứ tự bao nhiêu. Bạn hãy sử dụng mảng 1 chiều
để giải quyết vấn đề này. (Điểm của sinh viên được nhập từ bàn phím)
Chào các bạn! Chúng ta tiếp tục đồng hành trong khóa học lập trình trực tuyến ngôn
Trong bài học này, mình sẽ hướng dẫn các bạn thực hiện một số thao tác cơ bản với mảng một
chiều, giúp các bạn hình thành tư duy giải các bài toán có thể giải quyết được bằng mảng một chiều
cơ bản.
5.1 Các thao tác cơ bản với mảng một chiều
Sao chép mảng một chiều
Để tạo ra một bản sao khác của mảng một chiều ban đầu, chúng ta cần khai báo thêm 1 mảng một
chiều khác có cùng kích thước với mảng ban đầu. Ví dụ, ta có mảng một chiều cần sao chép như
sau:
#define ARRAY_SIZE 50
//........
int32_t arr[ARRAY_SIZE];
int32_t arr_clone[ARRAY_SIZE];
Việc thực hiện sao chép giá trị từ mảng arr ban đầu sang mảng arr_clone đơn giản chỉ là gán giá trị
của phần tử có cùng chỉ số ở mảng arr cho mảng arr_clone.
for(int32_t index = 0; index <= (ARRAY_SIZE - 1); index++) {
arr_clone[index] = arr[index];
}
Tìm kiếm một phần tử trong mảng một chiều
Vấn đề này cũng tương đương với việc kiểm tra sự tồn tại của một phần tử (hoặc giá trị) trong mảng
một chiều.
Đặt vấn đề
Ví dụ:
char ch_array[] = { 'L', 'e', 'T', 'r', 'a', 'n', 'D', 'a', 't', '\n' };
Mình có mảng một chiều kiểu dữ liệu char đã được khởi tạo bằng các kí tự trong tên của mình như
trên, mình muốn xác định xem liệu 1 kí tự mình nhập từ bàn phím có giống với kí tự nào trong tên
của mình hay không.
Tìm hướng giải quyết
Giả sử kí tự mình nhập vào từ bàn phím là 'D', nếu chưa sử dụng đến máy tính mà chỉ dùng mắt
thường thì chúng ta sẽ làm gì để nhận biết kí tự 'D' có tồn tại trong mảng ch_array hay không?
Mình sẽ lần lượt nhìn vào từng kí tự của mảng ch_array, so sánh từng kí tự mình đang xem xét với kí
tự 'D' mình đã nhập từ bàn phím. Phép so sánh sẽ được mình thực hiện từ kí tự có chỉ số 0 đến kí tự
có chỉ số (10 - 1) trong mảng. Với mỗi lần kiểm tra, sẽ xảy ra 2 trường hợp:
• Nếu mình bắt gặp kí tự giống với kí tự 'D' mà mình đã nhập, mình sẽ không so sánh tiếp nữa,
mà kết luận ngay là kí tự 'D' có ồn tại trong mảng ch_array.
• Nếu kí tự mình đang xem xét khác kí tự 'D' mà mình vừa nhập, mình chuyển đến kí tự tiếp
theo và thực hiện so sánh tương tự.
Nếu đã so sánh hết phần tử trong mảng mà không tìm được kí tự nào trùng khớp với kí tự 'D' mình
đã nhập, lúc này mình có thể kết luận không có phần tử 'D' nào trong mảng ch_array.
Định hình giải pháp dưới dạng sơ đồ khối
Chúng ta hoàn toàn có thể thực hiện giải pháp này trên máy tính, nhưng mình chưa bắt tay vào viết
code ngay, mà mình sẽ vẻ ra sơ đồ khối để các bạn hình dung trước.
0.png?raw=true712x734
Viết code cho từng bước
Đầu tiên, chúng ta cần khai báo mảng một chiều kiểu char như yêu cầu, sau đó ta tính luôn số lượng
phần tử có trong mảng:
char ch_array[] = { 'L', 'e', 'T', 'r', 'a', 'n', 'D', 'a', 't', '\n' };
int32_t N = sizeof(ch_array) / sizeof(char); //calculate the number of elements
Chúng ta phải nhập 1 kí tự từ bàn phím và dùng kí tự đó để so sánh, chúng ta cần 1 biến kiểu char
để lưu trữ kí tự nhập vào:
char ch;
cout << "Enter a character: ";
cin >> ch;
Để so sánh biến ch với từng phần tử trong mảng ch_array, chúng ta sẽ dùng vòng lặp for để truy xuất
đến tất cả các phần tử từ chỉ số 0 đến chỉ số (N - 1):
for (int32_t index = 0; index <= (N - 1); index++) {
}
Trong vòng lặp for này, chúng ta sẽ thực hiện so sánh biến ch với phần từ ch_array[index] để kiểm
tra xem chúng có giống nhau hay không. Chúng ta sẽ dùng 1 biến kiểu bool khai báo ở trên vòng
lặp for để lưu kết quả.
bool check = false;
for (int32_t index = 0; index <= (N - 1); index++) {
}
Biến check ban đầu có giá trị false, nghĩa là hiện tại không tìm thấy phần tử nào giống với
biến ch đã nhập vào. Nếu bắt gặp phần tử có kí tự giống với kí tự mà biến ch lưu trữ, biến check sẽ
chuyển sang giá trị true.
bool check = false;
for (int32_t index = 0; index <= (N - 1); index++) {
if(ch_array[index] == ch) {
check = true;
}
}
Cuối cùng, chúng ta dựa vào giá trị của biến check để chúng ta kết luận:
if(check == true)
cout << "Found" << endl;
else
cout << "Not found" << endl;
Thử chạy chương trình và nhập vào kí tự 'D' và xem kết quả:
Các bạn thử chạy lại chương trình và nhập những kí tự khác để kiểm tra lại.
Dưới đây là mã nguồn đầy đủ của mình:
#include
#include
using namespace std;
int main()
{
char ch_array[] = { 'L', 'e', 'T', 'r', 'a', 'n', 'D', 'a', 't', '\n' };
int32_t N = sizeof(ch_array) / sizeof(char); //calculate the number of elements
char ch;
cout << "Enter a character: ";
cin >> ch;
bool check = false;
for (int32_t index = 0; index <= (N - 1); index++) {
if (ch_array[index] == ch) {
check = true;
break; //break the loop immediately when ch is found.
}
}
if (check == true)
cout << "Found" << endl;
else
cout << "Not found" << endl;
system("pause");
return 0;
}
Tại câu lệnh điều kiện if trong vòng lặp for, mình thực hiện lệnh break để thoát ra khỏi vòng lặp khi
tìm thấy kí tự giống với biến ch. Làm như thế có thể tiết kiệm thời gian tính toán của máy tính.
Chèn một phần tử mới vào vị trí bất kì trong mảng một chiều
Đặt vấn đề
Chúng ta có một mảng được khai báo với số phần tử tối đa được định nghĩa trước. Ví dụ:
#define MAX_SIZE = 100;
//.........
int32_t arr[MAX_SIZE];
Và N là số phần tử đang được sử dụng trong mảng (0 < N < MAX_SIZE). Ví dụ:
int32_t N = 5;
Có người yêu cầu bạn thực hiện công việc chèn 1 giá trị số nguyên insert_value nào đó vào vị
trí insert_position (với insert_value và insert_position là 2 giá trị được nhập từ bàn phím).
Tìm giải pháp
Chúng ta cùng thử tự đặt ra một số câu hỏi liên quan đến vấn đề trên và tự tìm ra câu trả lời để đưa
ra giải pháp.
Điều gì xảy ra nếu 1 phần tử mới được chèn vào mảng?
Điều đầu tiên dễ nhận thấy nhất là số lượng phần tử N hiện có trong mảng sẽ tăng lên 1. Vì thế,
chúng ta cần tăng giá trị của biến N lên 1 để có thêm chổ trống chứa phần tử mới được thêm vào.
N++;
Có một yêu cầu nhỏ khác là vị trí chèn phần tử insert_position sẽ phải nằm trong khoảng
từ 0 đến (N - 1), lúc đó, phần tử mới được chèn vào mới có chỉ số hợp lệ.
Ví dụ mảng ban đầu có 5 phần tử như sau:
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
arr[3] = 4;
arr[4] = 5;
Nếu phần tử cần chèn có giá trị 100 và vị trí chèn là phần tử có chỉ số 2. Mảng kết quả sau khi
chèn sẽ là gì?
2.png?raw=true833x493
Phần tử đầu tiên của mảng gắn liền với địa chỉ ô nhớ đầu tiên mà hệ điều hành cấp phát cho mảng,
vì thế, các phần tử có chỉ số nhỏ hơn vị trí cần chèn không thể thay đổi vị trí. Cách duy nhất là đẩy tất
cả các phần tử có chỉ số từ vị trí cần chèn lui sau 1 ô nhớ, và chúng ta sẽ đặt phần tử mới vào ví trí
cần chèn.
Full source code:
#include
#include
#include
using namespace std;
#define MAX_SIZE 100
int main()
{
//initialize array
int32_t arr[MAX_SIZE];
int32_t N = 5;
for (int32_t index = 0; index <= N - 1; index++) {
arr[index] = index + 1;
}
//input insert_value and insert_position from keyboard
int32_t insert_value, insert_position;
cout > insert_value;
cout > insert_position;
//inserting
N++;
for (int32_t i = N - 2; i >= insert_position; i--) {
int32_t after_i = i + 1;
arr[after_i] = arr[i];
}
arr[insert_position] = insert_value;
//output array
for (int32_t index = 0; index <= N - 1; index++)
cout << arr[index] << " ";
cout << endl;
system("pause");
return 0;
}
Các bạn thử giải thích trong đoạn code thực hiện đẩy các phần tử đứng sau vị trí insert_position này
lui sau một vị trí, tại sao mình cho chỉ số bắt đầu từ (N - 2) nhé.
// why (N - 2)?
for (int32_t i = N - 2; i >= insert_position; i--) {
int32_t after_i = i + 1;
arr[after_i] = arr[i];
}
Xóa một phần tử có giá trị nào đó trong mảng một chiều
Việc thực hiện xóa 1 phần tử có giá trị delete_value nào đó đơn giản hơn việc chèn 1 phần tử mới
vào mảng. Chúng ta chỉ cần làm ngược lại công đoạn chèn phần tử.
Giả sử chúng ta có mảng một chiều được khai báo và khởi tạo như sau:
//initialize array
int32_t arr[MAX_SIZE]; //MAX_SIZE = 100
int32_t N = 5;
for (int32_t index = 0; index <= N - 1; index++) {
arr[index] = index + 1;
}
Chúng ta phải tìm vị trí cần xóa trước đã. Phương pháp tìm kiếm phần tử trong mảng một chiều đã
được mình trình bày ở phần trên, nhưng trong trường hợp tìm kiếm này, chúng ta có một chút thay
đổi. Kết quả nhận được sau khi tìm kiếm không còn là giá trị đúng/sai nữa, mà là chỉ số của phần tử
cần được xóa (nếu tìm thấy).
//input delete_value
int32_t delete_value;
cout > delete_value;
//finding the delete_position
int32_t delete_position = -1;
for (int32_t index = 0; index <= N - 1; index++) {
if (delete_value == arr[index]) {
delete_position = index;
break;
}
}
//Check if program found the delete_value in arr
if (delete_position != -1) {
//remove the element at index delete_position from arr
}
Sau khi tìm kiếm phần tử delete_value trong mảng arr, nếu biến delete_position bị thay đổi thì chúng
ta hiểu rằng phần tử delete_value được tìm thấy. Việc còn lại chúng ta chỉ cần lấp những phần tử
đứng sau vị trí delete_position lên trước 1 chỉ số thì phần tử tại vị trí delete_position sẽ bị ghi đè lên.
3.png?raw=true833x461
Cuối cùng, chúng ta giảm số lượng phần tử N hiện có trong mảng đi 1.
//Check if program found the delete_value in arr
if (delete_position != -1) {
for (int32_t i = delete_position + 1; i <= N - 1; i++) {
int32_t before_i = i - 1;
arr[before_i] = arr[i];
}
N--;
}
//output array
for (int32_t index = 0; index <= N - 1; index++)
cout << arr[index] << " ";
cout << endl;
Sắp xếp mảng một chiều
Ngày nay, chúng ta có rất nhiều cách sắp xếp các phần tử trong mảng một chiều theo thứ
tự tăng/giảm dần. Trong bài học này, mình giới thiệu đến các bạn phương pháp Selection sort để
sắp xếp mảng một chiều theo thứ tự tăng dần.
Selection sort có cách cài đặt và vận hành khá giống với việc sắp xếp mà con người chúng ta
thường làm. Giả sử mình khởi tạo 1 mảng có 10 phần tử với các giá trị được khởi tạo có độ lớn giảm
dần:
//initialize array
int32_t arr[MAX_SIZE];
int32_t N = 10;
for (int32_t i = 0; i <= N - 1; i++) {
arr[i] = N - i;
}
//10 9 8 7 6 5 4 3 2 1
Công việc của chúng ta là sử dụng thuật toán Selection sort để hoán vị các phần tử trong mảng
theo cách nào đó để kết quả ta thu được là mảng arr có giá trị tăng dần: 1 2 3 4 5 6 7 8 9 10.
Làm thế nào để hoán vị giá trị của hai biến có cùng kiểu dữ liệu?
4.png?raw=true746x287
Hoán vị giá trị của hai biến là trao đổi giá trị của hai biến đó. Ví dụ:
//Before swap value
int32_t a = 5;
int32_t b = 10;
//After swap value
a = 10;
b= 5;
Có nhiều cách để hoán vị giá trị hai biến, mình sẽ đưa ra một cách sử dụng phổ biến nhất, đó là dùng
thêm 1 biến để tạm lưu trữ giá trị của một trong hai biến cần hoán vị.
5.png?raw=true746x544
Bằng cách sử dụng thêm một biến Temp để lưu trữ một trong hai giá trị của biến (A hoặc B), chúng ta
có thể dễ dàng thực hiện hoán vị qua 3 bước.
int32_t temp = a;
a = b;
b = temp; //b = old_value_of_a
Chúng ta sẽ thực hiện hoán vị các phần tử trong mảng một chiều để đưa mảng một chiều về
dạng tăng dần trong bài học này.
Thuật toán selection sort
Tư tưởng của thuật toán này là chia mảng thành hai phần, phần đã được sắp xếp có chỉ số thấp,
phần chưa được sắp xếp là những phần tử có chỉ số đứng sau chỉ số của phần tử cuối cùng đã
được sắp xếp.
Khi chúng ta cần sắp xếp mảng một chiều theo thứ tự tăng dần, chúng ta sẽ tìm trong phần mảng
chưa được sắp xếp ra một phần tử nhỏ nhất, và hoán vị với phần tử đứng sau chỉ số cuối cùng của
phần đã được sắp xếp.
6.png?raw=true432x527
Dưới đây là phần code cho việc sắp xếp mảng một chiều bằng thuật toán selection sort:
//sorting
for (int32_t after_sorted_part = 0; after_sorted_part <= N - 1; after_sorted_part++) {
int32_t min_index = after_sorted_part;
for (int32_t find_min_index = after_sorted_part; find_min_index <= N - 1;
find_min_index++) {
if (arr[find_min_index] < arr[min_index]) {
min_index = find_min_index;
}
}
//swap value of arr[min_index] and arr[after_sorted_part]
int32_t temp = arr[min_index];
arr[min_index] = arr[after_sorted_part];
arr[after_sorted_part] = temp;
}
Tổng kết
Qua bài học này, hi vọng các bạn đã có thể tự mình hình thành tư duy các bài toán liên quan đến
mảng một chiều với các thao tác xử lý mảng một chiều mà mình vừa đưa ra.
Bài tập cơ bản
1/ Cho mảng một chiều như sau:
int32_t arr[] = { 2, 6, 5, 7, 9, 1, 3 };
Viết chương trình đảo ngược mảng trên. Mảng kết quả sau khi thực hiện đảo ngược là.
3 1 9 7 5 6 2
2/ Viết chương trình nhập vào một dãy các số nguyên từ bàn phím và lưu vào mảng một chiều, so
sánh tổng các phần tử chẵn với tổng các phần tử lẻ và đưa ra màn hình kết quả.
3/ Viết chương trình in ra tất cả các phần tử của mảng nhưng bỏ qua các giá trị bị trùng lặp. Ví dụ với
mảng một chiều như sau:
4 6 2 2 1 6 9
Kết quả in ra màn hình sẽ là:
4 6 2 1 9
Đến với bài học ngày hôm nay, chúng ta sẽ tiếp tục làm việc với kiểu dữ liệu mảng một chiều, nhưng
chúng ta sẽ sử dụng thư viện array trong namespace std.
Đây là một thư viện giúp chúng ta sử dụng mảng một chiều một cách hiệu quả, rõ ràng hơn, ngoài ra
nó còn giúp chúng ta hạn chế được lỗi thường gặp như truy cập đến chỉ số vượt ngoài giới hạn số
phần tử đang sử dụng.
Thư viện array
Để sử dụng thư viện array, các bạn chỉ cần include thư viện này như sau:
#include
using namespace std;
Chúng ta cũng cần có thêm dòng khai báo namespace std vì thư viện array được định nghĩa bên
trong nó.
Thư viện array cung cấp cho chúng ta kiểu dữ liệu array, biến được tạo ra bởi kiểu dữ liệu này chỉ là
một biến đơn, nhưng vùng nhớ mà nó quản lý sẽ tương đương với số lượng phần tử tối đa mà
chúng ta khai báo từ trước (gần giống như mảng một chiều).
5.2 Thư viện array trong STL
Đối tượng được tạo ra bởi lớp array chỉ cung cấp cho chúng ta một vùng nhớ để lưu trữ một số
lượng phần tử xác định trước, nhưng thông qua một số phương thức được định nghĩa bên trong
lớp array này, chúng ta còn có thể truy xuất một số thông tin liên quan như số lượng phần tử, kiểm
tra mảng có rỗng hay không, ...
Khai báo biến với kiểu dữ liệu array
Một đối tượng có kiểu array khi được khai báo cần xác định được 2 điều:
• Kiểu dữ liệu của các phần tử mà biến array sẽ chứa.
• Số lượng phần tử tối đa của mảng.
Cú pháp khai báo biến kiểu array:
array, > ;
Ví dụ chúng ta cần sử dụng một mảng kiểu int32_t có 10 phần tử, chúng ta khai báo như sau:
array arr;
Khởi tạo giá trị
Khi chưa khởi tạo giá trị cho biến kiểu array, chúng ta sẽ nhận được những giá trị không có ý nghĩa
khi in chúng ra màn hình. Ví dụ với mảng arr trên:
Chúng ta có thể khởi tạo giá trị cho toàn bộ phần tử trong mảng chỉ với 1 dòng lệnh:
.assign();
Giả sử mình muốn gán giá trị 10 cho toàn bộ phần tử trong mảng arr, mình viết như sau:
arr.assign(10);
Kết quả của việc in mảng arr ra màn hình sau khi khởi tạo:
Truy xuất đến các thành phần trong biến có kiểu array
Chúng ta có thể truy cập đến một phần tử của đối tượng của lớp array bằng toán tử [ ] như lúc các
bạn sử dụng mảng một chiều. Ví dụ:
arr[1];
Hoặc các bạn có thể sử dụng phương thức at() được định nghĩa trong lớp array như sau:
arr.at(1);
Sử dụng arr[1] và arr.at(1) đều trả về kết quả là giá trị của phần tử thứ 2 trong mảng.
Truy xuất một số thông tin bên trong đối tượng của lớp array
Chúng ta có thể truy xuất một vài thông tin liên quan đến mảng một chiều bằng một số phương thức
bên trong đối tượng của lớp array.
• Xem số lượng phần tử mà đối tượng của lớp array có thể chứa:
cout << "Number of elements: " << arr.size() << endl;
Phương thức size() trả về số lượng phần tử mà bạn đã khai báo lúc tạo ra đối tượng của
class array.
• Kiểm tra xem mảng một chiều được chứa bên trong đối tượng của lớp array có rỗng hay
không:
Mảng một chiều rỗng nghĩa là số lượng phần tử bằng 0.
if(arr.empty())
cout << "Array is empty." << endl;
else
cout << "Number of elements: " << arr.size() << endl;
Phương thức empty() trả về giá trị true nếu mảng bên trong đối tượng arr có số lượng phần
tử là 0.
• Truy xuất đến phần tử đầu tiên và phần tử cuối cùng của mảng bên trong đối tượng của
lớp array:
• cout << "The first element: " << arr.front() << endl;
cout << "The last element: " << arr.back() << endl;
Ví dụ mảng một chiều của mình được khởi tạo giá trị là 1 2 3 4 5. Kết quả in ra màn hình sẽ là:
Phương thức front() sẽ trả về giá trị của phần tử đầu tiên trong mảng, ngược lại, phương
thức back() sẽ trả về giá trị của phần tử cuối cùng trong mảng.
Nhập dữ liệu cho đối tượng của lớp array
Tương tự lúc các bạn nhập dữ liệu cho mảng một chiều thông thường, chúng ta sử dụng đối
tượng cin để đưa giá trị được nhập từ bàn phím vào trong mỗi phần tử mà đối tượng của
lớp array đang nắm giữ.
for (int i = 0; i < arr.size(); i++) {
cout << "Enter value to element " << i + 1 << ": ";
cin >> arr[i];
}
Lớp array ngăn chặn hành vi truy cập phần tử có chỉ số không phù hợp
Chúng ta chỉ có thể truy xuất đến các phần tử trong đối tượng của lớp array với chỉ số trong phạm vi
từ 0 đến (size() - 1). Sau đây là những hành vi truy xuất hợp lệ:
#define ARRAY_SIZE 10
array arr;
arr.assign(10);
//Access to all of elements of arr object
for (int32_t index = 0; index <= arr.size() - 1; index++) {
cout << arr[index] << " ";
}
cout << endl;
Và dưới đây là một số hành vi truy xuất giá trị của đối tượng arr bằng những chỉ số không hợp lệ:
//Try to access array with wrong index
arr[-1];
arr[arr.size() + 10];
Khi gặp những dòng lệnh này, compiler sẽ đưa ra cảnh báo:
Vì bên trong lớp array có sử dụng thư viện cassert để đặt ra những Assertion, những Assertion này
kiểm tra về chỉ số mà bạn đưa vào cho toán tử [ ] và phương thức at() để kiểm tra sự hợp lệ của chỉ
số trước khi thực hiện lệnh. Mọi hành vì không phù hợp với điều kiện trong Assertion sẽ bị ngăn
chặn.
Các bạn cũng có thể tự mình tạo ra những Assertion bằng cách sử dụng thư viện cassert.
Thư viện cassert
Thư viện cassert cung cấp cho chúng ta macro có tên là assert(expression) giúp chúng ta tạo ra
những Assertion trong chương trình.
Khi gặp macro assert(expression), chương trình sẽ kiểm tra biểu thức expression (là một biểu thức
điều kiện có thể trả về giá trị true/false) và có hai trường hợp có thể xảy ra:
• expression trả về giá trị true:
Chương trình sẽ tiếp tục thực hiện các dòng lệnh phía sau Assertion một cách bình thường.
Ví dụ:
float f_value = 1.0;
assert(typeid(f_value) == typeid(float));
f_value++;
cout << f_value << endl;
Đoạn chương trình trên có 1 Assertion thực hiện công việc kiểm tra kiểu dữ liệu của
biến f_value. Vì biểu thức typeid(f_value) == typeid(float) trả về giá trị true, nên chương
trình vẫn được tiếp tục hoạt động.
• expression trả về giá trị false:
Chương trình sẽ dừng lại tại thời điểm phát hiện biểu thức bên trong Assertion cho giá
trị false.
#define ARRAY_SIZE 5
array arr;
for(int32_t index = 0; index <= arr.size(); index++) {
assert(index >= 0 && index <= arr.size() - 1);
cin >> arr[index];
}
Đoạn chương trình trên thực hiện nhập dữ liệu cho đối tượng arr có kiểu array. Bên trong
vòng lặp for, mình đặt 1 Assertion nhằm kiểm tra chỉ số của mảng có được cung cấp chính
xác hay không. Chỉ số chính xác sẽ nằm trong khoảng từ 0 đến (arr.size() - 1).
Bây giờ mình sẽ chạy đoạn chương trình trên để xem kết quả:
4.png?raw=true793x372
Ngoài việc chương trình đưa ra cửa sổ thông báo lỗi và bắt các bạn Abort chương trình đang
chạy, trên cửa sổ console còn đưa ra thông báo lỗi tại dòng mình đặt Assertion.
Trước khi lỗi xảy ra, mình vẫn nhập dữ liệu bình thường. Vì lúc đó chỉ số index của vòng
lặp for vẫn thõa mãn biểu thức điều kiện bên trong Assertion. Nhưng mà vòng lặp for của
mình lại lặp với biến index chạy từ 0 đến arr.size(), vì thế, giá trị index tại lần lặp cuối cùng đã
vi phạm biểu thức trong Assertion mà mình tự đặt ra.
Tổng kết
Trong bài học hôm nay, các bạn đã được tìm hiểu thêm thư viện array hổ trợ cho các bạn quản lý
mảng một chiều một cách hiệu quả và dễ dàng hơn. Mình cũng đã hướng dẫn cho các bạn cách để
tạo ra những Assertion cho chương trình của các bạn với thư viện cassert. Bất cứ khi nào các bạn
cần đảm bảo chương trình của các bạn không vi phạm quy tắc nào đó, các bạn có thể dùng
macro assert(expression) của thư viện cassert để hạn chế những lỗi có thể xảy ra.
Trong các bài học trước, mình đã giới thiệu đến các bạn về mảng một chiều trong ngôn ngữ C/C++.
5.3 Mảng hai chiều
Mảng một chiều có thể được hiểu là một dãy các phần tử có cùng kiểu dữ liệu được đặt liên tiếp
nhau trong một vùng nhớ, chúng ta có thể ngay lập tức truy xuất đến một phần tử của dãy đó thông
qua chỉ số của mỗi phần tử.
Bây giờ các bạn thử tưởng tượng nếu kiểu dữ liệu của mảng một chiều là mảng một chiều? Hay nói
cách khác, chúng ta có một mảng chứa các mảng một chiều? Lúc này, chúng trở thành mảng 2
chiều.
2D Array
Trước hết, mình cho các bạn xem lại hình ảnh minh họa cho mảng một chiều trên máy tính:
Đây là mảng 1 chiều gồm có 5 phần tử được đánh chỉ số từ 0 đến 4.
Và dưới đây là hình ảnh minh họa cho cách tổ chức dữ liệu mảng hai chiều:
Đây là bảng câu đố của game Sudoku được tạo thành từ 9x9 ô vuông (9 dòng và 9 cột). Giả sử
mình tách dòng đầu tiên của bảng game này ra đứng riêng biệt:
Nó lại trở thành mảng 1 chiều có 9 phần tử.
Vậy, mảng một chiều khi mô phỏng nó bằng hình ảnh, chúng ta chỉ thấy được 1 hàng ngang có
nhiều cột phân chia thành các ô (tượng trưng cho các ô nhớ trong máy tính). Còn khi chúng ta nhìn
vào mảng hai chiều, chúng ta thấy có nhiều hàng, mỗi hàng lại có nhiều cột, đặc biệt hơn là số
lượng cột ở mỗi hàng đều bằng nhau.
Ngôn ngữ C/C++ có hổ trợ cho chúng ta tổ chức dữ liệu theo dạng bảng như trên, hay thường gọi
là mảng hai chiều. Thế thì khi nào chúng ta cần sử dụng mảng hai chiều trong chương trình máy
tính? Trong thực tế, chúng ta gặp rất nhiều thứ được bố trí dưới dạng mảng 2 chiều. Dưới đây là một
số ví dụ thực tế:
• Phòng học:
Như hình minh họa, chúng ta có một phòng học có 2 dãy bàn hàng ngang, mỗi dãy bàn ngang
có thể đủ chổ cho 3 sinh viên. Như vậy mình gọi đây là mảng hai chiều 2x3 (2 hàng, 3 cột).
• Bàn cờ vua:
Bàn cờ vua là một bảng hình vuông có 8 hàng, mỗi hàng có 8 cột, tổng cộng có 64 ô vuông,
mỗi ô có thể đặt 1 quân cờ. Chúng ta có thể gọi đây là một mảng hai chiều 8x8 (8 dòng, 8 cột).
• Trò chơi Tic Tac Toe:
Trò chơi này được chơi trên một bảng 3x3 (3 hàng, 3 cột). Nếu trò chơi này được mô phỏng
trên máy tính, chúng ta có thể sử dụng một mảng hai chiều 3x3 để lưu trữ các kí
tự 'x' hoặc 'o'.
Qua một số hình ảnh minh họa như trên, hi vọng các bạn đã có thể hình dung được mảng hai chiều
là như thế nào. Bây giờ mình sẽ đi vào chi tiết về cách khai báo, khởi tạo giá trị và cách sử dụng
mảng hai chiều trong ngôn ngữ C++.
Khai báo mảng hai chiều
Đối với mảng một chiều, chúng ta chỉ cần khai báo số lượng phần tử (số lượng cột) cho một hàng
duy nhất, do đó, khai báo mảng một chiều có dạng:
[num_of_columns];
Ví dụ:
int iArray[100]; //declare an array of integer can hold 100 elements
Bây giờ, khi quản lý mảng hai chiều, chúng ta còn phải quan tâm thêm về số hàng mà mảng hai
chiều cần cấp phát:
[num_of_rows][num_of_columns];
Lưu ý, khi khai báo số lượng phần tử của mảng hai chiều, số hàng phải đặt trước số cột.
Ví dụ:
int array2D[3][5]; // 3x5 elements (3 rows, 5 columns)
Có thể nói cách khác, mảng có tên array2D có kiểu dữ liệu int, mảng array2D gồm có 3 mảng một
chiều, mỗi mảng một chiều trong đó có thể chứa được tối đa 5 phần tử.
Khởi tạo mảng hai chiều
Mình lấy lại ví dụ về mảng có tên array2D như trên, mình sẽ khởi tạo giá trị cho mảng như sau:
int array2D[3][5] =
{
{ 1, 2, 3, 4, 5 }, //row 1
{ 6, 7, 8, 9, 10 }, //row 2
{ 11, 12, 13, 14, 15 } //row 3
};
Do mảng array2D có 3 hàng, mỗi hàng lại là một mảng một chiều khác nhau, nên mình đã sử dụng
cách khởi tạo của mảng một chiều, áp dụng cho mỗi hàng trong mảng hai chiều array2D.
Các bạn có thể khởi tạo mảng hai chiều theo cách sau:
int array2D[3][5] =
{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
};
Nhưng mình vẫn khuyến khích các bạn sử dụng cách mình trình bày ở trước để tránh nhầm lẫn trong
việc tổ chức dữ liệu.
Những phần tử chưa được khởi tạo giá trị sẽ được gán bằng giá trị mặc định tùy vào mỗi kiểu dữ liệu
khác nhau. Như ví dụ sau mình sử dụng kiểu int để khai báo mảng hai chiều:
int seats[3][5] =
{
{ 1, 2 }, //row 1 = 1, 2, 0, 0, 0
{ 6, 7, 8 }, //row 2 = 6, 7, 8, 0, 0
{ 11 }, //row 3 = 11, 0, 0, 0, 0
};
Tương tự mảng một chiều, nếu các bạn khởi tạo mảng hai chiều ngay khi khai báo, compiler có thể
tự xác định số hàng cần cấp phát:
int array2D[][4] =
{
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 }
};
Các bạn có thể bỏ trống phần khai báo số lượng hàng, nhưng không thể không khai báo số
lượng cột.
Truy cập các phần tử trong mảng hai chiều
Lấy ví dụ mình có một mảng hai chiều có 3 hàng và 4 cột tạo thành bảng như sau:
int board[3][4];
Để xác định tọa độ (ví trị) của một phần tử trong một mảng hai chiều, chúng ta cần xác định hai tham
số là chỉ số dòng và chỉ số cột. Chúng ta truy cập vào chỉ số dòng trước và chỉ số cột sau. Ví dụ:
board[1][2]; //Access element on row 2 and column 3
Thực hiện truy cập mảng board với chỉ số dòng là 1 và chỉ số cột là 2 sẽ trỏ đến ô nhớ tại dòng thứ 2
và cột thứ 3, do chỉ số của mảng sẽ bắt đầu từ 0. Tương tự, để truy cập phần tử của cùng của mảng
hai chiều 3x4, chúng ta truy cập với chỉ số (2, 3).
Để truy cập toàn bộ mảng hai chiều, chúng ta có thể sử dụng 2 vòng lặp: vòng lặp ngoài sẽ truy cập
lần lượt các dòng, vòng lặp bên trong sẽ truy cập tất cả các cột của dòng hiện tại mà vòng lặp ngoài
đang truy cập đến.
int board[3][4] =
{
{ 1, 1, 1, 1 },
{ 2, 2, 2, 2 },
{ 3, 3, 3, 3}
};
for(int row = 0; row < 3; row++)
{
for(int col = 0; col < 4; col++)
{
cout << board[row][col] << " ";
}
cout << endl;
}
Nhập dữ liệu cho mảng hai chiều
Cũng tương tự việc các bạn nhập dữ liệu cho mảng một chiều, chúng ta sử dụng đối tượng cin trong
thư viện iostream. Các bạn chỉ cần lưu ý rằng khi thao tác với các phần tử trong mảng hai chiều,
chúng ta phải cung cấp đủ 2 chỉ số (hàng và cột) thì mới xác định được địa chỉ phần tử mà chúng ta
cần thao tác.
cin >> [row_index][col_index];
Trong đó, row_index là chỉ số dòng của phần tử, col_index là chỉ số cột của phần tử.
Ví dụ:
int board[3][3];
for(int row = 0; row < 3; row++)
{
for(int col = 0; col < 3; col++)
{
cin >> board[row][col];
}
}
Tổng kết
Trong bài học này, chúng ta đã cùng tìm hiểu về một cách tổ chức dữ liệu mới trên máy tính. Mảng
hai chiều được sử dụng khá phổ biến để giải quyết một số thuật toán yêu cầu tối ưu như Quy Hoạch
Động, bài toán đồ thị, ... Cũng có thể được sử dụng trong việc thiết kế một số trò chơi đơn giản, ví dụ
game Minesweeper. Chúng ta sẽ còn ứng dụng nhiều về mảng hai chiều trong các bài học sau.
Bài tập cơ bản
1/ Viết chương trình nhập dữ liệu cho mảng hai chiều có số dòng, số cột dương (tùy ý bạn). In ra
màn hình kết quả là tổng của mỗi dòng trong mảng hai chiều bạn vừa nhập.
Ví dụ mình nhập mảng hai chiều 3x3 như sau:
1 3 4
2 1 6
3 3 5
Kết quả in ra màn hình sẽ là:
8
9
11
Trong đó, 8 là tổng các giá trị trong dòng đầu tiên, 9 là tổng các giá trị của dòng thứ 2, 11 là tổng các
giá trị của dòng thứ 3.
2/ Viết chương trình tìm kiếm sự xuất hiện của giá trị X nhập từ bàn phím trong mảng hai chiều.
Trong bài học này, mình sẽ hướng dẫn các bạn thực hiện một số thao tác cơ bản với mảng hai
chiều, cũng có thể coi đây là giải một số bài tập mẫu cơ bản, giúp các bạn hình thành tư duy giải các
bài toán có thể giải quyết được bằng mảng hai chiều cơ bản.
Tính tổng các phần tử trên đường chéo chính
Trường hợp mảng hai chiều có đường chéo chính và đường chéo phụ chỉ tồn tại khi số hàng bằng
số cột (có nghĩa là ma trận vuông). Khi đó, đường chéo chính có dạng:
Đặc điểm của các phần tử nằm trên đường chéo chính của ma trận vuông là chỉ số hàng luôn bằng
chỉ số cột.
{ a[i][i] | 0 <= i <= n-1 }
5.4 Các thao tác cơ bản với mảng hai chiều
Giả sử số hàng (hoặc số cột) của ma trận vuông này là N, chúng ta chỉ cần sử dụng vòng lặp for để
lặp từ giá trị 0 đến N-1, cứ mỗi lần lặp với biến vòng lặp index, chúng ta cộng dồn giá trị của phần
tử Array[index][index] vào biến tổng nào đó.
int main()
{
int myArr[100][100];
int level;
cout << "Enter level of squared matrix: ";
cin >> level;
//input
for (int row = 0; row < level; row++)
{
for (int col = 0; col < level; col++)
{
cin >> myArr[row][col];
}
}
//calculate
int sum = 0;
for (int index = 0; index < level; index++)
{
sum += myArr[index][index];
}
//output
cout << "Result: " << sum << endl;
system("pause");
return 0;
}
Trong chương trình trên, mảng hai chiều myArr chưa được khởi tạo khi khai báo, nên mình phải
cung cấp thông tin số hàng và số cột cụ thể cho compiler.
Xóa một dòng trong mảng hai chiều
Về phần input, chúng ta nhập dữ liệu bao gồm số hàng, số cột và giá trị của mỗi phần tử trong mảng
hai chiều.
Trong phần xử lý, chúng ta cần nhập số dòng cần loại bỏ khỏi mảng hai chiều. Mình chưa thiết kế
phần xử lý trường hợp nhập sai số dòng. Sau đó, tương tự việc xóa một phần tử trong mảng một
chiều, ở mảng hai chiều, một phần tử chính là một mảng một chiều. Do đó, chúng ta không phải ghi
đè giá trị sau lên giá trị trước, mà chúng ta cần ghi đè dữ liệu của dòng sau lên dòng trước đó.
int main()
{
int myArr[100][100];
int num_of_row, num_of_col;
//input
cout > num_of_row;
cout > num_of_col;
for (int row = 0; row < num_of_row; row++)
{
for (int col = 0; col < num_of_col; col++)
{
cin >> myArr[row][col];
}
}
//process
int removeRow;
cout << "Enter the row you want to remove: ";
cin >> removeRow;
//Overide the next row onto the previous row
for (int row = removeRow; row < num_of_row - 1; row++)
{
for (int col = 0; col < num_of_col; col++)
{
myArr[row][col] = myArr[row + 1][col];
}
}
num_of_row--;
//output
for (int row = 0; row < num_of_row; row++)
{
for (int col = 0; col < num_of_col; col++)
{
cout << myArr[row][col] << " ";
}
cout << endl;
}
system("pause");
return 0;
}
Tổng kết
Trên đây chỉ mới là một số thao tác cơ bản khi cần sử dụng đến mảng hai chiều. Hi vọng bài học này
có thể giúp các bạn hiểu rõ hơn về bản chất của mảng hai chiều khi lưu trữ trong máy tính.
Bài tập cơ bản
Dựa trên chương trình xóa một dòng trong mảng hai chiều mà mình đã làm mẫu ở trên, các bạn hãy
viết chương trình xóa một cột X được nhập từ bàn phím trong mảng hai chiều.
Các file đính kèm theo tài liệu này:
- c_co_ban_5279.pdf