Arrays verwalten — Umgang mit freien Zellen, etc.
in Arbeit ...
Sehr häufig werden Arrays als (geordneter oder ungeordneter) Werte-Container verwendet, der häufig nicht ganz voll ist. Um dies umsetzen zu können, müssen Strategien für den Umgang mit leeren Zellen definiert und umgesetzt werden.
Eine "sehr teure" Variante wäre, parallel zum Werte-Array ein Boolean-Array bereitzustellen, in dem leere Zellen z.B. mit false
markiert werden.
Dies ist tatsächlich ein letzter Ausweg, wenn exotische Anforderungen bestehen und die im Weiteren gezeigten Ansätze nicht verwendbar sind.
1. Verwendung eines reservierten Leer-Wertes
Eine sehr einfache Möglichkeit besteht darin, einen im jeweils vorliegenden Datenbestand nicht gültigen oder nicht benötigten Wert dafür zu reservieren – z.B. -1
oder -999
für Stückzahlen (die ja üblicherweise nicht negativ sein können).
Alternativ kann man z.B. den kleinsten oder größten für den vorliegenden Datentyp möglichen Zahlenwert verwenden.
Dafür gibt es für alle Zahlentypen jeweils Konstante der jeweiligen Wrapper-Klasse (können – sehr vereinfacht ausgedrückt – anstelle der jeweiligen primitiven Typen verwendet werden, wenn Zahlen als Objekte benötigt werden).
Z.B. bei Int-Werten: Integer.MIN_VALUE
und Integer.MAX_VALUE
,
analog für long
und short
: Long.MIN_VALUE
, … Short.MAX_VALUE
Für double
und float
hingegen: -Float.MAX_VALUE
(mathematisch kleinster, maximal negativer Wert, beachte das MINUS!) oder Float.MAX_VALUE
,
analog: -Double.MAX_VALUE
oder Double.MAX_VALUE
Für Float
und Double
hat MIN_VALUE
– leider inkonsistent – die Bedeutung "kleinster von 0 unterscheidbarer Wert".
Idealerweise definiert man eine Konstante LEER_WERT
– ein sprechender Name für die Bedeutung des Wertes:
// Konstante, die irgendeinen für die enthaltenen Daten ungültigen Wert
//.. erhält (z.B. -999). Dieser dient zur Kennzeichnung leerer Zellen:
public static final int LEER_WERT = Integer.MIN_VALUE; // -2_147_483_648
public void leereZellen1(int[] arr) {
arr[5] = LEER_WERT;
System.out.println("===== Array 'arr' (Kapazität: " + arr.length + "): =====");
for (int i = 0; i < arr.length; i++) {
if (arr[i] == LEER_WERT) { // Zelle ist leer!!!
System.out.println(" arr[" + i + "]: " + arr[i] + " (LEERE Zelle!)");
} else { // Normalfall
System.out.println(" arr["+ i + "]=" + arr[i]);
}
}
}
public void runTheMethod20() {
int[] daten = {369, -1, 99, LEER_WERT, 0, -12};
leereZellen1(daten);
System.out.println("(Konstante LEER_WERT=" + LEER_WERT + ")");
}
liefert:
===== Array 'arr' (Kapazität: 6): ===== arr[0]=369 arr[1]=-1 arr[2]=99 arr[3]: -2147483648 (LEERE Zelle!) arr[4]=0 arr[5]: -2147483648 (LEERE Zelle!) (Konstante LEER_WERT=-2147483648)
Berechnung des Durchschnitts der tatsächlichen Werte sieht dann so aus:
public final int EMPTY_VAL = Integer.MIN_VALUE; // -2_147_483_648
public double smartAverageOf(int[] werte) {
int sum = 0;
int count = 0;
for (int i = 0; i < werte.length; i++) {
if (werte[i] != EMPTY_VAL) { // wegfiltern Leer-Werte
sum += werte[i];
count++;
}
}
return (double)sum/count;
}
public double runTheMethod21() {
int[] data = {2, 3, EMPTY_VAL, 1, 4, EMPTY_VAL};
return smartAverageOf(data); // liefert 2.5 zurück
}
2. Arbeiten mit lückenlos gefülltem Array und "Füllstand"
Wenn man dafür sorgt, dass das Array immer lückenlos von Zelle 0 weg gefüllt (oder leer) ist, kann man den Index des ersten freien Feldes in einer Füllstand-Variable
(im weiteren freeIdx
genannt) verwalten.
Beim Hinzufügen und Entfernen eines Elements ist jeweils der Füllstand zu adaptieren.
-
Ein
freeIdx
von 0 bedeutet, dass das Array leer ist -
Ein
freeIdx
gleich der Array-Länge bedeutet, das Array ist voll
Am besten geeignet ist das Konzept für Arrays, die als Instanzvariable definiert sind, da es einen Satz von Manipulationsmethoden geben muss, die alle auf Array und freeIdx
zugreifen können.
Anderenfalls müsste man immer Array und Füllstand als Parameter übergeben.
Das kann man leicht umsetzen, wenn man eine eigene Klasse für das Array verwendet, die man immer dort als Assoziation verwendet, wo man das Array benötigt.
Im Anschluss 2 Beispiele zur Demonstration des konzepts.
2.1. Beispiel kompakte Klasse:
public class ArrayMitFuellstand
{
private float[] werte;
private int freeIdx;
public ArrayMitFuellstand() {
this(10);
}
public ArrayMitFuellstand(int kapazitaet) {
werte = new float[kapazitaet];
freeIdx = 0;
}
public ArrayMitFuellstand(float[] werte, int freeIdx) {
this.werte = werte;
this.freeIdx = freeIdx;
}
public static void main(String[] args) {
ArrayMitFuellstand amf = new ArrayMitFuellstand(
new float[]{3.0f, 99.f, 12.f, -0.1f, -33f}, 3);
amf.printArray("Nach Erzeugung");
amf.insert(1, 33.3f);
amf.printArray("Nach Einfügen an Pos 1 von 33.3");
amf.insert0(44.4f);
amf.printArray("Nach vorne Einfügen von 44.4");
amf.add(55.5f); // liefert Error - Array voll!
amf.printArray("Nach Versuch, hinten anzuhängen von 55.5");
amf.remove(2);
amf.printArray("Nach Entfernen von 33.3");
}
public boolean full() { // Array voll ?
return freeIdx == werte.length;
}
public boolean add(float val) { // neuen 'val' am Ende dazufügen (in 'freeIdx')
return insert(freeIdx, val); // nutzt die flexible Methode 'insert'
}
public boolean insert0(float val) { // neuen Wert am Anfang "hineinquetschen"
return insert(0, val); // nutzt die flexible Methode 'insert'
}
public boolean insert(int idx, float val) {
boolean done = false;
if (idx < 0) {
System.out.format("ERR in insert(..) - idx < 0: %d%n", idx);
} else if (idx > freeIdx) {
System.out.format("ERR in insert(..) - idx (%d) > freeIdx (%d)%n", idx, freeIdx);
} else if (full()) {
System.out.format("ERR in insert(..) - Array voll%n");
} else {
for (int i = freeIdx; i > idx; i--) { // i ist pos, AUF die ich hin-verschiebe
werte[i] = werte[i-1]; // von "rechts" kommend Werte in höhere Zelle "ziehen"
}
werte[idx] = val; // eigentliches 'insert'
freeIdx++; // Füllstand erhöhen
done = true;
}
return done;
}
public boolean remove(int idx) {
boolean done = false;
if (idx < 0) {
System.out.format("ERR in remove(..) - idx < 0: %d%n", idx);
} else if (idx >= freeIdx) {
System.out.format("ERR in remove(..) - idx >= freeIdx (%d): %d%n", idx, freeIdx);
} else {
for (int i = idx; i < freeIdx-1; i++) { // i ist pos, in die ich "hineinziehe"
werte[i] = werte[i+1]; // von "links" kommend Werte von oberer Zelle holen
}
done = true;
freeIdx--; // könnte auch vor Werte-Verschiebungen passieren, dann oben kein "-1"
}
return done;
}
public void printArray(String title) {
System.out.format("=== %s, Kapazität %d, Füllstand %d: ===%n", title, werte.length, freeIdx);
for (int i = 0; i < freeIdx; i++) {
System.out.format(" %1.2f", werte[i]);
}
System.out.println();
}
}
Liefert bei Aufruf der statischen Methode 'main(..)':
=== Nach Erzeugung, Kapazität 5, Füllstand 3: === 3,00 99,00 12,00 === Nach Einfügen an Pos 1 von 33.3, Kapazität 5, Füllstand 4: === 3,00 33,30 99,00 12,00 === Nach vorne Einfügen von 44.4, Kapazität 5, Füllstand 5: === 44,40 3,00 33,30 99,00 12,00 ERR in insert(..) - Array voll === Nach Versuch, hinten anzuhängen von 55.5, Kapazität 5, Füllstand 5: === 44,40 3,00 33,30 99,00 12,00 === Nach Entfernen von xx.x, Kapazität 5, Füllstand 4: === 44,40 3,00 99,00 12,00
2.2. Beispiel umfangreiche Klasse
Hier sind viele Kommentare enthalten. Mehr Methoden, mehr Funktionalität (z.B. mehrspaltige tabellarische Ausgabe)
Die Methoden zum Einfügen und Entfernen von Werten sind besonders wichtig, da diese den Füllstand verwalten müssen:
boolean append(int value)
, boolean insert(int idx, int value)
und boolean remove(int idx)
(selbst zu implementieren).
/* zum Spaltenzählen (j = Spalte 100) - falls Editor (wie BlueJ) keine Spaltenanzeige hat:
* Source-Code 100 bis max. 120 Spalten breit, sonst Druck- und Übersichtlichkeits-Probleme!
* 4 6 8 a 2 4 6 8 b 2 4 6 8 c 2 4 6 8 d 2 4 6 8 e 2 4 6 8 f 2 4 6 8 g 2 4 6 8 h 2 4 6 8 i 2 4 6 8 j
*/
/**
* Demo lückenloses Array und dazugehörige Operationen.
* <p>Wenn man dafür sorgt, dass das Array immer einen bestimmten Füllstand OHNE Lücke hat,
* kann man den Index des ersten freien Feldes in einer Füllstand-Variable ({@code freeIdx)}
* verwalten.</p><p>Beim Hinzufügen und Entfernen eines Elements ist jeweils der Füllstand zu
* adaptieren.</p>
* <p>Im vorliegenden Beispiel ist das Array als Instanzvariable/Attribut definiert. Konsistent
* dazu muss auch {@code freeIdx} an dieser Stelle definiert sein.</p>
* <p>Sinnvoll zum Testen: eher kleine Kapazitäten, z.B. 3-7, bei Ausgabe Spaltenzahl 2-5</p>
*
* @author Maximilian Renkin
* @version 2020-03-01
*/
public class SmartIntContainer
{
// Solange wir noch keine Exceptions verwenden können:
public static final int BAD_VAL = Integer.MIN_VALUE; // als Fehler-Rückgabewert
private int[] values;
private int freeIdx;
/**
* Konstruktor mit Parameter für vorzusehende Array-Kapazität
* @param capacity die gewollte Kapazität (>= 0!!)
*/
public SmartIntContainer(int capacity) {
if (capacity <= 0) {
if (capacity < 0) {
System.out.println("Negative Array-Kapazität unmöglich, gewünscht war aber: "
+ capacity + " ->\n ein Array der Länge 0 wird erzeugt.");
} else { // Kapazität 0 grundsätzlich möglich und auch oft sinnvoll!
System.out.println("Array-Kapazität ist 0");
}
values = new int[0]; // ein Array der Länge 0 wird erzeugt.
//values = new int[]{}; // oder alternativ so!
} else {
values = new int[capacity];
}
freeIdx = 0;
}
/**
* Konstruktor, der DIREKT das fertige Array UND den Füllstand erhält.
* Alles, was ab 'freeIdx' an Werten enthalten ist, wird ignoriert!!!
* In BlueJ kann beim Objekt-Erzeugen direkt ein
* Array übergeben werden in der Form: <code>new int[5] oder {17, -5, 3, 0, 1}</code>
* @param theArray Array wird verwendet. Falls null, wir neues mit Länge 0 erzeugt.
* @param freeIdx Füllstand wird verwendet. Falls zu groß, wird Arrraylänge gesetzt.
*/
public SmartIntContainer(int[] theArray, int freeIdx) {
if (theArray == null) {
System.out.println("Übergebenes Array ist NULL, Array mit Länge 0 wird erzeugt.");
values = new int[]{}; // ein Array der Länge 0 wird erzeugt.
} else if (theArray.length == 0) {
values = theArray; // vorhandenes Array kann verwendet werden.
} else {
values = theArray;
}
this.freeIdx = (freeIdx > values.length) ? values.length : freeIdx; // Spezial-If-Else!
// äquivalent zu:
// if (freeIdx > values.length) {
// this.freeIdx=values.length;
// } else {
// this.freeIdx=freeIdx;
// }
}
/**
* Standard-Konstruktor, der ein Array mit 5 Zellen erzeugt.
*/
public SmartIntContainer() {
// man kann als ERSTES Statement auch einen anderen Konstruktor aufrufen mit this(...)
this(5); // damit erspart man sich u.U. Prüfungen, die ja schon im aufgerufenen
// Konstruktor erfolgen.
// alternativ (und hier fast genauso einfach): values = new int[5]; freeIdx = 0;
}
/**
* Liefert den aktuellen Füllstand des Arrays. Dieser entspricht genau dem Wert des ersten
* freien Index - {@code freeIdx}.
*/
public int usedCells() {
//return 0;
return freeIdx;
}
/**
* Liefert die Anzahl freier Zellen zurück. Für ein leeres Array (freeIdx = 0)
* mit Länge 3 wird geliefert: 3 - 0 = 3,
* wenn es voll ist (freeIdx = 3): 3 - 3 = 0.
*/
public int freeCells() {
return values.length - freeIdx;
}
/**
* Damit kann etwas kompakter und aussagekräftiger (Methodenname entspricht der Intention)
* geprüft werden, ob das Array voll ist. Wird oben im Code verwendet.
*/
public boolean isFull() {
return freeIdx == values.length;
}
/**
* Füllt das Array bis zum gewünschten Füllstand mit Zufallszahlen.
* <p>Optional werden nur die Werte ab dem aktuellen {@code this.freeIdx} bis zum
* übergebenen neuen {@code freeIdx} gefüllt. Werte sind durch {@code minVal} und
* {@code maxVal} begrenzt.</p>
* <p>Zu prüfen ist, ob gewünschter Füllstand mit der bestehenden Kapazität möglich.</p>
* @param newFreeIdx der neu zu erreichende Füllstand. Wenn kleiner als bisher und
* {@code replace} auf {@code false} gesetzt, wird nur Füllstand neu gesetzt, wenn
* {@code replace} auf {@code false} gesetzt UND der neue Füllstand GRÖSSER als der
* bestehende, werden NUR DIE FEHLENDEN Werte zufällig ERGÄNZT.
* @param replace wenn true, werden alle Zellen bis zum neuen Füllstand neu gesetzt, sonst
* nur die zwischen altem und neuem Füllstand
* @param minVal kleinstmöglicher Wert der Zufallszahlen
* @param maxVal grösstmöglicher Wert der Zufallszahlen
* @return {@code false}, wenn freeIdx für bestehende Arraykapazität zu groß ist,
* sonst {@code true}.
*/
public boolean fillWithRandom(int newFreeIdx, boolean replace, int minVal, int maxVal) {
for (int i = 0; i < newFreeIdx; i++) {
//TODO: selbst implementieren!
}
//TODO: selbst implementieren!
return false; // dummy-Wert, um Kompilieren zu ermöglichen.
}
/**
* Fügt den übergebenen Wert als neuen letzten Wert hinzu. Bei Erfolg ist {@code freeIdx}
* um eins zu erhöhen. Wichtig ist die Kontrolle, ob das Array schon voll ist. In diesem
* Falle wird eine Info-Meldung ausgegeben und {@code false} zurückgeliefert (sonst {@code true}).
*/
public boolean append(int value) {
boolean ok = false;
if (value == BAD_VAL) {
System.out.println("FEHLER in append(..) - BAD_VAL übergeben: " + BAD_VAL);
} else if (isFull()) {
System.out.println("Alle Zellen des Array voll (" + values.length + " Zellen)");
// return;
} else {
values[freeIdx] = value;
System.out.println("Zelle " + freeIdx + " auf Wert " + value + " gesetzt");
freeIdx++;
ok = true;
}
return ok;
}
/**
* Zweite Implementation des Anfügens am Ende mithilfe der Methode {@code insert(..)} und
* Verwenden des ersten freien Index ({@code freeIdx}) als Position.
* Der Vorteil einer solchen Vorgehensweise ist u.a., dass damit auch gleich bestimmte
* Fälle für die Methode {@code insert(..)} getestet werden.
*/
public boolean append2(int value) {
return insert(freeIdx, value);
}
/**
* Fügt den Wert als neues erstes Element ein.
* <p>Keine Prüfungen nötig, da diese ohnehin innerhalb der Methode {@code insert(..)}
* durchgeführt werden müssen.</p>
*/
public boolean insertAsFirst(int value) {
return insert(0, value);
}
/**
* Fügt an einem betimmten Index einen neuen Wert ein.
* <p>Der vorher an dieser Stelle gespeicherte Wert soll nicht ersetzt werden, sondern alle
* Zellen ab {@code idx} sollen um einen Platz nach hinten geschoben werden. Dabei ist zu
* beachten, dass das Array nicht schon voll ist.</p>
* <p>Weiters ist zu überprüfen, dass {@code idx} nicht größer als der erste
* freie Platz ist, sonst würde im Array eine Lücke entstehen und das Grundprinzip der
* Lückenlosigkeit wäre verletzt - damit funktioniert die gesamte Logik nicht mehr.</p>
* <p>Damit ist auch sichergestellt (wenn die Obergrenze des {@code freeIdx} sinnvoll
* kontrolliert wird), dass der Index nicht unzulässig groß werden kann.</p>
* <p>Sicherstellung eines nicht-negativen Index muss separat erfolgen.</p>
*/
public boolean insert(int idx, int value) {
boolean ok = false;
if (idx < 0) {
System.out.println("FEHLER in 'insert(..)' - Gegebener Index darf nicht"
+ " negativ sein, ist aber: " + idx);
} else if (isFull()) {
System.out.println("INFO zu insert(..) - Alle Zellen des Array voll ("
+ values.length + " Zellen)");
} else if (idx > freeIdx) { // idx darf noch auf die erste freie Stelle zeigen!!!!
System.out.println("FEHLER in 'insert(..)' - idx (" + idx
+ ") größer als der erste freie Index (" + freeIdx + ") des Arrays");
} else {
openGap(idx); // Platz machen
values[idx] = value; // Wert an freigemachte Stelle schreiben
System.out.println("INFO zu 'insert(..)' - Wert " + value
+ " in Zelle " + idx + " eingefügt");
freeIdx++;
ok = true;
}
return ok;
}
private void openGap(int idx) { // schiebt gültige Werte ab 'idx' um 1 höher im Array
for (int i = freeIdx; i > idx; i--) {
values[i] = values[i-1];
}
values[idx] = -999_999_999; // unnötig, zeigt aber beim Debuggen Korrektheit deutlicher
}
private void closeGap(int idx) { // füllt Lücke in Zelle 'idx' durch nachrücken
//TODO: selbst implementieren
}
/**
* Ersetzt den Inhalt einer gültigen Array-Zelle. <br>
* Fürs Ersetzen bestehender Inhalte muss nichts verschoben werden,
* daher ist die Implementation sehr einfach.
*
* @param idx Index der Zelle, deren Inhalt ersetzt werden soll.
* muss kleiner als freeIdx sein (genutzte Zelle).
* @param value der neue Wert der Zelle.
* @return {@code true}, wenn {@code idx} gültig, sonst {@code false}
*/
public boolean replace(int idx, int value) {
if (idx < 0 || idx >= freeIdx) {
System.out.println("FEHLER in 'replace(..)' - idx ungültig (Füllstand="
+ freeIdx + "): " + idx);
return false;
} else {
values[idx] = value;
return true;
}
}
/**
* Entfernt das Element am gegebenen Index. Nach Entfernen müssen alle dahinter liegenden
* Zellen um einen Platz nach vorne gerückt werden. Sinngemäß müssen alle Aspekte, die in
* Methode {@code insert(..)} erwähnt sind, auch hier berücksichtigt werden.
*/
public boolean remove(int idx) {
//TODO: selbst zu implementieren!!
return false; //TODO: Platzhalter, damit kein Kompilierfehler auftritt
}
/**
* Entfernt das letzte Element.
* <p>Keine Prüfungen nötig, da diese ohnehin innerhalb der Methode {@code remove(..)}
* durchgeführt werden müssen.</p>
*/
public boolean removeLast() {
return remove(freeIdx-1);
}
/**
* Entfernt das erste Element.
* <p>Keine Prüfungen nötig, da diese ohnehin innerhalb der Methode {@code remove(..)}
* durchgeführt werden müssen.</p>
*/
public boolean removeFirst() {
return remove(0);
}
/**
* Liefert den angegebenen Zell-Wert.
* @param idx der Index des zu liefernden Zell-Wertes
* @return Wert der angegebenen Zelle oder BAD_VAL bei ungültigem Index
* (solange wir noch keine Exceptions verwenden können)
*/
public int getCell(int idx) {
// Solange wir noch keine Exceptions verwenden können:
if (idx < 0 || idx >= freeIdx) {
System.out.println("FEHLER in getCell(..) - ungültiger idx: " + idx);
return BAD_VAL;
}
return values[idx];
}
/**
* Gibt das Array tabellarisch mit Titelzeile und Abschlusszeile am Bildschirm aus.
* @param columns Anzahl der zu druckenden Spalten. Sinnvoll zum Testen: 3 Spalten
*/
public void printArray(int cols) {
String title = "==== Int-Array mit Kapazität " + values.length + ", derzeit "
+ usedCells() + " Zellen genutzt ====";
System.out.println(title);
if (usedCells() == 0) {
System.out.println("\t--- leer ---");
} else {
for (int i = 0; i < freeIdx; i++) {
if (i > 0 && i % cols == 0) { // nach z.B. jedem 3. Wert (wenn Divisionsrest 0
System.out.println(); // außer Beginn) Zeilenschaltung einfügen
}
System.out.print("\t" + values[i]); // \t ... Tabulator, für schöne Tabelle
}
System.out.println();
}
// "-+".repeat(3) gibt den String "---" zurück. Nachfolgend wird Abschlussleiste
// mit exakt der Länge des Titeltexts produziert:
System.out.println("-".repeat(title.length()) + "\n"); // \n ... zusätzliche Neuzeile
}
}
Wichtig zur Qualitätskontrolle ist hier das Erstellen von Unit-Tests/einer Test-Klasse, in der sowohl gültige als auch ungültige Parameter (und auch die Grenzfälle) getestet werden.
Sinnvoll zum Testen: eher kleine Kapazitäten, z.B. 3-7, bei Ausgabe Spaltenzahl 2-5
2.3. Erläuterung "openGap(int idx)"
Der Algorithmus des Lücke-Schaffens lässt sich sehr leicht z.B. mit einer Box mit mehreren nebeneinanderliegenden Abschnitten umsetzen, die lückenlos, aber nicht ganz voll gefüllt ist ### lückenlos mit unterscheidbaren Objekten (z.B. größengeordneten Kugeln) gefüllt sind, wobei mindestens eine Box noch ungenutzt ist.
Index | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
Wert |
17 |
-12 |
33 |
0 |
99 |
Hier nochmals die Methode openGap(…)
:
private void openGap(int idx) { // schiebt gültige Werte ab 'idx' um 1 höher im Array
//Zu Beginn Prüfung ob idx < 0, > freeIdx, Array voll!!
for (int i = freeIdx; i > idx; i--) {
values[i] = values[i-1];
}
values[idx] = -999_999_999; // unnötig, zeigt aber beim Debuggen Korrektheit besser
}
Ein anderes Beispiel:
Füllstand/freeIdx zu Beginn: 5, am Ende: 6.
Natürlich muss noch ein gültiger Wert in die entstandene Lücke gefüllt werden, was ja der Sinn dieses Vorgangs war!
-
Weiße Felder: "Schrott"
-
Hellgrau: gültige Inhalte
-
Dunkelgrau: Zelle, die freigemacht werden soll/wurde
-
Kleine Ziffer bei geraden/gekrümmten Pfeilen unterhalb: Reihenfolge