Cây nhị phân tự cân bằng nào bạn muốn giới thiệu?


18

Tôi đang học Haskell và như một bài tập Tôi đang tạo cây nhị phân. Đã tạo một cây nhị phân thông thường, tôi muốn điều chỉnh nó để tự cân bằng. Vì thế:

  • Cái nào hiệu quả nhất?
  • Cái nào dễ thực hiện nhất?
  • Mà thường được sử dụng?

Nhưng quan trọng là, bạn đề nghị gì?

Tôi cho rằng điều này thuộc về nơi này vì nó mở để tranh luận.


Về hiệu quả và dễ thực hiện, hiệu quả chung được xác định rõ nhưng đối với việc triển khai của bạn, tôi cho rằng điều tốt nhất sẽ là thực hiện càng nhiều càng tốt và sau đó cho chúng tôi biết cách nào hiệu quả nhất ...
glenatron

Câu trả lời:


15

Tôi khuyên bạn nên bắt đầu với một cây Đỏ-Đen hoặc cây AVL .

Cây đỏ-đen nhanh hơn để chèn, nhưng cây AVL có một cạnh nhỏ để tra cứu. Cây AVL có thể dễ thực hiện hơn một chút, nhưng không nhiều dựa trên kinh nghiệm của riêng tôi.

Cây AVL đảm bảo rằng cây được cân bằng sau mỗi lần chèn hoặc xóa (không có cây con nào có hệ số cân bằng lớn hơn 1 / -1, trong khi cây Đỏ đen đảm bảo rằng cây được cân bằng hợp lý bất cứ lúc nào.


1
Cá nhân, tôi thấy chèn màu đỏ-đen dễ dàng hơn chèn AVL. Lý do là thông qua sự tương tự (không hoàn hảo) với cây B. Chèn là khó khăn, nhưng xóa là xấu xa (rất nhiều trường hợp để xem xét). Trong thực tế, tôi không còn thực hiện xóa C ++ đỏ đen của riêng mình nữa - tôi đã xóa nó khi tôi nhận ra (1) Tôi không bao giờ sử dụng nó - mỗi lần tôi muốn xóa tôi đều xóa nhiều mục, vì vậy tôi đã chuyển đổi từ cây sang liệt kê, xóa khỏi danh sách, sau đó chuyển đổi trở lại thành một cây và (2) dù sao nó cũng bị hỏng.
Steve314

2
@ Steve314, cây đỏ đen thì dễ hơn, nhưng bạn chưa thể thực hiện được? Cây AVL như thế nào?
dan_waterworth

@dan_waterworth - Tôi chưa thực hiện ngay cả một phương pháp chèn vẫn hoạt động - có ghi chú, hiểu nguyên tắc cơ bản, nhưng không bao giờ có được sự kết hợp đúng đắn của động lực, thời gian và sự tự tin. Nếu tôi chỉ muốn các phiên bản hoạt động, đó chỉ là bản sao-mã giả từ sách giáo khoa và dịch (và đừng quên C ++ có các thùng chứa thư viện tiêu chuẩn), nhưng đâu là niềm vui?
Steve314

BTW - Tôi tin rằng (nhưng không thể cung cấp tài liệu tham khảo) rằng một cuốn sách giáo khoa khá phổ biến bao gồm triển khai lỗi của một trong các thuật toán cây nhị phân cân bằng - không chắc chắn, nhưng nó có thể bị xóa đen đỏ. Vì vậy, không chỉ riêng tôi ;-)
Steve314

1
@ Steve314, tôi biết, cây cối có thể rất phức tạp trong ngôn ngữ mệnh lệnh, nhưng đáng ngạc nhiên, việc thực hiện chúng trong Haskell đã rất dễ dàng. Tôi đã viết một cây AVL thông thường và cũng là biến thể không gian 1D vào cuối tuần và cả hai chỉ có khoảng 60 dòng.
dan_waterworth

10

Tôi sẽ xem xét một giải pháp thay thế nếu bạn ổn với cấu trúc dữ liệu ngẫu nhiên : Bỏ qua danh sách .

Từ quan điểm cấp cao, đó là một cấu trúc cây, ngoại trừ việc nó không được triển khai dưới dạng cây mà là một danh sách có nhiều lớp liên kết.

Bạn sẽ nhận được các phần chèn / tìm kiếm / xóa O (log N) và bạn sẽ không phải đối phó với tất cả các trường hợp tái cân bằng khó khăn đó.

Mặc dù vậy, tôi chưa bao giờ xem xét việc triển khai chúng bằng Ngôn ngữ chức năng và trang wikipedia không hiển thị bất kỳ, vì vậy có thể không dễ dàng (wrt thành bất biến)


Tôi thực sự thích các danh sách bỏ qua và tôi đã thực hiện chúng trước đây, mặc dù không phải bằng ngôn ngữ chức năng. Tôi nghĩ rằng tôi sẽ thử chúng sau này, nhưng hiện tại tôi đang ở trên những cây tự cân bằng.
dan_waterworth

Ngoài ra, mọi người thường sử dụng skiplists cho các cấu trúc dữ liệu đồng thời. Có thể tốt hơn, thay vì ép buộc tính bất biến, sử dụng các nguyên hàm đồng thời của haskell (như MVar hoặc TVar). Mặc dù, điều này sẽ không dạy tôi rất nhiều về việc viết mã chức năng.
dan_waterworth

2
@ Fanatic23, Danh sách bỏ qua không phải là một ADT. ADT là một tập hợp hoặc một mảng kết hợp.
dan_waterworth

@dan_waterworth xấu của tôi, bạn đúng.
Fanatic23

5

Nếu bạn muốn có một cấu trúc tương đối dễ dàng để bắt đầu (cả cây AVL và cây đỏ đen đều khó xử), một lựa chọn là một treap - được đặt tên là sự kết hợp giữa "cây" và "đống".

Mỗi nút được một giá trị "ưu tiên", thường được gán ngẫu nhiên khi nút được tạo. Các nút được định vị trong cây sao cho thứ tự khóa được tuân thủ và do đó thứ tự các giá trị ưu tiên giống như đống được tôn trọng. Thứ tự giống như đống có nghĩa là cả hai đứa trẻ của cha mẹ có mức độ ưu tiên thấp hơn cha mẹ.

EDIT đã xóa "trong các giá trị khóa" ở trên - thứ tự ưu tiên và khóa được áp dụng cùng nhau, vì vậy mức độ ưu tiên là đáng kể ngay cả đối với các khóa duy nhất.

Đó là một sự kết hợp thú vị. Nếu các khóa là duy nhất và các ưu tiên là duy nhất, thì có một cấu trúc cây duy nhất cho bất kỳ tập hợp nút nào. Mặc dù vậy, chèn và xóa là hiệu quả. Nói một cách chính xác, cây có thể bị mất cân bằng đến mức nó thực sự là một danh sách được liên kết, nhưng điều này rất khó xảy ra (như với cây nhị phân tiêu chuẩn), bao gồm cả các trường hợp thông thường như các khóa được chèn theo thứ tự (không giống như cây nhị phân tiêu chuẩn).


1
+1. Treaps là lựa chọn cá nhân của tôi, tôi thậm chí đã viết một bài đăng trên blog về cách chúng được thực hiện.
P Shved

5

Cái nào hiệu quả nhất?

Mơ hồ và khó trả lời. Các phức tạp tính toán đều được xác định rõ. Nếu đó là những gì bạn muốn nói về hiệu quả, thì không có tranh luận thực sự. Thật vậy, tất cả các thuật toán tốt đi kèm với bằng chứng và các yếu tố phức tạp.

Nếu bạn có nghĩa là "thời gian chạy" hoặc "sử dụng bộ nhớ" thì bạn sẽ cần so sánh việc triển khai thực tế. Sau đó, ngôn ngữ, thời gian chạy, hệ điều hành và các yếu tố khác đi vào hoạt động, làm cho câu hỏi khó trả lời.

Cái nào dễ thực hiện nhất?

Mơ hồ và khó trả lời. Một số thuật toán có thể phức tạp với bạn, nhưng tầm thường với tôi.

Mà thường được sử dụng?

Mơ hồ và khó trả lời. Đầu tiên là "bởi ai?" một phần của điều này? Haskell chỉ? Còn C hay C ++ thì sao? Thứ hai, có vấn đề phần mềm độc quyền nơi chúng tôi không có quyền truy cập vào nguồn để thực hiện khảo sát.

Nhưng quan trọng là, bạn đề nghị gì?

Tôi cho rằng điều này thuộc về nơi này vì nó mở để tranh luận.

Chính xác. Vì các tiêu chí khác của bạn không hữu ích lắm, đây là tất cả những gì bạn sẽ nhận được.

Bạn có thể lấy nguồn cho một số lượng lớn các thuật toán cây. Nếu bạn muốn học một cái gì đó, bạn có thể chỉ cần thực hiện mọi thứ bạn có thể tìm thấy. Thay vì yêu cầu một "khuyến nghị", chỉ cần thu thập mọi thuật toán bạn có thể tìm thấy.

Đây là danh sách:

http://en.wikipedia.org/wiki/Self-balANCE_binary_search_tree

Có sáu cái phổ biến được định nghĩa. Bắt đầu với những cái đó.


3

Nếu bạn quan tâm đến cây Splay, có một phiên bản đơn giản hơn của những cái mà tôi tin đã được mô tả lần đầu tiên trong một bài báo của Allen và Munroe. Nó không có cùng một đảm bảo hiệu suất, nhưng tránh các biến chứng trong việc xử lý tái cân bằng "zig-zig" so với "zig-zag".

Về cơ bản, khi tìm kiếm (bao gồm tìm kiếm điểm chèn hoặc nút cần xóa), nút bạn tìm thấy sẽ được xoay trực tiếp về phía gốc, từ dưới lên (ví dụ như thoát khỏi chức năng tìm kiếm đệ quy). Ở mỗi bước, bạn chọn một vòng xoay trái hoặc phải tùy thuộc vào việc đứa trẻ bạn muốn kéo một bước khác về phía gốc là con phải hay con trái (nếu tôi nhớ chính xác hướng xoay của mình, thì đó là tương ứng).

Giống như cây Splay, ý tưởng là các mục được truy cập gần đây luôn ở gần gốc của cây, vì vậy hãy nhanh chóng truy cập lại. Đơn giản hơn, những cây xoay gốc này của Allen-Munroe (cái mà tôi gọi là chúng - không biết tên chính thức) có thể nhanh hơn, nhưng chúng không có bảo đảm hiệu suất được khấu hao tương tự.

Một điều - vì cấu trúc dữ liệu theo định nghĩa này đột biến ngay cả đối với các hoạt động tìm kiếm, có lẽ nó sẽ cần phải được thực hiện một cách đơn lẻ. IOW nó có thể không phù hợp cho lập trình chức năng.


Splays có một chút khó chịu khi họ sửa đổi cây ngay cả khi tìm thấy. Điều này sẽ khá đau đớn trong môi trường đa luồng, đây là một trong những động lực lớn để sử dụng một ngôn ngữ chức năng như Haskell ngay từ đầu. Sau đó, một lần nữa, tôi chưa bao giờ sử dụng ngôn ngữ chức năng trước đây, vì vậy có lẽ đây không phải là một yếu tố.
Nhanh chóng Joe Smith

@Quick - phụ thuộc vào cách bạn định sử dụng cây. Nếu bạn đang sử dụng nó trong mã kiểu chức năng thực sự, bạn sẽ bỏ đột biến trên mỗi lần tìm kiếm (làm cho cây Splay hơi ngớ ngẩn) hoặc cuối cùng bạn sẽ sao chép một phần đáng kể của cây nhị phân trên mỗi lần tra cứu, và theo dõi trạng thái cây nào bạn đang làm việc khi công việc của bạn tiến triển (lý do có thể sử dụng kiểu đơn điệu). Việc sao chép đó có thể được tối ưu hóa bởi trình biên dịch nếu bạn không còn tham chiếu trạng thái cây cũ sau khi trạng thái cây mới được tạo (các giả định tương tự là phổ biến trong lập trình chức năng), nhưng có thể không.
Steve314

Không có cách tiếp cận âm thanh đáng giá nỗ lực. Sau đó, một lần nữa, không làm ngôn ngữ chức năng thuần túy cho hầu hết các phần.
Nhanh chóng Joe Smith

1
@Quick - Sao chép cây là những gì bạn sẽ làm cho bất kỳ cấu trúc dữ liệu cây nào bằng ngôn ngữ chức năng thuần túy để thay đổi thuật toán, chẳng hạn như chèn. Về mặt nguồn, mã sẽ không khác với mã bắt buộc phải cập nhật tại chỗ. Sự khác biệt đã được xử lý, có lẽ, đối với cây nhị phân không cân bằng. Vì vậy, miễn là bạn không cố gắng thêm các liên kết cha vào các nút, các bản sao sẽ chia sẻ các cây con chung ở mức tối thiểu và tối ưu hóa sâu trong Haskell là khá khó nếu không hoàn hảo. Về nguyên tắc, tôi chống lại Haskell, nhưng đây không hẳn là vấn đề.
Steve314

2

Một cây cân bằng rất đơn giản là một cây AA . Nó bất biến đơn giản hơn và do đó dễ thực hiện hơn. Bởi vì sự đơn giản của nó, hiệu suất của nó vẫn còn tốt.

Là một bài tập nâng cao, bạn có thể thử sử dụng GADT để thực hiện một trong các biến thể của cây cân bằng có bất biến được thực thi theo loại hệ thống loại.

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.