Arrays

Inhalt

 

 

Eindimensionale Arrays

  Vereinfachte for-Schleife

 

Beispiele

 

Initialisierung

 

Mehrdimensionale Arrays

 

Nicht rechteckige Arrays

 

Beispiele

 

Übungsaufgaben

Eindimensionale Arrays

Bisher können wir Variablen der in Java vorgegebenen Datentypen verwenden. Dabei kann jede Variable genau einen Wert des Datentyps speichern. Arrays geben nun die Möglichkeit, eine beliebige Anzahl von Variablen gleichen Typs simultan zu erzeugen und zu verwenden. Der Platz für die Daten wird auf dem Heap bereitgestellt. Auf dem Stack, der ja die lokalen Daten eines Unterprogramms aufnimmt, wird nur eine Referenz gespeichert.

Variablen, die Arrays (eigenlich Referenzen auf Arrays) aufnehmen können, werden mit eckigen Klammern [] gekennzeichnet. Das Array an sich muss mit new bereitgestellt werden. Im folgenden Beispiel wollen wir 1000 Variablen vom type double erzeugen, die in einem Array zusammengefasst werden.

double a[];
a = new double[1000];

oder kürzer

double a[] = new double[1000];

Eine alternative Syntax ist

double[] a = new double[1000];

Diese Syntax beruht darauf, dass der Datentyp des Arrays a mit double[] bezeichnet werden kann. Im Unterschied zu anderen Programmiersprachen, hat ein konkretes Array zwar eine fest definierte Größe, die aber im Datentyp nicht angegeben wird. Sie muss lediglich bei der Erzeugung konkretisiert werden. Die Größe des Arrays lässt sich nachträglich mit a.length bestimmen, aber nicht ändern.Außerdem gibt es dynamisch wachsende Arrays, die allerdings etwas ineffizienter sind. Wir werden diese Arrays erst später kennenlernen.

Mit obigem Befehl stehen also 1000 double-Variablen zur Verfügung. Der Zugriff auf eine einzelne dieser Variablen erfolgt durch Indizierung. Dabei wird der gewünschte Index in eckigen Klammern angegeben. Als Beispiel speichern wir die Quadratzahlen 1^2, 2^2, ..., 1000^2 in dem Array ab.

for (i=0; i<1000; i++) a[i]=i*i;

Vorsicht: Man beachte, dass die Nummerierung mit 0 beginnt! Das letzte Element hat also den Index 999 (Größe minus 1).

Die allgemeine Syntax einer indizierten Array-Variable ist also

arrayname[index]

wobei index ein Ausdruck vom Typ int sein muss. Eine solche indizierte Variable kann wie jede andere Variable des entsprechenden Typs (im Beipspiel double) verwendet werden.

Hier ein anderes Beispiel, bei dem eine Variable des Arrays sowohl rechts als auch links von = erscheint.

a[0] = 1.0;
for (i=1; i<1000; i++) a[i] = 1.01*a[i-1];

Dieses Beispiel setzt jedes Element des Arrays auf das 1.01-fache des vorherigen, beginnend mit 1. Man muss also das erste Element setzen und dann die Restlichen in einer Schleife bearbeiten.

Insgesamt werden Arrays wie folgt gespeichert.

Referenz a auf dem Stack ---> a[0],...,a[n-1] auf dem Heap

Dabei ist n die Größe des Arrays.

Falls ein Index außerhalb der Grenzen 0 bis n-1 liegt, wird eine index-out-of-bounds exception erzeugt. Bei unseren einfachen Test-Programmen führt dies zum Programmabbruch mit einer Fehlermeldung.

Wird die Initialisierung des Arrays mit new weggelassen, so hat die Referenz den vordefinierten Inhalt null. Jeder Versuch, auf das Array zuzugreifen, erzeugt dann eine sogenannte NullPointerException.

Arrays werden automatisch vom Heap entfernt, wenn sie im Programm nicht mehr benötigt werden. Java kann feststellen, wann dies der Fall ist. Man nennt diesen Vorgang Garbage Collection.

Vereinfachte For-Schleife

Es gibt eine einfachere Möglichkeit auf jedes Element eines Arrays zuzugreifen. Eine Schleifenvariable wird dabei gar nicht mehr benötigt. Das sieht so aus.

int a[] = new int[10];
for (int i=0; i<10; i++) a[i]=i*i;
for (int k : a) System.out.println(k);

Die erste Schleife, in der der Inhalt von a gesetzt wird, ist uns schon bekannt. Die zweite Schleife ist eine Kurzform, bei der die Variable k alle Elemente von a durchläuft. Wie bei der normalen for-Schleife kann man diese Variable auch gleich in der Schleife deklarieren.

Man beachte zwei wichtige Dinge.

Aufgrund dieser Einschränkungen eignet sich diese Schleifenform mehr für den Zugriff auf allgemeine Kollektorklassen, die wir später kennenlernen.

Beispiele

Statistik von Zufallszahlen

Wir erzeugen 10000 Zufallszahlen und bestimmen den Mittelwert, die Standardabweichung, und zählen, wieviele Zahlen in jedem Intervall 0 bis 0.1, 0.1 bis 0.2 etc. liegen.

Es wird zunächst Platz für 10000 Zahlen in einem Array geschaffen. Dann werden 10000 Zufallszahlen erzeugt und in dem Array abgespeichert. Beim Hantieren mit dem Array beachte man immer, dass der Index von 0 bis 9999 laufen muss.

Ein weiteres Array I der Größe 10 nimmt schließlich die Zähler auf. I[0] soll z.B. die Anzahl der Zufallszahlen zwischen 0 und 0.1 enthalten. Der Index des Zählerintervalls wird aus der Zufallszahl durch Multiplikation mit 10 und Abschneiden des Nachkomma-Anteils bestimmt.

public class Test
{
    public static void main(String args[])
    {
        int n = 10000; // Anzahl der Zufallszahlen
        double R[] = new double[n]; // erzeugt Platz fuer n Zahlen
        int i;

        // Zufallszahlen:
        for (i = 0; i < n; i++)
            R[i] = Math.random();

        // Mittelwert:
        double sum = 0;
        for (i = 0; i < n; i++)
            sum += R[i]; // berechne Summe
        double mean = sum / n;
        System.out.println("Mittelwert : " + mean);

        // Stichprobenabweichung:
        sum = 0;
        for (i = 0; i < n; i++)
            sum += (mean - R[i]) * (mean - R[i]);

        // Berechne Varianz:
        System.out.println("Standard-Abweichung : " + Math.sqrt(sum / (n - 1)));

        // Verteilung:
        int m = 10;
        int I[] = new int[m]; // Platz fuer m Zaehler
        int j;
        for (j = 0; j < m; j++)
            I[j] = 0; // mit 0 initialisieren
        for (i = 0; i < n; i++) // zaehlen
        {
            // in welchem Intervall liegt R[i]?
            j = (int) Math.floor(R[i] * m);
            I[j]++; // erhoehe Zaehler
        }

        // Ausgabe:
        for (j = 0; j < m; j++) // Ausgabe der Zaehlungen
            System.out.println((j + 1) + " : " + I[j]);
    }
}

Einige Dinge sind erklärungsbedürftig.

Man beachte, dass die Größe des Arrays im new-Befehl auch durch eine Variable oder einen Ausdruck angegeben werden kann. Das Programm erzeugt die folgende Ausgabe

Mittelwert : 0.5000445575184346
Standard-Abweichung : 0.28746642473628076
1 : 967
2 : 991
3 : 1060
4 : 992
5 : 1001
6 : 988
7 : 1065
8 : 952
9 : 993
10 : 991

Diese Ausgabe ist nicht jedesmal gleich. Im Unterschied zu C wird der Zufallsgenerator bei jedem Lauf mit der Systemzeit initialisiert.

Einfaches Sortieren

Das folgende Programm erzeugt Zufallszahlen und sortiert sie. Der Sortieralgorithmus ist der denkbar schlechteste, aber auch der einfachste. Daher ist dieses Programm nur für kleine Datenmengen brauchbar.

Der Algorithmus funktioniert so: Die Zahlen R[2] bis R[n] werden solange nach vorne getauscht, bis sie an der richtigen Stelle stehen. Man nennt dies Bubble-Sort.

  1. Lasse j vom zweiten Index (1) bis zum letzten Index (n-1) laufen.
  2. Schaue nach, ob die j-te Zahl kleiner als die (j-1)-te ist. Wenn ja vertausche die beiden Zahlen. Wenn nein, so steht die Zahl richtig und wir gehen in 1. zum nächsten Index.
  3. Falls vertauscht wurde, so wiederhole den Schritt 2. mit der (j-2)-ten und der (j-1)-ten Zahl, und dann immer weiter bis zur 2. und 1. Zahl.

Durch 2. und 3. wird die j-te Zahl an die richtige Stelle sortiert.

public class Test
{
    public static void main(String args[])
    {
        int n = 20;
        double R[] = new double[n];
        double h;
        int i, j;

        // Erzeuge Zufallszahlen:
        for (i = 0; i < n; i++)
            R[i] = Math.random();

        // Sortiere:
        for (i = 1; i < n; i++)
        {
            j = i; // Sortiere R[i] ein
            while (j > 0) // solange es nicht ganz vorne steht
            {
                if (R[j - 1] > R[j]) // steht noch falsch
                {
                    h = R[j];
                    R[j] = R[j - 1];
                    R[j - 1] = h; // vertauschen
                } 
                else break; // steht richtig
                j--;
            }
        }

        // Ausgabe:
        for (i = 0; i < n; i++)
            System.out.println(R[i]);
    }
}

Initialisierung

Man kann ein Array schon bei der Deklaration mit Werten versehen. In diesem Fall ist kein new notwendig. Beispiel:

int n[] = {2,5,3,1};
System.out.println(n.length); // druckt 4

Die Größe des Arrays ergibt sich aus der Anzahl der angegebenen Werte. Diese Werte brauchen keine Konstanten zu sein.

Mehrdimensionale Arrays

In Java werden mehrdimensionale Arrays (Matrizen etc.) durch Arrays von Arrays erzeugt. Dies ist aber unter Umständen für den Programmierer transparent, d.h. er braucht sich darum nicht zu kümmern. Will man etwa Platz für 3 Reihen von 4 Elementen schaffen, so genügt es

int M[][] = new int[3][4];

zu setzen. Diese Matrix ist ein Array der Dimension 2. Schleifen über solche Matrizen müssen dann meist geschachtelte Schleifen sein. Etwa

int i,j;
for (i=0; i<3; i++)
    for (j=0; j<4; j++)
        M[i][j] = i*j;

Der Zugriff erfolgt also durch ebensoviele Klammerpaare, wie die Dimension des Arrays angibt.

Dass es sich hier in der Tat um ein Array von drei Arrays handelt, zeigt folgendes Programm

int a[]=M[0];
for (i=0; i<a.length; i++) a[i]=i;
System.out.println(a.length);

Hier wird die erste Zeile von M einem eindimensionalen Array mit vier Elementen zugewiesen. Also wird M folgendermaßen abgespeichert.

M ----> M[0], M[1], M[2]
M[0] ----> M[0,0], M[0,1], M[0,2], M[0,3]
M[1] ----> M[1,0], M[1,1], M[1,2], M[1,3]
M[2]
----> M[2,0], M[2,1], M[2,2], M[2,3]

Nicht rechteckige Arrays

Es ist in der Tat nicht notwendig so, dass mehrdimensionale Arrays rechteckig sein müssen. Jede Zeile kann eine andere Größe haben.

int M[][] = new int[3][];
for (int i=0; i<3; i++) M[i] = new int[i+1];

In diesem Beispiel haben die Zeilen von M die Längen 1,2,3. Es entsteht so ein dreieckiges Array.

Der wesentliche Unterschied zwischen den Befehlen

new int [3][4];
new int[3][];

ist also, dass im ersten Fall die Referenzen auf die Zeilen automatisch erzeugt werden und im zweiten Fall nicht. Der Programmierer muss in diesem Fall für die Initialisierung jeder einzelnen Zeile sorgen, bevor auf die Elemente zugegriffen werden kann.

Entsprechendes gilt für drei- und mehrdimensionale Arrays.

Beispiele

Beispiel 1

Wir erzeugen die ersten n Primzahlen. Wir wollen den verwendeten Algorithmus zunächst beschreiben

  1. Zunächst merken wir uns die ersten beiden Primzahlen 2 und 3.
  2. Dann testen wir ausgehend von der höchsten bisher gefundenen Primzahl alle ungeraden Zahlen auf Teilbarkeit durch eine bisher gefundene Primzahl. Dabei muss nur bis zur Quadratzahl der zu testenden Zahl geprüft werden. Fall dabei eine neue Primzahl entdeckt dadurch wird, dass sie keinen Teiler unter den bisher gefundenen Primzahlen hat, merken wir sie uns.

Die Schwierigkeit liegt darin diesen einfachen Algorithmus in ein Programm umzusetzen.

public class Test
{
    public static void main(String args[])
    {
        int n = 10;
        int p[] = new int[n];

        // Setze die ersten beiden Primzahlen
        p[0] = 2;
        if (n > 1)
            p[1] = 3;

        // erzeuge den Rest:
        int i, j;
        for (i = 2; i < n; i++) // erzeuge p[2] bis p[n-1]
        {
            int m = p[i - 1] + 2;

            // versuche letzte Primzahl plus 2,4,...
            search: while (true) // eigentlich eine Dauerschleife
            {
                j = 0;
                while (true)
                // teste auf Teilbarkeit durch p[0],...,p[i-1]
                {
                    if (m % p[j] == 0)
                        break;
                    // m ist nicht prim
                    if (p[j] * p[j] > m || j >= i)
                    // m ist prim, genuegend gesucht
                    {
                        p[i] = m;
                        break search;
                    }
                    j++;
                }
                m += 2;
            }
        }

        // Ausgabe:
        for (i = 0; i < n; i++)
            System.out.println(p[i]);
    }
}

Hier wird break mit einem Label benutzt. Dadurch wird die Suche nach der nächsten Primzahl abgebrochen, sobald eine Primzahl gefunden wurde. Dieses Beispiel wird deutlich lesbarer, wenn man Unterprogramme verwendet.

Beispiel 2

Der folgende Code löst ein Gleichungssystem mit dem Gaußschen Verfahren. Vermutlich ist dieses Beispiel wirklich nur für Mathematiker interessant. Wir beschreiben zunächst den verwendeten Algorithmus

  1. Gelöst wird das Gleichungssytem Ax=b. Dabei ist A eine quadratische Matrix und b ein Spaltenvektor.
  2. Zunächst wird durch Vertauschungen von Zeilen und Addition von Vielfachen einer Zeile zu einer anderen erreicht, dass die Matrix A unterhalb der Diagonalen nur 0 enthält. Dies geschieht Spalte für Spalte von links nach rechts. Dabei verwenden wir die folgende teilweise Pivotsuche: Es wird in jeder Spalte das betragsgrößte Element unterhalb der Diagonalen gesucht, und diese Zeile wird mit der Zeile, die das Diagonalelement enthält, vertauscht.
  3. Danach wird durch Additionen von Vielfachen einer Zeile zu einer anderen erreicht, dass oberhalb der Diagonalen nur 0 steht. Dies geschieht Spalte für Spalte von rechts nach links.
  4. Danach wird durch Multiplikation von Zeilen mit einem Vielfachen erreicht, dass die Diagonale nur 1 enthält.
  5. Beachten Sie, dass alle Operationen, die mit A gemacht werden, auch mit b gemacht werden müssen.
  6. Nun kann man das Ergebnis in b ablesen.
public class Test
{
    public static void main(String args[])
    {
        double a[][] =
        {
                { 1, 2, 3 },
                { 4, 5, 6 },
                { 7, 8, 1 } };
        double b[] =
        { 6, 15, 16 };
        double h[];

        int i, j, k, n = b.length;
        double m;
        int mk;

        // Mache M zur oberen Dreiecksmatrix:
        for (i = 0; i < n - 1; i++)
        { 
            // Suche Maximum in einer Spalte unterhalb M[i][i]:
            m = Math.abs(a[i][i]);
            mk = i;
            
            // bisheriges Maximum und seine Zeile
            for (k = i + 1; k < n; k++)
            {
                if (Math.abs(a[k][i]) > m)
                {
                    m = Math.abs(a[k][i]);
                    mk = k;
                }
            }
            
            // Tausche eventuell Zeilen aus:
            if (mk != i)
            {
                h = a[i];
                a[i] = a[mk];
                a[mk] = h;
                m = b[i];
                b[i] = b[mk];
                b[mk] = m;
            }
            
            // Mache Spalte unterhalb M[i][i] zu 0:
            for (k = i + 1; k < n; k++)
            {
                double lambda = a[k][i] / a[i][i];
                for (j = i + 1; j < n; j++)
                    a[k][j] -= lambda * a[i][j];
                b[k] -= lambda * b[i];
            }
        }

        // Mache M zur Diagonalmatrix:
        for (i = n - 1; i >= 1; i--)
        {
            for (k = i - 1; k >= 0; k--)
                b[k] -= b[i] * a[k][i] / a[i][i];
        }

        // Mache M zur Einheitsmatrix:
        for (i = 0; i < n; i++)
            b[i] /= a[i][i];

        // Gib Ergebnis aus:
        for (i = 0; i < n; i++)
            System.out.println(b[i]);
    }
}

Der Code dieses Beispiels ist schon recht kompliziert. Es ist unter Umständen schwer, einen Fehler (Bug) darin zu finden. Auch dieses Programm lässt sich durch Strukturieren mit Unterprogrammen lesbarer gestalten.

Wichtige Details

Übungsaufgaben

  1. Erzeugen Sie 1000 Zufallsvariablen, die sie auf einem Array zwischenspeichern, und bestimmen Sie die größte davon. Wie lässt sich die Aufgabe ohne Array lösen?
  2. Wie 1. Jedoch ist diesmal nach der zweitgrößten Zahl gefragt.
  3. Versuchen Sie 1000 (10000) Zahlen mit dem Sortieralgorithmus zu sortieren.
  4. Erzeugen Sie das Pascalsche Dreieck:
    1
    1 1
    1 2 1
    1 3 3 1
    1 4 6 4 1
    Speichern Sie die Zeilen in einem einfachen Vektor ab und geben Sie diesen Vektor in einer Zeile aus (für die ersten 20 Zeilen).
  5. Speichern Sie den Wert der Kosinusfunktion an den Stellen 0,0.01,0.02,...,1. Addieren Sie die Werte dann und multiplizieren Sie die Summe mit 0.01, um ein angenähertes Integral zu berechnen. Addieren Sie dann die Werte mit den Gewichten 1,4,2,4,2,...,2,4,1 und multiplizieren Sie die Summe mit 0.01/3. Vergleichen Sie beides mit dem korrekten Wert für das Integral.
  6. Schreiben Sie die Beispiel in diesem Abschnitt mit der vereinfachten for-Schleife um, wenn dies möglich ist.

Lösungen.

Aufgaben ohne Lösung

  1. Speichern Sie wieder eine große Anzahl von Zufallszahlen und testen Sie, wie oft die nächste größer als die vorhergehende ist. Testen Sie, wie oft die nächste Zahl größer als die beiden vorhergehenden ist. Entspricht das Ergebnis Ihren Erwartungen?
  2. Schreiben Sie ein Programm, das eine Lottoziehung (6 aus 49 mit Zusatzzahl) simuliert. Wichtig ist natürlich, keine Zahlen doppelt zu ziehen. Eine einfache Strategie dazu ist, solange eine Zufallszahl zu erzeugen, bis eine neue Zahl entsteht.
  3. Schreiben Sie die Zahlen 1 bis n in ein Array und mischen Sie dieses Array. Dazu Vertauschen Sie k=2 bis k=n mit einer Zahl an einer zufälligen Stelle von 1 bis k.
  4. Führen Sie 3. für ein großes Array durch und testen Sie, wie viele Zahlen an derselben Stelle stehen bleiben.
  5. Erzeugen Sie eine große Anzahl von Primzahlen (100000) und finden Sie die größte Lücke zwischen zwei Primzahlen.

Zurück zum Java-Kurs