Tại sao std :: map được triển khai dưới dạng cây đỏ-đen?


193

Tại sao được std::mapthực hiện như một cây đỏ-đen ?

Có một số cây tìm kiếm nhị phân cân bằng (BST) ngoài kia. Thiết kế đánh đổi trong việc chọn cây đỏ đen là gì?


26
Mặc dù tất cả các triển khai tôi đã thấy sử dụng cây RB, lưu ý rằng điều này vẫn phụ thuộc vào việc triển khai.
Thomas

3
@Thomas. Nó phụ thuộc vào việc triển khai, vậy tại sao tất cả việc triển khai đều sử dụng cây RB?
Denis Gorodetskiy

1
Tôi thực sự muốn biết liệu có người triển khai STL nào đã nghĩ đến việc sử dụng Danh sách bỏ qua hay không.
Matthieu M.

2
Bản đồ và bộ của C ++ thực sự là thứ tự bản đồ và bộ được đặt hàng. Chúng không được thực hiện bằng cách sử dụng các hàm băm. Mọi truy vấn sẽ mất O(logn)và không O(1), nhưng các giá trị sẽ luôn được sắp xếp. Bắt đầu từ C ++ 11 (tôi nghĩ), có unordered_mapunordered_set, được triển khai bằng các hàm băm và trong khi chúng không được sắp xếp, hầu hết các truy vấn và thao tác đều có thể trong O(1)(trung bình)
SomethingS Something

@Thomas đó là sự thật, nhưng không thú vị trong thực tế. Tiêu chuẩn này đảm bảo độ phức tạp với một thuật toán cụ thể hoặc tập hợp các thuật toán trong tâm trí.
Justin Meiners

Câu trả lời:


125

Có lẽ hai thuật toán cây tự cân bằng phổ biến nhất là cây Đỏ-Đencây AVL . Để cân bằng cây sau khi chèn / cập nhật cả hai thuật toán, sử dụng khái niệm xoay trong đó các nút của cây được xoay để thực hiện cân bằng lại.

Mặc dù trong cả hai thuật toán, các thao tác chèn / xóa là O (log n), trong trường hợp xoay vòng cân bằng lại cây Đỏ-Đen là thao tác O (1) trong khi với AVL, đây là thao tác O (log n) , thực hiện Cây Đỏ-Đen hiệu quả hơn trong khía cạnh này của giai đoạn cân bằng lại và một trong những lý do có thể khiến nó được sử dụng phổ biến hơn.

Cây đỏ-đen được sử dụng trong hầu hết các thư viện bộ sưu tập, bao gồm các dịch vụ từ Java và Microsoft .NET Framework.


54
bạn làm cho âm thanh giống như cây đỏ đen có thể thực hiện sửa đổi cây trong thời gian O (1), điều này không đúng. sửa đổi cây là O (log n) cho cả cây đỏ-đen và AVL. điều đó làm cho nó cân nhắc xem phần cân bằng của sửa đổi cây là O (1) hay O (log n) vì thao tác chính đã là O (log n). thậm chí sau tất cả các công việc hơi thừa mà cây AVL thực hiện dẫn đến cây cân bằng chặt chẽ hơn dẫn đến việc tra cứu nhanh hơn một chút. do đó, đây là một sự đánh đổi hoàn toàn hợp lệ và không làm cho cây AVL thua kém cây đỏ đen.
Necromancer

35
Bạn phải nhìn xa hơn sự phức tạp so với thời gian chạy thực tế để thấy sự khác biệt - Cây AVL thường có tổng thời gian chạy thấp hơn khi có nhiều tra cứu hơn so với chèn / xóa. Cây RB có tổng thời gian chạy thấp hơn khi có nhiều lần chèn / xóa hơn. Tất nhiên, tỷ lệ chính xác xảy ra sự cố phụ thuộc vào nhiều chi tiết triển khai, phần cứng và cách sử dụng chính xác, nhưng vì các tác giả thư viện phải hỗ trợ một loạt các mô hình sử dụng, họ phải đoán. AVL cũng khó thực hiện hơn một chút, vì vậy bạn có thể muốn một lợi ích đã được chứng minh để sử dụng nó.
Steve Jessop

6
Cây RB không phải là "triển khai mặc định". Mỗi người thực hiện chọn một thực hiện. Theo như chúng tôi biết, tất cả họ đều đã chọn cây RB, vì vậy có lẽ đây là để thực hiện hoặc để dễ thực hiện / bảo trì. Như tôi đã nói, điểm dừng cho hiệu suất có thể không ngụ ý rằng họ nghĩ rằng có nhiều phần chèn / xóa hơn phần tra cứu, chỉ là tỷ lệ giữa hai phần này vượt quá mức mà họ nghĩ RB có thể vượt qua AVL.
Steve Jessop

9
@Denis: thật không may, cách duy nhất để có được các con số là lập danh sách std::maptriển khai, theo dõi các nhà phát triển và hỏi họ những tiêu chí nào họ đã sử dụng để đưa ra quyết định, vì vậy điều này vẫn chỉ là suy đoán.
Steve Jessop

4
Thiếu từ tất cả điều này là chi phí, mỗi nút, để lưu trữ thông tin phụ trợ cần thiết để đưa ra quyết định cân bằng. Cây đỏ-đen yêu cầu 1 bit để thể hiện màu. Cây AVL yêu cầu ít nhất 2 bit (để biểu thị -1, 0 hoặc 1).
SJHowe

46

Nó thực sự phụ thuộc vào cách sử dụng. Cây AVL thường có nhiều vòng quay tái cân bằng. Vì vậy, nếu ứng dụng của bạn không có quá nhiều thao tác chèn và xóa, nhưng nặng về tìm kiếm, thì cây AVL có lẽ là một lựa chọn tốt.

std::map sử dụng cây Đỏ-Đen vì nó có sự đánh đổi hợp lý giữa tốc độ chèn / xóa nút và tìm kiếm.


1
Bạn có chắc chắn về điều đó không??? Cá nhân tôi nghĩ rằng cây Đỏ-Đen là một hoặc phức tạp hơn, không bao giờ đơn giản hơn. Điều duy nhất, là ở cây Rd-Black, việc cân bằng lại xảy ra ít thường xuyên hơn AVL.
Eric Ouellet

1
@Eric Về mặt lý thuyết, cả cây R / B và cây AVL đều có độ phức tạp O (log n)) để chèn và xóa. Nhưng một phần lớn của chi phí vận hành là xoay vòng, khác nhau giữa hai cây này. Vui lòng tham khảo thảo luận.fogcalet.com/joelonsoftware/ Vì Trích dẫn: "việc cân bằng cây AVL có thể yêu cầu xoay O (log n), trong khi một cây đen đỏ sẽ mất tối đa hai lần quay để cân bằng (mặc dù có thể phải cân bằng kiểm tra các nút O (log n) để quyết định nơi cần quay). " Chỉnh sửa ý kiến ​​của tôi cho phù hợp.
webbertiger

26

Cây AVL có chiều cao tối đa 1,44logn, trong khi cây RB có tối đa 2logn. Chèn một phần tử trong AVL có thể ngụ ý sự cân bằng tại một điểm trên cây. Việc tái cân bằng kết thúc việc chèn. Sau khi chèn một chiếc lá mới, việc cập nhật tổ tiên của chiếc lá đó phải được thực hiện đến tận gốc, hoặc đến một điểm mà hai cây con có độ sâu bằng nhau. Xác suất phải cập nhật k nút là 1/3 ^ k. Cân bằng lại là O (1). Loại bỏ một yếu tố có thể ngụ ý nhiều hơn một sự cân bằng lại (tối đa một nửa độ sâu của cây).

Cây RB là cây B của bậc 4 được biểu thị dưới dạng cây tìm kiếm nhị phân. Một nút 4 trong cây B dẫn đến hai cấp độ trong BST tương đương. Trong trường hợp xấu nhất, tất cả các nút của cây là 2 nút, chỉ có một chuỗi 3 nút xuống một chiếc lá. Lá đó sẽ ở khoảng cách 2logn từ gốc.

Đi xuống từ gốc đến điểm chèn, người ta phải thay đổi 4 nút thành 2 nút, để đảm bảo mọi thao tác chèn sẽ không bão hòa một chiếc lá. Quay trở lại từ việc chèn, tất cả các nút này phải được phân tích để đảm bảo chúng đại diện chính xác cho 4 nút. Điều này cũng có thể được thực hiện đi xuống trong cây. Chi phí toàn cầu sẽ như nhau. Không có bữa trưa miễn phí đâu! Loại bỏ một yếu tố khỏi cây là theo cùng một thứ tự.

Tất cả các cây này yêu cầu các nút mang thông tin về chiều cao, cân nặng, màu sắc, v.v ... Chỉ các cây Splay không có thông tin bổ sung như vậy. Nhưng hầu hết mọi người đều sợ cây Splay, vì sự tàn bạo trong cấu trúc của chúng!

Cuối cùng, cây cũng có thể mang thông tin trọng lượng trong các nút, cho phép cân bằng trọng lượng. Đề án khác nhau có thể được áp dụng. Người ta phải cân bằng lại khi một cây con chứa hơn 3 lần số phần tử của cây con khác. Cân bằng lại được thực hiện một lần nữa hoặc xoay vòng một hoặc hai lần. Điều này có nghĩa là một trường hợp xấu nhất của 2,4logn. Người ta có thể thoát ra với 2 lần thay vì 3, tỷ lệ tốt hơn nhiều, nhưng điều đó có thể có nghĩa là để lại ít hơn 1% số cây con không cân bằng ở đây và đó. Khó khăn!

Loại cây nào là tốt nhất? AVL chắc chắn. Chúng là mã đơn giản nhất và có chiều cao xấu nhất gần nhất để đăng nhập. Đối với cây có 1000000 phần tử, AVL sẽ có chiều cao tối đa 29, RB 40 và trọng lượng cân bằng 36 hoặc 50 tùy theo tỷ lệ.

Có rất nhiều biến số khác: tính ngẫu nhiên, tỷ lệ thêm, xóa, tìm kiếm, v.v.


2
Câu trả lời tốt. Nhưng nếu AVL là tốt nhất, tại sao thư viện chuẩn thực hiện std :: map dưới dạng cây RB?
Denis Gorodetskiy

13
Tôi không đồng ý rằng cây AVL chắc chắn là tốt nhất. Mặc dù chúng có chiều cao thấp, nhưng chúng đòi hỏi (tổng cộng) nhiều công việc phải làm hơn so với công việc tái cân bằng cây đỏ / đen (O (log n) so với công việc tái cân bằng khấu hao O (1)). Cây Splay có thể tốt hơn nhiều, tốt hơn nhiều và sự khẳng định của bạn rằng mọi người sợ chúng là không có cơ sở. Không có một sơ đồ cân bằng cây "tốt nhất" nào ngoài kia.
templatetypedef

Câu trả lời gần như hoàn hảo. Tại sao bạn nói AVL là tốt nhất. Điều đó đơn giản là sai và đó là lý do tại sao hầu hết việc thực hiện chung sử dụng cây Đỏ-Đen. Bạn cần phải có tỷ lệ thao tác đọc qua khá cao để chọn AVL. Ngoài ra, AVL có dung lượng bộ nhớ ít hơn một chút so với RB.
Eric Ouellet

Tôi đồng ý rằng AVL có xu hướng tốt hơn trong hầu hết các trường hợp, bởi vì thông thường cây được tìm kiếm thường xuyên hơn so với chúng được chèn vào. Tại sao cây RB lại được coi là tốt hơn như vậy khi nó là cây có một chút lợi thế trong trường hợp viết chủ yếu và quan trọng hơn là một nhược điểm nhỏ trong trường hợp đọc chủ yếu? Có thực sự tin rằng bạn sẽ chèn nhiều hơn bạn sẽ tìm thấy?
doug65536

25

Các câu trả lời trước chỉ giải quyết các lựa chọn thay thế cây và màu đen đỏ có lẽ chỉ còn lại vì lý do lịch sử.

Tại sao không phải là bảng băm?

Một loại chỉ yêu cầu <toán tử (so sánh) được sử dụng làm khóa trong cây. Tuy nhiên, các bảng băm yêu cầu mỗi loại khóa có một hashhàm được xác định. Giữ các yêu cầu loại ở mức tối thiểu là rất quan trọng đối với lập trình chung để bạn có thể sử dụng nó với nhiều loại và thuật toán khác nhau.

Thiết kế một bảng băm tốt đòi hỏi kiến ​​thức sâu sắc về bối cảnh mà nó sẽ được sử dụng. Nó nên sử dụng địa chỉ mở, hoặc chuỗi liên kết? Những mức tải nào nên chấp nhận trước khi thay đổi kích thước? Nó có nên sử dụng một hàm băm đắt tiền để tránh va chạm, hay một thứ thô và nhanh?

Vì STL không thể lường trước đâu là lựa chọn tốt nhất cho ứng dụng của bạn, nên mặc định cần phải linh hoạt hơn. Cây "chỉ hoạt động" và quy mô độc đáo.

(C ++ 11 đã thêm các bảng băm với unordered_map. Bạn có thể thấy từ tài liệu mà nó yêu cầu thiết lập các chính sách để định cấu hình nhiều tùy chọn này.)

Còn những cây khác thì sao?

Cây Đỏ Đen cung cấp tra cứu nhanh và tự cân bằng, không giống như các BST. Một người dùng khác đã chỉ ra những ưu điểm của nó so với cây AVL tự cân bằng.

Alexander Stepanov (Người tạo ra STL) nói rằng anh ta sẽ sử dụng Cây B * thay vì cây Đỏ-Đen nếu anh ta viết std::maplại, vì nó thân thiện hơn với bộ nhớ đệm hiện đại.

Một trong những thay đổi lớn nhất kể từ đó là sự phát triển của cache. Việc bỏ lỡ bộ nhớ cache rất tốn kém, vì vậy địa phương tham chiếu quan trọng hơn nhiều bây giờ. Các cấu trúc dữ liệu dựa trên nút, có địa phương tham chiếu thấp, có ý nghĩa ít hơn nhiều. Nếu tôi thiết kế STL ngày hôm nay, tôi sẽ có một bộ container khác. Ví dụ, B * -tree trong bộ nhớ là lựa chọn tốt hơn nhiều so với cây đỏ đen để thực hiện một thùng chứa kết hợp. - Alexander Stepanov

Bản đồ có nên sử dụng cây xanh?

Một triển khai bản đồ có thể khác sẽ là một vectơ được sắp xếp (sắp xếp chèn) và tìm kiếm nhị phân. Điều này sẽ hoạt động tốt đối với các container không được sửa đổi thường xuyên nhưng được truy vấn thường xuyên. Tôi thường làm điều này trong C như qsortbsearchđược xây dựng trong.

Tôi thậm chí có cần sử dụng bản đồ?

Cân nhắc bộ nhớ cache có nghĩa là hiếm khi có ý nghĩa để sử dụng std::listhoặc std::dequehơn std:vectorngay cả đối với những tình huống chúng tôi được dạy ở trường (chẳng hạn như xóa một yếu tố khỏi giữa danh sách). Áp dụng lý do tương tự, sử dụng vòng lặp for để tìm kiếm tuyến tính một danh sách thường hiệu quả và sạch sẽ hơn so với việc xây dựng bản đồ cho một vài tra cứu.

Tất nhiên việc chọn một container có thể đọc được thường quan trọng hơn hiệu suất.


3

Cập nhật 2017-06-2014: webbertiger chỉnh sửa câu trả lời sau khi tôi nhận xét. Tôi nên chỉ ra rằng câu trả lời của nó bây giờ tốt hơn rất nhiều đối với mắt tôi. Nhưng tôi vẫn giữ câu trả lời của mình như thông tin bổ sung ...

Do thực tế là tôi nghĩ rằng câu trả lời đầu tiên là sai (sửa: không phải cả hai nữa) và thứ ba có một khẳng định sai. Tôi cảm thấy mình phải làm rõ mọi chuyện ...

Hai cây phổ biến nhất là AVL và Red Black (RB). Sự khác biệt chính nằm ở việc sử dụng:

  • AVL: Tốt hơn nếu tỷ lệ tham vấn (đọc) lớn hơn thao tác (sửa đổi). In chân bộ nhớ ít hơn một chút so với RB (do bit cần thiết để tô màu).
  • RB: Tốt hơn trong các trường hợp chung khi có sự cân bằng giữa tham vấn (đọc) và thao tác (sửa đổi) hoặc sửa đổi nhiều hơn so với tham vấn. Dấu chân bộ nhớ lớn hơn một chút do lưu trữ cờ đỏ-đen.

Sự khác biệt chính đến từ màu sắc. Bạn có ít hành động cân bằng lại trong cây RB hơn AVL vì việc tô màu cho phép bạn đôi khi bỏ qua hoặc rút ngắn các hành động cân bằng lại có chi phí hi tương đối. Do tô màu, cây RB cũng có mức nút cao hơn vì nó có thể chấp nhận các nút màu đỏ giữa các nút đen (có khả năng tăng gấp 2 lần mức) khiến cho việc tìm kiếm (đọc) kém hiệu quả hơn một chút ... nhưng vì nó là một không đổi (2x), nó ở trong O (log n).

Nếu bạn xem xét cú đánh hiệu suất để sửa đổi cây (có ý nghĩa) so với cú đánh hiệu suất của sự tham khảo ý kiến ​​của cây (gần như không đáng kể), thì việc chọn RB hơn AVL trong trường hợp chung là điều tự nhiên.


2

Nó chỉ là lựa chọn thực hiện của bạn - chúng có thể được thực hiện như bất kỳ cây cân bằng nào. Các lựa chọn khác nhau đều có thể so sánh với sự khác biệt nhỏ. Vì vậy, bất kỳ là tốt như bất kỳ.

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.