Tải bản đầy đủ (.doc) (39 trang)

SKKN phân tích, vận dụng các thuật toán sắp xếp để giải quyết một số bài toán viết bằng NNLT c++

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 (158.31 KB, 39 trang )

SỞ GIÁO DỤC VÀ ĐÀO TẠO NGHỆ AN

SÁNG KIẾN KINH NGHIỆM

ĐỀ TÀI

PHÂN TÍCH, VẬN DỤNG CÁC THUẬT TỐN SẮP XẾP ĐỂ
GIẢI QUYẾT MỘT SỐ BÀI TOÁN VIẾT BẰNG NNLT C++
SKKN thuộc lĩnh vực: Tin học

Nghệ An năm 2021


1. MỞ ĐẦU
1.1. Lý do chọn đề tài
Chúng ta biết rằng để có kết quả cao trong kì thi tuyển chọn học sinh giỏi
mơn tin học nói chung thì phải có vốn kiến thức về thuật tốn để giải được các
bài tốn khó, hoặc những bài tốn dữ liệu vào lớn, sau đó học sinh lựa chọn
NNLT để lập trình dựa vào thuật tốn đã tìm được và giải bài tốn theo u cầu.
Trong q trình dạy bời dưỡng học sinh giỏi tơi gặp rất nhiều bài tốn có sử
dụng giải thuật sắp xếp. Đây là dạng bài tập không khó nhưng học sinh thường
hay chủ quan nên khơng chạy được hết các test lớn.
Nguyên nhân cơ bản là học sinh khơng áp dụng phương pháp phù hợp cho
bài tốn đó.
Với mong muốn giúp học sinh giải quyết tốt hơn các bài tập sắp xếp dãy số,
tôi đã dày công nghiên cứu các thuật tốn sắp xếp và phân tích để áp dụng vào
từng dạng toán phù hợp.
Mặt khác, theo chương trình mới của Bộ Giáo Dục khuyến khích giáo viên
dạy NNLT mới thay Pascal nên tơi viết chương trình bằng NNLT C++ để làm tài
liệu tham khảo mới cho giáo viên và học sinh.
Từ những lý do trên tôi đã mạnh dạn trình bày sáng kiến kinh nghiệm:


“Phân tích, vận dụng các thuật toán sắp xếp để giải quyết một số bài tốn
viết bằng NNLT C++”.
1.2. Mục đích nghiên cứu
Mục đích chính của sáng kiến là nghiên cứu, phân tích và vận dụng các
thuật tốn sắp xếp dành cho đối tượng HSG khối THPT. Việc lĩnh hội được sáng
kiến sẽ giúp học sinh:
- Mô tả đúng khái niệm, bản chất và mục đích của việc sắp xếp
- Trình bày và thực hiện cài đặt một cách chính xác các thuật toán sắp xếp.
- Đánh giá đúng về các thuật tốn sắp xếp và tìm ra thuật tốn sắp xếp phù
hợp cho từng bài toán.
- Giúp các em học giỏi môn Tin Học đạt kết quả cao
- Tạo ra nguồn tài liệu tham khảo về thuật toán hỗ trợ cho học sinh, giáo
viên dạy Tin học bậc THPT
- Sử dụng NNLT C++ trong chương trình giáo dục phổ thơng mới.

2


1.3. Đối tượng nghiên cứu
Sáng kiến kinh nghiệm có đối tượng nghiên cứu là các thuật toán sắp xếp
và viết một số chương trình có sử dụng các thuật tốn sắp xếp bằng NNLT C++.
1.4. Phương pháp nghiên cứu
Để trình bày sáng kiến kinh nghiệm này, chúng tôi đã sử dụng phối kết hợp
nhiều phương pháp như: nghiên cứu tài liệu, thuyết trình, quan sát, điều tra cơ
bản, thực nghiệm so sánh, phân tích kết quả thực nghiệm, vận dụng… phù hợp
với môn học thuộc lĩnh vực Tin học.

3



2. NỘI DUNG NGHIÊN CỨU
2.1. Cơ sở lý luận
Nếu học sinh thực hiện tốt việc lựa chọn và cài đặt chương trình tối ưu khi
giải các bài tốn sắp xếp nói riêng và các bài tập lập trình nói chung thì chất
lượng học sinh giỏi sẽ được nâng cao.
Học sinh dần được làm quen với NNLT C++.
2.2. Thực trạng
2.2.1 Thực trạng trước khi nghiên cứu
Đối với thi học sinh giỏi, dù kết quả output của 2 thí sinh có giống hệt nhau
với cùng một bộ input, nhưng việc chênh lệch về thời gian quyết định thí sinh có
thể chiến thắng hay thất bại (yêu cầu thời gian xử lí chương trình khơng q 1
giây/1 test). Lý do khi vận dụng các thuật tốn thì thường học sinh chỉ chọn
những thuật tốn quen thuộc mà ít khi phân tích bài tốn để tìm ra thuật tốn phù
hợp hơn.
Vấn đề đặt ra, là làm thế nào để lấy được điểm với các bộ input có dữ liệu
lớn. Muốn vậy cần phải lựa chọn và cài đặt được chương trình hiệu quả (tối ưu).
Chương trình hiệu quả là chương trình giải quyết được những bộ input có dữ
liệu lớn, chính xác, dung lượng sử dụng bộ nhớ nhỏ, thời gian thực chương trình
ngắn,...
2.3. Các biện pháp sử dụng để giải quyết vấn đề
2.3.1. Giới thiệu về bài toán sắp xếp
2.3.1.1. Khái niệm
Sắp xếp là quá trình xử lý một danh sách các phần tử (hoặc các mẫu tin)
để đặt chúng theo một thứ tự, thỏa mãn một tiêu chuẩn nào đó dựa trên nội dung
thơng tin lưu trữ tại mỗi phần tử.
Ví dụ: cho trước một dãy số a1, a2,..., an được lưu trữ trong cấu trúc dữ liệu
mảng.
Sắp xếp dãy số a1, a2,..., an là thực hiện việc bố trí lại các phần tử sao cho được
dãy mới có thứ tự tăng dần (hoặc giảm dần).
Hai thao tác so sánh và gán là các thao tác cơ bản của hầu hết các thuật

toán sắp xếp. Bài toán sắp xếp xuất hiện trong bất kỳ lĩnh vực nào của tin học.
Khi xây dựng một bài tốn sắp xếp, cần tìm cách giảm thiểu những phép
so sánh và đổi chỗ không cần thiết để tăng hiệu quả của thuật tốn.
Trong đề tài này tơi giới thiệu một số giải thuật sắp xếp từ đơn giản đến
phức tạp có thể áp dụng thích hợp cho việc sắp xếp.

4


2.3.1.2. Mục đích của sắp xếp
Mục đích của việc sắp xếp chính là giúp chúng ta có cái nhìn tổng quan
hơn về những dữ liệu mà ta có, dễ dàng tìm kiếm những phần tử đứng thứ nhất
về một tiêu chí nào đó....
2.3.2. Một thuật tốn sắp xếp cơ bản
2.3.1.1. Sắp xếp nổi bọt (Bubble Sort)
2.3.1.1.1. Ý tưởng của thuật toán
Sắp xếp nổi bọt là thuật toán đơn giản nhất, ý tưởng thuật toán này như sau:
Duyệt qua danh sách, làm cho các phần tử lớn nhất hoặc nhỏ nhất về cuối
danh sách, tiếp tục làm phần tử lớn hơn hoặc nhỏ hơn cận kề đó dịch chuyển về
cuối... cứ như thế cho đến hết danh sách.
2.3.1.1.2. Nội dung và cài đặt chương trình
Các bước thực hiện giải thuật:
1. Gán i

0

2. Gán j

0


3. Nếu A[j] > A[j+1] thì đổi chỗ A[j] và A[j+1]
4. Nếu j < n-j-1:
4.1: Đúng thì j=j+1 và quay lại bước 3
4.2: Sai thì sang bước 5
5. Nếu i < n-1:
5.1: Đúng thì i=i+1 và quay lại Bước 2
5.2: Sai thì dừng lại
Cài đặt trong C++:
#include <iostream>
#include <fstream>
#include <time.h>
using namespace std;
int a[100001];
int n,tg;
int main()
{
clock_t begin=clock();
5


ifstream fi("bubble.inp");
ofstream fo("bubble.out");
fi>>n;
for (int i=0;i<=n-1;i++)
fi>>a[i];
for (int i=0;i<=n-2;i++)
for (int j=i+1;j<=n-1;j++)
if (a[i]>a[j])
{
swap(a[i],a[j]);

}
//in ket qua
for (int i=0;i<=n-1;i++)
fo<fo<<"\n";
clock_t end=clock();
fo<<"time run:"<<(float) (end-begin)/CLOCKS_PER_SEC<<" s";
fi.close();fo.close();
return 0;
}
2.3.1.1.3. Đánh giá độ phức tạp của thuật toán
Ta nhận thấy trong giải thuật Bubble số lượng các phép so sánh không
phụ thuộc vào trạng thái ban đầu của dãy cần sắp xếp, nhưng số hoán vị lại phụ
thuộc vào trạng thái ban đầu của dãy.
Sắp xếp nổi bọt là một thuật toán sắp xếp ổn định.
Về độ phức tạp, do dùng hai vịng lặp lờng nhau nên độ phức tạp thời gian
trung bình của thuật tốn này là O(n2).
2.3.1.2. Sắp xếp chọn (Selection Sort)
2.3.1.2.1. Ý tưởng của thuật toán
Ý tưởng của thuật toán này như sau:
Duyệt từ phần tử đầu đến phần tử kề cuối danh sách, duyệt tìm phần tử
nhỏ nhất từ vị trí kề phần tử đang duyệt đến hết, sau đó đổi vị trí của phần tử
6


nhỏ nhất đó với phần tử đang duyệt và cứ tiếp tục như vậy.
2.3.1.2.2. Nội dung và cài đặt chương trình
Các bước thực hiện giải thuật:
B1: i


0

B2: j

i+1; min

A[i]

B3: Nếu j < n
B3.1: Nếu A[j] < A[min] thì min
B3.2: j

j

j+1

B3.3: Quay lại B3
B4: Đổi chỗ A[min] và A[i] cho nhau
B5: Nếu i < n-1 thì i

i+1 và quay lại B2

B6: Dừng lại.
Cài đặt trong C++
#include <iostream>
#include <fstream>
#include <time.h>
using namespace std;
int a[100001];
int n,tg,mi;

int main()
{
clock_t begin=clock();
ifstream fi("select.inp");
ofstream fo("select.out");
//doc mang
fi>>n;
for (int i=0;i<=n-1;i++)
fi>>a[i];
//sap xep
for (int i=0;i{
7


mi=i;
for (int j=i+1;jif (a[j]mi=j;
if (mi!=i)
{
tg=a[i];
a[i]=a[mi];
a[mi]=tg;
}
}
//in ket qua
for (int i=0;i<=n-1;i++)
fo<fo<<"\n";

clock_t end=clock();
fo<<"time run:"<<(float) (end-begin)/CLOCKS_PER_SEC<<" s";
fi.close();fo.close();
return 0;
}
2.3.1.2.3. Đánh giá độ phức tạp của thuật toán
Ta nhận thấy ở lượt thứ i, để chọn ra phần tử nhỏ nhất bao giờ cũng cần ni phép so sánh. Số lượng phép so sánh trong thuật tốn này khơng phụ thuộc vào
tình trạng ban đầu của mảng.
Về độ phức tạp, do dùng hai vòng lặp lờng nhau nên độ phức tạp thời gian
trung bình của thuật toán này là O(n2).
2.3.1.3. Sắp xếp chèn (Insert Sort)
2.3.1.3.1. Ý tưởng của thuật toán
Ý tưởng của thuật toán là chèn phần tử thứ i + 1 vào i phần tử đầu tiên của
dãy đã có thứ tự để được dãy mới trở nên có thứ tự. Việc chèn thực hiện như
sau: Tiến hành tìm vị trí của phần tử i+1 trong k phần tử đầu tiên bằng cách vận
dụng giải thuật tìm kiếm, sau khi tìm được vị trí chèn thì chúng ta sẽ tiến hành
chèn phần tử i+1 bằng cách dời các phần tử từ vị trí chèn đến phần tử thứ i sang
8


phải 1 vị trí và chèn phần tử i+1 vào vị trí đó.
2.3.1.3.2. Nội dung và cài đặt chương trình
Các bước thực hiện của thuật toán:
B1: i

1

B2: x

A[i]; pos


i-1

B3: Nếu Pos >= 0 và A[pos] > x:
B3.1:A[Pos+1]
B3.2: Pos

A[Pos]

Pos-1;

B3.3: Quay lại B3
B4: A[Pos+1]

x

B5: Nếu i < n thì i

i+1 và quay lại B2

B6: Dừng lại.
Cài đặt thuật toán trong C++:
#include <iostream>
#include <fstream>
#include <time.h>
using namespace std;
int a[100001];
int n,tg,pos,x;
int main()
{

clock_t begin=clock();
ifstream fi("insert.inp");
ofstream fo("insert.out");
//doc mang
fi>>n;
for (int i=0;i<=n-1;i++)
fi>>a[i];
//sap xep
for (int i=1;i<=n-1;i++)
9


{
x=a[i];
pos=i-1;
while (pos>=0 && a[pos]>x)
{
a[pos+1]=a[pos];
pos--;
}
a[pos+1]=x;
}
//in ket qua
for (int i=0;i<=n-1;i++)
fo<fo<<"\n";
clock_t end=clock();
fo<<"time run:"<<(float) (end-begin)/CLOCKS_PER_SEC<<" s";
fi.close();fo.close();
return 0;

}
2.3.1.3.3. Đánh giá độ phức tạp của thuật toán
Đối với giải thuật sắp xếp kiểu chèn, các phép so sánh xảy ra trong mỗi
vòng lặp While là tìm vị trí chèn thích hợp j. Mỗi lần xác định vị trí đang xét
khơng thích hợp, sẽ dời chỗ phần tử a[j] tương ứng. Giải thuật thực hiện tất cả
N-1 vòng lặp While. Số lượng phép so sánh và phép dời chỗ này phụ thuộc vào
tình trạng của dãy số ban đầu
Về độ phức tạp, do dùng hai vòng lặp lờng nhau nên độ phức tạp thời gian
trung bình của thuật toán này là O(n2).
2.3.1.4. Thuật toán Shell Sort
2.3.1.4.1. Ý tưởng của thuật toán
Shell sort là một giải thuật sắp xếp mang lại hiệu quả cao dựa trên giải thuật
sắp xếp chèn (Insertion Sort). Giải thuật này tránh các trường hợp phải tráo đổi
vị trí của hai phần tử xa nhau trong giải thuật sắp xếp chọn (Nếu như phần tử
nhỏ hơn ở vị trí bên phải khá xa so với phần tử lớn hơn bên trái).
10


Giải thuật này sử dụng giải thuật sắp xếp chọn trên các phần tử có khoảng
cách xa nhau, sau đó sắp xếp các phần tử có khoảng cách hẹp hơn.
2.3.1.4.2. Nội dung và cài đặt chương trình
Cài đặt C++:
#include <iostream>
#include <fstream>
#include <time.h>
using namespace std;
int a[100001];
int n;
void shellsort(int a[],int n)
{

int k;
for (int i=n/2;i>0;i=i/2)
{
for (int j=i;j{
for (k=j-i;k>=0;k=k-i)
{
if (a[k+i]>=a[k])
break;
else
{
swap(a[k],a[k+1]);
}
}
}
}
}
int main()
{
11


clock_t begin=clock();
ifstream fi("shell.inp");
ofstream fo("shell.out");
fi>>n;
for (int i=0;i<=n-1;i++)
fi>>a[i];
shellsort(a,n);
for (int i=0;i<=n-1;i++)

fo<fo<<"\n";
clock_t end=clock();
fo<<"time run:"<<(float) (end-begin)/CLOCKS_PER_SEC<<" s";
fi.close();fo.close();
return 0;
}
2.3.1.4.3. Đánh giá độ phức tạp của thuật toán
Đây là giải pháp khá hiệu quả với các tập dữ liệu có kích thước trung bình
khi mà độ phức tạp trường hợp xấu nhất và trường hợp trung bình là O(n2).
2.3.1.5. Sắp xếp kiểu phân đoạn (QuickSort)
2.3.1.5.1. Ý tưởng của thuật toán
Sắp xếp nhanh (QuickSort) hay sắp xếp phân đoạn (Partition) là thuật toán
sắp xếp dựa trên kỹ thuật chia để trị, cụ thể ý tưởng là: chọn một điểm làm chốt
(gọi là pivot), sắp xếp mọi phần tử bên trái chốt đều nhỏ hơn chốt và mọi phần
tử bên phải đều lớn hơn chốt, sau khi xong ta được 2 dãy con bên trái và bên
phải, áp dụng tương tự cách sắp xếp này cho 2 dãy con vừa tìm được cho đến
khi dãy con chỉ cịn 1 phần tử.
2.3.1.5.2. Nội dung và cài đặt chương trình
Thuật toán như sau:
B1: Chọn một phần tử làm chốt
B2: Sắp xếp phần tử bên trái nhỏ hơn chốt
B3: Sắp xếp phần tử bên phải nhỏ hơn chốt
B4: Sắp xếp hai mảng con bên trái và bên phải pivot
Phần tử được chọn làm chốt rất quan trọng, nó quyết định thời gian thực
12


thi của thuật toán. Phần tử được chọn làm chốt tối ưu nhất là phần tử trung vị,
phần tử này làm cho số phần tử nhỏ hơn trong dãy bằng hoặc xấp xỉ số phần tử

lớn hơn trong dãy. Tuy nhiên, việc tìm kiếm này rất tốn kém, phải có thuật tốn
tìm kiếm riêng, từ đó làm giảm hiệu śt của thuật tốn tìm kiếm nhanh, do đó,
để đơn giản, người ta sử dụng phần tử chính giữa làm chốt.
Cài đặt C++:
#include <iostream>
#include <fstream>
#include <time.h>
using namespace std;
int a[100001];
int n;
void swap(int &a,int &b)
{
int tg=a;
a=b;
b=tg;
}
void QuickSort(int a[],int left, int right)
{
int i=left, j=right;
int pivot=a[(left+right)/2];
while (i<=j)
{
while (a[i]i++;
while (a[j]>pivot)
j--;
if (i<=j)
{
swap(a[i],a[j]);
i++;

13


j--;
}
}
if (leftQuickSort(a,left,j);
if (iQuickSort(a,i,right);
}
int main()
{
clock_t begin=clock();
ifstream fi("Quick.inp");
ofstream fo("Quick.out");
fi>>n;
for (int i=0;i<=n-1;i++)
fi>>a[i];
QuickSort(a,0,n-1);
for (int i=0;i<=n-1;i++)
fo<fo<<"\n";
clock_t end=clock();
fo<<"time run:"<<(float) (end-begin)/CLOCKS_PER_SEC<<" s";
fi.close();fo.close();
return 0;
}
2.3.1.5.3. Đánh giá độ phức tạp của thuật toán
Hiệu quả của giải thuật Quick Sort phụ thuộc vào việc chọn khóa chốt để

phân đoạn. Trường hợp tốt nhất xảy ra nếu mỗi lần phân đoạn đều chọn được
khóa chốt lớn hơn (hoặc bằng) nửa số phần tử, và nhỏ hơn (hoặc bằng) nửa số
phần tử cịn lại; khi đó độ phức tạp tính tốn của Quick Sort là O(nlogn). Nhưng
nếu mỗi lần chọn khóa chốt để phân đoạn lại chọn nhằm phần tử có giá trị cực
đại (hay cực tiểu) là khóa chốt, dãy sẽ bị chia thành hai phần không đều: một
phần chỉ có 1 phần tử, phần cịn lại có n-1 phần tử thì lúc đó cần n lần phân đoạn
14


mới sắp xếp xong. Lúc đó độ phức tạp của Quick Sort là O(n 2). Vì vậy thời gian
thực hiện giải thuật Quick Sort trung bình là O(nlogn).
2.3.1.6. Sắp xếp bằng phép đếm phân phối (Counting Sort)
2.3.1.6.1. Ý tưởng của thuật toán
Counting sort là một thuật toán sắp xếp cực nhanh một mảng các phần tử
mà mỗi phần tử là các số nguyên không âm; hoặc là một danh sách các ký tự
được ánh xạ về dạng số để sort theo bảng chữ cái. Counting sort là một thuật
toán sắp xếp các con số nguyên không âm, không dựa vào so sánh.
2.3.1.6.2. Nội dung và cài đặt chương trình
Cài đặt C++:
#include <iostream>
#include <fstream>
using namespace std;
int a[100001];
int n;
void counting_sort(int a[],int n)
{
int output[n];
int max=a[0];
int min=a[0];
for (int i=1;i

{
if (a[i]>max)
max=a[i];
else if(a[i]min=a[i];
}
int k=max-min+1;
int dem[k];
fill_n(dem,k,0);
for (int i=0;idem[a[i]-min]++;
15


for (int i=1;idem[i]+=dem[i-1];
for (int i=0;i{
output[dem[a[i]-min]-1]=a[i];
dem[a[i]-min]--;
}
for (int i=0;ia[i]=output[i];
}
int main()
{
ifstream fi("count.inp");
ofstream fo("count.out");
fi>>n;
for (int i=0;i<=n-1;i++)

fi>>a[i];
counting_sort(a,n);
//in ket qua
for (int i=0;i<=n-1;i++)
fo<fi.close();fo.close();
return 0;
}
2.3.1.6.3. Đánh giá độ phức tạp của thuật toán
Trong khi các thuật toán sắp xếp tối ưu sử dụng so sánh có độ phức tạp
O(nlogn) thì Counting sort chỉ cần O(n) nếu độ dài của danh sách không quá
nhỏ so với phần tử có giá trị lớn nhất.
2.3.1.7. Sắp xếp kiểu vun đống (Heap sort)
2.3.1.7.1. Ý tưởng của thuật toán
Giải thuật Heap sort cịn được gọi là giải thuật vun đống, có thể được xem
như bản cải tiến của Selection Sort khi chia các phần tử thành 2 mảng con:
16


+ Mảng 1 các phần tử đã được sắp xếp
+ Mảng 2 các phần tử chưa được sắp xếp
Trong mảng chưa được sắp xếp, các phần tử lớn nhất sẽ được tách ra và
đưa vào mảng đã được sắp xếp. Điều cải tiến ở Heap Sort so với Selection Sort
ở việc sử dụng cấu trúc dữ liệu heap thay vì tìm kiếm tuyến tính để tìm ra phần
tử lớn nhất.
2.3.1.7.2. Nội dung và cài đặt chương trình
Cài đặt trong C++:
#include <iostream>
#include <fstream>
#include <time.h>

using namespace std;
int a[100001];
int n;
void swap(int &a, int &b)
{
int tg=a;
a=b;
b=tg;
}
void ImplHeap(int a[], int n, int i)
{
int root=i;
int l=2*i+1;
int r=2*i+2;
if (l<n && a[l]>a[root])
root=l;
if (r<n && a[r]>a[root])
root=r;
if (root!=i)
{
swap(a[i],a[root]);
17


ImplHeap(a,n,root);
}
}
void ImplHeapSort(int a[],int n)
{
for(int i=(n/2)-1;i>=0;i--)

ImplHeap(a,n,i);
for (int i=n-1;i>=0;i--)
{
swap(a[0],a[i]);
ImplHeap(a,i,0);
}
}
int main()
{
clock_t begin=clock();
ifstream fi("Heap.inp");
ofstream fo("Heap.out");
//doc mang
fi>>n;
for (int i=0;i<=n-1;i++)
fi>>a[i];
//sap xep
int n=sizeof(a)/sizeof(a[0]);
ImplHeapSort(a,n);
//in ket qua
for (int i=0;ifo<fo<<"\n";
clock_t end=clock();
18


fo<<"time run:"<<(float) (end-begin)/CLOCKS_PER_SEC<<" s";
fi.close();fo.close();
return 0;

}
2.3.1.7.3. Đánh giá độ phức tạp của thuật toán
Heap Sort là giải thuật đảm bảo trong trường hợp xấu nhất, độ phức tạp
giải thuật cũng là O(nlogn). Giải thuật này cũng không cần thêm các cấu trúc dữ
liệu phụ trợ trong quá trình thực thi, do đó sẽ có tốc độ nhanh và thường được sử
dụng rộng rãi do khơng khó để thực hiện.
2.3.1.8. Sắp xếp trộn (Merge Sort)
2.3.1.8.1. Ý tưởng của thuật toán
Sắp xếp trộn (merge sort) là một thuật toán dựa trên kỹ thuật chia để trị, ý
tưởng của thuật toán này như sau: chia đôi mảng thành hai mảng con, sắp xếp
hai mảng con đó và trộn lại theo đúng thứ tự, mảng con được sắp xếp bằng cách
tương tự.
2.3.1.8.2. Nội dung và cài đặt chương trình
Giả sử left là vị trí đầu và right là cuối mảng đang xét, cụ thể các bước
của thuật tốn như sau:
Nếu mảng cịn có thể chia đơi được (leftB1: Tìm vị trí chính giữa mảng
B2: Sắp xếp mảng thứ nhất (từ vị trí left đến mid)
B3: Sắp xếp mảng thứ 2 (từ vị trí mid+1 đến right)
B4: Trộn hai mảng đã sắp xếp với nhau
Cài đặt trong C++:
#include <iostream>
#include <fstream>
#include <time.h>
using namespace std;
int arr[100001];
int n;
void ImplMerge(int arr[], int left, int mid, int right)
{
int n1=mid-left+1;

19


int n2=right-mid;
int* L=new int[n1];
int* R=new int[n2];
for (int ii=0;iiL[ii]=arr[left+ii];
for (int jj=0;jjR[jj]=arr[mid+1+jj];
int i=0;
int j=0;
int k=left;
while (i{
if (L[i]<=R[j])
{
arr[k]=L[i];
i++;
}
else
{
arr[k]=R[j];
j++;
}
k++;
}
while (i{
arr[k]=L[i];

i++;
k++;
}
}
20


void mergesort(int arr[],int left, int right)
{
if (left{
int mid=left+(right-left)/2;
mergesort(arr,left,mid);
mergesort(arr,mid+1,right);
ImplMerge(arr,left,mid,right);
}
}
int main()
{
clock_t begin=clock();
ifstream fi("merge.inp");
ofstream fo("merge.out");
fi>>n;
for (int i=0;i<=n-1;i++)
fi>>arr[i];
int arr_size=sizeof(arr)/sizeof(arr[0]);
mergesort(arr,0,arr_size-1);
//in ket qua
for (int i=0;i<=n-1;i++)
fo<

fo<<"\n";
clock_t end=clock();
fo<<"time run:"<<(float) (end-begin)/CLOCKS_PER_SEC<<" s";
fi.close();fo.close();
return 0;
}

21


2.3.1.8.3. Đánh giá độ phức tạp của thuật toán
Về độ phức tạp, thuật tốn Merge Sort có độ phức tạp thời gian trung bình
là O(nlog(n)), về khơng gian, do sử dụng mảng phụ để lưu trữ và 2 mảng phụ
dài nhất là hai mảng phụ ở lần chia đầu tiên có tổng số phần tử đúng bằng số
phần tử cùa mảng nên độ phức tạp sẽ là O(n). Sắp xếp trộn là thuật toán sắp xếp
ổn định.
2.3.3. Đánh giá thuật toán và những yếu tố cần quan tâm khi sử dụng thuật
tốn
2.3.3.1. Đánh giá thuật tốn
Trong chương trình cài đặt các thuật tốn sắp xếp ở trên, trong chương
trình tơi tính ln thời gian chạy chương trình trong tệp ra để dễ so sánh.
Trường hợp 1: Với 5 bộ test ngẫu nhiên các phần tử, bảng so sánh thời
gian thực hiện của các thuật toán như sau:
Độ phức
50
tạp

1000

5000


9000

10000

Bubble

O(n2)

0.001

0.008

0.118

0.357

0.419

Selection

O(n2)

0.001

0.011

0.088

0.254


0.302

Insertion

O(n2)

0.001

0.005

0.003

0.087

0.09

Shell

O(n2)

0.001

0.006

0.045

0.142

0.167


Quick

O(nlogn) 0.002

0.005

0.021

0.043

0.047

Couting

O(n)

0.002

0.007

X

X

X

Heap

O(nlogn) 0.001


0.004

0.021

0.042

0.043

Merge

O(n)

0.019

0.072

0.05

0.059

0.016

Phân tích kết quả: từ bảng so sánh thời gian chạy ở trên ta có thể thấy
những giải thuật có độ phức tạp O(n 2) thì thời gian chạy vẫn nhiều nhất, tuy
nhiên nó vẫn là thời gian chấp nhận được đối với 1 giải thuật (chưa quá 1s).
Những thuật toán có độ phức tạp O(n) hoặc O(nlogn) thì thời gian chạy nhanh
hơn. Tuy nhiên đối với Counting Sort mặc dù thời gian chạy nhanh nhưng không
phù hợp với bộ test ngẫu nhiên (các phần tử có thể âm).
Trường hợp 2: Với 5 bộ test mà các phần tử dương, số lượng phần tử

<=10000.
Độ phức
6000
tạp

8000

8500

9000

10000

22


Bubble

O(n2)

0.195

0.473

0.523

0.593

0.706


Selection

O(n2)

0.122

0.2

0.211

0.245

0.219

Insertion

O(n2)

0.048

0.126

0.139

0.164

0.189

Shell


O(n2)

0.189

0.314

0.363

0.402

0.483

Quick

O(nlogn) 0.021

0.031

0.032

0.042

0.043

Couting

O(n)

0.021


0.029

0.031

0.033

0.036

Heap

O(nlogn) 0.023

0.031

0.042

0.043

0.044

Merge

O(n)

0.043

0.047

0.055


0.063

0.04

Phân tích kết quả: Ở bảng kết quả này ta có thể thấy độ chênh lệch thời
gian giữa các nhóm thuật tốn.
Các thuật tốn có độ phức tạp O(n2) có vẻ khơng ổn khi dữ liệu vào lớn.
Thuật toán Counting đứng top đầu về thời gian chạy. Dữ liệu càng lớn thì càng
thấy rõ tính tối ưu của Counting Sort.
Những thuật tốn có độ phức tạp O(nlogn) và O(n) vẫn ổn định khi dữ liệu vào
lớn.
Trường hợp 3: Với 5 bộ test mà các phần tử dương, số lượng phần tử
50000<=N<=100000.
Đối với trường hợp này tôi chỉ test những giải thuật Sort thuộc top nhanh của 2
trường hợp trên.
Độ phức
50000
tạp

60000

80000

90000

100000

Quick

O(nlogn) 0.184


0.221

0.295

0.324

0.365

Couting

O(n)

0.21

0.287

0.322

0.356

Heap

O(nlogn) 0.204

0.259

0.332

0.383


0.451

Merge

O(n)

0.35

0.422

0.459

0.509

0.190

0.315

Phân tích kết quả: Ở trường hợp này, thuật toán Counting Sort vẫn đang
chứng minh là thuật tốn ổn nhất. Tiếp theo đó là Quick Sort, Heap Sort, Merge
Sort.
2.3.3.2. Những yếu tố cần quan tâm khi sử dụng thuật tốn
Từ những phân tích trên ta nhận thấy, cùng một mục đích sắp xếp như
nhau nhưng có nhiều phương pháp giải quyết khác nhau. Nếu chỉ dựa vào thời
gian tính tốn của thuật tốn đo được trong một ví dụ cụ thể mà đánh giá giải
23


thuật này tốt hơn giải thuật kia về mọi mặt là chưa đủ. Việc lựa chọn một thuật

toán sắp xếp phù hợp với từng yêu cầu, từng điều kiện cụ thể là kỹ năng của
người lập trình.
Vì vậy đối với người lập trình khơng cần phải học hết tất cả các thuật toán
Sort, chỉ cần nắm vững tư tưởng của nó, học thuộc vài thuật tốn thơng dụng
như Bubble Sort, Intertion Sort, Selection Sort. Sau đó khi học nâng cao hơn,
với những bài tốn với dữ liệu vào lớn thì người lập trình có thể học Quick Sort,
Merge Sort, Heap Sort.
Sau đây là một số yếu tố cần quan tâm.
2.3.3.2.1. Độ phức tạp
Đây là yếu tố chính khi đánh giá thuật tốn sort. Độ phức tạp của thuật
tốn có thể hiểu là tổng khối lượng cơng việc mà thuật tốn sẽ xử lý.
Độ phức tạp của thuật toán chủ yếu được đánh giá bởi 3 thành phần là độ
lớn dữ liệu (n), số phép so sánh và số phép gán.
Một thuật tốn có độ phức tạp trung bình là O(n 2) thì được đánh giá là
chưa tốt, thời gian chạy rất lâu. Tuy nhiên những thuật tốn này thì đa số lại dễ
cài đặt và rất thông dụng như: Bubble Sort, Intertion Sort, Selection Sort.
Ví dụ những bài tốn đơn giản như sắp xếp danh sách 1000 phần tử, sử
dụng 1 trong những thuật tốn trên là q đủ rời. Vừa đơn giản vừa dễ cài đặt.
Người lập trình khơng cần phải sử dụng các thuật toán trong top chạy nhanh vì
hầu hết chúng rất phức tạp, cài đặt dễ sai.
Thuật tốn tốt và chạy nhanh hiện nay thường có độ phức tạp O(nlogn)
hoặc O(n).
2.3.3.2.2. Tài nguyên sử dụng
Một số thuật toán cần sử dụng các biến tạm để hỗ trợ, cần sử dụng thêm
RAM, phần cứng khác... chúng đều được xem là tài nguyên sử dụng.
Các thuật toán thường có nguyên tắc đánh đổi: Nếu tài nguyên sử dụng
nhiều thì thuật tốn chạy nhanh và ngược lại.
Các thuật tốn thông dụng như Bubble Sort, Intertion Sort, Selection
Sort... thường sử dụng tài nguyên cực nhỏ (1,2 hoặc 3 biến tạm).
2.3.3.2.3. Tính tổng qt

Một số thuật tốn chỉ chạy được trên số nguyên, mặc dù vẫn thuộc top
chạy nhanh nhưng không có tính tổng qt. Trong những giải thuật tơi trình bày
ở trên thì có thuật tốn Counting Sort. Rõ ràng thuật toán này chạy nhanh
thường ở top đầu nhưng chỉ với phần tử dương và không quá lớn.

24


2.3.4. Một số bài toán áp dụng
2.3.4.1. Nguồn />Vaxia đã viết được một số lớn trên một cuộn giấy dài và muốn khoe với
anh trai Petia về thành quả vừa đạt được. Tuy nhiên, khi Vaxia vừa ra khỏi
phòng để gọi anh trai thì cơ em Kachia chạy vào phịng xé rách cuộn giấy thành
một số mảnh. Kết quả là trên mỗi mảnh có một hoặc vài kí số theo thứ tự đã
viết.
Bây giờ Vaxia khơng thể nhớ chính xác mình đã viết số gì. Vaxia chỉ nhớ
rằng đó là một số rất lớn.
Để làm hài lòng cậu em trai, Petia quyết định truy tìm số nào là lớn nhất
mà Vaxia đã có thể viết lên cuộn giấy trước khi bị xé. Bạn hãy giúp Petia làm
việc này.
Dữ liệu: Vào từ file văn bản NUMCON.INP: ghi một hoặc nhiều dòng. Mỗi
dịng ghi một dãy kí tự số. Số dịng khơng vượt q 100. Mỗi dịng ghi từ 1 đến
100 kí tự số. Đảm bảo rằng có ít nhất một dịng mà kí tự số đầu tiên khác 0.
Kết quả: Ghi ra file NUMCON.OUT - số lớn nhất có thể viết trên cuộn giấy
trước khi bị xé rách.
Ví dụ:
NUMCON.INP
2

NUMCON.OUT
66220004


20
004
66
3

3

Ý tưởng: Bài tốn này cần sắp xếp theo tiêu chí sau: Nếu xâu a[i] đứng trước
xâu a[j] thì a[i]+a[j]>a[j].
Bài tốn này dữ liệu vào tương đối nhỏ, nên người lập trình chỉ cần sử
dụng những giải thuật Sort đơn giản, dễ cài đặt như Bubble Sort, Selection Sort,
Insert Sort, Shell Sort. Trong chương trình tham khảo, tơi sử dụng thuật tốn
quen thuộc Bubble Sort.
Chú ý phép cộng này chính là phép ghép hai xâu. Kết quả bài toán là ghi
lần lượt các xâu theo thứ tự đã sắp xếp.
Cài đặt chương trình:
#include <iostream>
25


×