Câu trả lời:
Để tranh luận về hiệu suất của cây nhị phân là vô nghĩa - chúng không phải là cấu trúc dữ liệu, mà là một họ cấu trúc dữ liệu, tất cả đều có các đặc tính hiệu suất khác nhau. Mặc dù đúng là cây nhị phân không cân bằng hoạt động kém hơn nhiều so với cây nhị phân tự cân bằng để tìm kiếm, có nhiều cây nhị phân (như cố gắng nhị phân) mà "cân bằng" không có nghĩa.
map
và set
các đối tượng trong thư viện của nhiều ngôn ngữ.Lý do mà cây nhị phân được sử dụng thường xuyên hơn cây n-ary để tìm kiếm là vì cây n-ary phức tạp hơn, nhưng thường không mang lại lợi thế tốc độ thực sự.
Trong cây nhị phân (cân bằng) với m
các nút, việc chuyển từ cấp độ này sang cấp độ tiếp theo đòi hỏi một so sánh và có các log_2(m)
cấp độ, cho tổng số log_2(m)
so sánh.
Ngược lại, một cây n-ary sẽ yêu cầu log_2(n)
so sánh (sử dụng tìm kiếm nhị phân) để chuyển sang cấp độ tiếp theo. Vì có log_n(m)
tổng cấp, nên việc tìm kiếm sẽ yêu cầu log_2(n)*log_n(m)
= log_2(m)
so sánh tổng. Vì vậy, mặc dù cây n-ary phức tạp hơn, chúng không cung cấp lợi thế về tổng số so sánh cần thiết.
(Tuy nhiên, cây n-ary vẫn hữu ích trong các tình huống thích hợp. Các ví dụ xuất hiện ngay lập tức là cây bốn lá và cây phân vùng không gian khác, trong đó việc phân chia không gian chỉ sử dụng hai nút cho mỗi cấp sẽ khiến logic trở nên phức tạp không cần thiết; Cây B được sử dụng trong nhiều cơ sở dữ liệu, trong đó yếu tố giới hạn không phải là có bao nhiêu phép so sánh được thực hiện ở mỗi cấp mà là có thể tải bao nhiêu nút từ ổ cứng cùng một lúc)
Khi hầu hết mọi người nói về cây nhị phân, họ thường không nghĩ về cây tìm kiếm nhị phân , vì vậy tôi sẽ đề cập đến điều đó trước tiên.
Cây tìm kiếm nhị phân không cân bằng thực sự hữu ích cho việc học nhiều hơn so với việc giáo dục học sinh về cấu trúc dữ liệu. Đó là bởi vì, trừ khi dữ liệu đến theo thứ tự tương đối ngẫu nhiên, cây có thể dễ dàng thoái hóa thành dạng trường hợp xấu nhất, là danh sách được liên kết, vì cây nhị phân đơn giản không được cân bằng.
Một trường hợp tốt: tôi đã từng phải sửa một số phần mềm tải dữ liệu của nó vào cây nhị phân để thao tác và tìm kiếm. Nó đã viết dữ liệu ra dưới dạng sắp xếp:
Alice
Bob
Chloe
David
Edwina
Frank
do đó, khi đọc nó trở lại, kết thúc với cây sau:
Alice
/ \
= Bob
/ \
= Chloe
/ \
= David
/ \
= Edwina
/ \
= Frank
/ \
= =
đó là hình thức thoái hóa. Nếu bạn đi tìm Frank trong cây đó, bạn sẽ phải tìm kiếm tất cả sáu nút trước khi bạn tìm thấy anh ta.
Cây nhị phân trở nên thực sự hữu ích cho việc tìm kiếm khi bạn cân bằng chúng. Điều này liên quan đến việc xoay các cây con thông qua nút gốc của chúng sao cho chênh lệch chiều cao giữa hai cây con nhỏ hơn hoặc bằng 1. Thêm các tên trên một lần vào một cây cân bằng sẽ cho bạn trình tự sau:
1. Alice
/ \
= =
2. Alice
/ \
= Bob
/ \
= =
3. Bob
_/ \_
Alice Chloe
/ \ / \
= = = =
4. Bob
_/ \_
Alice Chloe
/ \ / \
= = = David
/ \
= =
5. Bob
____/ \____
Alice David
/ \ / \
= = Chloe Edwina
/ \ / \
= = = =
6. Chloe
___/ \___
Bob Edwina
/ \ / \
Alice = David Frank
/ \ / \ / \
= = = = = =
Bạn thực sự có thể thấy toàn bộ các cây con quay sang trái (trong bước 3 và 6) khi các mục nhập được thêm vào và điều này mang lại cho bạn một cây nhị phân cân bằng trong đó tra cứu trường hợp xấu nhất O(log N)
thay vì O(N
) mà dạng thoái hóa đưa ra. Không có điểm nào cao nhất NULL ( =
) khác với mức thấp nhất nhiều hơn một cấp. Và, trong cây cuối cùng ở trên, bạn có thể tìm thấy Frank chỉ bằng cách nhìn vào ba nút ( Chloe
,Edwina
và cuối cùng, Frank
).
Tất nhiên, chúng có thể trở nên hữu ích hơn nữa khi bạn làm cho chúng cân bằng cây đa chiều hơn là tress nhị phân. Điều đó có nghĩa là mỗi nút chứa nhiều hơn một mục (về mặt kỹ thuật, chúng giữ N mục và con trỏ N + 1, cây nhị phân là trường hợp đặc biệt của cây đa chiều 1 chiều với 1 mục và 2 con trỏ).
Với cây ba chiều, bạn kết thúc với:
Alice Bob Chloe
/ | | \
= = = David Edwina Frank
/ | | \
= = = =
Điều này thường được sử dụng trong việc duy trì các khóa cho một chỉ mục của các mục. Tôi đã viết phần mềm cơ sở dữ liệu được tối ưu hóa cho phần cứng trong đó một nút có kích thước chính xác bằng một khối đĩa (giả sử là 512 byte) và bạn đặt càng nhiều khóa càng tốt vào một nút. Các con trỏ trong trường hợp này thực sự là các số ghi vào một tệp truy cập trực tiếp có độ dài cố định tách biệt với tệp chỉ mục (vì vậy X
có thể tìm thấy số bản ghi chỉ bằng cách tìm kiếm X * record_length
).
Ví dụ: nếu con trỏ là 4 byte và kích thước khóa là 10, số lượng khóa trong nút 512 byte là 36. Đó là 36 phím (360 byte) và 37 con trỏ (148 byte) cho tổng số 508 byte với 4 byte lãng phí cho mỗi nút.
Việc sử dụng các khóa đa chiều giới thiệu sự phức tạp của tìm kiếm hai pha (tìm kiếm đa chiều để tìm nút chính xác kết hợp với tìm kiếm tuần tự nhỏ (hoặc nhị phân tuyến tính) để tìm khóa chính xác trong nút) nhưng lợi thế trong làm ít I / O đĩa hơn bù cho việc này.
Tôi thấy không có lý do gì để làm điều này cho cấu trúc trong bộ nhớ, tốt hơn hết bạn nên gắn bó với cây nhị phân cân bằng và giữ cho mã của bạn đơn giản.
Ngoài ra, hãy nhớ rằng những lợi thế của O(log N)
hơn O(N)
không thực sự xuất hiện khi bộ dữ liệu của bạn nhỏ. Nếu bạn đang sử dụng cây đa chiều để lưu trữ mười lăm người trong sổ địa chỉ của bạn, điều đó có thể là quá mức cần thiết. Những lợi thế có được khi bạn lưu trữ thứ gì đó như mọi đơn hàng từ hàng trăm nghìn khách hàng của bạn trong mười năm qua.
Toàn bộ quan điểm của ký hiệu big-O là chỉ ra những gì xảy ra khi N
tiếp cận vô hạn. Một số người có thể không đồng ý nhưng thậm chí vẫn ổn khi sử dụng sắp xếp bong bóng nếu bạn chắc chắn các bộ dữ liệu sẽ ở dưới một kích thước nhất định, miễn là không có sẵn thứ gì khác :-)
Đối với việc sử dụng khác cho cây nhị phân, có rất nhiều, chẳng hạn như:
Dựa vào mức độ giải thích mà tôi đã tạo cho các cây tìm kiếm, tôi rất thận trọng khi đi sâu vào nhiều chi tiết khác, nhưng điều đó đủ để nghiên cứu chúng, nếu bạn mong muốn.
Tổ chức của mã Morse là một cây nhị phân.
Cây nhị phân là một cấu trúc dữ liệu cây trong đó mỗi nút có nhiều nhất hai nút con, thường được phân biệt là "trái" và "phải". Các nút có con là các nút cha và các nút con có thể chứa các tham chiếu đến cha mẹ của chúng. Bên ngoài cây, thường có một tham chiếu đến nút "gốc" (tổ tiên của tất cả các nút), nếu nó tồn tại. Bất kỳ nút nào trong cấu trúc dữ liệu đều có thể đạt được bằng cách bắt đầu tại nút gốc và liên tục theo dõi các tham chiếu đến con trái hoặc phải. Trong cây nhị phân, một mức độ của mỗi nút là tối đa hai.
Cây nhị phân rất hữu ích, vì như bạn có thể thấy trong hình, nếu bạn muốn tìm bất kỳ nút nào trong cây, bạn chỉ phải nhìn tối đa 6 lần. Ví dụ, nếu bạn muốn tìm kiếm nút 24, bạn sẽ bắt đầu từ thư mục gốc.
Tìm kiếm này được minh họa dưới đây:
Bạn có thể thấy rằng bạn có thể loại trừ một nửa các nút của toàn bộ cây trên đường chuyền đầu tiên. và một nửa số cây con bên trái vào thứ hai. Điều này làm cho các tìm kiếm rất hiệu quả. Nếu điều này được thực hiện trên 4 tỷ phần tử, bạn sẽ chỉ phải tìm kiếm tối đa 32 lần. Do đó, càng có nhiều yếu tố chứa trong cây, việc tìm kiếm của bạn càng hiệu quả.
Xóa có thể trở nên phức tạp. Nếu nút có 0 hoặc 1 con, thì đó đơn giản chỉ là vấn đề di chuyển một số con trỏ để loại trừ nút bị xóa. Tuy nhiên, bạn không thể dễ dàng xóa một nút có 2 con. Vì vậy, chúng tôi có một cắt ngắn. Giả sử chúng ta muốn xóa nút 19.
Vì cố gắng xác định nơi để di chuyển con trỏ trái và phải sang không dễ dàng, chúng tôi tìm một cái để thay thế nó. Chúng tôi đi đến cây con bên trái, và đi xa nhất có thể. Điều này cho chúng ta giá trị lớn nhất tiếp theo của nút mà chúng ta muốn xóa.
Bây giờ chúng tôi sao chép tất cả nội dung của 18, ngoại trừ các con trỏ bên trái và bên phải và xóa nút 18 ban đầu.
Để tạo ra những hình ảnh này, tôi đã triển khai một cây AVL, một cây tự cân bằng, để tại bất kỳ thời điểm nào, cây có nhiều nhất một mức độ khác nhau giữa các nút lá (các nút không có con). Điều này giữ cho cây không bị lệch và duy trì O(log n)
thời gian tìm kiếm tối đa , với chi phí cần thêm một chút thời gian để chèn và xóa.
Dưới đây là một mẫu cho thấy cây AVL của tôi giữ cho nó nhỏ gọn và cân bằng nhất có thể.
Trong một mảng được sắp xếp, việc tra cứu vẫn sẽ mất O(log(n))
, giống như một cái cây, nhưng việc chèn và loại bỏ ngẫu nhiên sẽ lấy O (n) thay vì của cây O(log(n))
. Một số container STL sử dụng các đặc tính hiệu suất này để lợi thế của chúng vì vậy thời gian chèn và loại bỏ tối đa O(log n)
, rất nhanh. Một số các container là map
, multimap
, set
, và multiset
.
Mã ví dụ cho cây AVL có thể được tìm thấy tại http://ideone.com/MheW8
Ứng dụng chính là cây tìm kiếm nhị phân . Đây là một cấu trúc dữ liệu trong đó tìm kiếm, chèn và xóa đều rất nhanh (về các log(n)
thao tác)
Một ví dụ thú vị về cây nhị phân chưa được đề cập là biểu thức toán học được đánh giá đệ quy. Về cơ bản, nó vô dụng theo quan điểm thực tế, nhưng đó là một cách thú vị để nghĩ về những biểu hiện như vậy.
Về cơ bản, mỗi nút của cây có một giá trị vốn có hoặc được đánh giá bằng cách đệ quy bằng cách thao tác trên các giá trị của con của nó.
Ví dụ: biểu thức (1+3)*2
có thể được biểu thị như sau:
*
/ \
+ 2
/ \
1 3
Để đánh giá biểu thức, chúng tôi yêu cầu giá trị của cha mẹ. Nút này lần lượt nhận các giá trị từ các phần tử con của nó, một toán tử cộng và một nút chỉ đơn giản chứa '2'. Lần lượt toán tử cộng nhận các giá trị của nó từ các con với các giá trị '1' và '3' và thêm chúng, trả về 4 cho nút nhân, trả về 8.
Việc sử dụng cây nhị phân này gần giống với ký hiệu đánh bóng theo nghĩa, theo thứ tự các thao tác được thực hiện là giống hệt nhau. Ngoài ra, một điều cần lưu ý là nó không nhất thiết phải là cây nhị phân, chỉ là các toán tử được sử dụng phổ biến nhất là nhị phân. Ở cấp độ cơ bản nhất của nó, cây nhị phân ở đây thực tế chỉ là một ngôn ngữ lập trình đơn thuần rất đơn giản.
Tôi không nghĩ có bất kỳ việc sử dụng nào cho cây nhị phân "thuần túy". (ngoại trừ mục đích giáo dục) Cây nhị phân cân bằng, chẳng hạn như cây Đỏ-Đen hoặc cây AVL hữu ích hơn nhiều, vì chúng đảm bảo các hoạt động O (logn). Cây nhị phân bình thường cuối cùng có thể là một danh sách (hoặc gần như danh sách) và không thực sự hữu ích trong các ứng dụng sử dụng nhiều dữ liệu.
Cây cân bằng thường được sử dụng để thực hiện các bản đồ hoặc bộ. Chúng cũng có thể được sử dụng để sắp xếp trong O (nlogn), thậm chí tho có tồn tại những cách tốt hơn để làm điều đó.
Ngoài ra để tìm kiếm / chèn / xóa bảng Hash có thể được sử dụng, thường có hiệu suất tốt hơn so với cây tìm kiếm nhị phân (cân bằng hoặc không).
Một ứng dụng trong đó cây tìm kiếm nhị phân (cân bằng) sẽ hữu ích nếu cần tìm kiếm / chèn / xóa và sắp xếp. Sắp xếp có thể tại chỗ (gần như, bỏ qua không gian ngăn xếp cần thiết cho đệ quy), với một cây cân bằng xây dựng sẵn sàng. Nó vẫn sẽ là O (nlogn) nhưng với hệ số không đổi nhỏ hơn và không cần thêm không gian (ngoại trừ mảng mới, giả sử dữ liệu phải được đưa vào một mảng). Mặt khác, các bảng băm không thể được sắp xếp (ít nhất là không trực tiếp).
Có thể chúng cũng hữu ích trong một số thuật toán tinh vi để làm một cái gì đó, nhưng tôi không nghĩ gì cả. Nếu tôi tìm thấy nhiều hơn tôi sẽ chỉnh sửa bài viết của tôi.
Các cây khác như cây phong B + được sử dụng rộng rãi trong cơ sở dữ liệu
Một trong những ứng dụng phổ biến nhất là lưu trữ hiệu quả dữ liệu ở dạng được sắp xếp để truy cập và tìm kiếm các phần tử được lưu trữ một cách nhanh chóng. Chẳng hạn, std::map
hoặcstd::set
trong Thư viện chuẩn C ++.
Cây nhị phân như cấu trúc dữ liệu rất hữu ích cho việc triển khai các bộ phân tích biểu thức và bộ giải biểu thức khác nhau.
Nó cũng có thể được sử dụng để giải quyết một số vấn đề về cơ sở dữ liệu, ví dụ như lập chỉ mục.
Nói chung, cây nhị phân là một khái niệm chung về cấu trúc dữ liệu dựa trên cây cụ thể và các loại cây nhị phân cụ thể khác nhau có thể được xây dựng với các thuộc tính khác nhau.
Trong C ++ STL và nhiều thư viện tiêu chuẩn khác bằng các ngôn ngữ khác, như Java và C #. Cây tìm kiếm nhị phân được sử dụng để thực hiện thiết lập và bản đồ.
Một trong những ứng dụng quan trọng nhất của cây nhị phân là các cây tìm kiếm nhị phân cân bằng như:
Những loại cây này có đặc tính là sự khác biệt về độ cao của cây con trái và cây con bên phải được duy trì nhỏ bằng cách thực hiện các thao tác như xoay mỗi lần một nút được chèn hoặc xóa.
Do đó, chiều cao tổng thể của cây vẫn theo thứ tự của log n và các hoạt động như tìm kiếm, chèn và xóa các nút được thực hiện trong thời gian O (log n). STL của C ++ cũng thực hiện các cây này dưới dạng tập hợp và bản đồ.
Trên phần cứng hiện đại, một cây nhị phân gần như luôn luôn tối ưu do bộ đệm và hành vi không gian xấu. Điều này cũng đi cho các biến thể cân bằng (bán). Nếu bạn tìm thấy chúng, đó là nơi hiệu suất không được tính (hoặc bị chi phối bởi chức năng so sánh), hoặc nhiều khả năng vì lý do lịch sử hoặc thiếu hiểu biết.
Trình biên dịch sử dụng cây nhị phân để biểu diễn AST, có thể sử dụng các thuật toán đã biết để phân tích cây như postorder, inorder. Lập trình viên không cần phải đưa ra thuật toán riêng. Vì cây nhị phân cho tệp nguồn cao hơn cây n-ary, nên việc xây dựng nó mất nhiều thời gian hơn. Thực hiện việc sản xuất này: selstmnt: = "if" "(" expr ")" stmnt "ELSE" stmnt Trong một cây nhị phân, nó sẽ có 3 nút, nhưng cây n-ary sẽ có 1 cấp độ
Đó là lý do tại sao các hệ điều hành dựa trên Unix chậm.