You are on page 1of 32

THUẬT TOÁN

THUẬT TOÁN TRÊN CẤU TRÚC CÂY

C programming. 2003 - 2005 1 C programming. 2003 - 2005 2


Giới thiệu Tuy nhiên, một thuật toán tồi dù có chạy trên một máy tính cực
nhanh nhưng vẫn có thể xử lý bài toán chậm hơn so với một thuật
toán tốt chạy trên máy Abacus.
Thuật toán - Thuật giải (Algorithm) là phương pháp để giải một bài
toán.
Nghiên cứu thuật toán là vấn đề luôn tồn tại từ xưa đến nay.
Cấu trúc dữ liệu (Data structure): cách lưu trữ thông tin.

Các thuật toán hiệu quả dùng những cấu trúc dữ liệu được tổ chức Phân tích thuật toán
tốt.

Người ta so sánh các thuật toán dựa trên các phép ước lượng chi
Trong chương trình, chúng ta sẽ nghiên cứu các thuật toán sau: phí (thời gian chạy của thuật toán, lượng bộ nhớ mà thuật toán
- sắp xếp (sorting) phải dùng) cho một thuật toán khi áp dụng thuật toán vào một bài
- tìm kiếm (searching) toán cụ thể.
- ... Luôn phải tinh chỉnh và kiểm tra các ước lượng để có được thuật
toán tốt nhất.

Sử dụng máy tính trong công việc, con người luôn mong muốn
Đối với một thuật toán, chúng ta thường quan tâm đến các yếu tố
- máy tính chạy càng ngày càng nhanh hơn sau.
- xử lý được nhiều dữ liệu hơn Kích thước, dung lượng của dữ liệu đầu vào: N
- có thể giải quyết được những vấn đề tưởng chừng như không Thời gian thực hiện. Thường tỉ lệ với:
thể giải quyết được.
1
log N
Công nghệ máy tính chỉ nâng cao được các thứ theo một hệ số cố
định. N
Hãy suy nghĩ N log N
Một thuật toán được thiết kế cẩn thận có thể thực hiện được mức N2
độ cải tiến lớn hơn nhiều: 2N
- Abacus
- Bàn tính (Slide Rule) Đôi khi chúng ta cũng gặp các tỉ lệ khác như:
- Máy tính (Calculator) log log N (log(log(N))
- Máy vi tính (Computer) log* N số các log cho đến khi đạt đến 1
- Máy siêu tính (Supercomputer)
Thường thì người ta sẽ viết ra các công thức ước lượng thời gian
chạy trong các trường hợp:
C programming. 2003 - 2005 3 C programming. 2003 - 2005 4
- xấu nhất (đây là trường hợp đảm bảo thuật toán không vượt Biểu Diễn Xếp Chồng (Stack) Bằng Cấu Trúc DSLK
qua ngưỡng này)
- trung bình (mang tính ước lượng) Các thao tác trên Stack đã được giới thiệu trong phần trước.

Việc phân tích phụ thuộc vào các thông tin chi tiết trên Chúng ta sẽ cài đặt Stack bằng DSLK
- chi phí của các thao tác cơ sở trong quá trình xử lý
- thuộc tính của dữ liệu đầu vào.

Con trỏ đến nút đầu tiên trong danh sách


các phần tử của stack
Để phân tích độ phức tạp của thuật toán, người ta sử dụng ký pháp
chữ O lớn (big O-notation). Số lượng phần tử trong danh sách (# số
phần tử của Stack)
T(N) được gọi là có mức độ phức tạp O(F(N)) nếu tồn tại một giá trị
c (hằng) và n0 , sao cho với mọi N > n0 ta có:
typedef struct intListNode * IntListPtr;
T(N) ≤ c * F(N)
typedef struct intListNode
T(N) là độ phức tạp chính xác của một thuật toán được đề nghị để { int data;
IntListPtr next;
giải bài toán có kích thước N. F(N) là giới hạn trên, nghĩa là các } IntListNode;
thời gian/không gian hay nói chung là các chi phí cho bài toán có
kích thước N không vượt qua mức F(N). typedef struct intStack
Trên thực tế, bao giờ người ta cũng muốn tính được giá trị F(N) { IntListPtr top;
unsigned size;
nhỏ nhất – chi phí nhỏ nhất phải có. } IntStack;

int MakeStack (IntStack * ps)


Ví dụ: T(N) = 3 * N2 + 5. {
Dễ thấy rằng, với c = 4 và n0 = 2, ta có: ps->top = NULL;
ps->size = 0;
3 * N 2 + 5 ≤ 4 * N2 return 1; /* success */
nghĩa là T(N) là O(N2). }

int IsEmptyStack (IntStack * ps)


{
return (ps->top == NULL);
}

int PushStack (IntStack * ps, int num)


{
C programming. 2003 - 2005 5 C programming. 2003 - 2005 6
IntListPtr new; *pi = ps->top->data;
new = malloc (sizeof(IntListNode)); temp = ps->top;
if (new == NULL) ps->top = ps->top->next;
{ free (temp);
return 0; /* FAILURE */ ps->size--;
} return 1; /* success */
new->data = num; }
new->next = ps->top;
ps->top = new;
ps->size++; Sử dụng Stack
return 1; /* success */
}
#include “intStack.h”
/* Cach 1 */ ...
int PopStack (IntStack * ps) int main (void)
{ {
IntListPtr temp; IntStack s;

if (ps->top == NULL) MakeStack(s);


{
return 0; /* FAILURE */ PushStack(&s, 1);
} PushStack(&s, 1);
temp = ps->top; PushStack(&s, 1);
ps->top = ps->top->next;
free (temp); while(!IsEmptyStack(&s))
ps->size--; {
return 1; /* success */ printf(“%d\n”, TopStack(&s));
} PopStack(&s); // su dung PopStack cach 1
}
int TopStack (IntStack * ps)
{ /* assume that stack is not empty */ FreeStack(&s);
return ps->top->data;
} return EXIT_SUCCESS;
}

/* cach 2: cai dat khac cua PopStack, vua day phan Trong trường hợp sử dụng PopStack cách 2, đoạn mã trên cần
tu o dinh ra khoi stack vua lay gia tri cua
phan tu nay */
sửa lại như sau:
int PopStack (IntStack * ps, int * pi) ...
{ PushStack(&s, 1);
IntListPtr temp; ...
while(PopStack(&s,&I))
if (ps->top == NULL) {
{ printf(“%d\n”, I);
return 0; /* FAILURE */ }
} ...

C programming. 2003 - 2005 7 C programming. 2003 - 2005 8


Danh Sách Liên Kết Tổng Quát
Hàng Đợi Bằng Cấu Trúc DSLK Trong danh sách thường, mỗi phần tử mang dữ liệu riêng.
(1,2,3,4)
Tương tự như cách biểu diễn Xếp chồng bằng cấu trúc DSLK, hãy Trong danh sách tổng quát, mỗi phần tử có thể là một danh sách.
biểu diễn Hàng Đợi cũng bằng cấu trúc DSLK. (1,2,(3,4),5)

struct intGenListNode
front {
data+next data+next data+next int data;
1 2 3 null struct intGenListNode * subList;
rear struct intGenListNode * next;
};
size
3 Head
1 null 2 null ? 5 null null
Size

3 null
Danh Sách Liên Kết Kép (Doubly-Linked List)
4 null null

front Với cấu trúc danh sách tổng quát, chúng ta có thể sử dụng đệ quy
Prev+Data+Next Prev+Data+Next Prev+Data+Next
null 1 2 3 null để duyệt và hiển thị nội dung toàn bộ danh sách.
rear
void DisplayIntgenList (struct intGenListNode * glptr)
{
size while (glptr != NULL)
3 {
if (glptr->subList == NULL)
{ /* atomic value */
printf ("%d\n", glptr->data);
}
Danh Sách Liên Kết Hai Đầu (Double Ended Queue) else
{ /* sub-list */
DisplayIntGenList (glptr->subList);
}
glptr = glptr->next;
}
}

C programming. 2003 - 2005 9 C programming. 2003 - 2005 10


Sắp xếp (Sorting) Trên đây chúng ta dùng cách khai báo macro, không phải là định
nghĩa hàm con (subroutine).
Các thuật toán sắp xếp cơ bản - Macro: đơn giản, chi phí thấp
- Hàm con: tổng quát hơn, nhưng tốn kém hơn
Insertion Sort - Sắp xếp bằng cách chèn

Selection Sort - Sắp xếp bằng cách chọn

Bubble Sort - Sắp xếp theo nguyên lý nổi bọt

Shellsort - Sắp xếp

Đây là những thuật toán sắp xếp đơn giản, dễ cài đặt.
Chạy rất nhanh với những tập tin dữ liệu kích thước nhỏ.
Trong một số trường hợp đặc biệt, các thuật toán này chạy rất hiệu
quả.

Khái niệm và điều kiện:


- Các tập tin (Files) chứa các bản ghi (Records) phân biệt với
nhau bởi khóa (Keys).
- Nội dung của tập tin chứa được trong bộ nhớ

typedef int itemType


#define less (A, B) ( A < B )
#define exch (A, B) {itemType t = A; A = B; B = t; }

C programming. 2003 - 2005 11 C programming. 2003 - 2005 12


Insertion sort - Sắp xếp bằng cách chèn

Selection sort - Sắp xếp bằng cách chọn

A S O R T I N G E X A M P L E
A S O R T I N G E X A M P L E A S O R T I N G E X A M P L E
A S O R T I N G R X A M P L E A O S R T I N G E X A M P L E
A A O R T I N G E X S M P L E A O R S T I N G E X A M P L E
A A E R T I N G O X S M P L E A O R S T I N G E X A M P L E
A A E E T I N G O X S M P L R A I O R S T N G E X A M P L E
A A E E G I N T O X S M P L R A I N O R S T G E X A M P L E
A A E E G I N T O X S M P L R A G I N O R S T E X A M P L E
A A E E G I L T O X S M P N R A E G I N O R S T X A M P L E
A A E E G I L M O X S T P N R A E G I N O R S T X A M P L E
A A E E G I L M N X S T P O R A A E G I N O R S T X M P L E
A A E E G I L M N O S T P X R A A E G I M N O R S T X P L E
A A E E G I L M N O P T S X R A A E G I M N O P R S T X L E
A A E E G I L M N O P R S X T A A E G I L M N O P R S T X E
A A E E G I L M N O P R S X T A A E E G I L M N O P R S T X
A A E E G I L M N O P R S T X A A E E G I L M N O P R S T X
A A E E G I L M N O P R S T X

void insertion (itemType a[], int l, int r)


{
int i, j;

void selection (itemType a[], int l, int r) for (i = l+1; i <= r; i++)
{ {
int i, j; itemType v = a[i];
for (i = l; i < r; i++) j = i;
{ while (j>l && less(v, a[j-1]))
int min = i; { a[j] = a[j-1]; j--; }
for ( j = i+1; j <= r; j++) a[j] = v;
if (less (a[j], a[min])) min = j; }
exch(a[i], a[min]); }
}
}

C programming. 2003 - 2005 13 C programming. 2003 - 2005 14


Bubble sort - Sắp xếp theo nguyên lý nổi bọt Tính chất của các giải thuật sắp xếp cơ bản

Thời gian chạy là bậc 2.


A S O R T I N G E X A M P L E
A A S O R T I N G E X E M P L
Selection sort:
A A E S O R T I N G E X L M P
A A E E S O R T I N G L X M P số lần so sánh: N-1 + N-2 + . . . + 2 + 1 = N^2/2
A A E E G S O R T I N L M X P số lần hoán vị: N
A A E E G I S O R T L N M P X
A A E E G I L S O R T M N P X
A A E E G I L M S O R T N P X Insertion sort:
A A E E G I L M N S O R T P X số lần so sánh: (N-1 + N-2 + . . . + 2 + 1) / 2 = N^2/4
A A E E G I L M N O S P R T X
số lần hoán vị: N^2/4
A A E E G I L M N O P S R T X
A A E E G I L M N O P R S T X
A A E E G I L M N O P R S T X Bubble sort:
A A E E G I L M N O P R S T X số lần so sánh: N-1 + N-2 + . . . + 2 + 1 = N^2/2
A A E E G I L M N O P R S T X
A A E E G I L M N O P R S T X số lần hoán vị: khoảng N^2/2

Với bộ dữ liệu trong đó các bản ghi có kích thước lớn, khóa nhỏ
selection sort tăng tuyến tính theo số các bản ghi
void bubble (itemType a[], int l, int r) N bản ghi M từ (khóa là một từ)
{
int i, j; số lần so sánh: N^2/2
for (i = l; i < r; i++) số lần hoán vị: NM
for (j = r; j > i; j--)
compexch(a[j], a[j-1]); if N tỉ lệ với M
}
chi phí và số bản ghi tỉ lệ với:

Thuật toán nổi bọt được cải tiến để chạy nhanh hơn:
Với các tập tin có các bản ghi gần như đã theo thứ tự
- thêm kiểm tra điều kiện dừng nếu không có hoán vị.
bubble sort và insertion sort có thể đạt mức tuyến tính
- nổi bọt hai chiều. (trong trường hợp này thuật toán sắp xếp nhanh quicksort lại
có mức độ phức tạp bình phương)

C programming. 2003 - 2005 15 C programming. 2003 - 2005 16


Sắp xếp con trỏ Sắp xếp với các bản ghi có hai khóa

Khi sắp xếp các bản ghi lớn, có nhiều trường, nên thực hiện cách Sắp xếp theo khóa thứ nhất, sau đó sắp tiếp theo khóa thứ 2
hoán vị các tham chiếu đến các bản ghi thay vì phải hoán vị toàn
bộ nội dung bản ghi.
Aaron 4 Fox 1
Andrews 3 Quilici 1
1 9 Fox 1
Battle 4 Chen 2
2 4 Quilici 1
Chen 2 Furia 3
3 8 Chen 2
Fox 1 Kanaga 3
4 3 Furia 3
Furia 3 Andrews 3
5 1 Kanaga 3
Gazsi 4 Rohde 3
6 5 Andrews 3
Kanaga 3 Battle 4
7 10 Rohde 3
Quilici 1 Aaron 4
8 6 Battle 4
Rohde 3 Gazsi 4
9 2 Aaron 4
10 7 Gazsi 4
Sắp xếp theo khóa thứ 2, nếu bằng nhau thì dừng lại, thực hiện
sắp xếp theo khóa thứ nhất cho các bản ghi cùng khóa 2.
Việc cài đặt chỉ cần thay đổi chút ít trong phần so sánh giá trị giữa
các phần tử.
Fox 1
Trong trường hợp dùng mảng
Quilici 1
Chen 2
typedef int itemType
Andrews 3
#define less(A, B) (data[A].key < data[B].key)
#define exch(A, B) {itemType t = A; A = B; B = t;}
Furia 3
Kanaga 3
Trong trường hợp dùng con trỏ đến các bản ghi Rohde 3
Aaron 4
typedef dataType* itemType Battle 4
#define less(A, B) (*A.key < *B.key)
#define exch(A, B) {itemType t = A; A = B; B = t;} Gazsi 4

C programming. 2003 - 2005 17 C programming. 2003 - 2005 18


Sắp xếp chia 4 Sắp xếp đan xen bộ 4

Ta chia tập dữ liệu thành 4 phần: Sử dụng thuật toán sắp xếp chèn với bước tăng là 4
- cứ đến phần tử thứ 4 tính từ phần tử thứ nhất.
- cứ đến phần tử thứ 4 tính từ phần tử thứ hai. A S O R T I N G E X A M P L E
- cứ đến phần tử thứ 4 tính từ phần tử thứ ba. A I O R T S N G E X A M P L E
A I N R T S O G E X A M P L E
- cứ đến phần tử thứ 4 tính từ phần tử thứ tư. A I N G T S O R E X A M P L E
A I N G E S O R T X A M P L E
A S O R T I N G E X A M P L E A I N G E S O R T X A M P L E
A S O R E I N G T X A M P L E A I A G E S N R T X O M P L E
A S O R E I N G P X A M T L E A I A G E S N M T X O R P L E
A I A G E S N M P X O R T L E
A I O R E S N G P X A M T L E A I A G E L N M P S O R T X E
A I O R E S N G P X A M T L E A I A G E L E M P S N R T X O
A I O R E L N G P S A M T X E
Cài đặt thuật toán
A I N R E L O G P S A M T X E
A I A R E L N G P S O M T X E
h = 4;
A I A R E L E G P S N M T X O for(i = 1+h; i<=r; i++)
{
A I A G E L E R P S N M T X O itemType v = a[i];
A I A G E L E M P S N R T X O j = i;
while(j >= 1+h && less(v, a[j-h]))
A I A G E L E M P S N R T X O { a[j] = a[j-h]; j -= h; }
a[j] = v;
}

C programming. 2003 - 2005 19 C programming. 2003 - 2005 20


Shellsort Cài đặt thuật toán
Sử dụng các bước tăng giảm dần
void shellshort(itemType a[], int l, int r)
{
13 A S O R T I N G E X A M P L E int i,j;
A S O R T I N G E X A M P L E int incs[16] = {1391376, 463792, 198768,
A E O R T I N G E X A M P L S 86961, 33936, 13776, 4592,
1968, 861, 336,112, 48,
4 A E O R T I N G E X A M P L S 21, 7, 3, 1 };
A E O R T I N G E X A M P L S for(k = 0; ; < 16; k++)
{
A E N R T I O G E X A M P L S int h = incs[k];
A E N G T I O R E X A M P L S for(i = l+h; i <= r; i++)
A E N G E I O R T X A M P L S {
A E N G E I O R T X A M P L S itemType v = a[i];
A E A G E I N R T X O M P L S j = i;
A E A G E I N M T X O R P L S while(j >= h && less(v, a[j-h]))
{ a[j] = a[j-h]; j -= h; }
A E A G E I N M P X O R T L S
a[j] = v;
A E A G E I N M P L O R T X S }
A E A G E I N M P L O R T X S }
}
1 A E A G E I N M P L O R T X S
A A E G E I N M P L O R T X S Thuật toán Shellsort có tốc độ sắp xếp nhanh, cài đặt đơn giản; rất
A A E G E I N M P L O R T X S thích hợp với các tập tin nhỏ và vừa, với các tập tin lớn, thuật toán
A A E E G I N M P L O R T X S Shellsort vẫn có hiệu suất thực hiện rất cao.
A A E E G I N M P L O R T X S
A A E E G I N M P L O R T X S
A A E E G I M N P L O R T X S Tỉ lệ tăng nào là thích hợp?
A A E E G I M N P L O R T X S
• có nhiều tỉ lệ đã được chứng minh hiệu quả.
A A E E G I L M N P O R T X S
A A E E G I L M N O P R T X S • dễ chọn nhất là dùng: 1, 4, 13, 40, 121, 363, 1090, ...
A A E E G I L M N O P R T X S
A A E E G I L M N O P R T X S
A A E E G I L M N O P R T X S
A A E E G I L M N O P R S T X
A A E E G I L M N O P R S T X

Qua mỗi bước, thứ tự của danh sách ngày càng rõ hơn.

C programming. 2003 - 2005 21 C programming. 2003 - 2005 22


Thuật toán CombSoft Cài đặt thuật toán

Giả sử chúng ta sắp xếp tăng dần một danh sách bằng thuật toán #define SHRINKFACTOR 1.3
nổi bọt. comb_sort(itemType a[], int size)
{
Thuật toán sắp xếp nổi bọt có một nhược điểm là nếu phần tử int switches, i, j, top, gap;
tương đối nhỏ nằm ở gần cuối danh sách thì sẽ di chuyển rất chậm
về phía đầu (có thể gọi đây là con rùa). Còn phần tử có trị khóa gap = size;
lớn, nằm gần đầu danh sách thì lại di chuyển rất nhanh về phía vị do {
trí của nó (hãy cứ gọi phần tử loại thế này là con thỏ). gap = (int) ((float)gap/SHRINKFACTOR);
switch (gap)
{
case 0: /* the smallest gap is 1 bubble sort */
gap = 1;
break;
case 9:
case 10:
gap = 11;
break;
default:
break;
}
switches = 0; /* dirty pass flag */
top = size - gap;
for(i=0; i<top; ++i)
{
j = i + gap;
if(a[i] > a[j])
{ /* swap */
exch(a[i], a[j]);
++switches;
}
}
} while(switches || (gap>1));
}
Sắp xếp nổi bọt
Đây không phải là thuật toán Shellsort.
Một cải tiến nhỏ, trong đó khoảng cách giữa các phần tử cần so (tham khảo Stephen Lacey, Richard Box. Byte 4,1991)
sánh lớn hơn 1 sẽ cho phép biến các “con rùa” thành “con thỏ”.

C programming. 2003 - 2005 23 C programming. 2003 - 2005 24


Quicksort Phân vùng (partitioning)
Để phân vùng một mảng trước khi thực hiện sắp xếp, đầu tiên,
Để sắp xếp một mảng, đầu tiên chia mảng thành 3 phần: chúng ta chọn ra một phần tử làm mốc a[i0]
Các phần tử a[i] có giá trị bằng nhau không thay đổi vị trí • quét mảng từ bên phải sang để tìm phần tử nhỏ hơn a[i0],
Các phần tử không lớn hơn a[i] nằm ở bên trái phần tử thứ i • quét mảng từ bên trái sang để tìm phần tử lớn hơn a[i0].
Các phần tử không nhỏ hơn a[i] nằm ở bên phải phần tử thứ i. • hoán vị hai phần tử tìm được.
Sau đó, thực hiện việc sắp xếp các phần bên trái và bên phải. Việc • lặp lại 3 bước trên cho đến khi các vị trí dò tìm vượt qua mốc.
sắp xếp này thực hiện đệ quy.
A S O R T I N G E X A M P L E

A S O R T I N G E X A M P L E A S
A A E E T I N G O X S M P L R A M P L
A A E A A S M P L E
A A
A O
L I N G O P M R X T S E X
L I G M O P N A A E O X S M P L E
G I L
I L R
I E R T I N G
N P O
O P A A E E T I N G O X S M P L R
P
S T X
T X Cài đặt thuật toán phân vùng
T
A A E E G I L M N O P R S T X
v: phần tử mốc
i: vị trí dò từ trái sang phải
j: vị trí dò từ phải sang trái.

int partition(Item a[], int l, int r)


{
int i, j;
Item v;
v = a[r]; i = l-1; j = r;
for( ; ; )
C programming. 2003 - 2005 25 C programming. 2003 - 2005 26
{ Thuật Toán Quicksort Không Đệ Qui.
while(less(a[++i], v));
while(less(v, a[--j]));
if(j == l) break; Chúng ta có thể dùng stack để khử tính đệ quy trong cài đặt thuật
if(i >= j) break; toán Quicksort ở trên.
exch(a[i], a[j]);
}
exch(a[i], a[r]); #define push2(A, B) push(A); push(B);
return i; void quicksort(Item a[], int l, int r)
} {
int i;
stackinit(); push2(l, r);
Cài Đặt Quicksort while(!stackempty())
{
r = pop(); l = pop();
quicksort (Item a[], int l, int r)
if(r <= l) continue;
{
i = partition(a, l, r);
int i;
if(i-l > r-i)
if(r > l)
{ push2(l, i-1); push2(i+1, r); }
{
else
i = partition(a, l, r);
{ push2(i+1; r); push2(l, i-1); }
quicksort(a, l, i-1);
}
quicksort(a, i+1, r);
}
}
}
Với cài đặt trên, trong trường hợp xấu nhất, chúng ta có thể đạt
Vấn đề phát sinh: được mức chi phí bộ nhớ nhỏ hơn (lg N) nhưng thời gian chạy thì
vẫn ở tỉ lệ bình phương.
Sẽ có quá nhiều lần gọi đệ qui.
thời gian chạy phụ thuộc vào đầu vào.
Trong trường hợp xấu nhất Phân tích thuật toán
- thời gian tăng theo tỉ lệ bình phương.
- bộ nhớ tăng tuyến tính. Tổng thời gian chạy là tổng của
chi_phí * tần_suất
của tất cả các phép toán cơ bản.

chi_phí (cost) phụ thuộc vào kiểu máy tính


tần_suất (frequency) phụ thuộc vào thuật toán và loại dữ liệu đầu
vào.

C programming. 2003 - 2005 27 C programming. 2003 - 2005 28


Đối với thuật toán Quicksort, gọi Tìm Kiếm Trên Mảng Đã Có Thứ Tự
A là số lần thực hiện phân vùng. Bài toán: tìm x trong một mảng.
B là số lần hoán vị Điều kiện: mảng đã được sắp thứ tự tăng (giảm) dần
C là số lần so sánh. Các hàm tìm kiếm sẽ trả về -1 nếu x không xuất hiện trong dãy,
ngược lại, các hàm tìm kiếm sẽ trả về chỉ số của x trong dãy.

Số lần so sánh trong trường hợp xấu nhất là


N + (N-1) + (N-2) + . . . = N(N-1)/2 Tìm kiếm theo phương pháp lặp
Giả sử rằng các dữ liệu được đưa vào theo thứ tự ngẫu nhiên,
nghĩa là Do dãy số đã có thứ tự tăng dần, nên nếu tìm được phần tử đầu
• mỗi phần tử đều có khả năng được chọn làm phần tử phân tiên có giá trị lớn hơn x thì có thể kết luận dãy số không chứa phần
vùng ngang nhau tử x. Vì vậy, chúng ta có thể rút ngắn thời gian tìm kiếm.
• các tập con cũng có thứ tự ngẫu nhiên.
Số lần so sánh trung bình được xác định theo công thức: Cài đặt thuật toán
C(N) = N+1
+ (C(1) + C(N-1))/N int search(int a[], int n, int x)
{
+ (C(2) + C(N-2))/N int i = 0;
... while(i < n && a[i] < x)
i++;
+ (C(N-1) + C(1))/N return (i<n && a[i]==x ? i : -1);
}
C(N) = N+1 + 2( C(1) + C(2) + . . . + C(N-1) )/N
NC(N) = N(N+1) + 2( C(1) + C(2) + . . . + C(N-1) ) void main(void)
{
NC(N) – (N-1)C(N-1) = 2N + 2C(N-1) int x, pos, size;
NC(N) = (N+1)C(N-1) + 2N int a[];
C(N) / (N+1) = C(N-1) /N + 2 / (N+1) // nhap day so va sap xep day so (tang/giam) dan
= 2( 1 + 1/2 + 1/3 + . . . + 1/(N+1) ) size = enter_array(a);
quicksort(a,size);
= 2 ln N + (eps)
// nhap gia tri can tim
scanf(“%d”, &x);
Như vậy, trung bình, thuật toán Quicksort dùng khoảng 2N ln N = pos = search(a, size, x);
(1.38...) N lg phép so sánh (và số lần hoán vị khoảng bằng 1/6 số if(pos != -1)
lần so sánh). printf(“%d has been found at %d\n”, x, pos);
else
printf(“%d is not in array\n”, x);
}

C programming. 2003 - 2005 29 C programming. 2003 - 2005 30


Tìm Kiếm Nhị Phân Tìm Kiếm Nhị Phân bằng Đệ Qui

1. Phạm vi tìm kiếm ban đầu là toàn bộ danh sách 1. Phạm vi tìm kiếm ban đầu là toàn bộ danh sách k=0 đến
2. Lấy phần tử chính giữa của phạm vi tìm kiếm (a[j]) rồi so sánh m=n-1.
với x. 2. Lấy phần tử chính giữa của phạm vi tìm kiếm (a[j]) rồi so sánh
• Nếu a[j] = x. trả về chỉ số j. STOP. với x.

• Nếu a[j] > x. Phạm vi tìm kiếm mới là các phần tử có chỉ • Nếu a[j] = x. trả về chỉ số j. STOP.
số nhỏ hơn j. • Nếu a[j] > x. Phạm vi tìm kiếm mới là các phần tử có chỉ
• Nếu a[j] < x. Phạm vi tìm kiếm mới là các phần tử có chỉ số nhỏ hơn j. Gọi đệ quy hàm tìm kiếm với phạm vi mới
số lớn hơn j. là (k, j-1)
3. Nếu còn tồn tại phạm vi tìm kiếm thì lặp lại bước 2, • Nếu a[j] < x. Phạm vi tìm kiếm mới là các phần tử có chỉ
ngược lại, không tìm thấy x. STOP. số lớn hơn j. Gọi đệ quy hàm tìm kiếm với phạm vi mới là
(j+1, m)
3. Điều kiện dừng: x=a[j] hoặc k > m.
Cài đặt thuật toán

Cài đặt thuật toán


int Binary_Search(int a[], int n, int x)
{
// gia su ban dau chua tim duoc int Binary_Search2(int a[], int k,int m, int x)
unsigned char found=FALSE; {
int j=(k+m) /2;
// Pham vi tìm kiem ban dau tu k=0 den m = n-1 if (k>m) return -1 ;
int k=0; else if (a[j]==x) return j ;
int m=n-1; else
int j; Binary_Search2(a, (a[j]<x ? j+1:k),
(a[j] > x ?j-1:m),x);
while (k<=m && !found) }
{
j=(k+m) /2; // chi so phan tu giua
if (a[j]==x)
found=TRUE;
else
if (x>a[j])
k=j+1; //Pham vi tim moi la (j+1, m)
else
m=j-1; // Pham vi tim moi la (k, j-1)
}
return (found ? j : -1) ;
}

C programming. 2003 - 2005 31 C programming. 2003 - 2005 32


Các Thuật Toán Trên Cấu Trúc Cây Độ sâu hay mức của một nút (Depth/level): được tính bằng
chiều dài đường đi từ nút gốc đến nút đang xét.
Cây là một cấu trúc dữ liệu rất thông dụng và quan trọng trong Chiều cao của cây (height): chiều dài đường đi dài nhất trong
nhiều phạm vi khác nhau của kỹ thuật máy tính. cây.
Ví dụ: Tổ chức các quan hệ họ hàng trong một gia phả, mục lục Cây con (subtree): cây bao gồm nút và tất cả các nút hậu duệ của
của một cuốn sách, xây dựng cấu trúc cú pháp trong các trình biên nó. Nút gốc và toàn bộ cây không được xem là cây con.
dịch.
Ví dụ:
Số nút 11 R
Khái niệm Chiều cao của cây 5
Cây là tập hợp các phần tử gọi là nút, (một nút có thể có kiểu bất Nút gốc (root) R
kỳ) và tập các cạnh có định hướng kết nối các cặp nút trong cây. Nút lá (leaves) B,C,D,K,M,N
Nút trong D, H, I, R, L I L
Nút ở mức 2 H,K,M,N
Nút gốc (Root): là nút ở “trên cùng” trong một cây. Trên nút gốc Nút tổ tiên (Ancestors) của H I, R
không có nút nào nữa. Nút hậu duệ (Decesdants) của H D, E, B, C, D
Nút ở cây con nhánh trái của I H,E,B,C,D H K M N
Nút con (child): nút kế tiếp (phía dưới) của một nút trong cây. Một
nút có thể có nhiều nút con, các nút con này được nhìn theo
thứ tự từ trái sang phải. Nút con tận cùng bên trái là nút đầu
tiên và nút con tận cùng bên phải là nút con cuối cùng. E
Nút cha (parent): nút liền kề (phía trên) của một nút trong cây. Một
nút chỉ có duy nhất một nút cha.
Các nút anh em (siblings): các nút con của cùng một nút. C
D B
Các cạnh/nhánh (edge/branch): đường nối từ nút cha đến các nút
con của nó.
Nút tổ tiên (Ancestors): Các nút tổ tiên của một nút bao gồm nút
Định nghĩa cây theo đệ quy:
cha của nút, nút cha của nút cha, v.v đến trên cùng là nút
gốc. - Một nút đơn cũng chính là một cây.
Nút hậu duệ (Descendant): Các nút hậu duệ của một nút bao gồm - Các nút được gọi là ở cùng một cây khi có đường đi giữa các
các nút con của nút, các nút con của nút con, v.v đến các nút này.
nút lá của các nhánh thuộc nút. - Một cây sẽ bao gồm một nút gốc (Root) và m cây con, trong
Đường đi (Path): là chuỗi các cạnh nối từ một nút đến một trong mỗi cây con lại có một nút gốc và m1 cây con nhỏ hơn.
số các nút hậu duệ của nó. - Một cây không có một nút nào cả gọi là cây rỗng.
Chiều dài đường đi (Path length): số cạnh trong đường đi.
Nút lá (Leaf): Nút không có nút con.
Nút trung gian (Interior node): nút có ít nhất một nút con.
C programming. 2003 - 2005 33 C programming. 2003 - 2005 34
Cây Nhị Phân Các cây nhị phân đặc biệt

Cây nhị phân đúng


Trong cây nhị phân, mỗi nút có tối đa hai nút con: nút con nhánh
Một cây nhị phân được gọi là cây nhị phân đúng nếu nút gốc và tất
trái và nút con nhánh phải.
cả các nút trung gian đều có 2 nút con.
Khi một nút chỉ có một nút con, cần phải phân biệt là nút con bên
nhánh trái, hay nút con bên nhánh phải, chứ không chỉ đơn thuần
gọi là nút con. Ghi chú: nếu cây nhị phân đúng có n nút lá thì cây này sẽ có tất cả
2n-1 nút.

Cây nhị phân


Cây nhị phân đầy
Một cây nhị phân được gọi là đầy với chiều cao d thì
- nó phải là cây nhị phân đúng
- tất cả các nút lá đều có mức d
Ghi chú: cây nhị phân đầy là cây nhị phân có số nút tối đa ở mỗi
mức.

C programming. 2003 - 2005 35 C programming. 2003 - 2005 36


Cây nhị phân tìm kiếm (Binary search tree) Cây nhị phân cân bằng hoàn toàn
Một cây nhị phân được gọi là cây nhị phân tìm kiếm nếu và chỉ nếu Một cây nhị phân được gọi là cân bằng hoàn toàn nếu và chỉ nếu
đối với mọi nút của cây thì khóa của một nút bất kỳ phải lớn hơn đối với mọi nút của cây thì số nút của cây con bên trái và số nút
khóa của tất cả các nút trong cây con bên trái của nó và phải nhỏ của cây con bên phải hon kém nhau nhiều nhất là 1.
hơn khóa của tất cả các nút trong cây con bên phải của nó.

Cây nhị phân cân bằng (AVL tree):


Một cây nhị phân được gọi là cây nhị phân cân bằng nếu và chỉ
nếu đối với mọi nút của cây thì chiều cao của cây con bên trái và
chiều cao của cây con bên phải hơn kém nhau nhiều nhất là 1.
(Theo Adelson Velski và Landis).

C programming. 2003 - 2005 37 C programming. 2003 - 2005 38


Các phép duyệt cây nhị phân (Traverse) Inorder (LNR)
Là quá trình đi qua các nút đúng một lần. Qua cây con bên trái duyệt trước (theo thứ tự LNR). Sau đó duyệt
qua nút gốc. Cuối cùng duyệt qua cây con bên phải (theo thứ tự
LNR).

If the node is NULL


Return
Else
Traverse the node’s left subtree (LNR)
Visit the item in the node to do something
Traverse the node’s right subtree (LNR)

Ví dụ trong hình: 2Æ1Æ6Æ4Æ7Æ3Æ8Æ5Æ9

Preorder (NLR) Postorder (LRN)


Duyệt qua nút gốc trước, sau đó qua cây con bên trái, dùng Qua cây con bên trái duyệt trước (theo thứ tự LRN). Sau đó duyệt
preorder cho cây con bên trái. Cuối cùng qua cây con bên phải và qua cây con bên phải (theo thứ tự LRN). Cuối cùng duyệt qua nút
dùng preorder cho cây con bên phải. gốc.

If the node is NULL If the node is NULL


Return Return
Else Else
Visit the item in the node to do something Traverse the node’s left subtree (LRN)
Traverse (Preorder) the node’s left subtree Traverse the node’s right subtree (LRN)
Traverse (Preorder) the node’s right subtree Visit the item in the node to do something

Ví dụ trong hình: 1Æ2Æ3Æ4Æ6Æ7Æ5Æ8Æ9 Ví dụ trong hình: 2Æ6Æ7Æ4Æ8Æ9Æ5Æ3Æ1

C programming. 2003 - 2005 39 C programming. 2003 - 2005 40


Biểu diễn cây struct nodetype
{
Sử dụng khái niệm liên kết để biểu diễn cây. int key;
int info;
Cây tổng quát: struct nodetype* firstchild;
Mỗi nút có: struct nodetype* nextsiblings;
};
- dữ liệu của nút typedef struc nodetype* NODEPTR;
- các con trỏ chỉ tới các nút con của nó. NODEPTR tree;

Các nút có thể có số nút con khác nhau.


Cây nhị phân
a
a a
a

b c d e
b c d e
b c b c

f g h
f g h
g g

Mỗi nút có đúng hai con trỏ chỉ tới nút con bên trái và nút con bên
Vì số lượng các nút con của một nút không xác định trước nên sẽ phải của nó.
rất khó khăn nếu đưa các liên kết trực tiếp từ nút đến các nút con
của nó. struct nodetype
Thay vào đó, chúng ta có thể quản lý các nút con của một nút bằng {
int key;
một danh sách liên kết. int info;
struct nodetype* left;
struct nodetype* right;
};
typedef struc nodetype* NODEPTR;
NODEPTR tree;

C programming. 2003 - 2005 41 C programming. 2003 - 2005 42


Các phép toán trên cây nhị phân Tạo cây BST (Create_Tree)

Tạo cây
void Insert(NODEPTR root, int x, int a)
{
Khởi tạo (inititalize) NODEPTR p;
Khởi động cây nhị phân, cho chương trình biết hiện tại cây nhị if(x == root->key) // key duplicated, STOP
phân rỗng. {
printf("bi trung khoa, khong them nut nay duoc");
return;
void Inititialize (NODEPTR *root) }
{
*root = NULL; // Stop condition
} if(x < root->info && root->left == NULL)
{
p = New_Node();
Gọi hàm: p->key =x;
Initialize (&tree); p->info = a;
p->left = NULL;
p->right = NULL;
root->left=p;
Cấp phát vùng nhớ (New_Node)
return;
Cấp phát một nút cho cây nhị phân. Hàm New_Node trả về địa chỉ }
của nút vừa cấp phát.
// stop condition
if(x > root->info && root->right == NULL)
NODEPTR New_Node(void) {
{ p = New_Node();
NODEPTR p; p->key =x;
p = (NODEPTR)malloc(sizeof(struct nodetype)); p->info = a;
return(p); p->left = NULL;
} p->right = NULL;
root->right=p ;
return;
Gọi hàm: }
p = New_Node();
if(x < root->info) // recursion step
Insert(root->left, x,a); // recursion on left
else
Insert(root->right, x,a); // recursion on right
}

void Create_Tree(NODEPTR &root)


{
int khoa, noidung;
char so[10];

C programming. 2003 - 2005 43 C programming. 2003 - 2005 44


NODEPTR p; Kiểm tra cây nhị phân rỗng (Empty)
do {
printf("Nhap khoa :"); int Empty(NODEPTR root)
gets(so) ; {return(root == NULL ? TRUE : FALSE);}
khoa = atoi(so);
if (khoa !=0)
{ printf("Nhap noi dung :"); Gọi hàm:
gets(so) ; Empty(&tree);
noidung = atoi(so);
if (root==NULL)
{ p = New_Node();
Hủy một nút trong cây BST (Remove)
p->key = khoa;
p->info = noidung; Xóa nút có địa chỉ p trong BST sao cho sau khi xóa, cây vẫn là
p->left = NULL; BST. Có 3 trường hợp:
p->right = NULL;
root =p;
} Trường hợp 1: nút p cần xóa là nút lá. Việc xóa nút chỉ đơn giản là
else Insert(root,khoa,noidung); gỡ nút ra khỏi cây và hủy nó đi.
}
} while (khoa!=0); // Stop entering when key=0
}

Để tạo cây nhị phân do biến tree quản lý, ta gọi:


Create_Tree (&tree);

Cập nhật cây Trường hợp 2: Nút p cần xóa có một cây con. Chọn nút con của p
là nút thay thế (rp) vào vị trí của p. Sau đó, tạo liên kết từ nút cha
Giải phóng vùng nhớ (Free_Node)
của p đến rp. Cuối cùng, hủy p.

void Free_Node(NODEPTR p)
{
free(p);
}

Gọi hàm:
Free_Node (p);

C programming. 2003 - 2005 45 C programming. 2003 - 2005 46


{
if(p->right == NULL) // no right subtree
rp = p->left;
else
{
if(p->left == NULL) // no left subtree
rp = p->right;
else // node has left and right subtrees
{
/* search for p’s replacement: the most left
node of right subtree*/
/* f is parent of rp */
f = p;
rp = p->right;
Trường hợp 3: Nút p có hai cây con. Do tính chất nút cực trái của while(rp->left != NULL)
cây con bên nhánh phải của p có khóa lớn hơn khóa của p, nên để {
loại p thì chọn nút cực trái làm nút thay thế (rp) cho vị trí của p. Sau f = rp;
rp = rp->left;
đó, hủy p.
}
if(f != p)
{
f->left = rp->right;
rp->right = p->right;
}
rp->left = p->left;
}
}
Free_Node(p);
return (rp);
}
}

Gọi hàm:
Remove(p);

Hàm Remove xóa nút p, trả về con trỏ chỉ tới nút thay thế (rp).
Do hàm Remove trả về địa chỉ của nút thay thế nên nếu dùng
NODEPTR Remove(NODEPTR p) Remove để xóa nút gốc, nút thay thế sẽ là nút gốc mới của cây. Khi
{ đó ta gọi.
NODEPTR rp, f; tree = Remove(tree);
if(p == NULL)
printf(“p is not real. Cannot delete!\n”);
else
C programming. 2003 - 2005 47 C programming. 2003 - 2005 48
Tìm kiếm (Search) Duyệt cây
Tìm nút có khóa x trên BST có nút gốc là root. Nếu tìm thấy thì trả
về địa chỉ của nút có khóa x, ngược lại, trả về trị NULL. Duyệt theo thứ tự NLR (Preorder)
Tìm kiếm bằng phương pháp nhị phân
void Preorder (NODEPTR root)
{
NODEPTR Search(NODEPTR root, int x) if(root != NULL)
{ {
NODEPTR p; printf("%d ", root->info);
p = root; Preorder(root->left);
while(p != NULL && x != p->key) Preorder (root->right);
if(x < p->key) }
p = p->left; }
else
p = p->right;
return (p);
}
Duyệt theo thứ tự LNR (Inorder)
Gọi hàm:
void Inorder(NODEPTR root)
p = Search(tree, x);
{
if(root != NULL)
{
Inorder(root->left);
printf("%d ", root->info);
Inorder(root->right);
}
}

Duyệt theo thứ tự LRN (Postorder)

void Posorder(NODEPTR root)


{
if(root != NULL)
{
Posorder(root->left);
Posorder(root->right);
printf("%d ", root->info);
}
}

C programming. 2003 - 2005 49 C programming. 2003 - 2005 50


Cây Nhị Phân Tìm Kiếm Cân Bằng (AVL tree) Với cây AVL, việc thêm hay bỏ 1 nút trên cây có thể làm cây mất
cân bằng. Khi đó ta phải cân bằng lại cây. Tuy nhiên việc cân bằng
lại cây chỉ thực hiện ở phạm vi cục bộ tại nút mất cân bằng bằng
Tạo cây BST với mục đích là tìm nút nhanh. Tuy nhiên, nếu cây có
cách xoay trái hoặc phải ở một vài nhánh cây con nên giảm thiểu
nhánh dài và không cân bằng thì việc tìm kiếm một nút trở thành
chi phí cân bằng.
phép tìm tuần tự.

4 Định nghĩa:
Chỉ số cân bằng (balance factor) của một nút p trên cây AVL:
2 5 bf(p) = lh(p) – rh(p)
lh(p): chiều cao của nhánh cây con trái của p
7 rh(p): chiều cao của nhánh cây con phải của p.
1 3

8 Các trường hợp:


bf(p) = 0 nếu lh(p) = rh(p); nút p cân bằng.
9 bf(p) = 1 nếu lh(p) = rh(p) + 1; nút p bị lệch trái.
bf(p) = -1 nếu lh(p) = rh(p) – 1; nút p bị lệch phải.

Để đảm bảo tốc độ tìm kiếm thì cây BST phải có các nút có hai
nhánh trái và phải cân đối.

Cây BST cần phải được tổ chức lại cho cân bằng. Tổ chức thành
cây AVL.
4

2 5

1 3 7 Minh họa các vị trí có thể thêm nút lá vào cây AVL, khi thêm vào một trong
các vị trí B thì cây vẫn cân bằng. Khi thêm nút lá vào một trong các vị trí U
thì cây sẽ mất cân bằng. Các số tại các nút trên cây là chỉ số cân bằng của các
6 nút trước khi thêm vào nút mới

C programming. 2003 - 2005 51 C programming. 2003 - 2005 52


Các phép toán trên cây AVL Trong hình trên
Khai báo kiểu dữ liệu - Nếu thêm nút vào 1 trong 6 vị trí B trên cây thì cây vẫn cân
bằng.
struct nodetype - Nếu thêm nút vào 1 trong các vị trí U1ÆU12 trên cây thì cây sẽ
{
int key;
mất cân bằng.
int info; o Thêm nút vào sau bên trái của nút A(bfA = 1) tại các vị trí
int bf; U1ÆU4 thì cây sẽ mất cân bằng vì nút A đang bị lệch trái.
struct nodetype* left;
struct nodetyp* right; o Thêm nút vào sau bên phải nút C (bfC = -1) tại các vị trí
}; U9ÆU12 thì cây sẽ mất cân bằng vì nút C đang bị lệch
typedef struct nodetype* NODEPTR; phải.

Thêm nút Hai trường hợp khi thêm nút khóa x vào cây AVL làm cây mất cân
Thêm nút có khoá x, nội dung a vào AVL sao cho sau khi thêm, cây bằng là khi thêm nút vào sau bên trái nút có bf = 1 và thêm nút vào
nhị phân vẫn là AVL. sau bên phải nút có bf = -1.

Giải thuật: Cân bằng lại cây


- Thêm nút vào cây như bình thường. Nút thêm vào sẽ là nút lá. Gọi ya là nút trước gần nhất bị mất cân bằng khi thêm nút khóa x
- Tính lại chỉ số cân bằng của các nút bị ảnh hưởng. vào cây AVL. Không mất tính tổng quát, chỉ cần xét trường hợp bfya
= 1 và nút lá thêm vào là nút sau bên phải của nút ya.
- Kiểm tra cây có bị mất cân bằng? Nếu cây bị mất cân bằng thì
cân bằng lại cây.

Các trường hợp làm cây mất cân bằng

C programming. 2003 - 2005 53 C programming. 2003 - 2005 54


Nhận xét:
- Vì nút ya có có bfya = 1 nên nút ya chắc chắn có nút con bên
trái s với bfs = 0.
- Vì nút ya là nút gần nhất có bf là 1 nên nút s và các nút trước
khác của nút x (sẽ thêm vào) có bf là 0.
- Độ cao: h(T1) = h(T2) = h(T3).

Trường hợp 1a:


Nếu thêm nút mới khóa x vào vị trí nút sau bên trái của s (thuộc
nhánh T1) thì phải xoay phải quanh nút ya.
- Nút s sẽ là nút gốc mới của nhánh cây này với bfs = 0.
- Nút ya sẽ là nút con bên phải của s với bfya = 0.

Trường hợp 1b:


Nếu thêm nút mới khóa x vào vị trí nút sau bên phải của s (thuộc
nhánh T2) thì phải xoay 2 lần (xoay kép): xoay trái quanh nút s và
xoay phải quanh nút ya.
- Nút p sẽ là nút gốc mới của nhánh cây này với bfp = 0.
- Nút ya sẽ là nút con bên phải của p với bfya = -1.
- Nút s là nút con bên trái của p với bfs = 0.
C programming. 2003 - 2005 55 C programming. 2003 - 2005 56
p = root->right;
root->right = p->left;
Bảng phân biệt các trường hợp cây bị mất cân bằng khi thêm nút p->left = root;
và các phép xoay tương ứng để cân bằng lại cây. }

return p;
}

Phép xoay phải (Rotate_Right): xoay phải cay nhị phân tìm kiếm có
nút gốc là root, yêu cầu root phải có nút con bên trái (p). Sau khi
xoay phải thì p trở thành nút gốc, nút gốc cũ trở thành nút con bên
phải của nút gốc mới.
Hàm xoay phải trả về con trỏ chỉ nút gốc mới.

NODEPTR Rotate_Right(NODEPTR root)


{
NODEPTR p;

if(root == NULL)
printf("Khong the xoay phai vi cay bi rong.");
else
if(root->left == NULL)
Cài đặt giải thuật: printf("Khong the xoay phai vi khong \
co nut con ben trai.");
Phép xoay trái (Rotate_Left): xoay trái cây nhị phân tìm kiếm có nút else
gốc là root, yêu cầu root phải có nút con bên phải (p). Sau khi xoay {
trái thì p trở thành nút gốc, nút gốc cũ trở thành nút con bên trái p = root->left;
của nút gốc mới. root->left = p->right;
p->right = root;
Hàm xoay trái trả về con trỏ chỉ tới nút gốc mới. }

return p;
NODEPTR Rotate_Left(NODEPTR root) }
{
NODEPTR p;

if(root == NULL)
printf("Khong the xoay trai vi cay bi rong.");
else
if(root->right == NULL)
printf("Khong the xoay trai vi khong \
co nut con ben phai.");
else
{
C programming. 2003 - 2005 57 C programming. 2003 - 2005 58
Thêm nút (Insert) q->key =x;
q->info = a;
Thêm nút khóa x, nội dung a vào cây AVL q->bf = 0;
- Thêm nút theo giải thuật thêm nút vào cây nhị phân tìm kiếm. q->left = NULL;
q->right = NULL;
- Cân bằng lại cây bằng cách xoay đơn hay xoay kép.
if(x < fp->info)
fp->left = q;
void Insert(NODEPTR &pavltree, int x, int a)
else
{
fp->right = q;
NODEPTR fp, p, q, // fp: p’s father, q: p’s child
fya, ya, /* ya: the closest node that is
/* Adjust the balance factor for nodes from ya to q.
possible to be imbalanced */
If left imbalanced, bf of those nodes would be 1.
/* fya: ya’s father */
If right imbalanced, bf of those nodes would be –1.*/
s; // s: ya’s child in the imbalanced branch
int imbal; /* imbal = 1 if left imbalanced
if(x < ya->info)
-1 if right imbalanced */
p = ya->left;
else
// initializing
p = ya->right;
fp = NULL;
s = p; // s is a child of ya
p = pavltree;
while(p != q)
fya = NULL;
{
ya = p;
if(x < p->info)
{
/* search for fp, ya and fya, inserted node will be
p->bf = 1;
fp’s child */
p = p->left;
while(p != NULL)
}
{
else
if(x == p->info) // duplicated -> STOP
{
return;
p->bf = -1;
if (x < p->info)
p = p->right;
q = p->left;
}
else
}
q = p->right;
// where to go?
if(q != NULL)
if(x < ya->info)
if(q->bf != 0)
imbal = 1;
{
else
fya = p;
imbal = -1;
ya = q;
}
if(ya->bf == 0)
fp = p;
{
p = q;
ya->bf = imbal;
}
return;
}
// Insert new node
if(ya->bf != imbal)
q = New_Node();
C programming. 2003 - 2005 59 C programming. 2003 - 2005 60
{ else
ya->bf = 0; if(ya == fya->right)
return; fya->right = p;
} else
if(s->bf == imbal) // single rotation fya->left = p;
{ }
if(imbal == 1) // right rotating
p = Rotate_Right(ya);
else // left rotating
p = Rotate_Left(ya);
Để tạo cây AVL, ta dùng giải thuật sau.
ya->bf = 0;
s->bf = 0; void Create_AVLTree(NODEPTR &root)
} {
else // double rotation int khoa, noidung;
{ char so[10];
if(imbal == 1) // right-right double rotation NODEPTR p;
{
ya->left = Rotate_Left(s); do {
p = Rotate_Right(ya); printf("Nhap khoa :");
} gets(so) ;
else // right-left double rotation khoa = atoi(so);
{ if (khoa !=0)
ya->right = Rotate_Right(s); {
p = Rotate_Left(ya); printf("Nhap noi dung :");
} gets(so) ;
if(p->bf == 0) // p is the new node noidung = atoi(so);
{ if (root==NULL)
ya->bf = 0; {
s->bf = 0; p = New_Node();
} p->key = khoa;
else p->info = noidung;
if(p->bf == imbal) p->bf = 0 ;
{ p->left = NULL;
ya->bf = -imbal; p->right = NULL;
s->bf = 0; root =p;
} }
else else
{ Insert(root,khoa,noidung);
ya->bf = 0; }
s->bf = imbal; } while (khoa!=0); // STOP
} }
p->bf = 0;
}
Để tạo cây nhị phân tìm kiếm do biến tree quản lý, lời gọi hàm là:
if(fya == NULL) Create_AVLTree(&tree);
pavltree = p;
C programming. 2003 - 2005 61 C programming. 2003 - 2005 62
Cập nhật cây + gọi đệ quy để xóa nút x ở nhánh trái của root:
Remove(root->left, x);
Tìm kiếm (Search)
Tìm nút có khóa x trên cây AVL có gốc là root. Nếu tìm thấy thì trả + Gọi balance_left để cân bằng lại cây có nút gốc root
về địa chỉ của nút có khóa bằng x, ngược lại, nếu không tìm thấy nếu nhánh cây con bên trái bị giảm độ cao.
thì trả về giá trị NULL. Nếu (x > root->info)
Cây AVL là một BST nên ta tìm kiếm nhanh bằng phương pháp tìm + gọi đệ quy để xóa nút x ở nhánh phải của root:
nhị phân. Hơn nữa, cây AVL luôn cân bằng nên thời gian tìm kiếm
trong mọi trường hợp nhanh hơn nhiều so với việc tìm kiếm trên Remove(root->right, x);
cây BST thông thường. + Gọi balance_right để cân bằng lại cây có nút gốc root
nếu nhánh cây con bên phải bị giảm độ cao.
NODEPTR search(NODEPTR root, int x) Nếu (x == root->info)
{
NODEPTR p; Xóa nút root như phép toán xóa trên cây BST.
p = root;
while(p != NULL && x!=p->key)
if(x < p->key) Cài đặt:
p = p->left; (Tự cài đặt)
else
p = p->right;

return(p);
}
Duyệt cây
Cây AVL cũng là cây nhị phân nên ta sẽ áp dụng các phương pháp
Gọi hàm: duyệt Preorder, Inorder và Postorder vào cây AVL.
Search(tree, x);

Xóa nút (Remove)

Xóa nút khóa x trên cây AVL sao cho sau khi xóa, cây vẫn là cây
AVL.

Giải thuật:
Nếu (root == NULL)
Thông báo (“Không thể xóa nút khóa x trên cây”);
Nếu (root != NULL)
Nếu (x < root->info)
C programming. 2003 - 2005 63 C programming. 2003 - 2005 64

You might also like