Kĩ thuật lập trình - Phần 1: C ++ cơ bản

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

pdf123 trang | Chia sẻ: huyhoang44 | Lượt xem: 737 | Lượt tải: 0download
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:

  • pdfc_co_ban_5279.pdf