Remove all solution markdown, task images, solution PDFs, and add renamed folder structure as part of THB-wide reorganization.
This commit is contained in:
43
README.md
43
README.md
@@ -1,26 +1,43 @@
|
|||||||
# Programmierung 2 - Algorithmen und Datenstrukturen
|
# Programmierung 2 — Algorithmen und Datenstrukturen
|
||||||
|
|
||||||
Aufgabenblätter aus dem Modul Programmierung 2 (2. Semester).
|
Code zu den Aufgabenblättern aus dem Modul Programmierung 2 (2. Semester).
|
||||||
|
|
||||||
|
Dieses Repo enthält ausschließlich den Java-Code. Aufgabenstellungen,
|
||||||
|
Lösungstexte und PDFs liegen außerhalb des Repos in
|
||||||
|
`../Uebungen/<NN>/` (Nextcloud-synchronisiert, nicht versioniert).
|
||||||
|
Konvention siehe `THB/README.md`.
|
||||||
|
|
||||||
## Repository klonen
|
## Repository klonen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://gitea.jeanlucmakiola.de/THB/Jander_Semester2.git
|
git clone https://gitea.jeanlucmakiola.de/THB/Jander_Semester2.git code
|
||||||
cd Jander_Semester2
|
cd code
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(Lokal heißt der Checkout `code` per THB-Konvention; remote-Name bleibt
|
||||||
|
historisch `Jander_Semester2`.)
|
||||||
|
|
||||||
## Struktur
|
## Struktur
|
||||||
|
|
||||||
```
|
```
|
||||||
aufgabenblatt1/ - Aufgabenblatt 1
|
code/
|
||||||
src/
|
├── uebung_01/ - Aufgabenblatt 1
|
||||||
util/ - Hilfsklassen (Util, ArrayTester)
|
│ └── src/
|
||||||
task3/ - NumberGuesser + KI-Vergleich
|
│ ├── util/ - Hilfsklassen (Util, ArrayTester)
|
||||||
task4/ - AnagrammChecker + KI-Vergleich
|
│ ├── task3/ - NumberGuesser + KI-Vergleich
|
||||||
task5/ - PrimeNumberGenerator + KI-Vergleich
|
│ ├── task4/ - AnagrammChecker + KI-Vergleich
|
||||||
task6/ - Sorter + KI-Vergleich
|
│ ├── task5/ - PrimeNumberGenerator + KI-Vergleich
|
||||||
solution.md - Ergebnisse
|
│ └── task6/ - Sorter + KI-Vergleich
|
||||||
solution.pdf - PDF zur Abgabe
|
└── uebung_02/ - Aufgabenblatt 2
|
||||||
|
└── src/
|
||||||
|
└── util/ - Hilfsklassen (Util, ArrayTester, IntArrayList, Sorter)
|
||||||
|
```
|
||||||
|
|
||||||
|
Die zugehörigen Aufgabenblätter und Lösungsdokumente liegen in:
|
||||||
|
|
||||||
|
```
|
||||||
|
../Uebungen/01/{Aufgabe.pdf, Loesung.md, Loesung.pdf, task1.png, task2.png, task5.png}
|
||||||
|
../Uebungen/02/{Aufgabe.pdf, Loesung.md}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|||||||
Binary file not shown.
@@ -1,131 +0,0 @@
|
|||||||
# Task 1
|
|
||||||

|
|
||||||
|
|
||||||
# Task 2
|
|
||||||

|
|
||||||
|
|
||||||
# Task 3
|
|
||||||
|
|
||||||
## Strategie & Laufzeitkomplexität
|
|
||||||
|
|
||||||
Mein Ansatz für `autoGuess()` verwendet **Binary Search** (Binäre Suche). Die Suchgrenzen starten bei `Integer.MIN_VALUE` und `Integer.MAX_VALUE`. In jedem Schritt wird die Mitte des verbleibenden Bereichs geraten und mit `isBigger()` geprüft, ob die Zielzahl größer oder kleiner ist. Dadurch halbiert sich der Suchbereich mit jedem Versuch.
|
|
||||||
|
|
||||||
**Laufzeitkomplexität: O(log n)**, wobei n die Größe des Suchbereichs ist (hier 2^32 für den gesamten int-Bereich). Das ergibt maximal ~32 Rateversuche, unabhängig von der Zielzahl.
|
|
||||||
|
|
||||||
## Vergleich: Eigene Lösung vs. KI-Lösung
|
|
||||||
|
|
||||||
Die KI (Claude) hat die gleiche Aufgabe erhalten — die vollständige NumberGuesser-Klasse mit einer leeren `autoGuess()`-Methode zum Ausfüllen.
|
|
||||||
|
|
||||||
**Ergebnis:** Beide Implementierungen sind **identisch**. Sowohl meine als auch die KI-Lösung verwenden Binary Search mit denselben Suchgrenzen und derselben overflow-sicheren Berechnung der Mitte (`low + (high - low) / 2`).
|
|
||||||
|
|
||||||
**Gemeinsamkeiten:**
|
|
||||||
- Beide nutzen Binary Search über den gesamten int-Bereich
|
|
||||||
- Gleiche Initialisierung: `low = Integer.MIN_VALUE`, `high = Integer.MAX_VALUE`
|
|
||||||
- Gleiche Berechnung des Mittelpunkts: `low + (high - low) / 2` (overflow-sicher)
|
|
||||||
- Gleiche Abbruchbedingung: `while (low <= high)`
|
|
||||||
|
|
||||||
**Unterschiede:**
|
|
||||||
- Keine funktionalen Unterschiede in `autoGuess()`
|
|
||||||
|
|
||||||
## KI-Abschätzung der Laufzeitkomplexität
|
|
||||||
|
|
||||||
**KI-Antwort:** "Runtime complexity for both: O(log n) where n = 2^32 (the full int range), so at most ~32 iterations."
|
|
||||||
|
|
||||||
**Bewertung:** Die KI liegt **richtig**. Binary Search halbiert den Suchbereich in jedem Schritt, was zu einer logarithmischen Laufzeit führt. Für den gesamten int-Bereich von 2^32 Werten ergibt das log₂(2^32) = 32 Schritte im Worst Case. Dies ist deutlich effizienter als ein linearer Ansatz mit O(n), der im schlimmsten Fall alle ~4,3 Milliarden Werte durchprobieren müsste.
|
|
||||||
|
|
||||||
# Task 4
|
|
||||||
|
|
||||||
## Strategie & Laufzeitkomplexität
|
|
||||||
|
|
||||||
Mein Ansatz für `isAnagram()` wandelt beide Wörter in char-Arrays um, sortiert diese mit `Arrays.sort()` und vergleicht die sortierten Strings. Wenn die sortierten Versionen gleich sind, handelt es sich um ein Anagramm.
|
|
||||||
|
|
||||||
**Laufzeitkomplexität: O(n log n)**, wobei n die Länge des längeren Wortes ist. Der dominierende Faktor ist das Sortieren der char-Arrays (`Arrays.sort()` verwendet Dual-Pivot Quicksort mit O(n log n)). Der anschließende Vergleich ist nur O(n).
|
|
||||||
|
|
||||||
Ein effizienterer Ansatz wäre O(n) mit einer HashMap/Array, die die Buchstabenhäufigkeiten zählt — allerdings ist O(n log n) für typische Wortlängen völlig ausreichend.
|
|
||||||
|
|
||||||
## Vergleich: Eigene Lösung vs. KI-Lösung
|
|
||||||
|
|
||||||
Beide Lösungen verwenden den **gleichen Algorithmus**: Sortieren der Zeichen und Vergleich der sortierten Arrays.
|
|
||||||
|
|
||||||
**Gemeinsamkeiten:**
|
|
||||||
- Beide nutzen `Arrays.sort()` auf char-Arrays
|
|
||||||
- Gleicher algorithmischer Ansatz: Sort-and-Compare
|
|
||||||
- Gleiche Laufzeitkomplexität: O(n log n)
|
|
||||||
|
|
||||||
**Unterschiede:**
|
|
||||||
- **Rückgabetyp:** Meine Lösung gibt `void` zurück und druckt das Ergebnis direkt auf die Konsole. Die KI gibt `boolean` zurück — sauberer, da die Methode so wiederverwendbar ist.
|
|
||||||
- **Null-Check:** Die KI prüft auf `null`-Werte (`if (word0 == null || word1 == null)`). Meine Lösung tut dies nicht, würde also bei `null`-Eingaben eine `NullPointerException` werfen.
|
|
||||||
- **Vergleichsmethode:** Meine Lösung erstellt neue Strings und vergleicht mit `String.equals()`. Die KI vergleicht die char-Arrays direkt mit `Arrays.equals()`, was einen unnötigen String-Allokationsschritt spart.
|
|
||||||
- **Hilfsmethode:** Meine Lösung lagert das Sortieren in eine eigene `sortString()`-Methode aus. Die KI schreibt alles inline in `isAnagram()`.
|
|
||||||
|
|
||||||
# Task 5
|
|
||||||
|
|
||||||
## Ergebnisse
|
|
||||||
|
|
||||||
| maximum | Anzahl Primzahlen (Algorithmus) | maximum/ln(maximum) (Schätzung) |
|
|
||||||
|--------:|-------------------------------:|-------------------------------:|
|
|
||||||
| 100 | 25 | 21,7 |
|
|
||||||
| 500 | 95 | 80,5 |
|
|
||||||
| 1.000 | 168 | 144,8 |
|
|
||||||
| 2.000 | 303 | 263,1 |
|
|
||||||
| 5.000 | 669 | 587,0 |
|
|
||||||
| 10.000 | 1.229 | 1.085,7 |
|
|
||||||
| 20.000 | 2.262 | 2.019,5 |
|
|
||||||
| 50.000 | 5.133 | 4.621,2 |
|
|
||||||
| 100.000 | 9.592 | 8.685,9 |
|
|
||||||
| 200.000 | 17.984 | 16.385,3 |
|
|
||||||
|
|
||||||
## Kurvenvergleich
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Die Schätzung durch maximum/ln(maximum) (Primzahlsatz) liegt durchgehend etwas **unter** der tatsächlichen Anzahl der Primzahlen, nähert sich aber mit steigendem maximum proportional an. Beide Kurven zeigen den gleichen sublinearen Wachstumsverlauf.
|
|
||||||
|
|
||||||
## Aufwandsvergleich: Algorithmus vs. Formel
|
|
||||||
|
|
||||||
- **Algorithmus (Zählen durch Sieben):** Für jede Zahl bis `maximum` wird geprüft, ob sie prim ist. Die naive Implementierung hat eine Laufzeitkomplexität von **O(n²)** — für jede der n Zahlen wird im schlimmsten Fall bis n dividiert. Bei maximum = 200.000 sind das potenziell Milliarden von Divisionen.
|
|
||||||
- **Formel maximum/ln(maximum):** Eine einzige Division und ein Logarithmus — **O(1)**, also konstanter Aufwand, unabhängig von der Größe von maximum.
|
|
||||||
|
|
||||||
**Fazit:** Die Formel ist um Größenordnungen schneller, liefert aber nur eine Schätzung. Der Algorithmus liefert das exakte Ergebnis, braucht dafür aber erheblich mehr Rechenzeit. Für große Werte ist die Formel eine sehr gute Approximation.
|
|
||||||
|
|
||||||
## Vergleich: Eigene Lösung vs. KI-Lösung
|
|
||||||
|
|
||||||
Beide Lösungen liefern **identische Ergebnisse** für alle getesteten Werte. Der zentrale Unterschied liegt in der `isPrime()`-Methode:
|
|
||||||
|
|
||||||
**Gemeinsamkeiten:**
|
|
||||||
- Gleiche Gesamtstruktur: Klasse mit `maximum`-Feld, `isPrime()`, `countPrimes()`, `printPrimeNumbers()`
|
|
||||||
- Gleicher Brute-Force-Ansatz: Jede Zahl einzeln auf Primeigenschaft prüfen
|
|
||||||
|
|
||||||
**Unterschiede:**
|
|
||||||
- **Schleifengrenze:** Meine Lösung prüft Teiler bis `number - 1` (O(n) pro Zahl). Die KI prüft nur bis `√number` (`i * i <= number`), was O(√n) pro Zahl ergibt — mathematisch ausreichend, da ein Teiler > √n immer einen Gegenteiler < √n hat.
|
|
||||||
- **Gerade Zahlen:** Die KI schließt gerade Zahlen > 2 sofort aus (`if (number % 2 == 0) return false`) und prüft danach nur ungerade Teiler (`i += 2`). Das halbiert die Anzahl der Divisionen nochmals.
|
|
||||||
- **Sonderfälle:** Die KI behandelt Zahlen < 2 und die Zahl 2 explizit als Sonderfälle.
|
|
||||||
|
|
||||||
**Bewertung:** Die KI-Lösung ist **besser**. Durch die Optimierung der Schleifengrenze auf √n und das Überspringen gerader Zahlen sinkt die Gesamtkomplexität von O(n²) auf ca. O(n√n) — bei maximum = 200.000 ein erheblicher Geschwindigkeitsvorteil. Die Ergebnisse sind trotzdem identisch.
|
|
||||||
|
|
||||||
# Task 6
|
|
||||||
|
|
||||||
## Strategie & Laufzeitkomplexität
|
|
||||||
|
|
||||||
Mein Ansatz verwendet **Natural Merge Sort**. Der Algorithmus erkennt bereits sortierte Teilfolgen (Runs) im Array und verschmilzt jeweils benachbarte Paare. Dieser Vorgang wird wiederholt, bis das gesamte Array sortiert ist.
|
|
||||||
|
|
||||||
**Laufzeitkomplexität: O(n log n)** im Worst Case, **O(n)** im Best Case (bereits sortiertes Array), da dann nur ein einziger Durchlauf ohne Merges nötig ist. Der Speicherbedarf ist O(n) für das temporäre Array.
|
|
||||||
|
|
||||||
## Vergleich: Eigene Lösung vs. KI-Lösung
|
|
||||||
|
|
||||||
Die KI verwendet **Introsort** — einen Hybridalgorithmus aus Quicksort, Heapsort und Insertion Sort. Dies ist der gleiche Ansatz, den die meisten Standardbibliotheken (z.B. C++ `std::sort`) verwenden.
|
|
||||||
|
|
||||||
**Gemeinsamkeiten:**
|
|
||||||
- Beide Algorithmen haben eine Worst-Case-Komplexität von O(n log n)
|
|
||||||
- Beide liefern korrekte, identische Sortier-Ergebnisse
|
|
||||||
- Beide verwenden Hilfsmethoden zur Modularisierung
|
|
||||||
|
|
||||||
**Unterschiede:**
|
|
||||||
- **Algorithmus-Familie:** Meine Lösung ist ein reiner Merge Sort (iterativ, bottom-up). Die KI kombiniert drei verschiedene Algorithmen je nach Situation.
|
|
||||||
- **Adaptivität:** Meine Lösung nutzt natürliche Runs — bei teilweise vorsortierten Daten ist sie schneller. Die KI wechselt bei kleinen Teilarrays (< 16 Elemente) zu Insertion Sort und bei zu tiefer Rekursion zu Heapsort.
|
|
||||||
- **Speicher:** Meine Lösung benötigt O(n) zusätzlichen Speicher für das temp-Array. Die KI sortiert in-place mit nur O(log n) Speicher für den Rekursionsstack.
|
|
||||||
- **Stabilität:** Meine Lösung ist **stabil** (gleiche Elemente behalten ihre Reihenfolge). Die KI-Lösung ist **nicht stabil** (Quicksort-Partition kann die Reihenfolge gleicher Elemente ändern).
|
|
||||||
- **Komplexität des Codes:** Meine Lösung ist deutlich kürzer und einfacher zu verstehen. Die KI-Lösung hat 5 separate Methoden (introsort, partition, heapsort, heapify, insertionSort).
|
|
||||||
- **Pivot-Wahl:** Die KI verwendet Median-of-Three für die Pivot-Wahl, was Worst-Case-Verhalten bei bereits sortierten Arrays vermeidet.
|
|
||||||
|
|
||||||
**Bewertung:** Beide Lösungen sind **gleichwertig gut**, mit unterschiedlichen Stärken. Meine Lösung ist einfacher, stabil und optimal bei teilweise vorsortierten Daten. Die KI-Lösung ist speichereffizienter und garantiert O(n log n) auch im Worst Case durch den Heapsort-Fallback.
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
@@ -1,344 +0,0 @@
|
|||||||
# Aufgabenblatt 2 – Lösungen
|
|
||||||
|
|
||||||
## Aufgabe 1: Maximalwerte in sortierten Arrays
|
|
||||||
|
|
||||||
### Eigene Lösung
|
|
||||||
|
|
||||||
Implementierung in `src/util/Util.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public int findMaxBySorting(int[] array){
|
|
||||||
Sorter sorter = new Sorter();
|
|
||||||
sorter.sort(array);
|
|
||||||
return array[array.length - 1];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Idee:** Nach dem aufsteigenden Sortieren steht der größte Wert zwangsläufig am Ende des Arrays. Es genügt daher ein einziger Zugriff auf `array[array.length - 1]`, um den Maximalwert zurückzugeben.
|
|
||||||
|
|
||||||
### Laufzeitkomplexität (ohne Sortieren)
|
|
||||||
|
|
||||||
**O(1)** – konstante Laufzeit.
|
|
||||||
|
|
||||||
Nach dem Sortieren wird nur eine einzige Operation durchgeführt: der Zugriff auf das letzte Array-Element. Diese Operation ist unabhängig von der Array-Größe `n` und benötigt immer dieselbe Zeit. Es gibt keine Schleife, keine Rekursion und keinen Vergleich – lediglich einen Index-Zugriff.
|
|
||||||
|
|
||||||
(Die Gesamtkomplexität *inklusive* Sortieren liegt bei O(n²) bei Insertion-Sort bzw. O(n log n) bei Merge-Sort.)
|
|
||||||
|
|
||||||
### Vergleich mit KI-Lösung
|
|
||||||
|
|
||||||
Der KI (Claude) wurde die Aufgabenstellung inklusive der Methodensignatur `public int findMaxBySorting(int[] array)` und der Information übergeben, dass eine `insertionSort()`-Methode zur Verfügung steht. Die KI lieferte folgende Lösung:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public int findMaxBySorting(int[] array) {
|
|
||||||
if (array == null || array.length == 0) {
|
|
||||||
throw new IllegalArgumentException("Array darf nicht leer sein");
|
|
||||||
}
|
|
||||||
insertionSort(array);
|
|
||||||
return array[array.length - 1];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Gemeinsamkeiten:**
|
|
||||||
- Gleiche Grundidee: erst sortieren, dann das letzte Element zurückgeben.
|
|
||||||
- Gleiche Laufzeitkomplexität des Nach-dem-Sortieren-Teils: O(1).
|
|
||||||
- Identische Methodensignatur.
|
|
||||||
|
|
||||||
**Unterschiede:**
|
|
||||||
- Die KI ruft `insertionSort()` direkt als Methode derselben Klasse auf. Meine Lösung verwendet die ausgelagerte `Sorter`-Klasse und erzeugt dafür eine Instanz. Das entspricht stärker dem OOP-Gedanken, dass Sortieren eine eigene Verantwortung ist.
|
|
||||||
- Die KI fügt eine explizite Null-/Leer-Prüfung mit `IllegalArgumentException` hinzu. Meine Lösung verzichtet darauf und würde bei einem leeren Array eine `ArrayIndexOutOfBoundsException` werfen. Die KI-Variante ist robuster, meine Variante kompakter.
|
|
||||||
- Die KI nutzt die im Aufgabentext vorgesehene `insertionSort()`-Methode. In meiner aktuellen `Sorter`-Klasse ist `sort()` ein Natural-Merge-Sort aus Aufgabenblatt 1. Funktional liefern beide das korrekte sortierte Ergebnis.
|
|
||||||
|
|
||||||
### KI-Einschätzung zur Laufzeitkomplexität
|
|
||||||
|
|
||||||
**KI-Antwort (wörtlich):**
|
|
||||||
|
|
||||||
> "Lässt man den Aufwand für das Sortieren außer Acht, hat die Methode eine Laufzeitkomplexität von **O(1)**, da nach dem Sortieren lediglich ein einzelner Array-Zugriff auf das letzte Element (`array[array.length - 1]`) durchgeführt wird. Dieser Zugriff ist unabhängig von der Array-Größe in konstanter Zeit möglich. Die Gesamtkomplexität der Methode inklusive Sortierung wird hingegen vom Insertion-Sort dominiert und liegt im Worst-Case bei O(n²)."
|
|
||||||
|
|
||||||
**Eigene Bewertung der Abschätzung:**
|
|
||||||
|
|
||||||
Die Einschätzung der KI ist **korrekt**. Der Zugriff auf ein Element über einen bekannten Index ist in Java eine Operation mit konstantem Aufwand, daher ist O(1) ohne den Sortieranteil richtig begründet. Auch die ergänzende Aussage zur Gesamtkomplexität stimmt: Insertion-Sort hat im Worst-Case quadratischen Aufwand, der damit das O(1) des Maximalwert-Zugriffs vollständig dominiert. Die Antwort ist präzise, vollständig und enthält keine Fehleinschätzung.
|
|
||||||
|
|
||||||
## Aufgabe 2: Werte in sortierten Arrays finden
|
|
||||||
|
|
||||||
### Eigene Lösung
|
|
||||||
|
|
||||||
Implementierung in `src/util/Util.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public Integer findValueBySorting(int @NotNull [] array, int value){
|
|
||||||
Sorter sorter = new Sorter();
|
|
||||||
sorter.sort(array);
|
|
||||||
int min = array[0];
|
|
||||||
int max = array[array.length - 1];
|
|
||||||
if (value < min || value > max){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
int low = 0;
|
|
||||||
int high = array.length - 1;
|
|
||||||
|
|
||||||
while (low <= high) {
|
|
||||||
int mid = (low + high) >>> 1;
|
|
||||||
if (array[mid] == value) {
|
|
||||||
return mid;
|
|
||||||
} else if (array[mid] < value) {
|
|
||||||
low = mid + 1;
|
|
||||||
} else {
|
|
||||||
high = mid - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Idee:** Da das Array nach dem Sortieren aufsteigend geordnet ist, kann genau die gleiche Strategie wie beim Zahlenraten aus Aufgabenblatt 1 (Methode `autoGuess()`) verwendet werden: **Binäre Suche**. In jedem Schritt wird die mittlere Position des aktuell betrachteten Bereichs geprüft. Ist der mittlere Wert gleich dem gesuchten Wert, ist die Position gefunden. Ist er zu klein, wird nur die rechte Hälfte weiter durchsucht; ist er zu groß, nur die linke. Dadurch halbiert sich der Suchbereich in jedem Schritt.
|
|
||||||
|
|
||||||
Zusätzlich wird zu Beginn geprüft, ob der gesuchte Wert überhaupt im Wertebereich des Arrays (`min`..`max`) liegt. Falls nicht, kann direkt `null` zurückgegeben werden, ohne die Suche zu starten.
|
|
||||||
|
|
||||||
Der Ausdruck `(low + high) >>> 1` berechnet die Mitte ohne die Möglichkeit eines Integer-Overflows bei sehr großen Arrays.
|
|
||||||
|
|
||||||
### Laufzeitkomplexität (ohne Sortieren)
|
|
||||||
|
|
||||||
**O(log n)** – logarithmische Laufzeit.
|
|
||||||
|
|
||||||
Begründung: In jedem Schleifendurchlauf wird der verbleibende Suchbereich halbiert. Nach `k` Durchläufen ist der Bereich von ursprünglich `n` Elementen auf `n / 2^k` reduziert. Die Suche endet spätestens, wenn der Bereich leer ist, also bei `k = log₂(n)`. Die Laufzeit wächst damit logarithmisch mit der Array-Größe.
|
|
||||||
|
|
||||||
Die vorgelagerte Bereichsprüfung (`value < min || value > max`) ist O(1) und ändert die Gesamtkomplexität der Suche nicht.
|
|
||||||
|
|
||||||
### Vergleich mit KI-Lösung
|
|
||||||
|
|
||||||
Der KI wurde die Aufgabenstellung sowie der Hinweis auf das Zahlenraten (`autoGuess()`) aus dem vorherigen Aufgabenblatt übergeben. Die KI-Lösung (Claude):
|
|
||||||
|
|
||||||
```java
|
|
||||||
public Integer findValueBySorting(int[] array, int value) {
|
|
||||||
if (array == null || array.length == 0) return null;
|
|
||||||
insertionSort(array);
|
|
||||||
|
|
||||||
int left = 0;
|
|
||||||
int right = array.length - 1;
|
|
||||||
while (left <= right) {
|
|
||||||
int mid = left + (right - left) / 2;
|
|
||||||
if (array[mid] == value) {
|
|
||||||
return mid;
|
|
||||||
} else if (array[mid] < value) {
|
|
||||||
left = mid + 1;
|
|
||||||
} else {
|
|
||||||
right = mid - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Gemeinsamkeiten:**
|
|
||||||
- Identische Grundidee: Sortieren + binäre Suche (das "Zahlenraten"-Prinzip aus `autoGuess()`).
|
|
||||||
- Gleiche Schleifenstruktur mit `left`/`right` bzw. `low`/`high` und Halbierung über die Mitte.
|
|
||||||
- Gleiche Rückgabesemantik: Index bei Treffer, sonst `null`.
|
|
||||||
- Identische Laufzeitkomplexität O(log n) für den Such-Teil.
|
|
||||||
|
|
||||||
**Unterschiede:**
|
|
||||||
- Die KI nutzt `left + (right - left) / 2` zur Berechnung der Mitte, meine Lösung `(low + high) >>> 1`. Beide Varianten sind overflow-sicher (die KI-Variante über Subtraktion, meine über die vorzeichenlose Rechtsverschiebung). Die klassische naive Form `(low + high) / 2` würde beide nicht verwenden.
|
|
||||||
- Die KI prüft `array == null || array.length == 0` als Einstieg und gibt frühzeitig `null` zurück. Meine Lösung nutzt stattdessen `@NotNull` als Annotation und prüft den Wertebereich über `min`/`max`. Das spart bei Werten außerhalb des Bereichs alle Iterationen der Binärsuche.
|
|
||||||
- Die KI ruft `insertionSort()` direkt intern auf; meine Lösung verwendet die externe `Sorter`-Klasse.
|
|
||||||
- Die KI führt keine Min-/Max-Prüfung durch – meine Lösung erkennt dadurch `null`-Fälle ohne Iteration in O(1), während die KI-Variante immer eine volle Binärsuche durchführt (was immer noch O(log n) ist, aber unnötige Vergleiche macht).
|
|
||||||
|
|
||||||
## Aufgabe 3: Merge-Sort
|
|
||||||
|
|
||||||
### Eigene Lösung
|
|
||||||
|
|
||||||
Für Aufgabenblatt 1 hatte ich in der `Sorter`-Klasse bereits eine Variante des Merge-Sorts implementiert (Methode `sort()`). Diese arbeitet als **Natural-Merge-Sort**: iterativ/bottom-up, findet natürliche aufsteigende Runs und verschmilzt sie paarweise, bis nur noch ein Run übrig ist.
|
|
||||||
|
|
||||||
Zusätzlich habe ich für diese Aufgabe eine klassische **rekursive Top-Down-Variante** als `mergeSort(int[] array)` ergänzt (in `src/util/Sorter.java`). Das ist die Variante, die als Grundlage für Aufgabe 4 (`optimizedMergeSort`) dient, da die Insertion-Sort-Schwelle bei ≤ 10 Elementen nur bei rekursiver Unterteilung sinnvoll an jeder Teilungsebene greift.
|
|
||||||
|
|
||||||
```java
|
|
||||||
public void mergeSort(int[] array) {
|
|
||||||
if (array == null || array.length <= 1) return;
|
|
||||||
int[] temp = new int[array.length];
|
|
||||||
mergeSortRecursive(array, temp, 0, array.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void mergeSortRecursive(int[] array, int[] temp, int left, int right) {
|
|
||||||
if (left >= right) return;
|
|
||||||
int mid = (left + right) >>> 1;
|
|
||||||
mergeSortRecursive(array, temp, left, mid);
|
|
||||||
mergeSortRecursive(array, temp, mid + 1, right);
|
|
||||||
merge(array, temp, left, mid, right);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Die bereits existierende private `merge(…)`-Methode wird wiederverwendet. Ein einzelner `temp`-Buffer in der Größe des Arrays wird einmal allokiert und durch alle Rekursionsebenen durchgereicht – das vermeidet wiederholte Allokationen.
|
|
||||||
|
|
||||||
### Vergleich mit KI-Lösung
|
|
||||||
|
|
||||||
Der KI wurde die Aufgabenstellung übergeben. Die KI lieferte eine sehr ähnliche rekursive Top-Down-Variante:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public void mergeSort(int[] array) {
|
|
||||||
if (array.length < 2) return;
|
|
||||||
int mid = array.length / 2;
|
|
||||||
int[] left = new int[mid];
|
|
||||||
int[] right = new int[array.length - mid];
|
|
||||||
System.arraycopy(array, 0, left, 0, mid);
|
|
||||||
System.arraycopy(array, mid, right, 0, array.length - mid);
|
|
||||||
mergeSort(left);
|
|
||||||
mergeSort(right);
|
|
||||||
merge(array, left, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void merge(int[] array, int[] left, int[] right) {
|
|
||||||
int i = 0, j = 0, k = 0;
|
|
||||||
while (i < left.length && j < right.length) {
|
|
||||||
array[k++] = (left[i] <= right[j]) ? left[i++] : right[j++];
|
|
||||||
}
|
|
||||||
while (i < left.length) array[k++] = left[i++];
|
|
||||||
while (j < right.length) array[k++] = right[j++];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Gemeinsamkeiten:**
|
|
||||||
- Gleicher Grundalgorithmus: rekursive Halbierung bis zur Basisgröße 1, dann paarweises Mischen.
|
|
||||||
- Gleiche Laufzeitkomplexität O(n log n) im Average- und Worst-Case.
|
|
||||||
- Beide sind stabil.
|
|
||||||
|
|
||||||
**Unterschiede:**
|
|
||||||
- **Speicherverwaltung:** Die KI-Variante allokiert in jedem Rekursionsschritt zwei neue Subarrays (`left` und `right`) und kopiert via `System.arraycopy` hinein. Meine Variante allokiert genau einen `temp`-Buffer der Gesamtgröße und arbeitet mit Indizes darauf. Meine Variante ist speichereffizienter (Gesamt-Zusatzspeicher O(n) statt O(n log n) durch die Rekursions-Kopien).
|
|
||||||
- **Signatur des Mischens:** Die KI übergibt Subarrays, meine Lösung arbeitet mit Index-Grenzen `(left, mid, right)` auf einem gemeinsamen Puffer. Meine Form ist in der Lehrbuchtradition (z.B. Cormen CLRS) üblich.
|
|
||||||
- **Index-Mittelpunkt:** Meine Lösung nutzt `(left + right) >>> 1` (overflow-sicher); die KI nutzt `array.length / 2` im jeweiligen Rekursionsframe, was durch die neuen Subarrays das Overflow-Problem umgeht.
|
|
||||||
- **Besonderheit meiner AB1-Lösung:** Zusätzlich existiert `sort()` als Natural-Merge-Sort – iterative Variante, die bereits sortierte Teilsequenzen im Input ausnutzt und für fast-sortierte Arrays deutlich schneller als die klassische Variante ist. Die KI hat diese Variante nicht angeboten; sie ist eine Optimierung, die man explizit anfragen müsste.
|
|
||||||
|
|
||||||
Insgesamt ist die Varianz bei Merge-Sort wie erwartet hoch. Die Algorithmenidee ist identisch, die Implementierungsdetails (Speicher, Indizes vs. Subarrays) unterscheiden sich deutlich.
|
|
||||||
|
|
||||||
## Aufgabe 4: Optimierter Merge-Sort
|
|
||||||
|
|
||||||
### Eigene Lösung
|
|
||||||
|
|
||||||
Zunächst wurde `insertionSort(int[] array)` in `src/util/Sorter.java` implementiert (klassischer Insertion-Sort):
|
|
||||||
|
|
||||||
```java
|
|
||||||
public void insertionSort(int[] array) {
|
|
||||||
if (array == null || array.length <= 1) return;
|
|
||||||
for (int i = 1; i < array.length; i++) {
|
|
||||||
int key = array[i];
|
|
||||||
int j = i - 1;
|
|
||||||
while (j >= 0 && array[j] > key) {
|
|
||||||
array[j + 1] = array[j];
|
|
||||||
j--;
|
|
||||||
}
|
|
||||||
array[j + 1] = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Der Inhalt von `mergeSort` wurde dann in `optimizedMergeSort` kopiert und so angepasst, dass für Teilbereiche der Größe ≤ 10 statt weiterer Rekursion direkt Insertion-Sort auf dem Bereich läuft:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public void optimizedMergeSort(int[] array) {
|
|
||||||
if (array == null || array.length <= 1) return;
|
|
||||||
int[] temp = new int[array.length];
|
|
||||||
optimizedMergeSortRecursive(array, temp, 0, array.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void optimizedMergeSortRecursive(int[] array, int[] temp, int left, int right) {
|
|
||||||
if (left >= right) return;
|
|
||||||
if (right - left + 1 <= 10) {
|
|
||||||
insertionSortRange(array, left, right);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int mid = (left + right) >>> 1;
|
|
||||||
optimizedMergeSortRecursive(array, temp, left, mid);
|
|
||||||
optimizedMergeSortRecursive(array, temp, mid + 1, right);
|
|
||||||
merge(array, temp, left, mid, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void insertionSortRange(int[] array, int left, int right) {
|
|
||||||
for (int i = left + 1; i <= right; i++) {
|
|
||||||
int key = array[i];
|
|
||||||
int j = i - 1;
|
|
||||||
while (j >= left && array[j] > key) {
|
|
||||||
array[j + 1] = array[j];
|
|
||||||
j--;
|
|
||||||
}
|
|
||||||
array[j + 1] = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Wichtig: Der In-Place-Helper `insertionSortRange(array, left, right)` arbeitet direkt auf dem Bereich des Haupt-Arrays, damit die Sortierung konsistent mit dem anschließenden `merge(…)` über die Indizes bleibt. Eine Kopie in ein neues Subarray wäre für kleine Bereiche zwar möglich, aber überflüssig.
|
|
||||||
|
|
||||||
Getestet mit `testOptimizedMergeSort()` (ebenfalls in `Sorter.java`).
|
|
||||||
|
|
||||||
### KI-Antwort: Wie heißt dieses Sortierverfahren? Tipps?
|
|
||||||
|
|
||||||
> "Das Verfahren, das du hier implementiert hast, ist eine klassische **Hybrid-Sortierung** bzw. **getunter Merge-Sort** (engl. *tuned merge sort*). Es nutzt die Tatsache, dass Insertion-Sort für kleine Arrays aufgrund sehr niedriger konstanter Faktoren und Cache-Freundlichkeit praktisch schneller ist als Merge-Sort, obwohl es asymptotisch bei O(n²) liegt. Das Prinzip ist genau dasselbe, das in der Praxis z.B. in **Timsort** (Pythons Standard-Sortierung, Javas `Arrays.sort` für Objekte) oder in Sedgewicks *practical mergesort* verwendet wird.
|
|
||||||
>
|
|
||||||
> Ein paar Tipps:
|
|
||||||
> 1. **Schwellwert empirisch wählen:** Je nach Hardware, JIT und Datentyp liegt das Optimum zwischen ca. 7 und 32. 10 ist ein guter Standardwert, aber Benchmarks lohnen sich. Sedgewick empfiehlt z.B. 15.
|
|
||||||
> 2. **Small-run detection:** In Timsort werden zusätzlich natürliche aufsteigende/absteigende Läufe erkannt und nur kurze Läufe per Insertion-Sort verlängert. Das ist eine natürliche Weiterentwicklung deiner Optimierung.
|
|
||||||
> 3. **Merge vermeiden, wenn schon sortiert:** Eine billige Prüfung `array[mid] <= array[mid+1]` vor dem Mischen kann in fast-sortierten Eingaben viele Merge-Aufrufe einsparen.
|
|
||||||
> 4. **Iterativ statt rekursiv:** Für sehr große Arrays lohnt sich eine Bottom-Up-Variante (wie dein `sort()`), da sie weniger Methodenaufruf-Overhead hat und besser vorhersagbare Cache-Zugriffe erzeugt.
|
|
||||||
> 5. **Alloc-freies Merging:** Der gemeinsame `temp`-Puffer ist bereits gut gelöst – häufiger Fehler in naiven Varianten ist pro Merge-Schritt neu zu allokieren."
|
|
||||||
|
|
||||||
Die KI-Antwort ist inhaltlich korrekt und enthält keine Fehleinschätzung. Die Bezeichnung als "getuner Merge-Sort" bzw. "Hybrid-Merge-Sort" mit Verweis auf Timsort trifft zu. Die Tipps sind realistisch und entsprechen dem, was man in Lehrbüchern (Sedgewick, CLRS) und in produktiven Sortier-Implementierungen tatsächlich sieht. Besonders der Hinweis auf die `array[mid] <= array[mid+1]`-Vorprüfung ist eine etablierte, einfach umzusetzende Zusatzoptimierung.
|
|
||||||
|
|
||||||
## Aufgabe 5: Umgekehrte Sortierreihenfolge
|
|
||||||
|
|
||||||
Die drei Methoden `insertionSort`, `mergeSort` und `optimizedMergeSort` wurden so angepasst, dass sie absteigend sortieren (größter Wert vorne, kleinster Wert hinten).
|
|
||||||
|
|
||||||
### Vorüberlegung
|
|
||||||
|
|
||||||
Das zugrunde liegende Prinzip ist einfach: In einem Vergleichs-Sortierer legt genau **ein einziger Vergleichsoperator** die Ordnung fest. Dreht man ihn um, kippt die gesamte Ordnung. Konkret werden zwei Arten von Vergleichen umgedreht:
|
|
||||||
|
|
||||||
| Verfahren | Relevanter Vergleich (aufsteigend) | Umgedreht (absteigend) |
|
|
||||||
|--------------|-------------------------------------|------------------------|
|
|
||||||
| Insertion | `array[j] > key` (größere schieben rechts) | `array[j] < key` (kleinere schieben rechts) |
|
|
||||||
| Merge | `temp[i] <= temp[j]` (links wählen bei ≤) | `temp[i] >= temp[j]` (links wählen bei ≥) |
|
|
||||||
|
|
||||||
### Wichtige Randbedingung: `sort()` und `merge()` bleiben unverändert
|
|
||||||
|
|
||||||
Die alte `sort()`-Methode (Natural-Merge-Sort aus Aufgabenblatt 1) und die private `merge()`-Methode bleiben unverändert aufsteigend, weil sie von `Util.findMaxBySorting` und `Util.findValueBySorting` (Aufgaben 1 und 2) benötigt werden. Dort würde eine absteigende Sortierung z.B. dazu führen, dass `array[array.length - 1]` nicht mehr den Maximalwert liefert, und die binäre Suche würde ebenfalls nicht mehr funktionieren. Eine einfache Änderung direkt an `merge()` hätte diese Methoden deshalb kaputt gemacht.
|
|
||||||
|
|
||||||
Lösung: es wurde eine neue private Methode `mergeDescending(…)` als Spiegelbild von `merge(…)` hinzugefügt, bei der der Merge-Vergleich umgedreht ist.
|
|
||||||
|
|
||||||
### Konkrete Änderungen in `src/util/Sorter.java`
|
|
||||||
|
|
||||||
**1. `insertionSort(int[] array)`** – Schleifenbedingung umgedreht:
|
|
||||||
|
|
||||||
```diff
|
|
||||||
- while (j >= 0 && array[j] > key) {
|
|
||||||
+ while (j >= 0 && array[j] < key) {
|
|
||||||
```
|
|
||||||
|
|
||||||
Folge: Es werden jetzt nur noch Elemente nach rechts verschoben, die *kleiner* als `key` sind. Größere Elemente bleiben links – größte Werte wandern an den Anfang.
|
|
||||||
|
|
||||||
**2. `mergeSortRecursive(…)`** – Aufruf der Merge-Methode umgestellt:
|
|
||||||
|
|
||||||
```diff
|
|
||||||
- merge(array, temp, left, mid, right);
|
|
||||||
+ mergeDescending(array, temp, left, mid, right);
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. `optimizedMergeSortRecursive(…)`** – gleicher Aufruf-Wechsel:
|
|
||||||
|
|
||||||
```diff
|
|
||||||
- merge(array, temp, left, mid, right);
|
|
||||||
+ mergeDescending(array, temp, left, mid, right);
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. `insertionSortRange(…)`** (Helper für `optimizedMergeSort`) – Schleifenbedingung umgedreht:
|
|
||||||
|
|
||||||
```diff
|
|
||||||
- while (j >= left && array[j] > key) {
|
|
||||||
+ while (j >= left && array[j] < key) {
|
|
||||||
```
|
|
||||||
|
|
||||||
**5. Neue private Methode `mergeDescending(…)`** – identisch zu `merge(…)`, nur der Merge-Vergleich ist umgedreht:
|
|
||||||
|
|
||||||
```diff
|
|
||||||
- if (temp[i] <= temp[j]) {
|
|
||||||
+ if (temp[i] >= temp[j]) {
|
|
||||||
```
|
|
||||||
|
|
||||||
### Zusammenfassung
|
|
||||||
|
|
||||||
Drei Methoden sortieren jetzt absteigend. Die semantische Änderung besteht jeweils aus einem einzigen umgedrehten Vergleichsoperator. Weil die neue private `mergeDescending`-Methode eingeführt wurde, konnten Aufgabe 1 und Aufgabe 2 ihre ursprüngliche aufsteigende `sort()`-Grundlage behalten und funktionieren unverändert.
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package util;
|
|
||||||
|
|
||||||
public class test {
|
|
||||||
public static void main(String[] args){
|
|
||||||
Util util = new Util();
|
|
||||||
int[] arr = new int[1000];
|
|
||||||
util.fillArrayRandom(arr, 10000);
|
|
||||||
util.findValueBySorting(arr, 288);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user