Fortgeschrittene Algorithmen und Programmierung
Prof. Dr. Alexandra Mikityuk
HTW Berlin
NULL â und warum eine Kante âein Pfeil" istImmer wenn Dinge miteinander verbunden sind, habt ihr einen Graphen â auch ohne es zu merken.
Menschen = Knoten, Freundschaften = Kanten. âFreunde von Freunden" ist eine Graph-Frage.
Orte = Knoten, StraĂen = Kanten. Das Navi sucht den kĂŒrzesten Weg im Graphen.
Router = Knoten, Leitungen = Kanten. Datenpakete suchen sich einen Weg durch den Graphen.
Ein Graph besteht aus zwei Dingen: Knoten (die Punkte) und Kanten (die Verbindungen).
Die âDinge": hier A, B, C, D, E. Personen, Orte, Router âŠ
Die Verbindungen: AâB, AâC, BâC, BâD, CâE, DâE. âX ist mit Y verbunden."
Ungerichtet: Kante geht in beide Richtungen. âAnna ist mit Bob befreundet" = âBob mit Anna".
Gerichtet: Kante hat eine Richtung (ein Pfeil). âIch folge dir" auf Instagram ist nicht automatisch gegenseitig.
Ungewichtet: alle Kanten zĂ€hlen gleich (nur âverbunden ja/nein").
Gewichtet: jede Kante hat einen Wert â z. B. die LĂ€nge einer StraĂe in km, oder die Dauer in Minuten.
Auf Papier ist ein Graph schnell gemalt. Aber ein Computer kennt keine âPunkte und Striche" â er kennt Zahlen, Arrays und Speicheradressen. Wie also?
Eine Tabelle Knoten Ă Knoten (ein 2D-Array): âIst Knoten X mit Knoten Y verbunden? 1 oder 0." Das nutzt nur, was ihr schon kennt â Arrays.
| A | B | C | |
|---|---|---|---|
| A | 0 | 1 | 1 |
| B | 1 | 0 | 1 |
| C | 1 | 1 | 0 |
Eine Tabelle Knoten Ă Knoten. Steht eine 1 bei Zeile X, Spalte Y â es gibt eine Kante XâY. Sonst 0.
| A | B | C | D | E | |
|---|---|---|---|---|---|
| A | 0 | 1 | 1 | 0 | 0 |
| B | 1 | 0 | 1 | 1 | 0 |
| C | 1 | 1 | 0 | 0 | 1 |
| D | 0 | 1 | 0 | 0 | 1 |
| E | 0 | 0 | 1 | 1 | 0 |
// A=0, B=1, C=2, D=3, E=4
int graph[5][5] = {
{0,1,1,0,0}, // A ist mit B,C verbunden
{1,0,1,1,0}, // B mit A,C,D
{1,1,0,0,1}, // C mit A,B,E
{0,1,0,0,1}, // D mit B,E
{0,0,1,1,0} // E mit C,D
};
// "Sind B und D verbunden?" â graph[1][3] == 1 â
Bei groĂen Netzwerken hat fast jedes Feld eine 0 (= keine Kante). Die paar grĂŒnen Felder sind die echten Kanten. Der ganze leere Rest kostet trotzdem Speicher.
Die Matrix verschwendet Platz â fast nur Nullen. Bevor wir eine Lösung zeigen: ĂŒberlegt kurz selbst.
Wie könnte man einen Graphen speichern, sodass man nur die echten Kanten behĂ€lt â und den ganzen leeren Platz spart?
Statt einer riesigen Tabelle: was, wenn jeder Knoten einfach eine kleine Liste hĂ€tte â âauf diese Nachbarn zeige ich"?
A â B, C
B â A, C, D
C â A, B, E
D â B, E
E â C, D
Nur die echten Kanten, kein verschwendeter Platz.
Der Speicher (RAM) ist wie eine riesige StraĂe mit nummerierten HĂ€usern. Jede Variable wohnt in einem Haus mit einer Adresse.
Im Haus an Adresse 0x7f3a liegt der Wert 5. Das ist eure normale Variable int x = 5;.
Ein Zeiger speichert nicht den Wert, sondern die Adresse â wie ein Zettel, auf dem âgeh zu Haus 0x7f3a" steht. Eine Schatzkarte, kein Schatz.
& und *int x = 5; // normale Variable
int *p = &x; // p zeigt auf x
// ââ &x = "die Adresse von x"
printf("%d", *p); // â 5
// â *p = "der Wert, auf den p zeigt"
*p = 9; // Ă€ndert x ĂŒber den Zeiger!
printf("%d", x); // â 9
&x = âAdresse von x" (wo wohnt x?)
*p = âfolge dem Zeiger" (was liegt dort?) â heiĂt Dereferenzieren.
int *p beim Deklarieren = âp ist ein Zeiger". *p beim Benutzen = âfolge ihm".
*p = 9 haben wir x verĂ€ndert, ohne x ĂŒberhaupt zu nennen. Der Zeiger gibt uns Fernzugriff.
Bisher haben eure Funktionen mit Kopien gearbeitet â und das ist völlig richtig so. Eine Funktion, die etwas berechnet und zurĂŒckgibt, braucht keine Zeiger. Nur wenn eine Funktion die Variablen des Aufrufers selbst verĂ€ndern soll, reichen Kopien nicht.
void tausche(int a, int b) {
int t = a; a = b; b = t;
}
// a und b sind nur KOPIEN.
// DrauĂen Ă€ndert sich nichts.
void tausche(int *a, int *b) {
int t = *a; *a = *b; *b = t;
}
tausche(&x, &y); // Adressen geben
// jetzt sind x und y wirklich
// vertauscht â
scanf("%d", &x) aus VL2? Jetzt versteht ihr das &: scanf bekommt die Adresse von x, um den eingelesenen Wert dort hineinzuschreiben. Ihr habt Zeiger lÀngst benutzt!
NULLAchtung â zwei verschiedene Dinge: einen NULL-Zeiger zu haben ist völlig ok. Ihm zu folgen ist der Fehler.
int *p = NULL; heiĂt bewusst âzeigt gerade auf nichts". Ein gĂŒltiger, nĂŒtzlicher Zustand â z. B. als Ende einer Nachbarliste oder ânoch kein Ergebnis".
*p sagt âgeh zur Adresse und lies dort". Aber bei NULL gibt es keine Adresse â das Programm stĂŒrzt ab (Segmentation fault).
if (p != NULL) { *p ... } â bevor du dem Zeiger folgst, prĂŒfe, ob er ĂŒberhaupt irgendwohin zeigt. Bild: NULL = ein leerer Adress-Zettel â besitzen ist harmlos, aber zur âAdresse" auf einem leeren Zettel hinfahren knallt.
!= NULL-PrĂŒfung seid ihr auf der sicheren Seite.
Der Gedanke in drei Schritten: Kante AâB heiĂt âvon A erreiche ich B". â In C heiĂt âerreichen" = einen Zeiger haben. â Also speichert jeder Knoten Zeiger auf seine Nachbarn.
A âââș B âââș C âââș â
B âââș A âââș C âââș D âââș â
C âââș A âââș B âââș E âââș â
D âââș B âââș E âââș â
E âââș C âââș D âââș â
B âââș A âââș C âââș D. Einem Zeiger folgen = eine Kante entlanggehen. So âlĂ€uft" BFS spĂ€ter durch den Graphen: es folgt den Zeigern.
| Adjazenzmatrix (Array) | Adjazenzliste (Zeiger) | |
|---|---|---|
| Speicher | immer NĂN | nur echte Kanten |
| âXâY verbunden?" | sofort (O(1)) | Liste durchgehen |
| Nachbarn auflisten | ganze Zeile prĂŒfen (N) | direkt da |
| Gut fĂŒr | kleine / dichte Graphen | groĂe / dĂŒnne Graphen |
| In C | 2D-Array (kennt ihr) | Knoten mit Zeigern auf Nachbarn |
Zwei der hÀufigsten Fragen der Informatik:
âKomme ich von A ĂŒberhaupt zu E?" â Besuche alle Knoten, die von A aus erreichbar sind.
âWie wenige Schritte von A nach E?" â z. B. âwie viele Ecken bis zu diesem Kontakt?"
Wirf einen Stein ins Wasser: die Welle erreicht erst alle nahen Punkte, dann die nĂ€chst-ferneren. BFS erkundet den Graphen genau so â Ebene fĂŒr Ebene.
Start: A (Abstand 0)
A's Nachbarn: B, C (Abstand 1)
deren neue Nachbarn: D, E (Abstand 2)
Beim Erkunden entdeckt ihr laufend neue Knoten â besuchen könnt ihr aber immer nur einen nach dem anderen. Die ĂŒbrigen mĂŒsst ihr euch merken. Genau dafĂŒr die Queue: eine To-do-Liste der Knoten âentdeckt, aber noch nicht besucht".
Den nĂ€chsten Knoten von der Liste nehmen und ihn besuchen â d. h. seine Nachbarn anschauen. Danach ist er erledigt (raus). Darum ânimmt man heraus": der Knoten ist abgehakt.
Die dabei neu entdeckten Nachbarn ans Ende der Liste schreiben â sie sind noch zu erledigen und kommen spĂ€ter dran.
âŠdann dasselbe mit B (vorne raus, dessen neue Nachbarn hinten dran), dann C, ⊠bis die Queue leer ist.
Die Welle wĂ€chst â â Start, â Abstand 1, â Abstand 2:
| Schritt | nimm heraus | neue Nachbarn | Queue danach | besucht |
|---|---|---|---|---|
| Start | â | A | [A] | A |
| 1 | A | B, C | [B, C] | A,B,C |
| 2 | B | D (A,C schon besucht) | [C, D] | A,B,C,D |
| 3 | C | E (A,B schon besucht) | [D, E] | A,B,C,D,E |
| 4 | D | â (alle besucht) | [E] | A,B,C,D,E |
| 5 | E | â | [ ] | fertig â |
HĂ€ufige Frage: âWenn ich einen Knoten aus der Queue nehme â ist er dann nicht weg?" Nein: es gibt zwei Listen mit zwei Aufgaben. Schaut, was bei einem Schritt passiert (A wird bearbeitet):
queue und ein Array besucht[]. dequeue Àndert nur die queue; besucht[A] bleibt gesetzt.
Nebenbei liefert BFS etwas Wertvolles gratis â den kĂŒrzesten Weg (in Schritten). Aber warum eigentlich?
BFS geht Welle fĂŒr Welle: erst alle Knoten in Abstand 1, dann alle in Abstand 2 ⊠Ein Knoten wird also genau in der Welle entdeckt, die seinem Abstand entspricht.
GĂ€be es einen kĂŒrzeren Weg, hĂ€tte ihn eine frĂŒhere Welle schon erreicht. Also ist das erste Ankommen automatisch das kĂŒrzeste.
Beispiel E: kein direkter Nachbar von A â kann frĂŒhestens in Welle 2 auftauchen. BFS findet E genau dort (AâCâE) â 2 Schritte sind das Minimum. â
Wie viele Ecken bis zu einer fremden Person? BFS im Freundes-Graphen. (BerĂŒhmt: ~6 Schritte zu jedem Menschen.)
Jede Zelle = Knoten, Nachbarzellen = Kanten. BFS findet den kĂŒrzesten Weg zum Ausgang.
Wenigste âHops" von einem Rechner zum anderen â wieder eine BFS-Frage.
BFS sagt ânimm den Weg mit den wenigsten Kanten". Aber das ist nicht immer der schnellste.
BFS nimmt AâB: nur 1 Kante â aber Kosten 10.
KlĂŒger: AâCâB ĂŒber 2 Kanten, aber nur 3 + 4 = 7. Der Weg mit mehr Knoten ist hier der gĂŒnstigere!
BFS zĂ€hlt nur Kanten â nicht ihr Gewicht. Deshalb wĂ€hlt es falsch.
*p in C?*p heiĂt âdereferenzieren" â folge der Adresse in p und hol den Wert dort. Die Adresse von etwas bekommt man dagegen mit &.
&x = Adresse von x · *p = Wert, auf den p zeigt · NULL = zeigt auf nichts.Eine Kante ist ein Pfeil. đžïž
Prof. Dr. Alexandra Mikityuk
HTW Berlin · BĂŒro Raum 308
NĂ€chste Woche: Dijkstra â der kĂŒrzeste Weg