Ackermannfunktion
Die Ackermannfunktion ist eine, 1926 von Wilhelm Ackermann erfundene, extrem schnell wachsende Funktion, mit deren Hilfe in der theoretischen Informatik Grenzen von Computer- und Berechnungsmodellenen aufgezeigt werden können. Heute gibt es eine ganze Reihe von Funktionen, die als Ackermannfunktion bezeichnet werden. Diese weisen alle ein ähnliches Bildungsgesetz wie die ursprüngliche Ackermannfunktion auf und haben ein ähnliches Wachstumsverhalten.
Entstehungsgeschichte
1926 vermutete David Hilbert, dass jede berechenbare Funktion primitiv-rekursiv ist. Vereinfacht gesprochen bedeutet dies, dass jede Funktion, die durch einen Computer berechnet werden kann, sich aus einigen wenigen sehr einfachen Regeln zusammensetzen lässt und die Dauer der Berechnung sich im Vorfeld abschätzen lässt. Dies trifft auf nahezu alle in der Praxis vorkommenden Funktionen zu. (Die Ausnahmen sind vorwiegend im Bereich des Compilerbaus zu finden.) Im gleichen Jahr konstruierte Ackermann eine Funktion, die diese Vermutung widerlegt, und veröffentlichte sie 1928. Diese Funktion wird heute ihm zu Ehren Ackermannfunktion genannt. Die Ackermannfunktion kann also von einem Computer in endlicher Zeit ausgewertet werden, ist aber nicht primitiv-rekursiv.
1955 konstruierte Rózsa Péter eine vereinfachte Version, die den gleichen Dienst erweist. Diese Funktion, gelegentlich auch als Ackermann-Péter-Funktion bezeichnet, wird heute vorwiegend benutzt.
Idee
Man betrachtet die Folge , , , Hierbei wird bei jedem Folgeglied die Operation des vorigen Folgeglieds mal auf angewandt, also ist gerade , wobei die Variable -mal vorkommt und so weiter. Die Idee hinter der Ackermannfunktion ist es, diese Folge als Funktion aufzufassen.
Die Ackermannfunktion ist also eine Funktion, die die folgenden Gleichungen erfüllt:
Ab der vierten Zeile können die Funktionswerte nicht mehr mit herkömmlichen Operatorenen formuliert werden; man braucht erweiterte Notationen wie beispielsweise den Hyper-Operator.
Die Ackermannfunktion definiert man üblicherweise rekursiv, d.h. man macht für einige Anfangswerte explizite Angaben und gibt eine Anleitung (das Rekursionsschema), wie man weitere Funktionswerte aus den bereits berechneten erhält.
Auf der Suche nach den Grenzen von Computern stößt man sehr schnell auf den Begriff der berechenbaren Funktionen. Das sind all die Funktionen, für die man einen Algorithmus angeben kann, mit anderen Worten alle Funktionen, die ein Computer berechnen kann.
Diese Definition stellt einen sehr schnell vor ein Problem, wenn man von einer konkreten Funktion entscheiden möchte, ob sie berechenbar ist. Findet man einen Algorithmus, der die Funktion berechnet, so ist sie offensichtlich berechenbar. Findet man aber keinen Algorithmus, so ist ungewiss ob die Funktion wirklich nicht berechenbar ist oder ob es zwar einen Algorithmus gibt, man ihn aber nicht gefunden hat.
Aus diesem Grund sucht man nach alternativen Definitionen, die einen solchen Nachweis einfacher führen lassen. Ein erster Ansatz hierfür waren die primitiv-rekursiven Funktionen. Dies sind Funktionen, die sich durch einige wenige Regeln aus sehr einfachen Funktionen zusammensetzen lassen.
Einige Zeit vermutete man, dass alle berechenbaren Funktionen primitiv-rekursiv sind, mit den primitiv-rekursiven Funktionen also ein Werkzeug zur Lösung des oben geschilderten Problems gefunden sei. Diese Hoffnung zerstörte die Ackermannfunktion, von der man nachweisen kann, dass sie berechenbar ist, aber nicht primitiv-rekursiv. (Siehe nachfolgenden Expertenabschnitt.)
Anmerkung: Inzwischen weiß man, dass man aus den primitiv-rekursiven Funktionen die berechenbaren erhält, wenn man noch eine weitere Konstruktionsregel, die sogenannte µ-Rekursion, zulässt.Definition und Varianten
Definition von Ackermann
Ackermann selbst definiert die Funktion auf recht umständliche Weise, gibt aber kurz darauf die folgende äquivalente Definition an.
Dabei ist ein weitere Funktion, die Ackermann nicht weiter beschreibt. (Sie liefert die Startwerte , , , .)Definition von Péter
Rózsa Péter definierte 1955 eine einfachere Version der Ackermannfunktion, die nur zwei Parameter besitzt und zudem ohne die Hilfsfunktion auskommt:
Auch hier meint man im Zusammenhang mit Wachstumsuntersuchungen oftmals die Funktion , wenn man von der Ackermannfunktion spricht.Bedeutung in der theoretischen Informatik
Wie eingangs schon erwähnt, erfand Ackermann diese Funktion als Beispiel einer Funktion, die nicht primitiv-rekursiv ist, aber berechenbar. Dies ist auch heute noch das wichtigste Anwendungsgebiet der Ackermannfunktion.
Expertenabschnitt: Dieser Abschnitt enthält tiefergehende Informationen, für die zum Teil zusätzliches Fachwissen notwendig ist. Beweisskizze zur Behauptung, dass die Ackermannfunktion nicht primitiv-rekursiv ist:
|
In diesem Zusammenhang wird die Ackermannfunktion gerne als Benchmark zur Überprüfung von rekursiven Prozeduraufrufenaufrufen benutzt, da ein Programm zur Berechnung der Ackermannfunktion im Wesentlichen nur aus solchen Prozeduraufrufen besteht. In der Definition von Péter wird ja nur direkt berechnet. Die Schwierigkeit bei der Berechnung der Funktionswerte sind also nicht allein deren Größe, sondern die tiefe Verschachtelung der Funktionsaufrufe, die leicht zu einem Stack Overflow führt, also dazu, dass dem System der Speicher ausgeht.
Diese Idee geht zurück auf Yngre Sundblad, der 1971 die Funktion benutzte um diverse Programmiersprachen zu vergleichen. Um zu berechnen, werden Aufrufe getätigt.
Sundblad testete unter anderem, wie groß n gewählt werden kann, damit der Computer noch in der Lage ist, diese Zahl zu berechnen. Damals erreichte er . Zum Vergleich hierzu: Mit Java 1.4.2 mit den Standardspeichereinstellungen erreicht man heutzutage .
Im Laufe der Berechnung werden viele identische Aufrufe mehrfach ausgerechnet. Ein intelligenter Compiler kann dies ausnutzen und die Ergebnisse zwischenspeichern, um solche Aufrufe nur einmal durchführen zu müssen. Damit waren schon 1971 Aufrufe bis durchführbar. Einen bedeutenden Zeitvorteil erhält man auch, wenn man direkt berechnet, statt es rekursiv zu zu expandieren. Die direkte Berechnung von erfordert lineare Zeit in . Die Berechnung von erfordert quadratische Zeit, denn sie führt zu (also für eine Konstante ) verschachtelten Aufrufen von für verschiedene . Die Berechnung von erfordert schließlich eine zu proportionale Zeit (; zur Erklärung der Groß-O-Notation siehe Landau-Symbole).
Anwendungen
Für die Ackermannfunktion gibt es nur sehr wenige Anwendnungen. Die zwei wichtigsten sind als Benchmarktest für rekursive Aufrufe in Programmiersprachen und für die Laufzeitabschätzung im Zusammenhang mit der Union-Find-Datenstruktur.Benchmark für rekursive Aufrufe
Bei der Einführung von neuen Programmiersprachen, Compilern und Computern möchte man deren Leistungsfähigkeit untersuchen. Eine Möglichkeit hierfür sind Benchmarktests, bei denen ein spezielles Programm benutzt wird, um bestimmte Eigenschaften zu überprüfen.Laufzeitabschätzung bei Union-Find
Da die Funktion sehr schnell wächst, wächst ihre Umkehrfunktion sehr langsam. Diese Umkehrfunktion taucht in der Laufzeitanalyseanalyse bestimmter Algorithmen auf, zum Beispiel beim Union-Find-Problem. In diesem und anderen Zusammenhängen wird die Ackermannfunktion oft leicht umdefiniert zu einer Funktion mit ähnlichem asymptotischem Verhalten. Diese modifizierten Funktionen sind nicht äquivalent zur Ackermannfunktion, aber nach den Maßstäben der Laufzeitanalyse können sie als äquivalent betrachtet werden. Die Umkehrfunktion der Funktion f ist für jede vorstellbare Eingabegröße kleiner als 4, kann also in der praktischen Analyse von Algorithmen als konstant betrachtet werden.Implementation
In der Programmiersprache C++ mit den Datenstrukturen der Standard Template Library lässt sich die Ackermannfunktion wie folgt ohne Rekursion implementieren:int ack(int x, int y)
{
std::stack<int> stack;
while (x > 0 || !stack.empty())
{
if (x == 0)
{
x = stack.top();
stack.pop();
y = y + 1;
}
else if (y == 0)
{
y = 1;
x = x - 1;
}
else
{
y = y - 1;
stack.push(x - 1);
}
}
return y + 1;
}
Die Rekursion wird dabei durch einen Stack simuliert.
n\\m | 0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | |
1 | 2 | 3 | 4 | 5 | 6 | |
2 | 3 | 5 | 7 | 9 | 11 | |
3 | 5 | 13 | 29 | 61 | 125 | |
4 | 13 | 65533 | ¹ | ( Terme) | ||
5 | 65533 | |||||
6 |
¹eine Zahl mit 19729 Dezimalstellen
Wir betrachten nun einen komplexeren Fall, nämlich , den ersten Funktionswert, der so groß ist, dass er praktisch nicht dezimal aufgeschrieben werden kann.
Das ist für einige Zeit der einfachste Fall einer solchen Expansion, und es ist nun offensichtlich, warum Funktionswerte wie dieser in der obigen Tabelle selten direkt berechnet werden. Es ist auch interessant festzustellen, wie viele Schritte nötig sind, um schon sehr einfach aussehende Ackermann-Ausdrücke zu vereinfachen. Jede Zeile im vorigen Beispiel ist eine einzige Anwendung eines der drei Teile der Definition der Ackermannfunktion. Wenn wir an dieser Stelle mehrere logische Schritte überspringen, könnten wir zu 13 auswerten und dann versuchen, auszuwerten – das ist 8179. Der nächste Funktionsaufruf, , liefert eine Zahl, die der Zahl der Atome im Universum entspricht, einige Dutzend mal mit sich selbst multipliziert. Diese Zahl wird schließlich in die Berechnung eingesetzt, der irgendwann zu einem Ausdruck der Form ausgeschrieben würde, der aber mit unseren Mitteln nicht mehr aufgeschrieben werden kann.
Der wirklich faszinierende Aspekt der Ackermann-Funktion ist, dass die einzige Berechnung, die neben den rekursiven Aufrufen tatsächlich auftaucht, die Berechnung von ist, die einfach um 1 erhöht.
1962 gab Tibor Radó mit der Busy-Beaver-Funktion (fleißiger Biber) eine noch stärker wachsende Funktion als die Ackermannfunktion an, die allerdings nicht mehr berechenbar ist.
Dieser Artikel befindet sich derzeit im Reviewprozess. Hilf mit, ihn zu verbessern! Genauere Betrachtung
Intuitiv ist die erste Zeile der Wertetabelle (genauer: die Werte von ) einfach eine Liste aller natürlichen Zahlen, und die angewandte mathematische Operation ist die Addition einer 1 zu . Alle folgenden Zeilen enthalten einfach Anweisungen, in dieser Zeile einen Wert zu suchen. Im Falle der Zeile vereinfacht sich diese Anweisung dahingehend, den Wert in der Zeile zu suchen, aber diese Vereinfachung ist schon etwas schwieriger herzuleiten – zum Beispiel:
a(1, 2) = a(0, a(1,1))
= a(0, a(0, a(1,0)))
= a(0, a(0, 2))
= a(0, 3)
= 4
a(4, 3) = a(3, a(4, 2))
= a(3, a(3, a(4, 1)))
= a(3, a(3, a(3, a(4, 0))))
= a(3, a(3, a(3, a(3, 1))))
= a(3, a(3, a(3, a(2, a(3, 0)))))
= a(3, a(3, a(3, a(2, a(2, 1)))))
= a(3, a(3, a(3, a(2, a(1, a(2, 0))))))
= a(3, a(3, a(3, a(2, a(1, a(1, 1))))))
= a(3, a(3, a(3, a(2, a(1, a(0, a(1, 0)))))))
= a(3, a(3, a(3, a(2, a(1, a(0, a(0, 1)))))))
= a(3, a(3, a(3, a(2, a(1, a(0, 2))))))
= a(3, a(3, a(3, a(2, a(1, 3)))))
= a(3, a(3, a(3, a(2, a(0, a(1, 2))))))
= a(3, a(3, a(3, a(2, a(0, a(0, a(1, 1)))))))
= a(3, a(3, a(3, a(2, a(0, a(0, a(0, a(1, 0))))))))
= a(3, a(3, a(3, a(2, a(0, a(0, a(0, a(0, 1))))))))
= a(3, a(3, a(3, a(2, a(0, a(0, a(0, 2))))))
= a(3, a(3, a(3, a(2, a(0, a(0, 3)))))
= a(3, a(3, a(3, a(2, a(0, 4)))))
= a(3, a(3, a(3, a(2, 5))))
Sonstiges
a(4,2)
Es sei noch festgehalten, dass der Wert a(4,2), der in Form einer 19727-stelligen Dezimalzahl auf verschiedenen Internetseiten auftaucht, wahrscheinlich nicht mit der rekursiven Definition der Ackermannfunktion praktisch berechnet werden kann.Die Busy-Beaver-Funktion
Literatur
Weblinks