Zur Haupt­na­vi­ga­ti­on sprin­gen [Alt]+[0] Zum Sei­ten­in­halt sprin­gen [Alt]+[1]

Bäume

Ein Baum ist – wie die ver­ket­te­te Liste – eine Da­ten­struk­tur, die aus ein­zel­nen Ele­men­ten auf­ge­baut sind, die auf ihre Nach­barn ver­wei­sen. Genau wie Lis­ten­kno­ten hat jeder Baum­kno­ten ein Da­ten­ele­ment, das be­lie­bi­ge Werte um­fas­sen kann. Al­ler­dings haben Baum­kno­ten im Un­ter­schied zur Liste meh­re­re Nach­fol­ge­kno­ten. Diese wer­den als Kin­der des Kno­tens be­zeich­net. Um­ge­kehrt hat jeder Kno­ten genau einen El­tern­kno­ten, der auf ihn ver­weist. Die ein­zi­ge Aus­nah­me ist der Wur­zel­kno­ten, der keine El­tern hat. Auf diese Weise ist vom El­tern­kno­ten aus jeder Kno­ten des Baums durch einen ein­deu­ti­gen Pfad er­reich­bar, wenn man wie­der­holt die Nach­fol­ger­ver­wei­se ver­folgt. In man­chen Im­ple­men­ta­tio­nen ist es sinn­voll, wenn ein Kind­kno­ten zu­sätz­lich einen Ver­weis auf sei­nen El­tern­kno­ten be­sitzt16. Dies macht al­ler­dings die Ver­än­de­rung des Bau­mes kom­pli­zier­ter.

 

Bäume sind Spe­zi­al­fäl­le von ge­rich­te­ten Gra­phen mit der Zu­sat­zei­gen­schaft, dass jeder Kno­ten mit Aus­nah­me der Wur­zel den Ein­gangs­grad 1 hat. Das be­deu­tet, dass alle Al­go­rith­men, die auf ge­rich­te­ten Gra­phen aus­ge­führt wer­den kön­nen, auch für Bäume an­wend­bar sind. In die­sem Do­ku­ment und die­ser Un­ter­richts­ein­heit wird die Da­ten­struk­tur Baum be­han­delt, nicht der ADT Baum17.

Binärbäume

Bild­quel­le: Bi­när­bäu­me von ZPG In­for­ma­tik [CC BY-NC-SA 3.0 DE], aus 02_hin­ter­grun­d_adts.odt

Ein Spe­zi­al­fall von Bäu­men stel­len Bi­när­bäu­me dar. Hier hat jeder Kno­ten ma­xi­mal zwei Kind­kno­ten. Man spricht dann vom „lin­ken“ und „rech­ten“ Kind(-kno­ten).

Es gibt in der In­for­ma­tik eine Viel­zahl von An­wen­dun­gen von Bäu­men. Bäume wer­den zur Dar­stel­lung von Ver­zeich­nis­hier­ar­chi­en, Ver­er­bungs­be­zie­hun­gen, Ent­schei­dungs­re­geln, Re­chen­ter­men, der Struk­tur von HTML- oder XML-Da­tei­en und vie­lem mehr ver­wen­det. Mit­un­ter wer­den die Baum­struk­tu­ren nicht ex­pli­zit auf­ge­baut, son­dern er­ge­ben sich durch re­kur­si­ve Al­go­rith­men, wie z.B. bei der re­kur­si­ven Be­rech­nung der Fi­bo­nac­ci-Funk­ti­on oder Spiel­bäu­men.

Tra­ver­sie­run­gen

Viele Baumal­go­rith­men er­for­dern eine voll­stän­di­ge Tra­ver­sie­rung des Bau­mes. Das be­deu­tet, dass jeder Kno­ten des Baums nach­ein­an­der durch­lau­fen wird. Es bie­tet sich an, eine re­kur­si­ve Im­ple­men­ta­ti­on zu ver­wen­den.

Bei­spiel: Ein Bi­när­baum ent­hält Zah­len als Da­ten­wer­te. Es soll die Summe aller Werte des Baums be­rech­net wer­den. summe(b: Bi­när­baum):

sum = b.daten
  falls b.links != null:
    sum += summe(b.links)
  falls b.rechts != null:
    sum += summe(b.rechts)
  return sum

Der erste Auf­ruf der Funk­ti­on wird mit der Wur­zel des Bau­mes aus­ge­führt. Wir gehen davon aus, dass die­ser nicht be­reits null ist. Bei der Aus­füh­rung wer­den alle Kno­ten des Baums re­kur­siv durch­lau­fen.

Beim Durch­lau­fen eines Bau­mes gibt es drei grund­sätz­lich ver­schie­de­ne Tra­ver­sie­rungs­stra­te­gi­en:

  • Pre­or­der-Tra­ver­sie­rung: Es wird zu­erst der Kno­ten selbst ver­ar­bei­tet, dann seine Kin­der. Das Bei­spiel oben ist als Pre­or­der-Tra­ver­sie­rung ge­schrie­ben.
  • Pos­t­or­der-Tra­ver­sie­rung: Es wer­den zu­erst die Kin­der des Kno­tens ver­ar­bei­tet, dann seine Kin­der.
  • In­or­der-Tra­ver­sie­rung: Es wird zu­erst der linke Kind­kno­ten ver­ar­bei­tet, dann der Kno­ten selbst, dann der rech­te Kind­kno­ten. In­or­der-Tra­ver­sie­rung ist nur auf Bi­när­bäu­me sinn­voll an­wend­bar.

Hier zum Ver­gleich die Sum­men-Funk­ti­on mit den bei­den an­de­ren Tra­ver­sie­rungs­stra­te­gi­en:

summePostorder(b: Binärbaum):
  sum = 0
  falls b.links != null:
    sum += summePostorder(b.links)
  falls b.rechts != null:
    sum += summePostorder(b.rechts)
  sum += b.daten
  return sum
summeInorder(b: Binärbaum):
  sum = 0
  falls b.links != null:
    sum += summeInorder(b.links)
  sum += b.daten
  falls b.rechts != null:
    sum += summeInorder(b.rechts)
  return sum
Traversierungsreihenfolgen

Tra­ver­sie­rungs­rei­hen­fol­gen ZPG In­for­ma­tik [CC BY-NC-SA 3.0 DE], aus 02_hin­ter­grun­d_adts.odt

Bei­spiel für un­ter­schied­li­che Tra­ver­sie­rungs­rei­hen­fol­gen:

Pre­or­der-Tra­ver­sie­rung: A → B → D → E → F → G → C

Pos­t­or­der-Tra­ver­sie­rung: D → F → G → E → B → C → A

In­or­der-Tra­ver­sie­rung: D → B → F → E → G → A → C

Beim Auf­sum­mie­ren spielt es keine Rolle, wie der Baum tra­ver­siert wird, weil die Ad­di­ti­on (zu­min­dest bei In­te­ger-Wer­ten) kom­mu­ta­tiv ist.

Es gibt al­ler­dings ver­schie­de­ne Si­tua­tio­nen, in denen die Tra­ver­sie­rungs­rei­hen­fol­ge re­le­vant ist.

Pre­or­der-Tra­ver­sie­rung: z.B. Thron­fol­ge in Kö­nigs­häu­sern – der äl­tes­te Nach­kom­me des ak­tu­el­len Re­gen­ten ist di­rek­ter Thron­fol­ger, da­nach des­sen äl­tes­ter Nach­kom­me usw.

Pos­t­or­der-Tra­ver­sie­rung: z.B. Aus­wer­ten von Re­chen­baums – zu­erst müs­sen die Er­geb­nis­se der bei­den Kin­der fest­ste­hen, bevor sie mit dem Ope­ra­tor des ak­tu­el­len Kno­tens ver­ar­bei­tet wer­den.

In­or­der-Tra­ver­sie­rung: z.B. li­nea­re Dar­stel­lung eines Re­chen­baums – zu­erst wird der linke Teil­baum aus­ge­ge­ben, dann das Re­chen­zei­chen des ak­tu­el­len Kno­tens, dann der rech­te Teil­baum.

Tie­fen- und Brei­ten­su­che

Bei den oben be­schrie­be­nen Tra­ver­sie­run­gen geht man „au­to­ma­tisch“ re­kur­siv vor. Ein sol­cher Al­go­rith­mus führt zu einer Tie­fen­su­che: Es wird zu­erst der erste Kind­kno­ten der Wur­zel und dann des­sen Kind­kno­ten usw. be­ar­bei­tet, bevor man zum zwei­ten Kind­kno­ten der Wur­zel kommt. Auf diese Weise wan­dert man im Baum „zu­erst in die Tiefe“, was den Namen „Tie­fen­su­che“ er­klärt (im Eng­li­schen „depth-first-se­arch“). Wenn man einen Kno­ten mit einer be­stimm­ten Ei­gen­schaft sucht, wird der am wei­ten „links“ ste­hen­de Kno­ten ge­fun­den, der die Ei­gen­schaft auf­weist.

tiefensuche(b: Binärbaum):
  falls b != null UND Attribut(b):
    markiere b und Ende
  tiefensuche(b.links)
  tiefensuche(b.rechts)

Man kann die­sel­be Ab­ar­bei­tungs­rei­hen­fol­ge auch mit einem ite­ra­ti­ven Al­go­rith­mus er­rei­chen. An­stel­le eines Re­kur­si­ons­auf­rufs wech­selt man bei jedem Schlei­fen­durch­lauf zu­nächst zum ers­ten Kind­kno­ten. Zudem muss man sich die rest­li­chen Kin­der mer­ken, um sie ver­ar­bei­ten zu kön­nen, wenn man mit der Ver­ar­bei­tung des Teil­baums un­ter­halb des ers­ten Kin­des fer­tig ist. Es gilt dabei das „last-in-first-out“-Prin­zip, also muss ein Stack ver­wen­det wer­den.

tiefensucheIterativ(b: Binärbaum):
  s = neuer Stack
  s.push(b)
  solange s nicht leer ist:
    k = s.pop()
    falls k != null UND Attribut(k)
      markiere k und Ende
    s.push(b.rechts)
    s.push(b.links)

Im Co­de­bei­spiel wird der rech­te Kind­kno­ten vor dem lin­ken Kind­kno­ten auf den Stack ge­legt, damit im je­weils nächs­ten Schlei­fen­durch­lauf zu­erst der linke Kind­kno­ten ver­ar­bei­tet wird. Würde man die Rei­hen­fol­ge um­keh­ren, hätte man eben­so eine Tie­fen­su­che, würde aber den am wei­tes­ten rechts ste­hen­den Baum­kno­ten fin­den.

Unter man­chen Um­stän­den ist es not­wen­dig, den Kno­ten zu fin­den, der eine be­stimm­te Ei­gen­schaft hat und am wei­tes­ten oben im Baum steht, d.h. dass der Pfad von der Wur­zel zum ge­fun­de­nen Kno­ten so kurz wie mög­lich ist18 . Dies lässt sich ein­fach durch den Aus­tausch der Con­tai­ner-Da­ten­struk­tur im obi­gen Al­go­rith­mus er­rei­chen: An­stel­le des Stacks nimmt man eine Queue. Der re­sul­tie­ren­de Al­go­rith­mus heißt „Brei­ten­su­che“, weil er zu­erst „die ge­sam­te Brei­te“ des Baums durch­sucht, bevor er wei­ter unten wei­ter­macht (eng­lisch „bre­adth-first-se­arch“).

breitensucheIterativ(b: Binärbaum):
  q = neue Queue
  q.enqueue(b)
  solange q nicht leer ist:
    k = q.dequeue()
    falls k != null UND Attribut(k)
      markiere k und Ende
    q.enqueue(b.rechts)
    q.enqueue(b.links)

Auf diese Weise wird der Baum „Schicht für Schicht“ ab­ge­ar­bei­tet: Zu­erst die Wur­zel, dann die di­rek­ten Kin­der, dann deren Kin­der usw.19

Es fällt auf, dass die bei­den Al­go­rith­men struk­tu­rell sehr ähn­lich sind. Wenn man Stack und Queue in einer ge­mein­sa­men Ober­klas­se zu­sam­men­ge­fasst hat (vgl. Seite 11), kann man Tie­fen- und Brei­ten­su­che in eine ge­mein­sa­me Funk­ti­on zu­sam­men­fas­sen:

tiefenBreitenSuche(b: Binärbaum, c: Container):
  c.insert(b)
  solange c nicht leer ist:
    k = c.removeNext()
    falls k != null UND Attribut(k)
      markiere k und Ende
    q.insert(b.rechts)
    q.insert(b.links)
tiefensuche(b: Binärbaum):
  tiefenBreitenSuche(b, neuer Stack)
breitensuche(b: Binärbaum):
  tiefenBreitenSuche(b, neue Queue)

 

16 Bei Ver­zeich­nis­bäu­men ist ein sol­ches Vor­ge­hen z.B. sinn­voll.

17 Die Da­ten­struk­tur Baum ist eine mög­li­che Im­ple­men­ta­ti­on des ADTs Baum, so wie die Da­ten­struk­tur „ver­ket­te­te Liste“ eine mög­li­che Im­ple­men­ta­ti­on des ADTs Liste ist.

18 Da in einem Baum der Pfad von der Wur­zel zu einem Kno­ten immer ein­deu­tig ist, füh­ren Tie­fen- und Brei­ten­su­che auf Bäu­men stets zum glei­chen Er­geb­nis, wenn nur ein Kno­ten das Suchat­tri­but er­füllt. Für all­ge­mei­ne Gra­phen ist der Un­ter­schied hin­ge­gen deut­lich re­le­van­ter.

19 Bei der Brei­ten­su­che wird zu­erst der rech­te, dann der linke Kind­kno­ten ein­ge­reiht. Damit wird jede Schicht „von rechts nach links“ ver­ar­bei­tet. Bei der Tie­fen­su­che wird aber zu­erst der linke, dann der rech­te Kind­kno­ten ver­ar­bei­tet. Dies ist nütz­lich, um spä­ter Tie­fen- und Brei­ten­su­che zu ver­ein­heit­li­chen.

 

Hin­ter­grund­in­for­ma­tio­nen: Her­un­ter­la­den [odt][305 KB]

 

Wei­ter zu Re­kur­si­on