Ch−ơng 4 B cõy và bộ nhớ ngoà
4.2. B-cây
4.2.3. Tỡm kiếm trờn B cõ
Giả sử chỳng ta cần tỡm bản ghi h cú khúa x. Chúng ta xt phát từ gốc của B - cõy, và đi theo nhỏnh cõy thớch hợp. Giả sử ta đang ở đỉnh B. Nếu đỉnh B là lỏ thỡ ta phải tỡm trong khối B xem có chứa bản ghi h hay khụng. Vỡ cỏc khúa trong B đ−ợc sắp theo thứ tự tăng dần nờn ta có thĨ áp dơng kỹ thuật tỡm kiếm nhị phõn hoặc tuần tự.
Nếu B là đỉnh trong chứa các khóa k0, k1,..., kr-2 thỡ ta cần xỏc định vị trớ của x trong dãy khóa nàỵ Thí dơ nếu ki-1< x < ki thỡ ta tỡm x trờn cõy con nằm giữa ki-1 và ki.
4.2.4. Thờm khúa vào B - cõy
Khi thờm một khúa x vào B- cõy thỡ chỳng ta phải ỏp dụng thủ tục tỡm kiếm để tỡm ra khối B cần phải xen và Nếu khúa x đà tồn tại trờn cõy thỡ ta khụng thờm nữa, vỡ cỏc khúa trờn cõy phải duy nhất, khụng đ−ợc lặp lạ Ta cú thể thấy rằng thủ tục thờm nỳt luụn luụn cú thể đ−ỵc thực hiện ở nỳt lỏ. Thật vậy, vỡ nút x ch−a cú trờn cõy nờn xuất phỏt từ nút gốc ta sẽ đi theo con đ−ờng đi qua cỏc nỳt trong để đến một nỳt lỏ nào đú. Tại một nỳt trung gian ta luụn cú hoặc là x
50
20 40 70 80
30 31 32 35
Cấu trúc dữ liƯu2 – Ch−ơng 4. B - cõy và bộ nhớ ngoài
< k0 trong đó k0 là khúa nhỏ nhất trong đỉnh đú, khi này ta đi theo nhỏnh cõy con nằm bờn trỏi khóa k0 . Nếu x > kr-2 trong đó kr-2 là khóa lớn nhất trong đỉnh đú, khi này ta đi theo nhỏnh cõy con nằm bờn phải khóa kr-2. Nếu ki-1< x < ki thỡ ta đi theo cõy con nằm giữa ki-1 và ki. Vậy thao tỏc thờm nỳt luụn đ−ỵc thực hiện ở một nỳt lỏ nào đú đợc xỏc định bởi x. Giả sử ta đến nỳt lỏ B. Ta đặt n= 2*m là số khúa tối đa và n1=n+1 là số con tối đa trong một nỳt. Ta thực hiƯn viƯc chèn nút x nh− sau:
• Nếu nút B ch−a đầy thỡ chỳng ta chốn khúa x vào nỳt này theo thao tỏc chốn nỳt vào danh sỏch sắp thứ tự.
• Nếu nút B đà đầy, tức là cú n khúa, nếu ta thờm khúa x thỡ nỳt B bị tràn. Ta gọi midkey là khúa ở giữ Ta tỏch nỳt B thành 2 nỳt, mỗi nỳt gồm m nỳt. Nỳt nửa trỏi gồm các khóa từ 0 đến midkey-1, ta gọi là nỳt nd. Nỳt nửa phải (gọi là nỳt nd2) gồm các nút có khóa từ midkey+1 đến n1. Khúa midkey và nỳt con nd2 đ−ợc chuyển lờn chốn vào nỳt ch Nếu nỳt cha bị đầy ta lại thực hiện t−ơng tự nh− với nút B. Nh− vậy viƯc tách nút có thĨ lan truyền tới gốc và nỳt gốc cú thể bị tỏch làm 2. Đõy chớnh là cỏch duy nhất để B-cõy cú thể tăng chiều cao: nú lớn lờn từ lỏ đến gốc.
Vớ dụ: Xột B-cõy sau đõy:
Hình 4.3 Thêm khóa 22 vào
B-cây
1. Giả sử ta phải thờm khúa 22 vào cõ Nỳt A khụng cú khúa 22 và ta tiếp tục tỡm kiếm trên nút C. Nút C là nỳt lỏ cuối cựng trờn đ−ờng tìm kiếm khóa 22, do vậy ta sẽ thực hiƯn viƯc chèn khúa 22 ở đõ
2. Tuy nhiờn nỳt C đà đầy, do đú ta tỏch nỳt C thành 2 nỳt và ta cú thờm nỳt mới là D.
3. 2*m+1 khoá đ−ợc phõn phối đều vào 2 nỳt C và D, khúa ở giữa là khúa 30 đ−ợc chuyển lờn nỳt ch Đồng thời ta đặt mối liờn kết từ nút cha xuống nút D.
4.2.5. Loại bỏ khúa trờn B - cõy
Về nguyờn tắc, việc loại bỏ nỳt trờn B-cõy hoàn toàn đơn giản nh−ng phức tạp trong chi tiết thực hiƯn. ĐĨ loại bỏ một khóa x, tr−ớc hết ta tìm kiếm trờn cõy để xỏc định vị trớ của khúa x. Việc tiến hành loại bỏ dĩ nhiờn chỉ đợc thực hiện khi thao tỏc tỡm kiếm cú kết quả. Cú 2 tỡnh huống sau:
(1) Phần tử cần loại bỏ ở nỳt lỏ: việc loại bỏ phần tử này đ−ỵc thực hiƯn khỏ dễ dàng nh loại bỏ phần tử trờn danh sỏch.
(2) Phần tử cần loại bỏ nằm ở nỳt trung gian. Lỳc này ta khụng thể loại bỏ trực tiếp phần tử trờn nỳt này, vỡ phần tử này cũn cú cỏc cõy con liờn quan. Ta phải tỡm một phần tử khỏc nằm ở nỳt lỏ làm phần tử thay thế, nghĩa là giỏ trị khúa của phần tử thay thế sẽ đợc gỏn cho giỏ trị khóa
20 7 10 15 18 26 30 35 40 20 30 7 10 15 18 22 26 35 40 A B C A B C D
http://www.ebook.edụvn
Cấu trúc dữ liƯu2 – Chơng 4. B - cõy và bộ nhớ ngoài
77 cđa phần tư cần xóa, sau đú phần tử thay thế đợc loại bỏ khỏi nút lá chứa nó. Cịng nh− trong tr−ờng hợp cõy cõn bằng, ta thấy phần tử thay thế phải là phần tử cực phải của nhỏnh cõy con trỏi hoặc nỳt cực trỏi của nhỏnh cõy con phải của phần tư cần xóạ Chúng ta sẽ quy −ớc chọn phần tử cần thay thế là phần tử cực phải trờn nhỏnh cõy con trỏ
Gọi nút chứa khúa cần xúa là Z. Ta sẽ gặp tỡnh huống ng−ỵc với tr−ờng hợp thờm khúa: khi xóa khóa ta có thĨ làm cho nút lỏ bị cạn kiệt (underflow), nghĩa là cú số khúa ít hơn mức tối thiĨụ Lỳc này ta chỉ cú cỏch giải quyết là m−ỵn một khóa từ nút lân cận. Nếu nút lân cận cũng đang ở trạng thỏi sắp cạn kiệt, nghĩa là cú đỳng m khúa thỡ ta phải đa cỏc khúa và cây con cđa nút Z vào nỳt lõn cận, sau đú xúa nỳt Z. Dĩ nhiờn để xúa nỳt Z, ta phải đ−a cả khóa t−ơng ứng cđa nó trong nỳt cha vào nỳt lõn cận. Trong trờng hợp này nếu nỳt cha sắp cạn kiệt thỡ sẽ trở nờn cạn kiệt và cú thể ta phải cõn bằng lại nỳt cha bằng cỏch vay khóa cđa nút lân cận nỳt cha hoặc lại tiến hành ghộp nố Quỏ trỡnh này có thĨ lan trun tới gốc và có thĨ nỳt gốc cũ bị loại bỏ: cõy đà thấp xuống. Đõy chớnh là cỏch duy nhất để B-cõy giảm chiều ca Ví dơ: Ta xét B cây sau đõy:
Hình 4.4. Xúa khúa 32 trờn B-cõy
Giả sử ta cần xúa khúa 32. Nỳt chứa khúa 32 khụng phải nỳt lỏ, vỡ vậy ta phải tỡm một khúa thay thế. Theo quy −ớc, nút cực phải trờn cõy con trỏi của khúa 32 là khúa 30. Ta đa giỏ trị khóa 30 vỊ vị trí cđa khóa 32 rồi xóa khóa 30 trên nút lỏ chứa nú và nhận đợc cõy mới nh− sau:
Hỡnh 4.5. B - cõy sau khi xúa khóa 32 Ta nhận thấy nút F ở trong tình trạng cạn kiệt. Ta sẽ nối nỳt F vào nỳt E, đồng thời chun khóa 26 xuống nút E rồi xóa nút F. Ta đ−ợc kết quả sau
30 20 26 38 44 B 7 10 15 18 22 24 28 34 36 40 42 46 48 A C F D E I J K 32 20 26 38 44 B 7 10 15 18 22 24 28 30 34 36 40 42 46 48 A C F D E I J K 30 20 38 44 B 7 10 15 18 34 36 40 42 46 48 A C F D E I J K 22 24 26 28
Cấu trúc dữ liƯu2 – Ch−ơng 4. B - cõy và bộ nhớ ngoài
Hình 4.6. Cân bằng lại nút
F
Bây giờ nỳt B lại trở thành cạn kiệt và ta phải cõn bằng lại bằng cỏch ghộp với nỳt C , đồng thời lấy khóa cđa nút gốc xng. Nút gốc vốn chỉ cú một khúa, sau khi lấy khúa này đi thỡ thành rỗng. ta xúa nỳt gốc này và lấy nút B làm gốc mớị
Hình 4.7. Cân bằng lại nút
B
Nh− vậy việc xúa nỳt 32 đà tạo ra một "phản ứng dõy chuyền". Ta phải cõn bằng lại tất cả cỏc nỳt từ nút có khóa thay thế cho đến tận nút gốc và kết quả là cõy đà bị thấp xuống. Tuy nhiên trong thực tế khụng phải lỳc nào ta cũng phải cõn bằng lại tất cả cỏc nỳt từ nỳt lỏ (chứa khúa cần xoỏ hoặc khúa thay thế) cho đến nỳt gốc. Quỏ trỡnh cõn bằng đ−ỵc thực hiƯn từ nút lá trở vỊ gốc và dừng lại khi gặp một nỳt cân bằng. Tr−ờng hợp đơn giản nhất là nỳt lỏ có nhiỊu hơn M khóạ Khi đú sau khi xúa khúa trờn nỳt này thỡ nỳt vẫn cõn bằng và ta khụng phải cõn bằng lạ
4.2.6. Phân tích cỏc thuật toỏn trờn B - cõy
Các thao tác tỡm kiếm hay chốn trờn B-cõy cấp M, tức là số khúa trờn một nỳt trong khoảng từ M đến 2*M cú độ phức tạp tớnh toỏn khoảng O(logM n) trong đú n là số nỳt của câỵ
4.3. Cài đặt B - cõy
Chỳng ta sẽ cài đặt cõy B - cõy theo kiểu liờn kết dựng biến động. Sau đõy chỳng tụi giải thớch một số tác vơ chính cđa chơng trỡnh. Cỏc tỏc vụ này khụng đợc liệt kờ lại trong chơng trỡnh mà chỉ cú dũng chỳ thích. Khi gõ ch−ơng trỡnh để chạy thử bạn đọc cần gừ lại chi tiết.
• Khai báo B - cây
Sau cỏc lệnh #include để đa vào cỏc tệp header, các lƯnh #define M 2
#define N 2*M #define N1 2*M+1
sẽ định nghĩa cấp và bậc của cõ M là cấp của cõy và là số khúa tối thiểu mà một nỳt trờn cõy cần cú (trừ nỳt gốc). M phải là hằng số vỡ ta sẽ dựng mảng tĩnh để khai bỏo cỏc khúa và cỏc nỳt trờn cõ Trong ch−ơng trỡnh ta đặt M = 2, tức là cây cấp 2 hay cây bậc 5, số khóa tối đa có thĨ có trờn cõy là 4. Bạn đọc cú thể đặt lại giỏ trị M nếu muốn định nghĩa cõy cấp cao hơn.
Tiếp theo là khai bỏo cấu trỳc của một nỳt thụng tin trờn cõ Mỗi nỳt trờn cõy là một cấu trúc chứa các tr−ờng sau:
- Tr−ờng keynum : là số khóa hiƯn có trên nút
- Tr−ờng key: là mảng chứa cỏc khúa của nỳt. Thực ra số khúa tối đa là N = 2*M, nh− vậy chỉ cần một mảng cú N phần tử nguyờn là cú thể chứa đợc số khúa cần thiết. Tuy nhiên, nh− ta sẽ
B 7 10 15 18 34 36 40 42 46 48 F D E I J K 22 24 26 28 20 30 38 44
http://www.ebook.edụvn
Cấu trúc dữ liƯu2 – Ch−ơng 4. B - cõy và bộ nhớ ngoài
79 thấy trong phần sau cđa chơng trỡnh, việc thờm một vị trớ cho mảng để chứa phần tử thờm vào trong tr−ờng hợp nỳt bị đầy sẽ làm cho việc lập trỡnh đơn giản hơn. Trong thực tế ứng dụng bạn đọc cần cõn nhắc xem việc thờm một vị trớ cho mỗi nỳt cú ảnh h−ởng đến việc khai thỏc tài nguyờn bộ nhớ khụng, và sẽ quyết định chấp nhận thờm một nỳt để việc lập trỡnh đơn giản hơn hay triƯt đĨ tiết kiƯm nguồn tài nguyờn và lập trỡnh cú phức tạp hơn chút ít.
- Tr−ờng son: là mảng cỏc con trỏ trỏ tới địa chỉ cỏc cõy con của nỳt. Mảng này cũng đợc thờm một vị trớ để việc lập trỡnh đơn giản hơn.
Nh− chỳng tụi đà mụ tả trong phần tr−ớc, viƯc cân bằng lại cõy cú thể phải thực hiện từ vị trớ thờm hoặc xóa nút cho đến gốc. Nh− vậy ta cần một Stack đĨ l−u lại địa chỉ cỏc nỳt từ gốc cho đến nỳt cú phần tư đ−ợc thờm hoặc loại bỏ. Cỏc phần tử của Stack sẽ là một cấu trúc Snode gồm 2 thành phần: con trỏ chỉ đến cỏc nỳt trờn cõy và biến nguyờn k chỉ nhỏnh cõy con cần đi tiếp trờn đ−ờng tới nỳt cú khúa cần thờm hoặc loại bỏ.
#define M 2 // cap cua cay, tuc la so khoa it nhat cua mot nut #define N 2*M // So khoa toi da trong mot nut
#define N1 2*M+1 // So con toi da trong mot nut, tuc la bac cua cay // Khai bao cau truc cua mot nut
struct Node
{int keynum; // so khoa cua mot nut
int key[N+1]; //moi nut co nhieu nhat N khoa
Node *son[N1+1]; //cac con tro chi cac nut con cua mot nut };
//Them 1 vi tri vao key[] va son[] de tien thao tac
struct Snode {Node *pnode;int k;};//k la di tiep tren son[k]; #include "stack_h.cpp"
class Btree {public:
Node* proot; //A pointer to the root of the treẹ long count; BTree(); ~BTree(); void Initialize(); int Empty(); void Clear(Node*&); Node* NewNode(); void Traverse(Node*); int NodeSearch(Node*&,int); Node* Search(Node*,int &,int); void Split(Node*&,Node*&,int); void Insert(int);
void Merge(Node*&,Node*&,int); void Remove(int);
Cấu trúc dữ liƯu2 – Ch−ơng 4. B - cõy và bộ nhớ ngồi
• Tỏc vụ tỏch nỳt khi nỳt bị tràn
Tỏc vụ này đ−ỵc gọi bởi tác vơ insert. Khi một nút p bị tràn, tức là cú 2*M+1 khúa thỡ nó đ−ợc tỏch làm 2 nỳt. Cỏc khúa và cõy con kốm theo từ vị trớ 0 đến vị trớ M-1 đợc giữ lại trong nút p. Các nỳt và cõy con kốm theo từ vị trớ M+1 đến 2*M đợc chuyển vào nỳt mới p2. Sau đó nút mới p2 cùng với khóa ở vị trí M đ−ợc đa lờn nỳt cha ở vị trớ thớch hợp. Vị trớ này chớnh là vị trớ k nhận đ−ỵc từ Stack.
void BTree::Split(Node* &p,Node* &fp, int k) {if(p->keynum<=N) return;
int i;
Node *p2=NewNode(); /*Tach nut p lam 2 phan:
Tu vi tri 0->M-1 giu lai trong p, tu M+1 -> N chuyen sang p2 khoa p->key[M] chuyen len vi tri fp->key[k]; nut p2 duoc gan vao fp->son[k]
Dich chuyen cac nhanh cay con va cac khoa ben phai vi tri k sang phai mot vi tri trong nut fp de lay vi tri fp->key[k] va fp->son[k] chen khoa va con moi*/
for(i=fp->keynum;i>k;i--) {fp->son[i+1]=fp->son[i]; fp->key[i] = fp->key[i-1]; }
fp->keynum++; //Bat dau tach nut p; int newkey=p->key[M];
//copy tu vi tri khoa M+1 sang p2 for(i=0;i<M;i++)
{p2->key[i]=p->key[M+i+1]; p2->son[i]=p->son[M+i+1]; }
p2->son[M]=p->son[N1];//con cuoi cung p->keynum=p2->keynum=M;
fp->key[k]=newkey; fp->son[k]=p; fp->son[k+1]=p2; }
• Tỏc vụ tỡm khúa x trờn nỳt p và trả về vị trớ của khúa x. Nếu khụng tỡm thấy thỡ trả về giỏ trị k, và ta cú thể tỡm khúa x trờn nhỏnh cây p->son[k]
/* Tac vu Nodesearch: tim trong nut vi tri cua khoa bat dau >= x. Truong hop x lon hon tat ca cac khoa trong nut thi tra ve vi tri p->keynum, tuc la vi tri sau khoa cuoi cung
Input: Khoa x va con tro toi nut p
http://www.ebook.edụvn
Cấu trúc dữ liƯu2 – Ch−ơng 4. B - cõy và bộ nhớ ngoài
81 int BTree::NodeSearch(Node* &p, int x)
{int i;
for(i=0;i<p->keynum && p->key[i]<x; i++); return(i);
//Neu co khoa key[i] thi ta co key[i-1] < x <= key[i] }
• Tỏc vụ thờm khúa x vào cõy
Tỏc vụ này thờm một nỳt cú khúa x vào B - cõy và thực hiện cỏc thao tỏc cõn bằng lại cõy sao cõy vẫn là B - cõ
Input: B - cõy và khúa cần chốn vào câỵ
Output: B - cõy trong đú đà có nút x.
Quỏ trỡnh thờm nỳt vào cõy đợc tiến hành qua cỏc b−ớc sau: B−ớc 1: Kiểm tra nếu cõy rỗng thỡ tạo nỳt gốc cú khúa x và kết thúc. B−ớc 2: Tạo một Stack để l−u lại cỏc nỳt đi qu
Đặt fp = NULL, p = proot.
B−ớc 3: Dùng tỏc vụ NodeSearch để xỏc định xem khúa x cú trờn nỳt p khụng, nếu cú thỡ thụng bỏo là nỳt đà cú, khụng thờm nữa và kết thỳc. Căn cứ vào giỏ trị k trả về của hàm NodeSearch ta đặt fp = p ,p = p->son[k]. L−u fp và k vào Stack rồi chun sang b−ớc 4.
B−ớc 4: Nếu p khác NULL ta trở lại b−ớc 3.
Nếu nút p = NULL, ta lấy phần tử từ đỉnh Stack r Thành phần con trỏ cđa phần tư này chớnh là nỳt fp cuối cựng và giỏ trị k là vị trớ ta cú thể chốn khúa x vàọ Sau khi