Technischer Neustart: Verwendung von Zensical statt MkDocs
This commit is contained in:
BIN
docs/grundlagen/img/ascii.png
Normal file
BIN
docs/grundlagen/img/ascii.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/grundlagen/img/feld.png
Normal file
BIN
docs/grundlagen/img/feld.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/grundlagen/img/feld2d.png
Normal file
BIN
docs/grundlagen/img/feld2d.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/grundlagen/img/hanoi.jpg
Normal file
BIN
docs/grundlagen/img/hanoi.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
BIN
docs/grundlagen/img/string.png
Normal file
BIN
docs/grundlagen/img/string.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/grundlagen/img/vigenere.png
Normal file
BIN
docs/grundlagen/img/vigenere.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
8
docs/grundlagen/index.md
Normal file
8
docs/grundlagen/index.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# I. Grundlagen
|
||||
|
||||
Der vorliegende Bereich fasst alle Themen zusammen, die grundlegend für die Arbeit mit den Zentralabiturklassen in der Qualifikationsphase sind.
|
||||
|
||||
- [Java Grundlagen](javagrundlagen.md)
|
||||
- [Suchen und Sortieren](suchensortieren.md)
|
||||
- [Kryptologie](kryptologie.md)
|
||||
- [Rekursion](rekursion.md)
|
||||
501
docs/grundlagen/javagrundlagen.md
Normal file
501
docs/grundlagen/javagrundlagen.md
Normal file
@@ -0,0 +1,501 @@
|
||||
# 1. Java-Grundlagen
|
||||
|
||||
Das vorliegende Kapitel fast wesentliche Java Grundlagen zusammen, die im Laufe der Einführungsphase behandelt werden. Einige ausgewählte Themen wie Zeichenketten oder Felder werden im Rahmen der beiden Kapitel [Suchen und Sortieren](suchensortieren.md) sowie [Kryptologie](kryptologie.md) vertiefend aufgegriffen.
|
||||
|
||||
|
||||
|
||||
## Kontrollstrukturen
|
||||
|
||||
Kontrollstrukturen beeinflussen des Verlauf eines Programms. Die nachfolgenden Beispiele sind an das Greenfoot-Szenario "Mars-Rover" angelehnt, können aber auch ohne Kenntnis des Szenarios nachvollzogen werden.
|
||||
|
||||
|
||||
### Bedingte Anweisung
|
||||
|
||||
Die bedingte Anweisung überprüft den Wahrheitswert einer Bedingung. Ist die Bedingung erfüllt, so wird der erste Anweisungsbock ausgeführt, ansonsten der zweite mit *else* gekennzeichnete Anweisungsblock.
|
||||
|
||||
#### Zweiseitige Auswahl
|
||||
|
||||
Werden beide Fälle aufgeführt, so sprechen wir von *zweiseitiger Auswahl*.
|
||||
|
||||
```java
|
||||
if (huegelVorhanden("vorne")) {
|
||||
drehe("rechts");
|
||||
}
|
||||
else {
|
||||
fahre();
|
||||
}
|
||||
```
|
||||
|
||||
#### Einseitige Auswahl
|
||||
|
||||
Verzichten wir auf den *else*-Fall, so sprechen wir von *einseitiger Auswahl*.
|
||||
|
||||
```java
|
||||
if (huegelVorhanden("vorne")) {
|
||||
drehe("rechts");
|
||||
fahre();
|
||||
}
|
||||
```
|
||||
|
||||
#### Mehrseitige Auswahl
|
||||
|
||||
Gibt es mehr als zwei gleichwertige Alternativen, so kommt entsprechend die *mehrseitige Auswahl* zur Anwendung.
|
||||
|
||||
```java
|
||||
if (huegelVorhanden("vorne")) {
|
||||
drehe("rechts");
|
||||
}
|
||||
else if (huegelVorhanden("rechts")) {
|
||||
drehe("links");
|
||||
}
|
||||
else {
|
||||
fahre();
|
||||
}
|
||||
```
|
||||
|
||||
#### switch-Anweisung
|
||||
|
||||
Kann der weitere Verlauf vom Wert eines primitiven Datentyps oder vom Wert einer Zeichenkette abhängig gemacht werden, so steht als übersichtliche Alternative zur Mehrfachauswahl auch die switch-Anweisung bereit.
|
||||
|
||||
Die *break*-Anweisungen sorgen dafür, dass der Programmablauf nach der Abarbeitung eines Falls nach der *switch*-Anweisung fortgesetzt wird. Der optionale *default*-Fall tritt ein, wenn keiner der vorherigen *case*-Fälle zutrifft.
|
||||
|
||||
```java
|
||||
switch (anzahlMeinerSchritte) {
|
||||
case 0: { fahre();
|
||||
break;
|
||||
}
|
||||
case 1: { drehe("links");
|
||||
break;
|
||||
}
|
||||
default: { drehe("rechts");
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Schleifen
|
||||
|
||||
Zur wiederholten Ausführung einer oder mehrerer Anweisungen werden Schleifen verwendet.
|
||||
|
||||
#### while-Schleife
|
||||
|
||||
Die while-Schleife wiederholt eine oder mehrere Anweisungen so lange, bis eine gegebene Bedingung nicht mehr erfüllt ist. Da die Bedingung vor dem Anweisungsblock überprüft wird ("vorprüfende Schleife"), ist es möglich, dass der Anweisungsblock gar nicht ausgeführt wird.
|
||||
|
||||
```java
|
||||
while (!huegelVorhanden("vorne")) {
|
||||
fahre();
|
||||
}
|
||||
```
|
||||
|
||||
#### do-while-Schleife
|
||||
|
||||
Die do-while-Schleife wiederholt eine oder mehrere Anweisungen so lange, bis eine gegebene Bedingung nicht mehr erfüllt ist. Da die Bedingung hier erst nach dem Anweisungsblock überprüft wird ("nachprüfende Schleife"), wird der Anweisungsblock mindestens einmal ausgeführt.
|
||||
|
||||
```java
|
||||
do {
|
||||
setzeMarke();
|
||||
fahre();
|
||||
}
|
||||
while (!huegelVorhanden("vorne"));
|
||||
```
|
||||
|
||||
#### Zählschleife
|
||||
|
||||
In der Zählschleife wird eine Zählvariable mit einem Anfangswert belegt. Der zugehörige Anweisungsblock wird solange ausgeführt wie eine Schleifenbedingung erfüllt ist. In der Regel wird diese Bedingung von der Zählvariable selbst abhängen. Zusätzlich wird durch die dritte Komponente des Schleifenkopfes die Zählvariable nach jedem Durchlauf verändert, d.h. meist um eins erhöht oder erniedrigt.
|
||||
|
||||
Das folgende Beispiel sorgt dafür, dass die *fahre()*-Anweisung genau vier Mal ausgeführt wird:
|
||||
|
||||
```java
|
||||
for (int i=0; i<4; i++) {
|
||||
fahre();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Primitive Datentypen
|
||||
|
||||
Im Unterricht verwenden wir die primitiven Datentypen *int*, *double*, *char* und *boolean*, die wir vereinfachend als ganze Zahlen, Kommazahlen, Buchstaben und Wahrheitswerte beschreiben.
|
||||
|
||||
Datentyp | Wertebereich | Beispiele
|
||||
--- | --- | ---
|
||||
int | -2147483648 bis + 2147483647 | -1, 1
|
||||
double | 4.9E-324 bis 1.8E308 | -3.14, 0, 6.28
|
||||
char | alle Zeichen | 'A', 'Z'
|
||||
boolean | true, false | true, false
|
||||
|
||||
|
||||
### Deklaration und Initialisierung
|
||||
|
||||
Soll eine Variable eines primitiven Datentyps verwendet werden, so ist sie zunächst zu *deklarieren*, anschließend zu *initialisieren*, d.h. mit einem Anfangswert zu belegen.
|
||||
|
||||
```java
|
||||
int zahl; // Deklaration
|
||||
zahl = 0; // Initialisierung
|
||||
```
|
||||
|
||||
Die beiden Schritte können auch kompakt zu einem zusammengefasst werden.
|
||||
|
||||
```java
|
||||
int zahl = 0; // Deklaration + Initialisierung
|
||||
```
|
||||
|
||||
|
||||
### Vergleichsoperatoren
|
||||
|
||||
Variablen des gleichen primitiven Datentyps lassen sich mit den Operatoren <, >, <=, >=, == und != vergleichen. Man beachte den Vergleichsoperator ==, der zur besseren Abgrenzung gegenüber dem Zuweisungssymbol = mit zwei Gleichheitszeichen geschrieben wird.
|
||||
|
||||
```java
|
||||
int zahl1 = 1;
|
||||
int zahl2 = 11;
|
||||
|
||||
if (zahl1 < zahl2) {
|
||||
zahl1 = zahl1 + 10;
|
||||
}
|
||||
if (zahl1 == zahl2) {
|
||||
zahl1 = zahl1 + 100;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Konvertieren von Datentypen
|
||||
|
||||
Primitive Datentypen können ineinander konvertiert werden. In den meisten Fällen erfolgt die Konvertierung recht intuitiv - so wird eine double-Zahl in eine int-Zahl einfach durch Abschneiden der Nachkommastellen verwandelt. Die Verwandlung selbst wird durch den sogenannten *type cast*-Operator angezeigt, der den gewünschten Datentypen in Klammern vor die zu konvertierende Variable setzt:
|
||||
|
||||
```java
|
||||
double zahl = 1.47484;
|
||||
int zahl2 = (int) zahl;
|
||||
```
|
||||
|
||||
Interessant ist die Verwandlung von Buchstaben (char) in ganze Zahlen (int) und umgekehrt. Dieser Transfer richtet sich nach der Nummerierung aller bekannten Zeichen gemäß der genormten ASCII-Tabelle (vgl. [Wikipedia-Artikel](https://de.wikipedia.org/wiki/American_Standard_Code_for_Information_Interchange)). Er weist beispielsweise dem Großbuchstaben A eine 65, B eine 66 usw. zu (Bildausschnitt aus [WikiPedia](https://commons.wikimedia.org/wiki/File:Ascii-codes-table.png)).
|
||||
|
||||

|
||||
|
||||
Diese Festlegung kann geschickt für Veränderungen von Buchstaben genutzt werden. Das folgende Beispiel etwa verschiebt den gegebenen Buchstaben H um vier Stellen im Alphabet.
|
||||
|
||||
```java
|
||||
char c = 'H';
|
||||
int zahl = (int) c;
|
||||
zahl = zahl + 4;
|
||||
c = (char) zahl;
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Klassen und Objekte
|
||||
|
||||
|
||||
### Java-Klassengerüst
|
||||
|
||||
Klassen werden in Java mit dem Schlüsselwort *class* eingeleitet. Bei der Modellierung einer Klasse wird festgehalten, welche Attribute (Eigenschaften) und Methoden (Anfragen oder Aufträge) für die Klasse sinnvoll sind.
|
||||
|
||||
Im folgenden Beispiel wird für eine Klasse *Schueler* festgelegt, dass die Attribute Alter und Name verwendet werden. Beide bekommen durch den Konstruktor - eine besondere Methode mit dem gleichen Namen wie die Klasse - bei der Erzeugung eines Objekts der Klasse bereits saubere Anfangswerte. Zudem sind beide Attribute durch die beiden set-Methoden jederzeit veränderbar und die beiden get-Methoden jederzeit abfragbar.
|
||||
|
||||
```java
|
||||
public class Schueler {
|
||||
|
||||
private String name;
|
||||
private int alter;
|
||||
|
||||
public Schueler(String name, int alter) {
|
||||
this.name = name;
|
||||
this.alter = alter;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setAlter(int alter) {
|
||||
this.alter = alter;
|
||||
}
|
||||
|
||||
public int getAlter() {
|
||||
return alter;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Erzeugen von Objekten
|
||||
|
||||
Die Syntax zum Erzeugen eines Objekts sieht die Verwendung des Schlüsselworts *new* vor. Greifen wir das obige Beispiel der Klasse *Schueler* wieder auf, so müssen bei der Erzeugung eines Objekts der Klasse *Schueler* bereits Werte für die beiden Attribute übergeben werden.
|
||||
|
||||
```java
|
||||
Schueler s = new Schueler("Otto", 15); // Objekt erzeugen
|
||||
|
||||
String z = s.getName(); // Namen abfragen
|
||||
|
||||
int a = s.getAlter(); // Alter abfragen
|
||||
s.setAlter(a+1); // und neu besetzen
|
||||
```
|
||||
|
||||
|
||||
### Vererbung
|
||||
|
||||
Bei der Vererbung stehen der Unterklasse alle Attribute und Methoden der Oberklasse zur Verfügung. Auch der geerbte Konstruktor kann dabei zur Verwendung kommen.
|
||||
|
||||
Im folgenden Beispiel wird einem Oberstufenschüler gegenüber einem Schüler zusätzlich das Attribut *Tutor* zugeordnet. Dieses Attribut kann hier zwar abgefragt, aber nach der Erzeugung nicht mehr verändert werden.
|
||||
|
||||
Zusätzlich demonstriert diese Klasse mit der Methode *setName()*, wie eine Methode überschrieben werden kann, ohne die bestehende vererbte Methoden komplett neu gestalten zu müssen. Auch die Methode *erhoeheAlter()* ist geschickt gelöst, da das Alter erhöht wird, ohne direkt auf das Attribut *alter* zugreifen zu müssen. (*Bemerkung:* Eine Unterklasse kann nur dann direkt auf ein Attribut einer Oberklasse zugreifen, wenn die Sichtbarkeit des Attributs statt *private* auf *protected* gesetzt ist.)
|
||||
|
||||
```java
|
||||
public class Oberstufenschueler extends Schueler {
|
||||
|
||||
private String tutor;
|
||||
|
||||
public Oberstufenschueler(String name, int alter, String tutor) {
|
||||
super(name, alter);
|
||||
}
|
||||
|
||||
public String getTutor() {
|
||||
return tutor;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
if (name.length()==0) {
|
||||
super.setName("unbekannt");
|
||||
}
|
||||
else {
|
||||
super.setName(name);
|
||||
}
|
||||
}
|
||||
|
||||
public void erhoeheAlter() {
|
||||
int alter = getAlter();
|
||||
alter++;
|
||||
setAlter(alter);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Zeichenketten
|
||||
|
||||

|
||||
|
||||
Zeichenketten sind eine Aneinanderreihung von Zeichen, deren Anzahl beliebig ist. Eine Zeichenkette kann jederzeit verändert werden. In Java wird eine Zeichenkette durch ein Objekt der Klasse *String* repräsentiert.
|
||||
|
||||
|
||||
### Durchlaufen einer Zeichenkette
|
||||
|
||||
Um eine Zeichenkette mit einer Zählschleife zu durchlaufen, wird zunächst die Länge der Zeichenkette mit der Methode *length()* abgefragt. Die Zählschleife durchläuft nun die Positionen *0, 1, 2, ..., Länge-1* der Zeichenkette und greift per *charAt()* auf den i-ten Buchstaben zu. Die Nummerierung mit 0 beginnend ist anfangs ungewohnt, sie wird aber in Java konsequent auch in anderen Bereichen (z.B. bei Feldern) durchgeführt.
|
||||
|
||||
```java
|
||||
String eingabe = "TEST";
|
||||
|
||||
for (int i=0; i<eingabe.length(); i++) {
|
||||
char c = eingabe.charAt(i);
|
||||
System.out.println("Das Zeichen Nr. "+i+"ist: "+c);
|
||||
}
|
||||
```
|
||||
|
||||
Im Beispiel ergeben sich demnach folgende Ausgaben auf dem Bildschirm:
|
||||
|
||||
```
|
||||
Das Zeichen Nr. 0 ist: T
|
||||
Das Zeichen Nr. 1 ist: E
|
||||
Das Zeichen Nr. 2 ist: S
|
||||
Das Zeichen Nr. 3 ist: T
|
||||
```
|
||||
|
||||
|
||||
### Durchlaufen und Verändern einer Zeichenkette
|
||||
|
||||
Im folgenden Beispiel wird die Zeichenkette *TEST* als Eingabe von vorn nach hinten durchlaufen und für die Ausgabe jeder Buchstabe doppelt angehängt. Als Ausgabe ergibt sich somit: *TTEESSTT*
|
||||
|
||||
```java
|
||||
String eingabe = "TEST";
|
||||
String ausgabe = "";
|
||||
|
||||
for (int i=0; i<eingabe.length(); i++) {
|
||||
char c = eingabe.charAt(i);
|
||||
ausgabe = ausgabe + c + c;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Durchlaufen und Verändern einer Zeichenkette (Switch)
|
||||
|
||||
Das nächste Beispiel zeigt sehr schön, wie mithilfe einer Switch-Struktur übersichtlich verschiedene Fälle bearbeitet werden können. Der i-te Buchstabe wird hier in der Variablen *c* vom Typ *char* (also ein Buchstabe) zwischengespeichert. Abhängig vom Buchstaben wird ein anderer Buchstabe oder der ursprüngliche Buchstabe (default-Fall) angehängt.
|
||||
|
||||
Das Beispiel demonstriert insgesamt die Normalisierung einer Zeichenkette, in der Umlaute in eine adäquate Darstellung (z.B. *Ä* in *AE*) gebracht werden. Eine solche Normalisierung findet z.B. bei Suchmaschinen Anwendung, die Sonderzeichen in ihrer internen Speicherung möglichst vermeiden möchten.
|
||||
|
||||
```java
|
||||
String eingabe = "HÖHENÄRGER";
|
||||
String ausgabe = "";
|
||||
|
||||
for (int i=0; i<eingabe.length(); i++) {
|
||||
char c = eingabe.charAt(i);
|
||||
|
||||
switch (c) {
|
||||
case 'Ä': { ausgabe = ausgabe + "AE"; break; }
|
||||
case 'Ö': { ausgabe = ausgabe + "OE"; break; }
|
||||
case 'Ü': { ausgabe = ausgabe + "UE"; break; }
|
||||
default: { ausgabe = ausgabe + c; break; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Durchlaufen und Verändern einer Zeichenkette (ASCII)
|
||||
|
||||
Abschließend betrachten wir noch ein Beispiel aus der Kryptologie, das auch im gleichnamigen Kapitel noch einmal in erweiterter Fassung aufgegriffen wird. Wir möchten eine gegebene Zeichenkette Buchstabe für Buchstabe um drei Zeichen im Alphabet nach vorn schieben ([Caesar-Verschlüsselung](kryptologie.md#caesar-verschlusselung)).
|
||||
|
||||
Dazu wird jeder Buchstabe zunächst in eine Zahl umgewandelt. Diese Verwandlung richtet sich nach der ASCII-Tabelle (vgl. [Wikipedia-Artikel](https://de.wikipedia.org/wiki/American_Standard_Code_for_Information_Interchange)), wodurch dem A eine 65, B eine 66 usw. zugeordnet wird. Dieser Zahl wird 3 hinzuaddiert, die resultierende Zahl wird anschließend wieder in einen Buchstaben zurückverwandelt. Diese geschieht ebenfalls nach der ASCII-Tabelle, wodurch beispielsweise aus dem Buchstaben *G* ein *J* wird (Bildausschnitt aus [WikiPedia](https://commons.wikimedia.org/wiki/File:Ascii-codes-table.png)).
|
||||
|
||||

|
||||
|
||||
Sollte der Buchstabe beim Umwandeln eine Zahl größer als 90 (entspricht dem Buchstaben *Z*) ergeben, so würde der ASCII-Bereich der Großbuchstaben verlassen. Dies wird mit der Subtraktion von 26 korrigiert, d.h. es geht hier bei der Verschiebung im Alphabet wieder von vorn los.
|
||||
|
||||
```java
|
||||
String zeichenkette = "GEHEIMNIS";
|
||||
String ergebnis = "";
|
||||
int schluessel = 3;
|
||||
|
||||
for (int i=0; i<zeichenkette.length(); i++) {
|
||||
|
||||
int zahl = (int) zeichenkette.charAt(i);
|
||||
zahl = zahl + schluessel;
|
||||
if (zahl>90) { zahl = zahl - 26; }
|
||||
|
||||
char zeichen = (char) zahl;
|
||||
ergebnis = ergebnis + zeichen;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Felder
|
||||
|
||||

|
||||
|
||||
In einem Feld kann eine *beliebige, aber feste* Anzahl gleichartiger Elemente verwaltet werden. Die Anzahl der Elemente wird dabei beim Erzeugen des Felds festgelegt und kann anschließend nicht mehr verändert werden.
|
||||
|
||||
Ungewohnt ist die Nummerierung der Elemente bei 0 beginnend und bei der Feldlänge-1 endend. Diese Form der Nummerierung findet in Java aber auch bei Zeichenketten konsequent Anwendung.
|
||||
|
||||
Wir werden im Folgenden das Verwalten primitiver Datentypen (vgl. Zahlen im obigen Beispiel) in einem Feld betrachten. Das Speichern gleichartiger Objekte in einem Feld wird im Abschnitt [Felder](../zentralabitur/linear.md#feld) im Kapitel [Lineare Datenstrukturen](../zentralabitur/linear.md) behandelt.
|
||||
|
||||
|
||||
### Erzeugen eines Felds
|
||||
|
||||
Das folgende Beispiel zeigt zu Beginn die Syntax der Erzeugung eines Felds. Im Beispiel wird ein Feld der Länge 100 über int-Zahlen zunächst deklariert und anschließend initialisiert. Das Feld selbst ist aber zu diesem Zeitpunkt noch leer.
|
||||
|
||||
```java
|
||||
int[] meinfeld; // Deklaration
|
||||
meinfeld = new int[100]; // Initialisierung
|
||||
```
|
||||
|
||||
Als abkürzende Schreibweise können Deklaration und Initialisierung wieder zusammengefasst werden.
|
||||
|
||||
```java
|
||||
int[] meinfeld = new int[100]; // Deklaration + Initialisierung
|
||||
```
|
||||
|
||||
Folgt die geplante Besetzung des Felds einer festen Systematik, hilft eine Schleife bei der Besetzung der einzelnen Feldelemente. Im folgenden Beispiel werden die natürlichen Zahlen 1, 2, 3 usw. in das Feld eingewiesen.
|
||||
|
||||
```java
|
||||
// Erzeuge Feld
|
||||
int[] feld = new int[10];
|
||||
|
||||
// Besetze mit 1,2,3,...
|
||||
for (int i=0; i<feld.length; i++) {
|
||||
feld[i] = i+1;
|
||||
}
|
||||
```
|
||||
|
||||
Sind die Elemente des Feldes bei der Erzeugung bekannt und handelt es sich um ein eher kurzes Feld, so ist die folgende Kompaktschreibweise interessant, die Deklaration, Erzeugung und Initialisierung in einer Anweisung verbindet:
|
||||
|
||||
```java
|
||||
int[] feld = { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 };
|
||||
```
|
||||
|
||||
|
||||
### Durchlaufen eines Felds
|
||||
|
||||
Das nächste Beispiel setzt ein Feld über int-zahlen voraus und summiert bzw. multipliziert die enthaltenen Werte, unabhängig von der Länge des Feldes. Die Abfrage *feld.length* der Feldlänge sorgt leider oft für Verwirrung, da sie in Java im Gegensatz zu Zeichenketten für Felder nicht als Methodenaufruf mit anschließenden runden Klammern *()* realisiert ist.
|
||||
|
||||
```java
|
||||
int summe = 0;
|
||||
for (int i=0; i<feld.length; i++) {
|
||||
summe = summe + feld[i];
|
||||
}
|
||||
|
||||
int produkt = 1;
|
||||
for (int i=0; i<feld.length; i++) {
|
||||
produkt = produkt * feld[i];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Zeichenkette in Feld verwandeln
|
||||
|
||||
Eine erste komplexere Aufgabe besteht darin, die in einer Zeichenkette enthaltenen Ziffern in ein Feld über int-Zahlen zu übertragen. Dazu wird im ersten Schritt ein Feld erzeugt, das genauso lang ist wie die Zeichenkette. In einem zweiten Schritt geht nun die Schleife Buchstabe für Buchstabe durch die Zeichenkette, konvertiert die einzelnen Ziffern in Zahlen (gemäß der ASCII-Tabelle entspricht 0 einer 48, 1 einer 49 usw.), wandelt sie durch die Subtraktion von 48 in die gewünschte Zahl zwischen 0 und 9 um und speichert sie an der gleichen Feldposition wie die Buchstabenposition.
|
||||
|
||||
```java
|
||||
String zeichenkette = "54678932";
|
||||
|
||||
int n = zeichenkette.length();
|
||||
int[] feld = new int[n];
|
||||
|
||||
for (int i=0; i<zeichenkette.length(); i++) {
|
||||
|
||||
int zahl = (int) zeichenkette.charAt(i);
|
||||
zahl = zahl - 48; // ASCII-Code von 0 ist 48, von 1 ist 49 usw.
|
||||
feld[i] = zahl;
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Zweidimensionale Felder
|
||||
|
||||
Zweidimensionale Felder eignen sich dazu, eine Tabelle bzw. Matrix gleichartiger Elemente zu verwalten. Das folgende Anwendungsbeispiel zeigt auf, wie bei dem Spiel *Schiffe versenken* die Schiffe einer Spielers in einer Matrix repräsentiert werden:
|
||||
|
||||

|
||||
|
||||
Der Spieler hat auf seinem Spielfeld (der "Seekarte") ein 2er-, ein 3er- und ein 4er-Schiff versteckt. Jedes Schiff ist durch die Koordinaten seiner Bestandteile (z.B. 0-2 und 0-3 für das 2er-Schiff) eindeutig festgelegt. Der Gegenspieler kann nun versuchen, durch einen Schuss auf eine vorgegebene Koordinate einen Teil eines Schiffes zu versenken.
|
||||
|
||||
Eine einfache Modellierung mit einem Java-Feld besteht darin, an Positionen mit einem Schiffsbestandteile eine 1, an allen anderen Koordinaten eine 0 abzuspeichern. Damit ergibt sich die Definition der Seekarte des Benutzers im Konstruktor der hier angegebenen Klasse.
|
||||
|
||||
Die erste Methode *getroffen()* überprüft, ob an der übergebenen Koordinate ein Schiffsbestandteil zu finden ist. Ist das der Fall, so wird er auf 0 gesetzt und damit "versenkt" und als Ergebnis *true* zurückgegeben. Ansonsten wird *false* zurückgegeben.
|
||||
|
||||
Die zweite Methode überprüft, ob auf der gesamten Seekarte alle Schiffe versenkt wurden (*return true;* am Ende) oder ob es noch mindestens ein Schiffsbestandteil auf der Seekarte (vorzeitiges *return false;*) gibt.
|
||||
|
||||
```java
|
||||
public class SchiffeVersenken {
|
||||
|
||||
private int[][] meer = { { 0, 1, 1, 1, 1, 0 },
|
||||
{ 0, 0, 0, 0, 0, 0 },
|
||||
{ 1, 0, 0, 1, 1, 1 },
|
||||
{ 1, 0, 0, 0, 0, 0 } };
|
||||
|
||||
public boolean getroffen(int zeile, int spalte) {
|
||||
if (meer[zeile][spalte] == 1) {
|
||||
meer[zeile][spalte] = 0;
|
||||
return true;
|
||||
}
|
||||
else { return false; }
|
||||
}
|
||||
|
||||
public boolean alleVersenkt() {
|
||||
for (int i=0; i<4; i++) {
|
||||
for (int j=0; j<6; j++) {
|
||||
if (meer[i][j]==1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
274
docs/grundlagen/kryptologie.md
Normal file
274
docs/grundlagen/kryptologie.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# 3. Kryptologie
|
||||
|
||||
Bei den hier besprochenen Verfahren geht es nicht um die Kryptoanalyse, d.h. die Beurteilung der Sicherheit der Verfahren, sondern um die Umsetzung der Verfahren in Java. Dabei kommen elementare Inhalte wie Zeichenketten, Felder und vor allem das Verwandeln von char-Buchstaben in int-Zahlen und umgekehrt nach der ASCII-Tabelle zum Tragen.
|
||||
|
||||
|
||||
|
||||
## Caesar-Verschlüsselung
|
||||
|
||||
Das wohl bekannteste Verschlüsselungsverfahren geht auf Julius Caesar zurück. Er verschob die Buchstaben des Alphabets einfach um eine feste Anzahl von Buchstaben. Jedem Klartextbuchstaben wird damit ein verschobener Geheimtextbuchstabe zugeordnet. Bei der Entschlüsselung geschieht die Verschiebung dann in entgegengesetzter Richtung.
|
||||
|
||||
|
||||
### Einfache Umsetzung
|
||||
|
||||
Die folgende Umsetzung greift den Gedanken auf und ordnet im Rahmen einer switch-Anweisung die Buchstaben einander zu. Bei der zugehörigen Entschlüsselungsmethode würde die Anordnung entgegengesetzt erfolgen.
|
||||
|
||||
```java
|
||||
public String verschluesseleEinfach(String klartext) {
|
||||
|
||||
String ergebnis = "";
|
||||
|
||||
for (int i=0; i<klartext.length(); i++) {
|
||||
|
||||
switch (klartext.charAt(i)) {
|
||||
case 'A': { ergebnis = ergebnis + 'D'; break; }
|
||||
case 'B': { ergebnis = ergebnis + 'E'; break; }
|
||||
case 'C': { ergebnis = ergebnis + 'F'; break; }
|
||||
case 'D': { ergebnis = ergebnis + 'G'; break; }
|
||||
case 'E': { ergebnis = ergebnis + 'H'; break; }
|
||||
case 'F': { ergebnis = ergebnis + 'I'; break; }
|
||||
case 'G': { ergebnis = ergebnis + 'J'; break; }
|
||||
case 'H': { ergebnis = ergebnis + 'K'; break; }
|
||||
case 'I': { ergebnis = ergebnis + 'L'; break; }
|
||||
case 'J': { ergebnis = ergebnis + 'M'; break; }
|
||||
case 'K': { ergebnis = ergebnis + 'N'; break; }
|
||||
case 'L': { ergebnis = ergebnis + 'O'; break; }
|
||||
case 'M': { ergebnis = ergebnis + 'P'; break; }
|
||||
case 'N': { ergebnis = ergebnis + 'Q'; break; }
|
||||
case 'O': { ergebnis = ergebnis + 'R'; break; }
|
||||
case 'P': { ergebnis = ergebnis + 'S'; break; }
|
||||
case 'Q': { ergebnis = ergebnis + 'T'; break; }
|
||||
case 'R': { ergebnis = ergebnis + 'U'; break; }
|
||||
case 'S': { ergebnis = ergebnis + 'V'; break; }
|
||||
case 'T': { ergebnis = ergebnis + 'W'; break; }
|
||||
case 'U': { ergebnis = ergebnis + 'X'; break; }
|
||||
case 'V': { ergebnis = ergebnis + 'Y'; break; }
|
||||
case 'W': { ergebnis = ergebnis + 'Z'; break; }
|
||||
case 'X': { ergebnis = ergebnis + 'A'; break; }
|
||||
case 'Y': { ergebnis = ergebnis + 'B'; break; }
|
||||
case 'Z': { ergebnis = ergebnis + 'C'; break; }
|
||||
default: { ergebnis = ergebnis + klartext.charAt(i); }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Geschickte Umsetzung
|
||||
|
||||
Ein geschickterer Ansatz macht sich die ASCII-Tabelle zunutze. Er durchläuft den Klartext Buchstabe für Buchstabe. Für jeden Buchstaben wird die Nummer in der ASCII-Tabelle ermittelt und dieser Nummer der Schlüssel (d.h. die Anzahl der zu verschiebenden Stellen) hinzuaddiert. Anschließend wird die so ermittelte Nummer wieder in einen Buchstaben zurückverwandelt (Bildausschnitt aus [WikiPedia](https://commons.wikimedia.org/wiki/File:Ascii-codes-table.png)).
|
||||
|
||||

|
||||
|
||||
Dabei kann es passieren, dass das Alphabet verlassen wird (*Z* entspricht 90 am Ende des Alphabets). In diesem Fall geht es durch Subtraktion von 26 wieder im Alphabet von vorn los.
|
||||
|
||||
Elegant ist die Idee, die Entschlüsselung durch einen Schlüssel als negative Zahl zu realisieren. Dazu muss zusätzlich mit überprüft werden, ob das Alphabet nach vorn (*A* entspricht 65 am Anfang des Alphabets) verlassen wird.
|
||||
|
||||
```java
|
||||
public String verschluessele(String zeichenkette, int schluessel) {
|
||||
|
||||
String ergebnis = "";
|
||||
|
||||
for (int i=0; i<zeichenkette.length(); i++) {
|
||||
|
||||
int zahl = (int) zeichenkette.charAt(i);
|
||||
|
||||
zahl = zahl + schluessel;
|
||||
if (zahl>90) { zahl = zahl - 26; }
|
||||
if (zahl<65) { zahl = zahl + 26; }
|
||||
|
||||
char zeichen = (char) zahl;
|
||||
ergebnis = ergebnis + zeichen;
|
||||
}
|
||||
|
||||
return ergebnis;
|
||||
|
||||
}
|
||||
|
||||
public String entschluessele(String zeichenkette, int schluessel) {
|
||||
return verschluessele(zeichenkette, -schluessel);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Monoalphabetische Substitution
|
||||
|
||||
Da die Verschiebung des Alphabets durch die Caesar-Verschlüsselung zu wenig Sicherheit bietet, ordnet die monoalphabetische Substitution jedem Buchstaben des Alphabets zufällig einen anderen Buchstaben des Alphabets zu. Es ergeben sich 26! mögliche Schlüssel.
|
||||
|
||||
|
||||
### Einfache Umsetzung
|
||||
|
||||
Auch hier ist zunächst wieder eine Zuordnung der Buchstaben in einer switch-Anweisung denkbar:
|
||||
|
||||
```java
|
||||
public String verschluessele(String klartext) {
|
||||
|
||||
String ergebnis = "";
|
||||
|
||||
for (int i=0; i<klartext.length(); i++) {
|
||||
|
||||
switch (klartext.charAt(i)) {
|
||||
case 'A': { ergebnis = ergebnis + 'Q'; break; }
|
||||
case 'B': { ergebnis = ergebnis + 'W'; break; }
|
||||
case 'C': { ergebnis = ergebnis + 'E'; break; }
|
||||
case 'D': { ergebnis = ergebnis + 'R'; break; }
|
||||
case 'E': { ergebnis = ergebnis + 'T'; break; }
|
||||
case 'F': { ergebnis = ergebnis + 'Z'; break; }
|
||||
case 'G': { ergebnis = ergebnis + 'U'; break; }
|
||||
case 'H': { ergebnis = ergebnis + 'I'; break; }
|
||||
case 'I': { ergebnis = ergebnis + 'O'; break; }
|
||||
case 'J': { ergebnis = ergebnis + 'P'; break; }
|
||||
case 'K': { ergebnis = ergebnis + 'A'; break; }
|
||||
case 'L': { ergebnis = ergebnis + 'S'; break; }
|
||||
case 'M': { ergebnis = ergebnis + 'D'; break; }
|
||||
case 'N': { ergebnis = ergebnis + 'F'; break; }
|
||||
case 'O': { ergebnis = ergebnis + 'G'; break; }
|
||||
case 'P': { ergebnis = ergebnis + 'H'; break; }
|
||||
case 'Q': { ergebnis = ergebnis + 'J'; break; }
|
||||
case 'R': { ergebnis = ergebnis + 'K'; break; }
|
||||
case 'S': { ergebnis = ergebnis + 'L'; break; }
|
||||
case 'T': { ergebnis = ergebnis + 'Y'; break; }
|
||||
case 'U': { ergebnis = ergebnis + 'X'; break; }
|
||||
case 'V': { ergebnis = ergebnis + 'C'; break; }
|
||||
case 'W': { ergebnis = ergebnis + 'V'; break; }
|
||||
case 'X': { ergebnis = ergebnis + 'B'; break; }
|
||||
case 'Y': { ergebnis = ergebnis + 'N'; break; }
|
||||
case 'Z': { ergebnis = ergebnis + 'M'; break; }
|
||||
default: { ergebnis = ergebnis + klartext.charAt(i); }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ergebnis;
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Geschickte Umsetzung (Schlüssel als Zeichenkette)
|
||||
|
||||
Bei der ersten geschickten Alternative wird der Schlüssel als Zeichenkette mit 26 Buchstaben repräsentiert (z.B. analog zur Umsetzung oben: ```"QWERTZUIOPASDFGHJKLYXCVBNM"```). Bei der Umwandlung eines Klartextbuchstaben wird dieser in eine fortlaufende Nummer umgewandelt (also z.B. *A* in *0*). Diese Nummer gibt an, an welcher Stelle der Schlüssel-Zeichenkette der zugeordnete Geheimtextbuchstabe zu finden ist.
|
||||
|
||||
```java
|
||||
public String verschluesseleSchlau(String zeichenkette, String schluessel) {
|
||||
|
||||
String ergebnis = "";
|
||||
|
||||
for (int i=0; i<zeichenkette.length(); i++) {
|
||||
|
||||
int zahl = ((int) zeichenkette.charAt(i)) - 64 - 1;
|
||||
|
||||
ergebnis = ergebnis + schluessel.charAt(zahl);
|
||||
}
|
||||
|
||||
return ergebnis;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Geschickte Umsetzung (Schlüssel als Feld)
|
||||
|
||||
Die zweite Alternative ähnelt der ersten. Hier wird der Schlüssel als Feld über 26 Buchstaben statt als Zeichenkette repräsentiert. Bei der Umwandlung eines Klartextbuchstaben wird dieser wieder in eine fortlaufende Nummer umgewandelt (also z.B. *A* in *0*). Diese Nummer gibt hier an, an welcher Stelle des Feldes der zugeordnete Geheimtextbuchstabe zu finden ist.
|
||||
|
||||
```java
|
||||
public String verschluesseleSchlau(String zeichenkette, char[] schluessel) {
|
||||
|
||||
String ergebnis = "";
|
||||
|
||||
for (int i=0; i<zeichenkette.length(); i++) {
|
||||
|
||||
int zahl = ((int) zeichenkette.charAt(i)) - 65;
|
||||
|
||||
ergebnis = ergebnis + schluessel[zahl];
|
||||
}
|
||||
|
||||
return ergebnis;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Polyalphabetische Substitution
|
||||
|
||||
|
||||
### Idee
|
||||
|
||||
Reicht ein Zielalphabet nicht aus, um für genügend Sicherheit zu sorgen, dann nehme man mehrere Zielalphabete, die sich untereinander abwechseln. Diese Idee ist als polyalphabetische Substitution bekannt und wird dem Kryptologen Blaise de Vigenère zugeordnet.
|
||||
|
||||
Grundlage des Verfahren ist das Vigenère-Quadrat, dessen Anwendung hier demonstriert wird (Quelle: [Wikipedia]( https://commons.wikimedia.org/wiki/File:Vigenere-Beispiel.png)):
|
||||
|
||||

|
||||
|
||||
Der Schlüssel (im Beispiel *AKEY*) gibt an, welches Alphabet gerade aktiv ist, d.h. welche Zeile zu wählen ist. So wird für den ersten Buchstaben des Klartextes die Zeile *A* gewählt, für den zweiten Buchstaben *K*, für den dritten Buchstaben *E* und für den vierten Buchstaben *Y*. Anschließend geht es wieder von vorne los, also wieder mit dem *A*.
|
||||
|
||||
Um nun z.B. den sechsten Buchstaben des Klartextes *M* mit dem zugehörigen Schlüsselbuchstaben *K* zu verknüpfen, wird die Spalte *M* mit der Zeile *K* verbunden und der Kreuzungspunkt *W* als Geheimtextbuchstabe festgehalten. Ein Vertauschen von Zeile und Spalte ist kein Problem, da das Vigenère-Quadrat symmetrisch ist.
|
||||
|
||||
|
||||
### Umsetzung
|
||||
|
||||
Der erste Teil der Java-Umsetzung besteht aus der Definition des Vigenère-Quadrats.
|
||||
|
||||
```java
|
||||
private char[][] tabelle = {
|
||||
|
||||
{'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'},
|
||||
{'B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A'},
|
||||
{'C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B'},
|
||||
{'D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C'},
|
||||
{'E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D'},
|
||||
{'F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E'},
|
||||
{'G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F'},
|
||||
{'H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G'},
|
||||
{'I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H'},
|
||||
{'J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I'},
|
||||
{'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J'},
|
||||
{'L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K'},
|
||||
{'M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L'},
|
||||
{'N','O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M'},
|
||||
{'O','P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N'},
|
||||
{'P','Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O'},
|
||||
{'Q','R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P'},
|
||||
{'R','S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q'},
|
||||
{'S','T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R'},
|
||||
{'T','U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S'},
|
||||
{'U','V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T'},
|
||||
{'V','W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U'},
|
||||
{'W','X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V'},
|
||||
{'X','Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W'},
|
||||
{'Y','Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X'},
|
||||
{'Z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y'}
|
||||
|
||||
};
|
||||
```
|
||||
|
||||
Der zweite Teil setzt die Verknüpfung von Zeile und Spalte um: In der Variablen *schluessel_pos* wird die Position des aktuellen Schlüsselbuchstaben festgehalten. Dieser Positionszeiger bewegt sich fortlaufend durch die Schlüssel-Zeichenkette und fängt bei Erreichen des Endes wieder vorne an. Die Variable *schluessel_nr* enthält entsprechend die Nummer des aktuellen Schlüsselbuchstaben. Schließlich legt die Variable *klar_nr* die Nummer des aktuellen Klartextbuchstaben fest. Durch diese beiden Variablen sind Zeile und Spalte festgelegt, wodurch der Geheimtextbuchstabe in der Tabelle nachgeschaut werden kann.
|
||||
|
||||
```java
|
||||
public String verschluessele(String klartext, String schluesselwort) {
|
||||
|
||||
String ergebnis = "";
|
||||
int schluessel_pos = 0;
|
||||
|
||||
for (int i=0; i<klartext.length(); i++) {
|
||||
|
||||
char klar = klartext.charAt(i);
|
||||
int klar_nr = (int) klar - 65;
|
||||
|
||||
char schluessel = schluesselwort.charAt(schluessel_pos);
|
||||
int schluessel_nr = (int) schluessel - 65;
|
||||
|
||||
char geheim = tabelle[klar_nr][schluessel_nr];
|
||||
ergebnis = ergebnis + geheim;
|
||||
|
||||
schluessel_pos++;
|
||||
if (schluessel_pos >= schluesselwort.length()) {
|
||||
schluessel_pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return ergebnis;
|
||||
}
|
||||
```
|
||||
160
docs/grundlagen/rekursion.md
Normal file
160
docs/grundlagen/rekursion.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# 4. Rekursion
|
||||
|
||||
Bei der Rekursion handelt es sich um eine Programmiertechnik, die ein Problem auf ein oder mehrere kleinere Probleme zurückführt. In einer Programmiersprache wie Java umgesetzt äußert sich die Rekursion in einer Java-Methode, die sich selbst aufruft.
|
||||
|
||||
|
||||
|
||||
## Türme von Hanoi
|
||||
|
||||
Die Türme von Hanoi sind ein klassisches Beispiel für einen rekursiven Algorithmus. Bei diesem Spiel müssen mehrere verschieden große Scheiben von einem Quellstapel auf einen Zielstapel gebracht werden. Dabei darf aber immer nur eine Scheibe bewegt werden, zudem darf immer nur eine kleinere Scheibe auf einer größeren Scheibe liegen. Das folgende Bild illustriert das Spiel (Quelle [Wikipedia](https://commons.wikimedia.org/wiki/File:Tower_of_Hanoi.jpeg)):
|
||||
|
||||

|
||||
|
||||
Eine rekursive Herangehensweise beschreibt die Lösung des Problems folgendermaßen: Wenn du *n* Scheiben vom Quell- auf den Zielstapel bringen willst, dann verschiebe zunächst *n-1* Scheiben auf den Hilfsstapel, dann *1* Scheibe (die unterste) auf den Zielstapel und zum Schluss *n-1* Scheiben vom Hilfs- auf den Zielstapel.
|
||||
|
||||
Der folgende Java-Algorithmus setzt voraus, dass die drei Stapel mit 1, 2, 3 durchnummeriert sind. Zu Beginn wird die Methode also beispielsweise zur Lösung des 5-Scheiben-Problems mit den Parametern 5, 1, 3, 2 aufgerufen. Die Scheibenbewegungen werden durch Bildschirmausdrucke dargestellt.
|
||||
|
||||
```java
|
||||
public class Hanoi {
|
||||
|
||||
public void TvH(int n, int start, int ziel, int hilf) {
|
||||
|
||||
if (n==1) {
|
||||
System.out.println(start+" -> "+ziel);
|
||||
}
|
||||
else {
|
||||
TvH(n-1, start, hilf, ziel);
|
||||
TvH( 1, start, ziel, hilf);
|
||||
TvH(n-1, hilf, ziel, start);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Für das 5-Scheiben-Problem ergibt sich demnach folgende Ausgabe:
|
||||
|
||||
```
|
||||
1 -> 3
|
||||
1 -> 2
|
||||
3 -> 2
|
||||
1 -> 3
|
||||
2 -> 1
|
||||
2 -> 3
|
||||
1 -> 3
|
||||
1 -> 2
|
||||
3 -> 2
|
||||
3 -> 1
|
||||
2 -> 1
|
||||
3 -> 2
|
||||
1 -> 3
|
||||
1 -> 2
|
||||
3 -> 2
|
||||
1 -> 3
|
||||
2 -> 1
|
||||
2 -> 3
|
||||
1 -> 3
|
||||
2 -> 1
|
||||
3 -> 2
|
||||
3 -> 1
|
||||
2 -> 1
|
||||
2 -> 3
|
||||
1 -> 3
|
||||
1 -> 2
|
||||
3 -> 2
|
||||
1 -> 3
|
||||
2 -> 1
|
||||
2 -> 3
|
||||
1 -> 3
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Rekursive Algorithmen
|
||||
|
||||
An dieser Stelle werden exemplarisch zwei bekannte rekursive Algorithmen vorgestellt.
|
||||
|
||||
|
||||
### Binäre Suche
|
||||
|
||||
Als erstes Beispiel wird die binäre Suche auf einem Feld besprochen. Gegenüber der iterativen Lösung wird der Suchraum hier durch den rekursiven Aufruf verkleinert. Zu Beginn wird der Suchvorgang auf dem kompletten Feld in Gang gebracht. Je nach Fall verengt sich der Suchraum in jeder Rekursionsebene, bis entweder das gewünschte Element gefunden wurde oder der Suchraum leer ist (Fall *l>r*).
|
||||
|
||||
```java
|
||||
public int starteBinaereSucheRekursiv(int[] array, int key) {
|
||||
|
||||
int n = array.length;
|
||||
|
||||
return sucheBinaer(array, key, 0, n-1);
|
||||
}
|
||||
|
||||
/* Fuehrt die eigentliche binaere Suche durch. */
|
||||
private int sucheBinaer(int[] array, int key, int l, int r) {
|
||||
|
||||
// Abbruchbedingung fuer erfolglose Suche: l>r
|
||||
if (l>r) { return -1; }
|
||||
|
||||
// Bestimme die Mitte des Feldabschnitts
|
||||
int m = (l+r) / 2;
|
||||
|
||||
// Unterscheide die drei F?lle
|
||||
|
||||
// 1. Fall: GEFUNDEN
|
||||
if (array[m] == key) {
|
||||
return m;
|
||||
}
|
||||
|
||||
// 2. Fall: Suche LINKS weiter
|
||||
else if (key < array[m]) {
|
||||
return sucheBinaer(array, key, l, m-1);
|
||||
}
|
||||
|
||||
// 3. Fall: Suche RECHTS weiter
|
||||
else {
|
||||
return sucheBinaer(array, key, m+1, r);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### QuickSort
|
||||
|
||||
Das Sortierverfahren QuickSort basiert auf einer simplen Idee, die rekursiv elegant umgesetzt werden kann: Bestimme im aktuellen Teilfeld ein vermutlich mittelgroßes Element (*Pivot*-Element) und ordne nun links vom Pivot alle kleineren und rechts vom Pivot alle größeren Elemente an. Sortiere den linken und rechten Bereich anschließend durch zwei Rekursionsaufrufe.
|
||||
|
||||
```java
|
||||
public void doQuickSort(int[] feld) {
|
||||
|
||||
quickSort(feld,0,feld.length-1);
|
||||
|
||||
}
|
||||
|
||||
private void quickSort(int[] feld, int l, int r) {
|
||||
|
||||
if (l>=r) { return; }
|
||||
|
||||
int i, j, m;
|
||||
int pivot, hilf;
|
||||
|
||||
// Bestimme Pivotelement
|
||||
i = l;
|
||||
j = r;
|
||||
m = (l+r)/2;
|
||||
pivot = feld[m];
|
||||
|
||||
// Teile in zwei Teilfelder
|
||||
while (i<=j) {
|
||||
while (feld[i]<pivot) { i++; }
|
||||
while (feld[j]>pivot) { j--; }
|
||||
if (i<=j) { // Tausche Feldinhalte
|
||||
hilf = feld[i];
|
||||
feld[i] = feld[j];
|
||||
feld[j] = hilf;
|
||||
i++;
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
quickSort(feld,i,r);
|
||||
quickSort(feld,l,j);
|
||||
|
||||
}
|
||||
```
|
||||
170
docs/grundlagen/suchensortieren.md
Normal file
170
docs/grundlagen/suchensortieren.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# 2. Suchen und Sortieren
|
||||
|
||||
Die Operationen Suchen und Sortieren stellen zwei der zentralen Operationen der Informatik dar. Sie werden hier exemplarisch auf der Datenstruktur Feld besprochen, spielen aber auch bei den Datenstrukturen [Liste](../zentralabitur/linear.md#liste) und [Suchbaum](../zentralabitur/baum.md#suchbaum) eine zentrale Rolle.
|
||||
|
||||
Zur weiteren Vereinfachung werden Felder über dem primitiven Datentyp *int* betrachtet. [Felder über Objekten](../zentralabitur/linear.md#feld) werden im Rahmen der linearen Datenstrukturen besprochen.
|
||||
|
||||
|
||||
|
||||
## Suchen
|
||||
|
||||
|
||||
### Lineare Suche
|
||||
|
||||
Bei der linearen Suche wird das Feld von vorn nach hinten sequentiell solange durchsucht, bis das gewünschte Element gefunden wird (*erfolgreiche Suche*) oder das Feld erfolglos vollständig abgesucht wurde (*erfolglose Suche*). Es ist leicht einzusehen, dass die lineare Suche lineare Laufzeit besitzt.
|
||||
|
||||
Als Konvention gibt die Methode hier nicht nur die Erfolgsmeldung aus (*true/false*), sondern die Position des gefunden Elements bzw. *-1* im Falle der erfolglosen Suche.
|
||||
|
||||
```java
|
||||
public int starteLineareSuche(int[] array, int key) {
|
||||
|
||||
for (int i=0; i<array.length; i++) {
|
||||
|
||||
if (array[i]==key) {
|
||||
return i;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Binäre Suche
|
||||
|
||||
Die binäre Suche setzt ein bereits sortiertes Feld voraus (vgl. nächster Abschnitt [Sortieren](#sortieren)). Diese Sortierung wird durch die Idee der Intervallhalbierung ausgenutzt. In jedem Durchlauf wird die mittlere Position des verbleibenden Suchraums bestimmt. Ist das gesuchte Element an dieser Stelle vorhanden, so wird die Suche erfolgreich beendet. Ist das gesuchte Element kleiner, so kann der Suchraum auf alle Feldelemente links von der mittleren Position eingeschränkt werden. Ist das Element größer, so kann der Suchraum entsprechend auf alle Feldelemente rechts von der mittleren Position eingeschränkt werden. Sollten sich die Suchraumgrenzen gegenseitig überholen (*l>r*), so wird die Suche erfolglos abgebrochen.
|
||||
|
||||
Durch die Intervallhalbierung ist eine logarithmische Laufzeit bedingt, die viel besser als eine lineare Laufzeit einzustufen ist. Allerdings darf nicht vergessen werden, dass der zusätzliche Aufwand der Sortierung ebenfalls Laufzeit kostet.
|
||||
|
||||
```java
|
||||
public int starteBinaereSuche(int[] array, int key) {
|
||||
|
||||
// Lege die Bereichsgrenzen fest
|
||||
int n = array.length;
|
||||
int m;
|
||||
|
||||
int l = 0;
|
||||
int r = n-1;
|
||||
|
||||
while (l<=r) {
|
||||
|
||||
// Bestimme Mitte
|
||||
m = (l+r) / 2;
|
||||
|
||||
// Unterscheide drei Fälle
|
||||
|
||||
// 1. Fall: GEFUNDEN
|
||||
if (array[m] == key) {
|
||||
return m;
|
||||
}
|
||||
|
||||
// 2. Fall: Suche LINKS weiter
|
||||
else if (key < array[m]) {
|
||||
r = m-1;
|
||||
}
|
||||
|
||||
// 3. Fall: Suche RECHTS weiter
|
||||
else {
|
||||
l = m+1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return -1;
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Sortieren
|
||||
|
||||
|
||||
### BubbleSort
|
||||
|
||||
Das vermutlich bekannteste Sortierverfahren ist *BubbleSort*. Hier wird in einem Felddurchlauf für je zwei Nachbarelemente geschaut, ob sie sich in der richtigen Reihenfolge befinden. Wenn nicht, so werden sie getauscht (*paarweises Tauschen*). Auf diese Weise rutscht das größte Element im ersten Durchlauf an die letzte Position, das zweitgrößte Element im zweiten Durchlauf an die vorletzte Position usw. Es ergibt sich insgesamt eine quadratische Laufzeit.
|
||||
|
||||
```java
|
||||
public void doBubbleSort(int[] feld) {
|
||||
|
||||
int hilfe;
|
||||
|
||||
for (int i=feld.length-1; i>=1; i--) {
|
||||
|
||||
for (int j=0; j<=i-1; j++) {
|
||||
|
||||
if (feld[j] > feld[j+1]) {
|
||||
hilfe = feld[j];
|
||||
feld[j] = feld[j+1];
|
||||
feld[j+1] = hilfe;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Straight Selection
|
||||
|
||||
Beim *Sortieren durch Auswahl* bzw. *Straight Selection* wird im ersten Durchlauf das kleinste Element an die 0-te Stelle des Felds, im zweiten Durchlauf das zweitkleinste Element an die 1-te Stelle des Felds usw. gebracht. Bei der Suche nach dem verbleibenden kleinsten Element wird das Restfeld von vorn nach hinten durchlaufen. Es ergibt sich wieder eine quadratische Laufzeit.
|
||||
|
||||
```java
|
||||
public void doStraightSelection(int[] feld) {
|
||||
|
||||
int hilfe;
|
||||
|
||||
for (int i=0; i<feld.length; i++) {
|
||||
|
||||
for (int j=i+1; j<feld.length; j++) {
|
||||
|
||||
if (feld[j]<feld[i]) {
|
||||
hilfe = feld[j];
|
||||
feld[j] = feld[i];
|
||||
feld[i] = hilfe;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Straight Insertion
|
||||
|
||||
Beim *Sortieren durch Einfügen* (*Straight Insertion*) wird nach und nach ein immer größer werdendes sortiertes Teilfeld aufgebaut. Im ersten Schritt ist das Teilfeld, das nur aus dem ersten Element besteht, bereits sortiert. Nun wird das zweite Element mit in das sortierte Teilfeld aufgenommen, d.h. es wird an der passenden Einfügestelle eingefügt. Diese Idee wiederholt sich so lange, bis das gesamte Feld sortiert ist. Auch dieses Verfahren weist wieder quadratische Laufzeit auf.
|
||||
|
||||
```java
|
||||
public void doStraightInsertion(int[] feld) {
|
||||
|
||||
int hilfe;
|
||||
|
||||
for (int i=1; i<feld.length; i++) {
|
||||
|
||||
int j=0;
|
||||
|
||||
while ((j<i) && (feld[j]<feld[i])) {
|
||||
j++;
|
||||
}
|
||||
|
||||
hilfe = feld[i];
|
||||
|
||||
for (int k=i; k>=j+1; k--) {
|
||||
feld[k] = feld[k-1];
|
||||
}
|
||||
|
||||
feld[j] = hilfe;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Schnelle Sortierverfahren
|
||||
|
||||
Es gibt weitere Sortierverfahren, die mit *n log n* eine Laufzeit aufweisen, die der quadratischen überlegen ist. Stellvertretend sei hier QuickSort genannt, das als rekursives Verfahren im Abschnitt [Rekursive Algorithmen](rekursion.md#rekursive-algorithmen) vorgestellt wird.
|
||||
Reference in New Issue
Block a user