A szegmensfa egy hatékony adatszerkezet, amelyet főként tartományokra vonatkozó műveletek gyors elvégzésére használnak. Egy bináris fa struktúrát használ, amely lehetővé teszi a tartományokra vonatkozó műveletek (pl. összegzés, minimum, maximum, LNKO) hatékony végrehajtását.
Fontos: A vizualizációk során kizárólag az összegzés műveletével fogunk dolgozni.
Amikor egy tömbben ismétlődő intervallum-lekérdezéseket végzünk (pl. egy adott tartomány összegét számítjuk), a naiv megoldás O(n) időben fut, ami nagyobb adathalmazoknál lassú lehet. Egy szegmensfa használatával viszont a lekérdezések és módosítások O(log n) idő alatt végrehajthatók, így sokkal hatékonyabb megoldást biztosít.
Tekintsük a következő ábrát:[36] / \ [7] [29] / \ / \ [4] [3] [18] [11]
Jól látható, hogy az ábrán a levelek képviselik a tartomány egyes elemeit, továbbá mindegyik szülő node a
két alatta lévő
gyermek node összegéből adódik.
Ebből következik, hogy a tömb amiből a fa épült: [4, 3, 18, 11]
Tegyük fel, hogy van egy tömbünk: [1, 3, 5, 7, 9, 11]
. És tekintsük a
következő kódot:
build(nodeIndex, left, right) {
if (left === right) {
this.tree[nodeIndex] = this.inputArray[left];
} else {
let mid = Math.floor((left + right) / 2);
this.build(nodeIndex * 2, left, mid);
this.build(nodeIndex * 2 + 1, mid + 1, right);
this.tree[nodeIndex] = this.tree[nodeIndex * 2] + this.tree[nodeIndex * 2 + 1];
}
}
Megjegyzés: A kód a statikus szegmensfa építési algoritmusát szemlélteti.
build(nodeIndex, left, right) {
➡️ Ez a függvény egy szakaszt épít fel a fából: az inputArray
tömb left
és
right
közötti részét.
if (left === right) {
➡️ Ha left == right
, akkor ez egy levélcsúcs a fában, vagyis csak egyetlen
elemről van szó.
this.tree[nodeIndex] = this.inputArray[left];
➡️ A levélcsúcs értéke maga az inputArray
-ben lévő szám.
} else {
let mid = Math.floor((left + right) / 2);
➡️ Ha a tartomány több elemet fed le, akkor kettéosztjuk: kiszámoljuk a közepét (mid
).
this.build(nodeIndex * 2, left, mid);
this.build(nodeIndex * 2 + 1, mid + 1, right);
➡️ Rekurzívan építjük fel a bal és jobb részfát.
nodeIndex * 2
nodeIndex * 2 + 1
this.tree[nodeIndex] = this.tree[nodeIndex * 2] + this.tree[nodeIndex * 2 + 1];
➡️ Miután a bal és jobb részfák készen vannak, a szülő csomópont értéke azok összege lesz.
build(1, 0, 5)
A teljes tömb: [1, 3, 5, 7, 9, 11]
Első lépés: Felosztjuk a tömböt:
mid = Math.floor((0 + 5)/2) = 2
[1, 3, 5]
→ build(2, 0, 2)
[7, 9, 11]
→ build(3, 3, 5)
build(2, 0, 2)
):mid = 1
build(4, 0, 1)
és build(5, 2, 2)
build(4, 0, 1)
továbboszt:
mid = 0
build(8, 0, 0)
→ levél → tree[8] = 1
build(9, 1, 1)
→ levél → tree[9] = 3
tree[4] = tree[8] + tree[9] = 1 + 3 = 4
build(5, 2, 2)
→ levél → tree[5] = 5
tree[2] = tree[4] + tree[5] = 4 + 5 = 9
build(3, 3, 5)
):mid = 4
build(6, 3, 4)
és build(7, 5, 5)
build(6, 3, 4)
:
mid = 3
build(12, 3, 3)
→ tree[12] = 7
build(13, 4, 4)
→ tree[13] = 9
tree[6] = 7 + 9 = 16
build(7, 5, 5)
→ tree[7] = 11
tree[3] = tree[6] + tree[7] = 16 + 11 = 27
tree[1] = tree[2] + tree[3] = 9 + 27 = 36
[0,5] (36) / \ [0,2] (9) [3,5] (27) / / / \ / \ [0,0](1) [1,1](3) [3,3](7) [4,4](9)
A szegmensfa egy rendkívül hatékony és hasznos adatszerkezet, amely lehetővé teszi a gyors intervallum-lekérdezéseket. Kiemelkedő előnye az O(log n) időbonyolultságú műveletek, amelyek révén nagy adathalmazokkal is hatékonyan dolgozhatunk.
A szegmensfák tehát nem csupán egyszerű adatstruktúrák, hanem kulcsfontosságú szerepet játszanak a modern számítástechnika egyes területein, különösen a versenyprogramozásban és algoritmus-tervezésben. Alkalmazásuk számos különféle területen elterjedt, és lehetővé teszik a rendkívül hatékony adatkezelést és számításokat, még akkor is, ha az adatok dinamikusan változnak.
A szegmensfa minden csúcsába a tartomány maximuma kerül. A lekérdezéshez a
Math.max()
használatos.
[0,7] (30) / \ [0,3] (18) [4,7] (30) / \ / \ [0,1] [2,3] [4,5] [6,7] (18) (9) (21) (30) / \ / \ / \ / \ (18) (6)(9) (3) (15) (21) (12) (30)
Megjegyzés: Hatékony maximum keresés bármely intervallumban.
Itt minden csomópont a szakasz LNKO-ját tárolja. A lekérdezésekhez az
LNKO(a, b)
műveletet alkalmazzuk.
[0,7] (3) / \ [0,3] (3) [4,7] (3) / \ / \ [0,1] [2,3] [4,5] [6,7] (6) (3) (3) (6) / \ / \ / \ / \ (18) (6)(9) (3)(15) (21) (12) (30)
Megjegyzés: Gyors és hatékony LNKO lekérdezések intervallumokra.
A minimum szegmensfa minden szakasz legkisebb értékét tárolja,
Math.min()
alapján.
[0,7] (3) / \ [0,3] (3) [4,7] (12) / \ / \ [0,1] [2,3] [4,5] [6,7] (6) (3) (15) (12) / \ / \ / \ / \ (18) (6)(9) (3) (15) (21) (12) (30)
Megjegyzés: Hasznos minimumok gyors kiszámításához bármely tartományban.
Tulajdonság | Statikus szegmensfa | Dinamikus szegmensfa | Perzisztens szegmensfa |
---|---|---|---|
Adatkezelés | Fix adatokkal dolgozik, amelyek nem változnak | Támogatja az adatok frissítését és módosítását | Minden frissítés új verziót hoz létre – nincs adatvesztés |
Lekérdezés ideje | O(log n) | O(log n) | O(log n) |
Frissítés ideje | Nem támogatott (újraépítés szükséges) | O(log n) | O(log n) – új verzió jön létre |
Memóriahasználat | Alacsony | Közepes – dinamikus struktúra miatt | Magas – minden verzióhoz új faágak tárolódnak |
Implementáció összetettsége | Könnyű | Közepes – rekurzió + mutáció kezelése | Magas – verziókezelés, funkcionális szemlélet szükséges |
Tipikus használat | Statikus adatok, például előre ismert konfigurációk | Real-time rendszerek, pénzügyi alkalmazások | Verziókövetés, időutazós lekérdezések (pl. undo/redo) |
Frissítés visszavonása | Nem lehetséges | Csak utolsó állapot elérhető | Igen, bármely korábbi verzióhoz vissza lehet térni |
Gyorsan kiszámíthatjuk egy tömb adott tartományában (pl. [3,7]
) szereplő számok
összegét.
A szegmensfa csomópontjai előre kiszámított részösszegeket tartalmaznak, így a lekérdezés ideje O(log n).
sum(3, 7) → 42
Meghatározhatjuk egy intervallum legkisebb vagy legnagyobb elemét valós időben.
A fa csomópontjai a gyermekek minimumát/maximumát tárolják. Így frissítés és lekérdezés is O(log n).
min(2, 5) → 3
Rendezés közben segíthet meghatározni, hány inverzió (fordított sorrendű pár) van egy tömbben.
A szegmensfát prefix összegek és előfordulások tárolására használjuk.
[4, 1, 3, 2] → 4 inverzió
Pénzügyi adatok, érzékelők, vagy valós idejű streamelt értékek esetén fontos, hogy gyorsan frissíthető és lekérdezhető legyen a tartomány.
A dinamikus szegmensfa lehetővé teszi az adatok gyors frissítését O(log n) időben.
update(5, +10)
Gyorsan meghatározható, hogy egy adott időintervallumban mennyi esemény történt.
Minden időpillanatra eltárolunk egy számlálót, így lekérdezhetők a tartományi összesítések.
events(10, 20) → 7 esemény
Egy játékban vagy modellben gyorsan lekérdezhetjük, hogy egy adott érték hányszor fordul elő egy tartományban.
Módosított szegmensfában minden értékhez külön gyakoriságot tárolunk.
count(3, 6, 7) → 2 db 7-es
Bizonyítás: A szegmensfa egy teljes bináris fa, amelynek csomópontjait egy \( 2n \) méretű tömbben tárolhatjuk.
Az eredeti tömb elemeit a levelekhez másoljuk:
\[ v[n+i] = a[i], \quad 0 \leq i < n \]
Ez \( O(n) \) időt igényel.
Alulról építjük a fát, minden belső csomópont értékét a gyerekeiből számítjuk ki:
\[ v[i] = f(v[2i], v[2i+1]) \]
Minden csomópontot pontosan egyszer számolunk ki, így az időbeli komplexitás:
\[ B(n) = B(n/2) + O(n) \Rightarrow O(n) \]
A szegmensfa felépítése \( O(n) \) idő alatt végrehajtható.
Bizonyítás: Egy módosítás során:
A keresett indexű levélcsomópontot \( O(\log n) \) lépés alatt elérjük.
Az új értéket beállítjuk a levélben.
A módosítás hatását végighúzzuk a szülőkön:
\[ v[i] = f(v[2i], v[2i+1]) \]
Mivel a fa magassága \( O(\log n) \), az időbeli komplexitás:
\[ U(n) = U(n/2) + O(1) \Rightarrow O(\log n) \]
A módosítás \( O(\log n) \) idő alatt végrehajtható.
Bizonyítás: Egy adott \([L, R]\) intervallumra vonatkozó lekérdezés során:
Az indexeket a megfelelő levelekhez mozgatjuk.
A teljes intervallum lefedéséhez csak azokat a csomópontokat választjuk, amelyek teljes átfedésben vannak:
\[ \text{Ha } L \text{ páratlan, hozzáadjuk } v[L] \text{-t és } L++ \] \[ \text{Ha } R \text{ páros, hozzáadjuk } v[R] \text{-t és } R-- \]
Ezután \( L \) és \( R \) szülőire lépünk:
\[ L = L/2, \quad R = R/2 \]
Mivel a fa magassága \( O(\log n) \), a művelet legfeljebb ennyi lépést végez, és minden lépés \( O(1) \).
Az időbeli komplexitás:
\[ Q(n) = Q(n/2) + O(1) \Rightarrow O(\log n) \]
Tehát egy lekérdezés \( O(\log n) \) idő alatt végrehajtható.