# 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}`.


```{julia}
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.

```{julia}
v = [33, "33", 1.2]
```
Falls `T` ein numerischer Typ ist, werden die Elemente in diesen Typ umgewandelt.

```{julia}
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  

```{julia}
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`.

```{julia}
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  

```{julia}
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.
```{julia}
# 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.  

```{julia}
v = zeros(7) 
```

- `zeros(T,n)` legt einen Nullvektor vom Typ `T` an.

```{julia}
v=zeros(Int, 4)
```

- `fill(x, n)` legt `Vector{typeof(x)}` der Länfe `n` an und füllt mit `x`.

```{julia}
v = fill(sqrt(2), 5)
```

- `similar(v)` legt einen uninitialisierten Vektor von gleichem Typ und Größe wie `v` an.

```{julia}
w = similar(v)
```



### Konstruktion durch implizite Schleife _(list comprehension)_

Implizite `for`-Schleifen sind eine weitere Methode, Vektoren zu erzeugen.
```{julia}
v4 = [i for i in 1.0:8]
```


```{julia}
v5 = [log(i^2) for i in 1:4 ]
```

Man kann sogar noch ein `if` unterbringen.

```{julia}
v6 = [i^2 for i in 1:8 if i%3 != 2]
```

### Bitvektoren {#sec-bitvec}

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 `Bool`s ein Byte verwendet wird, erfolgt die Speicherung in einem BitVector bitweise. 

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

```{julia}
vb = BitVector([true, false, true, true])
```

Für die Gegenrichtung gibt es `collect()`.

```{julia}
collect(vb)
```

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

```{julia}
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.


```{julia}
v = [ 3i + 5.2 for i in 1:8]
```


```{julia}
v[5]
```

Bei Zuweisungen wird die rechte Seite wenn nötig mit `convert(T,x)` in den Vektorelementetyp umgewandelt.
```{julia}
v[6] = 9999
v
```


Überschreiten der Indexgrenzen führt zu einem `BoundsError`. 


```{julia}
v[77]
```

Mit einem `range`-Objekt kann man einen Teilvektor adressieren.

```{julia}
vp = v[3:5]
vp
```

```{julia}
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]` adressiert die gesamte  3. Spalte von `A`. 


```{julia}
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],...]$


```{julia}
v[ [1, 3, 4] ]
```

ist also gleich
```{julia}
[ 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. 

```{julia}
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.    


```{julia}
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:

```{julia}
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:

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

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

```{julia}
m2 = zeros(3, 4, 2)  # oder zeros((3,4,2))   
```


```{julia}
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.

```{julia}
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:

```{julia}
M2 = [2  3  -1
      4  5  -2]
```

- dieselbe Matrix:

```{julia}
M2 = [2 3 -1; 4 5 -2]
```

- Ein Array mit 3 Indizes:

```{julia}
M3 = [2   3  -1 
      4   5   6 ;;;
      7   8   9
     11  12  13]
```

- und nochmal die Matrix `M2`:

```{julia}
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, $1\!\times\!n$-Matrizen und $n\!\times\!1$-Matrizen sind drei verschiedene Dinge!


```{julia}
v1 = [2,3,4]
```


```{julia}
v2 = [2;3;4]
```


```{julia}
v3 = [2 3 4]
```


```{julia}
v3 = [2;3;4;;]
```

:::


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


```{julia}
v = [[2,3,4], [5,6,7,8]]
```


```{julia}
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


```{julia}
# 6x6 Matrix mit Zufallszahlen gleichverteilt aus [0,1) ∈ Float64
A = rand(6,6)
```

Die übliche Indexnotation:

```{julia}
A[2, 3] = 77.77777
A
```

Man kann mit Ranges Teilfelder adressieren:

```{julia}
B = A[1:2, 1:3]
```

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

```{julia}
# die 3. Spalte als Vektor (slicing)

C = A[:, 3]
```


```{julia}
# die 3. Zeile als Vektor (slicing)

E = A[3, :]
```

Natürlich sind damit auch Zuweisungen möglich:

```{julia}
# 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._



```{julia}
A = [1, 2, 3]
B = A
```

`A` und `B` sind jetzt Namen desselben Objekts.

```{julia}
A[1] = 77
@show B;
```


```{julia}
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.

```{julia}
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()`.
```{julia}
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)
```

```{julia}
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.


```{julia}
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: 

```{julia}
A = [1 2 3
     3 4 5]

v = @view A[:,2]
@show  v

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


Diese Technik wird von Julia aus Effizienzgründen auch bei einigen Funktionen der linearen Algebra verwendet. 
Ein Beispiel ist der Operator `'`, der zu einer Matrix `A` die adjungierte Matrix `A'` liefert.

- Die adjungierte _(adjoint)_ Matrix `A'` ist die transponierte  und elementweise komplex-konjugierte Matrix zu `A`. 
- Der Parser macht daraus den Funktionsaufruf `adjoint(A)`.  
- Für reelle Matrizen ist die Adjungierte gleich der  transponierten Matrix.
- Julia implementiert `adjoint()` als  _lazy function_, d.h., 
- es wird aus Effizienzgründen kein neues Objekt konstruiert, sondern nur ein alternativer 'View' auf die Matrix ("mit vertauschten Indizes") und ein alternativer 'View' auf die Einträge (mit Vorzeichenwechsel im Imaginärteil).
- Aus Vektoren macht `adjoint()` eine $1\!\times\!n$-Matrix (einen Zeilenvektor).


```{julia}
A = [ 1.  2.
      3.  4.]
B = A'
```

Die Matrix `B` ist nur ein modifizierter 'View' auf `A`:

```{julia}
A[1, 2] =10
B
```


Aus Vektoren macht `adjoint()` eine $1\!\times\!n$-Matrix (einen Zeilenvektor).


```{julia}
v = [1, 2, 3]
v'
```

Eine weitere solche Funktion, die  einen alternativen 'View', eine andere Indizierung, derselben Daten 
liefert, ist `reshape()`. 

Hier wird ein Vektor mit 12 Einträgen in eine 3x4-Matrix verwandelt.
```{julia}
A = [1,2,3,4,5,6,7,8,9,10,11,12]

B = reshape(A, 3, 4)   
```

## Speicherung eines Arrays

- Speicher wird linear adressiert. Eine Matrix kann zeilenweise _(row major)_ oder spaltenweise _(column major)_ im Speicher angeordnet sein. 
- C/C++/Python(NumPy) verwenden eine zeilenweise Speicherung: Die 4 Elemente einer 2x2-Matrix sind abgespeichert in der Reihenfolge $a_{11},a_{12},a_{21},a_{22}$. 
- Julia, Fortran, Matlab speichern spaltenweise: $a_{11},a_{21},a_{12},a_{22}$. 

Diese Information  ist wichtig, um effizient über Matrizen zu iterieren:


```{julia}
function column_major_add(A, B)
    (n,m) = size(A)
    for j = 1:m
        for i = 1:n     # innere Schleife durchläuft eine Spalte
            A[i,j] += B[i,j]
        end
    end
end

function row_major_add(A, B)
    (n,m) = size(A)
    for i = 1:n
        for j = 1:m     # inere Schleife durchläuft eine Zeile
            A[i,j] += B[i,j]
        end
    end
end
```


```{julia}
A = rand(10000, 10000);
B = rand(10000, 10000);
```


```{julia}
using BenchmarkTools

@benchmark row_major_add($A, $B) 
```


```{julia}
@benchmark column_major_add($A, $B)
```

### Lokalität von Speicherzugriffen und _Caching_

Wir haben gesehen, dass die Reihenfolge von innerem und äußerem Loop einen erheblichen Geschwindigkeitsunterschied macht:

__Es ist effizienter, wenn die innerste Schleife über den linken Index läuft__, also eine Spalte und nicht eine Zeile durchläuft.  Die Ursache dafür liegt in der Architektur moderner Prozessoren.

- Speicherzugriffe erfolgt über mehrere Cache-Ebenen. 
- Ein _cache miss_, der ein Nachladen aus langsameren Caches auslöst, bremst aus.
- Es werden immer gleich größere Speicherblöcke nachgeladen, um die Häufigkeit von _cache misses_ zu minimieren.
- Daher ist es wichtig, Speicherzugriffe möglichst lokal zu organisieren.

::: {.content-visible when-format="html"}
![Speicherhierarchie von Intel-Prozessoren, aus: Victor Eijkhout,_Introduction to High-Performance Scientific Computing_, [https://theartofhpc.com/](https://theartofhpc.com/)](../images/cache.png){width="75%"}

:::

::: {.content-visible when-format="pdf"}
![Speicherhierarchie von Intel-Prozessoren, aus: Victor Eijkhout,_Introduction to High-Performance Scientific Computing_, [https://theartofhpc.com/](https://theartofhpc.com/)](../images/cache.png){width="70%"}
:::


## Mathematische Operationen mit Arrays

Arrays der gleichen Dimension  (z.B. alle $7\!\times\!3$-Matrizen) bilden einen linearen Raum. 

 - Sie können mit Skalaren multipliziert werden und
 - sie können addiert und subtrahiert werden.


```{julia}
0.5 * [2, 3, 4, 5]
```


```{julia}
0.5 * [ 1  3
        2  7] - [ 2  3; 1 2]
```

### Matrixprodukt

Das Matrixprodukt ist definiert für 

:::{.narrow}

| 1. Faktor | 2. Faktor | Produkt |
| :-:  | :-:   | :-: |
| $(n\!\times\!m)$-Matrix | $(m\!\times\!k)$-Matrix | $(n\times k)$-Matrix|
| $(n\!\times\!m)$-Matrix | $m$-Vektor | $n$-Vektor |
| $(1\!\times\!m)$-Zeilenvektor | $(m\!\times\!n)$-Matrix | $n$-Vektor |
| $(1\!\times\!m)$-Zeilenvektor | $m$-Vektor | Skalarprodukt |
| $m$-Vektor | $(1\times n)$-Zeilenvektor | $(m\!\times\!n)$-Matrix |

:::

Beispiele:
```{julia}
A = [1 2 3
     4 5 6]
v = [2, 3]
w = [1, 3, 4];
```

- (2,3)-Matrix `*` (3,2)-Matrix
```{julia}
A * A'
```

- (3,2)-Matrix `*` (2,3)-Matrix

```{julia}
A' * A
```

- (2,3)-Matrix `*` 3-Vektor

```{julia}
A * w
```

- (1,2)-Vektore `*` (2,3)-Matrix

```{julia}
v' * A
```

- (3,2)-Matrix `*` 2-Vektor

```{julia}
A' * v
```

- (1,2)-Vektor `*` 2-Vektor (Skalarprodukt)

```{julia}
v' * v
```

2-Vektor `*` (1,3)-Vektor (äußeres Produkt) 

```{julia}
v * w'
```

## Broadcasting {#sec-broadcast}

- Beim _broadcasting_ werden Operationen oder Funktionen __elementweise__ auf Arrays angewendet.
- Die Syntax dafür ist ein Punkt _vor_ einem Operationszeichen oder _nach_ einem Funktionsnamen. 
- Der Parser setzt `f.(x,y)` um zu `broadcast(f, x, y)` und analog für Operatoren `x .⊙ y` zu `broadcast(⊙, z, y)`. 
- Dabei werden Operanden, denen eine oder mehrere Dimensionen fehlen, in diesen Dimensionen (virtuell) vervielfältigt. 
- Das *broadcasting* von Zuweisungen `.=`, `.+=`,... verändert die Semantik. Es wird kein neues Objekt erzeugt, sondern die Werte werden in das links stehende Objekt (welches die richtige Dimension haben muss) eingetragen.


Einige Beispiele:

- Elementweise Anwendung einer Funktion
```{julia}
sin.([1, 2, 3])
```

- Das Folgende liefert nicht die algebraische [Wurzel aus einer Matrix](https://de.wikipedia.org/wiki/Quadratwurzel_einer_Matrix), sondern die elementweise Wurzel aus jedem Eintrag. 
```{julia}
A = [8 2
     3 4]

sqrt.(A)
```

- Das Folgende liefert nicht $A^2$, sondern die Einträge werden quadriert.
```{julia}
A.^2   
```

- Zum Vergleich das Ergebnis der algebraischen Operationen:
```{julia}
@show A^2 A^(1/2);
```

- Broadcasting geht auch mit Funktionen mehrerer Variablen.
```{julia}
hyp(a,b) = sqrt(a^2+b^2)

B = [3 4
     5 7]

hyp.(A, B) 
```



Bei Operanden verschiedener Dimension wird der Operand mit fehlenden Dimensionen in diesen durch Vervielfältigung virtuell 
'aufgeblasen'.

Wir addieren einen Skalar zu einer Matrix:
```{julia}
A = [ 1 2 3
      4 5 6]
```


```{julia}
A .+ 300
```

Der Skalar wurde durch Replikation auf dieselbe Dimension wie die Matrix gebracht. Wir lassen uns von `broadcast()` die Form des 2. Operanden nach dem *broadcasting* anzeigen: 

```{julia}
broadcast( (x,y) -> y, A, 300)
```

(Natürlich findet diese Replikation nur virtuell statt. Dieses Objekt wird bei anderen Operationen nicht wirklich erzeugt.)

Als weiteres Beispiel: Matrix und (Spalten-)Vektor

```{julia}
A .+ [10, 20]
```

Der Vektor wird durch Wiederholung der Spalten aufgeblasen:



```{julia}
broadcast((x,y)->y, A, [10,20])
```

Matrix und Zeilenvektor: Der Zeilenvektor wird zeilenweise vervielfältigt:


```{julia}
A .* [1,2,3]'      # Adjungierter Vektor
```

Der 2. Operand wird von `broadcast()` durch Vervielfältigung der Zeilen 'aufgeblasen'.

```{julia}
broadcast((x,y)->y, A, [1,2,3]')
```


#### _Broadcasting_  bei Zuweisungen

Zuweisungen  `=`, `+=`, `/=`,..., bei denen links ein Name steht, laufen in Julia so ab, dass aus der rechten Seite ein  Objekt konstruiert und diesem Objekt der neue Name zugewiesen wird. 

Beim Arbeiten mit Arrays will man allerdings sehr oft aus Effizienzgründen einen bestehenden Array weiterverwenden. Die rechts  berechneten Einträge sollen in das bereits existierende Objekt auf der linken Seite eingetragen werden. 

Das erreicht man mit den Broadcast-Varianten  `.=`, `.+=`,... der Zuweisungsoperatoren.

```{julia}
A .= 3
```

```{julia}
A .+= [1, 4]
```


## Weitere Array-Funktionen - eine Auswahl

Julia stellt eine große Anzahl von Funktionen bereit, die mit Arrays arbeiten.

```{julia}
A = [22 -17 8 ; 4 6 9]
```

- Finde das Maximum

```{julia}
maximum(A)
```

- Finde das Maximum jeder Spalte
```{julia}
maximum(A, dims=1)  
```

- Finde das Maximum jeder Zeile
```{julia}
maximum(A, dims=2)
```

- Finde das Minimum und seine Position
```{julia}
amin, i = findmin(A)
```

- Was ist ein `CartesianIndex`?
```{julia}
dump(i)
```

- Extrahiere die Indizes des Minimum als Tupel
```{julia}
i.I
```

- Summe und Produkt aller Einträge
```{julia}
sum(A), prod(A)
```

- Spaltensumme (1. Index wird reduziert)

```{julia}
sum(A, dims=1) 
```
 
- Zeilensummen   (2. Index wird reduziert)

```{julia}
sum(A, dims=2) 
```

- Summiere nach elementweiser Anwendung einer Funktion

```{julia}
sum(x->sqrt(abs(x)), A)   #   sum_ij sqrt(|a_ij|)
```

- Reduziere (falte) den Array mit einer Funktion

```{julia}
reduce(+, A)    #  equivalent to sum(A)
```

-  `mapreduce(f, op, array)`: Wende `f` auf alle Einträge an, dann reduziere mit `op`

```{julia}

mapreduce(x -> x^2, +, A )     # Summe der Quadrate aller Einträge
```


- Gibt es Elemente in A, die > 5 sind? 

```{julia}
any(x -> x>5, A)    
```

- Wieviele Elemente in A sind > 5?

```{julia}
count(x-> x>5, A)  
```

-  sind alle Einträge positiv?
```{julia}
all(x-> x>0, A)
```


