Giáo trình Cấu trúc dữ liệu và giải thuật - Chương 10: Cây nhiều nhánh
Trước khi mở rộng về các loại cây, chúng ta xét đến các định nghĩa. Trong
toán học, khái niệm cây có một ý nghĩa rộng: đó là một tập bất kỳ các điểm (gọi
là đỉnh), và tập bất kỳ các cặp nối hai đỉnh khác nhau (gọi là cạnh hoặc nhánh)
sao cho luôn có một dãy liên tục các cạnh (đường đi) từ một đỉnh bất kỳ đến một
đỉnh bất kỳ khác, và không có chu trình, nghĩa là không có đường đi nào bắt đầu
từ một đỉnh nào đó lại quay về chính nó.
Đối với các ứng dụng trong máy tính, chúng ta thường không cần nghiên cứu
cây một cách tổng quát như vậy, và khi cần làm việc với những cây này, để nhấn
mạnh, chúng ta thường gọi chúng là các cây tự do (free tree). Các cây của chúng
ta phần lớn luôn có một đỉnh đặc biệt, gọi là gốc của cây, và các cây dạng này
chúng ta sẽ gọi là các cây có gốc (rooted tree).
Một cây có gốc có thể được vẽ theo cách thông thường của chúng ta là gốc nằm
trên, các nút và nhánh khác quay xuống dưới, với các nút lá nằm dưới cùng. Mặc
dù vậy, các cây có gốc vẫn chưa phải là tất cả các dạng cây mà chúng ta thường
dùng. Trong một cây có gốc, thường không phân biệt trái hoặc phải, hoặc khi một
nút có nhiều nút con, không thể nói rằng nút nào là nút con thứ nhất, thứ hai,
v.v.Nếu không vì một lý do nào khác, sự thi hành tuần tự các lệnh thường buộc
chặt một thứ tự lên các nút con của một nút.
ngược lại chỉ là đối xứng. Chúng ta cần phân biệt hai trường hợp tương ứng với màu của nút con còn lại của nút ông, nghĩa là nút chú của nút đỏ mới xuất hiện. Trước tiên, giả sử nút chú có màu đen. Trường hợp này có gộp cả trường hợp nút chú rỗng, do quy ước cây con rỗng có màu đen. Các đặc tính của cây đỏ đen sẽ được khôi phục nhờ một phép quay đơn hay một phép quay kép qua phải, như hai phần đầu của hình 10.17. Trong cả hai sơ đồ này, phép quay, kéo theo sự thay đổi các màu nút tương ứng, sẽ loại được sự vi phạm điều kiện đỏ, đồng thời bảo toàn điều kiện đen do không làm thay đổi số nút đen trong bất kỳ đường đi nào từ gốc đến các nút lá. Giờ chúng ta giả sử nút chú có màu đỏ, như hai phần bên dưới của hình 10.17. Việc biến đổi rất đơn giản: không có phép quay nào xảy ra, chỉ có sự thay đổi các màu. Nút cha và nút chú trở thành màu đen, nút ông trở thành màu đỏ. Điều kiện đỏ vẫn bảo đảm khi xét mối quan hệ giữa nút ông và nút cha, nút chú; giữa nút Chương 10 – Cây nhiều nhánh Giáo trình Cấu trúc dữ liệu và Giải thuật 279 cha và nút con màu đỏ mới xuất hiện bên dưới. Điều kiện đen không bị vi phạm do số nút đen trong các đường đi dọc theo cây không hề thay đổi. Tuy nhiên, khi nút ông vừa được biến đổi thành màu đỏ, điều kiện đỏ có thể bị vi phạm khi xét mối quan hệ với nút cha của nút này. Quá trình xử lý chưa thể chấm dứt. Mặc dù vậy, chúng ta có thể thấy rằng nút ông vừa được đổi sang màu đỏ hoàn toàn giống với trường hợp một nút đỏ mới xuất hiện lúc trước. Vậy chúng ta chỉ cần để lại cho các lần gọi đệ quy bên trên xử lý tại các nút bên trên nữa trong cây, bằng cách lại cho chỉ số trạng thái về lại trường hợp có một nút đỏ mới xuất hiện. Như vậy quá trình xử lý khi điều kiện đỏ bị vi phạm được lan truyền dọc lên phía trên của cây. Quá trình này có thể kết thúc tại một nút nào đó, hoặc có thể lan truyền lên tận gốc. Và khi nút gốc cần được đổi sang màu đỏ, thì chương trình ngoài cùng chỉ cần đổi nút này thành màu đen cho đúng quy ước. Điều này cũng không vi phạm điều kiện đen, do nó làm cho số nút đen trong tất cả các đường đi dọc theo cây tăng thêm một đơn vị. Và đây cũng chính là trường hợp duy nhất làm cho số nút đen trong các đường đi này tăng lên. 10.4.5. Phương thức thêm vào. Hiện thực Chúng ta sẽ chuyển giải thuật trên thành chương trình C++. Cũng như mọi khi, phần lớn công việc đều được thực hiện bởi hàm đệ quy, phương thức insert chỉ cần đổi nút gốc thành màu đen khi cần và kiểm tra lỗi. Phần quan trọng nhất của giải thuật thêm phần tử vào cây đỏ đen là nắm giữ được chỉ số trạng thái mỗi khi có một lần đệ quy kết thúc. Chúng ta cần một kiểu liệt kê mới để phân biệt các trạng thái: enum RB_code {okay, red_node, left_red, right_red, duplicate}; /* Các giá trị trạng thái mà một lần đệ quy cần chuẩn bị trước khi kết thúc để trả về cho lần đệ quy bên trên như sau (giả sử gọi nút đang được xử lý trong lần đệ quy hiện tại là *current): okay: Màu của *current không có sự thay đổi nào. red_node: Màu của *current vừa chuyển từ đen sang đỏ. Lần đệ quy bên trên khi nhận được thông tin này cần xem xét để xử lý. right_red: Màu của *current và nút con phải của nó đều là đỏ, có sự vi phạm điều kiện đỏ. Lần đệ quy bên trên khi nhận được thông tin này cần thực hiện phép quay hoặc đổi màu cần thiết. left_red: Màu của *current và nút con trái của nó đều là đỏ, có sự vi phạm điều kiện đỏ. Lần đệ quy bên trên khi nhận được thông tin này cần thực hiện phép quay hoặc đổi màu cần thiết. duplicate: Phần tử cần thêm vào cây đã có trong cây. */ template Error_code RB_tree::insert(const Record &new_entry) Chương 10 – Cây nhiều nhánh Giáo trình Cấu trúc dữ liệu và Giải thuật 280 /* post: Nếu khóa trong new_entry đã có trong RB_tree, phương thức trả về duplicate_error. Ngược lại, phương thức trả về success và new_entry được thêm vào cây sao cho cây vẫn thỏa cây RB-tree. uses: Các phương thức của struct RB_node và hàm đệ quy rb_insert. */ { RB_code status = rb_insert(root, new_entry); switch (status) { case red_node: root->set_color(black); case okay: return success; case duplicate: return duplicate_error; case right_red: case left_red: cout << "WARNING: Program error detected in RB_tree::insert" << endl; return internal_error; } } Hàm đệ quy rb_insert thực hiện thực sự việc thêm phần tử mới vào cây: tìm kiếm trong cây theo cách thông thường, gặp cây con rỗng, thêm nút mới vào tại đây, các việc còn lại được thực hiện trên đường quay về của các lần gọi đệ quy. Hàm này có gọi modify_left hoặc modify_right để thực hiện các phép quay và đổi màu tương ứng với các trường hợp trong hình 10.17. template RB_code RB_tree::rb_insert(Binary_node *¤t, const Record &new_entry) /* pre: current là NULL hoặc là địa chỉ của nút gốc của một cây con trong RB_tree. post: Nếu khóa trong new_entry đã có trong RB_tree, phương thức trả về duplicate_error. Ngược lại, phương thức trả về success và new_entry được thêm vào cây con có gốc là current. Tính chất cây đỏ đen trong cây con này vẫn thỏa ngoại trừ màu tại nút gốc của nó và một trong hai nút con của nút gốc này. Trạng thái này sẽ được hàm modify_right hoặc modify_left điều chỉnh thích hợp (tương ứng các trị của RB_code) để trả về cho lần đệ quy bên trên của lần gọi rb_insert này. uses: Các phương thức của lớp RB_node, các hàm rb_insert (một cách đệ quy), modify_left, và modify_right. */ { RB_code status, child_status; if (current == NULL) { current = new RB_node(new_entry); status = red_node; } else if (new_entry == current->data) return duplicate; else if (new_entry data) { Chương 10 – Cây nhiều nhánh Giáo trình Cấu trúc dữ liệu và Giải thuật 281 child_status = rb_insert(current->left, new_entry); status = modify_left(current, child_status); } else { child_status = rb_insert(current->right, new_entry); status = modify_right(current, child_status); } return status; } Hàm modify_left dựa vào trạng thái của các nút con để thực hiện phép quay hay sửa đổi màu tương ứng, đồng thời cập nhật lại và trả về chỉ số trạng thái cho rb_insert. Chính trong hàm này chúng ta quyết định trì hoãn công việc khôi phục các đặc tính của cây đỏ đen. Khi modify_left được gọi, chúng ta biết rằng việc thêm vào vừa được thực hiện trong cây con bên trái của nút hiện tại, biết được màu của nút hiện tại, và thông qua chỉ số trạng thái, chúng ta còn biết được trường hợp nào đã xảy ra ở cây con trái của nó. Bằng cách sử dụng các thông tin này, chúng ta có thể xác định chính xác những việc cần làm để khôi phục các đặc tính của cây đỏ đen. template RB_code RB_tree::modify_left(Binary_node *¤t, RB_code &child_status) /* pre: Cây con bên trái của current vừa được thêm nút mới, trị trong child_status sẽ quyết định việc xử lý kế tiếp trong hàm này. post: Việc đổi màu hoặc quay cần thiết đã được thực hiện, trạng thái thích hợp được trả về bởi hàm này. uses: Các phương thức của struct RB_node, các hàm rotate_right, double_rotate_right, và flip_color. */ { RB_code status = okay; Binary_node *aunt = current->right; Color aunt_color = black; if (aunt != NULL) aunt_color = aunt->get_color(); switch (child_status) { case okay: break; // Việc xử lý đã kết thúc và không cần lan truyền lên trên nữa. case red_node: if (current->get_color() == red) status = left_red; else status = okay;// current màu đen, nút con trái màu đỏ, đã thỏa cây RB-tree. break; case left_red: if (aunt_color == black) status = rotate_right(current); else status = flip_color(current); break; case right_red: if (aunt_color == black) status = double_rotate_right(current); else status = flip_color(current); Chương 10 – Cây nhiều nhánh Giáo trình Cấu trúc dữ liệu và Giải thuật 282 break; } return status; } Hàm phụ trợ modify_right cũng tương tự, nó xử lý cho các tình huống cây có dạng như những hình ảnh phản chiếu qua gương của các tình huống trong hình 10.17. Hàm đổi màu flip_color được xem như bài tập. Các hàm quay dựa trên cơ sở các hàm quay của cây AVL, có thêm việc gán lại các màu và chỉ số trạng thái thích hợp. 10.4.6. Loại một nút Cũng như cây B-tree, việc loại bỏ một nút phức tạp hơn việc thêm vào, đối với cây đỏ đen, việc loại nút còn khó khăn hơn rất nhiều. Việc thêm vào tạo ra một nút mới màu đỏ dẫn đến nguy cơ vi phạm điều kiện đỏ, chúng ta cần xem xét một cách cẩn thận để giải quyết các vi phạm này. Việc loại một nút đỏ ra khỏi cây không khó lắm. Tuy nhiên, việc loại một nút đen khỏi cây dẫn đến nguy cơ vi phạm điều kiện đen, và nó đòi hỏi chúng ta phải xem xét rất nhiều trường hợp đặc biệt để khôi phục điều kiện đen cho cây.
File đính kèm:
- giao_trinh_cau_truc_du_lieu_va_giai_thuat_chuong_10_cay_nhie.pdf