You are on page 1of 87

HƯỚNG DẪN THỰC HÀNH

MÔN
DATA STRUCTURE and
ALGORITHMS IN JAVA
(Gv. Thân Văn Sử)

Tháng 9/2011
Mục lục

1- Sử Dụng Danh Sách Liên Kết..............................................................................................................4


1.1- Khi nào dùng danh sách liên kết.......................................................................................................4
1.2- Nên dùng loại danh sách nào?..........................................................................................................4
1.3- Dùng danh sách liên kết như thế nào trong Java?.............................................................................4
Bài tập...................................................................................................................................................12
2-Bài toán ma trận thưa.............................................................................................................................13
2.1-Mô tả bài toán..................................................................................................................................13
2.2- Phân tích để chọn cách lưu trữ.......................................................................................................13
2.3- Hiện thực........................................................................................................................................15
2.4- Một cách dùng................................................................................................................................15
Bài tập...................................................................................................................................................21
3-Sử dụng Stack – Chồng – Ngăn xếp.......................................................................................................22
3.1-Stack là gì........................................................................................................................................22
3.2-Khi nào dùng stack..........................................................................................................................22
3.3-Dùng stack như thế nào?.................................................................................................................22
3.4-Minh hoạ.........................................................................................................................................23
3.5-Bài toán số nguyên lớn....................................................................................................................25
Bài tập...................................................................................................................................................28
4- Sử dụng Kỹ thuật Đệ quy - Recursion...................................................................................................29
4.1-Đệ quy là gì?...................................................................................................................................29
4.2-Khi nào dùng kỹ thuật đệ quy..........................................................................................................29
4.3-Phân loại các dạng đệ quy...............................................................................................................29
4.4-Kỹ thuật xây dựng hành vi đệ quy...................................................................................................29
4.5-Nhận xét về hàm đệ quy..................................................................................................................29
4.6-Minh hoạ.........................................................................................................................................30
4.7-Khử đệ quy......................................................................................................................................32
4.8-Hồi quy – Backtracking...................................................................................................................34
Bài tập...................................................................................................................................................41
5- SỬ DỤNG CẤU TRÚC CÂY NHỊ PHÂN...........................................................................................41
5.1-Định nghĩa và tính chất...................................................................................................................41
5.2-Mô tả một phần tử trên cây và mô tả một cây nhị phân...................................................................42
5.3-Khi nào dùng cây nhị phân..............................................................................................................42
5.4-Tại sao lại là cây nhị phân...............................................................................................................42
5.5-Tóm tắt về phép duyệt cây- Traversing...........................................................................................42
5.6-Minh hoạ về cây không thứ tự.........................................................................................................43
5.7- Minh hoạ về cây BST các số nguyên..............................................................................................47
5.8- Minh hoạ cách dùng cây BST để viết chương trình quản lý...........................................................57
6- HƯỚNG DẪN CÀI ĐẶT ĐỒ THỊ TRONG JAVA..............................................................................62
6.1-Đồ thị là gì?.....................................................................................................................................62
6.2-Mô hình đồ thị được dùng để biểu diễn những bài toán nào?..........................................................62
6.3-Phân loại đồ thị................................................................................................................................62
6.4-Biểu diễn đồ thị...............................................................................................................................62
6.5-Cài đặt đồ thị sử dụng danh sách kề................................................................................................62
7- CÀI ĐẶT LỚP CHO SORTING...........................................................................................................70
8- NÉN DỮ LIỆU HUFFMAN.................................................................................................................77
8.1- Minh hoạ nén chuỗi bằng Huffman Coding...................................................................................77
1- Sử Dụng Danh Sách Liên Kết
Nội dung:
- Khi nào dùng danh sách liên kết?
- Nên dùng loại danh sách nào?
- Dùng danh sách liên kết như thế nào trong Java
- Minh hoạ
- Bài tập

1.1- Khi nào dùng danh sách liên kết

- Số phần tử của danh sách không biết trước


- Chức năng thêm/xóa thường được dùng
- Ít/ không có yêu cầu sử dụng chức năng sắp xếp.

1.2- Nên dùng loại danh sách nào?

Có 3 loại danh sách liên kết : DSLK đơn (Singly Linked List), DSLK đôi (Doubly Linked List) và
DSLK vòng (Circular List). Dùng loại nào cũng được. Mỗi loại có khác nhau một chút về cách xử lý.

Chức năng SLL DLL CL


Thêm vào đầu Tương đương nhau: O(1)
Thêm vào cuối Tương đương nhau: O(1)
Chèn vào trước 1 phần O(n) vì phải xác định O(1) O(n) vì phải xác định
tử đã biết địa chỉ vị trí đứng trước vị trí đứng trước
Chèn vào trước 1 phần Tương đương nhau: O(1)
tử đã biết địa chỉ
Tìm kiếm Tương đương nhau: O(n) vì tìm tuyến tính
Xoá phần tử tại địa chỉ O(n) vì phải xác định O(1) vì trong phần tử O(n) vì phải xác định
đã biết phần tử đứng ngay có chứa tham khảo đến phần tử đứng ngay
trước phần tử trước nó. trước

Tuỳ thuộc vào các chức năng cần hỗ trợ chúng ta chọn được loại danh sách phù hợp.

Chú ý về chức năng insert một phần tử vào DSLK đơn theo một tiêu chuẩn

1- Dựa vào tiêu chuẩn định trước, duyệt DSLK để xác định phần tử đứng trước (before) và phần tử
đứng sau phần tử mới sẽ được chèn vào.
2- Chèn phần tử mới vào DSLK

info info
next next
before after

info newNode.next= after;


newNode next before.next= newNode;

1.3- Dùng danh sách liên kết như thế nào trong Java?
Bước 1: Xây dựng class mô tả cho một nút trong danh sách ở mức tổng quát.
Bước 2: Xây dựng class mô tả cho một DSLK chứa các nút có dữ liệu trong danh sách ở mức tổng quát
cùng các tác vụ cơ bản trên DSLK này ( tham khảo giải thuật cho từng loại DSLK trong tài liệu giáo
khoa).
Bước 3: Xây dựng class cho dữ liệu của bài toán
Bước 4: Xây dựng class mô tả cho DSLK cụ thể của bài toán. Class này là class con của class DSLK
tổng quát của bước 2. Ở class này, chúng ta hiện thực thêm các hành vi phù hợp với các chức năng của
bài toán.
Bước 5: Xây dựng lớo có hàm main để dùng các lớp đã có để thành chương trình.

Minh hoạ

Phát triển một ứng dụng loại Java console dùng DSLK đơn để quản lý một danh sách nhân viên < code,
name, salary> các chức năng phải hỗ trợ thông qua một menu đơn giản gồm các chức năng: Thêm nhân
viên mới, xoá nhân viên với mã được nhập, tăng lương cho nhân viên với mã được nhập, in danh sách.

Kiến trúc các lớp

Lớp Mô tả cho
SLLNode một nút tổng quát trong DS
SLL DSLK tồng quát
Menu menu dạng ứng dụng console
Employee Nhân viên
LL_EmpList DSLK cụ thể, con của SLL
LL_Employee_Program : chương trình

Code cụ thể:
Kỹ thuật xoá một phần tử có dữ liệu el ( thí dụ xoá trị 13) đã biết:

Chú ý về duyệt DSLK: DSLK chỉ có một chiều để đi từ phần tử đầu đến phần tử cuối. Do vậy, phép
duyệt DSLK để làm một tác vụ nào đó trên các nút nếu dữ liệu trong nút này thoả một điều kiện nào đó
được diễn tả như sau:
for ( SLLNode p = head; p!= null; p= p.next) if (condition) Xử lý nút p ;
Lớp cho DSLK của bài toán
Lớp cho bài toán
Một Kết Quả

Sau khi thêm một số nhân viên:


Bài tập

(1) Hiện thực lại bài toán này nhưng dùng danh sách liên kết đôi hoặc vòng
(2) Hiện thực lại bài toán này nhưng sử dụng gói Swing để tạo giao diện đồ hoạ cho người dùng.
(3) Tương tự bài mẫu, xây dựng ứng dụng thêm/xoá/sửa giá (cho phép tăng/giảm)/ sửa số tháng bảo
hành cho danh sách mặt hàng <code, name, price, guaranty>, in danh sách mặt hàng có số tháng
bảo hành (guaranty) lớn hơn một số nhập từ bàn phím.
2-Bài toán ma trận thưa
Nội dung
- Mô tả bài toán
- Phân tích để chọn cách lưu trữ
- Hiện thực
- Một cách dùng
- Bài tập

2.1-Mô tả bài toán

Có một ma trận R hàng C cột với R và C là các số lớn ( trị khoảng vài ngàn hoặc lớn hơn) nhưng chỉ
thực sự chứa rất ít phần tử mang trị hữu ích. Hãy hiện thực các tác vụ cơ bản trên ma trận này như:
Thêm một phần tử vào dòng i cột j, xoá phần tử vào dòng i cột j, xuất hàng I, xuất cột j …

Đây là bài toán điển hình về cân bằng hiệu suất sử dụng bộ nhớ với hiệu suất về thời gian truy xuất
phần tử/dòng/cột.

Vài áp dụng

- Tìm ma trận tổng, tích của hai ma trận thưa trong đó đa phần là trị 0, rất ít trị khác 0.
- Quản lý bảng điểm sinh viên theo môn học: Có độ 500 môn học với 5000 sinh viên nhưng mỗi
sinh viên chỉ học khoảng 30 môn học và mỗi môn học chỉ có khoảng 100 sinh viên  Khả năng:
500.5000=2500000 phần tử, hiệu dụng: 30.100= 3000 phần tử.

2.2- Phân tích để chọn cách lưu trữ

2.2.1- Cách 1: Dùng mảng hai chiều

Gọi S (size) là kích thước bộ nhớ của dữ liệu của mỗi phần tử.
Số phần tử cần: RC ( tích này có trị hàng triệu)
Tổng bộ nhớ cần: RCS
Nhưng số phần tử thực sự hữu ích rất nhỏ so với tích này  Phí bộ nhớ
Gọi r là số hàng trung bình có phần tử
Gọi c là số cột trung bình có phần tử
Số phần tử thực có: rc

Nhận xét:
- Rất phí bộ nhớ
- Truy xuất một phần tử rất nhanh nhờ hai chỉ số hàng và chỉ số cột  O(1)

Vậy có cách nào tiết kiệm bộ nhớ mà vẫn truy xuất phần tử khá nhanh không?  Góc nhìn cân bằng
(trade-off) giữa hiệu suất về bộ nhớ và hiệu suất về thời gian

2.2.2- Cách 2: Dùng danh sách liên kết đơn cơ bản

Mổi nút trong DSLK đơn có cấu trúc <row, col, info, next> mang ý nghĩa phần tử này có dữ liệu là
info( lưu trữ tốn kích thước bộ nhớ là S) nằm ở hàng row, cột col, và phần tử kế tiếp ở địa chỉ next.
Giả sử rằng 3 thành phần row, col, next cùng tốn bộ nhớ là 4 bytes.

Số phần tử trung bình là rc nên kích thước bộ nhớ cần lưu trữ: rc( S+ 12)
Tích này cũng vẫn rất bé so với RCS ( tham khảo phân tích trước)
Việc lưu trữ các info có thứ tự theo chỉ số hàng, trong một hàng lại có thứ tự theo chỉ số cột. Thí dụ: (
Nên hiểu rằng các phần tử được trình bầy cạnh nhau nhưng là danh sách liên kết đơn)
Row 0 .. 0 2 .. 2 5 .. 5 7 .. 7 .. 9 .. 9 .. 15 .. 15
Col 3 .. 9 13 .. 50 4 .. 90 6 .. 88 .. 20 .. 111 .. 90 .. 99
Info 5 .. 2 1 .. -7 -5 .. 6 .. .. 3 .. 1 .. 66 .. 2 .. -1
Next

Nhận xét:
- Sử dụng bộ nhớ: rc(S+12)
- Truy xuất một phần tử chậm vì phải duyệt tuyến tính: O(rc)

2.2.3- Cách 3: Dùng danh sách liên kết đơn có mảng phụ trợ:

Vẫn dùng cách lưu trữ như trên nhưng ta chịu tốn thêm mảng Rows chứa R tham khảo đến các phần
tử đầu hàng và loại bỏ thành phần row trong cấu trúc phần tử.

Nhận xét:
- Kích thước bộ nhớ phải dùng: 4R + rc(S+8)
o Mảng địa chỉ Rows: 4R
o Một phần tử: S + 8
o Tập phần tử: rc(S+8)
- Truy xuất một phần tử nhanh hơn cách tiếp cận danh sách liên kết đơn cơ bản khá nhiều vì từ chỉ
số hàng i, thông qua Rows[i] ta biết ngay nhóm phần tử của dòng này duyệt nhóm con của dòng
này ta lấy được phần tử ở cột Col=j  O(c)

2.2.4- Cách 4: Dùng một biến thể của danh sách liên kết đôi

Dùng 2 mảng: Mảng rowRefs chứa các địa chỉ của phần tử đầu của các dòng, mảng colRefs chứa các
địa chỉ phần tử đầu của các cột.
Mỗi phần tử có cấu trúc <info, nextInCol, nextInRow> là đủ  Tốn (S + 4+4) = S + 8 bytes
Tuy nhiên, với cấu trúc này khi duyệt để tìm phận phần tử ở hàng i cột j chúng ta phải tiến hành như
sau:
Từ rowRefs[i], ta duyệt dòng i. Khi đến một phần tử, chúng ta không biết phần tử này thuộc cột
nào nên lại phải duyệt tất cả các cột.
 Hiệu suất quá kém.
 Đưa thêm thông tin về dòng, cột sẽ giúp tìm nhanh hơn rất nhiều. Cấu trúc của một nút:
<row, col, info, nextInCol, nextInRow>  Bộ nhớ tốn 8 + S + 8= S+16 bytes

Nhận xét
- Tổng bộ nhớ: 4R + 4C + rc(S+16) trong đó:
o Bộ nhớ cho mảng rowRefs: 4R
o Bộ nhớ cho mảng colRefs: 4C
o Bộ nhớ cho rc phần tử: rc(S+16)
- Truy xuất phần tử dòng i, cột j: Chỉ cần tiến hành duyệt tuyến tính danh sách rowRefs[i] hoặc
colRefs[j]  max(O(c), O(r))

Tóm lại:

Cách 1: Cách 2: DSLK Cách 3: DSLK đơn có Cách 4: Dùng biến thể
Mảng 2 đơn cơ bản sử dụng màng địa chỉ của DSLK đôi
chiều hàng
Lưu trữ RCS rc(S+12) 4R + rc(S+8) 4R + 4C + rc(S+16)
Thử với R=1000 1000.1000.4= 20.30.16=9600 4.1000 + 20.30.12= 4.1000 + 4.1000 +
C=1000, S=4 (int), 000000 11200 20.30.20= 20000
r=20, c=30
Tỉ lệ sử dụng bộ 100% 0.24% 0.28% 0.5%
nhớ
Truy xuất 1 dòng O(c) O(rc) vì phải duyệt O(c) vì chỉ phải duyệt O(c) vì chỉ phải duyệt
toàn bộ danh sách DSLK của dòng tương DSLK của dòng tương
ứng ứng
Truy xuất 1 cột O(r) O(rc) vì phải duyệt O(rc) vì phải duyệt hết O(r) vì chỉ phải duyệt
toàn bộ danh sách các dòng DSLK của cột tương ứng
Tìm phần tử dòng O(1) O(rc) O(c) Max(O(r), O(c
i cột j
Như vậy, ta có thể kết luận cách 4 là tốt nhất trong 4 cách tiếp cận với hệ số sử dụng bộ nhớ chấp
nhận được và hiệu suất về thời gian truy xuất khả thi.

2.3- Hiện thực

Chọn cách 3 để hiện thực.

Bước 1: Xây dựng lớp mô tả tổng quát cho một phần tử trong ma trận thưa (lớp SparseMatrixNode
trong phần minh hoạ).
Bước 2: Xây dựng lớp mô tả cho ma trận thưa tổng quát (lớp SparseMatrix trong phần minh hoạ).
Bước 3: Xây dựng lớp cụ thể cho info của một nút nếu cần ( nếu info của nút chỉ có kiểu cơ bàn như
String, Number) thì bỏ qua bước này.
Bước 4: Xây dựng lớp con của lớp DSLK đã mô tả trong bước 2 cùng với những hành vi đặc thù của
bài toán (lớp IntSparseMatrix trong phần minh hoạ để mô tả ma trận thưa các số nguyên cùng với
hai phép toán cộng, nhân hai ma trận).
Bước 5: Xây dựng lớp có hàm main để thành chương trình (lớp TestIntSparseMatrix trong phần
minh hoạ)..

2.4- Một cách dùng

Hiện thực hai phép toán cộng và nhân hai ma trận thưa chứa các số nguyên rồi chạy thử chương trình
với hai ma trận được để trong phần chú thích của chương trình.

Kiến trúc của lới giải

SparseMatricNode: Lớp cho một nút tổng quát


trong ma trận thưa.
SparseMatrix: Lớp mô tả tổng quát cho 1 ma trận
thưa
IntSparseMatric: Lớp mô tả cho một ma trận thưa
các số nguyên, có tác vụ
cộng/nhân hai ma trận
TestIntSparseMatrix: Lớp có hàm main trong đó
nhập 2 ma trận, xuất hai ma
trận này cùng hai ma trận
tổng, tích của chúng

Codes
Kết quả
Bài tập

Áp dụng ma trận thưa để viết chương trình quản lý bảng điểm cho học sinh có menu đơn giản.
Có nhiều môn học, mỗi môn học chỉ cần được biểu diễn bằng một số nguyên ( mảng chỉ số hàng). Có
nhiều sinh viên. Mỗi sinh viên chỉ cần được biểu diễn bằng 1 số nguyên ( mảng chỉ số cột). Thông tin
bảng điểm mộ tả: môn học có chỉ số nào, được sinh viên chỉ số nào học, kết quả thi là bao nhiêu
điểm. Như vậy, ma trận thưa các số nguyên sẽ được dùng để quản lý bảng điểm.

Menu của chương trình:


1- Nhận điểm cho một môn học với thuật toán được đề nghị như sau:
- Nhập chỉ số môn học;
- Lặp nhập < chỉ số sinh viên, kết quả thi>. Hỏi user nhập nữa không? Kết thúc nhập khi
user chọn N
2- In bảng điểm của một môn học. Thuật toán được đề nghị:
- Nhập chỉ số môn học
- Chương trình in ra bảng điểm gồm <chỉ số sinh viên, kết quả>
3- In bảng điểm của sinh viên. Thuật toán được đề nghị:
- Nhập chỉ số sinh viên
- Chương trình in ra bảng điểm gồm <chỉ số môn học. kết quả>
4- Sửa điểm cho sinh viên. Thuật toán được đề nghị:
- Nhập chỉ số sinh viên
- Nhập chỉ số môn học
- Xuất điểm cũ
- Nhập điểm mới
- Hỏi user có cập nhật không?
- Nếu user trả lời Y thì cập nhật điểm mới cho sinh viên.
3-Sử dụng Stack – Chồng – Ngăn xếp
Nội dung:
- Stack là gì?
- Khi nào dùng stack
- Dùng stack như thế nào?
- Minh hoạ: Xuất số nguyên theo hệ thống số, tính trị biểu thức hậu tố, bài toán số
nguyên lớn dùng chuỗi.
- Bài tập.

3.1-Stack là gì

Stack là một cấu trúc dữ liệu chứa một nhóm phần từ ở đó các phần tử được truy xuất theo chiều
ngược với chiều đưa vào  Cơ chế List In First Out (LIFO)

3.2-Khi nào dùng stack

Khi có một nhóm phần tử ta cần xét duyệt đi từ trước đến sau nhưng xử lý lại từ sau lên trước
nhưng ta sợ rằng không cất những phần tử phiá trước vào kho thì ta không truy xuất lại được
chúng.
Thí dụ:
- Bài toán tìm chuỗi mô tả một số nguyên theo một cơ số cho trước ( cách làm: chia nguyên lấy
ngược số dư).
- Tính trị biểu thức dạng hậu tố/ tiền tố.
- Cộng/ nhân hai chuỗi số lớn.
- Bài toán dò tìm đường đi trong một mê cung ( Code trong sách, trang 161, bạn nên hiện thực)
- Duyệt cây không dùng kỹ thuật đệ quy
- …

3.3-Dùng stack như thế nào?

- Cách 1- Tự xây dựng: Xây dựng một lớp danh sách liên kết/ hoặc mảng để lưu trữ các phần tử
và mọi thao tác chỉ ở một đầu (head). Các tác vụ cần hiện thực: clear ( xoá trống), isEmpty ( kiểm
tra trống), push( đẩu vào stack 1 phần tử), pop (lấy ra khỏi stack 1 phần tử), topEl ( tham khảo
đến phần tử đỉnh stack nhưng không loại nó khỏi stack)
- Cách 2- Sử dụng lớp có sẵn java.util.Stack: Bảng sau cho thấy các hành vi thông dụng đã hiện
thực trong lớp này (hành vi peek tương đương với hành vi topEl đã đề cập ở trên).
-
3.4-Minh hoạ

Viết chương trình cho phép user nhập một số nguyên dương. Xuất dạng khai triển của số này theo
hệ thống số 16, 10, 8, 2.
//dòng 5: khai báo lớp class declaration is omitted
Viết chương trình tính trị một biểu thức dạng hậu tố
3.5-Bài toán số nguyên lớn (bỏ) có thư viện big interger rồi

Biểu diễn số nguyên trong máy tính có hạn chế vì các dữ liệu số nguyên có kích thước lớn nhất là 8
bytes (cho đến thời điểm tài liệu này được viết). Với số quá lớn, bộ nhớ này không đủ để lưu trữ. Do
vậy, cần một dạng khác giúp lưu trữ dố nguyên lớn. Một trong những dạng lưu trữ số lớn là dùng
chuỗi số. Biểu diễn số nguyên lớn dạng chuỗi là dạng tối ưu vì nếu lưu trữ dạng DSLK ta tốn thêm
tham khảo next đến ký số kế tiếp.

Cộng hay nhân hai chuỗi số nguyên đều tiến hành theo chiều từ phải sang trái.  Lưu trữ ký số theo
chiều trái sang phải, khi xử lý lại xử lý các ký số theo chiều phải sang trái. Điều này giống như cơ chế
nạp vào (push) và lấy ra (pop) của stack(Last In First Out) Như vậy, khi hiện thực các phép toán
trên số lớn ta không cần phải sử dụng thêm vùng nhớ tạm làm stack nữa.

Bài minh hoạ sau đây cho thấy cách dùng chuỗi như là stack khi hiện thực tác vụ cộng/ nhân hai
chuỗi số nguyên dài.
Bài tập

- Viết chương trình tính trị biểu thức dạng tiền tố có các toán + - * / ( Gợi ý: Tương tự như cách
tính biểu thức hậu tố nhưng duyệt ngược các thành phần để đưa vào stack  Chặt chuỗi thành
chuỗi con  chuyển vào mảng  Duyệt ngược mảng để đưa vào stack )
- Cải biên chương trình cộng/ nhân hai chuỗi số nguyên dương ở trên để cho phép cộng/ nhân hai
số thực và có hỗ trợ số có dấu.
- Tìm đường thoát khỏi mê cung (code trong sách) nhưng mê cung được xây dựng trong một file
văn bản.
4- Sử dụng Kỹ thuật Đệ quy - Recursion
Nội dung:
- Đệ quy là gì?
- Khi nào dùng kỹ thuật đệ quy
- Phân loại đệ quy
- Kỹ thuật xây dựng hành vi đệ quy
- Nhận xét về hàm đệ quy
- Minh hoạ:
- Khử đệ quy
- Hồi quy - Backtracking
- Bài tập.

4.1-Đệ quy là gì?

Hàm đệ quy là hàm chứa trong thân lời gọi chính nó.

4.2-Khi nào dùng kỹ thuật đệ quy

- Muốn thay thế một diễn đạt lặp  Đệ quy là phương tiện diễn đạt vòng lặp.
- Diễn đạt cách thực hiện trong tự nhiên : Làm việc 1; làm việc 2; …. và cứ thế.

4.3-Phân loại các dạng đệ quy

Góc nhìn về nơi gọi đệ quy trong hàm:


Đệ quy đầu (head, left, nonTail), đệ quy đuôi (tail, right recursion)

Góc nhìn về số lần và cách gọi đệ quy


Đệ quy lồng nhau – nested recursion: Gọi chính nó: Đệ quy tuyến tính (linear- gọi 1 lần – hàm
tìm giai thừa), nhị phân ( gọi 2 lần –hàm tìm trị Fibonacci),
Đệ quy gián tiếp/ hỗ tương (correlative recursion, indirect): Các hàm đệ quy gọi qua lại nhau.
Đệ quy quá mức (excessive): Thân hàm gọi đệ quy nhiều lần ( từ 2 lần trở lên – bài toán
Fibonacci)

4.4-Kỹ thuật xây dựng hành vi đệ quy

Thí dụ : Tìm n! = 1*2*3*…*n = n(1*2*3*…*(n-1)) Tác vụ int factorial (int n)


n! = 1 ; n<2
n! = n * (n-1)!

- Bước 1: Xác định tình huống nền – chặn ( ground case, anchor) ở đó không gọi hàm đệ quy: if
(n==1) return 1;
- Bước 2: Xác định quy luật gọi chính tác vụ này để độ phức tạp của tác vụ chuyển dần về tình
hướng bị chặn.
if (n>1) return n* factorial(n-1) ;

4.5-Nhận xét về hàm đệ quy


- Dễ hiện thực vì rất nhiều điều trong toán học hoặc tự nhiên được diễn đạt dạng đệ quy.
- Thực thi chậm (vì gọi hàm nhiều lần) và tốn bộ nhớ (vì có nhiều bản hàm phải được chạy tuần tự
 có nhiều vùng nhớ chứa biến của các bản hàm trong stack- activation record)  Có thể cạn
bộ nhớ.

4.6-Minh hoạ

Vẽ hình bánh tuyết von Kock ( von Kock snowflakes)

Trong minh hoạ này những điều học được:


- Cách tạo đồ hoạ cơ bản trong Java
- Dùng kỹ thuật đệ quy để tạo sản phẩm đồ hoạ.

Thiết kế GUI

Ý tưởng để vẽ đoạn thẳng có màu color từ điểm hiện hành curP(curP.x, curP.y) đến điểm p(p.x, p.y) trên
mà hình đồ hoạ:
Lặp ấn định mầu color cho các điểm từ curP đến p.
Làm sao để tính điểm kế tiếp từ điểm hiện hành: Dùng định lý Pythagore vì đã biết hệ số góc cùng toạ độ
điểm hiện hành. Điểm kế tiếp có chênh lệch với x (hoặc y) hiện hành  1.
4.7-Khử đệ quy

Đệ quy là một cách diễn đạt vòng lặp  Dùng vòng lặp để khử đệ quy khi đã biết cách xử lý.

Thí dụ: Hàm tính giai thừa của một số nguyên – viết dạng đệ quy và không đệ quy

int factorial1 ( int n) { int factorial2 ( int n) {


if (n<2) return 1; int result =1;
return n* fatorial1(n-1); for (int i=2; i<=n;i++) result *= i;
} return result;
}

Thí dụ: Hàm tính trị thứ n của dãy Fibonacci

int fibo1 ( int n) { int fibo2 ( int n) {


if (n<3) return 1; int t1=1, t2=1, result =1;
return fnibo1(n-2) + fibo1n-1); for (int i=3; i<=n;i++) {
} result = t1 + t2;
t1= t2;
t2= result;
}
return result;
}

Hàm đệ quy  cần cấp bộ nhớ để lưu trữ  dùng system stack
 Khử đệ quy: Dùng stack tự tạo để tạm lưu những các dữ liệu.  Chúng tương đương về
bản chất.

Thí dụ: Hàm xuất ngược các ký số của 1 số nguyên dương


4.8-Hồi quy – Backtracking

Bài toán: Tập các biến xi, mỗi biến có thể có miền trị riêng Di có thể kèm theo tập điều kiện C.
Lời giải – cấu hình (solution, configuration) là một trạng thái của bài toán ở đó các biến đã được
gán các trị thoả điều kiện C.
Không gian bài toán: problem space, state space: Tập các khả năng gán trị cho biến dù trị của biến
có thoả mãn C hay không.
Giải bài toán được xem là quá trình gán trị cho các biến sao cho thoả C.

Với một bài toán cho trước, việc tìm được một cấu hình có thể phải dò tìm từ nhiều cách (hướng giải
quyết) khác nhau (các cách gán trị khác nhau). Từ một hướng giải, hy vọng phát hiện một/vài cấu
hình lời giải của bài toán. Trong tình huống xấu nhất, chúng ta phải xét hết các khả năng có thể có để
giải bài toán (xét hết các hướng giải – vét cạn – exhaustive searching).

Hồi quy là kỹ thuật cho phép ta quay lui trở về bước trước đó nếu như bước hiện hành thất bại để tìm
ra lời giải.

Áp dụng:

Bài toán Cách áp dụng


Bài toán sinh dữ liệu như bài toán tìm các hoán vị, bài toán tìm tập con Vét cạn mọi khả năng
của một tập cho trước.
Tìm lời giải ban đầu cho các bài toán NP-complete như bài toán lập lịch Ngưng ngay sau khi
để sau đó tinh chỉnh lời giải phát hiện một lời giải

Nhận xét:
- Nếu bài toán thực sự không có lời giải thì giải thuật quay lui cũng không tìm thấy lời giải vì đã
vét cạn các khả năng.
- Giải thuật tốt nhưng có thể không hiệu quả vì có thể phải vét cạn mọi khả năng.
 Giải thuật backtrack là giải thuật tìm kiếm hệ thống (systematic searching) và chắc chắn tìm ra lời
giải nếu bài bài toán thực sự có lời giải.
 Một cách tiếp cận khác để giải các bài toán NP-Complete là giải bằng phương pháp kinh nghiệm
và chấp nhận những lời giải cận-tối-ưu . Đây là một nhánh của ngành trí tuệ nhân tạo.

Giải thuật Backtrack cơ bản dạng vét cạn – Quay lui 1 bước

Đầu vào: Tập biến xi, tập miền trị Di, điều kiện C (nếu có).
Đầu ra: Thành công/ thất bại, cấu hình v0, v1, v2, …..,vn-1 với vi thuộc tập Bi
Ý tưởng: Thử gán một trị cho biến thứ i. Nếu gán được thì tiếp tục gán biến thứ i+1. Nếu gán không
được trị cho biến thứ i+1 thì quay lại gán trị khác cho biến thứ i.

success = false;
try ( i, xi, Di, C)
Begin
for v in Di // xét từng trị trong miền trị của biến thứ i
if ( v satifies C when it is assigned to xi ) {
xi = v;
if ( i = n-1 ) { // đã có 1 cấu hình, xử lý cấu hình này
process (x0, x1, x2, …..,xn-1);
success= true;
}
else try ( i+1, xi, Di, C); // đệ quy tìm trị cho biến kế tiếp
}
End

Giải thuật Backtrack dạng ngưng ngay khi phát hiện một lời giải
Đầu vào: Biến chung proceed giúp ngắt tất cả các quá trình gọi đệ quy.
Tập biến xi, tập miền trị Di, điều kiện C (nếu có)
Đầu ra: Thành công/ thất bại, cấu hình v0, v1, v2, …..,vn-1 với vi thuộc tập Bi

proceed= true;
success = false;

try ( i, xi, Di, C)

Begin
for v in Di // xét từng trị trong miền trị của biến thứ i
if (proceed && v satifies C when it is assigned to xi ) {
xi = v;
if ( i = n-1 ) { // đã có 1 cấu hình, xử lý cấu hình này
process (x0, x1, x2, …..,xn-1);
success= true;
proceed= false;
break; // ngắt vòng lặp for
}
else try ( i+1, x0, x1, x2, …..,xn-1 , C); // đệ quy tìm trị cho biến kế tiếp
}
End

Việc quay lui ở chỗ nào trong giải thuật backtrack

Giả sử hàm try (5, xi, Di, C) đang thực thi để gán trị cho biến x5. Giả sử trị v2 trong miền D5 hợp với
x5, hàm try (6, xi, Di, C) được gọi trong hàm try (5, xi, Di, C) để gán trị cho biến x6. Già sử việc gán
trị cho biến x6 thất bại, việc quay lui sẽ quay lại để thử gán trị khác cho x 5. Chú ý rằng hàm try (5, xi,
Di, C) tại lúc này chưa thực thi xong ( xem lại vòng lặp for) nên biến kế tiếp v3 trong miền D5 sẽ được
xem xét ( quay lui về biến x5)

Sử dụng giải thuật Backtrack để giải bài toán

Đầu vào, Tập biến xi, tập miền trị Di, điều kiện C (nếu có)
Đầu ra: Thành công/ thất bại

solveProblem(x0, Di, C)
Begin
success= try ( 0, xi, Di, C, success); // gán biến đi từ biến đầu tiên
if (success==false) message (“Failed”);
End

Minh hoạ

Bái toán: Có n biến mang các trị phân biệt từ 0.. n-1. Hai trị kế nhau phải chệnh lệch trong khoảng d1
đến d2 Viết chương trình xuất các lời giải có thể có. Test với n=10.

Kết quả với n=10, d1= 3, d2=5


Nhận xét

- Nếu tập trị cho các biến chỉ là {0, 1} và không có điều kiện thì ta có lời giải sinh chuỗi bit (bạn tự
code)
- Nếu thay tập trị được xét như sau sẽ giải bài toán sinh tập con k phần tử từ tập cha có n phần tử
{ 0, 1, ,,,, n-1}- Bạn tự code.

Áp dụng Backtracking cho bài toán 8 hậu – 8-queens Problem

Người ta muốn đặt 8 quân hậu trên bàn cờ quốc tế 8x8 sao cho chúng không thể ăn nhau. Chú ý quân
hậu có thể ăn thẳng, ăn ngang, ăn chéo quân đối diện nó ( bàn cờ n ô có thể xếp n quân hậu).

Nhận xét:
- Khi đặt 1 quân vào một vị trí thì không thể đặt một quân khác vào hàng/cột này (luật ăn thẳng).
Như thế nếu xếp các quân theo hàng tăng dần thì không cần kiểm tra xung đột về hàng, chỉ cần
kiểm tra xung đột về cột  Cần mảng các cột columns giúp đánh dấu cột thứ i đang sẵn sáng hay
là đã bị khoá  boolean[] columns, cần mảng int[] rows để chứa vị trí của quân hậu được đặt.
Phần tử row[i]=a mang ý nghĩa quân hậu thứ i được đặt tại vi trí (i,a). Các chỉ số i sẽ đánh chỉ số
hàng từ dưới lên trên, cột từ trái sang phải.
- Khi đặt 1 quân vào một vị trí thì không thể đặt một quân vào một trong hai đường chéo tương
ứng (luật ăn chéo). Bàn cờ có n ô thì có 2n-1 đường chéo trái và 2n-1 đường chéo phải.Nếu
chúng ta lưu thông tin trạng thái của các đường chéo trái, phải sẽ giúp nhanh chóng khoá các
đường chéo.

Khi 1 quân được đặt vào vị trí dòng r cột c thì


leftDiagonals[ r+c] bị khoá ( r=c=0  r+c=0)
rightDiagonals[r-c+n-1] bị khoá (r=c=0, n=8  r-c + n-1=7)
Bài tập.

Hiện thực lại các bài mẫu này.

5- SỬ DỤNG CẤU TRÚC CÂY NHỊ PHÂN


BINARY TREES
Nội dung

- Một số định nghĩa và tính chất liên quan đến cây nhị phân cần nhớ
- Mô tả một phần tử trên cây và mô tả một cây nhị phân
- Khi nào dùng cây nhị phân
- Tại sao lại là cây nhị phân
- Tóm tắt về phép duyệt cây nhị phân
- Minh hoạ cây không thứ tự
- Minh hoạ cây BST-Binary Search Tree
- Minh hoạ dùng cây BST để viết chương trình qảun lý

5.1-Định nghĩa và tính chất

Cây nhị phân: Tập nút trong đó có 1 nút gốc, một nút có tối đa 2 nút con.

Các loại nút: Nút gốc (root), nút cha (father), nút con (child), nút trên (ancestor), nút dưới
(descendant), nút lá (leaf, terminal), nút trung gian/ nút trong (internal)
Đường đi- path: Đường duy nhất từ nút gốc đến nút đang được xét.
Mức (level) của một nút, chiều cao (height) của cây.

Cây có thứ tự- ordered tree: cây chứa tập trị thoả mãn một tiêu chuẩn định trước.
Cây BST: Cây nhị phân có thứ tự với điều kiện: trị nút này sẽ lớn trị nút con trái và nhỏ hơn trị của
nút con phải. Chính nội dung các tác vụ thêm/xoá/ sửa của cây giúp xác định cây này cây thông
dụng/ cây có thứ tự hay không (BST là một cây có thứ tự).

Một số quan hệ trên cây nhị phân.


- Số cạnh = số nút – 1
- Số nút là >= số
- Số nút tại mức L n(L) <= 2L
- Số nút lá của cây có chiều cao H: leaves <= 2H
- Số nút của một cây nhị phân đầy đủ với i nút trung gian: n= 2i+1
- Só nút lá c ủa một cây nhị phân với i nút trung gian: leaves = i+1

5.2-Mô tả một phần tử trên cây và mô tả một cây nhị phân

class BinTreeNode <T> { class BinTree <T> {


T info; BinTreeNode<T> root ;
Node<T> left; …….
Node<T> right; …….
…….. …….

} }

Nhận xét: Info trong nút nên có các đặc điểm:


- Có khả năng so sánh để có thể mở rộng thành nút được dùng cho các tình huống cần sắp xếp ( nút
trong cây BST chẳng hạn)
- Có trị nào đó nhằm phân biệt duy nhất (key) để giúp quá trình tìm kiếm với điều kiện là có tối đa
một kết quả.

5.3-Khi nào dùng cây nhị phân

- Dữ liệu có phân cấp tự nhiên như: danh sách gia phả, tập trạng thái trong các trò chơi.
- Dữ liệu có phân cấp dạng cấu trúc: Dữ liệu được trình bầy trên cửa số trình duyệt ( cây DOM,
Document Object Model)….
- Thường có tác vụ tìm kiếm trên tập dữ liệu.

5.4-Tại sao lại là cây nhị phân

Dĩ nhiên chúng ta có thể dùng cây n-phân. Nếu dùng cây n-phân, tại mỗi nút chúng ta phải lưu trữ
một mảng n tham khảo đến n nút con nhưng không phải lúc nào cũng có đủ n con  phí bộ nhớ.

Chúng ta hoàn toàn có thể dùng cây nhị phân để biểu diễn cây n phân với ý nghĩa của các tham chiếu
như sau:
- Tham chiếu left chỉ đến nút con đầu
- Tham chiếu right chỉ đến nút anh em cùng cha.
 Hai tham chiếu này mang ý nghĩa khác với hai tham chiếu được đề cập trong cây nhị phân ban
đầu  Các thuật toán trên cây phải được viết lại cho phù hợp.

5.5-Tóm tắt về phép duyệt cây- Traversing

Duyệt cây: Quá trình viếng thăm từng nút trong một cây.

Cơ chế duyệt cây:


Cả cây nhị phân chỉ được quản lý bằng nút gốc, mọi con đường đều đi từ nút gốc và theo nguyên tắc
biết nút cha mới biết được nút con nên mọin cách duyệt đều có nguyên tắc chung là:
- Tại một thời điểm chỉ viếng thăm một nút.
- Trật tự các nút sẽ được viếng thăm phải có cách đi đến bằng cách lưu trữ nút sẽ đi đến vào stack
hệ thống( cơ chế hàm đệ quy) hoặc stack/ hoặc queue tự quản lý.

- Các cơ chế thông dụng


Duyệt theo mức  Duyệt theo chiều rộng/ chiều ngang của cây ( Bread-first traversal): Khi
duyệt 1 nút thì cất các nút con vào queue.
Duyệt theo nhánh  Duyệt theo độ sâu, Deep-first traversal  Dùng kỹ thuật đệ quy và có 6
thứ tự duyệt (V: visit, viếng thăm nút hiện hành) VLR, VRL, LVR,
RVL , LRV, RLV – Dùng stack hệ thống (hàm đệ quy) hoặc kỹ thuật lặp
có sự hỗ trợ của stack tự tạo.
Biến thể: Duyêt có xâu kết cả một nhánh ( Threads Tree), Morris traversal

Chính tác vụ duyệt cây là cơ bản nhất để giải hầu hết các tác vụ trên cây dù cây đó là cây
có hoặc không có thứ tự

( chi tiết các phép duyệt này đã được đề cập chi tiết trong sách giáo khoa)

Cần chú ý rằng các phép duyệt cây là chung cho mọi cây kể cả cây BST. BST là một cây đặc
biệt nhằm giải quyết tốt các bài toán quản lý dữ liệu dạng cây có yêu cầu về tìm kiếm sao cho
hiệu quả (O(logn)) nên các tác vụ chèn/ xoá/ tìm kiếm được hiệu chỉnh phù hợp với thứ tự dữ
liệu đã được ấn định trước.

5.6-Minh hoạ về cây không thứ tự

Thí dụ

Thí dụ sau minh hoạ cách dùng pháp duyệt cây để nhập vào 1 cây số nguyên sau đó thực thi các thao
tác như xuất cây (theo bề sâu và bề rộng), tìm chiều cao của cây, tìm kiếm trên cây, in ra các nút
trong một mức. Kết quả của một lần chạy như hình sau:
Chú ý khi nhập dữ liệu vào cây
Ngôn ngữ C++ hỗ trợ tham số dạng tham chiếu nên chúng ta có cơ hội làm biến đổi đối số ngoài đã
truyền cho hàm. Do vậy, chúng ta có thể truyền đối số null từ ngoài hàm để rồi địa chỉ được cấp phát
động bên trong hàm sẽ cập nhật vào đối số null bên ngoài hàm. Trong khi đó, Java chỉ hỗ trợ truyền
tham số cho hàm dạng tham trị nên khi truyền tham số cho hàm là một tham khảo mang trị null thì trị
null này được chép vào tham số. Việc cấp phát động bên trong hàm không truyền lại giá trị địa chỉ
cho đối số ngoài  Đối số bên ngoài vẫn là null. Hình sau minh hoạ việc không thay đổi đối số dạng
tham khảo trong Java.

Xét phương thức input(…) trong Java Và dùng hàm này trong hành vi m() như sau:
void input( Student obj) { void m() {
obj= new Student (“Minh”, 7); Student st1= null; //1
…. input (st1); //2
} …
}

Bộ nhớ stack khi phát biều phương thức m() thực thi như sau:

Stack cho m() st1 = null st1 = null


Stack cho input () obj=null obj=7000

7000 Minh, 7

Trước phát biểu obj= new Student(...) Sau phát biểu obj= new Student(...)

Như vậy, sau khi hành vi input(…) thực thi xong, tham khảo st1 vẫn mang trị null.
Rút kinh nghiệm: Hai cách để nhập

Cách 1: Cấp phát bộ nhớ trước khi nhập trị vào các đối tượng. Nếu việc nhập trị này gây ra dữ liệu
không được chấp nhận thì huỷ đối tượng này đi sau khi nhập.

Cách 2: Hàm nhập trả về trị tham khảo đến đối tượng mới được cấp phát để có thể gán lại cho đối số
ngoài( tham khảo hai hành vi input(…) dưới đây.
Chạy chương trình: Tham khảo kết quả đã được gới thiệu ở trước.

5.7- Minh hoạ về cây BST các số nguyên

Chương trình sau minh hoạ cách sử dụng một cây BST các số nguyên. Ban đầu, khởi tạo một cây
BST cân bằng mang các trị 1..10. Sau đó cho phép user thêm/xoá phần tử, xem cây.

Kết quả một lần chạy chương trình


Các lớp cần xây dựng

Lớp Menu
Lớp Menu: Đã xây dựng từ bài trước
Lớp BSTNode: Mô tả chung cho 1 nút trên cây BST. Vì
data trong nút có thứ tự nên buộc lớp con sau này phải
implements interface Comparable.
Lớp BST: Mô tả cho 1 cây BST có dữ liệu BSTNode
Lớp MyQueue mô tả 1 hàng đợi đã có ở demo trước.
Lớp TestBST: Cây có dữ liệu Integer. Lớp Integer đã
implements interface Comparable rồi.
Chạy chương trình: Tham khảo kết quả đã được gới thiệu ở trước.

5.8- Minh hoạ cách dùng cây BST để viết chương trình quản lý

Phần này minh hoạ cách dùng cây BST để quản lý một danh sách. Tất cả các lớp để trong một
gói phục vụ cho các kỳ kiểm tra.

Đề bài: Viết chương trình quản lý một danh sách các điện thoại di động <(String)mã điện thoại,
(int) giá> có menu đơn giản cho phép người dùng thực hiện các chức năng:
1- Thêm điện thoại
2- Xuất điện thoại theo mã tăng dần
3- Xuất điện thoại trong một tầm giá
4- Thoát

Phân tích
- Chọn loại ứng dụng là console application với menu gồm 4 mục ứng với 4 chức năng
được yêu cầu.

Kiến trúc của ứng dụng


Menu: Lớp mô tả cho menu
PhoneManager: Lớp có hàm main
PhoneNode: Lớp mô tả cho một nút trên cây BST
Phones: Cây BST các phone, khoá là mã điện thoại
Codes
Tạo Bat file

Kết quả của một lần chạy


Bài tập

Sử dụng cây BST dựa trên mã sinh viên (code), viết


chương trình quản lý danh sách học sinh <code,
name, mark> có các tác vụ: thêm/xoá/sửa điểm học
sinh.

Gợi ý: Việc xoá một nút trên cây nhị phân thường
hay cây BST được thực hiện giống nhau. Tham
khảo code cho việc xoá một nút trong cây trong
minh hoạ về cây BST. Bạn chọn mộ tronmg hai
cách xoá: hoa75c xoá bằng phương pháp trộn
hoặc xoá bằng phươg pháp sao chép.
6- HƯỚNG DẪN CÀI ĐẶT ĐỒ THỊ TRONG JAVA
Nội dung
- Đồ thị là gì?
- Mô hình đồ thị được dùng để biểu diễn những bài toán nào?
- Phân loại đồ thị
- Biểu diễn đồ thị
- Cài đặt đồ thị

6.1-Đồ thị là gì?

Đồ thị = Tập đỉnh V + tập cạnh E, một cạnh nối hai đỉnh, cạnh có thể gán một trị số thực
( weight)

6.2-Mô hình đồ thị được dùng để biểu diễn những bài toán nào?

- Các bài toán về giao thông, mạng máy tính, hệ thống bơm, quy trình sản xuất,…

6.3-Phân loại đồ thị

- Đồ thị đơn vô hướng - Simple undirected graph


- Đồ thị đa vô hướng - Multiple undirected graph
- Đồ thị pseude – pseudograph: đa + vòng
- Đồ thị đa có hướng: dircted grah, digraph
- Đồ thị có trọng số - weighted graph: cạnh của đồ thị được gán trọng số

6.4-Biểu diễn đồ thị

Cách 1: Dùng danh sách kề - Adjacency list


Cách 2: Dùng ma trận đỉnh kề - Adjacency matrix (số đỉnh x số đỉnh)
Cách 3: Dùng ma trận cạnh nối - incidence matrix (số đỉnhx số cạnh)

Bài mẫu sau đây sẽ cài đặt đồ thị theo cách 1, qua việc rút kinh nghiệm của bài mẫu, các bạn
có thể cài đặt các ácch 2, 3 cùng các thuật toán đã học)

6.5-Cài đặt đồ thị sử dụng danh sách kề.

Lớp Vertex mô tả cho 1 đỉnh


Lớp Edge mô tả cho một cạnh
Lớp Graph mô tả một đồ thị dạng danh sách kề
Lớp TestGraph1 test lớp Graph với dữ liệu để trong file
graph1.txt
Mô tả đỉnh

Mô tả cạnh
Dòng đầu tiên là các đỉnh
Dòng thứ hai là đỉnh a, còn lại là các đỉnh kề với a
Dòng thứ ba là đỉnh b, còn lại là các đỉnh kề với b
7- CÀI ĐẶT LỚP CHO SORTING
Cài đặt lớp tồng quát cho việc sắp xếp tăng dần một mảng có kiều bất kỳ.

Lớp sorting.MyAscSorting chứa các hành vi static cho việc sắp xếp một mảng được truyền vào
từ tham số.
Lớp TestMyAscSorting giúp test các giải thuật sort. Chương trìnhs ẽ tạo ra một mảng số
nguyên ngẫu nhiên. Từ mảng số nguyên đã có, các giải thuật sắp xếp được dùng có đánh giá hiệu
quả của giải thuật mang tính thực nghiệm bằng cách tính toán thời gian thực thi.
Một kết quả
Bạn hãy hiện thực thêm các giải thuật sắp xếp khác đã học và kiểm tra tính hiệu quả của
chúng.
8- NÉN DỮ LIỆU HUFFMAN
Mục tiêu: Hướng dẫn cách hiện thực cơ chế mã hoá Huffman cơ bản.

8.1- Minh hoạ nén chuỗi bằng Huffman Coding

Minh hoạ sau sẽ tạo một chương trình Java giúp mã hoá và giải mã chuỗi ký tự bằng phương
pháp “pure Huffman” với giao diện người dùng như sau:

Cấu trúc các gói:


Lớp MyHuffmanStringProgram cho chương trình: Giao diện người dùng

You might also like