Tải bản đầy đủ (.docx) (17 trang)

(SKKN 2022) đánh giá độ phức tạp của thuật toán thông qua thuật toán sắp xếp trong bồi dưỡng học sinh giỏi môn tin học tại trường THPT tĩnh gia 2

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (271.51 KB, 17 trang )

1. MỞ ĐẦU
1.1. LÍ DO CHỌN ĐỀ TÀI
Khi giải một bài tốn chúng ta có thể có một số giải thuật khác nhau, vấn
đề là cần phải đánh giá các giải thuật đó để lựa chọn một giải thuật tốt nhất. Với
chương trình được sử dụng nhiều lần thì yêu cầu tiết kiệm thời gian thực hiện
chương trình lại rất quan trọng đặc biệt đối với những chương trình mà khi thực
hiện cần dữ liệu vào lớn do đó giải thuật thực hiện nhanh sẽ được xem xét một
cách kĩ càng. Hay nói cách khác để xác định thuật tốn đó chạy nhanh hay chậm
ta cần đo độ phức tạp của thuật toán. Đây cũng là một trong những nội dung
quan trọng trong việc bồi dưỡng cho học sinh ôn thi đội tuyển Tin học.
Xuất phát từ những lí do trên, từ những kinh nghiệm trong những năm
giảng dạy thực tế vừa qua, tôi đã xây dựng đề tài: “Đánh giá độ phức tạp của
thuật tốn thơng qua thuật tốn sắp xếp trong bồi dưỡng học sinh giỏi môn
Tin học tại trường THPT Tĩnh Gia 2”.
1.2. MỤC ĐÍCH NGHIÊN CỨU
Mục đích quan trọng nhất của đề tài làm thế nào để học sinh nh ận thấy
rằng để giải một bài tốn có nhiều cách, có nhiều thuật tốn. Tuỳ theo từng yêu
cầu của bài toán mà ta chọn thuật toán nào là phù hợp và tối ưu nhất.
1.3. ĐỐI TƯỢNG NGHIÊN CỨU
Học sinh tham gia đội tuyển học sinh giỏi môn Tin học trường THPT Tĩnh
Gia 2.
1.4. PHƯƠNG PHÁP NGHIÊN CỨU
- Phương pháp nghiên cứu lý thuyết: Độ phức tạp của thuật toán.
- Phương pháp thực tiễn: Đánh giá độ phức tạp của thuật toán bằng thuật toán
sắp xếp.
- Phương pháp thực nghiệm thông qua thực tế dạy bồi dưỡng học sinh giỏi môn
Tin học
- Phương pháp thống kê, xử lý số liệu: xử lý kết quả bài kiểm tra của học sinh ở
hai nhóm: đối chứng và thực nghiệm.
2. NỘI DUNG
2.1. CƠ SỞ LÍ LUẬN.


2.1.1. Thời gian thực hiện của chương trình.
Một phương pháp để xác định hiệu quả thời gian thực hiện của một giải
thuật là lập trình nó và đo lường thời gian thực hiện của hoạt động trên một máy
tính xác định đối với tập hợp được chọn lọc các dữ liệu vào. Thời gian thực hiện
khơng chỉ phụ thuộc vào giải thuật mà cịn phụ thuộc vào tập các chỉ thị của máy
tính, chất lượng của máy tính và kĩ xảo của người lập trình. Sự thi hành cũng có
thể điều chỉnh để thực hiện tốt trên tập đặc biệt các dữ liệu vào được chọn. Ðể
vượt qua các trở ngại này, các nhà khoa học máy tính đã chấp nhận tính phức tạp
của thời gian được tiếp cận như một sự đo lường cơ bản sự thực thi của giải
thuật. Thuật ngữ tính hiệu quả sẽ đề cập đến sự đo lường này và đặc biệt đối với
sự phức tạp thời gian trong trường hợp xấu nhất.
1


* Thời gian thực hiện chương trình.
Thời gian thực hiện một chương trình là một hàm của kích thước dữ liệu
vào, ký hiệu T(n) trong đó n là kích thước (độ lớn) của dữ liệu vào. Chương
trình tính tổng của n số có thời gian thực hiện là T(n) = cn trong đó c là một
hằng số. Thời gian thực hiện chương trình là một hàm khơng âm, tức là T(n) ≥ 0
với mọi n ≥ 0.
* Ðơn vị đo thời gian thực hiện.
Ðơn vị của T(n) không phải là đơn vị đo thời gian bình thường như giờ,
phút giây… mà thường được xác định bởi số các lệnh được thực hiện trong một
máy tính lý tưởng. Khi ta nói thời gian thực hiện của một chương trình là T(n) =
Cn thì có nghĩa là chương trình ấy cần Cn chỉ thị thực thi.
* Thời gian thực hiện trong trường hợp xấu nhất.
Nói chung thì thời gian thực hiện chương trình khơng chỉ phụ thuộc vào
kích thước mà cịn phụ thuộc vào tính chất của dữ liệu vào. Nghĩa là dữ liệu vào
có cùng kích thước nhưng thời gian thực hiện chương trình có thể khác nhau.
Chẳng hạn chương trình sắp xếp dãy số nguyên tăng dần, khi ta cho vào dãy có

thứ tự thì thời gian thực hiện khác với khi ta cho vào dãy chưa có thứ tự, hoặc
khi ta cho vào một dãy đã có thứ tự tăng thì thời gian thực hiện cũng khác so với
khi ta cho vào một dãy đã có thứ tự giảm. Vì vậy thường ta coi T(n) là thời gian
thực hiện chương trình trong trường hợp xấu nhất trên dữ liệu vào có kích thước
n, tức là: T(n) là thời gian lớn nhất để thực hiện chương trình đối với mọi dữ liệu
vào có cùng kích thước n.
2.1.2. Tỷ suất gia tăng và độ phức tạp của giải thuật.
* Tỷ suất tăng
Ta nói rằng hàm khơng âm T(n) có tỷ suất tăng f(n) nếu
Ta có thể chứng minh được rằng Cho một hàm khơng âm T(n) bất kỳ, ta ln
tìm được tỷ suất tăng f(n) của nó.
Giả sử
và tổng qt:
.
Ðặt
thì với mọi n ≥1 chúng ta dễ dàng chứng minh được rằng:
, tức là tỷ suất tăng của
.
Tỷ suất tăng của hàm
.
Thực vậy, cho
ta dễ dàng chứng minh rằng
* Khái niệm độ phức tạp của giải thuật
Giả sử ta có hai giải thuật P1 và P2 với thời gian thực hiện tương ứng là:

Giải thuật nào sẽ thực hiện nhanh hơn? Câu trả lời phụ thuộc vào kích thước dữ
liệu vào. Với n < 20 thì P2 sẽ nhanh hơn P1 (T2hơn hệ số của 100n^2 (5<100). Nhưng khi n > 20 thì ngược lại do số mũ của
100n^2 nhỏ hơn số mũ của 5n^3 (2<3). Ở đây chúng ta chỉ nên quan tâm đến
2



trường hợp n>20 vì khi n<20 thì thời gian thực hiện của cả P1 và P2 đều không
lớn và sự khác biệt giữa T1 và T2 là không đáng kể.
Như vậy một cách hợp lý là ta xét tỷ suất tăng của hàm thời gian thực hiện
chương trình thay vì xét chính bản thân thời gian thực hiện.
Cho một hàm T(n), T(n) gọi là độ phức tạp f(n)
nếu
(tức là T(n) có tỷ suất gia tăng là f(n)) và
ký hiệu là O(f(n)). (“ơ của f(n)”)
có tỷ suất tăng là n^2 nên
Chú ý: O(C.f(n))=O(f(n)) với C là hằng số. Ðặc biệt O(C) = O(1)
Nói cách khác độ phức tạp tính tốn của giải thuật là một hàm chặn trên của hàm
thời gian. Vì hằng nhân tử C trong hàm chặn trên khơng có ý nghĩa nên ta có thể
bỏ qua vì vậy hàm thể hiện độ phức tạp có các dạng thường gặp sau:
.
Ba hàm cuối cùng ta gọi là dạng hàm mũ, các hàm khác gọi là hàm đa
thức. Một giải thuật mà thời gian thực hiện có độ phức tạp là một hàm đa thức
thì chấp nhận được tức là có thể cài đặt để thực hiện, cịn các giải thuật có độ
phức tạp hàm mũ thì phải tìm cách cải tiến giải thuật.
Chú ý: trong logn ta không cần quan tâm cơ số của nó

(Quy tắc bỏ hệ số sẽ đề cập sau)
Khi nói đến độ phức tạp của giải thuật là ta muốn nói đến hiệu quả của
thời gian thực hiện của chương trình nên ta có thể xem việc xác định thời gian
thực hiên của chương trình chính là xác định độ phức tạp của giải thuật.
2.1.3. Các tính độ phức tạp của giải thuật.
Cách tính độ phức tạp của một giải thuật bất kỳ là một vấn đề khơng đơn
giản. Tuy nhiên ta có thể tn theo một số nguyên tắc sau:
* Quy tắc bỏ hằng số

Nếu đoạn chương trình P có thời gian thực hiện T(n) = O(c1.f(n)) với c1 là một
hằng số dương thì có thể coi đoạn chương trình đó có độ phức tạp tính tốn là
O(f(n)).
* Quy tắc cộng – lấy max
Nếu T1(n) và T2(n) là thời gian thực hiện của hai đoạn chương trình P1 và P2;
và T1(n)=O(f(n)), T2(n)=O(g(n)) thì thời gian thực hiện của đoạn hai chương
trình đó nối tiếp nhau là T(n)=O(max(f(n),g(n)))
VD: Lệnh gán x:=15 tốn một hằng thời gian hay O(1), Lệnh đọc dữ liệu
readln(x) tốn một hằng thời gian hay O(1).
Vậy thời gian thực hiện cả hai lệnh trên nối tiếp nhau là
O(max(1,1))=O(1)
* Quy tắc nhân
Nếu T1(n) và T2(n) là thời gian thực hiện của hai đoạn chương trình P1và P2 và
T1(n) = O(f(n)), T2(n) = O(g(n)) thì thời gian thực hiện của đoạn hai đoạn
chương trình đó lồng nhau là T(n) = O(f(n).g(n))
* Quy tắc tổng qt để phân tích một chương trình
Thời gian thực hiện của mỗi lệnh gán, READ, WRITE là O(1).
3


Thời gian thực hiện của một chuỗi tuần tự các lệnh được xác định bằng
qui tắc cộng. Như vậy thời gian này là thời gian thi hành một lệnh nào đó lâu
nhất trong chuỗi lệnh.
Thời gian thực hiện cấu trúc IF là thời gian lớn nhất thực hiện lệnh sau
THEN hoặc sau ELSE và thời gian kiểm tra điều kiện. Thường thời gian kiểm
tra điều kiện là O(1).
Thời gian thực hiện vòng lặp là tổng (trên tất cả các lần lặp) thời gian thực
hiện thân vòng lặp. Nếu thời gian thực hiện thân vịng lặp khơng đổi thì thời
gian thực hiện vịng lặp là tích của số lần lặp với thời gian thực hiện thân vịng
lặp.

VD: Tính thời gian thực hiện của thủ tục sắp xếp “nổi bọt”
procedure Bubble (var a: array[1..n] of integer);
var i,j,temp: integer;
begin
for i:=1 to n-1 do
{1}
for j:=n downto i+1 do
{2}
if a[j-1]>a[j] then
{3}
begin
temp:=a[j-1];
{4}
a[j-1]:=a[j];
{5}
a[j]:=temp;
{6}
end;
end;
Ta thấy toàn bộ chương trình chỉ gồm một lệnh lặp {1}, lồng trong lệnh
{1} là lệnh {2}, lồng trong lệnh {2} là lệnh {3} và lồng trong lệnh {3} là 3 lệnh
nối tiếp nhau {4}, {5} và {6}. Chúng ta sẽ tiến hành tính độ phức tạp theo thứ tự
từ trong ra.
Trước hết, cả ba lệnh gán {4}, {5} và {6} đều tốn O(1) thời gian, việc so
sánh a[j-1] > a[j] cũng tốn O(1) thời gian, do đó lệnh {3} tốn O(1) thời gian.
Vịng lặp {2} thực hiện (n-i) lần, mỗi lần O(1) do đó vịng lặp {2} tốn O((n-i).1)
= O(n-i).Vịng lặp {1} lặp có i chạy từ 1 đến n-1 nên thời gian thực hiện của
vòng lặp {1} và cũng là độ phức tạp của giải thuật là:
Chú ý: Trong trường hợp vòng lặp khơng xác định được số lần lặp thì chúng ta
phải lấy số lần lặp trong trường hợp xấu nhất.

* Ðộ phức tạp của chương trình có gọi chương trình con khơng đệ quy
Nếu chúng ta có một chương trình với các chương trình con khơng đệ quy, để
tính thời gian thực hiện của chương trình, trước hết chúng ta tính thời gian thực
hiện của các chương trình con khơng gọi các chương trình con khác. Sau đó
chúng ta tính thời gian thực hiện của các chương trình con chỉ gọi các chương
trình con mà thời gian thực hiện của chúng đã được tính. Chúng ta tiếp tục q
trình đánh giá thời gian thực hiện của mỗi chương trình con sau khi thời gian
4


thực hiện của tất cả các chương trình con mà nó gọi đã được đánh giá. Cuối
cùng ta tính thời gian cho chương trình chính.
Giả sử ta có một hệ thống các chương trình gọi nhau theo sơ đồ sau:

Chương trình A gọi hai chương trình con là B và C, chương trình B gọi hai
chương trình con là B1 và B2, chương trình B1 gọi hai chương trình con là B11
và B12.
Ðể tính thời gian thực hiện của A, ta tính theo các bước sau:
1. Tính thời gian thực hiện của C, B2, B11 và B12. Vì các chương trình con này
khơng gọi chương trình con nào cả.
2. Tính thời gian thực hiện của B1. Vì B1 gọi B11 và B12 mà thời gian thực hiện
của B11 và B12 đã được tính ở bước 1.
3. Tính thời gian thực hiện của B. Vì B gọi B1 và B2 mà thời gian thực hiện của
B1 đã được tính ở bước 2 và thời gian thực hiện của B2 đã được tính ở bước 1.
4. Tính thời gian thực hiện của A. Vì A gọi B và C mà thời gian thực hiện của B
đã được tính ở bước 3 và thời gian thực hiện của C đã được tính ở bước 1.
Chúng ta xét thủ tục MergeSort một cách phác thảo như sau:
FUNCTION MergeSort (L:List; n:Integer):List;
VAR L1,L2:List;
BEGIN

IF n=1 THEN RETURN(L)
ELSE
BEGIN
Chia đôi L thành L1 và L2, với độ dài n/2;
RETURN(Merge(MergeSort(L1,n/2),MergeSort(L2,n/2)));
END;
END;
Chẳng hạn để sắp xếp danh sách L gồm 8 phần tử 7, 4, 8, 9, 3, 1, 6, 2 ta có mơ
hình minh họa của MergeSort như sau:

5


Hàm MergeSort nhận một danh sách có độ dài n và trả về một danh sách đã
được sắp xếp. Thủ tục Merge nhận hai danh sách đã được sắp L1 và L2 mỗi
danh sách có độ dài n/2 trộn chúng lại với nhau để được một danh sách gồm n
phần tử có thứ tự. Giải thuật chi tiết của Merge ta sẽ bàn sau, chúng ta chỉ để ý
rằng thời gian để Merge các danh sách có độ dài n/2 là O(n). Gọi T(n) là thời
gian thực hiện MergeSort một danh sách n phần tử thì T(n/2) là thời gian thực
hiện MergeSort một danh sách n/2 phần tử. Khi L có độ dài 1 (n = 1) thì chương
trình chỉ làm một việc duy nhất là return(L), việc này tốn O(1) = C1 thời gian.
Trong trường hợp n > 1, chương trình phải thực hiện gọi đệ quy MerSort hai lần
cho L1 và L2 với độ dài n/2 do đó thời gian để gọi hai lần đệ quy này là 2T(n/2).
Ngồi ra cịn phải tốn thời gian cho việc chia danh sách L thành hai nửa bằng
nhau và trộn hai danh sách kết quả (Merge). Người ta xác đinh được thời gian để
chia danh sách và Merge là O(n) = C2n. Vậy ta có phương trình đệ quy như sau:

Bảng sau đây cho ta các cấp thời gian thực hiện thuật toán được sử dụng rộng
rãi nhất và tên gọi của chúng .


6


Ký hiệu O(f(x)) của f(x)
O(1)
O(log)
O(n)
O(nlogn)
O(n2)
O(n3)
O(2n)
O(n!)

Độ phức tạp loại
Hằng
Logarit
Tuyến tính
n log n
Bình phương
Lập phương

Giai thừa

2.2. THỰC TRẠNG CỦA VẤN ĐỀ
Hầu hết các bài tốn đều có nhiều thuật tốn khác nhau để giải quyết
chúng. Như vậy, làm thế nào để chọn được sự cài đặt tốt nhất? Đây là một lĩnh
vực được phát triển tốt trong nghiên cứu về khoa học máy tính. Chúng ta sẽ
thường xun có cơ hội tiếp xúc với các kết quả nghiên cứu mô tả các tính năng
của các thuật tốn cơ bản. Tuy nhiên, việc so sánh các thuật toán rất cần thiết và
chắc chắn rằng một vài dòng hướng dẫn tổng quát về phân tích thuật tốn sẽ rất

hữu dụng.
Khi nói đến hiệu quả của một thuật toán, người ta thường quan tâm đến
chi phí cần dùng để thực hiện nó. Chi phí này thể hiện qua việc sử dụng tài
nguyên như bộ nhớ, thời gian sử dụng CPU, … Ta có thể đánh giá thuật tốn
bằng phương pháp thực nghiệm thơng qua việc cài đặt thuật toán rồi chọn các bộ
dữ liệu thử nghiệm. Thống kê các thông số nhận được khi chạy các dữ liệu này
ta sẽ có một đánh giá về thuật toán.
Tuy nhiên, phương pháp thực nghiệm gặp một số nhược điểm sau khiến
cho nó khó có khả năng áp dụng trên thực tế:
- Do phải cài đặt bằng một ngơn ngữ lập trình cụ thể nên thuật tốn sẽ chịu sự
hạn chế của ngữ lập trình này.
- Đồng thời, hiệu quả của thuật toán sẽ bị ảnh hưởng bởi trình độ của người cài
đặt.
- Việc chọn được các bộ dữ liệu thử đặc trưng cho tất cả tập các dữ liệu vào của
thuật tốn là rất khó khăn và tốn nhiều chi phí.
- Các số liệu thu nhận được phụ thuộc nhiều vào phần cứng mà thuật toán được
thử nghiệm trên đó. Điều này khiến cho việc so sánh các thuật tốn khó khăn
nếu chúng được thử nghiệm ở những nơi khác nhau.
Vì những lý do trên, việc tìm kiếm những phương pháp đánh giá thuật tốn
hình thức hơn, ít phụ thuộc môi trường cũng như phần cứng hơn là rất quan
trọng.
2.3. ĐÁNH GIÁ ĐỘ PHỨC TẠP CỦA CÁC THUẬT TOÁN SẮP XẾP
2.3.1. THUẬT TOÁN SẮP XẾP NỔI BỌT
1. Ý trưởng
Ý trưởng của phương pháp sắp xếp này: 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
7


đứ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.
2. Chương trình
#include <bits/stdc++.h>
using namespace std;
long a[10000000], i, j, n,tam;
void swap(int &a, int &b)
{
int x = a;
a = b;
b = x;
}int main()
{
freopen ("SAPXEP.INP", "r", stdin);
freopen ("SAPXEP.OUT", "w", stdout);
ios_base::sync_with_stdio(false);
cin >> n;
for (i=1; i <= n; i++)
cin >> a[i];
for (j=n; j >= 2; j--)
for (i=1; i <= j-1; i++)
if (a[i]>a[i+1])
{
swap(a[i],a[i+1]);
}
for (i=1; i <= n; i++)
cout << (a[i])<<" ";
return 0;
}
3. Độ phức tạp
Độ phức tạp thuật toán

 Trường hợp tốt: O(n)
 Trung bình: O(n2 )
 Trường hợp xấu: O(n2 )
2.3.2. THUẬT TỐN SẮP XẾP CHÈN
1. Ý trưởng
Thuật tốn sắp xếp chèn thực hiện sắp xếp dãy số theo cách duyệt từng
phần tử và chèn từng phần tử đó vào đúng vị trí trong mảng con(dãy số từ đầu
đến phần tử phía trước nó) đã sắp xếp sao cho dãy số trong mảng sắp đã xếp đó
vẫn đảm bảo tính chất của một dãy số tăng dần.
2. Chương trình
#include<bits/stdc++.h>
int a[10000000], i, j, n;
8


using namespace std;
// doi cho 2 so
void Swap(int &a,int &b) {
int temp = a;
a = b;
b = temp;
}
void insertionSort(int a[], int n)
{
int i, key, j;
for (i = 1; i < n; i++)
{
key = a[i];
j = i-1;
/* Di chuyển các phần tử có giá trị lớn hơn giá trị

key về sau một vị trí so với vị trí ban đầu
của nó */
while (j >= 0 && a[j] > key)
{
a[j+1] = a[j];
j = j-1;
}
a[j+1] = key;
}
}
// ham main
int main()
{
freopen("SAPXEP.INP", "r", stdin);
freopen("SAPXEP.OUT", "w", stdout );
cin>>n;
for (int i=0; i <= n-1; i++)
cin >> a[i];
insertionSort(a,n);
for (int i=0; i <= n-1; i++)
cout << (a[i])<<" ";
}
3. Độ phức tạp
Độ phức tạp thuật toán
 Trường hợp tốt: O(n)
 Trung bình: O(n2 )
 Trường hợp xấu: O(n2 )
2.3.3. THUẬT TOÁN SẮP XẾP CHỌN
9



1. Ý trưởng
Thuật toán sắp xếp chọn sẽ sắp xếp một mảng bằng cách đi tìm phần tử có giá trị
nhỏ nhất(giả sử với sắp xếp mảng tăng dần) trong đoạn đoạn chưa được sắp xếp
và đổi cho phần tử nhỏ nhất đó với phần tử ở đầu đoạn chưa được sắp
xếp(khơng phải đầu mảng). Thuật tốn sẽ chia mảng làm 2 mảng con
2. Chương trình
#include<bits/stdc++.h>
int a[10000000], i, n;
using namespace std;
// doi cho 2 so
void Swap(int &a,int &b) {
int temp = a;
a = b;
b = temp;
}
// Hàm selection sort
void selectionSort(int a[], int n)
{
int i, j, min_idx;
// Di chuyển ranh giới của mảng đã sắp xếp và chưa sắp xếp
for (i = 0; i < n-1; i++)
{
// Tìm phần tử nhỏ nhất trong mảng chưa sắp xếp
min_idx = i;
for (j = i+1; j < n; j++)
if (a[j] < a[min_idx])
min_idx = j;
// Đổi chỗ phần tử nhỏ nhất với phần tử đầu tiên
swap(a[min_idx], a[i]);

}
}
// ham main
int main()
{
freopen("SAPXEP.INP", "r", stdin);
freopen("SAPXEP.OUT", "w", stdout );
cin>>n;
for (int i=0; i <= n-1; i++)
cin >> a[i];
selectionSort(a,n);
for (int i=0; i <= n-1; i++)
cout << (a[i])<<" ";
10


}
3. Độ phức tạp
Độ phức tạp thuật toán
 Trường hợp tốt: O(n2 )
 Trung bình: O(n2 )
 Trường hợp xấu: O(n2 )
2.3.4. THUẬT TOÁN SẮP XẾP NHANH
1. Ý trưởng
Giống như Merge sort, thuật toán sắp xếp quick sort là một thuật tốn chia
để trị. 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: Chọn một phần tử có giá trị nằm giữa mảng(median

element).
Tầm quan trọng của phân đoạn trong thuật toán quick sort. Mấu chốt chính của
thuật tốn quick sort 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ố A. 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à A[left] > pivot và
A[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)
2. Chương trình
#include <bits/stdc++.h>
using namespace std;
long a[10000000], i, j, n;
int main()
{
freopen ("SAPXEP.INP", "r", stdin);
freopen ("SAPXEP.OUT", "w", stdout);
ios_base::sync_with_stdio(false);
cin >> n;
for (i=1; i <= n; i++)
cin >> a[i];
11



sort (a+1, a + 1 +n);
for (i=1; i <= n; i++)
cout << (a[i])<<" ";
return 0;
}
3. Độ phức tạp
Độ phức tạp thuật toán của quick sort
 Trường hợp tốt: O(nlog(n))
 Trung bình: O(nlog(n))
 Trường hợp xấu: O(n2 )
2.3.5. THUẬT TỐN SẮP XẾP VUN ĐỐNG
1. Ý trưởng
Sắp xếp vun đống (Heap Sort) là một kỹ thuật sắp xếp phân loại dựa trên một
cấu trúc dữ liệu được gọi là đống nhị phân (binary heap), gọi đơn giản là đống.
Nó tương tự như thuật toán Sắp xếp chọn (Selection Sort) nơi phần tử lớn nhất
sẽ được xếp vào cuối danh sách. Lặp đi lặp lại các bước này cho các phần tử còn
lại của danh sách.
Khái niệm đống nhị phân (binary heap)
Mỗi mảng a[1..n] có thể xem như một cây nhị phân gần đầy (có trọng số là các
giá trị của mảng), với gốc ở phần tử thứ nhất, con bên trái của
đỉnh a[i] là a[2*i] con bên phải là a[2*i+1] (nếu mảng bắt đầu từ 1 còn nếu
mảng bắt đầu từ 0 thì hai nhánh con là a[2*i+1] và a[2*i+2]) (nếu 2*i ≤
n hoặc 2*i+1 ≤ n, khi đó các phần tử có chỉ số lớn hơn (int) (n/2) khơng có con,
do đó là lá).
- Một cây nhị phân, được gọi là đống cực đại nếu khóa của mọi nút khơng nhỏ
hơn khóa các con của nó. Khi biểu diễn một mảng a[ ] bởi một cây nhi phân
theo thứ tự tự nhiên điều đó nghĩa là a[i] ≥ a[2*i] và a[i] ≥ a[2*i+1] với mọi i
=1..int(n/2). Ta cũng sẽ gọi mảng như vậy là đống. Như vậy trong

đống a[1] (ứng với gốc của cây) là phần tử lớn nhất. Mảng bất kỳ chỉ có một
phần tử ln ln là một đống.
- Một đống cực tiểu được định nghĩa theo các bất đẳng thức ngược lại: a[i] ≤
a[2*i] và a[i] ≤ a[2*i+1]. Phần tử đứng ở gốc cây cực tiểu là phần tử nhỏ nhất.
Kỹ thuật vun đống
Việc sắp xếp lại các phần tử của một mảng ban đầu sao cho nó trở thành đống
được gọi là vun đống.
Vun đống tại đỉnh thứ i
Nếu hai cây con gốc 2*i và 2*i+1 đã là đống thì để cây con gốc i trở thành đống
chỉ việc so sánh giá trị a[i] với giá trị lớn hơn trong hai giá
trị a[2*i] và a[2*i+1], nếu a[i] nhỏ hơn thì đổi chỗ chúng cho nhau. Nếu đổi chỗ
cho a[2*i], tiếp tục so sánh với con lớn hơn trong hai con của nó cho đên khi
hoặc gặp đỉnh lá.
12


Vun một mảng thành đống
Để vun mảng a[1..n] thành đống ta vun từ dưới lên, bắt đầu từ phần tử a[j] với j
= Int (n/2) ngược lên tới a[1].
Sắp xếp bằng vun đống
- Đổi chỗ (Swap): Sau khi mảng a[1..n] đã là đống, lấy phần tử a[1] trên đỉnh
của đống ra khỏi đống đặt vào vị trí cuối cùng n, và chuyển phần tử thứ cuối
cùng a[n] lên đỉnh đống thì phần tử a[n] đã được đứng đúng vị trí.
- Vun lại: Phần còn lại của mảng a[1..n-1] chỉ khác cấu trúc đống ở phần tử a[1].
Vun lại mảng này thành đống với n-1 phần tử.
- Lặp: Tiếp tục với mảng a[1..n-1]. Q trình dừng lại khi đống chỉ cịn lại một
phần tử.
2. Chương trình
#include<bits/stdc++.h>
int a[10000000], i, j, n;

using namespace std;
// doi cho 2 so
void Swap(int &a,int &b) {
int temp = a;
a = b;
b = temp;
}
void Heapify(int A[],int n, int i) {
int Left = 2*(i+1)-1;
int Right = 2*(i+1);
int Largest;
if(Left<n && A[Left]>A[i])
Largest = Left;
else
Largest = i;
if(Right<n && A[Right]>A[Largest])
Largest = Right;
if(i!=Largest) {
Swap(A[i],A[Largest]);
Heapify(A,n,Largest);
}
}
void BuildHeap(int A[], int n) {
for(int i = n/2-1; i>=0; i--)
Heapify(A,n,i);
}
// heap-sort
void HeapSort(int A[], int n) {
13



BuildHeap(A,n);
for(int i = n-1; i>0; i--){
Swap(A[0],A[i]);
Heapify(A,i,0);
}
}
int main()
{
freopen("SAPXEP.INP", "r", stdin);
freopen("SAPXEP.OUT", "w", stdout );
cin>>n;
for (int i=0; i <= n-1; i++)
cin >> a[i];
HeapSort(a,n);
for (int i=0; i <= n-1; i++)
cout << (a[i])<<" ";
}
3. Độ phức tạp
Độ phức tạp thuật toán của quick sort
 Trường hợp tốt: O(nlog(n))
 Trung bình: O(nlog(n))
 Trường hợp xấu: O(nlog(n))
2.4. KẾT QUẢ THỰC HIỆN
Sử dụng 10 Test với số lượng phần tử tăngđần từ 10 đến 5000000 phần tử. Sử
dụng phần mềm Themis để kiểm tra thời gian chạy của các thuật tốn trên với
quy ước mỗi test chạy khơng quá 1s (1 giây). Kết quả như sau:
SX
SL
QUICKSO HEADSO

TES Phần
CHÈN
CHỌN
NOIBOT
RT
RT
T
tử
Test0
0.0300190 0.029138 0.0264705 0.03117351 0.0271934
10
1
6
62
0
0
29
Test0
0.0286986 0.028641 0.0273719 0.02545172 0.0283188
100
2
2
05
2
5
65
Test0
0.0248733 0.031702 0.0319160 0.02582077 0.0315135
1000
3

8
76
6
2
51
Test0
0.0368939 0.044406 0.0609962 0.02952239 0.0306862
5000
4
5
08
3
5
08
Test0
0.0535372 0.084300 0.1878804 0.03176478 0.0390114
10000
5
0
16
54
8
01
Test0
0.4880260 Chạy quá Chạy quá 0.04794616 0.0787814
50000
6
64
thời gian thời gian
8

23
Test0 10000 Chạy quá Chạy quá Chạy quá 0.07442515 0.1184108
14


7
Test0
8
Test0
9
Test1
0

0
thời gian
thời gian thời gian
5
77
50000 Chạy quá Chạy quá Chạy quá 0.22669996 0.4984356
0
thời gian
thời gian thời gian
9
79
10000 Chạy quá Chạy quá Chạy quá 0.43273644 Chạy quá
00
thời gian
thời gian thời gian
8
thời gian

50000 Chạy quá Chạy quá Chạy quá Chạy
quá Chạy quá
00
thời gian
thời gian thời gian
thời gian
thời gian
Phân tích bảng trên ta có thể thấy rằng đối với dữ liệu vào nhỏ thì sử dụng
thuật tốn nào cũng cho kết quả với thời gian thực hiên tương đương nhau.
Nhưng đối với bộ dữ liệu vào lớn thì các thuật tốn sẽ cho kết quả khác nhau rất
nhiều vạ phụ thuộc vào độ phức tạp của thuật toán. Nếu độ phức tạp càng lớn thì
thời gian chạy càng lớn và ngược lại.
Đối với những bài toán liên quan đến thuật toán sắp xếp. Đặc biệt là khi học sinh
biết sử dụng phần mền Themis để Test dữ liệu.
Thuật
Số
lượng Thuật toán
STT
Tỉ lệ
toán SX Tỉ lệ
HS
SX nổi bọt
nhanh
Năm 2020 - 2021
Trước khi
áp dụng đề 5
4
80 %
1
20 %

tài
Sau khi áp
5
0
0
5
100 %
dụng đề tài
Năm 2021 - 2022
Trước khi
áp dụng đề 3
2
67 %
1
33 %
tài
Sau khi áp
3
0
0
3
100 %
dụng đề tài
3. KẾT LUẬN VÀ KIẾN NGHỊ
3.1. KẾT LUẬN
Đối với các chương trình chỉ dùng 1 vài lần thì yêu cầu giải thuật đơn
giản sẽ được ưu tiên vì chúng ta cần 1 giải thuật dễ hiểu, dễ cài đặt, ở đây khơng
đề cao vấn đề thời gian chạy vì chúng ta chỉ chạy một vài lần (ví dụ: Thuật tốn
nổi bọt).
Tuy nhiên, khi 1 chương trình sử dụng nhiều lần, yêu cầu tiết kiệm thời

gian sẽ được đặc biệt ưu tiên. Tuy nhiên, thời gian thực hiện chương trình lại
phụ thuộc vào rất nhiều yếu tố như: cấu hình máy tính, ngơn ngữ sử dụng, trình
biên dịch, dữ liệu đầu vào, … Do đó ta khi so sánh 2 giải thuật đã được thực
hiện, chưa chắc chương trình chạy nhanh hơn đã có giải thuật tốt hơn. “Độ phức
tạp của thuật toán” sinh ra để giải quyết vấn đề này.
15


Qua việc phân tích độ phức tạp thuật tốn và cách tính tốn độ phức tạp
của giải thuật ta sẽ tối ưu thuật toán để đáp ứng thời gian chạy tốt hơn. Tuỳ vào
từng đối tượng học sinh, tuỳ vào từng quy định cụ thể của bài toán mà định
hướng học sinh chọn một thuật tốn cụ thể nào đó phù hợp với bài toán.
Thường căn cứ vào Max dữ liệu vào để chọn thuật toán phù hợp.
3.2. KIẾN NGHỊ
Đề nghị thư viện nhà trường bổ sung thêm nhiều sách về bồi dưỡng học
sinh giỏi nói chung và mơn tin học nói riêng.
Đề nghị Ban giám hiệu quan tâm nâng cấp phịng máy vi tính để cơng tác
dạy thực hành môn Tin học đạt hiệu quả tốt nhất.
XÁC NHẬN CỦA
Thanh Hóa, ngày 10 tháng 5 năm 2022
THỦ TRƯỞNG CƠ QUAN Tơi xin cam đoan đây là SKKN của mình viết,
khơng sao chép nội dung của người khác.

Lê Văn Vinh

16


TÀI LIỆU THAM KHẢO.
1. Lê Minh Hoàng (2002). Giải thuật và lập trình, NXB Đại học Sư Phạm Hà

Nội.
2. Đỗ Xn Lơi (2006). Giáo trình cấu trúc dữ liệu và giải thuật, NXB Đại học
quốc gia Hà Nội.
3.
4.

17



×