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

Ver­ket­tung

Grund­la­gen

Die meis­ten gän­gi­gen Pro­gram­mier­spra­chen de­fi­nie­ren eine Reihe von pri­mi­ti­ven Da­ten­ty­pen für ato­ma­re Werte. Ty­pi­scher­wei­se sind das Typen für Ganz- und Fließ­kom­ma­zah­len. Dazu kom­men aus Grün­den der Be­nutz­bar­keit Da­ten­ty­pen für boo­le­sche Werte, Buch­sta­ben1 oder Zei­chen­ket­ten. Will man meh­re­re gleich­ar­ti­ge Va­ria­blen zu­sam­men­fas­sen, un­ter­stüt­zen viele Spra­chen Ar­rays (auch Feld oder Rei­hung ge­nannt). Diese wer­den durch einen zu­sam­men­hän­gen­den Be­reich im Spei­cher re­prä­sen­tiert.

Bei­spiel: Den Spei­cher des Com­pu­ters (ge­nau­er ge­sagt den „Heap“) kann man sich als lange Reihe num­me­rier­ter Zel­len vor­stel­len, in denen je­weils eine Zahl ab­ge­legt wer­den kann.

Wenn eine Va­ria­ble an­ge­legt (de­kla­riert) wird, er­folgt zur Lauf­zeit eine Zu­ord­nung zwi­schen der Va­ria­blen und einer ge­ra­de frei­en Spei­cher­zel­le. Diese wird für die Va­ria­ble re­ser­viert:

int a;	// a entspricht Speicherzelle 1000

Wenn bei der Aus­füh­rung des Pro­gramms der Va­ria­blen ein Wert zu­ge­wie­sen wird, wird die­ser Wert an die ent­spre­chen­de Stel­le im Spei­cher ge­schrie­ben.

a = 42;	// speichere den Wert 42 in der Zelle 1000

Wird ein Array an­ge­legt, dann wird ein zu­sam­men­hän­gen­der frei­er Block Spei­cher für das Array re­ser­viert.

int[] b = new int[5];
  // b beginnt bei Adresse 1001 und ist 5 Zellen lang2
  int[] c = new int[10];
  // c beginnt bei Adresse 1006 und ist 10 Zellen lang3

Will man nun einen Wert im Array spei­chern, ver­wen­det man den Index, an dem der Wert ab­ge­legt wer­den soll. Dar­aus kann auf ein­fa­che Weise die zu­ge­hö­ri­ge Adres­se be­rech­net wer­den:

b[3] = 28;	// b[3] gehört nach 1001 + 3 = 10044

Der Spei­cher­be­darf eines Ar­rays ent­spricht dem Spei­cher­be­darf der darin ent­hal­te­nen Werte sowie einer In­for­ma­ti­on über die Länge des Ar­rays. Damit sind Ar­rays die kom­pak­tes­te Form, in der man Werte spei­chern kann. Zudem ist der wahl­freie Zu­griff (Lesen oder Über­schrei­ben) auf be­lie­bi­ge Ele­men­te des Ar­rays („ran­dom ac­cess“) in kon­stan­ter Zeit mög­lich.

Pro­ble­ma­tisch wird es, wenn einem Array wei­te­re Werte hin­zu­ge­fügt wer­den sol­len. Un­er­heb­lich, ob die Werte am Ende, am An­fang oder ir­gend­wo in der Mitte des Ar­rays ein­ge­fügt wer­den, müss­te eine wei­te­re Spei­cher­zel­le an den Block an­ge­fügt wer­den. Im obi­gen Bei­spiel müss­te beim Er­wei­tern des gelb mar­kier­ten Ar­rays die Zelle 1006 hin­zu­ge­nom­men wer­den. Lei­der be­ginnt ab Zelle 1006 be­reits der Spei­cher­block des grü­nen Ar­rays.

Wenn man als Pro­gram­mie­rer Ar­rays ver­wen­den will, muss man also im Vor­feld be­reits wis­sen, wie viele Ele­men­te das Array auf­neh­men soll. Das ist aber nicht in allen An­wen­dungs­fäl­len mög­lich. In die­sem Fall sind ver­ket­te­te Lis­ten eine mög­li­che Lö­sung.

Eine Ele­ment einer ver­ket­te­ten Liste be­steht aus einem Datum (z.B. einer Zahl) sowie einem Ver­weis auf ihren Nach­fol­ger.

Einfach verkettete Liste

Ab­bil­dung 1: Ein­fach ver­ket­te­te Liste von ZPG In­for­ma­tik [CC BY-NC-SA 3.0 DE], aus 02_hin­ter­grun­d_adts.odt

Im obi­gen Bei­spiel ver­weist das erste Ele­ment mit dem Wert „12“ auf sei­nen Nach­fol­ger mit dem Datum „99“, die­ser auf sei­nen Nach­fol­ger mit dem Wert „37“ und die­ser auf „nichts“ (bzw. „null“). Die 37 ist damit das Ende der Liste.

Im Spei­cher kann eine sol­che Liste wie folgt aus­se­hen:

Um mit der Liste ar­bei­ten zu kön­nen, be­nö­tigt man einen Ver­weis auf ihr ers­tes Ele­ment, die Spei­cher­adres­se 1001. Von dort aus kann man die wei­te­ren Ele­men­te der Liste er­rei­chen, indem man den Nach­fol­ger-Ver­wei­sen folgt.

Man sieht, dass die Ele­men­te der ver­ket­te­ten Liste nicht zwangs­läu­fig als ein zu­sam­men­hän­gen­der Block im Spei­cher lie­gen müs­sen, auch die Rei­hen­fol­ge im Spei­cher muss nicht un­be­dingt ihrer Rei­hen­fol­ge in der Liste ent­spre­chen.

Um ein neues Ele­ment an die Liste an­zu­hän­gen, wird es (z.B. an der Spei­cher­stel­le 1015) er­zeugt und der Nach­fol­ger­ver­weis des letz­ten Ele­ments auf seine Adres­se ge­setzt:

Auf diese Weise ist das Fas­sungs­ver­mö­gen einer ver­ket­te­ten Liste nur durch den ins­ge­samt ver­füg­ba­ren Spei­cher be­schränkt.

Auch das Ein­fü­gen eines Ele­ments am An­fang oder in der Mitte der Liste ist ef­fi­zi­ent mög­lich.

Bei­spiel: Der Wert 70 soll di­rekt hin­ter der 12 ein­ge­fügt wer­den. Dazu wer­den die fol­gen­den Schrit­te aus­ge­führt:

  • neues Ele­ment mit Datum 70 er­zeu­gen (hier an der Adres­se 1017)
  • der Nach­fol­ger des Ele­ments mit dem Wert 70 ist der bis­he­ri­ge Nach­fol­ger der 12 (also Adres­se 1009)
  • der neue Nach­fol­ger des Ele­ments mit dem Datum 12 ist das Ele­ment mit dem Wert 70 (also Adres­se 1017)

Die Schrit­te, die durch­ge­führt wur­den, sind nicht ab­hän­gig von der Länge der Liste. Al­ler­dings muss man den Ver­weis auf das Ele­ment, hin­ter dem ein­ge­fügt wer­den soll, erst ein­mal fin­den.

Ver­gleich von Array und ver­ket­te­ter Liste

So­wohl Array als auch ver­ket­te­te Liste haben ihre An­wen­dungs­be­rei­che. Je nach­dem, wel­che Ziele man hat, ist die eine oder die an­de­re Da­ten­struk­tur bes­ser ge­eig­net.

Zu­nächst ein­mal kann man fest­stel­len, dass der be­nö­tig­te Spei­cher­be­darf für ein Array ge­rin­ger ist. Man be­nö­tigt so viel Platz, wie die zu spei­chern­den Werte be­le­gen sowie ein­ma­lig Platz für die Array-Länge. Bei einer ver­ket­te­ten Liste be­nö­tigt man für jeden Wert zu­sätz­lich Platz, um die Adres­se sei­nes Nach­fol­gers zu spei­chern.

Bei­spiel: Um 1000 int-Zah­len (32 Bit = 4 Byte) in einem Array zu spei­chern, be­nö­tigt man 4∙1000 = 4000 Byte sowie 4 Byte für die Länge des Ar­rays. Eine ver­ket­te­te Liste mit 1000 int-Zah­len be­nö­tigt auf einer 32-Bit-Ma­schi­ne 4∙1000 Byte für die Daten sowie wei­te­re 4∙1000 Byte für die Nach­fol­ge­r­adres­se.

Um mit einem Array ar­bei­ten zu kön­nen, be­nö­tigt man die Adres­se des ers­ten Ele­ments sowie die Länge des Ar­rays. Für die Liste muss man die Adres­se des ers­ten Ele­ments ken­nen.

Bei Ar­rays ist – wie oben er­wähnt – ein di­rek­ter Zu­griff auf jedes Ele­ment mög­lich. Um auf ein Ele­ment einer ver­ket­te­ten Liste zu­zu­grei­fen, muss diese vom An­fang her durch­ge­gan­gen wer­den, indem je­weils der Nach­fol­ger­ver­weis ge­nom­men wird.

Im­ple­men­ta­ti­on in Java

Listenknoten

Bild­quel­le: Lis­ten­kno­ten<T> von ZPG In­for­ma­tik [CC BY-NC-SA 3.0 DE], aus 02_hin­ter­grun­d_adts.odt

Eine kon­kre­te Im­ple­men­ta­ti­on einer ver­ket­te­ten Liste kann mit der Klas­se Listenknoten<T> um­ge­setzt wer­den, wie sie im UML-Dia­gramm rechts dar­ge­stellt ist.

Hier wird auf das get/set-Pat­tern, das in der Ob­jekt­ori­en­tier­ten Pro­gram­mie­rung üb­lich ist, ver­zich­tet. Der Grund dafür ist, dass die Get­ter und Set­ter ei­gent­lich „nichts tun“. Es wird keine Über­prü­fung oder Be­rech­nung vor­ge­nom­men. Der Wert von daten bzw. nachfolger wird nur ge­setzt bzw. zu­rück­ge­ge­ben. Der Code würde nur un­nö­tig auf­ge­bläht wer­den. Da die Listenknoten-Ob­jek­te oh­ne­hin nur von der Klas­se Liste ver­wen­det wer­den, fällt auch die Not­wen­dig­keit, sich gegen spä­te­re Än­de­run­gen der in­ter­nen Im­ple­men­ta­ti­on ab­zu­si­chern, hier weg.

Die Liste be­ste­hend aus den Wer­ten „12, 99 und 37“ in Ab­bil­dung 1 könn­te man fol­gen­der­ma­ßen er­zeu­gen:

Listenknoten<Integer> a = new Listenknoten<Integer>(37, null);
Listenknoten<Integer> b = new Listenknoten<Integer>(99, a);
Listenknoten<Integer> c = new Listenknoten<Integer>(12, b);

Um mit der Liste ar­bei­ten zu kön­nen, be­nö­tigt man den Ver­weis auf das erste Lis­ten­ele­ment c. Jetzt könn­te das An­hän­gen eines Ele­ments wie folgt rea­li­siert wer­den: Man be­ginnt beim ers­ten Kno­ten der Liste und springt so lange zum Nach­fol­ger, bis man ein Ele­ment ohne Nach­fol­ger ge­fun­den hat.

void anhaengen(Listenknoten k, T neuerWert) {
  while(k.nachfolger != null) {
    k = k.nachfolger;
  }
  k.nachfolger = new Listenknoten(neuerWert, null);
}

Al­ter­na­tiv wäre eine re­kur­si­ve Im­ple­men­ta­ti­on mög­lich:

void anhaengen(Listenknoten k, T neuerWert) {
  if (k.nachfolger == null) {
    k.nachfolger = new Listenknoten(neuerWert, null);
  }
  else {
    anhaengen(k.nachfolger, neuerWert);
  }
}

Auf diese Weise kann man auch Ope­ra­tio­nen für das Ein­fü­gen am An­fang oder an einer be­stimm­ten Stel­le in der Mitte im­ple­men­tie­ren. Eben­so wäre das Su­chen eines Wer­tes, das Ab­fra­gen des n-ten Wer­tes oder das Ent­fer­nen eines Wer­tes mög­lich.

Es gibt aber zwei Pro­ble­me, wenn ein Pro­gram­mie­rer die Da­ten­struk­tur Listenknoten und die Al­go­rith­men, wie sie oben be­schrie­ben sind, di­rekt ver­wen­den würde:

  1. Nie­mand hin­dert den Pro­gram­mie­rer daran, zy­klisch ver­ket­te­te Lis­ten zu er­zeu­gen. Der fol­gen­de Code wäre mög­lich:
    Listenknoten<Integer> a = new Listenknoten<Integer>(37, null);
    Listenknoten<Integer> b = new Listenknoten<Integer>(99, a);
    a.nachfolger = b;
    Würde man z.B. die Me­tho­de anhaengen mit a oder b als Pa­ra­me­ter auf­ru­fen, würde diese nicht ter­mi­nie­ren, da nie­mals ein Kno­ten er­reicht würde, der kei­nen Nach­fol­ger hat.
  2. Die Me­tho­de oben geht davon aus, dass der über­ge­be­ne Listenknoten zu Be­ginn nicht null ist. Dies würde einer lee­ren Liste ent­spre­chen. Ein Pro­gram­mie­rer, der den Listenknoten selbst ver­wal­ten würde, müss­te Spe­zi­al­fäl­le für den Fall ein­bau­en, dass die Liste leer ist.

VerketteteListe

Bild­quel­le: Ver­ket­te­te­Lis­te<T> ZPG In­for­ma­tik [CC BY-NC-SA 3.0 DE], aus 02_hin­ter­grun­d_adts.odt

Als Lö­sung ist es daher sinn­voll, die Ver­wal­tung der Listenknoten in einer Klas­se zu kap­seln. Diese gibt zu kei­nem Zeit­punkt Zu­griff auf Ob­jek­te der Klas­se Listenknoten, son­dern er­laubt nur die Ma­ni­pu­la­ti­on der ge­sam­ten Liste mit­tels öf­fent­li­cher Me­tho­den5 . Das UML-Dia­gramm rechts zeigt eine Aus­wahl von Me­tho­den, die die Klas­se an­bie­ten könn­te. Alles, was sich die Lis­ten-Klas­se in ihrer ein­fachs­ten Form mer­ken muss, ist der erste Lis­ten­kno­ten, von dort aus kann sie alle an­de­ren Kno­ten er­rei­chen.

 

 

 

1 Bei­des sind auch ato­ma­re Typen, die in­tern auf Zah­len zu­rück­ge­führt wer­den.

2 Wo die Länge des Ar­rays ab­ge­legt wird, hängt von der Pro­gram­mier­spra­che ab. Bei C/C++ z.B. muss der Pro­gram­mie­rer noch selbst eine Va­ria­ble dafür an­le­gen und ver­wal­ten. In Java hängt es von der Java Vir­tu­al Ma­chi­ne ab, wo die Ar­ray­län­ge steht – für den Pro­gram­mie­rer ist sie je­doch stets als b.length ab­ruf­bar.

3 In Java wer­den int-Ar­rays stan­dard­mä­ßig mit Nul­len in­itia­li­siert. In an­de­ren Spra­chen wie z.B. C/C++ ist der Ar­ray­in­halt un­de­fi­niert, d.h. der vor­her dort vor­han­de­ne Wert bleibt ein­fach im Spei­cher. Für unser Bei­spiel ist das je­doch ir­re­le­vant.

4 Wir un­ter­schla­gen hier zur di­dak­ti­schen Re­duk­ti­on, dass die Werte im Array meist mehr als 1 Byte Spei­cher be­nö­ti­gen. ints be­nö­ti­gen z.B. meist 4 Byte. Die kor­rek­te Adres­se würde als „Adres­se des Ar­rays + Größe des Da­ten­typs * Index“ be­rech­net.

5 Java bie­tet das Sprach­kon­zept der in­ne­ren Klas­sen an, das hier nütz­lich sein könn­te. In­ne­re Klas­sen wer­den in­ner­halb einer an­de­ren („äu­ße­ren“) Klas­se de­fi­niert. Wenn ihre Sicht­bar­keit zudem auf pri­va­te oder pro­tec­ted ge­setzt wird, ist sie nur in­ner­halb ihrer um­ge­ben­den Klas­se bzw. deren Un­ter­klas­sen be­kannt. In­ne­re Klas­sen sind aber nicht Teil des Bil­dungs­plans.

 

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

 

Wei­ter zu Abs­trak­te Da­ten­ty­pen