Kahendotsing: erinevus redaktsioonide vahel

Eemaldatud sisu Lisatud sisu
Litvand (arutelu | kaastöö)
Uus lehekülg: 'Sissejuhatus Kahendotsing ehk binaarotsing on otsingualgoritm, mis võtab sisendiks sorteeritud [järjendi] ja otsitava väärtuse ning väljastab väärtuse asukohta järjendis...'
 
Litvand (arutelu | kaastöö)
Resümee puudub
1. rida:
Kahendotsing ehk binaarotsing on [otsingualgoritm], mis võtab sisendiks [sorteeritud | sortimisalgoritm] [järjendi | järjend] ja otsitava väärtuse ning väljastab väärtuse asukohta järjendis või teatab, et seda väärtust järjendis ei leidu. Algoritmi alguses võetakse vahemikuks, millest väärtust otsitakse, terve järjend ning igal sammul otsitakse väärtust edasi alamvahemikust, milles on algse vahemiku keskmisest elemendist kas kõik väiksemad või kõik suuremad elemendid. Alamvahemikku valitakse selle järgi, kas otsitav väärtus on väiksem või suurem kui algse vahemiku keskmine element. Kui vahemiku keskmine element on võrdne otsitava väärtusega, siis see väärtus on leitud. Kui aga vahemik on tühi, siis otsitavat väärtust ei leidu järjendis. [4Knuth]
Sissejuhatus
 
Võrdluste arv, mida kahendotsing sooritab ühe väärtuse leidmiseks, on halvimal juhul <math>O(\log n)</math>, kus <math>n</math> on järjendi elementide arv ja <math>O</math> tähistab [asümptootilist hinnangut [3| algoritmiline keerukus]. Seejuures kasutab kahendotsing konstantse mälu hulga. See on üldjuhul optimaalne väärtuse otsimiseks sorteeritud järjendist võrdluste abil, mis tuleneb võrdlemistega sorteerimise ajalise keerukuse alumisest piirist <math>O(n \log n)</math>. [5]
Kahendotsing ehk binaarotsing on otsingualgoritm, mis võtab sisendiks sorteeritud [järjendi] ja otsitava väärtuse ning väljastab väärtuse asukohta järjendis või teatab, et seda väärtust järjendis ei leidu. Algoritmi alguses võetakse vahemikuks, millest väärtust otsitakse, terve järjend ning igal sammul otsitakse väärtust edasi alamvahemikust, milles on algse vahemiku keskmisest elemendist kas kõik väiksemad või kõik suuremad elemendid. Alamvahemikku valitakse selle järgi, kas otsitav väärtus on väiksem või suurem kui algse vahemiku keskmine element. Kui vahemiku keskmine element on võrdne otsitava väärtusega, siis see väärtus on leitud. Kui aga vahemik on tühi, siis otsitavat väärtust ei leidu järjendis. [4]
[Knuth][cmpsort]
 
On palju kahendotsinguga sarnaseid algoritme, näiteks [eksponentsiaalne otsing], mille sisend võib olla piiramatu (dünaamiliselt genereeritud) suurusega. [Kahendotsingupuud | kahendotsingupuu] ja [B-puud | B-puu] on samuti tihedalt seotud kahendotsinguga. [1?]
Võrdluste arv, mida kahendotsing sooritab ühe väärtuse leidmiseks, on halvimal juhul O(log n), kus n on järjendi elementide arv ja O tähistab asümptootilist hinnangut [3]. Seejuures kasutab kahendotsing konstantse mälu hulga. See on üldjuhul optimaalne väärtuse otsimiseks sorteeritud järjendist võrdluste abil, mis tuleneb võrdlemistega sorteerimise ajalise keerukuse alumisest piirist O(n log n). [5]
On palju kahendotsinguga sarnaseid algoritme, näiteks [eksponentsiaalne otsing], mille sisend võib olla piiramatu (dünaamiliselt genereeritud) suurusega. [Kahendotsingupuud] ja [B-puud] on samuti tihedalt seotud kahendotsinguga. [1]
 
=== Kirjeldus ===
Olgu järjendi <code>J</code> mõne vahemiku esimese, keskmise ja viimase elemendi [nullipõhised indeksid | nullipõhine indekseerimine] vastavalt <code>e</code>, <code>k</code> ja <code>v</code> ning otsitav väärtus O<code>o</code>. AlgoritmistKahendotsingust võib olla lihtsam aru saada [rekursiivsel | rekursioon] kujul, kus alamvahemikust saab korduvalt algne vahemik:
 
funktsioon kahend_otsing(o, J, e, v):
Olgu järjendi J vahemiku esimese, keskmise ja viimase elemendi nullipõhised indeksid vastavalt e, k ja v ning otsitav väärtus O. Algoritmist võib olla lihtsam aru saada rekursiivsel kujul, kus alamvahemikust saab korduvalt algne vahemik:
Kui e > v:
J ei sisalda o
e = 0
k = alumine_täisosa((e + v) / 2)
Kui o < J[ek]:
tagasta kahend_otsing(o, J, e, k - 1)
Muidu kui o > J[k]:
tagasta kahend_otsing(o, J, k + 1, v)
Muidu:
tagasta k
 
(Reaalarvu [alumine [täisosa | Täis-_ja_murdosa] on suurim täisarv, mis ei ole sellest reaalarvust suurem.)
funktsioon kahend_otsing(o, J, e, v):
Kui e > v:
J ei sisalda o
 
Algsest vahemikust <code>[e, v]</code> saab sõltuvalt väärtustest <code>o</code> ja <code>J[k]</code> kas <code>[e, k-1]</code> või <code>[k+1, v]</code>. Kui järjendi <code>J</code> elementide arv on <code>n</code>, siis tervest järjendist väärtuse otsimiseks kutsume funktsiooni vahemikuga <code>[0, n-1]</code>: <code>kahend_otsing(o, J, 0, n-1)</code>.
k = alumine_täisosa((e + v) / 2)
Kui o < J[k]:
tagasta kahend_otsing(o, J, e, k - 1)
Muidu kui o > J[k]:
tagasta kahend_otsing(o, J, k + 1, v)
Muidu:
tagasta k
 
Tuleb märkida, et rekursiivne versioon kahendotsingust võib kasutada <math>O(\log n)</math> mälu, mitte optimaalse konstantse mälu hulga, kui funktsiooni [pinumälu] ei vabastata enne rekursiivset väljakutset ([''tail-call optimization'' | en: tail call]). Selle tõttu võib olla efektiivsem iteratiivne versioon:
(Reaalarvu alumine [täisosa] on suurim täisarv, mis ei ole reaalarvust suurem.)
 
funktsioon kahend_otsing(o, J):
Algsest vahemikust [e, v] saab sõltuvalt väärtustest o ja J[k] kas [e, k-1] või [k+1, v]. Kui järjendi J elementide arv on n, siis tervest järjendist väärtuse otsimiseks kutsume funktsiooni vahemikuga [0, n-1]: kahend_otsing(o, J, 0, n-1).
Kui e >= v:0
v = n - 1
Korda:
vKui =e k> - 1v:
Teata, et J ei sisalda o
tagasta k
k = alumine_täisosa((e + v) / 2)
Kui o < J[ke]:
e v = k +- 1
Muidu kui J[k] < o:
e = k + 1
Muidu:
tagasta k
 
===Õigsus===
Tuleb märkida, et rekursiivne versioon kahendotsingust võib kasutada O(log n) mälu, mitte optimaalse konstantse mälu hulga, kui funktsiooni [pinumälu] ei vabastata enne rekursiivset väljakutset (tail-call optimization). Selle tõttu võib olla efektiivsem iteratiivne versioon:
 
Näitame, et kui otsitav väärtus leidub järjendis, siis see on alati vahemikus, kust kahendotsing otsib seda. Algses vahemikus on kõik järjendi elemendid, nii et kui otsitav väärtus leidub, siis see peab olema nende hulgas. Kui väärtus leidub vahemikus <code>[e, v]</code> keskmise elemendiga <code>J[k]</code> ja on suurem keskmisest elemendist, siis see peab olema suurem ka kõikidest elementidest vahemikus <code>[e, k]</code>, sest järjend on sorteeritud ning elemendid vahemikus <code>[e, k]</code> on väiksemad keskmisest elemendist või võrdsed sellega. Analoogiliselt, kui otsitav väärtus on väiksem keskmisest elemendist, siis see peab olema väiksem ka kõikidest elementidest vahemikus <code>[k, v]</code>, mis on omakorda keskmisest elemendist suuremad või sellega võrdsed. Järelikult uus vahemik, kus võib leiduda otsitava väärtusega võrdne elementväärtus, on vastavalt kas <code>[k+1, v]</code> või <code>[e, k-1]</code>, mis ongi vahemik, kus kahendotsing jätkub. [Matemaatilise induktsiooni | matemaatiline induktsioon] tõttu kahendotsing alati otsib väärtust vahemikust, kus ta leidub, kui järjend sisaldab seda väärtust.
funktsioon kahend_otsing(o, J):
e = 0
v = n - 1
Korda:
Kui e > v:
Teata, et J ei sisalda o
 
Lisaks näeme, et vahemikud <code>[e, k-1]</code> ja <code>[k+1, v]</code> mõlemad sisaldavad vähem elemente kui eelmine vahemik <code>[e, v]</code>, mistõttu vahemik, kus kahendotsing toimub, pidevalt läheb väiksemaks ning muutub tühjaks lõpliku aja jooksul, kui väärtust ei leita enne seda. Järelikult kahendotsing alati lõpetab töö lõpliku aja jooksul.
k = alumine_täisosa((e + v) / 2)
Kui o < J[e]:
v = k - 1
Muidu kui J[k] < o:
e = k + 1
Muidu:
tagasta k
 
===Keerukus===
Õigsus
 
Kahendotsing sooritab ühe väärtuse leidmiseks halvimal juhul floor<math>\lfloor(\log( n))+1 \rfloor \in O(\log n)</math> võrdlust, kus floor(.)<math> \lfloor\cdot\rfloor</math> tähistab alamtäisosa. [Knuth]
Näitame, et kui otsitav väärtus leidub järjendis, siis see on alati vahemikus, kust kahendotsing otsib seda. Algses vahemikus on kõik järjendi elemendid, nii et kui otsitav väärtus leidub, siis see peab olema nende hulgas. Kui väärtus leidub vahemikus [e, v] keskmise elemendiga J[k] ja on suurem keskmisest elemendist, siis see peab olema suurem ka kõikidest elementidest vahemikus [e, k], sest järjend on sorteeritud ning elemendid vahemikus [e, k] on väiksemad keskmisest elemendist või võrdsed sellega. Analoogiliselt, kui otsitav väärtus on väiksem keskmisest elemendist, siis see peab olema väiksem ka kõikidest elementidest vahemikus [k, v], mis on omakorda keskmisest elemendist suuremad või sellega võrdsed. Järelikult uus vahemik, kus võib leiduda otsitava väärtusega võrdne element, on vastavalt kas [k+1, v] või [e, k-1], mis ongi vahemik, kus kahendotsing jätkub. Matemaatilise induktsiooni tõttu kahendotsing alati otsib väärtust vahemikust, kus ta leidub, kui järjend sisaldab seda väärtust.
 
<math>O(\log n)</math> on vähim võimalik võrdluste arv halvimal juhul algoritmi jaoks, mis otsib sorteeritud järjendist ühe väärtuse võrdluste abil. Kui oleks selline algoritm, mis kasutaks vähem kui <math>O(\log n)</math> võrdlust, siis selle abil saaks sorteerida <math>n</math> elemendiga järjendit vähem kui <math>O(n \log n)</math> võrdlusega halvimal juhul, mis on tegelikult võimatu. Selleks teeme uue järjendi <math>S</math>, mis on alguses tühi, ning sisestame sellesse ühekaupa kõik järjendi <math>J</math> elemendid nii, et see jääks sorteerituks. Kui järjendisse <math>S</math> on juba sisestatud <math>m</math> elementi, siis leiame hüpoteetilise otsingualgoritmi abil vähem kui <math>O(\log m)</math> võrdlusega koha, kuhu sisestada <code>J[m]<\code>, nii et järjend <math>S</math> jääks sorteerituks – teatame ebaõnnestunud otsingu lõpus koha, kust väärtust viimasena otsiti. Kui oleme korranud seda indeksist <math>m=0</math> kuni <math>m=n-1</math>, siis <math>S</math> on sorteeritud, sest igal sammul me säilitasime selle elementide järjekorda, ning <math>S</math> sisaldab kõiki järjendi <math>J</math> elemente, mistõttu oleme sorteerinud järjendit <math>J</math>. Me kasutasime võrdlusi ainult sisestamise koha leidmiseks, mistõttu võrdluste arv on kokku vähem kui <math>O(0) + \sum_{m=1}^{n-1} O(\log m) = O(\log (n-1)!) = O(n \log n)<\math>, sest logaritmide summa on korrutise logaritm ning <math>O(\log(n!)) = O(n \log n)<\math> [Stirlingi valemi | https://et.wikipedia.org/wiki/Faktoriaal#Stirlingi_valem] kohaselt. Tegelikult see on võimatu, mistõttu kahendotsing kasutab optimaalset võrdluste arvu halvimal juhul. [cmp sort]
Lisaks näeme, et vahemikud [e, k-1] ja [k+1, v] mõlemad sisaldavad vähem elemente kui eelmine vahemik [e, v], mistõttu vahemik, kus kahendotsing toimub, pidevalt läheb väiksemaks ning muutub tühjaks lõpliku aja jooksul, kui väärtust ei leita enne seda. Järelikult kahendotsing alati lõpetab töö lõpliku aja jooksul.
 
Saame otsemini näidata optimaalsust vaadeldes otsingualgoritme, millel on <math>T</math> võimalikku tulemust ning mis kasutavad nendeni jõudmiseks ainult kahe võimaliku tulemusega otsuseid. Kahendotsing on selline algoritm, sest see kasutab tulemuse valimiseks ainult võrdluseid, millel on kaks võimalikku tulemust – üks väärtus on teisest väiksem või mitte – ning kahendotsingul on <math>T=n+1</math> võimalikku tulemust, sest tulemus on kas väärtuse indeks või et väärtus ei leidu. See tähendab, et iga otsingualgoritm on alguses ühes olekus ning peab lõpus olema sõltuvalt sisenditest ühes <math>T</math> erinevast olekust. See vastab puule, mille tipud on algoritmi olekud ning lõigud on olekumuutused, mis võivad toimuda pärast mõnda otsust. Puu on tsükliteta, sest algoritm lõpetab iga sisendi puhul töö lõpliku aja jooksul. Puu on kahendpuu ehk igal tipul on kas 0 või 2 last, sest algoritm lõpetab töö või teeb järgmise tipuni jõudmiseks kahe võimaliku tulemusega otsust. Juurtipp on tipp, kus algoritm alustab töö. On olemas tee juurtipu ja iga lehe vahel, sest algoritmi alguses on võimalikud kõik <math>T</math> tulemust. Algoritmi otsuste arv halvimal juhul on maksimaalne lõikude arv teel juurtipu ja mõne lehe vahel. Käesoleva <math>T</math> lehega kahendpuu puhul lõikude arv teel on väikseim, kui puu on tasakaalus, ja on vähemalt <math>O(\log T) = O(\log(n+1)) = O(\log n)</math>. [bintree] Järelikult otsuste arv ehk võrdlustega otsimise puhul vähim võrdluste arv halvimal juhul on vähemalt <math>O(\log n)</math>. [decision tree]
Keerukus
 
Informatsiooniteoreetiliselt see tähendab, et algoritm saab iga otsusega ühe bitti informatsiooni juurde ning tulemus – kahendotsingu puhul indeks – sisaldab <math>O(\log T)</math> bitti informatsiooni, mistõttu tulemuse saamiseks on vaja vähemalt <math>O(\log T)</math> otsust. [?]
Kahendotsing sooritab ühe väärtuse leidmiseks halvimal juhul floor(log(n))+1 \in O(log n) võrdlust, kus floor(.) tähistab alamtäisosa. [Knuth]
O(log n) on vähim võimalik võrdluste arv halvimal juhul algoritmi jaoks, mis otsib sorteeritud järjendist ühe väärtuse võrdluste abil. Kui oleks selline algoritm, mis kasutaks vähem kui O(log n) võrdlust, siis selle abil saaks sorteerida n elemendiga järjendit vähem kui O(n log n) võrdlusega halvimal juhul, mis on tegelikult võimatu. Selleks teeme uue järjendi S, mis on alguses tühi, ning sisestame sellesse ühekaupa kõik järjendi J elemendid nii, et see jääks sorteerituks. Kui järjendisse S on juba sisestatud m elementi, siis leiame hüpoteetilise otsingualgoritmi abil vähem kui O(log m) võrdlusega koha, kuhu sisestada J[m], nii et järjend S jääks sorteerituks – teatame ebaõnnestunud otsingu lõpus koha, kust väärtust viimasena otsiti. Kui oleme korranud seda indeksist m=0 kuni m=n-1, siis S on sorteeritud, sest igal sammul me säilitasime selle elementide järjekorda, ning S sisaldab kõiki järjendi J elemente, mistõttu oleme sorteerinud järjendit J. Me kasutasime võrdlusi ainult sisestamise koha leidmiseks, mistõttu võrdluste arv on kokku vähem kui O(0) + \sum_{m=1}^{n-1} O(log m) = O(log (n-1)!) = O(n log n), sest logaritmide summa on korrutise logaritm ning O(log(n!)) = O(n log n) [Stirlingi valemi | https://et.wikipedia.org/wiki/Faktoriaal#Stirlingi_valem] kohaselt. Tegelikult see on võimatu, mistõttu kahendotsing kasutab optimaalset võrdluste arvu halvimal juhul. [cmp sort]
 
===Võrdlus===
Saame otsemini näidata optimaalsust vaadeldes otsingualgoritme, millel on T võimalikku tulemust ning mis kasutavad nendeni jõudmiseks ainult kahe võimaliku tulemusega otsuseid. Kahendotsing on selline algoritm, sest see kasutab tulemuse valimiseks ainult võrdluseid, millel on kaks võimalikku tulemust – üks väärtus on teisest väiksem või mitte – ning kahendotsingul on T=n+1 võimalikku tulemust, sest tulemus on kas väärtuse indeks või et väärtus ei leidu. See tähendab, et iga otsingualgoritm on alguses ühes olekus ning peab lõpus olema sõltuvalt sisenditest ühes T erinevast olekust. See vastab puule, mille tipud on algoritmi olekud ning lõigud on olekumuutused, mis võivad toimuda pärast mõnda otsust. Puu on tsükliteta, sest algoritm lõpetab iga sisendi puhul töö lõpliku aja jooksul. Puu on kahendpuu ehk igal tipul on kas 0 või 2 last, sest algoritm lõpetab töö või teeb järgmise tipuni jõudmiseks kahe võimaliku tulemusega otsust. Juurtipp on tipp, kus algoritm alustab töö. On olemas tee juurtipu ja iga lehe vahel, sest algoritmi alguses on võimalikud kõik T tulemust. Algoritmi otsuste arv halvimal juhul on maksimaalne lõikude arv teel juurtipu ja mõne lehe vahel. Käesoleva T lehega kahendpuu puhul lõikude arv teel on väikseim, kui puu on tasakaalus, ja on vähemalt O(log T) = O(log(n+1)) = O(log n). [bintree] Järelikult otsuste arv ehk võrdlustega otsimise puhul vähim võrdluste arv halvimal juhul on vähemalt O(log n).
 
Eksponentsiaalne otsing on sarnane kahendotsinguga, aga alustab otsimist vahemikust <code>[0, 1]<\code> ja kahekordistab vahemiku viimase elemendi indeksit kuni vahemik sisaldab otsitavat väärtust või teatab, et järjend ei sisalda seda väärtust. Kui vahemik sisaldab väärtust, siis seda leitakse kahendotsinguga. Eksponentsiaalne otsing kasutab halvimal juhul rohkem võrdlusi kui kahendotsing, kuid võib olla efektiivsem, kui otsitav väärtus on tihti järjendi alguses.
Informatsiooniteoreetiliselt see tähendab, et algoritm saab iga otsusega ühe bitti informatsiooni juurde ning tulemus – kahendotsingu puhul indeks – sisaldab O(log T) bitti informatsiooni, mistõttu tulemuse saamiseks on vaja vähemalt O(log T) otsust.
 
Kahendotsingupuud ja B-puud kasutavad sarnaselt kahendotsinguga võrdlusi elementide välistamiseks, mis on kas kõik väiksemad või suuremad mõne tipu väärtusest. Kui puu on tasakaalus, siis sellesse väärtuse sisestamise ajalise keerukuse hinnang on <math>O(\log n)</math> halvimal juhul, mis on väiksem kui sorteeritud järjendisse sisestamise ajalise keerukuse hinnang <math>O(n)</math> halvimal juhul. Samas kui puu ei ole tasakaalus, siis see võib kasutada ühe väärtuse otsimisel kuni <math>O(n)</math> võrdlust halvimal juhul.
Võrdlus
 
Erinevalt eelmistest andmestruktuuridest kasutavad [paisktabelid] võrdluste asemel [paiskfunktsiooni | räsifunktsioon] ja [põrgete tõrjet] väärtuse otsimiseks. Paiskfunktsiooni sisenditeks on väärtused, mida sisestame paisktabelisse – need ei pea olema arvud, vaid võivad olla ka näiteks [sõned] – ja väljunditeks on indeksid paisktabelisisesesse järjendisse. Paisktabelist väärtuse leidmiseks kulub keskmiselt konstantne aeg ning [ideaalse paiskamise] korral kulub ka halvimal juhul konstantne aeg. See on võimalik, sest kui paisktabelis on <math>n</math> elementi, siis paiskfunktsioon teeb sisuliselt <math>n</math> võimaliku tulemusega otsust ühekorraga, erinevalt kahendotsingust, mis teeb iga võrdlusega ainult kahe võimaliku tulemusega otsust. Samas paisktabeli efektiivsus sõltub tugevalt paiskfunktsiooni valikust ning kui ideaalne paiskamine ei ole võimalik, siis ühe väärtuse otsimise ajalise keerukuse hinnang võib olla kuni <math>O(n)</math> halvimal juhul.
Eksponentsiaalne otsing on sarnane kahendotsinguga, aga alustab otsimist vahemikust [0, 1] ja kahekordistab vahemiku viimase elemendi indeksit kuni vahemik sisaldab otsitavat väärtust või teatab, et järjend ei sisalda seda väärtust. Kui vahemik sisaldab väärtust, siis seda leitakse kahendotsinguga. Eksponentsiaalne otsing kasutab halvimal juhul rohkem võrdlusi kui kahendotsing, kuid võib olla efektiivsem, kui otsitav väärtus on tihti järjendi alguses.
 
===Välislingid===
Kahendotsingupuud ja B-puud kasutavad sarnaselt kahendotsinguga võrdlusi elementide välistamiseks, mis on kas kõik väiksemad või suuremad mõne tipu väärtusest. Kui puu on tasakaalus, siis sellesse väärtuse sisestamise ajalise keerukuse hinnang on O(log n) halvimal juhul, mis on väiksem kui sorteeritud järjendisse sisestamise ajalise keerukuse hinnang O(n) halvimal juhul. Samas kui puu ei ole tasakaalus, siis see võib kasutada ühe väärtuse otsimisel kuni O(n) võrdlust halvimal juhul.
 
Erinevalt eelmistest andmestruktuuridest kasutavad [paisktabelid] võrdluste asemel [paiskfunktsiooni | räsifunktsioon] ja [põrgete tõrjet] väärtuse otsimiseks. Paiskfunktsiooni sisenditeks on väärtused, mida sisestame paisktabelisse – need ei pea olema arvud, vaid võivad olla ka näiteks [sõned] – ja väljunditeks on indeksid paisktabelisisesesse järjendisse. Paisktabelist väärtuse leidmiseks kulub keskmiselt konstantne aeg ning [ideaalse paiskamise] korral kulub ka halvimal juhul konstantne aeg. See on võimalik, sest kui paisktabelis on n elementi, siis paiskfunktsioon teeb sisuliselt n võimaliku tulemusega otsust ühekorraga, erinevalt kahendotsingust, mis teeb iga võrdlusega ainult kahe võimaliku tulemusega otsust. Samas paisktabeli efektiivsus sõltub tugevalt paiskfunktsiooni valikust ning kui ideaalne paiskamine ei ole võimalik, siis ühe väärtuse otsimise ajalise keerukuse hinnang võib olla kuni O(n) halvimal juhul.
 
Välislingid
[Kursuse "Algoritmid ja andmestruktuurid" materjalid | https://courses.cs.ut.ee/2016/aa/fall/Main/Viited]
[Jüri Kiho õpik "Algoritmid ja andmestruktuurid" | http://dspace.ut.ee/handle/10062/16872]
 
===Viited===
 
[1] Knuth 6.2.1
[2] cmp sort
[3] decision tree