2003 - 2005 5- xấu nhất đây là trường hợp đảm bảo thuật toán không vượt qua ngưỡng này - trung bình mang tính ước lượng Việc phân tích phụ thuộc vào các thông tin chi tiết trên - chi phí
Trang 1C programming 2003 - 2005 1 C programming 2003 - 2005 2
THUẬT TOÁN THUẬT TOÁN TRÊN CẤU TRÚC CÂY
Trang 2C programming 2003 - 2005 3
Giới thiệu
Thuật toán - Thuật giải (Algorithm) là phương pháp để giải một bài
toán
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
Sử dụng máy tính trong công việc, con người luôn mong muốn
- máy tính chạy càng ngày càng nhanh hơn
- xử lý được nhiều dữ liệu hơn
- có thể giải quyết được những vấn đề tưởng chừng như không
thể giải quyết được
Công nghệ máy tính chỉ nâng cao được các thứ theo một hệ số cố
định
Hãy suy nghĩ
Một thuật toán được thiết kế cẩn thận có thể thực hiện được mức
độ cải tiến lớn hơn nhiều:
Nghiên cứu thuật toán là vấn đề luôn tồn tại từ xưa đến nay
Phân tích thuật toán
Người ta so sánh các thuật toán dựa trên các phép ước lượng chi phí (thời gian chạy của thuật toán, lượng bộ nhớ mà thuật toán 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 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
Đối với một thuật toán, chúng ta thường quan tâm đến các yếu tố sau
Kích thước, dung lượng của dữ liệu đầu vào: N Thời gian thực hiện Thường tỉ lệ với:
1 log N
N
N log N
N2
2N
Đôi khi chúng ta cũng gặp các tỉ lệ khác như:
log log N (log(log(N)) log* N số các log cho đến khi đạt đến 1
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:
Trang 3C programming 2003 - 2005 5
- xấu nhất (đây là trường hợp đảm bảo thuật toán không vượt
qua ngưỡng này)
- trung bình (mang tính ước lượng)
Việc phân tích phụ thuộc vào các thông tin chi tiết trên
- 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
Để 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)
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ó:
T(N) ≤ c * F(N)
T(N) là độ phức tạp chính xác của một thuật toán được đề nghị để
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
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)
Trên thực tế, bao giờ người ta cũng muốn tính được giá trị F(N)
nhỏ nhất – chi phí nhỏ nhất phải có
Ví dụ: T(N) = 3 * N2 + 5
Dễ thấy rằng, với c = 4 và n0 = 2, ta có:
3 * N2 + 5 ≤ 4 * N2 nghĩa là T(N) là O(N2)
C programming 2003 - 2005 6
Biểu Diễn Xếp Chồng (Stack) Bằng Cấu Trúc DSLK
Các thao tác trên Stack đã được giới thiệu trong phần trước
Chúng ta sẽ cài đặt Stack bằng DSLK
typedef struct intListNode * IntListPtr;
typedef struct intListNode { int data;
return (ps->top == NULL);
} int PushStack (IntStack * ps, int num) {
Con trỏ đến nút đầu tiên trong danh sách các phần tử của stack
Số lượng phần tử trong danh sách (# số phần tử của Stack)
Trang 4int TopStack (IntStack * ps)
{ /* assume that stack is not empty */
return ps->top->data;
}
/* cach 2: cai dat khac cua PopStack, vua day phan
tu o dinh ra khoi stack vua lay gia tri cua
PopStack(&s); // su dung PopStack cach 1 }
}
Trang 5C programming 2003 - 2005 9
Hàng Đợi Bằng Cấu Trúc DSLK
Tương tự như cách biểu diễn Xếp chồng bằng cấu trúc DSLK, hãy
biểu diễn Hàng Đợi cũng bằng cấu trúc DSLK
Danh Sách Liên Kết Kép (Doubly-Linked List)
Danh Sách Liên Kết Hai Đầu (Double Ended Queue)
Danh Sách Liên Kết Tổng Quát
Trong danh sách thường, mỗi phần tử mang dữ liệu riêng
(1,2,3,4) Trong danh sách tổng quát, mỗi phần tử có thể là một danh sách (1,2,(3,4),5)
struct intGenListNode {
struct intGenListNode * subList;
struct intGenListNode * next;
};
Với cấu trúc danh sách tổng quát, chúng ta có thể sử dụng đệ quy
để duyệt và hiển thị nội dung toàn bộ danh sách
void DisplayIntgenList (struct intGenListNode * glptr) {
while (glptr != NULL) {
if (glptr->subList == NULL)
printf ("%d\n", glptr->data);
} else
DisplayIntGenList (glptr->subList);
} glptr = glptr->next;
} }
Head Size
1 null 2 null ? 5 null null
3 null
4 null null
Trang 6Đâ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 12
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)
- 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
Trang 8Thuật toán nổi bọt được cải tiến để chạy nhanh hơn:
- thêm kiểm tra điều kiện dừng nếu không có hoán vị
- nổi bọt hai chiều
C programming 2003 - 2005 16
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
Với các tập tin có các bản ghi gần như đã theo thứ tự bubble sort và insertion sort có thể đạt mức tuyến tính (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)
Trang 9C programming 2003 - 2005 17
Sắp xếp con trỏ
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
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
typedef int itemType
#define less(A, B) (data[A].key < data[B].key)
#define exch(A, B) {itemType t = A; A = B; B = t;}
Trong trường hợp dùng con trỏ đến các bản ghi
typedef dataType* itemType
#define less(A, B) (*A.key < *B.key)
#define exch(A, B) {itemType t = A; A = B; B = t;}
C programming 2003 - 2005 18
Sắp xếp với các bản ghi có hai khóa
Sắp xếp theo khóa thứ nhất, sau đó sắp tiếp theo khóa thứ 2
Battle 4 Chen 2 Chen 2 Furia 3 Fox 1 Kanaga 3 Furia 3 Andrews 3 Gazsi 4 Rohde 3
Trang 10}
Trang 11Cài đặt thuật toán
void shellshort(itemType a[], int l, int r) {
int h = incs[k];
for(i = l+h; i <= r; i++) {
Thuật toán Shellsort có tốc độ sắp xếp nhanh, cài đặt đơn giản; rất 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 Shellsort vẫn có hiệu suất thực hiện rất cao
Tỉ lệ tăng nào là thích hợp?
• có nhiều tỉ lệ đã được chứng minh hiệu quả
• dễ chọn nhất là dùng: 1, 4, 13, 40, 121, 363, 1090,
Trang 12C programming 2003 - 2005 23
Thuật toán CombSoft
Giả sử chúng ta sắp xếp tăng dần một danh sách bằng thuật toán
nổi bọt
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ử
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
lớn, nằm gần đầu danh sách thì lại di chuyển rất nhanh về phía vị
trí của nó (hãy cứ gọi phần tử loại thế này là con thỏ)
Một cải tiến nhỏ, trong đó khoảng cách giữa các phần tử cần so
sánh lớn hơn 1 sẽ cho phép biến các “con rùa” thành “con thỏ”
Sắp xếp nổi bọt
C programming 2003 - 2005 24
Cài đặt thuật toán
#define SHRINKFACTOR 1.3 comb_sort(itemType a[], int size) {
int switches, i, j, top, gap;
top = size - gap;
for(i=0; i<top; ++i) {
}
Đây không phải là thuật toán Shellsort
(tham khảo Stephen Lacey, Richard Box Byte 4,1991)
Trang 13C programming 2003 - 2005 25
Quicksort
Để sắp xếp một mảng, đầu tiên chia mảng thành 3 phần:
Các phần tử a[i] có giá trị bằng nhau không thay đổi vị trí
Các phần tử không lớn hơn a[i] nằm ở bên trái phần tử thứ i
Các phần tử không nhỏ hơn a[i] nằm ở bên phải phần tử thứ i
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
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 A E E T I N G O X S M P L R
A A E
A A
A
L I N G O P M R X T S L I G M O P N
G I L
I L
I
N P O
O P
P
S T X T X T A A E E G I L M N O P R S T X C programming 2003 - 2005 26 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, chúng ta chọn ra một phần tử làm mốc a[i0] • quét mảng từ bên phải sang để tìm phần tử nhỏ hơn a[i0], • quét mảng từ bên trái sang để tìm phần tử lớn hơn a[i0] • hoán vị hai phần tử tìm đượ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 A S O R T I N G E X A M P L E
A S
A M P L A A S M P L E
O
E X
A A E O X S M P L E
R
E R T I N G
A A E E T I N G O X S M P L R
Cài đặt thuật toán phân vùng
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;
v = a[r]; i = l-1; j = r;
for( ; ; )
Trang 14Sẽ 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
- thời gian tăng theo tỉ lệ bình phương
- bộ nhớ tăng tuyến tính
C programming 2003 - 2005 28
Thuật Toán Quicksort Không Đệ Qui
Chúng ta có thể dùng stack để khử tính đệ quy trong cài đặt thuật toán Quicksort ở trên
#define push2(A, B) push(A); push(B);
void quicksort(Item a[], int l, int r) {
Phân tích thuật toán
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
Trang 15• 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:
Bài toán: tìm x trong một mảng
Điều kiện: mảng đã được sắp thứ tự tăng (giảm) dần 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
Tìm kiếm theo phương pháp lặp
Do dãy số đã có thứ tự tăng dần, nên nếu tìm được phần tử đầu 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
tử x Vì vậy, chúng ta có thể rút ngắn thời gian tìm kiếm
Cài đặt thuật toán
int search(int a[], int n, int x) {
int x, pos, size;
else
printf(“%d is not in array\n”, x);
}
Trang 16C programming 2003 - 2005 31
Tìm Kiếm Nhị Phân
1 Phạm vi tìm kiếm ban đầu là toàn bộ danh sách
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
với x
• Nếu a[j] = x trả về chỉ số j STOP
• 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
• Nếu a[j] < x Phạm vi tìm kiếm mới là các phần tử có chỉ
số lớn hơn j
3 Nếu còn tồn tại phạm vi tìm kiếm thì lặp lại bước 2,
ngược lại, không tìm thấy x STOP
Cài đặt thuật toán
int Binary_Search(int a[], int n, int x)
{
// gia su ban dau chua tim duoc
unsigned char found=FALSE;
// Pham vi tìm kiem ban dau tu k=0 den m = n-1
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 k=0 đến m=n-1
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 với x
• Nếu a[j] = x trả về chỉ số j STOP
• 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
là (k, j-1)
• Nếu a[j] < x Phạm vi tìm kiếm mới là các phần tử có chỉ
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
int Binary_Search2(int a[], int k,int m, int x) {
int j=(k+m) /2;
if (k>m) return -1 ; else if (a[j]==x) return j ; else
Binary_Search2(a, (a[j]<x ? j+1:k),
}
Trang 17C programming 2003 - 2005 33
Các Thuật Toán Trên Cấu Trúc Cây
Cây là một cấu trúc dữ liệu rất thông dụng và quan trọng trong
nhiều phạm vi khác nhau của kỹ thuật máy tính
Ví dụ: Tổ chức các quan hệ họ hàng trong một gia phả, mục lục
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
dịch
Khái niệm
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
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 gốc (Root): là nút ở “trên cùng” trong một cây Trên nút gốc
không có nút nào nữa
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
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á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
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
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 con của nút, các nút con của nút con, v.v đến các
nút lá của các nhánh thuộc nút
Đường đi (Path): là chuỗi các cạnh nối từ một nút đến một trong
số các nút hậu duệ của nó
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 34
Độ 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
Chiều cao của cây (height): chiều dài đường đi dài nhất trong
cây
Cây con (subtree): cây bao gồm nút và tất cả các nút hậu duệ của
nó Nút gốc và toàn bộ cây không được xem là cây con
Trang 18C programming 2003 - 2005 35
Cây Nhị Phân
Trong cây nhị phân, mỗi nút có tối đa hai nút con: nút con nhánh
trái và nút con nhánh phải
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
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
cả các nút trung gian đều có 2 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 đầ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
Trang 19C programming 2003 - 2005 37
Cây nhị phân tìm kiếm (Binary search tree)
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
đố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
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ỏ
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 38
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â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ì số nút của cây con bên trái và số nút của cây con bên phải hon kém nhau nhiều nhất là 1
Trang 20C programming 2003 - 2005 39
Các phép duyệt cây nhị phân (Traverse)
Là quá trình đi qua các nút đúng một lần
Preorder (NLR)
Duyệt qua nút gốc trước, sau đó qua cây con bên trái, dùng
preorder cho cây con bên trái Cuối cùng qua cây con bên phải và
dùng preorder cho cây con bên phải
If the node is NULL
Return
Else
Visit the item in the node to do something
Traverse (Preorder) the node’s left subtree
Traverse (Preorder) the node’s right subtree
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
Postorder (LRN)
Qua cây con bên trái duyệt trước (theo thứ tự LRN) Sau đó duyệt qua cây con bên phải (theo thứ tự LRN) Cuối cùng duyệt qua nút gốc
If the node is NULL Return
Else Traverse the node’s left subtree (LRN) Traverse the node’s right subtree (LRN) Visit the item in the node to do something
Ví dụ trong hình: 2Æ6Æ7Æ4Æ8Æ9Æ5Æ3Æ1