Professional Documents
Culture Documents
Sorting Algorithm
Sorting Algorithm
Mục lục
1 Định nghĩa 2
2 Cài đặt 2
2.1 Hàm main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.2 Exchange sort - Sắp xếp đổi chỗ trực tiếp . . . . . . . . . . . . . . . . . . . 4
2.3 Bubble Sort - Sắp xếp nổi bọt . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.4 Odd even sort - Biến thể của Bubble sort . . . . . . . . . . . . . . . . . . . 7
2.5 Insertion sort - Sắp xếp chèn . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.5.1 Không sử dụng thêm STL (Standard Template Library) . . . . . . 8
2.5.2 Sử dụng std::rotate và std::upper_bound của thư viện <algorithm> 9
2.6 Shell sort - Biến thể của Insertion sort . . . . . . . . . . . . . . . . . . . . 11
2.7 Selection sort - Sắp xếp chọn . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.8 Merge sort - Sắp xếp trộn . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.8.1 Không sử dụng thêm STL . . . . . . . . . . . . . . . . . . . . . . . 14
2.8.2 Sử dụng std::inplace_merge của thư viện <algorithm> . . . . . . . 15
2.9 3-way Merge sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.10 Quick sort - Sắp xếp nhanh . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.10.1 Chọn phần tử cuối cùng làm pivot . . . . . . . . . . . . . . . . . . 18
2.10.2 Chọn phần tử đầu tiên làm pivot . . . . . . . . . . . . . . . . . . . 19
2.10.3 Median of Three . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.10.4 Chọn pivot ngẫu nhiên . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.11 Heap sort - Sắp xếp vun đống . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.11.1 Heap là gì? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.11.2 Các thao tác trên heap . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.11.3 Heap sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4 Lời kết 26
Page 1/26
Các giải thuật sort trong C++
1 Định nghĩa
Giải thuật sắp xếp (Sorting algorithm) là giải thuật dùng để đảo thứ tự các phần tử
của 1 cấu trúc dữ liệu không có thứ tự nào đó (mảng: array, danh sách liên kết: linked
list,...) để các phần tử có một thứ tự nhất định (tăng dần hoặc giảm dần). Sau đây là các
giải thuật sắp xếp cơ bản và cách cài đặt chúng trên C++.
Trong bài viết này, mảng sẽ được thay thế bằng vector, là một cấu trúc dữ liệu với
khả năng tự ghi nhớ độ dài của chính nó. Để sử dụng cấu trúc dữ liệu này, ta phải thêm
thư viện <vector> vào. Do mỗi vector là 1 object của class vector (không như mảng là
1 dãy các vùng nhớ liên tục với con trỏ mảng giữ địa chỉ của vùng nhớ đầu tiên) nên việc
truyền tham chiếu cho vector là bắt buộc.
Thứ tự sắp xếp dùng cho các giải thuật dưới đây sẽ là sắp xếp tăng dần (giảm dần
tương tự).
Kiểu của các phần tử là kiểu pair<int, int> của thư viện <utility>, trong đó pair
là cấu trúc dữ liệu key - value, với giá trị đầu tiên là key - vị trí ban đầu của các phần tử
và giá trị thứ hai là value - là data cần để sắp xếp (có thể thay int bằng các kiểu dữ liệu
có thể so sánh được). Việc này giúp chúng ta đánh giá độ ổn định (stable) của giải thuật.
Giải thuật sắp xếp được gọi là ổn định nếu nó bảo toàn thứ tự trước sau của các phần tử
có giá trị trùng nhau.
Sử dụng hàm swap(a, b) để hoán đổi giá trị của a, b.
2 Cài đặt
2.1 Hàm main
1 int main () {
2 random_device rd ;
3 mt19937 gen ( rd () ) ;
4 uniform_int_distribution < int > dis (1 , 1 e5 - 1) ;
5 vector < pair < int , int > > v ;
6 const int N = 1 e5 ;
7 for ( int i = 0; i < N ; i ++) {
8 v . push_back ( make_pair (i , dis ( gen ) ) ) ;
9 }
Page 2/26
Các giải thuật sort trong C++
10 void (* f ) ( vector < pair < int , int > >&) = func ;
11 auto start = chrono :: h i g h _ r e s o l u t i o n _ c l o c k :: now () ;
12 f(v);
13 auto stop = chrono :: h i g h _ r e s o l u t i o n _ c l o c k :: now () ;
14 auto duration =
chrono :: duration_cast < chrono :: milliseconds >( stop -
start ) ;
15 bool flag = true ;
16 for ( int i = 0; i < N - 1; i ++) {
17 for ( int j = i + 1; j < N ; j ++) {
18 if ( v [ i ]. second == v [ j ]. second && v [ i ]. first >
v [ j ]. first ) {
19 flag = false ;
20 break ;
21 }
22 }
23 }
24 cout << " This algorithm is " << ( flag == true ? " stable "
: " not stable " ) << ’\ n ’;
25 cout << " This algorithm takes " << duration . count () <<
" ms to sort " ;
26 }
Đây là chương trình để kiểm tra độ ổn định cũng như tính thời gian chạy của các thuật
toán sắp xếp. Đừng hoảng loạn khi có nhiều hàm mới mà các bạn chưa được học, mình sẽ
giải thích đoạn chương trình này:
1. Dòng 2 và 3: khởi tạo bộ sinh ngẫu nhiên bằng thuật toán sinh số ngẫu nhiên
Mersenne Twister. Các bạn nhớ include thư viện random nhé.
2. Dòng 4: Khởi tạo khoảng sinh số ngẫu nhiên cho các phần tử trong mảng trong
khoảng từ 1 đến 105 − 1 sử dụng phân phối đồng nhất. Cái này cũng thuộc thư viện
random.
3. Dòng 5, 6: Khởi tạo mảng (dùng vector) và cố định số phần tử của mảng là 105 .
Với 105 phần tử và các phần tử đều thuộc đoạn [1; 105 − 1] thì sẽ luôn tồn tại ít nhất
2 phần tử bằng nhau (nguyên lý Dirichlet).
4. Dòng 7, 8, 9: Đưa các số ngẫu nhiên vào mảng v, chúng ta chọn key là index của
mảng luôn, value sẽ là cái chúng ta cần để sắp xếp.
Page 3/26
Các giải thuật sort trong C++
5. Dòng 10: Khởi tạo con trỏ hàm f trỏ đến các hàm sắp xếp (ở đây mình để tạm là
func)
6. Dòng 11, 12, 13, 14: Đo thời gian chạy của hàm f với đối số truyền vào là v, do có
sử dụng chrono:: nên các bạn nhớ include thư viện chrono nhé.
7. Từ dòng 15 đến 23: Kiểm tra tính ổn định của giải thuật sắp xếp.
Ngoài ra, tại các hàm sort để test, mình sẽ luôn thêm 1 câu lệnh ở đầu hàm:
std::source_location chỉ có ở phiên bản C++20, các bạn nhớ setup trình biên dịch
để có thể chạy phiên bản C++20 nhé.
Setup chương trình test xong rồi, chúng ta cùng đến với các giải thuật sắp xếp sau đây:
1 void exchangeSort ( vector < pair < int , int > >& v ) {
2 cout << " Running " +
string ( source_location :: current () . function_name () ) <<
’\ n ’;
3 for ( int i = 0; i < ( int ) v . size () ; i ++) {
4 for ( int j = i + 1; j < ( int ) v . size () ; j ++) {
5 if ( v [ i ]. second > v [ j ]. second ) {
6 swap ( v [ i ] , v [ j ]) ;
7 }
8 }
9 }
10 }
Page 4/26
Các giải thuật sort trong C++
Chạy hàm main với func là hàm exchangeSort ta được kết quả:
Note: Ở đây mình biên dịch với optimization -O3 và -std=c++20 để tăng tốc độ chạy
chương trình và biên dịch bằng c++20
Các bạn có thể thấy là giải thuật này không có tính ổn định và thời gian chạy lâu (21.7s
đã optimize bằng -O3 với 105 phần tử) nên không phù hợp để cài đặt khi số phần tử lớn
hơn. Tuy nhiên thuật toán này rất dễ hiểu cũng như đơn giản với người mới học cũng như
việc thuật toán này là thuật toán sort - in - place algorithm (sắp xếp tại chỗ) nên tiết kiệm
bộ nhớ hơn.
Độ phức tạp thời gian (mọi trường hợp): O(N 2 ).
Độ phức tạp không gian (mọi trường hợp): O(1).
1 void bubbleSort ( vector < pair < int , int > >& v ) {
2 cout << " Running " +
string ( source_location :: current () . function_name () ) <<
’\ n ’;
3 for ( int i = 0; i < ( int ) v . size () ; i ++) {
4 for ( int j = 0; j < ( int ) v . size () - i - 1; j ++) {
Page 5/26
Các giải thuật sort trong C++
Với 105 phần tử và optimization -O3, bubble sort tốn gần 17.4s để sắp xếp, do đó
Bubble sort không tối ưu khi sắp xếp mảng với số lượng phần tử lớn. Nhưng ngược lại đây
là giải thuật sort ổn định, bảo toàn thứ tự các phần tử giống nhau. Bubble sort, cũng như
Exchange sort, đều là các sort - in - place algorithm.
Có thể nhìn ra rằng hàm trên không tối ưu nếu mảng đã sắp xếp có thứ tự trùng với
thứ tự mà ta mong muốn, khi này hai vòng lặp vẫn phải chạy hết. Để tối ưu, ta sẽ thêm
cờ swapped ở ngoài cả 2 vòng lặp để kiểm tra xem có sự hoán đổi giá trị của 2 phần tử
liên tiếp hay không. Nếu không có thì ta dừng việc lặp lại và kết thúc hàm.
Đoạn code tối ưu cho chương trình trên như sau:
1 void b ubb le So rtO pt im ize ( vector < pair < int , int > >& v ) {
2 cout << " Running " +
string ( source_location :: current () . function_name () ) <<
’\ n ’;
3 bool swapped ;
4 for ( int i = 0; i < ( int ) v . size () ; i ++) {
5 swapped = false ;
6 for ( int j = 0; j < ( int ) v . size () - i - 1; j ++) {
7 if ( v [ j ]. second > v [ j + 1]. second ) {
8 swap ( v [ j ] , v [ j + 1]) ;
9 swapped = true ;
10 }
11 }
Page 6/26
Các giải thuật sort trong C++
12 if (! swapped ) {
13 return ;
14 }
15 }
16 }
Độ phức tạp thời gian: O(N ) (trường hợp tốt nhất), O(N 2 ) (trung bình), O(N 2 ) (trường
hợp tệ nhất khi mảng sắp xếp ngược thứ tự mà ta mong muốn).
Độ phức tạp không gian (mọi trường hợp): O(1)
Page 7/26
Các giải thuật sort trong C++
Độ phức tạp thời gian: O(N ) (trường hợp tốt nhất), O(N 2 ) (trung bình), O(N 2 ) (trường
hợp tệ nhất khi mảng sắp xếp ngược thứ tự mà ta mong muốn).
Độ phức tạp không gian (mọi trường hợp): O(1)
1. Chia mảng thành hai nửa, mảng đã sắp xếp và mảng chưa sắp xếp, ban đầu thì phần
tử đứng đầu mảng gốc sẽ thuộc về mảng đã sắp xếp, còn lại thuộc về mảng chưa sắp
xếp.
2. Lặp qua từng phần tử trong phần mảng chưa sắp xếp và chèn phần tử đó vào đúng
vị trí của nó trong mảng đã sắp xếp.
Ở đây mình xin trình bày hai phương pháp để cài đặt giải thuật này
Đoạn code sau đây cũng khá dễ hiểu, các bạn có thể đọc sơ qua:
1 void insertionSort ( vector < pair < int , int > >& v ) {
2 cout << " Running " +
string ( source_location :: current () . function_name () ) <<
’\ n ’;
3 for ( int i = 1; i < ( int ) v . size () ; i ++) {
4 pair < int , int > temp = v [ i ];
5 int idx = i - 1;
6 while ( idx >= 0 && v [ idx ]. second > temp . second ) {
7 v [ idx + 1] = v [ idx ];
8 idx - -;
Page 8/26
Các giải thuật sort trong C++
9 }
10 v [ idx + 1] = temp ;
11 }
12 }
Thử test thời gian và độ ổn định của giải thuật, ta có kết quả sau:
Hình 4: Kết quả khi chạy giải thuật Insertion sort (không sử dụng STL)
Thời gian cải thiện rất nhiều so với giải thuật Bubble sort, đồng thời Insertion sort
cũng ổn định nên được cài đặt trong một số giải thuật sắp xếp khác (sẽ nói sau).
Độ phức tạp thời gian: O(N ) (trường hợp tốt nhất), O(N 2 ) (trung bình), O(N 2 ) (trường
hợp tệ nhất).
Độ phức tạp không gian (mọi trường hợp): O(1)
1 void insertionSortSTL ( vector < pair < int , int > >& v ) {
2 cout << " Running " +
string ( source_location :: current () . function_name () ) <<
’\ n ’;
3 for ( auto it = v . begin () ; it != v . end () ; it ++) {
4 rotate ( upper_bound ( v . begin () , it , it - > second ,
[]( const int x , pair < int , int > p ) { return p . second < x ;
}) , it , it + 1) ;
5 }
6 }
Không cần phải hoảng loạn khi nhìn đoạn code tuy cực ngắn, nhưng cũng khó hiểu
hơn. Đừng lo, mình sẽ giải thích từng dòng code cho các bạn:
1. Dòng 2: Vòng lặp này sẽ lặp từ đầu mảng đến cuối mảng, ở đây sử dụng cấu trúc dữ
liệu là vector<pair<int, int>>::iterator làm biến con trỏ để lặp trong vector.
Page 9/26
Các giải thuật sort trong C++
Ở đây begin là iterator trỏ đến vị trí index 0 của mảng, còn end trỏ đến vị trí index
v.size() của mảng.
2. Dòng 3: Đây là sự kết hợp giữa 2 hàm trong thư viện <algorithm>:
• rotate(first, middle, last): thực hiện swap - in - place 2 mảng con: mảng
1 từ iterator first đến iterator middle (không tính middle), mảng 2 từ iterator
middle đến iterator last (không tính last), đồng nghĩa với việc mảng con đầu
tiên sẽ luôn nằm sau mảng con thứ 2. Phép rotate này bảo toàn thứ tự của các
giá trị trùng nhau trong mảng.
Trong đoạn code trên, kết quả của hàm upper_bound sẽ được dùng làm iterator
first của hàm rotate với middle = it và last = it + 1.
Test độ ổn định của giải thuật và thời gian chạy hàm insertionSortSTL, ta có kết
quả:
Page 10/26
Các giải thuật sort trong C++
Hình 5: Kết quả sau khi chạy hàm insertionSort có sử dụng thư viện <algorithm>
1 void shellSort ( vector < pair < int , int > >& v ) {
2 cout << " Running " +
string ( source_location :: current () . function_name () ) <<
’\ n ’;
3 int N = ( int ) v . size () ;
4 for ( int gap = N / 2; gap > 0; gap /= 2) {
5 for ( int i = gap ; i < N ; i ++) {
6 auto temp = v [ i ];
7 int j ;
8 for ( j = i ; j >= gap && v [ j - gap ]. second >
temp . second ; j -= gap ) {
Page 11/26
Các giải thuật sort trong C++
9 v [ j ] = v [ j - gap ];
10 }
11 v [ j ] = temp ;
12 }
13 }
14 }
Tất nhiên là tùy tình huống mà chọn gap và chọn cách giảm gap xuống cho phù hợp.
Với đoạn code trên, khi test độ ổn định và thời gian chạy, ta được kết quả:
Mặc dù thời gian chạy rất nhanh (chỉ có 27ms) nhưng Shell sort lại không ổn định (ai
quan tâm chứ, nhanh là được).
Độ phức tạp về thời gian: dựa trên việc chọn gap và cách giảm gap (với việc chọn gap
như đoạn code trên thì độ phức tạp trung bình là O(N 2 ). Trường hợp tốt nhất thì độ phức
tạp là O(N logN ) và trường hợp tệ nhất là O(N 2 )).
Có thể tham khảo các kỹ thuật chọn gap qua link: https://en.wikipedia.org/wiki/
Shellsort#Gap_sequences. Hiện tại thì Shell sort sử dụng Hibbard’s gap có độ phức
tạp trung bình rất ấn tượng: O(N 1.25 ) (có lẽ đây là cách chọn gap tối ưu nhất).
Độ phức tạp không gian (mọi trường hợp): O(1)
1 void selectionSort ( vector < pair < int , int > >& v ) {
2 cout << " Running " +
string ( source_location :: current () . function_name () ) <<
Page 12/26
Các giải thuật sort trong C++
’\ n ’;
3 for ( int i = 0; i < v . size () ; i ++) {
4 auto it = min_element ( v . begin () + i + 1 , v . end () ,
[]( pair < int , int > a , pair < int , int > b ) { return a . second
< b . second ; }) ;
5 if ( it != v . end () && v [ i ]. second > it - > second ) {
6 swap ( v [ i ] , v [ it - v . begin () ]) ;
7 }
8 }
9 }
Do mình lười viết vòng lặp tìm min nên mình dùng tạm hàm min_element của thư
viện algorithm. Hàm min_element(first, last, comp = less) dùng để tìm phần tử
có thứ hạng cao nhất (được quy định bởi hàm comp, mặc định comp là hàm less của C++)
trong vector bắt đầu từ iterator first đến iterator last (không tính last).Mình đã set
hàm comp là hàm lambda để so sánh 2 pair bằng cách so sánh 2 thuộc tính second của
chúng.
Kiểm tra độ ổn định và thời gian chạy của giải thuật, ta được kết quả sau:
Page 13/26
Các giải thuật sort trong C++
Khi này, ta phải hiện thực thêm hàm merge để trộn 2 mảng con đã sắp xếp để thành
một mảng lớn hơn đã sắp xếp.
Hàm merge được hiện thực như sau:
1 void merge ( vector < pair < int , int > >& v , int left , int mid ,
int right ) {
2 // Assume that mid and right are the same as interator
end in vector , we do not include it in the range of sub
arrays ( mid in sub 1 and right in sub 2)
3 vector < pair < int , int > > sub1 ( v . begin () + left ,
v . begin () + mid ) , sub2 ( v . begin () + mid , v . begin () +
right ) ;
4 int i = 0 , j = 0 , k = left ;
5 while ( i < ( int ) sub1 . size () && j < ( int ) sub2 . size () ) {
6 if ( sub1 [ i ]. second <= sub2 [ j ]. second ) {
7 v [ k ] = sub1 [ i ];
8 i ++;
9 }
10 else {
11 v [ k ] = sub2 [ j ];
12 j ++;
13 }
14 k ++;
15 }
16 while ( i < ( int ) sub1 . size () ) {
17 v [ k ] = sub1 [ i ];
18 i ++;
19 k ++;
20 }
21 while ( j < ( int ) sub2 . size () ) {
22 v [ k ] = sub2 [ j ];
23 j ++;
24 k ++;
25 }
26 }
Hàm mergeSortHelper (không cần thiết, nhưng để thuận tiện để test thì mình hiện
thực hàm này) sử dụng đệ quy để chia mảng làm đôi, sau đó gọi hàm merge để gộp các
mảng con lại. Đoạn code tham khảo dưới đây:
Page 14/26
Các giải thuật sort trong C++
1 void mergeSortHelper ( vector < pair < int , int > >& v , int left ,
int right ) {
2 if ( right - left <= 1) {
3 return ;
4 }
5 int mid = left + ( right - left ) / 2;
6 mergeSortHelper (v , left , mid ) ;
7 mergeSortHelper (v , mid , right ) ;
8 merge (v , left , mid , right ) ;
9 }
Hàm mergeSort gọi hàm mergeSortHelper với left = 0 và right = kích thước của
mảng truyền vào:
1 void mergeSort ( vector < pair < int , int > >& v ) {
2 cout << " Running " +
string ( source_location :: current () . function_name () ) <<
’\ n ’;
3 mergeSortHelper (v , 0 , ( int ) v . size () ) ;
4 }
Độ phức tạp thời gian (mọi trường hợp): O(N logN ) (do cứ mỗi lần chúng ta chia đôi
mảng ra và tốn O(N ) để trộn tất cả các mảng con kích thước 1 lại).
Độ phức tạp không gian (mọi trường hợp): Khác với các giải thuật trước đó, Merge
sort cần O(N ) không gian để tạo các mảng con.
Với hàm này, việc merge được thực hiện tại chỗ, không sử dụng thêm mảng phụ nên
độ phức tạp không gian được rút xuống O(1). Đồng thời ta không cần hàm merge nữa.
Page 15/26
Các giải thuật sort trong C++
1 void m erg eS or tHe lp er STL ( vector < pair < int , int > >& v , int
left , int right ) {
2 if ( right - left <= 1) {
3 return ;
4 }
5 int mid = left + ( right - left ) / 2;
6 mergeSortHelper (v , left , mid ) ;
7 mergeSortHelper (v , mid , right ) ;
8 inplace_merge ( v . begin () , v . begin () + mid , v . begin () +
right , []( const pair < int , int >& a , const pair < int , int >&
b ) { return a . second < b . second ; }) ;
9 }
10
11 void mergeSortSTL ( vector < pair < int , int > >& v ) {
12 cout << " Running " +
string ( source_location :: current () . function_name () ) <<
’\ n ’;
13 me rg eS ort He lp erS TL (v , 0 , ( int ) v . size () ) ;
14 }
Hàm inplace_merge(first, mid, last, comp = less) sẽ gộp 2 mảng con đã được
sắp xếp tăng dần (tùy theo hàm comp): mảng 1 từ iterator first đến iterator mid (không
tính mid), mảng 2 từ iterator mid đến iterator last (không tính last) và việc merge này
sẽ thực hiện trên chính mảng từ iterator first đến last (không tính last).
Test thử giải thuật này, ta thu được kết quả:
Hình 9: Kết quả sau khi chạy hàm mergeSort có sử dụng thư viện algorithm
. Độ phức tạp thời gian (mọi trường hợp): O(N logN ) (do cứ mỗi lần chúng ta chia đôi
mảng ra và tốn O(N ) để trộn tất cả các mảng con kích thước 1 lại).
Độ phức tạp không gian (mọi trường hợp): O(1).
Page 16/26
Các giải thuật sort trong C++
1 int partition ( vector < pair < int , int > >& v , int low , int high ) ;
Chúng ta giả sử high không nằm trong khoảng đang xét, để truy cập phần tử cuối
cùng ta sẽ phải truy cập index high - 1.
Khi chạy hàm partition, pivot sẽ được đưa đến đúng vị trí của nó trong mảng đã
sắp xếp và vị trí đó sẽ được trả về để thực hiện chia mảng.
Với hàm quickSortHelper để gọi đệ quy, chúng ta hiện thực như sau:
1 void quickSortHelper ( vector < pair < int , int > >& v , int low ,
int high ) {
2 if ( high - left > 1) {
3 int par_idx = partition (v , low , high ) ;
4 quickSortHelper (v , low , par_idx ) ;
5 quickSortHelper (v , par_idx + 1 , high ) ;
6 }
7 }
Khi tìm ra par_idx, hàm sẽ gọi đệ quy để chia mảng ra làm 2 nửa và sort mỗi nửa
Page 17/26
Các giải thuật sort trong C++
mảng (chúng ta không cần ghép lại mảng do việc sắp xếp mỗi nửa mảng đều thực hiện
trên chính mảng gốc).
Với hàm quickSort để test, đơn giản là gọi hàm quickSortHelper với đối số low là 0,
đối số high là kích thước mảng:
1 void quickSort ( vector < pair < int , int > >& v ) {
2 cout << " Running " +
string ( source_location :: current () . function_name () ) <<
’\ n ’;
3 quickSortHelper (v , 0 , ( int ) v . size () ) ;
4 }
Trong bài viết này mình xin chia sẻ 4 phương pháp chọn pivot như sau:
Do chọn phần tử pivot ở cuối cho nên ta cần phải đưa pivot này về đúng vị trí của
nó bằng cách hoán vị trước các phần tử không phải pivot để thỏa mãn các phần tử lớn
hơn pivot ở cùng 1 bên, các phần tử nhỏ hơn pivot ở cùng 1 bên. Cuối cùng đưa pivot
vào vị trí để chia 2 phần mảng đó. Hơi khó hiểu đúng không? Chúng ta đọc đoạn code sau
để tiện theo dõi:
1 int partition ( vector < pair < int , int > >& v , int low , int high )
{
2 pair < int , int > pivot = v [ high - 1];
3 int i = low - 1;
4 for ( int j = low ; j <= high - 2; j ++) {
5 if ( v [ j ]. second < pivot . second ) {
6 i ++;
7 swap ( v [ i ] , v [ j ]) ;
8 }
9 }
10 swap ( v [ i + 1] , v [ high - 1]) ;
11 return i + 1;
12 }
Page 18/26
Các giải thuật sort trong C++
Hình 10: Kết quả khi chạy hàm quickSort với pivot ở cuối mảng
Thời gian chạy được cải thiện đáng kể nên mới có tên gọi là Quick sort.
Độ phức tạp về thời gian: O(N logN ) (trường hợp tốt nhất khi mảng luôn được chia
đều), O(N logN ) (trung bình), O(N 2 ) (trường hợp tệ nhất khi pivot sau khi partition luôn
ở cuối mảng hoặc đầu mảng, khi này những dãy như thế gọi là Quick sort killer sequence).
Độ phức tạp không gian (mọi trường hợp): O(1)
Với việc chọn pivot ở đầu, chúng ta cần thay đổi 1 chút hàm partition như sau:
1 int partition ( vector < pair < int , int > >& v , int low , int high )
{
2 pair < int , int > pivot = v [ low ];
3 int i = low ;
4 for ( int j = low + 1; j < high ; j ++) {
5 if ( v [ j ]. second < pivot . second ) {
6 i ++;
7 swap ( v [ i ] , v [ j ]) ;
8 }
9 }
10 swap ( v [ i ] , v [ low ]) ;
11 return i ;
12 }
Vì sao ở đây ta chỉ dùng i thay vì i + 1? Hãy tưởng tượng pivot là số lớn nhất mảng
xem, khi này i sẽ chạy đến high - 1, và v[i + 1] bị lỗi out_of_bound. Test thử chương
trình, ta được:
Page 19/26
Các giải thuật sort trong C++
Hình 11: Kết quả khi chạy hàm quickSort với pivot ở đầu mảng
Độ phức tạp: Cũng giống như việc chọn pivot ở cuối mảng
Ý tưởng của phương pháp này là chọn số trung vị trong 3 phần tử: đầu mảng, cuối
mảng và phần tử chính giữa mảng để làm pivot. Để cho dễ thì ta swap pivot với phần
tử ở cuối mảng xong chạy hàm partition là được.
Code tham khảo:
1 int pivotLast ( vector < pair < int , int > >& v , int low , int high )
{
2 pair < int , int > pivot = v [ high - 1];
3 int i = low - 1;
4 for ( int j = low ; j < high - 1; j ++) {
5 if ( v [ j ]. second < pivot . second ) {
6 i ++;
7 swap ( v [ i ] , v [ j ]) ;
8 }
9 }
10 swap ( v [ i + 1] , v [ high - 1]) ;
11 return i + 1;
12 }
13
14 int partition ( vector < pair < int , int > >& v , int low , int high )
{
15 int first = v [ low ]. second , last = v [ high - 1]. second ,
mid = v [( high + low ) /2]. second ;
16 int median = ( first > last ? ( mid > first ? low : ( mid
> last ? ( high + low ) /2 : high - 1) ) : ( mid > last ? high
- 1 : ( first < mid ? ( high + low ) /2 : low ) ) ) ;
17 swap ( v [ high - 1] , v [ median ]) ;
18 return pivotLast (v , low , high ) ;
19 }
Page 20/26
Các giải thuật sort trong C++
Hình 12: Kết quả khi chạy hàm quickSort sử dụng Median of Three làm pivot
Độ phức tạp: Giống với việc chọn pivot ở đầu hoặc cuối mảng. Những dãy khiến giải
thuật này rơi vào trường hợp tệ nhất gọi là Median of Three killer sequence.
Ta sẽ random chỉ số của index, swap nó với phần tử cuối cùng của mảng, và thực hiện
partition mảng như bình thường.
Code tham khảo (sử dụng mt19937 và uniform_int_distribution sẽ rất lâu cho nên
mình dùng tạm hàm rand của thư viện random):
1 int pivotLast ( vector < pair < int , int > >& v , int low , int high )
{
2 pair < int , int > pivot = v [ high - 1];
3 int i = low - 1;
4 for ( int j = low ; j < high - 1; j ++) {
5 if ( v [ j ]. second < pivot . second ) {
6 i ++;
7 swap ( v [ i ] , v [ j ]) ;
8 }
9 }
10 swap ( v [ i + 1] , v [ high - 1]) ;
11 return i + 1;
12 }
13
14 int partition ( vector < pair < int , int > >& v , int low , int high )
{
15 int idx = rand () % ( high - low ) + low ;
16 swap ( v [ high - 1] , v [ idx ]) ;
17 return pivotLast (v , low , high ) ;
18 }
Page 21/26
Các giải thuật sort trong C++
Hình 13: Kết quả khi chạy hàm quickSort khi chọn pivot random
Độ phức tạp: giống như việc chọn pivot ở đầu, cuối hay Median of Three, tuy nhiên sẽ
giảm thiểu được tình trạng gặp phải killer sequence
Heap là trường hợp đặc biệt của cây nhỉ phân cân bằng, trong đó khóa của mỗi node
được so sánh với con của nó. Nếu khóa của node ≥ khóa của các node con của nó thì ta
gọi là Max heap, ngược lại nếu khóa của node ≤ khóa của các node con của nó thì ta gọi
là Min heap.
Để biểu diễn heap, chúng ta có thể sử dụng Binary Tree hoặc mảng. Trong phạm vi
bài này, chúng ta sẽ sử dụng mảng:
2. Với mỗi node tại index i, node con của nó được đặt tại index 2 ∗ i + 1 và 2 ∗ i + 2.
Đối với heap (chúng ta sẽ lấy min heap làm ví dụ, max heap tương tự), chúng ta có
các thao tác chính như sau:
1. reheapUp: khi có 1 node có giá trị nhỏ hơn node cha của nó thì ta di chuyển lên trên
2. reheapDown: khi có 1 node có giá trị lớn hơn node cha của nó thì ta di chuyển xuống
dưới
3. heapify: thực hiện build heap từ 1 cây nhị phân (hoặc mảng)
Page 22/26
Các giải thuật sort trong C++
2. Loại bỏ node gốc (node lớn nhất khi sắp xếp mảng tăng dần) và đưa xuống cuối
mảng và thay thế node gốc bởi node cuối cùng của heap ban đầu.
3. Thực hiện heapify phần mảng còn lại (phần mảng đã bỏ phần tử cuối cùng) với gốc
mới để build lại heap
4. Lặp lại các bước trên cho đến khi mảng được sắp xếp.
1. Gọi root là tại index i, khi đó con trái của root có index là 2 * i + 1 và con phải
của root có index là 2 * i + 2.
2. Kiểm tra xem root sẽ là con trái hay con phải, hay giữ nguyên (nhớ kiểm tra các
con trái và con phải của root có tồn tại hay không) bằng cách so giá trị của root với
2 con xem node nào có giá trị lớn hơn.
3. Nếu root không còn ở index i nữa, ta swap 2 phần tử ở vị trí root và vị trí i, thực
hiện đệ quy lại heapify với index mới là root.
Toàn bộ giải thuật Heap sort được mô phỏng lại thông qua đoạn code sau:
1 void heapify ( vector < pair < int , int > >:: iterator first ,
vector < pair < int , int > >:: iterator last , int idx ) {
2 int root = idx , left = 2 * idx + 1 , right = 2 * idx + 2;
3 int len = last - first ;
4 if ( left < len && ( first + root ) -> second < ( first +
left ) -> second ) {
5 root = left ;
6 }
7 if ( right < len && ( first + root ) -> second < ( first +
right ) -> second ) {
8 root = right ;
9 }
10 if ( root != idx ) {
Page 23/26
Các giải thuật sort trong C++
16 void heapSort ( vector < pair < int , int > >& v ) {
17 cout << " Running " +
string ( source_location :: current () . function_name () ) <<
’\ n ’;
18 int N = ( int ) v . size () ;
19 for ( int i = N / 2 - 1; i >= 0; i - -) {
20 heapify ( v . begin () , v . end () , i ) ;
21 }
22 for ( int i = N - 1; i > 0; i - -) {
23 swap ( v [ i ] , v [0]) ;
24 heapify ( v . begin () , v . begin () + i , 0) ;
25 }
26 }
Hàm heapify nhận vào 2 iterator của vector (không tính last) và index để heapify.
Kết quả khi chạy giải thuật Heap sort:
Page 24/26
Các giải thuật sort trong C++
Hàm này nhận đầu vào là con trỏ mảng (không dùng được vector ở đây), số lượng phần
tử của mảng, kích thước của kiểu dữ liệu lúc khai báo mảng và hàm compare (bắt buộc
phải có dạng giống như con trỏ hàm trong cú pháp, có thể sử dụng lambda function).
Có thể xem qua ví dụ sau:
Hàm này sử dụng giải thuật Quick sort và nằm trong thư viện <cstdlib> (hoặc
<stdlib.h> trong C)
3.2 std::sort
Cú pháp: void sort(first, last, comp = less)
Hàm này nhận tham số đầu vào là 2 RandomAccessIterator từ first (tính first) đến
last (không tính last), việc sort như thế nào sẽ phụ thuộc vào hàm comp.
RandomAccessIterator là 1 iterator mà ta có thể truy cập được phần tử tại vị trí iterator
đó (con trỏ mảng hay iterator của vector đều là các RandomAccessIterator)
Có thể xem qua ví dụ sau:
Hàm sort đảm bảo độ phức tạp về thời gian của giải thuật luôn là O(N logN ) trong
mọi trường hợp do sử dụng giải thuật Intro sort. Intro sort là một Hybrid sort algorithm sử
dụng 3 thuật toán sort: Quick sort (dùng Median of Three pivot), Heap sort và Insertion
sort. Insertion sort được dùng khi mảng có kích thước bé (do người dùng chọn, thường là
nhỏ hơn 16 phần tử), Heap sort và Quick sort được chọn dựa trên depth limit (ban đầu
được khởi tạo bằng 2 lần log của kích thước mảng). Nếu depth limit bằng 0, chuyển sang
Heap sort, ngược lại sử dụng Quick sort. Hàm này ở trong thư viện <algorithm>.
Page 25/26
Các giải thuật sort trong C++
3.3 std::stable_sort
Cú pháp: stable_sort(first, last, comp = less)
Tương tự như hàm sort, stable_sort cũng nhận vào 2 RandomAccessIterator (tính
first nhưng không tính last) và ta có thể quy định hàm sẽ sort như thế nào thông qua
hàm comp. Hàm này là hàm sort ổn định, tức là nó bảo toàn thứ tự giữa các phần tử giống
nhau trong mảng ban đầu. Hàm này cũng thuộc thư viện <algorithm>.
3.4 std::sort_heap
Cú pháp: sort_heap(first, last, comp = less)
Tương tự như hàm sort và stable_sort, sort_heap cũng nhận vào 2 RandomAcces-
sIterator (tính first nhưng không tính last) và ta có thể quy định hàm sẽ sort như thế
nào thông qua hàm comp.
Tuy nhiên, để hàm này chạy đúng kết quả thì ta cần 1 hàm phụ trợ nữa của C++, đó
là hàm make_heap.
Cú pháp hàm make_heap: make_heap(first, last, comp = less).
Hàm make_heap cũng nhận vào 2 RandomAccessIterator first và last (tính first và
không tính last) dùng để build 1 container bất kì (mảng, vector) thành heap. Mặc định
khi không chỉ định hàm comp, hàm này sẽ build Max heap. 2 hàm này đều thuộc thư viện
<algorithm>.
4 Lời kết
Hi vọng là qua bài viết này, bạn đọc có thể nắm được các giải thuật sắp xếp được nêu
trong bài viết, hiểu được các tính chất của nó cũng như độ phức tạp của giải thuật, qua
đó có thể lựa chọn cho mình giải thuật phù hợp với mình. Đây cũng là bài viết cuối cùng
của mình cho môn DSA này, hẹn gặp lại mọi người trong các bài viết sau.
Page 26/26