You are on page 1of 92

CHƯƠNG 2: SẮP XẾP

Tuần 4
– Sắp xếp chọn, xen, nổi bọt
– Sắp xếp nhanh (QUICK SORT)

Bộ môn CÔNG NGHỆ PHẦN MỀM


Khoa Công nghệ Thông tin & Truyền thông
ĐẠI HỌC CẦN THƠ

1
Võ Huỳnh Trâm
Mục tiêu

Sau khi hoàn tất bài học này, sinh viên cần phải:
• Hiểu các giải thuật sắp xếp.
• Vận dụng được giải thuật để minh họa việc sắp xếp.
• Hiểu các lưu đồ của các giải thuật sắp xếp.
• Hiểu các chương trình sắp xếp.
• Hiểu được việc đánh giá các giải thuật.

2
Tầm quan trọng của bài toán sắp xếp

• Sắp xếp một danh sách các đối tượng theo một thứ
tự nào đó là một bài toán thường được vận dụng
trong các ứng dụng tin học.
• Sắp xếp là một yêu cầu không thể thiếu trong khi
thiết kế các phần mềm.
• Việc nghiên cứu các phương pháp sắp xếp là rất
cần thiết để vận dụng trong khi lập trình.
3
Sắp xếp trong và sắp xếp ngoài

• Sắp xếp trong là sắp xếp dữ liệu được tổ chức ở bộ nhớ trong của
máy tính.
• Đối tượng sắp xếp: các mẩu tin gồm một hoặc nhiều trường.
Có 1 trường khóa (key) với kiểu quan hệ thứ tự (kiểu số nguyên, số thực,
chuỗi ký tự...). Danh sách đối tượng sắp xếp là mảng các mẩu tin trên.
• Mục đích sắp xếp: tổ chức lại các mẩu tin sao cho khóa của chúng được
sắp thứ tự tương ứng với quy luật sắp xếp.
• Quy luật sắp xếp: mặc nhiên theo thứ tự không giảm.

• Sắp xếp ngoài là sắp xếp khi số lượng đối tượng cần sắp xếp lớn,
không thể lưu trữ trong bộ nhớ trong mà phải lưu trữ ở bộ nhớ ngoài.
4
Tổ chức dữ liệu và ngôn ngữ cài đặt

• Ví dụ minh họa được thể hiện trên ngôn ngữ C: Turbo C++
(Version 3.0)
• Sử dụng khai báo:
typedef int keytype;
typedef float othertype;
typedef struct recordtype {
keytype key;
othertype otherfields;
}; 5
Tổ chức dữ liệu và ngôn ngữ cài đặt (tt)

void Swap(recordtype &x, recordtype &y)


{
recordtype temp;
temp = x;
x = y;
y = temp;
}
• Lưu ý: thủ tục Swap tốn O(1) thời gian vì thực hiện 3 lệnh gán nối tiếp.
6
Các thuật toán Sắp xếp

• Các thuật toán sắp xếp đơn giản (độ phức tạp O(n 2)):
– Sắp xếp chọn (Selection Sort)
– Sắp xếp xen (Insertion Sort)
– Sắp xếp nổi bọt (Bubble Sort)

• Các thuật toán sắp xếp phức tạp (độ phức tạp O(n logn))
– Sắp xếp phân đoạn/nhanh (Quick Sort)
– Sắp xếp vun đống (Heap Sort)
– Trường hợp dữ liệu đặc biệt (Bin Sort) (độ phức tạp O(n))
7
Thuật toán sắp xếp chọn (Selection Sort)

Bài toán: Cho mảng A = {a[0], a[1], ..., a[n-1]}, hãy sắp xếp mảng theo
chiều không giảm.
Ý tưởng: Chọn phần tử nhỏ nhất trong n phần tử ban đầu, đưa phần tử
này về vị trí đúng là vị trí đầu tiên của mảng hiện hành. Sau đó không
quan tâm đến nó nữa, xem mảng hiện hành chỉ còn n-1 phần tử của mảng
ban đầu. Bắt đầu từ vị trí thứ 2, lặp lại quá trình trên cho mảng hiện hành
đến khi chỉ còn 1 phần tử.
Do mảng ban đầu có n phần tử, vậy tóm tắt ý tưởng thuật toán là thực
hiện n-1 lượt việc đưa phần tử nhỏ nhất trong mảng hiện hành về vị trí
đúng ở đầu mảng.

8
Thuật toán sắp xếp chọn (Selection Sort)

Giải thuật :
•Bước 1: chọn phần tử có khóa nhỏ nhất trong n phần tử từ
a[0] đến a[n-1] và hoán vị nó với phần tử a[0].
•Bước 2: chọn phần tử có khóa nhỏ nhất trong n-1 phần tử từ
a[1] đến a[n-1] và hoán vị nó với a[1].
•Tổng quát ở bước i: chọn phần tử có khoá nhỏ nhất trong n-
i phần tử từ a[i] đến a[n-1] và hoán vị nó với a[i].
•Bước n-1: mảng đã được sắp xếp.

9
Phương pháp chọn phần tử

• Đầu tiên, đặt khoá nhỏ nhất là khoá của a[i] : lowkey = a[i].key
và chỉ số của phần tử có khoá nhỏ nhất là i : lowindex = i
• Xét các phần tử a[j] (với j từ i+1 đến n-1), nếu khoá của a[j] <
khoá nhỏ nhất (a[j].key < lowkey) thì đặt lại khoá nhỏ nhất là
khoá của a[j] : lowkey = a[j].key và chỉ số phần tử có khoá nhỏ
nhất là j : lowindex = j
• Khi đã xét hết các a[j] (j>n-1) thì phần tử có khoá nhỏ nhất là
a[lowindex].
10
Ví dụ sắp xếp chọn

Khóa
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]
Bước
Ban đầu 5 6 2 2 10 12 9 10 9 3 Góc trên
Bước 1 2 6 5 2 10 12 9 10 9 3 bên phải
Bước 2 2 5 6 10 12 9 10 9 3
Bước 3 3 6 10 12 9 10 9 5
Bước 4 5 10 12 9 10 9 6
Bước 5 6 12 9 10 9 10
Bước 6 9 12 10 9 10
Bước 7 9 10 12 10
Bước 8 10 12 10
Bước 9 10 12
Kết quả 2 2 3 5 6 9 9 10 10 12
11
Begin
Số bước chọn
for(i=0; i<=n-2; i++) i=0

S
i<=n-2 End
Đ
lowindex = i
lowkey = a[i].key

j = i+1
Số phép so sánh
Lưu đồ thuật toán for(j=i+1; j<=n-1; j++)
sắp xếp chọn
S
j<=n-1

a[j].key<lowkey

Đ
S
lowindex = j
lowkey = a[j].key

j = j+1

swap(a[i],a[lowindex])

i = i+1
12
Chương trình sắp xếp chọn
void SelectionSort (recordtype a[ ], int n){
int i,j, lowindex;
keytype lowkey;
 T(n) = O(n2)
{1}. for(i=0; i<=n-2; i++){
{2}. lowkey = a[i].key;
{3}. lowindex = i;
{4}. for(j=i+1; j<=n-1; j++)
{5}. if(a[j].key < lowkey) {
{6}. lowkey = a[j].key;
{7}. lowindex = j;
}
{8}. Swap(a[i],a[lowindex]);
} 13
}
Đánh giá sắp xếp chọn

• Hàm Swap tốn O(1).


• Toàn bộ chương trình chỉ bao gồm vòng for {1}. Vòng for {1}
chứa các lệnh nối tiếp {2}, {3}, {4} và {8}, trong đó các lệnh
{2}, {3} và {8} đều tốn thời gian O(1).
• Lệnh {6} và {7} đều tốn O(1) nên lệnh {5} tốn O(1).
• Vòng lặp {4} thực hiện n-i-1 lần, vì j chạy từ i+1 đến n-1, mỗi
lần lấy O(1), nên lấy O(n-i-1) thời gian.
• Gọi T(n) là thời gian thực hiện của chương trình, thì T(n) là thời
gian thực hiện vòng for {1}. Mà lệnh {1} có i chạy từ 0 đến n-2
nên ta có:
n -2
n(n - 1)
T(n)   (n - i - 1)   O(n 2 )
i=0 2
14
Thuật toán sắp xếp xen
(Insertion Sort)
Bài toán: Cho mảng A = {a[0], a[1], ..., a[n-1]}, hãy sắp
xếp mảng theo chiều không giảm.
Ý tưởng: Bắt chước cách sắp xếp quân bài của những
người chơi bài. Muốn sắp một bộ bài theo trật tự, người
chơi bài rút lần lượt từ quân bài thứ 2, so với các quân
đứng trước nó để chèn vào vị trí thích hợp.
Xét mảng con gồm k phần tử đầu. Với i = 1, mảng gồm một phần tử
đã được sắp. Giả sử trong mảng i-1 phần tử đầu đã được sắp, để
sắp xếp một phần tử ta tìm vị trí thích hợp của nó trong mảng. Vị trí
thích hợp đó là đứng trước phần tử lớn hơn nó và sau phần tử nhỏ
hơn hoặc bằng nó.
15
Thuật toán sắp xếp xen
(Insertion Sort)
Giải thuật : Trước hết ta xem phần tử a[0] là một mảng đã
có thứ tự.
•Bước 1: xen phần tử a[1] vào danh sách đã có thứ tự a[0]
sao cho a[0], a[1] là một danh sách có thứ tự.
•Bước 2: xen phần tử a[2] vào danh sách đã có thứ tự a[0],
a[1] sao cho a[0], a[1], a[2] là một danh sách có thứ tự.
•Tổng quát bước i : xen phần tử a[i] vào danh sách đã có
thứ tự a[0], a[1], … a[i-1] sao cho a[0], a[1],.. a[i] là một
danh sách có thứ tự.
•Bước n-1: mảng đã được sắp thứ tự.
16
Phương pháp xen phần tử

• Phần tử đang xét a[j] sẽ được xen vào vị trí thích hợp trong
danh sách các phần tử đã được sắp trước đó a[0], a[1], .. , a[j-1]
• So sánh khoá của a[j] với khoá của a[j-1] đứng ngay trước nó.
• Nếu khoá của a[j] nhỏ hơn khoá của a[j-1] (a[i] < a[j-1]) thì
hoán đổi a[j-1] và a[j] cho nhau và tiếp tục so sánh khoá của
a[j-1] (lúc này a[j-1] chứa nội dung của a[j]) với khoá của a[j-
2] đứng ngay trước nó.

17
Ví dụ sắp xếp xen

Khóa a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]
Bước
Ban đầu 5 6 2 2 10 12 9 10 9 3
Bước 1 5 6
Bước 2 2 5 6
Bước 3 2 2 5 6
Bước 4 2 2 5 6 10
Bước 5 2 2 5 6 10 12
Bước 6 2 2 5 6 9 10 12
Bước 7 2 2 5 6 9 10 10 12
Góc dưới Bước 8 2 2 5 6 9 9 10 10 12
bên trái Bước 9 2 2 3 5 6 9 9 10 10 12
Kết quả 2 2 3 5 6 9 9 10 10 12
18
Số bước xen Begin

for(i=1; i<=n-1; i++)


i=1

S
i<=n-1 End

j=i
Lưu đồ thuật toán
sắp xếp xen
(j>0) and
(a[j].key < a[j-1].key)

Đ
S

swap(a[j],a[j-1])
j = j-1

19
i = i+1
Chương trình sắp xếp xen

void InsertionSort(recordtype a[], int n){


int i,j;
 T(n) = O(n2)
{1} for(i=1; i<=n-1; i++){
{2} j=i;
{3} while ((j>0)&&(a[j].key<a[j-1].key)) {
{4} Swap(a[j],a[j-1]);
{5} j--;
}
}
}
20
Đánh giá sắp xếp xen

• Các lệnh {4} và {5} đều tốn O(1).


• Vòng lặp {3}, trong trường hợp xấu nhất, chạy i lần
(j giảm từ i đến 1), mỗi lần tốn O(1) nên {3} tốn
O(i) thời gian.
• Lệnh {2} và {3} là hai lệnh nối tiếp, lệnh {2} tốn
O(1) nên cả hai lệnh này tốn thời gian max là O(i).
• Vòng lặp {1} có i chạy từ 1 đến n-1 nên ta có:
n -1
n(n - 1)
T(n)   i   O(n 2 )
i 1 2

21
Thuật toán sắp xếp “nổi bọt”
(Bubble Sort)

Bài toán: Cho mảng A = {a[0], a[1], ..., a[n-1]}, hãy sắp
xếp mảng theo chiều không giảm.
Ý tưởng: Xuất phát từ cuối (hoặc đầu) mảng, đổi chổ các
cặp phần tử kế cận để đưa phần tử nhỏ (lớn) hơn trong cặp
phần tử đó về vị trí đúng đầu (cuối) mảng hiện hành. Sau
đó sẽ không xét đến nó ở vị trí tiếp theo, do vậy ở lần xử lý
thứ i sẽ có vị trí đầu mảng là i. Lặp lại xử lý trên cho đến
khi không còn cặp phần tử nào để xét.

22
Thuật toán sắp xếp “nổi bọt”
(Bubble Sort)
Thuật toán:
•Bước 1: Xét các a[j] (j giảm từ n-1 đến 1), so sánh khoá
của a[j] với khoá của a[j-1]. Nếu a[j] < a[j-1] thì đổi a[j] và
a[j-1]. Sau bước này thì a[0] có khoá nhỏ nhất.
•Bước 2: Xét các a[j] (j giảm từ n-1 đến 2), so sánh khoá
của a[j] với khoá của a[j-1]. Nếu a[j] < a[j-1] thì đổi a[j] và
a[j-1]. Sau bước này thì a[1] có khoá nhỏ thứ 2.
•…
•Bước n-1: mảng đã được sắp thứ tự
23
Ví dụ sắp xếp “nổi bọt”

Khóa a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]
Bước
Ban đầu 5 6 2 2 10 12 9 10 9 3
Góc trên
Bước 1 2 5 6 2 3 10 12 9 10 9
bên phải
Bước 2 2 5 6 3 9 10 12 9 10
Bước 3 3 5 6 9 9 10 12 10
Bước 4 5 6 9 9 10 10 12
Bước 5 6 9 9 10 10 12
Bước 6 9 9 10 10 12
Bước 7 9 10 10 12
Bước 8 10 10 12
Bước 9 10 12
Kết quả 2 2 3 5 6 9 9 10 10 12 24
Số bước nổi bọt Begin

for(i=0; i<=n-2; i++)


i=0

S
i<=n-2 End

j = n-1
Số phép so sánh
Lưu đồ thuật toán
for(j=n-1; j>=i+1; j--)
sắp xếp nổi bọt j>= i+1
S

a[j].key < a[j-1].key

Đ S

swap(a[j],a[j-1])

j = j-1

i = i+1
25
Chương trình sắp xếp “nổi bọt”

void BubbleSort(recordtype a[ ], int n) {


int i,j;
 T(n) = O(n )
2

{1} for(i= 0; i<= n-2; i++)


{2} for(j=n-1;j>=i+1; j--)
{3} if (a[j].key < a[j-1].key)
{4} Swap(a[j],a[j-1]);
}
26
Đánh giá sắp xếp “nổi bọt”

• Vòng lặp {3} tốn O(1)


• Vòng lặp {2} thực hiện (n – i -1 ) bước (giảm từ
n – 1 đến i+1), mỗi lần tốn O(1) nên vòng lặp {2}
tốn O(n – i - 1) thời gian.
• Vòng lặp {1} có i chạy từ 0 đến n-2 nên ta có:
n -2
n(n - 1)
T(n)   (n - i - 1)   O(n 2 )
i=0 2

27
Đánh giá chung về các thuật toán
sắp xếp đơn giản T(n) = O(n2)
(1) Sắp xếp chọn (Selection Sort): for – for
- Ở lượt thứ i, bao giờ cũng cần (n-i) phép so sánh để xác định phần tử nhỏ
nhất  T(n) không phụ thuộc vào tình trạng của mảng ban đầu.
(2) Sắp xếp xen (Insetion Sort): for – while
- Số lượng phép so sánh và đổi chỗ xảy ra khác nhau trong mỗi vòng lặp while
 T(n) phụ thuộc vào tình trạng của mảng ban đầu.
(3) Sắp xếp nổi bọt (Bubble Sort): for – for
- Số lượng phép so sánh ở mỗi bước xác định theo số lần lặp trong vòng for
 T(n) không phụ thuộc vào tình trạng của mảng ban đầu.
- Các phần tử nhỏ được đưa về vị trí đúng rất chậm trong khi các phần tử lớn
lại được đưa về vị trí đúng rất nhanh. 28
Bài tập 1

Trong 3 thuật toán sắp xếp đơn giản (chọn, xen, nổi
bọt), thuật toán nào thực hiện sắp xếp nhanh nhất với
một mảng đã có thứ tự ? Giải thích tại sao ?

29
Bài tập 2

Thực hiện 3 thuật toán sắp xếp đơn giản (chọn, xen, nổi bọt) để minh
họa việc sắp xếp mảng 12 phần tử có khóa là các số nguyên sau:
Khóa
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10] a[11]
Bước

Ban đầu 50 10 143 0 10 120 90 10 90 140 20 30

Bước 1

Bước 2

Bước 3

…..

Kết quả 30
Các thuật toán Sắp xếp

• Các thuật toán sắp xếp đơn giản (độ phức tạp O(n 2)):
– Sắp xếp chọn (Selection Sort)
– Sắp xếp xen (Insertion Sort)
– Sắp xếp nổi bọt (Bubble Sort)

• Các thuật toán sắp xếp phức tạp (độ phức tạp O(n logn))
– Sắp xếp phân đoạn/nhanh (Quick Sort)
– Sắp xếp vun đống (Heap Sort)
– Trường hợp dữ liệu đặc biệt (Bin Sort) (độ phức tạp O(n))
31
Thuật toán QuickSort

• Phát triển bởi C.A.R. Hoare (1960) dựa trên nguyên tắc chia danh
sách cần sắp xếp thành 2 danh sách con.
• Khác với sắp xếp trộn (MergeSort: chia thành 2 danh sách con có
kích thước tương đối bằng nhau nhờ chỉ số đứng giữa),
QuickSort chia danh sách bằng cách so sánh từng phần tử của
danh sách với một phần tử được chọn gọi là phần tử chốt. Những
phần tử nhỏ hơn phần tử chốt được đưa về phía trước và nằm
trong danh sách con bên trái, các phần tử lớn hơn hoặc bằng chốt
được đưa về phía sau và thuộc danh sách con bên phải.
• Cứ tiếp tục chia như vậy tới khi các danh sách con đều có độ dài
bằng 1. Sắp xếp mảng con “bên trái” và mảng con “bên phải”. 32
Ý tưởng của QuickSort

• Ý tưởng của thuật toán này là "chia để trị“

• Bước 1: Chọn một phần tử khóa v làm phần tử chốt (pivot)


• Bước 2: Phân hoạch dãy a[0] .. a[n-1] thành hai mảng con "bên trái" và
"bên phải". Mảng con "bên trái" bao gồm các phần tử có khóa nhỏ hơn
chốt. Mảng con "bên phải" bao gồm các phần tử có khóa lớn hơn hoặc
bằng chốt.
• Bước 3: Sau khi phân hoạch thành 2 mảng, thực hiện lại bước 2 ở từng
mảng - Chọn pivot ở từng mảng và tiếp tục phân chia thành các mảng
nhỏ hơn, sắp xếp đến khi mảng được sắp xếp hoàn toàn (mảng chỉ gồm 1
phần tử hoặc gồm nhiều phần tử có khóa bằng nhau)
33
Phương pháp chọn chốt

Chọn giá trị khóa lớn nhất trong 2 phần tử có khóa khác nhau
đầu tiên kể từ trái qua.
•Nếu mảng chỉ gồm 1 phần tử hay gồm nhiều phần tử có khóa bằng
nhau thì không có chốt.
•Ví dụ: Chọn chốt trong các mảng sau
– Cho mảng gồm các phần tử có khoá là 6, 6, 5, 8, 7, 4, ta chọn chốt là 6
(khoá của phần tử đầu tiên).
– Cho mảng gồm các phần tử có khoá là 6, 6, 7, 5, 7, 4, ta chọn chốt là 7
(khoá của phần tử thứ 3).
– Cho mảng gồm các phần tử có khoá là 6, 6, 6, 6, 6, 6 thì không có chốt
(vì các phần tử có khoá bằng nhau).
– Cho mảng gồm 1 phần tử có khoá là 6 thì không có chốt (vì chỉ có 1 phần tử).
34
Phương pháp phân hoạch

• Ðể phân hoạch mảng: dùng 2 "con nháy" L và R, trong đó L đi


từ bên trái và R đi từ bên phải.
• Cho L chạy sang phải tới khi gặp phần tử có khóa ≥ chốt
• Cho R chạy sang trái tới khi gặp phần tử có khóa < chốt
• Tại chỗ dừng của L và R: nếu L < R thì hoán vị a[L], a[R].
• Lặp lại quá trình dịch sang phải, sang trái của 2 "con nháy" L và
R cho đến khi L > R.
• Khi đó L sẽ là điểm phân hoạch, cụ thể là a[L] là phần tử đầu
tiên của mảng con “bên phải”.

35
L< v Ví dụ về phân hoạch R ≥ v
L=0 R=9

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 8 2 10 5 12 8 1 15 4

Chốt p = 8

36
L< v Ví dụ về phân hoạch R ≥ v
L=1 R=9

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 8 2 10 5 12 8 1 15 4

Chốt p = 8

37
L< v Ví dụ về phân hoạch R ≥ v
L=1 R=9

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 10 5 12 8 1 15 8

Chốt p = 8

38
L< v Ví dụ về phân hoạch R ≥ v
L=2 R=9

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 10 5 12 8 1 15 8

Chốt p = 8

39
L< v Ví dụ về phân hoạch R ≥ v
L=3 R=9

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 10 5 12 8 1 15 8

Chốt p = 8

40
L< v Ví dụ về phân hoạch R ≥ v
L=3 R=8

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 10 5 12 8 1 15 8

Chốt p = 8

41
L< v Ví dụ về phân hoạch R ≥ v
L=3 R=7

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 10 5 12 8 1 15 8

Chốt p = 8

42
L< v Ví dụ về phân hoạch R ≥ v
L=3 R=7

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 1 5 12 8 10 15 8

Chốt p = 8

43
L< v Ví dụ về phân hoạch R ≥ v
L=4 R=7

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 1 5 12 8 10 15 8

Chốt p = 8

44
L< v Ví dụ về phân hoạch R ≥ v
L=5 R=7

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 1 5 12 8 10 15 8

Chốt p = 8

45
L< v Ví dụ về phân hoạch R ≥ v
L=5 R=6

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 1 5 12 8 10 15 8

Chốt p = 8

46
L< v Ví dụ về phân hoạch R ≥ v
L=5 R=5

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 1 5 12 8 10 15 8

Chốt p = 8

47
L< v Ví dụ về phân hoạch R ≥ v
R=4 L=5

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 1 5 12 8 10 15 8

Chốt p = 8

48
L< v Ví dụ về phân hoạch R ≥ v
R=4 L=5

Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 4 2 1 5 12 8 10 15 8

Chốt p = 8

Điểm phân hoạch L =5

0 1 2 3 4 5 6 7 8 9
5 4 2 1 5 12 8 10 15 8

Mảng con “bên trái” Mảng con “bên phải”

49
Giải thuật QuickSort

• Ðể sắp xếp mảng a[i]..a[j] ta làm các bước sau:


– Xác định chốt trong mảng a[i]..a[j],
– Phân hoạch mảng a[i]..a[j] đã cho thành hai mảng
con a[i]..a[k-1] và a[k]..a[j].
– Sắp xếp mảng a[i]..a[k-1] (Ðệ quy).
– Sắp xếp mảng a[k]..a[j] (Ðệ quy).
• Đệ quy sẽ dừng khi không còn tìm thấy chốt.

50
L< v Ví dụ về QuickSort R≥v
Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 8 2 10 5 12 8 1 15 4
Chốt p = 8
5 4 2 1 5 12 8 10 15 8
1 5 8 12
Chốt p = 5 Chốt p = 12

1 4 2 5 5 8 8 10 15 12
2 4 12 15
Chốt p = 4 xong Chốt p = 10 Chốt p = 15

1 2 4 8 8 10 12 15

Chốt p = 2 xong xong xong xong xong

1 2
51
xong xong
Begin

Chỉ số dò
tìm từ i+1 i, j

k = i+1
firstkey = a[i].key

Khóa phần
tử đầu tiên
(k<=j) and
(a[k].key == firstkey

Lưu đồ Đ
S

hàm FindPivot k = k+1

Đ
Không tìm k>j
thấy chốt S

return -1 a[k].key>firstkey
S Đ

Trả về khóa lớn return i return k


hơn trong 2 phần tử
khac nhau đầu tiên
52
End
Chương trình hàm FindPivot

int FindPivot(recordtype a[ ], int i,int j) {


keytype firstkey;
int k ;
{1}. k = i+1;
{2}. firstkey = a[i].key;
{3}. while ( (k <= j) && (a[k].key == firstkey) ) k++;  T(n) = O(n)
{4}. if (k > j) return -1;
else
{5}. if (a[k].key>firstkey) return k; else return i;

}
53
Độ phức tạp của hàm FindPivot

int FindPivot(recordtype a[], int i,int j) • {1}, {2}, {3} và {4} nối tiếp
{ keytype firstkey; nhau.
int k ; • Lệnh While tốn nhiều thời
{1} k = i+1; gian nhất.
{2} firstkey = a[i].key; • Trong trường hợp xấu nhất
(không tìm thấy chốt): k chạy
{3} while ( (k <= j) && (a[k].key từ i+1 đến j, tức vòng lặp
== firstkey) ) k++; thực hiện j-i lần, mỗi lần
{4} if (k > j) return -1; O(1), do đó tốn O(j-i).
else • Đặc biệt khi i=0 và j=n-1, thì
{5} if (a[k].key>firstkey) return k; thời gian thực hiện là n-1 hay
else return i; T(n) = O(n).
}
54
Begin

i, j, pivot

L = i; R = j

S
L <= R

Lưu đồ a[L].key < pivot


S Chỉ số đầu
hàm Partition L = L+1
Đ
tiên của mảng
“bên phải”

a[R].key >= pivot

S Đ
L<R R = R-1
S
Đ
return L
Swap(a[L], a[R])

End 55
Hàm Partition

int Partition(recordtype a[], int i,int j, keytype pivot)


{ int L,R;
{1} L = i;
{2} R = j;
{3} while (L <= R) {  T(n) = O(n)
{4} while (a[L].key < pivot) L++;
{5} while (a[R].key >= pivot) R--;
{6} if (L<R) Swap(a[L],a[R]);
}
{7} return L; /*Tra ve diem phan hoach*/
}
56
Phân tích hàm Partition

int Partition(recordtype a[], int i, int j, • {1}, {2}, {3} và {7} nối tiếp nhau
keytype pivot) • Thời gian thực hiện của {3} lớn nhất.
{ int L,R; • Các lệnh {4}, {5} và {6} là thân của
{1} L = i; lệnh {3}, trong đó lệnh {6} lấy O(1).
• Lệnh {4} và lệnh {5} thực hiện việc di
{2} R = j; chuyển L sang phải và R sang trái cho
{3} while (L <= R) đến khi L và R gặp nhau, thực chất là
{4} { while (a[L].key < pivot) L++; duyệt các phần tử mảng, mỗi phần tử
một lần, mỗi lần tốn O(1) thời gian.
{5} while (a[R].key >= pivot) Tổng việc duyệt tốn O(j-i) thời gian.
R--; • Vòng lặp {3} thực chất để xét xem khi
{6} if (L<R) Swap(a[L],a[R]); nào thì duyệt xong, do đó thời gian
} thực hiện của {3} chính là thời gian
thực hiện của lệnh {4} và {5}, là O(j-i).
{7} return L;
• Khi i=0 và j=n-1, ta có: T(n) = O(n).
} 57
Hàm QuickSort
Chỉ số 0 1 2 3 4 5 6 7 8 9
Khoá 5 8 2 10 5 12 8 1 15 4

void QuickSort(recordtype a[], int i,int j)


{ keytype pivot; pivot: Biến giữ
int pivotindex, k; giá trị chốt
pivotindex = FindPivot(a,i,j);
if (pivotindex != -1) { k : Biến giữ giá trị
pivot = a[pivotindex].key; điểm phân hoạch
k = Partition(a,i,j,pivot);
QuickSort(a,i,k-1);
QuickSort(a,k,j);
}
58
}
Đánh giá QuickSort

 Trường hợp xấu nhất : Phân hoạch lệch


Phương trình đệ quy :
 1 nêu n = 1
 T(n) = O(n 2
)
T(n)  
 T(n - 1) + T(1) + n nêu n > 1
 Trường hợp trung bình  tốt nhất : Phân hoạch đều
Phương trình đệ quy :

 1 n nêu n  1
T(n)  
 T(n) = O(nlogn)
 2T( )  n nêu n  1
 2 59
Bài tập 3

Minh họa việc sắp xếp mảng gồm 12 phần tử có khóa là các
số nguyên sau bằng cách sử dụng thuật toán QuickSort:

Khóa
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10] a[11]
Bước

Ban đầu 50 10 143 0 10 120 90 10 90 140 20 30

60
Bài tập 4

Có một biến thể của QuickSort như sau: Chọn chốt là khóa của phần tử nhỏ
nhất trong hai phần tử có khóa khác nhau đầu tiên.
- Mảng con bên trái gồm các phần tử có khóa nhỏ hơn hoặc bằng chốt (≤ v)
- Mảng con bên phải gồm các phần tử có khóa lớn hơn chốt (> v)
Hãy viết lại các thủ tục cần thiết cho biến thể này và minh họa bằng việc sắp
xếp mảng trên.

Khóa
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10] a[11]
Bước
Ban đầu 50 10 143 0 10 120 90 10 90 140 20 30
61
CHƯƠNG 2: SẮP XẾP

Tuần 5 – Sắp xếp vun đống (HEAP SORT)

Bộ môn CÔNG NGHỆ PHẦN MỀM


Khoa Công nghệ Thông tin & Truyền thông
ĐẠI HỌC CẦN THƠ

62
Võ Huỳnh Trâm
Các thuật toán Sắp xếp

• Các thuật toán sắp xếp đơn giản (độ phức tạp O(n2)):
– Sắp xếp chọn (Selection Sort)
– Sắp xếp xen (Insertion Sort)
– Sắp xếp nổi bọt (Bubble Sort)

• Các thuật toán sắp xếp phức tạp (độ phức tạp O(n logn))
– Sắp xếp phân đoạn/nhanh (Quick Sort)
– Sắp xếp vun đống (Heap Sort)
– Trường hợp dữ liệu đặc biệt (Bin Sort) (độ phức tạp O(n))

63
HeapSort: Ý tưởng
0 1 2 ... n-1

n n-1 n-2
0 1 2 ... n-2 n-1

64
HeapSort: Ðịnh nghĩa Heap

• Cây nhị phân


HEAP = Cây sắp thứ tự bộ phận
(Đống) • Giá trị nút (khác nút lá)
không lớn hơn giá trị
các nút con của nó.
• Nhận xét:
- Nút gốc của cây sắp thứ tự bộ phận có giá trị nhỏ nhất.
- Đây không phải là cây tìm kiếm nhị phân.
65
Ví dụ về Heap

• Cây nhị phân


giá trị nhỏ nhất 2
• Trị nút trong không
lớn hơn trị nút con
3 6

5 9 7

7 6 9

66
HeapSort : Thuật toán

(1) Xem mảng là cây nhị phân:


- a[0] là nút gốc
- Nút trong a[i] có con trái a[2i+1] và con phải a[2i+2]
- Nút trong từ a[0],…, a[(n-2)/2] đều có 2 con
(trừ nút a[(n-2)/2] có thể chỉ có 1 con trái khi n chẵn)
(2) Sắp xếp cây thành Heap: dùng thủ tục PushDown
(3) Hoán đổi nút gốc a[0] cho nút lá cuối
(4) Sắp xếp lại cây sau khi đã bỏ đi nút lá cuối thành Heap mới dùng PushDown
Lặp lại (3) và (4) cho đến khi cây chỉ còn 2 nút.
2 nút này cùng các nút lá đã bỏ đi tạo thành mảng theo thứ tự giảm dần.
67
Dựng cây nhị phân từ mảng

n chẵn 0
(n=10)
Nút trong 1
Nút trong : a[0]  a[(n-2)/2] 2

(a[0]  a[(10-2)/2] = a[4])


3
 a[4] chỉ có 1 con trái 4 5 6

Nút lá 7 8 9
68
Dựng cây nhị phân từ mảng

n lẻ 0
(n =11)
Nút trong 1
Nút trong : a[0]  a[(n-2)/2] 2

(a[0]  a[(11-2)/2] = a[4])

 a[4] có đủ 2 con 3 4 5 6

Nút lá 7 8 9 10
69
Ví dụ: Dựng cây từ mảng
Chỉ số a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]

Khóa 5 6 2 2 10 12 9 10 9 3

- a[0] là nút gốc 5 0


- a[i] có con trái a[2i+1] và con
phải a[2i+2]. 6 1 2 2

2 3 10 4 12 5 9 6
-Nút trong: [0]  a[(n-2)/2]
(n chẵn: nút a[(n-2)/2] có 1 con trái)
10 7 9 8 3 9
PushDown: Tạo Heap từ cây

• PushDown nhận 2 tham số first và last để đẩy nút first xuống.


• Giả sử cây a[first],..,a[last] đã đúng vị trí của một Heap trừ a[first]:
PushDown sẽ đẩy a[first] xuống đúng vị trí của nó trong cây.
• Các khả năng có thể của a[first]:
(1) Nếu a[first] chỉ có một con trái và khoá > khoá con trái: hoán đổi
a[first] cho con trái của nó và kết thúc.
(2) Nếu a[first] có khoá > khóa con trái và khoá con trái ≤ khoá con
phải: hoán đổi a[first] cho con trái của nó  con trái có thể không đúng
vị trí nên phải xem xét lại con trái để có thể đẩy xuống.
(3) Ngược lại, nếu a[first] có khoá > khoá con phải và khoá con phải <
khoá con trái : hoán đổi a[first] cho con phải của nó  con phải có thể
không đúng vị trí nên phải xem xét lại con phải để có thể đẩy xuống.
Nếu các trường hợp trên không xảy ra : a[first] đã đúng vị trí. 71
PushDown: Trường hợp 1

(1)
(1) Nếu a[first] chỉ có
1 con trái (là nút lá) và a[f] > L
a[f]
khoá > khoá con trái:
Hoán đổi a[first] cho con
L
trái của nó và kết thúc.
72
PushDown: Trường hợp 2

(2) Nếu a[first] có (2)


khoá > khóa con trái và a[f] > L
khoá con trái ≤ khoá con phải:
a[f]
-Hoán đổi a[first] cho con trái
của nó
 con trái có thể không đúng vị L ≤ R
trí nên phải xem xét lại con trái
để đẩy xuống (PushDown L) 73
PushDown: Trường hợp 3

(3) Nếu a[first] có khoá (3)


> khoá con phải và khoá con
a[f] > R
phải < khoá con trái:
a[f]
-Hoán đổi a[first] cho con phải
của nó
 con phải có thể không đúng vị L > R
trí nên phải xem xét lại con phải
để đẩy xuống (PushDown R) 74
3 trường hợp PushDown: Tạo Heap từ cây

(1) (2) (3)


a[f] > L a[f] > L a[f] > R
a[f] a[f] a[f]

L L ≤ R L > R

75
Begin a, first, last
r = first

r <= (last-1)/2 End

Lưu đồ Đ
S

thủ tục PushDown


last==2*r+1
S
Đ
a[r].key > a[last].key
Đ S
Trường hợp a[first] chỉ
có 1 con trái và khóa > swap(a[r],a[last])
khóa con trái
r = last

a[r].key > a[2*r+1].key


and
Trường hợp a[2*r+1].key <= a[2*r+2].key
S
khóa a[first] > khóa con trái và
khóa con trái ≤ khóa con phải Đ

swap(a[r], a[2*r+1])
r = 2*r+1

a[r].key > a[2*r+2].key


Trường hợp and
a[2*r+2].key < a[2*r+1].key
khóa a[first] > khóa con phải S
và khóa con phải < khóa con Đ
trái
swap(a[r], a[2*r+2]) r = last
76
r = 2*r+2
Cài đặt thủ tục Pushdown
void PushDown(recordtype a[ ], int first,int last)  T(n) = O(logn)
{ int r= first;
while (r <= (last-1)/2)
if (last == 2*r+1) {
if (a[r].key > a[last].key) Swap(a[r],a[last]);
r = last;
} else
if ((a[r].key>a[2*r+1].key) && (a[2*r+1].key<=a[2*r+2].key))
{
Swap(a[r],a[2*r+1]);
r = 2*r+1 ;
} else
if ((a[r].key>a[2*r+2].key) && (a[2*r+2].key<a[2*r+1].key))
{
Swap(a[r],a[2*r+2]);
r = 2*r+2 ;
}
else
r = last;
} 77
Phân tích thủ tục PushDown

• Xét PushDown(0, n-1) : PushDown trên cây n nút.


• PushDown chỉ duyệt trên một nhánh nào đó của cây nhị phân, tức là
sau mỗi lần lặp thì số nút còn lại một nửa.
- Ban đầu PushDown trên cây có n nút;
- Sau lần lặp thứ nhất: PushDown trên cây có n/2 nút;
- Sau lần lặp thứ hai: PushDown trên cây có n/4 nút;…
Tổng quát, sau lần lặp thứ i: PushDown trên cây có n/2i nút.
• Trường hợp xấu nhất (luôn phải thực hiện việc đẩy xuống): while
thực hiện i lần sao cho n/2i = 1, tức i=logn. Mỗi lần lặp chỉ thực hiện
lệnh IF với thân là lời gọi Swap và lệnh gán, do đó tốn O(1) = 1.
• PushDown lấy O(logn) thời gian để đẩy xuống 1 nút trong cây n nút.
78
Thủ tục HeapSort

(1) Sắp xếp cây ban đầu thành Heap dùng thủ tục PushDown
Khởi đầu từ nút a[(n-2)/2], lần ngược tới nút gốc a[0]
(2) Hoán đổi nút gốc a[0] cho a[i].
(3) Sắp xếp cây a[0] .. a[i – 1] thành Heap dùng thủ tục
PushDown
Lặp lại (2) và (3) cho i chạy từ n – 1 đến 2
(4) Cuối cùng chỉ cần hoán đổi a[0] với a[1]
79
Chương trình hàm HeapSort

void HeapSort(recordtype a[], int n)


{ int i;
{1} for (i = (n-2)/2; i>=0; i--)
{2} PushDown(a, i, n-1);
{3} for (i = n-1; i>=2; i--) {  T(n) = O(nlogn)
{4} Swap(a[0],a[i]);
{5} PushDown(a, 0, i-1);
}
{6} Swap(a[0],a[1]);
} 80
Phân tích HeapSort

• Hàm PushDown lấy O(logn).


• Trong HeapSort,
– Vòng lặp {1}-{2} lặp (n-2)/2+1 lần, mỗi lần tốn
O(logn) nên thời gian thực hiện {1}-{2} là O(n
logn).
– Vòng lặp {3}-{5} lặp n-2 lần, mỗi lần tốn O(logn)
nên thời gian thực hiện của {3}-{5} là O(n logn).
• Thời gian thực hiện HeapSort là O(nlogn).

81
Ví dụ HeapSort: Mô hình cây

Trình bày việc sắp xếp mảng gồm 10 phần tử có khóa là các số
nguyên sau bằng cách sử dụng thuật toán HeapSort (Sắp thứ
tự giảm, sử dụng mô hình cây):

Chỉ số a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]

Khóa 5 6 2 2 10 12 9 10 9 3

82
Ví dụ: Dựng cây từ mảng
Chỉ số a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]

Khóa 5 6 2 2 10 12 9 10 9 3

5 0

6 1 2 2

2 3 10 4 12 5 9 6

10 7 9 8 3 9 83
Chương trình hàm HeapSort

void HeapSort(recordtype a[], int n)


{ int i;
{1} for (i = (n-2)/2; i>=0; i--)
{2} PushDown(a, i, n-1);
{3} for (i = n-1; i>=2; i--) {  T(n) = O(nlogn)
{4} Swap(a[0],a[i]);
{5} PushDown(a, 0, i-1);
}
{6} Swap(a[0],a[1]);
} 84
Chương trình hàm HeapSort

void HeapSort(recordtype a[], int n)


{ int i;
{1} for (i = (n-2)/2; i>=0; i--)
{2} PushDown(a, i, n-1);
{3} for (i = n-1; i>=2; i--) {  T(n) = O(nlogn)
{4} Swap(a[0],a[i]);
{5} PushDown(a, 0, i-1);
}
{6} Swap(a[0],a[1]);
} 85
Ví dụ HeapSort: Mô hình cây

Tiếp tục quá trình trên sẽ được một mảng có thứ tự giảm:

Chỉ số a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]

Ban đầu 5 6 2 2 10 12 9 10 9 3
Kết quả 12 10 10 9 9 6 5 3 2 2

86
Ví dụ HeapSort: Mô hình mảng

Khóa
Bước a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]

Ban đầu 5 6 2 2 10 12 9 10 9 3
Tạo Heap

i=9

i=8
i=7
i=6

87
Chương trình hàm HeapSort

void HeapSort(recordtype a[], int n)


{ int i;
{1} for (i = (n-2)/2; i>=0; i--)
{2} PushDown(a, i, n-1);
{3} for (i = n-1; i>=2; i--) {  T(n) = O(nlogn)
{4} Swap(a[0],a[i]);
{5} PushDown(a, 0, i-1);
}
{6} Swap(a[0],a[1]);
} 88
Chương trình hàm HeapSort

void HeapSort(recordtype a[], int n)


{ int i;
{1} for (i = (n-2)/2; i>=0; i--)
{2} PushDown(a, i, n-1);
{3} for (i = n-1; i>=2; i--) {  T(n) = O(nlogn)
{4} Swap(a[0],a[i]);
{5} PushDown(a, 0, i-1);
}
{6} Swap(a[0],a[1]);
} 89
HeapSort: Trình bày bằng mảng

Khóa
Bước a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]

Ban đầu 5 6 2 2 10 12 9 10 9 3
2 253 6 35 10
Tạo Heap 2 3 2 6 5 12 9 10 9 10
10 2 10 9 10 2
i=9 2 3 9 6 5 12 10 10 9 2
9 3 9 5 9 2
i=8 3 5 9 6 9 12 10 10 2
10 5 10 6 10 3
i=7 5 6 9 10 9 12 10 3
10 6 10 9 10 5
i=6 6 9 9 10 10 12 5
12 9 12 10 12 6

90
HeapSort: Trình bày bằng mảng

Khóa
Bước a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]

i=6 6 9 9 10 10 12 5
12 9 12 10 12 6

i=5 9 10 9 12 10 6
10 9 10 9

i=4 9 10 10 12 9
12 10 12 9

i=3 10 12 10 9
10 10

i=2 10 12 10
12 10
Kết quả 12 10 10 9 9 6 5 3 2 2 91
Bài tập 5

Minh họa việc sắp xếp mảng gồm 12 phần tử có khóa là các
số nguyên sau bằng cách sử dụng thuật toán HeapSort
(Sắp thứ tự giảm, trình bày bằng mảng):

Khóa
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10] a[11]
Bước

Ban đầu 50 10 143 0 10 120 90 10 90 140 20 30

92

You might also like