Files
JuliaKurs23/nb/7_ArraysP2.ipynb
T
2023-05-12 20:12:56 +02:00

45 KiB

Vektoren, Matrizen, Arrays

Allgemeines

Kommen wir nun zu den wohl wichtigsten Containern für numerische Mathematik:

  • Vektoren Vector{T}
  • Matrizen Matrix{T} mit zwei Indizes
  • N-dimensionale Arrays mit N Indizes Array{T,N}

Tatsächlich ist Vector{T} ein Alias für Array{T,1} und Matrix{T} ein Alias für Array{T,2}.

In [ ]:
Vector{Float64} === Array{Float64,1} && Matrix{Float64} ===  Array{Float64,2}

Beim Anlegen durch eine explizite Elementliste wird der 'kleinste gemeinsame Typ' für den Typparameter T ermittelt.

In [ ]:
v = [33, "33", 1.2]

Falls T ein numerischer Typ ist, werden die Elemente in diesen Typ umgewandelt.

In [ ]:
v = [3//7, 4, 2im]

Informationen über einen Array liefern die Funktionen:

  • length(A) --- Anzahl der Elemente
  • eltype(A) --- Typ der Elemente
  • ndims(A) --- Anzahl der Dimensionen (Indizes)
  • size(A) --- Tupel mit den Dimensionen des Arrays
In [ ]:
v1 = [12, 13, 15]

m1 = [ 1    2.5
       6    -3 ]

for f ∈ (length, eltype, ndims, size)
    println("$(f)(v) = $(f(v)),          $(f)(m1) = $(f(m1))")
end
  • Die Stärke des 'klassischen' Arrays für das wissenschaftliche Rechnen besteht darin, dass es einfach nur ein zusammenhängendes Speichersegment ist, in dem die Komponenten gleicher Länge (z.B. 64 Bit) geordnet hintereinander abgespeichert sind. Damit ist der Speicherbedarf minimal und die Zugriffsgeschwindigkeit auf eine Komponente, sowohl beim Lesen als auch beim Modifizieren, maximal. Der Platz der Komponente v[i] ist sofort aus i berechenbar.
  • Julias Array{T,N} (und damit Vektoren und Matrizen) ist für die üblichen numerischen Typen T in dieser Weise implementiert. Die Elemente werden unboxed gespeichert. Im Gegensatz dazu ist z.B. ein Vector{Any} implementiert als Liste von Adressen von Objekten (boxed) und nicht als Liste der Objekte selbst.
  • Julias Array{T,N} speichert seine Elemente direkt (unboxed), wenn isbitstype(T) == true.
In [ ]:
isbitstype(Float64), 
isbitstype(Complex{Rational{Int64}}), 
isbitstype(String)

Vektoren

Listen-artige Funktionen

  • push!(vector, items...) --- fügt Elemente am Ende des Vektors an
  • pushfirst!(vector, items...) --- fügt Elemente am Anfang des Vektors an
  • pop!(vector) --- entfernt letztes Element und liefert es als Ergebnis zurück,
  • popfirst!(vector) --- entfernt erstes Element und liefert es zurück
In [ ]:
v = Float64[]           # leerer Vector{Float64}

push!(v, 3, 7)
pushfirst!(v, 1)

a = pop!(v)
println("a= $a")

push!(v, 17)

Ein push!() kann sehr aufwändig sein, da eventuell neuer Speicher alloziert und dann der ganze bestehende Vektor umkopiert werden muss. Julia optimiert das Speichermanagement. Es wird in einem solchen Fall Speicher auf Vorrat alloziert, so dass weitere push!s sehr schnell sind und man 'fast O(1)-Geschwindigkeit' erreicht.

Trotzdem sollte man bei zeitkritischem Code und sehr großen Feldern Operationen wie push!() oder resize() vermeiden.

Weitere Konstruktoren

Man kann Vektoren mit vorgegebener Länge und Typ uninitialisiert anlegen. Das geht am Schnellsten, die Elemente sind zufállige Bitmuster.

In [ ]:
# fixe Länge 1000, uninitialisiert

v = Vector{Float64}(undef, 1000)
v[345]
  • zeros(n) legt einen Vector{Float64} der Länge n an und initialisiert mit Null.
In [ ]:
v = zeros(7) 
  • zeros(T,n) legt einen Nullvektor vom Typ T an.
In [ ]:
v=zeros(Int, 4)
  • fill(x, n) legt Vector{typeof(x)} der Länfe n an und füllt mit x.
In [ ]:
v = fill(sqrt(2), 5)
  • similar(v) legt einen uninitialisierten Vektor von gleichem Typ und Größe wie v an.
In [ ]:
w = similar(v)

Konstruktion durch implizite Schleife (list comprehension)

Implizite for-Schleifen sind eine weitere Methode, Vektoren zu erzeugen.

In [ ]:
v4 = [i for i in 1.0:8]
In [ ]:
v5 = [log(i^2) for i in 1:4 ]

Man kann sogar noch ein if unterbringen.

In [ ]:
v6 = [i^2 for i in 1:8 if i%3 != 2]

Bitvektoren

Neben Vector{Bool} gibt es noch den speziellen Datentyp BitVector (und allgemeiner auch BitArray) zur Speicherung von Feldern mit Wahrheitswerten.

Während für die Speicherung eines Bools ein Byte verwendet wird, erfolgt die Speicherung in einem BitVector bitweise.

Der Konstruktor wandelt einen Vector{Bool} in einen BitVector um.

In [ ]:
vb = BitVector([true, false, true, true])

Für die Gegenrichtung gibt es collect().

In [ ]:
collect(vb)

BitVectoren entstehen z.B. als Ergebnis von elementweisen Vergleichen (s. @sec-broadcast).

In [ ]:
v4 .> 3.5

Indizierung

Für den Mathematiker sind Indizes Ordinalzahlen. Also startet die Indexzählung mit 1.

Als Index kann man verwenden:

  • Integer
  • Integer-wertigen Range (gleiche Länge oder kürzer)
  • Integer-Vektor (gleiche Länge oder kürzer)
  • Bool-Vektor oder BitVector (gleiche Länge)

Mit Indizes kann man Arrayelemente/teile lesen und schreiben.

In [ ]:
v = [ 3i + 5.2 for i in 1:8]
In [ ]:
v[5]

Bei Zuweisungen wird die rechte Seite wenn nötig mit convert(T,x) in den Vektorelementetyp umgewandelt.

In [ ]:
v[6] = 9999
v

Überschreiten der Indexgrenzen führt zu einem BoundsError.

In [ ]:
v[77]

Mit einem range-Objekt kann man einen Teilvektor adressieren.

In [ ]:
vp = v[3:5]
vp
In [ ]:
vp = v[1:2:7]   # range mit Schrittweite
vp
  • Bei der Verwendung als Index kann in einem Range der Spezialwert end verwendet werden.
  • Bei der Verwendung als Index kann der "leere" Range : als Abkürzung von 1:end verwendet werden. Das ist nützlich bei Matrizen: A[:, 3] addresiert die gesamte 3. Spalte von A.
In [ ]:
v[6:end] = [7, 7, 7]
v

Indirekte Indizierung

Die indirekte Indizierung mit einem Vector of Integers/Indices erfolgt nach der Formel

v[ [i_1,\ i_2,\ i_3,...]] = [\ v[i_1],\ v[i_2],\ v[i_3],...]

In [ ]:
v[ [1, 3, 4] ]

ist also gleich

In [ ]:
[ v[1], v[3], v[4] ]

Indizierung mit einem Vektor von Wahrheitswerten

Als Index kann man auch einen Vector{Bool} oder BitVector (s. @sec-bitvec) derselben Länge verwenden.

In [ ]:
v[ [true, true, false, false, true, false, true, true] ]

Das ist nützlich, da man z.B.

  • Tests broadcasten kann (s. @sec-broadcast),
  • diese Tests dann einen BitVector liefern und
  • bei Bedarf solche Bitvektoren durch die Bit-weisen Operatoren & und | verknüpft werden können.
In [ ]:
v[  (v .> 13) .& (v.<20) ]

Matrizen und Arrays

Die bisher vorgestellten Methoden für Vektoren übertragen sich auch auf höherdimensionale Arrays.

Man kann sie uninitialisiert anlegen:

In [ ]:
A = Array{Float64,3}(undef, 6,9,3)

In den meisten Funktionen kann man die Dimensionen auch als Tupel übergeben. Die obige Anweisung lässt sich auch so schreiben:

A = Array{Float64, 3}(undef, (6,9,3))  

Funktionen wie zeros() usw. funktionieren natürlich auch.

In [ ]:
m2 = zeros(3, 4, 2)  # oder zeros((3,4,2))   
In [ ]:
M = fill(5 , (3, 3))   # oder fill(5, 3, 3)

Die Funktion similar(), die einen Array gleicher Größe uninitialisiert erzeugt, kann auch einen Typ als weiteres Argument bekommen.

In [ ]:
M2 = similar(M, Float64)

Konstruktion durch explizite Elementliste

Während man Vektoren kommagetrennt in eckigen Klammern notiert, ist die Notation für höherdimensionale Objekte etwas anders.

  • Eine Matrix:
In [ ]:
M2 = [2  3  -1
      4  5  -2]
  • dieselbe Matrix:
In [ ]:
M2 = [2 3 -1; 4 5 -2]
  • Ein Array mit 3 Indizes:
In [ ]:
M3 = [2   3  -1 
      4   5   6 ;;;
      7   8   9
     11  12  13]
  • und nochmal die Matrix M2:
In [ ]:
M2 = [2;4;; 3;5;; -1;-2]

Im letzten Beispiel kommen diese Regeln zur Anwendung:

  • Trenner ist das Semikolon.
  • Ein Semikolon ; erhöht den 1. Index.
  • Zwei Semikolons ;; erhöhen den 2. Index.
  • Drei Semikolons ;;; erhöhen den 3. Index usw.

In den Beispielen davor wurde folgende Syntaktische Verschönerung (syntactic sugar) angewendet:

  • Leerzeichen trennen wie 2 Semikolons -- erhöht also den 2. Index: \quad a_{12}\quad a_{13}\quad a_{14}\ ...
  • Zeilenumbruch trennt wie ein Semikolon -- erhöht also den 1. Index.

:::{.callout-important}

  • Vektorschreibweise mit Komma als Trenner geht nur bei Vektoren, nicht mit "Semikolon, Leerzeichen, Newline' mischen!
  • Vektoren mit einem Index; $1!\times!n$-Matrizen und $n!\times!1$-Matrizen sind drei verschiedene Dinge!
In [ ]:
v1 = [2,3,4]
In [ ]:
v2 = [2;3;4]
In [ ]:
v3 = [2 3 4]
In [ ]:
v3 = [2;3;4;;]

:::

Einen "vector of vectors" a la C/C++ kann man natürlich auch konstruieren.

In [ ]:
v = [[2,3,4], [5,6,7,8]]
In [ ]:
v[2][3]

Das sollte man nur in Spezialfällen tun. Die Array-Sprache von Julia ist in der Regel bequemer und schneller.

Indizes, Teilfelder, Slices

In [ ]:
# 6x6 Matrix mit Zufallszahlen gleichverteilt aus [0,1) ∈ Float64
A = rand(6,6)

Die übliche Indexnotation:

In [ ]:
A[2, 3] = 77.77777
A

Man kann mit Ranges Teilfelder adressieren:

In [ ]:
B = A[1:2, 1:3]

Das Adressieren von Teilen mit geringerer Dimension wird auch slicing genannt.

In [ ]:
# die 3. Spalte als Vektor (slicing)

C = A[:, 3]
In [ ]:
# die 3. Zeile als Vektor (slicing)

E = A[3, :]

Natürlich sind damit auch Zuweisungen möglich:

In [ ]:
# Man kann slices und Teilfeldern auch etwas zuweisen 

A[2, :] = [1,2,3,4,5,6]
A

Verhalten bei Zuweisungen, copy() und deepcopy(), Views

Zuweisungen und Kopien

  • Variablen sind Referenzen auf Objekte.
  • Eine Zuweisung zu einer Variablen erzeugt kein neues Objekt.
In [ ]:
A = [1, 2, 3]
B = A

A und B sind jetzt Namen desselben Objekts.

In [ ]:
A[1] = 77
@show B;
In [ ]:
B[3] = 300
@show A;

Dieses Verhalten spart viel Zeit und Speicher, ist aber nicht immer gewünscht. Die Funktion copy() erzeugt eine 'echte' Kopie des Objekts.

In [ ]:
A = [1, 2, 3]
B = copy(A)
A[1] = 100
@show A B;

Die Funktion deepcopy(A) kopiert rekursiv. Auch von den Elementen, aus denen A besteht, werden (wieder rekursive) Kopien erstellt.

Solange ein Array nur primitive Objekte (Zahlen) enthält, sind copy() und deepcopy() äquivalent.

Das folgende Beispiel zeigt den Unterschied zwischen copy() und deepcopy().

In [ ]:
mutable struct Person
    name :: String
    age  :: Int
end

A = [Person("Meier", 20), Person("Müller", 21), Person("Schmidt", 23)]
B = A
C = copy(A)
D = deepcopy(A)
In [ ]:
A[1] = Person("Mustermann", 83)
A[3].age = 199 

@show B C D;

Views

Wenn man mittels indices/ranges/slices einer Variablen ein Teilstück eines Arrays zuweist, wird von Julia grundsätzlich ein neues Objekt konstruiert.

In [ ]:
A = [1  2  3
     3  4  5]

v = A[:, 2]
@show  v

A[1, 2] = 77
@show  A  v;

Manchmal möchte man aber gerade hier eine Referenz-Semantik haben im Sinne von: "Vektor v soll der 2. Spaltenvektor von A sein und auch bleiben (d.h., sich mitändern, wenn sich A ändert)."

Dies bezeichnet man in Julia als views: Wir wollen, dass die Variable v nur einen 'alternativen Blick' auf die Matrix A darstellt.

Das kann man erreichen durch das @view-Macro:

Warning:
Output truncated. This notebook contains too many cells to display efficiently.