Tại sao cây Đỏ-Đen rất phổ biến?


46

Dường như ở mọi nơi tôi nhìn thấy, các cấu trúc dữ liệu đang được triển khai bằng cách sử dụng các cây đỏ đen ( std::settrong C ++, SortedDictionarytrong C #, v.v.)

Vừa mới bao phủ (a, b), cây đỏ-đen & AVL trong lớp thuật toán của tôi, đây là những gì tôi nhận được (cũng từ việc hỏi các giáo sư, xem qua một vài cuốn sách và googling một chút):

  • Cây AVL có độ sâu trung bình nhỏ hơn cây đỏ đen và do đó tìm kiếm giá trị trong cây AVL luôn nhanh hơn.
  • Cây đỏ đen tạo ra ít thay đổi cấu trúc hơn để tự cân bằng so với cây AVL, điều này có thể khiến chúng có khả năng chèn / xóa nhanh hơn. Tôi đang nói có khả năng, bởi vì điều này phụ thuộc vào chi phí thay đổi cấu trúc của cây, vì điều này sẽ phụ thuộc rất nhiều vào thời gian chạy và hàm ý (cũng có thể hoàn toàn khác nhau trong ngôn ngữ chức năng khi cây bất biến?)

Có rất nhiều điểm chuẩn trực tuyến so sánh cây AVL và cây Đen-đen, nhưng điều khiến tôi ngạc nhiên là về cơ bản, giáo sư của tôi đã nói rằng, thông thường bạn sẽ làm một trong hai điều sau:

  • Hoặc bạn không thực sự quan tâm nhiều đến hiệu suất, trong trường hợp đó, chênh lệch 10-20% của AVL so với Đỏ-đen trong hầu hết các trường hợp sẽ không thành vấn đề.
  • Hoặc bạn thực sự quan tâm đến hiệu suất, trong trường hợp bạn bỏ cả cây AVL và cây đỏ đen và đi với cây B, có thể được điều chỉnh để hoạt động tốt hơn nhiều (hoặc (a, b) -trees, tôi ' m sẽ đặt tất cả những thứ đó vào một giỏ.)

Lý do là bởi vì cây B lưu trữ dữ liệu gọn hơn trong bộ nhớ (một nút chứa nhiều giá trị) sẽ có ít lỗi nhớ cache hơn. Bạn cũng có thể điều chỉnh việc triển khai dựa trên trường hợp sử dụng và làm cho thứ tự của cây B phụ thuộc vào kích thước bộ đệm của CPU, v.v.

Vấn đề là tôi không thể tìm thấy hầu hết mọi nguồn phân tích việc sử dụng thực tế các cách thực hiện khác nhau của các cây tìm kiếm trên phần cứng hiện đại thực sự. Tôi đã xem qua nhiều cuốn sách về thuật toán và không tìm thấy bất cứ thứ gì có thể so sánh các biến thể cây khác nhau với nhau, ngoài việc chỉ ra rằng một cái có độ sâu trung bình nhỏ hơn cái kia (điều đó không thực sự nói nhiều về cách cây sẽ hành xử trong các chương trình thực tế.)

Điều đó đang được nói, có một lý do đặc biệt tại sao cây đen đỏ đang được sử dụng ở mọi nơi, khi dựa trên những gì được nói ở trên, cây B nên vượt trội hơn chúng? (như điểm chuẩn duy nhất tôi có thể tìm thấy cũng hiển thị http://lh3lh3.users.sourceforge.net/udb.shtml , nhưng nó có thể chỉ là vấn đề của việc triển khai cụ thể). Hay là lý do tại sao tất cả mọi người sử dụng cây Đỏ-đen vì chúng khá dễ thực hiện hoặc để đặt nó bằng các từ khác nhau, khó thực hiện kém?

Ngoài ra, điều này thay đổi như thế nào khi một người chuyển sang vương quốc của các ngôn ngữ chức năng? Có vẻ như cả Clojure và Scala đều sử dụng các thử nghiệm ánh xạ mảng Hash , trong đó Clojure sử dụng hệ số phân nhánh là 32.


8
Để thêm vào nỗi đau của bạn, hầu hết các bài viết so sánh các loại cây tìm kiếm khác nhau thực hiện ... ít hơn các thử nghiệm lý tưởng.
Raphael

1
Bản thân tôi chưa bao giờ hiểu điều này, theo tôi, cây AVL dễ thực hiện hơn cây đỏ đen (ít trường hợp hơn khi tái cân bằng) và tôi chưa bao giờ nhận thấy sự khác biệt đáng kể về hiệu suất.
Jordi Vermeulen

3
Một cuộc thảo luận có liên quan của bạn bè của chúng tôi tại stackoverflow Tại sao std :: map được triển khai dưới dạng cây đỏ-đen? .
Hendrik ngày

Câu trả lời:


10

Để trích dẫn câu trả lời cho Traversals từ gốc trong cây AVL và câu hỏi Red Black Plants '

Đối với một số loại cây tìm kiếm nhị phân, bao gồm cả cây đỏ đen nhưng không phải cây AVL, "sửa lỗi" cho cây có thể dễ dàng dự đoán trên đường đi xuống và thực hiện trong một lần vượt từ trên xuống, khiến cho lần thứ hai không cần thiết. Các thuật toán chèn như vậy thường được thực hiện với một vòng lặp chứ không phải đệ quy và thường chạy nhanh hơn một chút trong thực tế so với các đối tác hai lượt của chúng.

Vì vậy, việc chèn cây RedBlack có thể được thực hiện mà không cần đệ quy, trên một số đệ quy CPU rất tốn kém nếu bạn vượt quá bộ đệm của hàm gọi (ví dụ SPARC do sử dụng cửa sổ Đăng ký )

(Tôi đã thấy phần mềm chạy nhanh hơn 10 lần trên Sparc bằng cách xóa một lệnh gọi hàm, dẫn đến đường dẫn mã thường được gọi là quá sâu cho cửa sổ đăng ký. Vì bạn không biết cửa sổ đăng ký sẽ sâu đến mức nào hệ thống của khách hàng của bạn và bạn không biết bạn đang ở bao xa trong "đường dẫn mã nóng", không sử dụng đệ quy sẽ dễ dự đoán hơn.)

Cũng không mạo hiểm chạy ra khỏi ngăn xếp là một lợi ích.


Nhưng một cây cân bằng với 2 ^ 32 nút sẽ yêu cầu không quá 32 mức đệ quy. Ngay cả khi khung ngăn xếp của bạn là 64 byte, không quá 2 kb không gian ngăn xếp. Điều đó thực sự có thể làm cho một sự khác biệt? Tôi sẽ nghi ngờ nó.
Bjorn Lindqvist

@ BjornLindqvist, Trên bộ xử lý SPARC vào những năm 1990, tôi thường tăng tốc độ gấp 10 lần bằng cách thay đổi đường dẫn mã phổ biến từ độ sâu ngăn xếp từ 7 đến 6! Đọc về cách nó đã đăng ký tập tin ....
Ian Ringrose

9

Gần đây tôi cũng đã nghiên cứu chủ đề này, vì vậy đây là những phát hiện của tôi, nhưng hãy nhớ rằng tôi không phải là một chuyên gia về cấu trúc dữ liệu!

Có một số trường hợp bạn hoàn toàn không thể sử dụng cây B.

Một trường hợp nổi bật là std::maptừ C ++ STL. Tiêu chuẩn yêu cầu insertkhông làm mất hiệu lực các trình vòng lặp hiện có

Không có vòng lặp hoặc tài liệu tham khảo là vô hiệu.

http://en.cppreference.com/w/cpp/container/map/insert

Điều này loại trừ cây B dưới dạng triển khai vì việc chèn sẽ di chuyển xung quanh các phần tử hiện có.

Một trường hợp sử dụng tương tự khác là cơ sở dữ liệu xâm nhập. Đó là, thay vì lưu trữ dữ liệu của bạn bên trong nút của cây, bạn lưu trữ con trỏ tới trẻ em / cha mẹ bên trong cấu trúc của bạn:

// non intrusive
struct Node<T> {
    T value;
    Node<T> *left;
    Node<T> *right;
};
using WalrusList = Node<Walrus>;

// intrusive
struct Walrus {
    // Tree part
    Walrus *left;
    Walrus *right;

    // Object part
    int age;
    Food[4] stomach;
};

Bạn không thể tạo một cây B xâm nhập, vì nó không phải là cấu trúc dữ liệu chỉ con trỏ.

Cây đỏ đen xâm nhập được sử dụng, ví dụ, trong jemalloc để quản lý các khối bộ nhớ miễn phí. Đây cũng là một cấu trúc dữ liệu phổ biến trong nhân Linux.

Tôi cũng tin rằng việc thực hiện "đệ quy đuôi đơn" không phải là lý do cho sự phổ biến của cây đen đỏ như là một cấu trúc dữ liệu có thể thay đổi .

Trước hết, độ sâu ngăn xếp không liên quan ở đây, bởi vì (với chiều cao ) bạn sẽ hết bộ nhớ chính trước khi hết dung lượng ngăn xếp. Jemalloc là hài lòng với preallocating sâu trường hợp xấu nhất trên stack.logn

Có một số hương vị của việc thực hiện cây đỏ-đen. Một người nổi tiếng bị nghiêng trái cây đen đỏ bởi Robert Sedgewick ( THẬN TRỌNG! Có những biến thể khác cũng được đặt tên là "nghiêng trái", nhưng sử dụng một thuật toán khác). Biến thể này thực sự cho phép thực hiện các phép quay trên đường đi xuống cây, nhưng nó thiếu tính chất quan trọng của số lượng cố định được khấu hao của và điều này làm cho nó chậm hơn ( được đo bởi tác giả của jemalloc ). Hoặc, như opendatastrutures đặt nóO(1)

Biến thể của cây đen-đen của Andersson, biến thể của cây đỏ-đen và cây AVL của Sedgewick đều đơn giản để thực hiện hơn so với cấu trúc RedBlackTree được định nghĩa ở đây. Thật không may, không ai trong số họ có thể đảm bảo rằng thời gian khấu hao dành cho việc tái cân bằng là cho mỗi lần cập nhật.O(1)

Các biến thể được mô tả trong opendatastructures sử dụng các con trỏ cha, một đường dẫn xuống đệ quy để chèn và một vòng lặp lặp lên cho các bản sửa lỗi. Các cuộc gọi đệ quy nằm ở vị trí đuôi và trình biên dịch tối ưu hóa điều này thành một vòng lặp (Tôi đã kiểm tra điều này trong Rust).

Đó là, bạn có thể nhận được một triển khai vòng lặp bộ nhớ liên tục của cây tìm kiếm có thể thay đổi mà không cần bất kỳ phép màu đỏ đen nào nếu bạn sử dụng các con trỏ cha. Điều này làm việc cho cây B là tốt. Bạn cần phép thuật cho biến thể đệ quy bất biến đuôi đơn, và nó sẽ phá vỡ bản sửa lỗi .O(1)


3

Chà, đây không phải là một câu trả lời có thẩm quyền, nhưng bất cứ khi nào tôi phải mã hóa cây tìm kiếm nhị phân cân bằng, thì đó là cây đỏ đen. Có một vài lý do cho việc này:

1) Chi phí chèn trung bình là không đổi đối với cây đỏ đen (nếu bạn không phải tìm kiếm), trong khi đó là logarit cho cây AVL. Hơn nữa, nó liên quan đến nhiều nhất một tái cấu trúc phức tạp. Nó vẫn là O (log N) trong trường hợp xấu nhất, nhưng đó chỉ là sự đổi màu đơn giản.

2) Họ chỉ cần 1 bit thông tin bổ sung cho mỗi nút và bạn thường có thể tìm cách lấy thông tin đó miễn phí.

3) Tôi không phải làm điều này rất thường xuyên, vì vậy mỗi lần tôi làm điều đó tôi phải tìm ra cách làm lại từ đầu. Các quy tắc đơn giản và sự tương ứng với 2-4 cây làm cho nó có vẻ dễ dàng mọi lúc , mặc dù mã hóa ra rất phức tạp mỗi lần . Tôi vẫn hy vọng rằng một ngày nào đó mã sẽ trở nên đơn giản.

4) Cách cây đỏ-đen chia nút cây 2-4 tương ứng và chèn khóa giữa vào nút 2-4 cha mẹ chỉ bằng cách tô màu lại là siêu thanh lịch. Tôi chỉ thích làm điều đó.


0

Cây đỏ-đen hoặc AVL có lợi thế hơn cây B và tương tự khi khóa dài hoặc vì một lý do nào khác, việc di chuyển một phím là tốn kém.

Tôi đã tạo ra sự thay thế của riêng mình std::settrong một dự án lớn, vì một số lý do hiệu suất. Tôi đã chọn AVL thay vì màu đỏ-đen vì lý do hiệu suất (nhưng sự tăng cường hiệu suất nhỏ đó không phải là lý do để tự mình thay vì std :: set). "Chìa khóa" phức tạp và khó di chuyển là một yếu tố quan trọng. Cây (a, b) vẫn có ý nghĩa nếu bạn cần một mức độ gián tiếp khác trước các phím? AVL và cây đỏ đen có thể được cấu trúc lại mà không cần di chuyển phím, vì vậy chúng có lợi thế đó khi các phím đắt tiền để di chuyển.


Trớ trêu thay, cây đỏ-đen "chỉ" là một trường hợp đặc biệt của (a, b) -trees, vì vậy vấn đề dường như đi xuống để điều chỉnh các tham số? (cc @Gilles)
Raphael
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.