Binary Search Tree
BST invariantı: hər node üçün — sol alt-ağacdakı BÜTÜN dəyərlər node-dan kiçik, sağ alt-ağacdakı BÜTÜN dəyərlər node-dan böyükdür.
Diqqət, burada tez-tez səhv edilir: şərt yalnız birbaşa övladlara yox, bütün alt-ağaca aiddir. left.value < node.value yoxlaması kifayət deyil — left alt-ağacında dərində yerləşən bir dəyər də node-dan böyük ola bilməz. BST-ni yoxlayan funksiyanı yazarkən hər node üçün icazə verilən (min, max) diapazonunu aşağı ötürmək lazımdır.
Bu invariant nə verir? Hər müqayisədə axtarış sahəsinin yarısını atırsan — eynilə sıralı massivdə binary search kimi. Fərq odur ki, BST-də əlavə və silmə də O(log n)-dir, sıralı massivdə isə element yerləşdirmək O(n) tələb edir (qalanlarını sürüşdürmək lazımdır).
| Əməliyyat | Balanslaşdırılmış | Deqradasiya olmuş | Sıralı massiv | Hash map |
|---|---|---|---|---|
| Axtarış | O(log n) | O(n) | O(log n) | O(1) orta |
| Əlavə etmə | O(log n) | O(n) | O(n) | O(1) orta |
| Silmə | O(log n) | O(n) | O(n) | O(1) orta |
| Sıralı çıxış | O(n) — inorder | O(n) | O(n) — hazırdır | O(n log n) — sıralamaq lazımdır |
| Diapazon sorğusu | O(log n + k) | O(n) | O(log n + k) | O(n) |
Axtarış və əlavə etmə eyni məntiqlə işləyir: root-dan başla, dəyəri node ilə müqayisə et, kiçikdirsə sola, böyükdürsə sağa get. Axtarışda uyğunluq tapanda dayanırsan; əlavə etmədə null yerə çatanda yeni node yaradırsan. Hər ikisi ağacın hündürlüyü qədər addım atır — yəni O(h).
Silmə üç hala bölünür və müsahibədə məhz üçüncüsü soruşulur:
- Leaf (0 övlad) — sadəcə valideyndəki göstəricini
nullet. - 1 övlad — node-u yeganə övladı ilə əvəzlə. Alt-ağac bütövlükdə yuxarı qalxır, invariant pozulmur.
- 2 övlad — bu, çətin haldır. Node-u silmək əvəzinə onun inorder successor-u (sağ alt-ağacdakı ən kiçik dəyər) ilə dəyərini əvəzləyirsən, sonra həmin successor-u sağ alt-ağacdan silirsən. Successor-un ən çoxu bir övladı ola bilər (sol övladı yoxdur — o, ən kiçikdir), ona görə ikinci silmə mütləq sadə hallardan biridir və rekursiya dərinləşmir.
Successor əvəzinə inorder predecessor (sol alt-ağacdakı ən böyük dəyər) da işləyir — hər iki seçim invariantı qoruyur.
Niyə pis hal O(n)? BST-nin bütün üstünlüyü h ≈ log n fərziyyəsinə söykənir. Amma adi BST özünü balanslaşdırmır — forma tamamilə əlavə sırasından asılıdır.
Sıralı data əlavə etsən (1, 2, 3, 4, 5...), hər yeni element həmişə sağa gedir və ağac əslində əlaqəli siyahıya çevrilir: h = n, bütün əməliyyatlar O(n). Bu, nəzəri qorxu deyil — real datalar tez-tez sıralı gəlir: ID-lər, timestamp-lar, avtoinkrement açarlar, sıralanmış CSV. Yəni ən təbii input BST-nin ən pis halıdır.
Həll variantları:
- Özü-balanslaşan ağac — AVL və ya Red-Black. Onlar əlavə/silmə zamanı rotasiya edərək
h = O(log n)zəmanətini saxlayır. Növbəti mövzunun mövzusu budur. - Datanı qarışdırmaq — əlavə sırasını təsadüfiləşdirsən, gözlənilən hündürlük O(log n) olur. Bu, sadə həlldir, amma zəmanət deyil.
- Hazır struktur işlətmək — real işdə əl ilə BST yazmırsan: Kotlin-də
TreeMap, Dart-daSplayTreeMap, C++-dastd::map.
Hash map ilə müqayisə də vacibdir: hash O(1) verir, amma sıra saxlamır. BST-nin varlıq səbəbi sıradır — sıralı iterasiya, diapazon sorğuları, "k-dan kiçik ən böyük açar" (floor/ceiling) kimi əməliyyatlar hash-də mümkün deyil.
İnterview ipucu. İki sual demək olar ki, həmişə gəlir.
Birincisi: "BST-də iki övladlı node-u necə silirsən?" Bu, mövzunun əsl sınağıdır. Güclü cavab: "Node-u fiziki silmirəm — dəyərini inorder successor-la, yəni sağ alt-ağacdakı ən kiçik dəyərlə əvəzləyirəm, sonra həmin successor-u sağ alt-ağacdan silirəm. Successor-un sol övladı ola bilməz, ona görə ikinci silmə mütləq 0 və ya 1 övladlı sadə haldır." Nəyə görə məhz successor — çünki o, node-dan böyük olan ən kiçik dəyərdir, yəni onun yerinə keçməsi invariantı pozmur.
İkincisi: "BST-nin pis halı nədir?" Cavab: sıralı input. Bunu deyəndə mütləq real nümunə əlavə et — avtoinkrement ID və ya timestamp. Sonra həlli göstər: özü-balanslaşan ağac və ya hazır TreeMap.
Ən çox rast gəlinən səhv: BST invariantını yalnız birbaşa övladlara aid etmək. isValidBST yazarkən (min, max) diapazonunu aşağı ötürməsən, kod sadə testlərdə keçir, sonra dərində yerləşən bir node-da uğursuz olur.
📚 Mənbələr və sənədlər
- java.util.TreeMaprəsmidocs.oracle.com