Vorlesung Datenstrukturen ADT Stapel Verwendung von Stapeln Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 304 Wiederholung - Stapel Stapel Ein Stapel (auch als Keller oder Stack bezeichnet) ist eine Datenstruktur, die eine Menge von Daten aufnehmen und in umgekehrter Reihenfolge wieder abgeben kann. Funktionen Eine Stapel-Datenstruktur kennt im Wesentlichen nur zwei Operationen: push Ablegen eines Elements auf dem Stapel pop Entnehmen des zuletzt abgelegten Elements vom Stapel Prinzip Das zuletzt eingefügte Element wird als erstes wieder entnommen. Diese Art des Vorgehens bezeichnet man auch als LIFO-Prinzip (Last In, First Out). Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 305 Schnittstellendefinition Wunsch Es soll ein Stapel zur Verarbeitung von int-Werten definiert werden. Schnittstelle class Stack { Stack() // Konstruktor ~Stack() // Destruktor bool isEmpty() // Test auf leeren Stapel void push(int data) // Ablage eines int-Werts int // Entnehmen eines int-Werts pop() } Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 306 Implementierung Auswahl der internen Datenstruktur Wir könnten uns z.B. zwischen einer statischen Variante mittels Feld oder einer dynamischen Implementierung mittels Liste entscheiden. Eine dynamische Datenstruktur erscheint adäquater, da eine typische Eigenschaft eines Stapels die Speicherung einer beliebigen Elementanzahl ist und wir auf diesem Weg zudem nur den tatsächlich benötigten Speicher belegen. Algorithmuskriterien Wir müssen jederzeit Zugriff auf das oberste Element haben und nach dessen Entnahme auf das zweitoberste zugreifen können usw. Bei Ablage eines neuen Elementes auf dem Stapel muss deshalb sichergestellt werden, dass eine Verbindung zwischen dem neuen Element und dessen Vorgänger existiert. Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 307 ADT Stack - privater Teil class Stack { // Stapel für int-Werte private: struct node { int // interne Realisierung des Stapels mit Liste data; node* next; }; node* head; Dr. Frank Seifert // ADT „kennt“ das Kopfelement der Liste Vorlesung Datenstrukturen - Sommersemester 2016 Folie 308 Stack - Konstruktor & Destruktor public: Stack() { // Konstruktor erzeugt einen head = 0; // leeren Stapel } ~Stack() { // Destruktor löscht alle Elemente, node* temp = head; // die sich evtl. noch im Stapel while (temp) { // befinden und gibt den Speicher frei head = head->next; delete temp; temp = head; } } Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 309 ADT Stack - Methoden bool isEmpty() { // Test auf leeren Stapel return (head == 0); } void push(int data) { // Ablegen eines int-Elements node* temp = new node(); // Prinzip: Einfügen am Listenanfang temp->data = data; temp->next = head; head = temp; } Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 310 ADT Stack - Methoden int pop() { // Entnahme eines int-Elements if (isEmpty()) { printf("Fehler! Entnahme von leerem Stapel!"); return 0; // besser wäre ein Programmabbruch! } else { // Entnahme am Listenkopf node* temp = head; int data = head->data; head = head->next; delete temp; return data; } } }; Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 311 Bewertung: Stapel als ADT Vorteile Mit Hilfe des vorgestellten abstrakten Datentyps „Stapel“ kann eine Menge von Elementen mit lediglich drei Operationen (push, pop und isEmpty) verarbeitet werden. Der ADT abstrahiert so auf einfachem Weg die zugrundeliegende komplexe und fehleranfällige Listenverarbeitung (bei Listenimplementierung des Stapels) sowie die nötige Freispeicherverwaltung. Falls erforderlich, könnte zu einem späteren Zeitpunkt eine andere Implementierung erfolgen, ohne dass der Nutzer des ADTs „Stapel“ dies bemerkt. Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 312 Templates Potentielles Problem Mit den bisher kennengelernten ADT-Konzept müssten wir für Stapel verschiedener Elementtypen (z.B. double oder char) jeweils einen vollständigen individuellen ADT entwickeln (was zwar nur die jeweilige Ersetzung des Elementtyps verlangen würde, aber aufwändig und redundant erscheint). Abhilfe Mit dem Template-Konzept von C++ können gleichartige Operationen auf verschiedenen Datentypen nach den Vorgaben einer „Schablone“ automatisch implementiert werden. Syntax template <class Name> Anwendung und Funktionsweise Jedes Vorkommen eines generischen Datentyps wird durch Name ersetzt. Beim Aufruf einer Template-Funktion bestimmt der Compiler anhand des Typs der Parameter, welche Funktion generiert und ausgeführt werden muss ➔ Weitere Details siehe Sprachbeschreibung von C++. Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 313 Verwendung von Stapeln Test auf korrekte Klammerung Addition großer Zahlen Wertbestimmung arithmetischer Ausdrücke Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 314 Test auf korrekte Klammerung (1) Situation Es existieren verschiedene Klammerformen in C, z.B. ( ) [ ] { }. Diese können kombiniert und (nach gewissen Regeln) auch verschachtelt auftreten. Es soll vereinfachend für runde, eckige und geschweifte Klammerungen getestet werden, ob eine C-Anweisung korrekt geklammert ist, d.h. ob einer öffnenden immer eine schließende Klammer gleichen Typs folgt. Beispiele korrekte geklammerte Anweisungen: fehlerhafte geklammerte Anweisungen: a = b + (c - d) * (e - f); a = b + (c - d) * (e - f)); g[1] = h[i[0]] + (j + k) * l; g[1] = h[i[0]] + j + k) * l; while (m < (n[2] + o)) { p = 3; r = 4; } while (m < (n[2) + o]) { p = 3; r = 4; } Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 315 Test auf korrekte Klammerung (2) Gegeben • Zeichenkette, die die zu untersuchende C-Anweisung enthält • ADT Stapel vom Typ charStack (analog intStack, nur mit char als Elementtyp) Gesucht Funktion, die bei korrekter Klammerung true und sonst false als Funktionswert zurückliefert. Algorithmusbedingung Für einen korrekt geklammerten Ausdruck KA muss gelten: Jeder öffnenden Klammer folgt die korrespondierende schließende Klammer oder wiederum ein korrekter Klammerausdruck KA: KA = ( KA ) | [ KA ] | { KA } | KA KA | NKA NKA = NKA Nichtklammersymbol | ε Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 316 Algorithmusidee Für jedes Zeichen c der Eingabezeichenkette untersuchen wir: • ist c kein Klammersymbol, dann ignorieren wir es • ist c eine öffnende Klammer (also (, [ oder {), dann legen wir c auf dem Stapel ab • ist c eine schließende Klammer (also ), ] oder }) - dann muss c zur zuletzt auf dem Stapel abgelegten öffnenden Klammer korrespondieren - deshalb entnehmen wir das zuletzt auf dem Stapel abgelegte Zeichen und vergleichen es mit c - stimmen die beiden Zeichen nicht überein, dann liegt eine fehlerhafte Klammerung vor und wir geben eine entsprechende Fehlermeldung aus Sind alle Zeichen der Eingabezeichenkette abgearbeitet, darf auf dem Stapel keine Klammer mehr abgelegt sein, d.h. wenn der Stapel nach der Abarbeitung keine Elemente mehr enthält, dann ist die Eingabezeichenkette korrekt geklammert. Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 317 Quelltext bool delimiterMatching(char* str) { Stack s; // Stack mit Elementtyp char char c; while (c = *(str++)) { // Iteration über Zeichenkette if (c=='(' || c=='[' || c=='{') // Öffnende Klammern? s.push(c); else if (c==')' || c==']' || c=='}') {// Schließende Klammern? if (c==')') c = '('; else if (c==']') c = '['; else if (c=='}') c = '{'; if (c != s.pop()) // Keine korrespondierenden Klammern return false; } } return s.isEmpty(); Dr. Frank Seifert // Stapel muss am Ende leer sein Vorlesung Datenstrukturen - Sommersemester 2016 Folie 318 Verwendung von Stapeln Test auf korrekte Klammerung Addition großer Zahlen Wertbestimmung arithmetischer Ausdrücke Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 319 Addition großer Zahlen (1) Aufgabe Es sollen zwei beliebig große natürliche Zahlen miteinander addiert werden und das exakte Ergebnis der Addition ausgegeben werden. Für diese Aufgabe stehen keine vordefinierten arithmetischen Datentypen zur Verfügung, da deren Wertebereich zu gering für eine exakte Rechenoperation wäre. Lösungsidee Wir algorithmieren die „schriftliche“ Addition zweier Zahlen. Das heißt, wir addieren beginnend mit den letzten Stellen die jeweils bezüglich Zehnerpotenzen korrespondierenden Ziffern der beiden Zahlen. Beispiel 65 + 943 65 +943 Merke 1 Dr. Frank Seifert 65 +943 8 Merke 1 65 +943 08 Merke 1 100 65 +943 008 Merke 1 Vorlesung Datenstrukturen - Sommersemester 2016 1000 65 +943 1008 Merke 1 1008 Folie 320 Addition großer Zahlen (2) Algorithmusidee • Einlesen der Ziffern der ersten Zahl und Ablegen der Ziffern auf dem ersten Stapel • Einlesen der Ziffern der zweiten Zahl und Ablegen der Ziffern auf dem zweiten Stapel • Solange sich auf mindestens einem Stapel eine Ziffer befindet - Entnimm aus jedem Stapel eine Ziffer (bei leerem Stapel verwende Ziffer 0) - addiere die Ziffern und einen evtl. gemerkten Übertrag - lege die letzte Stelle des Ergebnisses auf einem dritten Stapel ab - Merke einen möglichen Übertrag • Hole die Ziffern vom dritten Stapel und gib sie aus Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 321 Addition großer Zahlen (2) void bigAddition(char str[]){ // Eingabezeichenkette in Form Zahl1+Zahl2 int i = 0; Stack s1, s2, s3; // Stack mit Elementtyp int while (str[i] && str[i] != '+') s1.push(str[i++] - '0'); if (str[i] == '+') i++; while (str[i]) s2.push(str[i++] - '0'); int value1, value2, sum, memory = 0; while ((!s1.isEmpty()) || (!s2.isEmpty())) { if (s1.isEmpty()) value1 = 0; else value1 = s1.pop(); if (s2.isEmpty()) value2 = 0; else value2 = s2.pop(); sum = value1 + value2 + memory; memory = sum / 10; s3.push(sum % 10); } if (memory) s3.push(memory); while (!s3.isEmpty()) printf("%d", s3.pop()); } Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 322 Verwendung von Stapeln Test auf korrekte Klammerung Addition großer Zahlen Wertbestimmung arithmetischer Ausdrücke Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 323 Darstellung von Ausdrücken Infixnotation Bezeichnet die Notation eines Ausdrucks mittels Operand Operator Operand, wobei ein Operand rekursiv wieder analog definiert sein kann. Um eine eindeutige Ausführungsreihenfolge der Operatoren festzulegen, muss diese durch Klammerung erzwungen werden. Deshalb ist ein vollständiger Infixausdruck als ( Operand Operator Operand ) definiert, wobei ein möglicher Infixausdruck als Operand selbst wieder vollständig sein muss. Postfixnotation Hierbei folgt der Operator den Operanden, also Operand Operand Operator Der Vorteil der Postfixdarstellung besteht darin, dass bei geschachtelten Ausdrücken die Position des Operators die Berechnungsreihenfolge bestimmt, wodurch generell keine Klammern mehr nötig sind. Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 324 Berechnung von Ausdrücken Wunsch Wir wollen den Wert arithmetischer Ausdrücke berechnen. Vereinfachung Berechnung von Postfixausdrücken, da hierbei keine Klammerungen zu berücksichtigen sind. Wie erhält man Postfixausdrücke Nach der Vorstellung des Algorithmus zur Berechnung von Postfixausdrücken stellen wir einen Algorithmus vor, der vollständig geklammerte Infixausdrücke in Postfixausdrücke umwandelt. Realisierung Für beide Aufgaben sind Stapel prädestiniert. Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 325 Berechnung eines Postfixausdrucks Allgemeiner Algorithmus Solange die Eingabe nicht leer ist Lese Eingabe e Wenn e ein Operand o ist, dann lege o auf dem Stapel ab Wenn e ein Operator p ist, dann hole die letzten beiden Operanden o1 und o2 vom Stapel berechne o1 p o2 lege das Berechnungsergebnis auf dem Stapel ab Das letzte Element auf dem Stapel ist das Ergebnis des Postfixausdrucks Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 326 Berechnung ganzzahliger Postfixausdrücke int postfix(char p[]) { // Postfixausdruck, Leerzeichen trennt Operanden Stack s; int i = 0, v1, v2; bool flag; while (p[i]) { // Stack mit Elementtyp int flag = true; if (p[i] == '+') s.push(s.pop() + s.pop()); // kommutativer Operator if (p[i] == '*') s.push(s.pop() * s.pop()); // kommutativer Operator if (p[i] == '-') { v2 = s.pop(); v1 = s.pop(); s.push(v1 - v2); } if (p[i] == '/') { v2 = s.pop(); v1 = s.pop(); s.push(v1 / v2); } if ((p[i] >= '0') && (p[i] <= '9')) { s.push(0); flag = false; } while ((p[i] >= '0') && (p[i] <= '9')) s.push(10 * s.pop() + (p[i++] - '0')); if (flag) i++; } return s.pop(); } Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 327 Konvertieren von Infix nach Postfix Wir haben gesehen, wie wir mit Hilfe eines Stapels einen Postfixausdruck auswerten können. Wir wollen nun einen Infix- in einen Postfixausdruck umwandeln. Dazu sei ein vollständiger, d.h. vollständig geklammerter Infixausdruck wie folgt gegeben: Infixausdruck = ( Operand Operator Operand ) Operand = Infixausdruck | Zahl Algorithmusidee Für jedes gelesene Zeichen c des Infixausdrucks: • Wenn c ein Operator ist, dann lege c auf dem Stapel ab • Wenn c ein Operand ist, dann gib c aus • Wenn c eine öffnende Klammer ist, dann ignoriere c • Wenn c eine schließende Klammer ist, dann entnimm Operator vom Stapel und gib ihn aus Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 328 Infix-Postfix-Konvertierung - Quelltext void infix2postfix(char e[]) {// Infix- wird mit Postfixausdruck überschrieben Stack s; // Stack mit Elementtyp char int pi = 0; // Postfixindex int ii = 0; // Infixindex while (e[ii]) { if (e[ii] == ')') e[pi++] = s.pop(); if ((e[ii]=='+') || (e[ii]=='*') || (e[ii]=='-') || (e[ii]=='/')) s.push(e[ii]); if ((e[ii] >= '0') && (e[ii] <= '9')) e[pi++] = e[ii]; ii++; } e[pi] = '\0'; // Endmarkierung der Zeichenkette } Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 329 Ende der Vorlesung Dr. Frank Seifert Vorlesung Datenstrukturen - Sommersemester 2016 Folie 330