Bài tập Kỹ thuật lập trình

Thực hiện n - 1 đợt sắp xếp (vòng lặp ngoài, chỉ số i), mỗi đợt sắp xếp cho vị trí a[i]. Mảng được sắp xếp từ cuối đến đầu. Trong mỗi đợt sắp xếp, ta so sánh các phần tử gần nhau, hoán đổi chúng cho đúng chiều sắp xếp. Chuỗi “so sánh - hoán đổi” liên tục này sẽ dần dần đẩy phần tử a[i] “nổi” dần lên đúng vị trí sắp xếp. Mảng được sắp xếp từ đầu đến cuối. Ta nhận thấy vòng lặp bên ngoài (i) đi ngược từ cuối mảng lên đầu. Thao tác này khó thực hiện được khi dùng danh sách liên kết đơn, chỉ có thể thực hiện trên danh sách liên kết đôi. Tuy nhiên, do một số đặc điểm của hai vòng lặp trên, ta vẫn có thể điều khiển được một con trỏ đi ngược từ cuối danh sách liên kết đơn lên đầu danh sách bằng một thủ thuật nhỏ sau: for ( p = NULL; p != l->next; p = q ) for ( q = l; q->next != p; q = q->next ) if ( q->data > q->next->data ) Swap( q->data, q->next->data ); Vòng lặp trong (j) dừng khi q->next chỉ đến con trỏ “chặn dưới” p (lần đầu gán p bằng NULL). Như vậy q chỉ đến node trước p; sau mỗi vòng lặp ngoài (i), ta đơn giản gán cho p trị chứa trong q, kết quả là con trỏ “chặn dưới” p di chuyển ngược lên đầu một node.

pdf343 trang | Chia sẻ: hachi492 | Ngày: 07/01/2022 | Lượt xem: 370 | Lượt tải: 0download
Bạn đang xem trước 20 trang tài liệu Bài tập Kỹ thuật lập trình, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
p->data, q->data ); } Xem lại giải thuật sắp xếp Selection Sort trên mảng: #define Swap( a, b ) { int t = a; a = b; b = t; } /* ... */ void selectsort( int a[], int n ) { int i, j; for ( i = 0; i < n - 1; ++i ) for ( j = i + 1; j < n; ++j ) if ( a[i] > a[j] ) Swap( a[i], a[j] ); } Thực hiện n - 1 đợt sắp xếp (vòng lặp ngoài, chỉ số i), mỗi đợt sắp xếp cho vị trí a[i]. Mảng được sắp xếp từ đầu đến cuối. Ðể sắp xếp đúng vị trí a[i], ta so sánh a[i] với các phần tử sau nó (từ j = i + 1 trở đi), chọn ra phần tử nhỏ nhất đặt vào vị trí a[i], bằng cách duyệt các phần tử sau a[i] (vòng lặp trong, chỉ số j), phần tử đang duyệt a[j] nào nhỏ hơn phần tử a[i] đang xét thì hoán đổi với a[i]. Danh sách liên kết có những thao tác tương tự như trên mảng: Mảng a Danh sách liên kết l Ý nghĩa với danh sách liên kết i = 0 p = l Chỉ đến đầu danh sách i next != NULL Chưa đến cuối danh sách i++ p = p->next Đến phần tử kế tiếp j = i + 1 q = p->next Phần tử này ngay sau phần tử kia a[i] > a[j] p->data > q->data So sánh hai trị Từ những nhận xét trên ta dễ dàng viết được giải thuật Selection Sort trên danh sách liên kết đơn chứa các trị nguyên. Bài 217: (trang 62) NODEPTR SwapTwoNode( NODEPTR p ) { NODEPTR t = p->next->next->next; p->next->next->next = p->next; (c) Dương Thiên Tứ www.trainingwithexperts.com 316 p->next = p->next->next; p->next->next->next = t; return p->next->next; } void Solution( NODEPTR* l, int x ) { NODEPTR p, g; /* chèn đầu ghost node, không cần lưu trị trong ghost node */ g = ( NODEPTR )malloc( sizeof( struct NODE ) ); g->next = *l; *l = g; for ( p = g; p->next && p->next->next; ) p = ( p->next->data == x ) ? SwapTwoNode( p ) : p->next; /* xóa đầu ghost node */ *l = ( *l )->next; free( g ); } /* gọi trong hàm main() */ printf( "Nhap k: " ); scanf( "%d", &k ); Solution( &l, k ); Đảo hai node kế tiếp nhau trong một danh sách liên kết được thực hiện như sau: Cần chú ý nguyên tắc: - Khi thay đổi một thông tin liên kết phải bảo đảm thông tin đó được lưu lại. Ví dụ: trước khi thay đổi p->next->next->next ta lưu thông tin này vào t. - Nếu không lưu thông tin liên kết lại trước khi thay đổi chúng thì phải bảo đảm là còn cách khác để nhận được thông tin này. Ví dụ: node a do p->next chỉ đến, ta thay p->next bằng p->next->next được vì sau đó vẫn xác định được node a do con trỏ p->next->next chỉ đến. - Một thông tin liên kết không còn ý nghĩa nữa thì có thể thay đổi mà không cần lưu lại. Ví dụ: ở bước cuối, thông tin liên kết đến node b đã được p->next lưu nên có thể thay đổi thông tin liên kết từ node a trỏ đi. Khi đảo hai node trong danh sách liên kết có các ngoại lệ sau: - Node a là node cuối, khi đó p->next->next = NULL, ta kiểm tra và không thực hiện b a p b a b a b a t = p->next->next->next t t p p->next->next->next = p->next p t p->next = p->next->next p->next->next->next = t p t (c) Dương Thiên Tứ www.trainingwithexperts.com 317 hoán chuyển nữa. - Node a là node đầu, ta chèn thêm node giả (ghost node) vào để đưa về trường hợp chung. Có thể nhận thấy quá trình tìm node để đảo không xét node giả này. - Danh sách rỗng (không kể node giả), khi đó p->next = NULL, ta kiểm tra và không thực hiện hoán chuyển nữa. Để đảo nhiều node chứa trị k yêu cầu ta cần chú ý khi đảo hai node, node chứa trị k chuyển về phía sau; nếu vòng lặp tiến đến node kế tiếp bằng p = p->next ta sẽ gặp lại node vừa đảo. Vì thế hàm SwapTwoNode() dùng đảo hai node được thiết kế trả về p->next->next để vượt qua node vừa đảo. Bài 218: (trang 62) void Swap( NODEPTR* a, NODEPTR* b ) { NODEPTR t = *a; *a = *b; *b = t; } void Solution( NODEPTR* l ) { NODEPTR p, p1, q, q1; for ( p1 = p = *l; p->next; p1 = p, p = p->next ) for ( q1 = q = p->next; q; q1 = q, q = q->next ) if ( p->data > q->data ) { Swap( &p->next, &q->next ); ( p == *l ) ? Swap( l, &q1->next ) : Swap( &p1->next, &q1->next ); Swap( &p, &q ); } } Vấn đề được giải quyết giống bài 216 (trang 315). Tuy nhiên, ta cần hoán chuyển hai node, không phải hoán chuyển trị chứa trong chúng. Trong danh sách liên kết, thông tin vị trí một node còn lưu trữ trong node truớc nó. Vì vậy một số thao tác trên một node cần đến con trỏ quản lý node trước node đó. Để có được con trỏ này người ta thường dùng kỹ thuật “hai con trỏ nối đuôi nhau”. for ( p1 = p = l; p->next; p1 = p, p = p->next ) { } p chạy từ đầu danh sách l đến cuối danh sách. Trước khi p di chuyển, dùng p1 để lưu lại vị trí cũ của p, vậy p1 luôn là con trỏ quản lý node đứng trước p. Chú ý nếu p chưa di chuyển, p1 và p chỉ chung một node. Khi hoán chuyển hai node, trước tiên hoán chuyển thông tin liên kết đến node sau trong nó, sau đó hoán chuyển thông tin liên kết đến nó chứa trong node ngay trước. Các bước cần thực hiện khi hoán chuyển hai node p (có node trước là p1) và q (có node trước là q1) trình bày trong hình trang sau: - Hoán chuyển thông tin liên kết chỉ đến node sau, chứa trong node đang xét. Swap( p->next, q->next ); - Hoán chuyển thông tin liên kết chỉ đến node đang xét, chứa trong node ngay trước node đang xét. Chú ý trường hợp node đang xét là node đầu. if ( p == l ) Swap( l, q1->next ); else Swap( p1->next, q1->next ); - Sau khi hoán chuyển vai trò của p và q đổi chỗ nhau nên cần hoán chuyển chúng trở lại: Swap( p, q ); (c) Dương Thiên Tứ www.trainingwithexperts.com 318 Hình dưới mô tả một vài trường hợp đặc biệt: hai node kế tiếp nhau (trái), một trong hai node là node đầu (phải). Bạn có thể kiểm chứng tính chính xác của cách hoán chuyển node trên. Bài 219: (trang 62) #define Swap( a, b ) { int t = a; a = b; b = t; } /* ... */ void Solution( NODEPTR l ) { NODEPTR p, q; for ( p = NULL; p != l->next; p = q ) for ( q = l; q->next != p; q = q->next ) if ( q->data > q->next->data ) Swap( q->data, q->next->data ); } Xem lại giải thuật sắp xếp Bubble Sort trên mảng: 1 l 3 2 p1 p q1 q 5 4 1 l 3 2 p1 p q1 q 5 4 1 l 3 2 p1 p q1 q 5 4 1 l 3 4 q p 5 2 1 l 4 3 2 p1 p q q1 1 l 4 3 2 p1 p q q1 1 l 4 3 2 p1 p q q1 1 l 4 2 3 q p 1 l 4 3 2 q q1 l 2 l 4 3 1 q p 1 l 4 3 2 q q1 p p1 p p1 l 1 4 3 2 q l p p1 (c) Dương Thiên Tứ www.trainingwithexperts.com 319 #define Swap( a, b ) { int t = a; a = b; b = t; } /* ... */ void bubblesort( int a[], int n ) { int i, j; for ( i = n - 1; i > 0; --i ) for ( j = 0; j < i; ++j ) if ( a[j] > a[j + 1] ) Swap( a[j], a[j+1] ); } Thực hiện n - 1 đợt sắp xếp (vòng lặp ngoài, chỉ số i), mỗi đợt sắp xếp cho vị trí a[i]. Mảng được sắp xếp từ cuối đến đầu. Trong mỗi đợt sắp xếp, ta so sánh các phần tử gần nhau, hoán đổi chúng cho đúng chiều sắp xếp. Chuỗi “so sánh - hoán đổi” liên tục này sẽ dần dần đẩy phần tử a[i] “nổi” dần lên đúng vị trí sắp xếp. Mảng được sắp xếp từ đầu đến cuối. Ta nhận thấy vòng lặp bên ngoài (i) đi ngược từ cuối mảng lên đầu. Thao tác này khó thực hiện được khi dùng danh sách liên kết đơn, chỉ có thể thực hiện trên danh sách liên kết đôi. Tuy nhiên, do một số đặc điểm của hai vòng lặp trên, ta vẫn có thể điều khiển được một con trỏ đi ngược từ cuối danh sách liên kết đơn lên đầu danh sách bằng một thủ thuật nhỏ sau: for ( p = NULL; p != l->next; p = q ) for ( q = l; q->next != p; q = q->next ) if ( q->data > q->next->data ) Swap( q->data, q->next->data ); Vòng lặp trong (j) dừng khi q->next chỉ đến con trỏ “chặn dưới” p (lần đầu gán p bằng NULL). Như vậy q chỉ đến node trước p; sau mỗi vòng lặp ngoài (i), ta đơn giản gán cho p trị chứa trong q, kết quả là con trỏ “chặn dưới” p di chuyển ngược lên đầu một node. Bài 220: (trang 62) void Solution( NODEPTR* l ) { NODEPTR t, t1, t2; NODEPTR p; if ( *l ) { /* list p có 1 phần tử "đã sắp xếp", là phần tử đầu list l */ p = *l; *l = ( *l )->next; p->next = NULL; while ( *l ) { /* xóa đầu l từng node, lưu node xóa vào t */ t = *l; *l = ( *l )->next; t->next = NULL; /* chèn t vào danh sách "đã sắp xếp" p */ t1 = NULL; t2 = p; while ( t2 && t2->data data ) { t1 = t2, t2 = t2->next; } if ( !t1 ) { t->next = p; p = t; } else { t->next = t1->next; t1->next = t; } } (c) Dương Thiên Tứ www.trainingwithexperts.com 320 /* cập nhật lại l với p "đã sắp xếp" */ *l = p; } } Giải pháp dựa trên giải thuật sắp xếp Insertion Sort trên mảng: void insertsort( int a[], int n ) { int v, i, j; for ( i = 1; i < n; ++i ) { v = a[i]; /* lưu a[i] vào v để dồn mảng */ j = i; /* dồn mảng từ a[i] ngược lên đến vị trí chèn */ while ( a[j - 1] > v ) { a[j] = a[j - 1]; j--; } a[j] = v; /* j là vị trí chèn, đưa v vào */ } } Thuật toán chia mảng thành hai phần; - Phần “đã sắp xếp”, nằm đầu mảng, được khởi tạo có một phần tử a[0]. - Phần còn lại là phần chưa sắp xếp. Từng phần tử trong phần này sẽ được chèn đúng vị trí vào phần “đã sắp xếp”. Ta thực hiện tương tự trên danh sách liên kết: - Tạo một danh sách “đã sắp xếp” do p quản lý, được khởi tạo có một phần tử là phần tử đầu danh sách l cần sắp xếp. - Xóa đầu dần danh sách l, từng node xóa chuyển cho t quản lý. Chèn t vào danh sách có thứ tự p sao cho vẫn bảo đảm thứ tự đó, xem bài 203 (trang 301). Bài 221: (trang 63) Gọi t là con trỏ chỉ đến node cần xóa. Ta sẽ xóa node này không phải thông qua t mà thông qua một con trỏ khác cũng chỉ đến node cần xóa, con trỏ d với d = t. Có ba trường hợp: - Node cần xóa không có cây con bên phải (t->right = NULL) - Node cần xóa không có cây con bên trái (t->left = NULL) - Node cần xóa có đủ các cây con bên trái và bên phải, tức node cần xóa là node trong. a) Node cần xóa không có cây con bên phải Vì node cần xóa đã được giữ bởi con trỏ d, ta đơn giản chỉ “vượt” qua node cần xóa xuống cây con bên trái: if ( t->right == NULL ) t = t->left; 18 12 t->left 6 7 4 10 15 8 d = t (c) Dương Thiên Tứ www.trainingwithexperts.com 321 Node cần xóa bây giờ đã tách khỏi cây và được quản lý bởi con trỏ d, có thể xóa dễ dàng: free( d ); b) Node cần xóa không có cây con bên trái Tương tự trường hợp trên, chỉ “vượt” qua node cần xóa xuống cây con bên phải: if ( t->left == NULL ) t = t->right; free( d ); c) Node cần xóa là node trong Khi xóa node t, node thay thế node t phải bảo đảm tính chất của cây BST, nghĩa là: phải có trị lớn hơn tất cả các trị trong các node của cây con bên trái node t, đồng thời phải có trị nhỏ hơn tất cả các trị trong các node của cây con bên phải node t. Vậy node thay thế node t phải là node cực phải của cây con bên trái node t (tức node lớn nhất trong cây con bên trái, gọi là node “thay thế trước”) hoặc node cực trái của cây con bên phải node t (tức node nhỏ nhất của cây con bên phải, gọi là node “thay thế sau”). Ta quy ước chọn node thay thế là node cực phải của cây con bên trái node t (node “thay thế trước”). Có hai cách xóa một node trong: - Cách thứ nhất, xóa bằng cách sao chép node. Tìm node “thay thế trước” là node phải nhất của cây con bên trái node t. Ta bắt đầu từ node gốc của cây con bên trái (node t->left, nghĩa là d = t->left), sau đó luôn đi về bên phải (d = d->right) cho đến khi không còn đi tiếp được, nghĩa là khi con trỏ d->right == NULL. Trong khi con trỏ d di chuyển, ta dùng con trỏ p nối đuôi con trỏ d, tức p luôn chỉ node cha của node đang xét (node đang xét do d chỉ). for ( p = t, d = t->left; d->right; p = d, d = d->right ) { } Kết thúc vòng lặp trên d sẽ chỉ node “thay thế trước” và p là cha của d. Sao chép chồng dữ liệu chứa trong node “thay thế trước” vào node cần xóa: 4 8 t->right 24 20 27 10 6 18 d = t 10 8 20 15 12 18 25 16 23 27 p d (c) Dương Thiên Tứ www.trainingwithexperts.com 322 t->data = d->data; Bây giờ có cây 2 node chứa dữ liệu giống nhau, ta sẽ xóa node “thay thế trước” vì node này hoặc là node lá, hoặc chỉ có một cây con bên trái nên dễ dàng xóa hơn. Node cha của node “thay thế trước” là p, lưu được do dùng kỹ thuật hai con trỏ nối đuôi ở bước trước. Cho p chỉ đến node gốc của cây bên trái node “thay thế trước”: p->right = d->left; Node “thay thế trước” bây giờ đã tách khỏi cây và quản lý bởi con trỏ d, ta xóa d: free( d ); Khi tìm node “thay thế trước”, có một trường hợp đặc biệt là node “thay thế trước” nằm ngay sau node cần xóa. Kết quả vòng lặp tìm node “thay thế trước” như sau: Trong trường hợp này p = t và d chính là p->left. Sau khi sao chép chồng dữ liệu chứa trong node “thay thế trước” vào node cần xóa: t->data = d->data; ta tách node “thay thế trước” ra khỏi cây bằng cách: p->left = d->left; 10 8 18 15 12 18 25 16 23 27 p d 10 8 18 15 12 18 25 16 23 27 p p->right cũng là d d->left 10 8 18 15 12 25 23 27 p = t 10 14 d (c) Dương Thiên Tứ www.trainingwithexperts.com 323 Node “thay thế trước” bây giờ đã tách khỏi cây và quản lý bởi con trỏ d, ta xóa d: free( d ); Tổng kết cách 1: void RemoveNodeByCopy( NODEPTR* t ) { NODEPTR p, d = *t; if ( !( *t )->left ) *t = ( *t )->right; else if ( !( *t )->right ) *t = ( *t )->left; else { for ( p = *t, d = ( *t )->left; d->right; p = d, d = d->right ) { } ( *t )->data = d->data; if ( p == *t ) p->left = d->left; else p->right = d->left; } free( d ); } - Cách thứ hai, xóa node bằng ghép cây (merge). Cách thứ hai để xóa node trong, gọn hơn cách trên, nhưng có một vài khác biệt khi tiến hành xóa node. Tìm node “thay thế trước” là node phải nhất của cây con bên trái node t, không cần dùng kỹ thuật hai con trỏ nối đuôi nhau. Ta bắt đầu từ node gốc của cây con bên trái (node t->left, nghĩa là p = t->left), sau đó luôn đi về bên phải (p = p->right) cho đến khi không còn đi tiếp được, nghĩa là khi con trỏ p->right == NULL. for ( p = t->left; p->right; p = p->right ) { } Kết thúc vòng lặp trên p sẽ chỉ node “thay thế trước”. Cây con bên phải node cần xóa sẽ được ghép vào bên phải node “thay thế trước” thành cây con bên phải node “thay thế trước”, chú ý t->right là con trỏ quản lý cây con bên phải của node t. p->right = t->right; p->left cũng là d d->left 10 8 15 15 12 25 23 27 p = t 10 14 10 8 20 15 12 18 25 16 23 27 (c) Dương Thiên Tứ www.trainingwithexperts.com 324 Như vậy, xem như node cần xóa không còn cây con bên phải (t->right vẫn tồn tại nhưng không còn ý nghĩa). Bạn có thể xóa dễ dàng bằng cách “vượt” qua node cần xóa xuống cây con bên trái: t = t->left; Node cần xóa bây giờ đã tách khỏi cây và quản lý bởi con trỏ d, có thể xóa dễ dàng: free( d ); Cách này tuy đơn giản hơn nhưng dễ làm cho cây mất cân bằng, gây ảnh hưởng đến hiệu suất tìm kiếm. Tổng kết cách 2: void RemoveNodeByMerge( NODEPTR* t ) { NODEPTR p, d = *t; if ( !( *t )->left ) *t = ( *t )->right; else if ( !( *t )->right ) *t = ( *t )->left; else { for ( p = ( *t )->left; p->right; p = p->right ) { } p->right = ( *t )->right; *t = ( *t )->left; } free( d ); } d) Xóa một node chứa trị chỉ định Trên cơ sở thao tác xóa một node ở trên, ta tiến hành xóa một node với trị chỉ định trong cây BST bằng cách đệ quy: 10 8 20 15 12 18 25 16 23 27 10 8 20 15 12 18 25 16 23 27 d = t t->left (c) Dương Thiên Tứ www.trainingwithexperts.com 325 - Nếu node đang xét chứa trị trùng với trị chỉ định thì xóa ngay node đó và dừng đệ quy. - Nếu node đang xét chứa trị nhỏ hơn trị chỉ định thì tiến hành xóa đệ quy node chứa trị chỉ định trong cây bên trái node đang xét. - Ngược lại, tiến hành xóa đệ quy node chứa trị chỉ định trong cây bên phải node đang xét. void Remove( NODEPTR* t, int x ) { if ( x == ( *t )->data ) { RemoveNode( t ); return; } ( x data ) ? Remove( &( *t )->left, x ) : Remove( &( *t )->right, x ); } /* dùng trong hàm main() */ Remove( &t, k ); Ta nhận thấy thao tác xóa một node (RemoveNode()) cách 1 tuy có ưu điểm hơn nhưng phải giải quyết trường hợp ngoại lệ và phải xác định node cha của node thay thế bằng kỹ thuật dùng hai con trỏ nối đuôi nhau. Nếu viết hàm RemoveNode() cách 1 ngay trong hàm đệ quy Remove(), cách 1 sẽ được thực hiện đơn giản như cách 2, xem chú thích trong bài: void Remove( NODEPTR* t, int x ) { if ( x == ( *t )->data ) { NODEPTR p, d = *t; if ( ( *t )->left && ( *t )->right ) { /* node có 2 con */ /* tìm node thay thế như cách 2, không cần tìm node cha của node thay thế */ for ( p = ( *t )->left; p->right; p = p->right ) { } /* hoán chuyển trị của node xóa và node thay thế */ int temp = ( *t )->data; ( *t )->data = p->data; p->data = temp; /* trị cần xóa đã "xuống" cây con bên trái */ /* gọi đệ quy với cây con bên trái để xóa nó */ Remove( &( *t )->left, x ); } else { *t = ( !( *t )->left ) ? ( *t )->right : ( *t )->left; free( d ); } return; } ( x data ) ? Remove( &( *t )->left, x ) : Remove( &( *t )->right, x ); } Mục tiêu của cách xóa node này là chuyển node cần xóa có hai cây con trái phải thành node xóa chỉ có một cây con trái hoặc phải. Việc xóa node giảm thiểu khả năng mất cân bằng của cây. Bài 222: (trang 63) void LNR( NODEPTR t ) { if ( t ) { LNR( t->left ); printf( "%d ", t->data ); (c) Dương Thiên Tứ www.trainingwithexperts.com 326 LNR( t->right ); } } void NLR( NODEPTR t ) { if ( t ) { printf( "%d ", t->data ); NLR( t->left ); NLR( t->right ); } } void LRN( NODEPTR t ) { if ( t ) { LRN( t->left ); LRN( t->right ); printf( "%d ", t->data ); } } Phương pháp mô tả sau là một trong những cách duyệt cây BST một cách thủ công, giúp kiểm tra nhanh các kết quả duyệt theo chiều sâu: - Đi theo một “đường bao” xung quanh các node cây nhị phân từ trái sang phải (nếu trong thứ tự duyệt L trước R). Khi di chuyển ta duyệt qua các node của cây 3 lần, tương ứng với ba cách duyệt cây. - Thứ tự xuất các node được xác định như sau (chú ý các mũi tên ): LNR: (inorder) xuất trị của node khi đi qua ngay “dưới” node. NLR: (preorder) xuất trị của node khi đi qua ngay “trước” (bên trái) node. LRN: (postorder) xuất trị của node khi đi qua ngay “sau” (bên phải) node. Các cách duyệt ngược lại (backward): RNL, NRL và RLN cũng tiến hành tương tự. LNR: 2 3 4 5 8 9 14 16 18 NLR: 9 3 2 5 4 8 16 14 18 LRN: 2 4 8 5 3 14 18 16 9 Ý nghĩa mỗi cách duyệt theo chiều sâu được trình bày trong bài 224 (trang 329). 9 3 16 2 5 14 18 4 8 End Start End Start 9 3 16 2 5 14 18 4 8 9 3 16 2 5 14 18 4 8 End Start (c) Dương Thiên Tứ www.trainingwithexperts.com 327 Bài 223: (trang 63) #include #include struct NODE { int data; struct NODE *left, *right; }; typedef struct NODE* NODEPTR; /* cấu trúc dữ liệu cho queue */ struct QNODE { NODEPTR data; /* dữ liệu của queue là một node của BST */ struct QNODE *next; }; typedef struct QNODE* QNODEPTR; typedef struct { QNODEPTR qdata; /* danh sách liên kết chứa các node của queue */ QNODEPTR front, rear; /* con trỏ quản lý đầu và cuối queue */ } QUEUE; /* chèn dữ liệu vào queue - chèn cuối nhờ con trỏ rear */ void InQueue( QUEUE* q, NODEPTR p ) { QNODEPTR h = ( QNODEPTR )malloc( sizeof( struct QNODE ) ); h->data = p; h->next = NULL; if ( ( *q ).rear ) ( *q ).rear->next = h; ( *q ).rear = h; if ( !( *q ).front ) ( *q ).front = h; } /* trả về dữ liệu xuất từ queue - xóa đầu */ NODEPTR OutQueue( QUEUE* q ) { NODEPTR t = ( *q ).front->data; QNODEPTR d = ( *q ).front; ( *q ).front = ( *q ).front->next; if ( !( *q ).front ) ( *q ).rear = NULL; free( d ); return t; } void Insert( NODEPTR* t, int x ) { if ( !*t ) { *t = ( NODEPTR )malloc( sizeof( struct NODE ) ); ( *t )->data = x; ( *t )->left = ( *t )->right = NULL; } else ( x data ) ? Insert( &( *t )->left, x ) : Insert( &( *t )->right, x ); } void InTree( NODEPTR* t ) { int x; (c) Dương Thiên Tứ www.trainingwithexperts.com 328 printf( "Nhap 0 de dung: " ); do { scanf( "%d", &x ); if ( x ) Insert( t, x ); } while ( x ); } void BFS( NODEPTR t ) { QUEUE q; q.front = q.rear = NULL; if ( t ) InQueue( &q, t ); while ( q.front ) { NODEPTR d = OutQueue( &q ); printf( "%d ", d->data ); if ( d->left ) InQueue( &q, d->left ); if ( d->right ) InQueue( &q, d->right ); } } int main() { NODEPTR t = NULL; InTree( &t ); printf( "BFS: " ); BFS( t ); putchar( '\n' ); return 0; } Duyệt theo chiều rộng cây BST là lần lượt duyệt từng mức (level-by-level) của cây, tại mỗi mức duyệt các node từ trái sang phải: BFS: 9 3 16 2 5 14 18 4 8 Khi duyệt một node ta đồng thời biết được thông tin liên kết đến hai node con, thông tin này được lưu trữ vào một cấu trúc dữ liệu bổ trợ (helper data structure) nào đó để xử lý sau. Như vậy cấu trúc dữ liệu bổ trợ đó phải đáp ứng: - Xử lý tuần tự. - Ta đặt node cha vào cấu trúc dữ liệu đó trước các node con. Sau đó node cha được xử lý trước các node con. Cấu trúc dữ liệu bổ trợ thích hợp là queue (hàng đợi), hoạt động theo nguyên tắc FIFO tuần tự, được xây dựng bằng danh sách liên kết đơn, với các thao tác: - InQueue(): chèn cuối một node vào queue, dữ liệu trong node của queue chính là một node của cây BST. - OutQueue(): xóa đầu một node của queue, trước khi xóa dữ liệu được lưu lại để trả về. Trong queue, thao tác nhập/xuất diễn ra ở hai đầu khác nhau của queue. 9 3 16 2 5 14 18 4 8 9 cấp 0 3 16 cấp 1 2 5 14 18 cấp 2 4 8 cấp 3 (c) Dương Thiên Tứ www.trainingwithexperts.com 329 Để thực hiện nhanh thao tác chèn cuối, ta xây dựng thêm cấu trúc QUEUE dùng quản lý queue, trong đó đưa thêm con trỏ rear nhằm hỗ trợ cho thao tác chèn cuối. Nếu dùng cấu trúc dữ liệu bổ trợ là stack, ta có kết quả như cách duyệt NRL. Một cách duyệt BFS khác, không dùng cấu trúc dữ liệu bổ trợ mà dùng đệ quy: #define Max( a, b ) ( ( a ) > ( b ) ? ( a ) : ( b ) ) /* ... */ void PrintLevel( NODEPTR t, int cLevel, int tLevel ) { if ( t ) if ( cLevel == tLevel ) printf( "%d ", t->data ); else { PrintLevel( t->left, cLevel + 1, tLevel ); PrintLevel( t->right, cLevel + 1, tLevel ); } } int MaxLevel( NODEPTR t ) { if ( !t ) return 0; return 1 + Max( MaxLevel( t->left ), MaxLevel( t->right ) ); } void BFS( NODEPTR t ) { int i; int maxLevel = MaxLevel( t ); for ( i = 0; i < maxLevel; ++i ) PrintLevel( t, 0, i ); } Ta dùng hai hàm đệ quy: - Hàm PrintLevel( t, cLevel, tLevel ), duyệt đệ quy cây t và in các node ở mức tLevel. cLevel là mức hiện hành, dùng xác định xem có đang duyệt mức tLevel hay không. Vì vậy khi bắt đầu duyệt, truyền tham số cLevel bằng 0 (mức 0). - Hàm MaxLevel( t ) trả về mức sâu nhất (chiều cao) của cây t, cộng 1. Bạn cũng có thể tìm chiều cao của cây trong khi chèn node để tạo cây, không nhất thiết dùng đến hàm này. Sau đó, hàm BFS() chỉ đơn giản chạy vòng lặp duyệt từ mức 0 đến mức sâu nhất. Tại mỗi mức, gọi hàm PrintLevel() để in các phần tử thuộc mức đó. Tuy nhiên, do cả hai hàm đệ quy trên đều hoạt động không hiệu quả nên phương pháp này không tối ưu. Ý nghĩa cách duyệt theo chiều rộng được trình bày trong bài 225 (trang 332). Bài 224: (trang 63) #include #include struct NODE { int data; struct NODE *left, *right; }; typedef struct NODE* NODEPTR; void Insert( NODEPTR* t, int x ) { if ( !*t ) { (c) Dương Thiên Tứ www.trainingwithexperts.com 330 *t = ( NODEPTR )malloc( sizeof( struct NODE ) ); ( *t )->data = x; ( *t )->left = ( *t )->right = NULL; } else ( x data ) ? Insert( &( ( *t )->left ), x ) : Insert( &( ( *t )->right ), x ); } void InTree( NODEPTR* t ) { int x; printf( "Nhap 0 de dung: " ); do { scanf( "%d", &x ); if ( x ) Insert( t, x ); } while ( x ); } void OutTree( NODEPTR t ) { if ( t ) { printf( "%d ", t->data ); OutTree( t->left ); OutTree( t->right ); } } void CopyTree( NODEPTR t, NODEPTR* t1 ) { if ( t ) { Insert( t1, t->data ); CopyTree( t->left, t1 ); CopyTree( t->right, t1 ); } } void SortTree( NODEPTR t ) { if ( t ) { SortTree( t->left ); printf( "%d ", t->data ); SortTree( t->right ); } } void RemoveTree( NODEPTR* t ) { if ( *t ) { RemoveTree( &( ( *t )->left ) ); RemoveTree( &( ( *t )->right ) ); printf( "%d ", ( *t )->data ); free( *t ); } } int main() { NODEPTR t, t1; t = t1 = NULL; InTree( &t ); printf( "\nCay goc : " ); OutTree( t ); (c) Dương Thiên Tứ www.trainingwithexperts.com 331 CopyTree( t, &t1 ); printf( "\nCay copy : " ); OutTree( t1 ); printf( "\nXuat tang: " ); SortTree( t ); printf( "\nXoa cay goc... " ); RemoveTree( &t ); if ( t ) printf( "\nCay goc rong\n" ); return 0; } Duyệt cây BST là lần lượt đi qua tất cả các node của cây và thực hiện một thao tác nào đó trên mỗi node. Tùy thao tác cần thực hiện, ta chọn cách duyệt cây thích hợp cho thao tác đó: - InOrder: vì trị các node chứa trong nhánh phải của cây BST lớn hơn trị các node chứa trong nhánh trái và node gốc chứa trị trung gian, ta dùng cách duyệt LNR để xuất trị chứa trong các node cây BST theo chiều tăng dần (L < N < R). Tương tự, cách duyệt RNL dùng xuất trị chứa trong các node cây BST theo chiều giảm dần (L > N > R). Cách duyệt này thật sự không ý nghĩa với cây tổng quát. - PreOrder: khi muốn tạo một cây BST mới sao chép từ cây gốc, ta duyệt từng node trên cây gốc và chèn trị đọc được vào cây sao chép. Để cây sao chép có cùng cấu trúc với cây gốc, phải bảo đảm thứ tự chèn node vào cây sao chép giống như ở cây gốc, tạo node gốc trước rồi mới tạo các node con. Cách duyệt NLR hoặc NRL đáp ứng được yêu cầu này. Do đặc điểm trên, cách duyệt này còn dùng khi muốn so sánh cấu trúc của hai cây BST: void CompareTree( NODEPTR t, NODEPTR t1, int* b ) { if ( t && t1 ) { if ( t->data != t1->data ) { *b = 0; return; } CompareTree( t->left, t1->left, b ); CompareTree( t->right, t1->right, b ); } } /* dùng trong hàm main() */ int b = 1; CompareTree( t, t2, &b ); if ( b ) printf( "\nCung cau truc cay\n" ); else printf( "\nKhac cau truc cay\n" ); Có thể dùng cách xuất sau để dễ dàng quan sát cấu trúc cây hơn: void OutTree( NODEPTR t, char label, int d ) { if ( t ) { OutTree( t->right, 'L', d + 1 ); printf( "%*c[%c%d]\n", 2 * d, ' ', label, t->data ); OutTree( t->left, 'R', d + 1 ); } } /* dùng trong hàm main() */ printf( "Cay goc:\n" ); OutTree( t, '*', 1 ); Kết quả xuất: (c) Dương Thiên Tứ www.trainingwithexperts.com 332 Nhap 0 de dung: 9 3 7 16 14 2 5 4 8 18 0  Cay goc: [L18] [L16] [R14] [*9] [L8] [L7] [R5] [R4] [R3] [R2] Cách duyệt PreOrder còn dùng khi muốn tìm một node trong cây BST. Các phương pháp duyệt đều duyệt qua các tất cả các node trong cây, nhưng phương pháp PreOrder xử lý node ngay từ lần đầu tiên gặp node. - PostOrder: Khi muốn xóa tất cả các node trong một nhánh cây hoặc cả cây BST, ta tiến hành xóa các node con (trái và phải) rồi mới xóa node cha của chúng. Vì vậy ta dùng cách duyệt LRN hoặc RLN cho thao tác này. Bài 225: (trang 63) void Solution( int n, NODEPTR t, int curlevel, int* sum ) { if ( t ) { if ( curlevel == n ) { printf( "%d ", t->data ); *sum += t->data; } else if ( curlevel < n ) { Solution( n, t->left, curlevel + 1, sum ); Solution( n, t->right, curlevel + 1, sum ); } } } /* dùng trong hàm main() */ s = 0; Solution( n, t, 0, &s ); printf( "\nTong = %d\n", s ); Ta duyệt đệ quy cây từ node gốc đến mức n yêu cầu, mức đang duyệt được xác định bởi curlevel được truyền như tham số. curlevel được khởi tạo mức 0 khi gọi hàm, khi đệ quy xuống nhánh trái hay phải curlevel tăng 1 đơn vị. Nếu curlevel bằng mức n yêu cầu thì xuất nội dung node lưu trữ. Để tính tổng các node thuộc mức n, ta truyền bằng con trỏ thêm một biến sum, những thay đổi của biến sum sẽ được lưu qua các lần gọi đệ quy. Cách trên dễ hiểu nhưng sum được truyền bằng con trỏ nên dễ nhầm lẫn. Một phương án khác: int Solution( int n, NODEPTR t, int curlevel ) { if ( t ) { if ( curlevel == n ) { printf( "%d ", t->data ); return t->data; } else if ( curlevel < n ) { return Solution( n, t->left, curlevel + 1 ) + Solution( n, t->right, curlevel + 1 ); (c) Dương Thiên Tứ www.trainingwithexperts.com 333 } } return 0; } Bài 226: (trang 63) int Solution( int x, NODEPTR t, int level ) { if ( !t ) return -1; if ( t->data == x ) return level; return ( x data ) ? Solution( x, t->left, level + 1 ) : Solution( x, t->right, level + 1 ); } /* dùng trong hàm main() */ n = Solution( x, t, 0 ); if ( n == -1 ) printf( "Khong tim thay\n" ); else printf( "Muc %d\n", n ); Ta tìm x trong cây t bằng cách duyệt cây, có hai trường hợp dừng: - Duyệt đến node lá (t = NULL) mà vẫn không tìm thấy, trả về -1 (mức không thể có trong cây). Chú ý, nếu chèn node chứa trị x vào, t = NULL chính là nơi chèn node. - Tìm thấy node (x = t->data) ta trả về mức level. Mức level khởi tạo bằng 0 (mức xuất phát) khi gọi hàm và sẽ tăng 1 đơn vị khi duyệt xuống cây con trái hoặc phải. Khi không gặp hai trường hợp trên, duyệt đệ quy xuống nhánh con trái hoặc phải tùy theo trị của x, chú ý tăng mức level. Bài 227: (trang 64) NODEPTR isMember( NODEPTR t, int x ) { if ( t ) { if ( x == t->data ) return t; return ( x data ) ? isMember( t->left, x ) : isMember( t->right, x ); } return NULL; } /* x, y phải chắc chắn thuộc cây, x < y */ NODEPTR isParent( NODEPTR t, int x, int y ) { if ( x == t->data || y == t->data || x data && y > t->data ) return t; return ( x data ) ? isParent( t->left, x, y ) : isParent( t->right, x, y ); } /* dùng trong hàm main() */ do { printf( "Nhap a, b: " ); scanf( "%d%d", &a, &b ); if ( a != b && isMember( t, a ) && isMember( t, b ) ) break; printf( "Nhap khong hop le...\n" ); } while ( 1 ); if ( a > b ) { int temp = a; a = b; b = temp; } printf( "Node cha: %d\n", isParent( t, a, b )->data ); (c) Dương Thiên Tứ www.trainingwithexperts.com 334 Ta sắp xếp trước a < b: giúp dễ dàng xác định được vị trí node còn lại khi xác định được một node a hoặc b. - Nếu 2 node chứa a và b nằm trên 2 cây con trái phải của node gốc, node gốc là node cha của chúng (TH1). - Nếu 2 node chứa a và b cùng nằm trong cây con bên trái hoặc bên phải của node gốc thì có hai trường hợp con: + Node cha là một trong hai node chứa a hoặc b (TH2). + Hai node nằm trên 2 nhánh của một cây con thuộc nhánh trái hoặc phải của node gốc, node cha là node gốc của cây con này (quy về TH1). Từ node gốc p, ta đệ quy xuống hai nhánh trái và phải cho đến khi xảy ra một trong các điều kiện dừng đệ quy TH1 hoặc TH2. Khi đó, trị trả về của hàm isParent() chính là node cha cần tìm. Để bảo đảm hàm isParent() có điểm dừng, trị a và b phải có trong cây. Ta dùng hàm isMember() để xác định điều kiện này: duyệt cây theo NLR (tốt nhất cho tìm node). Nếu tìm được node, trả về node cần tìm; nếu không, trả về NULL. Bài 228: (trang 64) void PathLeft( NODEPTR t, int x ) { if ( t ) { if ( x == t->data ) return; ( x data ) ? PathLeft( t->left, x ) : PathLeft( t->right, x ); printf( "%d ", t->data ); } } void PathRight( NODEPTR t, int x ) { if ( t ) { if ( x == t->data ) return; printf( "%d ", t->data ); ( x data ) ? PathRight( t->left, x ) : PathRight( t->right, x ); } } /* dùng trong hàm main() */ int main() { NODEPTR t, p; int a, b; t = NULL; InTree( &t ); do { printf( "Nhap a, b: " ); scanf( "%d%d", &a, &b ); if ( a != b && isMember( t, a ) && isMember( t, b ) ) break; printf( "Nhap khong hop le...\n" ); } while ( 1 ); if ( a > b ) { int temp = a; a = b; b = temp; } (c) Dương Thiên Tứ www.trainingwithexperts.com 335 p = isParent( t, a, b ); if ( p->data == a ) { PathRight( p, b ); printf( "%d", b ); } else if ( p->data == b ) { printf( "%d ", a ); PathLeft( p , a ); } else { printf( "%d ", a ); PathLeft( p , a ); PathRight( p->right, b ); printf( "%d", b ); } putchar( '\n' ); return 0; } Hàm xuất đường đi từ node gốc đến một node trong cây thực chất là duyệt cây để tìm node, tốt nhất là duyệt theo NLR; nhưng thêm thao tác xuất từng node đã duyệt trên đường đi tới node cần tìm. - Hàm PathRight( t, x ): xuất đường đi từ node gốc p đến node chứa trị x có trong cây con bên phải, node gốc chứa trị nhỏ hơn x. - Hàm PathLeft( t, x ): xuất đường đi từ node chứa trị x có trong cây con bên trái đến node gốc p chứa trị lớn hơn x. Vì ta duyệt đệ quy từ node gốc xuống nên để xuất đường đi theo thứ tự ngược lại, ta dùng đệ quy đầu. Đường đi không bao gồm node chứa x. Khi xét hai node chứa a và b, a < b và phải có mặt trong cây BST, ta gặp các trường hợp sau đây: - Node gốc đang xét p là node chứa a: vì a < b node chứa b nằm trong cây con bên phải node gốc p; gọi hàm PathRight( p, b ) để xuất đường đi từ node gốc p chứa a đến node chứa b. - Node gốc đang xét p là node chứa b: vì a < b node chứa a nằm trong cây con bên trái node gốc p; gọi hàm PathLeft( p, a ) để xuất đường đi từ node chứa a đến node gốc p chứa b. - Node gốc p không chứa a hoặc b: vì a < b, node a nằm trong cây con bên trái và node b nằm trong cây con bên phải cây này; gọi hàm PathLeft( p, a ) để xuất đường đi từ node a đến node gốc p, gọi tiếp hàm PathRight( p->right, b ) để xuất tiếp đường đi từ node phải của node gốc p đến node b. Xuất đường đi từ node phải của p do node gốc p đã xuất khi gọi hàm PathLeft( p, a ). Khi giải quyết các trường hợp trên, ta cần xác định node cha gần nhất của hai node chứa trị a và b; ta dùng hàm isParent() cho yêu cầu này, xem bài 227 (trang 333). Bài 229: (trang 64) #include #include #include #define FALSE 0 #define TRUE 1 enum eBALANCE { LEFT, EVEN, RIGHT }; struct DATA { (c) Dương Thiên Tứ www.trainingwithexperts.com 336 int key; char value[10]; }; struct NODE { enum eBALANCE flag; struct DATA* pData; struct NODE *left, *right; }; typedef struct NODE* NODEPTR; void RotateRight( NODEPTR* t ) { NODEPTR temp = ( *t )->left; ( *t )->left = temp->right; temp->right = *t; *t = temp; } void RotateLeft( NODEPTR* t ) { NODEPTR temp = ( *t )->right; ( *t )->right = temp->left; temp->left = *t; *t = temp; } void Rebalance_RIGHT( NODEPTR* t ) { NODEPTR temp2, temp = ( *t )->right; switch ( temp->flag ) { case RIGHT: /* RR - cây con bên phải (R) có nhánh phải (R) dài */ ( *t )->flag = EVEN; temp->flag = EVEN; RotateLeft( t ); break; case LEFT: /* RL - cây con bên phải (R) có nhánh trái (L) dài */ temp2 = temp->left; switch ( temp2->flag ) { case EVEN: ( *t )->flag = EVEN; temp->flag = EVEN; break; case LEFT: ( *t )->flag = EVEN; temp->flag = RIGHT; break; case RIGHT: ( *t )->flag = LEFT; temp->flag = EVEN; } temp2->flag = EVEN; RotateRight( &temp ); ( *t )->right = temp; RotateLeft( t ); } } void Rebalance_LEFT( NODEPTR* t ) { (c) Dương Thiên Tứ www.trainingwithexperts.com 337 NODEPTR temp2, temp = ( *t )->left; switch ( temp->flag ) { /* LL - cây con bên trái (R) có nhánh trái (R) dài */ case LEFT: ( *t )->flag = EVEN; temp->flag = EVEN; RotateRight( t ); break; /* LR - cây con bên trái (R) có nhánh phải (R) dài */ case RIGHT: temp2 = temp->right; switch ( temp2->flag ) { case EVEN: ( *t )->flag = EVEN; temp->flag = EVEN; break; case RIGHT: ( *t )->flag = EVEN; temp->flag = LEFT; break; case LEFT: ( *t )->flag = RIGHT; temp->flag = EVEN; } temp2->flag = EVEN; RotateLeft( &temp ); ( *t )->left = temp; RotateRight( t ); } } void Insert( NODEPTR* t, struct DATA* d, int* taller ) { /* chèn một node mới */ if ( *t == NULL ) { *t = ( NODEPTR )malloc( sizeof( struct NODE ) ); ( *t )->pData = d; ( *t )->flag = EVEN; ( *t )->left = ( *t )->right = NULL; *taller = TRUE; return; } /* trùng khóa */ if ( d->key == ( *t )->pData->key ) { *taller = 0; return; } /* chèn trái */ if ( d->key pData->key ) { Insert( &( *t )->left, d, taller ); if ( taller ) /* có khả năng làm mất cân bằng */ switch ( ( *t )->flag ) { case LEFT: /* node gốc thành //, cần cân bằng trái */ Rebalance_LEFT( t ); *taller = FALSE; break; case EVEN: /* node gốc lệch trái nhưng chưa mất cân bằng */ // \ \ // \ / // \ - - - - - / - - - \ temp temp temp t t t (c) Dương Thiên Tứ www.trainingwithexperts.com 338 ( *t )->flag = LEFT; *taller = TRUE; break; case RIGHT: /* thêm node chỉ làm cây cân bằng */ ( *t )->flag = EVEN; *taller = FALSE; break; } return; } /* chèn phải */ Insert( &( *t )->right, d, taller ); if ( *taller ) { /* có khả năng làm mất cân bằng */ switch ( ( *t )->flag ) { case LEFT: /* thêm node chỉ làm cây cân bằng */ ( *t )->flag = EVEN; *taller = FALSE; break; case EVEN: /* node gốc lệch phài nhưng chưa mất cân bằng */ ( *t )->flag = RIGHT; *taller = TRUE; break; case RIGHT: /* node gốc thành \\, cần cân bằng phải */ Rebalance_RIGHT( t ); *taller = FALSE; break; } } return; } void OutTree( NODEPTR t, int depth ) { int i; char b[3] = { '\\', '|', '/' }; if ( t == NULL ) return; OutTree( t->right, depth + 1 ); for ( i = 0; i < depth; ++i ) printf( " " ); printf( "(%d,%c)\n", t->pData->key, b[t->flag] ); OutTree( t->left, depth + 1 ); } struct DATA* Find( NODEPTR t, int key ) { for( ; t; ) { if ( t->pData->key == key ) return t->pData; t = ( t->pData->key > key ) ? t->left : t->right; } return NULL; } int main() { int i, taller, x; struct DATA* d; NODEPTR t = NULL; struct DATA data[6] = { { 11, "Cho" }, { 4, "Meo" }, { 12, "Heo" }, { 3, "Cop" }, { 9, "De" }, { 6, "Ran" } }; (c) Dương Thiên Tứ www.trainingwithexperts.com 339 for ( i = 0; i < 6; ++i ) { d = ( struct DATA* )malloc( sizeof( struct DATA ) ); d->key = data[i].key; strcpy( d->value, data[i].value ); Insert( &t, d, &taller ); OutTree( t, 0 ); printf( "----------\n" ); } printf( "Nhap khoa can tim: " ); scanf( "%d", &x ); if ( ( d = Find( t, x ) ) != NULL ) printf( "[%d,%s]\n", d->key, d->value ); else printf( "Khong tim thay khoa\n" ); return 0; } Cây AVL được Adelson - Velskii và Landis giới thiệu năm 1962. Cây AVL là một cây BST cân bằng 1 cấp (1-balanced BST), điều kiện cân bằng của cây là: x,1|))x(T(height))x(T(height| RL  Trong đó: height(): trả về chiều cao của cây, TL(x): cây con bên trái node x, TR(x): cây con bên phải node x Nghĩa là chênh lệch chiều cao giữa cây con bên trái và cây con bên phải của một node x bất kỳ trong cây không được vượt quá 1. Cấu trúc dữ liệu của một node trong cây AVL cần thêm một “tác nhân cân bằng” (balancing factor) flag để kiểm tra cân bằng của cây: - ))x(T(height))x(T(height RL  : flag = LEFT (minh họa bằng dấu /) - ))x(T(height))x(T(height RL  : flag = EVEN (minh họa bằng dấu -) - ))x(T(height))x(T(height RL  : flag = RIGHT (minh họa bằng dấu \) Khi chèn một node mới vào cây AVL, ta có thể làm cây AVL mất cân bằng. Có 4 trường hợp làm cây AVL mất cân bằng: - RR - cây con bên phải (R) có nhánh phải (R) dài - RL - cây con bên phải (R) có nhánh trái (L) dài - LL - cây con bên trái (L) có nhánh trái (L) dài - LR - cây con bên trái (L) có nhánh phải (R) dài Biến taller dùng trong hàm Insert() báo rằng cây đang “lệch” sau mỗi lần chèn node. Trong lần chèn node kế tiếp, khi nhận thấy lần chèn node trước đã làm cây “lệch”, cần xác định là lần chèn node hiện tại có làm cây mất cân bằng không (hoặc làm cây cân bằng trở lại). Sự mất cân bằng này liên quan đến 3 node: kể từ node gốc đi theo “hướng” nhánh gây mất cân bằng. Để cân bằng lại cây, người ta tái bố trí lại (trinode restructuring) sao cho 3 node này và các cây con liên quan vẫn giữ đúng thứ tự duyệt LNR (inoder, xem bài 222, trang 325). Các hình dưới trình bày cách tái bố trí lại cây, các con trỏ trong hình giúp dễ theo dõi các hàm quay cây: - Trường hợp RR: Thao tác tái bố trí gọi là “quay trái cây” (xem hàm RotateLeft()) có gốc là a. Chú ý thứ tự duyệt LNR (T1) a (T2) b (T3) c (T4) không thay đổi sau khi cân bằng cây. (c) Dương Thiên Tứ www.trainingwithexperts.com 340 - Trường hợp RL: Thao tác tái bố trí thực hiện quay kép: “quay phải cây” con gốc b để đưa về trường hợp RR, sau đó “quay trái cây” gốc a để giải quyết trường hợp RR. Chú ý thứ tự duyệt LNR (T1) a (T2) c (T3) b (T4) không thay đổi sau khi cân bằng cây. - Trường hợp LL: đối xứng với trường hợp RR, tái bố trí bằng cách tương tự, gọi là “quay phải cây” (xem hàm RotateRight()). - Trường hợp LR: đối xứng với trường hợp RL, tái bố trí bằng cách tương tự, “quay trái cây” chuyển về LL, sau đó “quay phải cây” để cân bằng. Các trường hợp RR và RL giải quyết bằng hàm Rebalance_RIGHT(). Các trường hợp LL và LR giải quyết bằng hàm Rebalance_LEFT(). Bài 230: (trang 65) void Delete( NODEPTR* t, int d, int* shorter ) { if ( d == ( *t )->pData->key ) { if ( ( *t )->left && ( *t )->right ) { /* có hai cây con */ NODEPTR p; void* temp; /* tìm node thay thế trước */ for ( p = ( *t )->left; p->right; p = p->right ) { } /* hoán chuyển dữ liệu node cần xóa với node thay thế trước */ temp = ( *t )->pData; ( *t )->pData = p->pData; p->pData = temp; /* gọi đệ quy xóa node thay thế (hiện mang khóa d) trong cây trái */ Delete( &( *t )->left, d, shorter ); /* cân bằng cây sau khi xóa node */ if ( shorter ) switch ( ( *t )->flag ) { case RIGHT: /* xóa node nhánh phải: cây mất cân bằng */ Rebalance_RIGHT( t ); break; case EVEN: /* xóa node nhánh phải: node gốc lệch phải */ t T2 \ T3 T1 T4 - T3 T4 - T1 T2 \ \ temp a c b b a c T4 / T2 T1 T3 - T3 T4 T1 T2 \ \ a c b c a b T2 \ T3 T1 T4 \ \ a b c t temp temp2 (c) Dương Thiên Tứ www.trainingwithexperts.com 341 ( *t )->flag = RIGHT; *shorter = FALSE; break; case LEFT: /* xóa node nhánh phải: cây cân bằng trở lại */ ( *t )->flag = EVEN; *shorter = TRUE; } } else { /* trường hợp chỉ có một cây con, xóa node tại đây */ NODEPTR dnode = *t; *t = ( !( *t )->left ) ? ( *t )->right : ( *t )->left; free( dnode->pData ); free( dnode ); *shorter = TRUE; } } else if ( d pData->key ) { /* xóa bên nhánh trái */ Delete( &( *t )->left, d, shorter ); /* cân bằng cây sau khi xóa node */ if ( shorter ) switch ( ( *t )->flag ) { case RIGHT: Rebalance_RIGHT( t ); break; case EVEN: ( *t )->flag = RIGHT; *shorter = FALSE; break; case LEFT: ( *t )->flag = EVEN; *shorter = TRUE; } } else { /* xóa bên nhánh phải */ Delete( &( *t )->right, d, shorter ); /* cân bằng cây sau khi xóa node */ if ( shorter ) switch ( ( *t )->flag ) { case LEFT: Rebalance_LEFT( t ); break; case EVEN: ( *t )->flag = LEFT; *shorter = FALSE; break; case RIGHT: ( *t )->flag = EVEN; *shorter = TRUE; } } } Áp dụng cách xóa node trong cây BST trình bày cuối bài tập 221 (trang 320). Kết quả node cần xóa được chuyển thành node thay thế chỉ có một cây con. Giống như biến taller trong hàm Insert(), biến shorter truyền trong hàm Delete() báo rằng cây đang “lệch” sau mỗi lần xóa node, dùng xác định việc xóa node có làm cây mất cân bằng không để tiến hành cân bằng lại cây sau khi xóa node. (c) Dương Thiên Tứ www.trainingwithexperts.com 342 TÀI LIỆU THAM KHẢO (Sắp xếp theo năm xuất bản) [1] Programming Language - C. ANSI X3.159-1989 aka ISO 9899-1990 - American National Standard for Information Systems. [2] Kernighan, Brian W. & Ritchie, Dennis M. - The C Programming Language - Second Edition - Prentice Hall, 1988. ISBN 0-131-10370-9 [3] Kochan, Stephen G. & Wood, Patrick H. - Topics in C Programming - Third Edition - John Wiley & Sons, 1991. ISBN 0-471-53404-8 [4] Schildt, Herbert - A Book on C: Programming in C - Fourth Edition - McGraw Hill/Osborne Media, 1995. ISBN 0-078-82101-0 (tiếng Anh). [5] Johnsonbaugh, R. & Kalin M. - Applications Programming in ANSI C - Third Edition - MacMillan, 1996. ISBN 0-023-61141-3 [6] Summit, Steve & Lafferty, Deborah - C Programming FAQs: Frequently Asked Questions - Addison Wesley, 1996. ISBN 0-201-84519-9 [7] Kelley, Al & Pohl, Ira - C: The Complete Reference - Third Edition - Addison Wesley, 1997. ISBN 0-078-82101-0 [8] Cassgne, Bernage - Introduction au Language C (norme ISO/ANSI) - Université Joseph Fourier & CNRS, 1998 (tiếng Pháp). [9] P.S. Deshpande & O.G. Kakde - C & Data Structures - Charles River Media, 2004. ISBN 1-584-50338-6 [10] Ivor Horton - Beginning C - Fifth Edition - Apress, 2013. ISBN 978-1-4302- 4881-1 [11] Jeri R. Hanly, Elliot B. Koffman - Problem Solving and Program Design in C - Seventh Edition - Pearson Education, Inc., 2013. ISBN 0-13-293649-6 [12] Deitel, H.M. & Deitel, P.J. - C How to Program - Seventh Edition - Prentice Hall, 2013. ISBN 0-13-299044-X [13] Stephen Prata - C Primer Plus - Sixth Edition – Pearson Education, Inc., 2014. ISBN 0-321-92842-3 [14] Tony Crawford, Peter Prinz - C: In a Nutshell - Second Edition - O'Reilly, 2016. ISBN 978-1-491-90475-6 (c) Dương Thiên Tứ www.trainingwithexperts.com 343 Mục lục Lời nói đầu ................................................................................................................ 1 Hướng dẫn sử dụng sách .......................................................................................... 2 Phần bài tập Khái niệm cơ bản - Toán tử - Cấu trúc lựa chọn - Cấu trúc lặp ............................ 3 Mảng .................................................................................................................... 17 Mảng nhiều chiều ................................................................................................ 25 Chuỗi ................................................................................................................... 35 Đệ quy ................................................................................................................. 41 Structure - Union - Bit Field ................................................................................ 46 Tập tin .................................................................................................................. 49 Các vấn đề khác ................................................................................................... 56 Cấu trúc dữ liệu ................................................................................................... 58 Phần bài giải Khái niệm cơ bản - Toán tử - Cấu trúc lựa chọn - Cấu trúc lặp .......................... 66 Mảng .................................................................................................................. 113 Mảng nhiều chiều .............................................................................................. 146 Chuỗi ................................................................................................................. 181 Đệ quy ............................................................................................................... 217 Structure - Union - Bit Field .............................................................................. 240 Tập tin ................................................................................................................ 252 Các vấn đề khác ................................................................................................. 279 Cấu trúc dữ liệu ................................................................................................. 296 Tài liệu tham khảo ................................................................................................ 342

Các file đính kèm theo tài liệu này:

  • pdfbai_tap_ky_thuat_lap_trinh.pdf