|

Cây Đỏ Đen (Red-Black Tree) – Phần 3 (Delete)

This entry is part 16 of 16 in the series Cấu trúc dữ liệu

Ở bài trước, chúng ta đã hoàn thành việc insert một Node vào Red Back Tree. Còn bây giờ, hãy cũng tìm cách để xóa một node khỏi cây đỏ đen nào.

Xóa (Delete) là một quá trình cực kì phức tạp. Mình đã phải tham khảo rất nhiều nguồn mới hoàn thành bài code hoàn chỉnh. Do đó, các bạn nên đọc kĩ tất cả và không nên bỏ qua bất cứ phần nào.

1. Nâng cấp hàm quay cây – Update Rotate

Tại sao phải nâng cấp?

Như các bạn đã biết, Red Black Tree có một phần tử đặc biệt – đó là Root (luôn luôn mang màu đen). Do đó khi lỡ xóa Root thì ta phải đặt lại Root mới. Mà muốn có được Root thì ta phải đưa hai hàm rotateRight và rotateLeft vào struct Red Black Tree:

2. Sơ lược về cách xóa ở cây đỏ đen

  • Khi xóa một Node ở Binary Search Tree, chúng ta chưa bao giờ thực sự một Node có hai con cả. Mà tìm một Node khác để xóa (lớn nhất bên trái) rồi quy về trường hợp (có một con) hoặc (có 0 con – lá). Như vậy, quy ước:
    • vDelete: Là node bị xóa (Đôi khi gọi tắt là Node v)
    • uReplace: Là sẽ thay thế vDelete (u có thể bằng NULL vì nó cũng là một Node đen)

    • Khi chúng ta lỡ xóa đi một Node đen: sẽ có ít nhất một đường dẫn bị giảm số lượng Node đen đi 1. Điều đó làm vi phạm quy tắc số 4 – Mọi đường dẫn từ một node đến bất kì node NULL (thuộc con của nó ) thì đều có cùng số lượng node đen.
    • Nếu vDelete hoặc uReplace màu đỏ: Ta chỉ đơn giản xóa đi Node màu đỏ với giá trị data của vDelete. Việc này hoàn toàn không vi phạm quy tắc trên. Do đó, đây được coi là trường hợp dễ nhất. (u và v không thể cùng màu đỏ vì theo quy tắc 3 – không có hai node đỏ liền kề)

 

    • Nếu vDelete và uReplace màu đen: Khi một nút màu đen bị xóa (v) và được thay thế bằng nút con màu đen (u), nút con đó (u) được đánh dấu là nút đen đôi . Nhiệm vụ chính bây giờ trở thành chuyển đổi màu đen đôi này thành màu đen đơn.

 

3. Xử lí trường hợp đen đôi – fixDoubleBlack

Note: Đên đôi + Đỏ đơn = Đen Đơn Ở bài trước, chúng ta phân chia các trường hợp dựa vào vị trí của Node X được thêm vào và cha của nó. Còn bây giờ, không hề có Node mới nào. Do đó ta phải dựa chia các trường hợp dựa trên Node Anh Em (sibling) của Node bị xóa (vDelete). Cách tìm Node Anh Em (sibling) cũng khá đơn giản vì nó có cùng cha với vDelete:

Mình sẽ viết thêm một hàm hỗ trợ với mục đích là kiểm tra một Node có con màu đỏ hay không – hasRedChild()

Giờ ta chia hàm fixDoubleBlack() ra 4 trường hợp dựa trên Node Anh Em (Sibling):

  1. Node Anh Em màu đen và có con đỏ.
  2. Node Anh Em màu đen và không có con đỏ.
  3. Node Anh Em có màu đỏ.
  4. Node Anh Em == NULL

1. sibing.color == BLACK and hasRedChild(sibling)

Ta cứ dựa trên vị trí Node sib và con đỏ của nó để phân chia các trường hợp con. Khá quen thuộc đó là Left left, Left right, Right right, Right left.

a) Right right case – Phải phải

    • Xảy ra: sib là con phải__ sib.right->color == RED
    • Xử lí:
          • sib.right->color = sib.color.
          • sib->color = sib.parent->color
          • par->color = BLACK
          • rotateLeft(par)

  • Khi sib nằm phía bên phải và có con phải màu đỏ (cả 2 con màu đỏ cũng không sao). Ta sẽ coi đây là trường hợp Phải phải và tiến hành quay Trái Node sibling.
  • Sau khi quay phải, Node sibling sẽ lên làm cha. Do đó, về màu sắc, sib.right phải thay thế vai trò của sib. Và đương nhiên sib phải mang màu sắc của par.
  • Par.color = BLACK. Vì Đên đôi (v) + Đỏ đơn(par) = Đen đơn(par)

Nhiều bạn sẽ thắc mắc rằng tại sao: mình ghi quay ở bước cuối nhưng trong hình lại quay ở bước đầu tiên? Thực ra thì bạn quay trước hay sau gì cũng được cả. Chỉ là do sau khi quay, vị trí các node có thay đổi nên mình đổi màu trước cho nó tiện.

b) Right left case – Phải trái

    • Xảy ra: sib là con phải__ ngược lại trường hợp trên
    • Xử lí:
          • sib.left->color = par.color.
          • par->color = BLACK
          • rotateRight(sib)
          • rotateLeft(par)

c) Left left case – Trái trái

    • Xảy ra: sib là con trái__ sib.left->color == RED
    • Xử lí:
          • sib.left->color = sib.color.
          • sib->color = sib.parent->color
          • par->color = BLACK
          • rotateRight(par)

d) Left right case – Trái phải

    • Xảy ra: sib là con trái__ ngược lại trường hợp trên
    • Xử lí:
          • sib.right->color = par.color.
          • par->color = BLACK
          • rotateLeft(sib)
          • rotateRight(par)

2. sibing.color == BLACK and NOT_hasRedChild(sibling)

  • Xử lí:
    • sib.color = RED
    • if par.color == BLACK
      • fixDoubleBlack(par)
    • else
      • par.color = BLACK
  • Có thể thấy Đen đôi (Double Black) tượng trưng cho việc thiếu hụt Node đen. Trong khi đó, Node Sibling có cả nhà màu đen (Node par auto đen nhé, nếu đỏ thì quy về u or v đỏ rồi). Vì vậy khi đổi sib.color = RED thì sẽ không gây ra xung đột đỏ.
  • Và vì sao phải fixDoubleBlack(par)? Mình có thể giải thích nhưng nó sẽ dài. Các bạn tự suy nghĩ nhé :))) Gợi ý: Không thể có 3 Node đen tiếp (Bài 1 – Introduction)

3. sibing.color == 1 (RED)

  • Xử lí khá đơn giản. Nếu sib nằm bên phải thì quay trái cha – rotatLeft(par). Ngược lại thì quay trái – rotateLeft(par). Rồi sau đó tô lại màu – recolor.
  • Như vậy ta đã đưa về TH – sibing.color == BLACK and hasRedChild(sibling). Giờ chỉ việc dùng lại hàm fixDoubleBlack cho Node u thôi.

4. sibing == NULL

Coi vai trò DoubleBlack của u sang cho Node cha. Rồi FixDoubleBlack(par).

5. FixDoubleBlack() – CODE

4. Tiến hành xóa – deleteNode(Node* vDelete)

5. Tổng kết – xóa ở cây đỏ đen

Full Code:

Console:

Chúc mừng bạn đã quay trúng ô “hại não”. Đọc hết đống này chắc các bạn đau đầu lắm :))) Mình định giải thích thêm nhưng bài đã dài lắm rồi. Nếu các bạn có thắc mắc gì thì có thể lên group Lập Trình Không Khó hỏi nhé. Ở đó, anh Nguyễn Văn Hiếu sẽ trực tiếp trả lời câu hỏi của bạn. Cảm ơn các rất nhiềuuuuuuuuuu.

Nguồn tham khảo:

Similar Posts

Subscribe
Notify of
guest
20 Bình luận
Inline Feedbacks
View all comments