Pohon biner penyeimbang mandiri mana yang akan Anda rekomendasikan?

18

Saya belajar Haskell dan sebagai latihan saya membuat pohon biner. Setelah membuat pohon biner biasa, saya ingin mengadaptasinya menjadi penyeimbang diri. Begitu:

  • Mana yang paling efisien?
  • Mana yang paling mudah diterapkan?
  • Mana yang paling sering digunakan?

Tetapi yang terpenting, yang Anda rekomendasikan?

Saya menganggap ini milik di sini karena itu terbuka untuk diperdebatkan.

dan_waterworth
sumber
Dalam hal efisiensi dan kemudahan implementasi, efisiensi umum didefinisikan dengan baik tetapi untuk implementasi Anda, saya rasa yang terbaik adalah mengimplementasikan sebanyak yang Anda bisa temukan dan kemudian beri tahu kami mana yang terbaik ...
glenatron

Jawaban:

15

Saya akan merekomendasikan Anda mulai dengan pohon Merah-Hitam , atau pohon AVL .

Pohon merah-hitam lebih cepat untuk dimasukkan, tetapi pohon AVL memiliki sedikit keunggulan untuk pencarian. Pohon AVL mungkin sedikit lebih mudah diimplementasikan, tetapi tidak berdasarkan pengalaman saya sendiri.

Pohon AVL memastikan bahwa pohon seimbang setelah setiap memasukkan atau menghapus (tidak ada sub-pohon memiliki faktor keseimbangan lebih besar dari 1 / -1, sedangkan pohon Merah-hitam memastikan bahwa pohon itu seimbang setiap saat.

Joe Smith Cepat
sumber
1
Secara pribadi, saya menemukan insert merah-hitam lebih mudah daripada insert AVL. Alasannya adalah melalui analogi (tidak sempurna) untuk pohon-B. Sisipan itu fiddly, tetapi penghapusan itu jahat (begitu banyak kasus untuk dipertimbangkan). Sebenarnya saya tidak lagi memiliki implementasi C ++ red-black delete saya sendiri - saya menghapusnya ketika saya menyadari (1) Saya tidak pernah menggunakannya - setiap kali saya ingin menghapus saya menghapus beberapa item, jadi saya mengkonversi dari pohon ke daftar, hapus dari daftar, lalu konversikan kembali ke pohon, dan (2) tetap saja rusak.
Steve314
2
@ Steve314, pohon merah-hitam lebih mudah, tetapi Anda belum dapat membuat implementasi yang berhasil? Seperti apakah pohon AVL?
dan_waterworth
@dan_waterworth - Saya belum membuat implementasi dengan metode insert yang berfungsi - memiliki catatan, memahami prinsip dasar, tetapi tidak pernah mendapatkan kombinasi motivasi, waktu, dan kepercayaan diri yang tepat. Jika saya hanya ingin versi yang berfungsi, itu hanya copy-pseudocode-from-textbook-and-translate (dan jangan lupa C ++ memiliki kontainer perpustakaan standar), tetapi di mana asyiknya?
Steve314
BTW - Saya percaya (tapi tidak bisa memberikan referensi) bahwa buku teks yang cukup populer mencakup implementasi buggy dari salah satu algoritma pohon biner seimbang - tidak yakin, tetapi mungkin menghapus merah-hitam. Jadi bukan hanya saya ;-)
Steve314
1
@ Steve314, saya tahu, pohon bisa sangat rumit dalam bahasa imperatif, tetapi yang mengejutkan, mengimplementasikannya di Haskell sangat mudah. Saya telah menulis pohon AVL biasa dan juga varian spasial 1D selama akhir pekan dan keduanya hanya sekitar 60 baris.
dan_waterworth
10

Saya akan mempertimbangkan alternatif jika Anda baik-baik saja dengan struktur data acak : Abaikan Daftar .

Dari sudut pandang tingkat tinggi, ini adalah struktur pohon, kecuali bahwa itu tidak diimplementasikan sebagai pohon tetapi sebagai daftar dengan banyak lapisan tautan.

Anda akan mendapatkan O (log N) penyisipan / pencarian / penghapusan, dan Anda tidak harus berurusan dengan semua kasus penyeimbangan ulang yang rumit.

Saya tidak pernah mempertimbangkan untuk mengimplementasikannya dalam Bahasa Fungsional, dan halaman wikipedia tidak menunjukkan apa pun, jadi mungkin tidak mudah (wrt to immutability)

Matthieu M.
sumber
Saya sangat menikmati daftar lompatan dan saya sudah menerapkannya sebelumnya, meskipun tidak dalam bahasa fungsional. Saya pikir saya akan mencoba mereka setelah ini, tetapi sekarang saya di pohon self-balancing.
dan_waterworth
Juga, orang-orang sering menggunakan skiplist untuk struktur data bersamaan. Mungkin lebih baik, daripada memaksakan imutabilitas, untuk menggunakan primitif konkurensi haskell (seperti MVar atau TVar). Meskipun, ini tidak akan banyak mengajarkan saya tentang menulis kode fungsional.
dan_waterworth
2
@ Fanatic23, Daftar Lewati bukan ADT. ADT adalah himpunan atau array asosiatif.
dan_waterworth
@dan_waterworth salahku, kau benar.
Fanatic23
5

Jika Anda ingin struktur yang relatif mudah untuk memulai (baik pohon AVL dan pohon merah-hitam adalah fiddly), satu opsi adalah treap - dinamai sebagai kombinasi "tree" dan "heap".

Setiap node mendapat nilai "prioritas", sering secara acak ditetapkan ketika node dibuat. Node diposisikan di pohon sehingga urutan kunci dihormati, dan sehingga urutan nilai prioritas seperti heap dihormati. Pemesanan mirip tumpukan berarti bahwa kedua anak dari orang tua memiliki prioritas lebih rendah daripada orang tua.

EDIT dihapus "di dalam nilai-nilai kunci" di atas - prioritas dan urutan kunci berlaku bersama, sehingga prioritas penting bahkan untuk kunci unik.

Kombinasi yang menarik. Jika kunci unik dan prioritas unik, ada struktur pohon unik untuk setiap set node. Meski begitu, menyisipkan dan menghapus itu efisien. Sebenarnya, pohon dapat tidak seimbang ke titik di mana ia secara efektif merupakan daftar yang ditautkan, tetapi ini sangat tidak mungkin (seperti dengan pohon biner standar), termasuk untuk kasus normal seperti kunci yang dimasukkan secara berurutan (tidak seperti pohon biner standar).

Steve314
sumber
1
+1. Treaps adalah pilihan pribadi saya, saya bahkan menulis posting blog tentang bagaimana mereka diterapkan.
P Shved
5

Mana yang paling efisien?

Tidak jelas dan sulit dijawab. Kompleksitas komputasi semuanya terdefinisi dengan baik. Jika itu yang Anda maksud dengan efisiensi, tidak ada perdebatan nyata. Memang, semua algoritma yang baik datang dengan bukti dan faktor kompleksitas.

Jika Anda bermaksud "waktu berjalan" atau "penggunaan memori" maka Anda harus membandingkan implementasi yang sebenarnya. Kemudian bahasa, run-time, OS dan faktor-faktor lain ikut bermain, membuat pertanyaan sulit dijawab.

Mana yang paling mudah diterapkan?

Tidak jelas dan sulit dijawab. Beberapa algoritma mungkin terlihat rumit bagi Anda, tetapi sepele bagi saya.

Mana yang paling sering digunakan?

Tidak jelas dan sulit dijawab. Pertama ada "oleh siapa?" bagian dari ini? Hanya Haskell? Bagaimana dengan C atau C ++? Kedua, ada masalah perangkat lunak berpemilik di mana kami tidak memiliki akses ke sumber untuk melakukan survei.

Tetapi yang terpenting, yang Anda rekomendasikan?

Saya menganggap ini milik di sini karena itu terbuka untuk diperdebatkan.

Benar. Karena kriteria Anda yang lain tidak terlalu membantu, ini yang akan Anda dapatkan.

Anda bisa mendapatkan sumber untuk sejumlah besar algoritma pohon. Jika Anda ingin mempelajari sesuatu, Anda bisa menerapkan setiap yang Anda bisa temukan. Daripada meminta "rekomendasi", kumpulkan saja setiap algoritma yang dapat Anda temukan.

Berikut daftarnya:

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

Ada enam yang populer didefinisikan. Mulailah dengan itu.

S.Lott
sumber
3

Jika Anda tertarik pada pohon Splay, ada versi yang lebih sederhana dari yang saya yakini pertama kali dijelaskan dalam makalah oleh Allen dan Munroe. Itu tidak memiliki jaminan kinerja yang sama, tetapi menghindari komplikasi dalam berurusan dengan penyeimbangan kembali "zig-zig" vs "zig-zag".

Pada dasarnya, ketika mencari (termasuk mencari titik atau simpul untuk dihapus), simpul yang Anda temukan akan diputar langsung ke arah root, dari bawah ke atas (mis. Saat fungsi pencarian rekursif keluar). Pada setiap langkah, Anda memilih satu putaran kiri atau kanan tergantung pada apakah anak yang ingin Anda tarik ke atas root adalah anak kanan atau anak kiri (jika saya ingat arah rotasi saya dengan benar, itu masing-masing).

Seperti pohon Splay, idenya adalah item yang baru diakses selalu dekat dengan akar pohon, jadi cepat diakses lagi. Menjadi lebih sederhana, pohon-pohon Allen-Munroe rotate-to-root (apa yang saya sebut - tidak tahu nama resmi) bisa lebih cepat, tetapi mereka tidak memiliki jaminan kinerja diamortisasi yang sama.

Satu hal - karena struktur data ini menurut definisi bermutasi bahkan untuk operasi pencarian, mungkin perlu diimplementasikan secara monadik. TKI itu mungkin tidak cocok untuk pemrograman fungsional.

Steve314
sumber
Splays agak menjengkelkan mengingat mereka memodifikasi pohon bahkan ketika menemukannya. Ini akan sangat menyakitkan di lingkungan multi-utas, yang merupakan salah satu motivasi besar untuk menggunakan bahasa fungsional seperti Haskell. Kemudian lagi, saya belum pernah menggunakan bahasa fungsional sebelumnya, jadi mungkin ini bukan faktor.
Cepat Joe Smith
@Cepat - tergantung pada bagaimana Anda bermaksud menggunakan pohon. Jika Anda menggunakannya dalam kode gaya fungsional yang sebenarnya, Anda akan menjatuhkan mutasi pada setiap temuan (membuat pohon Splay agak konyol), atau Anda akan menduplikasi bagian substansial dari pohon biner pada setiap pencarian, dan catat status pohon tempat Anda bekerja saat pekerjaan Anda berlangsung (alasan kemungkinan menggunakan gaya monadik). Penyalinan itu mungkin dioptimalkan oleh kompiler jika Anda tidak lagi mereferensikan status pohon lama setelah yang baru dibuat (asumsi yang sama sering digunakan dalam pemrograman fungsional), tetapi mungkin tidak.
Steve314
Tidak ada pendekatan yang terdengar sepadan dengan usaha. Kemudian lagi, tidak ada bahasa murni fungsional untuk sebagian besar.
Cepat Joe Smith
1
@Cepat - Menggandakan pohon adalah apa yang akan Anda lakukan untuk struktur data pohon apa pun dalam bahasa fungsional murni untuk bermutasi algoritma seperti sisipan. Dalam istilah sumber, kode tidak akan jauh berbeda dari kode penting yang melakukan pembaruan di tempat. Perbedaan sudah ditangani, mungkin, untuk pohon biner yang tidak seimbang. Selama Anda tidak mencoba menambahkan tautan induk ke simpul, duplikat tersebut akan membagikan subtree umum seminimal mungkin, dan pengoptimalan mendalam di Haskell sangat sulit jika tidak sempurna. Saya sendiri anti-Haskell pada prinsipnya, tetapi ini tidak selalu menjadi masalah.
Steve314
2

Pohon seimbang yang sangat sederhana adalah pohon AA . Itu invarian lebih sederhana dan dengan demikian lebih mudah diterapkan. Karena kesederhanaannya, kinerjanya masih bagus.

Sebagai latihan lanjutan, Anda dapat mencoba menggunakan GADT untuk menerapkan salah satu varian pohon seimbang yang invariannya ditegakkan oleh tipe sistem tipe.

Petr Pudlák
sumber