Nội dung bài học
Là tập hợp các phần tử có cùng dữ liệu. Giả sử bạn muốn lưu n số nguyên để tính trung bình, bạn không thể khai báo n biến để lưu n giá trị rồi sau đó tính trung bình. Ví dụ 1 : bạn muốn tính trung bình 10 số nguyên nhập vào từ bàn phím, bạn sẽ khai báo 10 biến: a, b, c, d, e, f, g, h, i, j có kiểu int và lập thao tác nhập cho 10 biến này như sau:
printf("Nhap vao bien a:");
scanf("%d", &a);
10 biến bạn sẽ thực hiện 2 lệnh trên 10 lần, sau đó tính trung bình: (a + b + c + d + e + f + g + h + i + j)/10
Điều này chỉ phù hợp với n nhỏ, còn đối với n lớn thì khó có thể thực hiện được. Vì vậy khái niệm mảng được sử dụng.
KieuDLptu Tenmang[sopt];
Ví dụ:
int ia[10];
với int là kiểu mảng (KieuDLptu), ia là tên mảng (Tenmang), 10 số phần tử mảng (sopt).
Ý nghĩa: Khai báo một mảng số nguyên gồm 10 phần tử, mỗi phần tử có kiểu int.
Tham chiếu đến từng phần tử mảng
Sau khi mảng được khai báo, mỗi phần tử trong mảng đều có chỉ số để tham chiếu. Chỉ số bắt đầu từ 0 đến n-1 (với n là kích thước mảng). Trong ví dụ trên, ta khai báo mảng 10 phần tử thì chỉ số bắt đầu từ 0 đến 9. 0 1 2 3 4 5 6 7 8 9
Cách tham chiếu: Tên mảng [chỉ số]. Ví dụ ia[0] là phần tử thứ nhất, ia[1] là phần tử thứ hai,...
Dữ liệu có thể được gán đến mảng khi khai báo mảng theo cú pháp:
KieuDLptu Tenmang[sopt] = {pt_1, pt_2,...,pt_sopt};
Ví dụ:
int ia[5] = {7, 5, 15, 3, 8};
Có thể dùng vòng lặp để gán dữ liệu đến mảng như ví dụ sau:
for (i = 0; i < 10; i++) //vòng for có giá trị i chạy từ 0 đến 9
{
printf("Nhap vao phan tu thu %d: ", i + 1);
scanf("%d", &ia[i]);
}
Dữ liệu từ mảng có thể được đọc thông qua cách tham chiếu mảng: Tên mảng [chỉ số]. Ví dụ ia[0] là phần tử thứ nhất, ia[1] là phần tử thứ hai,...
for(i = 0; i > 10; i++)
printf("%3d ", ia[i]);
Ví dụ: Chương trình tính trung bình cộng của n số nguyên cho trước.
Nhập giá trị n và kế tiếp nhập n số nguyên bất kỳ cách nhau bởi khoảng trắng vào ô Stdin Inputs. Ví dụ nhập 3 1 1 2 nghĩa là n = 3 và các phần tử mảng lần lượt là 1, 1, 2. Nhấn nút Execute màu xanh để xem kết quả.
☛ Bài tập:
- Từ ví dụ trên, nhấn dòng liên kết Edit this program in
JDoodle.com để truy cập trang chủ JDoodle.
- Thay đổi chương trình trên để hiển thị ra màn hình phần tử có giá trị lớn nhất của mảng.
- Lưu đến tài
khoản cá
nhân trên JDoodle.
Khi truyền mảng sang hàm, không tạo bản sao mảng mới. Vì vậy mảng truyền sang hàm có dạng tham biến. Nghĩa là giá trị của các phần tử trong mảng sẽ bị ảnh hưởng nếu có sự thay đổi trên chúng.
Ví dụ 1: Chương trình tìm số lớn nhất dùng hàm
Nhập n số nguyên bất kỳ cách nhau bởi khoảng trắng vào ô Stdin Inputs. Ví dụ nhập 3 10 7 2. Nhấn nút Execute màu xanh để xem kết quả.
Chương trình ban đầu hàm max có hai tham số truyền vào và kết quả trả về là giá trị max có kiểu nguyên, một tham số là mảng 1 chiều kiểu int và một tham số có kiểu int. Với chương trình sau khi sửa hàm max chỉ còn một tham số truyền vào nhưng cho kết quả như nhau.
Ví dụ 2: Chương trình tìm số lớn nhất dùng hàm max chỉ có một tham số
Nhập n số nguyên bất kỳ cách nhau bởi khoảng trắng vào ô Stdin Inputs. Ví dụ nhập 3 10 7 2. Nhấn nút Execute màu xanh để xem kết quả.
Do sau khi sửa chương trình mảng ia[MAX] được khai báo lại là biến toàn cục nên hàm max không cần truyền tham số mảng vào cũng có thể sử dụng được. Tuy vậy, khi lập trình bạn nên viết như chương trình theo ví dụ 1 là truyền tham số mảng vào (dạng tổng quát) để hàm max có thể thực hiện được trên nhiều mảng khác nhau. Còn với chương trình sửa lại bạn chỉ sử dụng hàm max được với mảng a mà thôi.
☛ Bài tập:
- Từ ví dụ 1, nhấn dòng liên kết Edit this program in
JDoodle.com để truy cập trang chủ JDoodle.
- Thay đổi hàm max trả về một vị trí mà giá trị tại vị trí đó là giá trị lớn nhất trong mảng.
- Lưu đến tài
khoản cá
nhân trên JDoodle.
Trong thực tế ta thường gặp những công việc cần phải sắp xếp theo một thứ tự nào đó, chẳng hạn danh sách học sinh trong lớp có thể được sắp xếp theo Họ hoặc Điểm trung bình. Việc sắp xếp thứ tự 1 mảng giúp ta dễ dàng tìm kiếm các phần tử của mảng.
Phần dưới đây trình bày hai giải thuật sắp xếp thường được sử dụng nhiều nhất đó là phương pháp nổi bọt (Bubble sort) và sắp xếp nhanh (QuickSort). Chúng ta sẽ gặp lại và tìm hiểu kỹ hơn về các thuật toán này trong môn học Cấu trúc dữ liệu và Giải thuật.
Phương pháp nổi bọt (Bubble sort)
Thuật toán sắp xếp nổi bọt thực hiện sắp xếp dãy số bằng cách lặp lại công việc đổi chỗ 2 số liên tiếp nhau nếu chúng đứng sai thứ tự (số sau bé hơn số trước với trường hợp sắp xếp tăng dần) cho đến khi dãy số được sắp xếp.
Ví dụ minh họa: Giả sử chúng ta cần sắp xếp dãy số [5 1 4 2 8] này tăng dần.
Lần lặp đầu tiên:
( 5 1 4 2 8 ) –> ( 1 5 4 2 8 ) So sánh hai phần tử đầu tiên, và đổi chỗ cho nhau do 5 >
1.
( 1 5 4 2 8 ) –> ( 1 4 5 2 8 ) Đổi chỗ do 5 > 4
( 1 4 5 2 8 ) –> ( 1 4 2 5 8 ) Đổi chỗ do 5 > 2
( 1 4 2 5 8 ) –> ( 1 4 2 5 8 ) Hai phần tử đang xét đã đúng thứ tự (8 > 5), vậy ta không cần đổi chỗ.
Lần lặp thứ 2:
( 1 4 2 5 8 ) –> ( 1 4 2 5 8 )
( 1 4 2 5 8 ) –> ( 1 2 4 5 8 ) Đổi chỗ do 4 > 2
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
Bây giờ, dãy số đã được sắp xếp, Nhưng thuật toán của chúng ta không nhận ra điều đó ngay được. Thuật toán sẽ cần thêm một lần lặp nữa để kết luận dãy đã sắp xếp khi và khi khi nó đi từ đầu tới cuối mà không có bất kỳ lần đổi chỗ nào được thực hiện.
Lần lặp thứ 3:
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
Ví dụ chương trình sau đây sắp xếp và hiển thị dãy số nguyên cho trước.
Nhấn nút Execute màu xanh để xem kết quả.
Đánh giá độ phức tạp thuật toán thuật toán sắp xếp nổi bọt
Thuật toán QuickSort - Sắp xếp nhanh
Thuật toán sắp xếp QuickSort là một thuật toán chia để trị (Divide and Conquer algorithm). Nó chọn một phần tử trong mảng làm điểm đánh dấu (pivot). Thuật toán sẽ thực hiện chia mảng thành các mảng con dựa vào pivot đã chọn. Việc lựa chọn pivot ảnh hưởng rất nhiều tới tốc độ sắp xếp. Nhưng máy tính lại không thể biết khi nào thì nên chọn theo cách nào. Dưới đây là một số cách để chọn pivot thường được sử dụng:
Tầm quan trọng của phân đoạn trong thuật toán QuickSort
Mấu chốt chính của thuật toán QuickSort là việc phân đoạn dãy số (Xem hàm partition()). Mục tiêu của công việc này là: Cho một mảng và một phần tử x là pivot. Đặt x vào đúng vị trí của mảng đã sắp xếp. Di chuyển tất cả các phần tử của mảng mà nhỏ hơn x sang bên trái vị trí của x, và di chuyển tất cả các phần tử của mảng mà lớn hơn x sang bên phải vị trí của x.
Khi đó ta sẽ có 2 mảng con: mảng bên trai của x và mảng bên phải của x. Tiếp tục công việc với mỗi mảng con (chọn pivot, phân đoạn) cho tới khi mảng được sắp xếp.
Thuật toán phân đoạn
Đặt pivot là phần tử cuối cùng của dãy số arr. Chúng ta bắt đầu từ phần tử trái nhất của dãy số có chỉ số là left, và phần tử phải nhất của dãy số có chỉ số là right -1(bỏ qua phần tử pivot). Chừng nào left < right mà arr[left] > pivot và arr[right] < pivot thì đổi chỗ hai phần tử left và right. Sau cùng, ta đổi chỗ hai phần tử left và pivot cho nhau. Xem hình minh họa phía dưới. Khi đó, phần tử left đã đứng đúng vị trí và chia dãy số làm đôi (bên trái và bên phải)
Code minh họa thuật toán phân đoạn
int partition (int arr[], int low, int high)
{
int pivot = arr[high]; // pivot
int left = low;
int right = high - 1;
while(true) {
while(left <= right && arr[left] < pivot) left++; // Tìm phần tử >=
arr[pivot]
while(right >= left && arr[right] > pivot) right--; // Tìm phần tử <=
arr[pivot]
if (left >= right) break; // Đã duyệt xong thì thoát vòng lặp
swap(&arr[left], &arr[right]); // Nếu chưa xong, đổi chỗ.
left++; // Vì left hiện tại đã xét, nên cần tăng
right--; // Vì right hiện tại đã xét, nên cần giảm
}
swap(&arr[left], &arr[high]);
return left; // Trả về chỉ số sẽ dùng để chia đổi mảng
}
Ví dụ cho quá trình phân đoạn
arr[] = {10, 80, 30, 90, 40, 50, 70}
Indexes: 0 1 2 3 4 5 6
pivot = 6, left = 0, right = 5
arr[left] = 10 < arr[pivot] = 70 và left <= right, left = 1
arr[left] = 80 > arr[pivot] = 70, tạm dừng
arr[right] = 50 < arr[pivot] = 70, tạm dừng
Do left < right, đổi chỗ arr[left], arr[right]
arr[] = {10, 50, 30, 90, 40, 80, 70}
left = 2, right = 4
arr[left] = 30 < arr[pivot] = 70 và left <= right, left = 3
arr[left] = 90 > arr[pivot] = 70, tạm dừng
arr[right] = 40 < arr[pivot] = 70, tạm dừng
Do left < right, đổi chỗ arr[left], arr[right]
arr[] = {10, 50, 30, 40, 90, 80, 70}
left = 4, right = 3
// Do left >= right
arr[] = {10, 50, 30, 40, 70, 80, 90}. // Đổi chỗ arr[left] và arr[pivot]
Bây giờ, 70 đã nằm đúng vị trí, các phần từ <= 70 nằm phía trước và lớn hơn 70 nằm phía sau.
Quy trình của thuật toán sắp xếp quick sort
Mã hàm quickSort
void quickSort(int arr[], int low, int high)
{
if (low < high)
{
/* pi là chỉ số nơi phần tử này đã đứng đúng vị trí
và là phần tử chia mảng làm 2 mảng con trái & phải */
int pi = partition(arr, low, high);
// Gọi đệ quy sắp xếp 2 mảng con trái và phải
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
Ví dụ chương trình sau đây sắp xếp tăng dần và hiển thị dãy số nguyên cho trước.
Nhấn nút Execute màu xanh để xem kết quả.
Hai mảng A và B có cùng kiểu thành phần (các phần tử mảng có cùng kiểu dữ liệu) có thể chuyển giá trị cho nhau bằng hàm memmove, cú pháp như sau:
memmove(b, a, sizeof(a))
a: Tên mảng nguồn; b: Tên mảng đích; sizeof(a): Số phần tử chép.
Hàm này thuộc thư viện string.h.
Ví dụ chương trình sau đây chuyển các giá trị từ mảng a cho trước sang mảng b.
Nhấn nút Execute màu xanh để xem kết quả.
Trong lập trình C, bạn có thể tạo một mảng mảng. Các mảng này được gọi là mảng đa chiều. Khai báo tổng quát của một mảng đa chiều:
kieu_du_lieu ten_mang[kichco_1][kichco_2]...[kichco_N];
Ví dụ khai báo mảng 2 chiều:
float x[3][4];
Ở đây, x là một mảng hai chiều (2d). Mảng có thể chứa 12 phần tử. Bạn có thể nghĩ rằng mảng như một bảng có 3 hàng (row) và mỗi hàng có 4 cột (column) như hình ảnh sau:
Tương tự, bạn có thể khai báo một mảng ba chiều (3d). Ví dụ:
float y[2][4][3];
Ở đây, mảng y có thể chứa 24 phần tử.
Tương tự mảng 1 chiều, mảng đa chiều cũng có thể được nhập trực tiếp hay từ bàn phím (thông qua cấu trúc lặp).
Ví dụ khởi tạo mảng 2 chiều
int c[2][3] = {{1, 3, 0}, {-1, 5, 9}};
int c[][3] = {{1, 3, 0}, {-1, 5, 9}};
int c[2][3] = {1, 3, 0, -1, 5, 9};
Ví dụ khởi tạo mảng 3 chiều
int a[2][3][4] = {
{{3, 4, 2, 3}, {0, -3, 9, 11}, {23, 12, 23, 2}},
{{13, 4, 56, 3}, {5, 9, 3, 5}, {3, 1, 4, 9}}};
Ví dụ nhập mảng 2 chiều từ bàn phím và hiển thị ra màn hình
// Chương trình C lưu trữ nhiệt độ (nhietdo) của 2 thành phố (THANHPHO) trong
// 1 tuần (TUAN) và hiển thị theo từng ngày (NGAY) trong tuần.
#include <stdio.h>
const int THANHPHO = 2;
const int TUAN = 7;
int main()
{
int nhietdo[THANHPHO][TUAN];
// lưu trữ giá trị trong mảng 2 chiều
for (int i = 0; i < THANHPHO; ++i)
{
for (int j = 0; j < TUAN; ++j)
{
printf("Thanh pho %d, Ngay %d: ", i + 1, j + 1);
scanf("%d", &nhietdo[i][j]);
}
}
printf("\nHien thi cac gia tri: \n\n");
// Hiển thị các giá trị dùng vòng lặp
for (int i = 0; i < THANHPHO; ++i)
{
for (int j = 0; j < TUAN; ++j)
{
printf("Thanh pho %d, Ngay %d = %d\n", i + 1, j + 1, nhietdo[i][j]);
}
}
return 0;
}
Kết quả:
Thanh pho 1, Ngay 1: 33
Thanh pho 1, Ngay 2: 34
Thanh pho 1, Ngay 3: 35
Thanh pho 1, Ngay 4: 33
Thanh pho 1, Ngay 5: 34
Thanh pho 1, Ngay 6: 32
Thanh pho 1, Ngay 7: 31
Thanh pho 2, Ngay 1: 28
Thanh pho 2, Ngay 2: 27
Thanh pho 2, Ngay 3: 22
Thanh pho 2, Ngay 4: 32
Thanh pho 2, Ngay 5: 32
Thanh pho 2, Ngay 6: 33
Thanh pho 2, Ngay 7: 31
Hien thi cac gia tri:
Thanh pho 1, Ngay 1: 33
Thanh pho 1, Ngay 2: 34
Thanh pho 1, Ngay 3: 35
Thanh pho 1, Ngay 4: 33
Thanh pho 1, Ngay 5: 34
Thanh pho 1, Ngay 6: 32
Thanh pho 1, Ngay 7: 31
Thanh pho 2, Ngay 1: 28
Thanh pho 2, Ngay 2: 27
Thanh pho 2, Ngay 3: 22
Thanh pho 2, Ngay 4: 32
Thanh pho 2, Ngay 5: 32
Thanh pho 2, Ngay 6: 33
Thanh pho 2, Ngay 7: 31
Mảng đa chiều trong C là chủ đề phức tạp và đa dạng. Người học có thể tham khảo thêm về chủ đề này từ Nguồn tham khảo.