diff --git a/0_Python_LinA/tutorial_0_python_exercise.ipynb b/0_Python_LinA/tutorial_0_python_exercise.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..609bdd4fd6564e30c71bb97935bbdb5a3f1fddd8
--- /dev/null
+++ b/0_Python_LinA/tutorial_0_python_exercise.ipynb
@@ -0,0 +1,1114 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "collapsed": true
+   },
+   "source": [
+    "## Einführung in Python"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Grundlagen\n",
+    "\n",
+    "Als Programmiersprache verwenden wir [Python](http://www.python.org). Diese ist zur Zeit\n",
+    "eine der [populärsten](http://spectrum.ieee.org/at-work/tech-careers/the-top-10-programming-languages) Programmiersprachen überhaupt.\n",
+    "Die Wahl von Python ist aber von sekundärer Bedeutung, und wir werden die Sprache sehr pragmatisch\n",
+    "verwenden, ohne uns im Detail mit dieser auseinander zu setzen. Nichtsdestotrotz sollten\n",
+    "Sie den Kurs auch als Möglichkeit betrachten, Ihre Programmierkenntnisse zu vertiefen. Eine Auswahl\n",
+    "geeigneter Literatur hierzu finden Sie auf der ISIS Seite des Kurses."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Erste Schritte\n",
+    "Unserem pragmatischem Ansatz folgend, werden wir die grundlegenden Konstrukte von Python anhand von Beispielen erklären. Für die Installation von Python gibt es eine Zusammenfassung im Anhang A. Die einfachste Möglichkeit in Python zu programmieren, ist durch eine interaktive Python Konsole. Diese wird in einem Jupyter Notebook zur Verfügung gestellt und wir können Python Code direkt ausführen. Dazu wählt man die auszuführende Zelle aus und startet die Ausführung mit der Tastenkombination [Umschalt+Enter]. Beginnen wir mit dem klassischen, ersten Programm:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(\"Hello World\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Wie in anderen Programmiersprachen sind Schlüsselwörter im Allgemeinen aus dem Englischen entlehnt. Eine Variablenzuweisung erfolgt ebenfalls wie in den meisten anderen Sprachen:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "x = 3.5\n",
+    "print(x)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "In Python gibt es veränderbare (mutable) und nicht veränderbare (immutable) Objekte die streng\n",
+    "typisiert sind. Mit dem = Operator wird einem Objekt ein Name zugewiesen über welchen man im\n",
+    "weiteren Programmablauf auf das Objekt zugreifen kann. Die Zuweisung eines Namens kann während\n",
+    "der Ausführung des Programms verändert werden. Man spricht daher auch von dynamischer Typisierung.\n",
+    "Einem Namen bzw. einer Variable können Objekte unterschiedlichen Typs zugewiesen werden. Der Typ eines Objektes kann zur Laufzeit bestimmt werden."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "y = True\n",
+    "print(y, type(y))\n",
+    "y = 3 + 5\n",
+    "print(y, type(y))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Kontrollstrukturen\n",
+    "Zentral für die Entwicklung komplexer Programme sind Kontrollstrukturen. Im\n",
+    "Wesentlichen gibt es dabei in Python dieselben Möglichkeiten, welche auch schon aus Java bekannt sind.\n",
+    "Obwohl auch die Syntax sehr ähnlich zu Java ist, können gerade kleine Unterschiede dem beginnenden\n",
+    "Python Programmierer Schwierigkeiten bereiten. Aber kein Verzagen: Ãœbung macht den Meister!\n",
+    "Eine if-Anweisung hat in Python die Form:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "y = 7\n",
+    "if y < 10:\n",
+    "    y += 10\n",
+    "else:\n",
+    "    y -= 10\n",
+    "\n",
+    "print(y)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Ein wichtige Besonderheit von Python ist die Definition von Blöcken ausschließlich mit Hilfe von Einrückung. Dies sehen wir in der dritten Zeile des obigen Beispiels. Die Einrückung kann dabei entweder durch Tabs oder auch durch Leerzeichen erfolgen. Beide Arten dürfen aber nicht gemischt werden. Üblich sind 4 Leerzeichen je Einrückung. Offizielle Style Guidelines zu Python gibt es [hier](http://legacy.python.org/dev/peps/pep-0008/). Eine ähnliche Syntax wie die if-Anweisung hat auch die for-Schleife:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for i in range(1, 5):\n",
+    "    print(i)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Das Beispiel stellt auch die *range()* Funktion vor, welche in vielen Schleifen nützlich ist. Natürlich\n",
+    "besitzt auch Python eine while-Schleife:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "i = 0\n",
+    "while i < 5:\n",
+    "    print(i)\n",
+    "    i += 1"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Versuchen Sie ein “Gefühl” für die Sprache zu entwickeln. Dies erspart es, die exakte Syntax für jeden\n",
+    "Befehl nachschauen zu müssen, und ermöglicht auch Stil-gerechtes Programmieren. Dies intuitiv zu\n",
+    "ermöglichen, war eines der wichtigsten Designkriterien für Python (http://python-history.blogspot.de/2009/01/pythons-design-philosophy.html)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Funktionen\n",
+    "In den meisten Python-Programmen sind Funktionen und Klassen die zentralen Programmstrukturen. Eine einfache Funktion wird in Python wie folgt definiert:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def add_one(x):\n",
+    "    x += 1\n",
+    "    return x\n",
+    "\n",
+    "y = 8\n",
+    "print(add_one(y))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Wie aus den vorherigen Syntax-Beispielen für Python zu erwarten ist, wird der Kopf der Funktion durch “:” beendet und der Funktionskörper durch Einrückung gekennzeichnet. Der Rückgabewert wird in Python nicht im Funktionskopf definiert sondern ausschließlich im Körper durch return angegeben. Nicht alle Parameter für eine Funktion müssen immer angegeben werden. Falls ein Parameter nicht angegeben wird, dann wir der in der Funktionsdefinition festgelegte Wert verwendet:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def add_n(x, n=10):\n",
+    "    x += n\n",
+    "    return x\n",
+    "\n",
+    "y = 8\n",
+    "print(add_n(y))\n",
+    "print(add_n(y, 5))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Wenn mehrere Argumente Default-Werte haben, dann müssen nicht alle angegeben werden:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def add_n_mult_m(x, n=10, m=1):\n",
+    "    x = (x + n) * m\n",
+    "    return x\n",
+    "\n",
+    "y = 8\n",
+    "print(add_n_mult_m(y))\n",
+    "print(add_n_mult_m(y, 1))\n",
+    "print(add_n_mult_m(y, 1, 2))\n",
+    "print(add_n_mult_m(y, m=2))\n",
+    "print(add_n_mult_m(y, n=0, m=2))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Darüber hinaus können Funktionen in Python auch mehrere Werte als Tupel zurückgeben:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def add_and_mult(x):\n",
+    "    x1 = x + 10.0\n",
+    "    x2 = x * 2.0\n",
+    "    return x1, x2\n",
+    "\n",
+    "y = 8\n",
+    "y1, y2 = add_and_mult(y)\n",
+    "print(\"add =\", y1, \"and mult =\", y2)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Weiterführendes Thema: Lambda Kalkül in Python\n",
+    "Funktionen und Klassen sind die wesentlichen Bausteine für moderne imperativer Programmierung. Neben imperativer Programmierung unterstützt Python auch funktionale Programmierung. Ein einfaches Beispiel soll hier genügen:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def addN(n): return lambda x: x + n\n",
+    "f = addN(10)\n",
+    "g = addN(20)\n",
+    "print(f(10))\n",
+    "print(g(10))\n",
+    "print(addN(3)(10))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Die Funktion *addN()* gibt eine namenlose Funktion zurück! Funktionen können also in gewisser Weise wie Variablen behandelt werden und als Parameter oder Rückgabewerte dienen. Viele Probleme lassen sich so besonders elegant lösen. Lambda Ausdrücke sind in Python insbesondere in Verbindung mit filter() und map() nützlich, welche wir in Kürze kennenlernen werden."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Klassen\n",
+    "Auch in Python muss zwischen der Definition einer Klasse, welche wir bis jetzt betrachtet haben, und der Instanzierung unterschieden werden. Eine Instanz wird mit Hilfe des Konstruktors erzeugt, welcher immer den Namen *\\__init\\__()* hat, und durch den Klassennamen als Funktionsaufruf aufgeführt wird.\n",
+    "Instanzvariablen werden in Python nicht explizit deklariert, sondern durch die Definition in einer Klassenfunktion, was der Konstruktor oder auch eine beliebige andere Klassenfunktion sein kann, erzeugt. Dass die Variablen Instanzvariablen sind, wird eindeutig bestimmt, indem sie zu self, der aktuellen Instanz, hinzugefügt werden. Um ein solches Hinzufügen, aber vor allem aber auch den Zugriff auf Instanzvariablen zu ermöglichen, besitzt jede nicht-statische Klassenfunktion self als erstes Argument. Das Objekt wird immer automatisch als Argument an die Funktion übergeben und muss nicht als\n",
+    "expliziter Parameter aufgeführt werden. Die Definition einer leeren Klasse hat in Python die Form:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Vector:\n",
+    "    \"\"\"A simple vector class.\"\"\"\n",
+    "    def __init__(self):\n",
+    "        self.x = 0.0\n",
+    "        self.y = 0.0\n",
+    "        self.z = 0.0\n",
+    "    def __repr__(self):\n",
+    "        return \"({0:.2f}, {1:.2f}, {2:.2f})\".format(self.x, self.y, self.z)\n",
+    "        \n",
+    "# Instantiate vector\n",
+    "vec3 = Vector()\n",
+    "print(vec3)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Natürlich kann auch in Python der Konstruktor Argumente erhalten:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Vector :\n",
+    "    \"\"\"A simple vector class.\"\"\"\n",
+    "    def __init__(self, x = 0.0, y = 0.0, z = 0.0) :\n",
+    "        self.x = x\n",
+    "        self.y = y\n",
+    "        self.z = z\n",
+    "    def __repr__(self):\n",
+    "        return \"({0:.2f}, {1:.2f}, {2:.2f})\".format(self.x, self.y, self.z)\n",
+    "    \n",
+    "# Instantiate vector\n",
+    "vec3 = Vector(1.0, 1.0, 0.0)\n",
+    "print(vec3)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Der Konstruktor *Vector()* ruft dabei die Funktion *\\__init\\__()* mit den übergebenen Parametern auf, um die Instanz zu initialisieren. Das gleiche geschieht bei den arithmetischen Operatoren, die durch die Definition der passenden Funktionen, wie zum Beispiel *\\__add\\__(self, other)* für die Addition, ebenfalls verwendet werden können. Diese Art von Funktionen werden in Python auch [Magic Methods](http://www.rafekettler.com/magicmethods.html) genannt. Weitere magische Funktionen sind *\\__str\\__()*, welche einen String als lesbare Repräsentation\n",
+    "des Objektes zurückgeben soll und z.B. von der print-Funktion verwendet wird, sowie *\\__cmp\\__(self,\n",
+    "other)* durch welche die Vergleichsoperatoren definiert werden können.\n",
+    "Die Zuweisung von Variablen in Python ist immer nur eine Zuweisung von Referenzen auf ein darunter liegendes Objekts. Zum Beispiel erhalten wir mit der definierten Vektor Klasse:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "v = Vector( 1.0, 1.0, 1.0)\n",
+    "w = v\n",
+    "w.x = 2.0\n",
+    "print(v)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Zur Erzeugung \"echter\" Kopien kann zum Beispiel eine Funktion *copy()* der Klasse hinzugefügt werden. Python unterstützt auch Vererbung und die Erzeugung von Klassenhierarchien. Wir werden uns mit diesem Thema jedoch nicht beschäftigen und sie sollten dies ggf. selbstständig erkunden."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Docstring-Kommentare in Python\n",
+    "Python hat einen empfohlenen Stil für die Dokumentation von Funktionen und Klassen. 7 Dieser ist zwar nicht Teil der Sprachdefinition, er wird jedoch sehr einheitlich befolgt und viel Funktionalität geht verloren, wenn dieser nicht befolgt wird; zum Beispiel basiert die automatische Hilfe der meisten Python-Editoren auf der Einhaltung des kanonischen Dokumentationsstils. Die Dokumentation von Funktionen erfolgt durch drei Paare von Anführungszeichen, welche zum\n",
+    "Öffnen und Schließen des Kommentars verwendet werden. Die Funktionalität der Python Funktion sollte in der Dokumentation direkt beschrieben werden (und nicht \"Die Funktion ...\"). Die Dokumentation von Klassen erfolgt analog zu der von Funktionen, wie wir bereits am Beispiel der Klasse Vector gesehen haben. Eine vollständige Beschreibung einer Funktion erstreckt sich über mehrere Zeilen:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def addN(x, N=10.0):\n",
+    "    \"\"\"Add N to argument x and return result.\n",
+    "    Keyword arguments:\n",
+    "    x -- number to which N is to be added\n",
+    "    N -- increment for x (default: 10.0)\n",
+    "    ...\n",
+    "    \"\"\"\n",
+    "    return x + N"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Module und Pakete\n",
+    "Klassen bieten eine Möglichkeit Daten und die dazugehörige Funktionalität\n",
+    "in einer programmtechnischen Einheit zu verbinden. Dies ist insbesondere dann nützlich, wenn viele Objekte gleichen Typs aber mit unterschiedlichen Daten benötigt werden. Oft möchte man jedoch auch nur Funktionalität bündeln, zum Beispiel alle Funktionen, welche zur Auswertung eines gegebenen mathematischen Funktionsklasse notwendig sind. Oder man möchte verschiedene Klassen, zum Beispiel eine Anzahl von geometrischen Formen, welche man implementiert hat, zu einer Einheit zusammenfassen. Für solche und ähnliche Aufgaben stellt Python Module (modules) und Pakete (packages) zur Verfügung.\n",
+    "Ein Modul ist in Python die gesamte Funktionalität in einer Python Datei (mit der Endung \\*.py). Die Funktionalität kann durch import in anderen Python-Skripten, oder im interaktiven Interpreter, zur Verfügung gestellt werden. Zum Beispiel wenn wir unsere Vektor-Klasse in einer Datei vec.py\n",
+    "speichern, dann können wir die Funktionalität nach dem Import des Moduls verwenden (funktioniert hier nur wenn die Datei vec.py angelegt wird):"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Default import\n",
+    "import vec\n",
+    "# Import with new name\n",
+    "import vec as vector\n",
+    "# Import globally, not recommended to use as naming conflicts may occur\n",
+    "from vec import *"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Pakete sind eine Erweiterung des Modul Konzepts: es sind Verzeichnisse, welche Python Module enthalten. Zum Beispiel die NumPy Bibliothek, welche wir im Detail noch betrachten werden, ist ein Paket. Der Zugriff auf die Funktionalität erfolgt wieder durch import und Module in einem Paket\n",
+    "werden erneut wie Klassenfelder adressiert:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "A = np.eye(3)\n",
+    "B = np.linalg.inv(A)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Listen und Wörterbücher\n",
+    "Die meistverwendeten Container-Datenstrukturen, welche von Python zur\n",
+    "Verfügung gestellt werden, sind Listen und assoziative Dictionaries (oder Wörterbücher; wir werden i.A. den englischen Begriff verwenden).\n",
+    "Eine Liste ist eine geordnete Menge von Elementen beliebigen Typs. Zum Beispiel:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = [1, \"House\", 3.2]\n",
+    "print(a, a[0], a[1])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Python stellt darüber hinaus Funktionalität zum \"Slicing\" von Listen zur Verfügung. Zum Beispiel, auf alle Elemente in einer Teilsequenz kann wie folgt zugegriffen werden:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a[0:2]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Zuszätzlich stehen zahlreiche Klassenfunktionen zum Arbeiten auf Listen zur Verfügung. Ein Element kann zum Beispiel wie folgt an eine existierende Liste angefügt werden:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a.append(\"dog\")\n",
+    "a.append([1, 2, 3])\n",
+    "print(a, a[4][0], len(a))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Weiterführendes Thema: Manipulation von Listen\n",
+    "Nützlich für die Manipulation von Listen sind die Funktionen filter() und map(). Diese Funktionen liefern, genau wie range() einen Iterator zurück, den man gut in einer for-Schleife verwenden kann. Um explizit eine Liste zu erhalten muss man list() aufrufen."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def lessThan3(x): \n",
+    "   return x < 3\n",
+    "\n",
+    "b = 10.0 * np.random.rand(10)\n",
+    "print(b, list(filter(lessThan3, b)))\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Noch kompakter lässt sich der Code mithilfe des Lambda Kalküls formulieren:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(list(filter(lambda x: x < 3, b)))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Funktoren wie *lessThan3()* erlauben es Listen in sehr kompakter Art und Weise zu manipulieren. Ein weiteres Beispiel ist:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import math\n",
+    "print(list(map( math.sqrt, range(1,10))))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Hier haben wir die Funktion *sqrt()* aus der Math-Bibliothek als Funktor verwendet. In der Form von List Comprehensions erlauben for-Schleifen auch eine sehr kompakte Erzeugung von Listen:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "squares = [x**2 for x in range(10)]\n",
+    "print(squares)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Wörterbücher (Dictionaries) sind Datenstrukturen, bei welchen der Zugriff auf Elemente über Keys anstatt über Indizes wie bei Listen (oder Arrays) erfolgt. Zum Beispiel:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "age = {\"John\": 35, \"Carl\": 41}\n",
+    "print(age[\"John\"], age[\"Carl\"])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Die Keys für ein Dictionary müssen in Python immutable Types sein, wie zum Beispiel string oder int. Wie bei Listen kann auch für Dictionaries Comprehension benutzt werden, um diese zu erzeugen:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "e = {x: f(x) for x in range(1,10)}\n",
+    "print(e)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Schlussbemerkung\n",
+    "Wir haben hier nur die elementaren Grundlagen von Python besprechen können.\n",
+    "Sie sollten sich bemühen, im Verlauf des Kurses Ihre Python Kenntnisse zu vertiefen und effektive Lösungen für die an Sie gestellten Programmieraufgaben zu finden. Machen Sie sich dazu mit dem Gebrauch der [Python Dokumentation](https://docs.python.org/3.5/) vertraut. Im nächsten Kapitel besprechen wir die NumPy Bibliothek. Auch hier wird sich ein Blick in die [Dokumentation](http://www.NumPy.org/) lohnen."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Lineare Algebra"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "In den kommenden Wochen werden wir sehr häufig das Python Paket NumPy verwenden, welches speziell für die Umsetzung von linearer Algebra in Python entwickelt wurde. Die Effizienz, welche insbesondere für die Verarbeitung von großen Vektoren und Matrizen notwendig ist, wird dabei durch eine Implementierung der NumPy-Funktionalität in C/C++ realisiert.\n",
+    "Um NumPy verwenden zu können, müssen wir zunächst das Paket importieren (Die Benennung als np ist dabei nicht zwingend, aber weit verbreitet):"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Vektoren und Matrizen\n",
+    "Das Herzstück von NumPy ist ein n-dimensionales Array, welches zur\n",
+    "Repräsentation von Daten dient. Wir werden hauptsächlich den Fall n = 1 und n = 2 benötigen, d.h. wenn das Array einem Vektor oder einer Matrix entspricht. \n",
+    "Die einfachste Möglichkeit, ein NumPy Array zu erzeugen, ist durch eine Python Liste:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.array([1.0, 2.0, 3.0])\n",
+    "print(a)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Mehrdimensionale Arrays können durch verschachtelte Listen erzeugt werden:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "b = np.array([ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ])\n",
+    "print(b)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Arrays sind Python-Objekte und haben damit Funktionen definiert, durch welche wir mit diesen arbeiten können. Die Größe eines NumPy Arrays erhalten wir zum Beispiel durch:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(a.shape)\n",
+    "print(b.shape)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Wohingegen die Anzahl der Elemente durch size bestimmt werden kann:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(b.size)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Neben der Konstruktion durch Python Listen stellt NumPy auch Funktionen zur Verfügung, welche häufig verwendete Arrays konstruieren. Die Einheits-Matrix erhält man mit"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a_id = np.eye(3)\n",
+    "print(a_id)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "und ein Array, in welchem alle Elemente einen konstanten Wert haben durch"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "cvals = np.pi * np.ones((2,2,2))\n",
+    "print(cvals, cvals.shape)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Der Zugriff auf die Elemente eines NumPy Arrays erfolgt wie bei anderen Arrays (und Python Listen) durch"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(a[0])\n",
+    "print(b[0, 1])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Der Zugriff auf nicht existente Elemente erzeugt einen Laufzeitfehler:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a[5]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Mit dem [] Operator können wir auch Teile, sogenannte Slices, eines Arrays erhalten. Das erfolgt ähnlich dem Slicing bei Python Listen. Die Zeilen und Spalten werden hier allerdings durch Komma getrennt."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.array([ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0] ])\n",
+    "print(a)\n",
+    "\n",
+    "b = a[0]\n",
+    "print(b)\n",
+    "\n",
+    "print(a[:, :2]) # bedeutet: \"alle Zeilen, Spalte 0 und 1\"\n",
+    "\n",
+    "print(a[1:, ]) # bedeutet: \"ab der 1. Zeile, alle Spalten\"\n",
+    "\n",
+    "print(a[[0,2],:]) # bedeutet: \"Zeile 0 und 2, alle Spalten\""
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "So lassen sich auch bequem die Zeilen oder Spalten einer Matrix tauschen:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(a)\n",
+    "\n",
+    "a[[0,1],:] = a[[1,0],:]\n",
+    "print(a)\n",
+    "\n",
+    "a[:,[1,2]] = a[:,[2,1]]\n",
+    "print(a)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Es ist wichtig zu beachten, dass Slices nur Referenzen erzeugen und kein neues Datenobjekt. In diesem Punkt unterscheiden sich NumPy-Arrays und die Python-Container. Für die meisten Anwendungen ist ein elementweiser Zugriff jedoch nicht notwendig und auch ineffizient (wir werden dies noch genauer in einer Hausaufgabe betrachten). Soweit es möglich ist, sollten die durch NumPy zur Verfügung gestellten Operationen zum Arbeiten mit Arrays verwendet werden. Einige Beispiele sind:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "b = np.array( [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ])\n",
+    "print(b.max(), b.mean(), b.cumsum())\n",
+    "b.fill(3.0)\n",
+    "print(b)\n",
+    "print(b.nonzero())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Viele dieser Funktionen sind auch als nicht-Klassenfunktionen verfügbar, zum Beispiel:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(np.nonzero(b))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Zuweisungen von NumPy Arrays erzeugen nur eine neue Referenz auf das bestehende Datenobjekt (dies entspricht einer shallow copy). Man hat also:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "b = np.array([ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ])\n",
+    "print(b)\n",
+    "\n",
+    "d = b\n",
+    "print(d)\n",
+    "\n",
+    "d[0,1] = 99.0\n",
+    "print(b)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Um tatsächlich ein neues Objekt zu erzeugen, muss man die Funktion *copy()* verwenden:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "b = np.array([ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ])\n",
+    "print(b)\n",
+    "\n",
+    "d = b.copy()\n",
+    "print(d)\n",
+    "\n",
+    "d[0,1] = 99.0\n",
+    "print(b)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Numpy implementiert auch die klassischen arithmetische Operationen für Arrays, zum Beispiel:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.array( [ [1, 2], [ 2, 1] ])\n",
+    "b = np.array( [ [3.0, 2.0], [ 2.0, 3.0] ])\n",
+    "a + b"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Analog zur Addition sind auch alle anderen arithmetischen Operationen auf NumPy Arrays elementweise\n",
+    "definiert. Für Multiplikation haben wir also:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a * b"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Um Matrix Multiplikation durchzuführen, muss die Funktion *dot()* verwendet werden. Um eine Matrix mehrfach mit sich selbst zu multiplizieren eignet sich die Funktion *linalg.matrix_power()*."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(a.dot(b))\n",
+    "print(np.linalg.matrix_power(a, 2))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Weitere mathematische Operationen, welche für NumPy Arrays verfügbar sind, sind zum Beispiel:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(np.exp(a))\n",
+    "print(np.log(a))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Weiterführendes Thema: Datentypen in NumPy\n",
+    "Bis jetzt haben wir die Frage, welchen Datentyp die Elemente eines NumPy Arrays besitzen, ignoriert, obwohl dies für die Genauigkeit von Berechnungen von entscheidender Bedeutung ist. NumPy stellt im Gegensatz zu Python eine Vielzahl von numerischen Datentypen zur Verfügung: bool, int, int8, int16, int32, int64, uint8, uint16, uint32, uint64, float, float16, float32, float64, complex, complex64, complex128.\n",
+    "Der Datentyp der Elemente eines NumPy Arrays kann zur Laufzeit ermittelt werden:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.array([ [1.0, 2.0], [ 2.0, 1.0] ])\n",
+    "a.dtype"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "In diesem Fall haben wir zum Beispiel einen 64-bit Gleitkommazahl Datentyp. Im Allgemeinen bestimmt NumPy den Datentyp automatisch anhand der Daten. Eine leichte Veränderung des obigen Beispiels führt zum Beispiel zu:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.array([ [1, 2], [ 2, 1] ])\n",
+    "a.dtype"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Bis auf wenige Ausnahmen werden wir immer Gleitkommazahlen verwenden. Eine Möglichkeit dies sicherzustellen ist die explizite Spezifikation des Datentyps bei der Erzeugung eines Arrays:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.array([ [1, 2], [ 2, 1] ], dtype=\"float64\")\n",
+    "a.dtype"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Matplotlib\n",
+    "In vielen Anwendungen im wissenschaftlichen Rechnen ist eine graphische Darstellung von Daten wesentlich effizienter als die Begutachtung der numerischen Werte. Das folgende Beispiel verdeutlicht das:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import matplotlib\n",
+    "import matplotlib.pyplot as plt\n",
+    "# Enable inline plotting\n",
+    "%matplotlib inline \n",
+    "\n",
+    "x = np.linspace( 0.0, np.pi, 1000)\n",
+    "y = np.sin(x)\n",
+    "print(x[500:508])\n",
+    "print(y[500:508])\n",
+    "\n",
+    "plt.plot(x, y)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Matplotlib stellt zahlreiche Funktionen zur professionellen Erstellung und Bearbeitung von Graphen zur Verfügung. Sie sollten die von Ihnen benötigten Befehle selbstständig in den einschlägigen [Referenzen](http://matplotlib.org/) nachschlagen."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Weiterführendes Thema: SciPy\n",
+    "[SciPy](http://www.scipy.org/) ist eine Erweiterung von NumPy, welche Algorithmen zur Integration, Optimierung, Interpolation, Fourier Transform, Signalverarbeitung und vielen anderen Themen implementiert. In der Tat sind viele der Verfahren und Algorithmen, welche wir in diesem Kurs kennenlernen und implementieren\n",
+    "wollen, bereits in SciPy vorhanden. Auch nur einen Überblick über die verschiedenen Module zu geben, würde den Rahmen dieses Tutoriums sprengen. Deshalb betrachten wir hier beispielhaft Integration. Die einfachste Möglichkeit ist die Verwendung der Funktion *quad()* mit deren Hilfe numerisch integriert werden kann:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import scipy.integrate\n",
+    "scipy.integrate.quad(lambda x : np.sin(x), 0.0, 2.0 * np.pi)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Der erste Rückgabewert ist die numerische Abschätzung für das Integral, und der zweite Werte eine Schätzung des Fehlers (der korrekte Wert für das Integral ist natürlich 0.0). Wie wir hier bereits sehen, müssen wir uns bei numerischen Berechnungen im Allgemeinen mit Abschätzungen zufrieden geben, obwohl diese hoffentlich mit genügend Aufwand auch beliebig genau gemacht werden können. SciPy stellt verschiedene Quadraturalgorithmen zur Berechnung bzw. Abschätung von Integralen zur Verfügung. Dass das Integrationsproblem im Allgemeinen schwieriger ist, als es vielleicht auf den ersten Blick erscheint, wird am Beispiel von $f(x) = y = sin(x)/x$ deutlich:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for i in range(1, 6):\n",
+    "    print(scipy.integrate.fixed_quad(lambda x : np.sin(x)/x, 0.0, 2.0 * np.pi, (), i))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Der Wert des Integrals ist hier $$\\int_0^{2\\pi}\\frac{\\sin{x}}{x}dx \\approx 1,41815$$\n",
+    "Wir sehen, dass das Quadraturverfahren und die gewählten Parameter einen großen Einfluß auf die erzielte Genauigkeit haben können. Ihnen ein Verständnis zu vermitteln, unter welchen Umständen welches Quadraturverfahren zu bevorzugen ist, und wie man Verfahren für spezielle\n",
+    "Anwendungen entwickelt, ist eines der Ziele dieser Veranstaltung."
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/0_Python_LinA/tutorial_0_python_exercise2.ipynb b/0_Python_LinA/tutorial_0_python_exercise2.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..7ef301cb975e2be64c06880fe0d14117163efb1b
--- /dev/null
+++ b/0_Python_LinA/tutorial_0_python_exercise2.ipynb
@@ -0,0 +1,1174 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Einführung in Python und NumPy"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Variablen und Datentypen in Python"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Primitive Datentypen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = 5\n",
+    "b = \"Hello World\"\n",
+    "c = 3.14\n",
+    "d = True\n",
+    "e = 4 + 3j\n",
+    "print(a, b, c, d, e)\n",
+    "print(type(a), type(b), type(c), type(d), type(e))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Variablen überschreiben"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "var = 50\n",
+    "print(var)\n",
+    "var = \"Hello World\" # Neuer Datentyp\n",
+    "print(var)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Mehrere Variablen initialisieren"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "x, y, z = 5, 6, 7\n",
+    "i = j = k = \"ha\"\n",
+    "print(x, y, z)\n",
+    "print(i, j, k)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Variablen tauschen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a, b = 1, 2\n",
+    "a, b = b, a\n",
+    "print(b)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Operatoren"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a, b = 2, 4\n",
+    "print(a + b)\n",
+    "print(a - b)\n",
+    "print(a * b)\n",
+    "print(a / b)\n",
+    "print(a // b)\n",
+    "print(a % b)\n",
+    "print(a**b)\n",
+    "print(b**(1/a))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Variablen mit Operatoren neu zuweisen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "i = j = k = 7\n",
+    "i += 5;\n",
+    "j *= 5;\n",
+    "k **= 5;\n",
+    "print(i, j, k)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Boolesche Operatoren"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(1 + 1 == 2)\n",
+    "print(1 + 2 != 2)\n",
+    "print(1 + 2 > 3)\n",
+    "print(1 + 2 <= 3)\n",
+    "print(1 + 2 is 3)\n",
+    "print(True or False)\n",
+    "print(True and False)\n",
+    "print(not True)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Listen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "l = [\"python\", \"numpy\", \"scipy\", 10, True]\n",
+    "print(l[0], l[1], l[2], l[3], l[4])\n",
+    "print(l[-1], l[-2], l[-3], l[-4], l[-5])\n",
+    "print(len(l))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Listen bearbeiten"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "l.append(False)\n",
+    "print(l)\n",
+    "l[3] = \"matplotlib\"\n",
+    "print(l)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Operatoren auf Listen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "l1, l2 = [1,2,3,4], [5,6,7,8]\n",
+    "l = l1 + l2\n",
+    "print(l)\n",
+    "print(3 * l1)\n",
+    "print(5 in l2)\n",
+    "print(5 in l1)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Mehrdimensionale Listen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "A = [[1,2,3], [4,5,6], [7,8,9]]\n",
+    "print(A[0][0], A[1][0], A[1][2])\n",
+    "print(A[0], A[1], A[2])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### List Slices\n",
+    "Notation: liste[start: stop: step]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(l)\n",
+    "print(l[2:5])\n",
+    "print(l[2:])\n",
+    "print(l[:5])\n",
+    "print(l[:])\n",
+    "\n",
+    "print(l[2:5:2])\n",
+    "print(l[5:2:-1])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Tupel"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "t = (\"Audi\", 250, 4.7, True)\n",
+    "print(t[0], t[1], t[2], t[3])\n",
+    "print(t[-1], t[-2], t[-3], t[-4])\n",
+    "print(len(t))\n",
+    "print(\"Audi\" in t)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Tupel können nicht verändert werden, nur zusammengefügt!"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "t += (\"white\",)\n",
+    "print(t)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Dictionaries"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [],
+   "source": [
+    "c = {\n",
+    "    \"brand\": \"Audi\",\n",
+    "    \"ps\": 250,\n",
+    "    \"acc\": 4.7\n",
+    "}\n",
+    "\n",
+    "c[\"color\"] = \"white\"\n",
+    "\n",
+    "print(c[\"brand\"])\n",
+    "print(c.keys())\n",
+    "print(c.values())\n",
+    "print(c.items()) "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Funktionen und Verzweigungen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def greet(): \n",
+    "    print(\"Hello world\")\n",
+    "\n",
+    "greet()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def square(x):\n",
+    "    return x**2\n",
+    "\n",
+    "print(square(4))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Optionale Parameter und Parameter mit Namen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def pow(x, y = 0):\n",
+    "    return x**y\n",
+    "\n",
+    "print(pow(10))\n",
+    "print(pow(10, 2))\n",
+    "print(pow(y=2, x=5))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Mehrere Rückgabewerte (Tupel)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def f(x, y):\n",
+    "    return x**2, y**2\n",
+    "\n",
+    "a, b  = f(3, 4)\n",
+    "print(a, b)\n",
+    "ab = f(3, 4)\n",
+    "print(ab)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Bedingte Anweisungen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def sign(x):\n",
+    "    if x < 0:\n",
+    "        return -1\n",
+    "    elif x > 0:\n",
+    "        return 1\n",
+    "    else:\n",
+    "        return 0\n",
+    "    \n",
+    "print(sign(17), sign(-3), sign(0))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def positive(x):\n",
+    "    return True if x > 0 else False\n",
+    "\n",
+    "print(positive(14), positive(0))\n",
+    "\n",
+    "# return x > 0 ? True : False"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Schleifen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for i in range(5):\n",
+    "    print(i)\n",
+    "    \n",
+    "print(list(range(1, 6)))\n",
+    "print(list(range(1, 10, 2)))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Funktionsweise: range(start, stop, step)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### List Comprehensions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = [i**2 for i in range(5)]\n",
+    "print(a)\n",
+    "\n",
+    "b = [i**2 for i in range(10) if i % 2 == 0]\n",
+    "print(b)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Ãœber Datenstrukturen iterieren"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(l2)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for el in l2:\n",
+    "    print(el)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for i, el in enumerate(l2):\n",
+    "    print(i, el)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(c)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for key, val in c.items():\n",
+    "    print(key, val)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Objektorientierte Programmierung"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Person:\n",
+    "    def __init__(self, name):\n",
+    "        self.name = name\n",
+    "        \n",
+    "    def greet(self):\n",
+    "        print(\"Hallo, ich bin {}\".format(self.name))\n",
+    "        \n",
+    "    def __repr__(self):\n",
+    "        return \"Person: {}\".format(self.name)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "p = Person(\"Anna\")\n",
+    "p.greet()\n",
+    "print(p)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Student(Person):\n",
+    "    def __init__(self, name, age, height):\n",
+    "        Person.__init__(self, name)\n",
+    "        self.__age = age # private\n",
+    "        self._height = height # protected\n",
+    "        \n",
+    "    def __repr__(self):\n",
+    "        return \"Person[name={},age={},height={}]\"\\\n",
+    "            .format(self.name, self.__age, self._height)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "s = Student(\"Anna\", 18, 175)\n",
+    "s.greet()\n",
+    "print(s)\n",
+    "# print(s.__age)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Funktionale Programmierung"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "g = lambda x: x**4\n",
+    "h = lambda x, y: x**(2*y)\n",
+    "print(g(3))\n",
+    "print(h(2,2))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(l)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "lsq = map(lambda x: x**2, l)\n",
+    "print(lsq)\n",
+    "print(list(lsq))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(list(filter(lambda x: x % 2 == 1, l)))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from functools import reduce\n",
+    "print(reduce(lambda a, b: a + b, l))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(reduce(lambda a, b: a if a > b else b, l))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## == vs. is"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(5 == 5)\n",
+    "print(5 is 5)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print([1,2,3] == [1,2,3])\n",
+    "print([1,2,3] is [1,2,3])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## NumPy Grundlagen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### NumPy Arrays"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.array([1, 2, 3, 4, 5, 6])\n",
+    "print(a)\n",
+    "print(a[0])\n",
+    "print(a[-1])\n",
+    "print(a[1:4])\n",
+    "print(a[:2])\n",
+    "print(a[::-1])\n",
+    "print(a[1::2])\n",
+    "print(a[[0, 2, 3, 4]])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Operatoren auf NumPy Arrays"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "b = np.array([6, 5, 4, 3, 2, 1]) # b = a[::-1]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(a + 2)\n",
+    "print(3 * a)\n",
+    "print(a**2)\n",
+    "print(2**a)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(a + b)\n",
+    "print(a * b)\n",
+    "print(a**b)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Zweidimensionale Arrays (Matrizen)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n",
+    "print(A)\n",
+    "print(A[0,0], A[1, 2])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A[0])\n",
+    "print(A[:, 0])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A[:2, :2])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A[:2, 1::-1])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A[1::-1, :2])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Werte überschreiben"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "B = A.copy()\n",
+    "print(B)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "B[0] = 12\n",
+    "print(B)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "B[:, 1] = -5\n",
+    "print(B)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "B[2] = [5, 6, 7]\n",
+    "print(B)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "B[:, 1] = B[0]\n",
+    "print(B)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Dimensionen hinzufügen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A[None])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A[:, None])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A[:, :, None])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Boolesche Funktionen auf NumPy Arrays"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "b = np.array([1, 0, 1])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(b == 1)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(b < 1)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A[b == 1])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A[[True, False, True]])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A > 5)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A[A > 5])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Nützliche NumPy Funktionen"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X = np.ndarray((4,4))\n",
+    "A = np.zeros((3, 4))\n",
+    "B = np.ones((3,4))\n",
+    "c = np.arange(0, 1, .1)\n",
+    "d = np.linspace(0, 1, 10)\n",
+    "I = np.identity(4)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(X) # zufällige Werte"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(A)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(B)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(c)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(d)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(I)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "M = np.linspace(1, 16, 16).reshape((4,4))\n",
+    "print(M)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(M.T)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(M.sum(), M.mean(), M.prod())"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(M.sum(0), M.mean(0), M.prod(0))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(M.sum(1), M.mean(1), M.prod(1))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "x = np.linspace(1,4, 4)\n",
+    "print(x)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(M.dot(x))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(M @ x)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/0_Python_LinA/tutorial_0_python_exercise3.ipynb b/0_Python_LinA/tutorial_0_python_exercise3.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..3fafb4063da156e606ad4398e133d63e926afa1b
--- /dev/null
+++ b/0_Python_LinA/tutorial_0_python_exercise3.ipynb
@@ -0,0 +1,695 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Aufgaben zu Python und NumPy\n",
+    "\n",
+    "Im Folgenden sind einige Aufgaben zu Python und NumPy zum selbst bearbeiten gegeben. Eine beispielhafte Lösung zu den Aufgaben findet sich weiter unten."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Um sich mit der Programmiersprache Python vertraut zu machen, implementieren Sie einen einfachen Bubblesort-Algorithmus. Eine beispielhafte Eingabe und Ausgabe könnte so aussehen:</li></ol>\n",
+    "<pre>Eingabe: [ 5 14 10  7  2 13  1 12  6  9  0  3  8 11  4]\n",
+    "Ausgabe: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14]</pre>"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def bubblesort(liste:list) -> (list):\n",
+    "    \"\"\"\n",
+    "    Implements Bubblesort using an list as input and\n",
+    "    returning a sorted list\n",
+    "\n",
+    "    :param list\n",
+    "    :return: list\n",
+    "    \"\"\"\n",
+    "    # TODO: Implement Bubblesort\n",
+    "    return [\"Not Implemented Yet\"] "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "sorted_list = bubblesort([52, 18, 65, 61, 31, 86, 36, 15, 22, 13])\n",
+    "\n",
+    "print(sorted_list)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Aufgaben zu NumPy\n",
+    "\n",
+    "Die Bibliothek <em>[Numpy](https://numpy.org/doc/stable/index.html)</em> ist sehr umfangreich und wir werden auch nicht jede Funktionalität brauchen, aber es ist wichtig mit den grundlegenden Datenstrukturen vertraut zu sein. Wir haben hier eine Liste von kleinen Aufgaben zusammengestellt. Die Lösungen sind häufig Einzeiler oder Zweizeiler. Die Musterlösung benötigt in jeder Teilaufgabe maximal eine Zeile *(wenn man von dem Initalisieren der Arrays absieht)*. Es muss aber nicht krampfhaft versucht werden diese Vorgabe zu erreichen, dies soll nur ein Hinweis darauf sein, dass Sie keinen langen Code schreiben müssen. Lesen Sie sich selbstständig in die Dokumentation ein und versuchen Sie die Fragen mit den notwendigen Numpy-Funktionen zu beantworten. Sollten Sie keine Funktion finden, implementieren Sie eine Lösung selbst in einer kleinen Hilfsfunktion. Die Anzahl der Sternchen sollen ein Hinweis auf die Schwierigkeit geben."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "a) (*) Erzeugen Sie einen Vektor $a$ mit Nullen der Länge 10 (10 Elemente) und setzen den Wert des 5. Elementes auf eine 1."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "b) (*) Erzeugen Sie einen Vektor $b$ mit Ganzahl-Werten von 10 bis 49."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "c) (*) Erzeugen Sie einen Vektor mit 8 Einträgen zwischen -1 und 1 bei dem alle Werte die gleichen Abstände haben und sowohl -1 als auch 1 enthalten sind."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "d) (*) Geben Sie nur das Stück (slice) von Vektor $b$ aus, das die Zahlen 21 bis 38 (Stellen 11 bis 28) beinhaltet."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "e) (*) Ändern Sie den Vektor b) indem sie das Stück (slice) von Stelle 15 bis einschließlich Stelle 25 mit den Werten negierten Werten von Stelle 1 bis einschließlich Stelle 11 überschreiben."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "f) (*) Drehen Sie die Werte des Vektors aus $b$ um."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "g) (*) Summieren Sie alle Werte in einem Array."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "h) (*) Erzeugen Sie eine 4x4 Matrix mit den Werte 0 (links oben) bis 15 (rechts unten)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "i) (*) Erzeugen Sie eine 5x3 Matrix mit Zufall-Integer-Werten zwischen 0-100."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "j) (*) Multiplizieren Sie eine 4x3 Matrix mit einer 3x2 Matrix."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "k) (*) Erzeugen Sie eine 7x7 Matrix und geben Sie jeweils die geraden und die ungeraden Zeile in Form eines Slices aus."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "l) (**) Erzeuge eine 5x5 Matrix mit Zufallswerteintegers zwischen 0-100 und finde deren Maximum und Minimum und normalisieren Sie die Werte (sodass alle Werte zwischen 0 und 1 liegen - das bedeutet ein Wert wird 1 sein und einer 0) (geht in einer Zeile)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "m) (**) Extrahieren Sie den Integer-Anteil jeder zahl eines float Arrays von zufälliger Zahlen zwischen 0-10 auf 3 verschiedene Arten."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "n) (**) Erzeugen Sie eine Matrix $M$ der Größe 4x3 und einen Vektor $v$ mit Länge 3. Multiplizieren Sie jeden Eintrag aus $v$ mit der kompletten Spalte aus $M$. Nutzen Sie dafür [Broadcasting](https://numpy.org/doc/stable/user/basics.broadcasting.html)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "o) (***) Erzeugen Sie eine Matrix $M$ der Größe 4x3 und einen Vektor $v$ mit Länge 4. Multiplizieren Sie jeden Eintrag aus $v$ mit der kompletten Zeile aus $M$. Nutzen Sie dafür [Broadcasting](https://numpy.org/doc/stable/user/basics.broadcasting.html)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "p) (***) Erzeugen Sie einen Zufallsmatrix der Größe 10x2, die Sie als Kartesische Koordinateninterpretieren können ([[x0, y0],[x1, y1],[x2, y2]]). Konvertieren Sie diese in Polarkoordinaten <a href=\"https://de.wikipedia.org/wiki/Polarkoordinaten\">https://de.wikipedia.org/wiki/Polarkoordinaten</a>."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "q) (***) Erzeugen Sie einen Matrix der Größe 6x2, die Sie als Kartesische Koordinaten interpretieren können ([[x0, y0],[x1, y1],[x2, y2]]). Schreiben Sie eine Funktion, die alle Punkt-Punkt Abstände berechnet und in einer 6x6 Matrix ausgibt."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Lösungen zu den Aufgaben"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def bubblesort(liste:list) -> (list):\n",
+    "    \"\"\"\n",
+    "    Implements Bubblesort using an list as input and\n",
+    "    returning a sorted list\n",
+    "\n",
+    "    :param list\n",
+    "    :return: list\n",
+    "    \"\"\"\n",
+    "    n = len(liste)\n",
+    "    for i in range(n-1): \n",
+    "        for j in range(0, n-i-1): \n",
+    "            if liste[j] > liste[j+1] : \n",
+    "                liste[j], liste[j+1] = liste[j+1], liste[j] \n",
+    "    return liste"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "sorted_list = bubblesort([52, 18, 65, 61, 31, 86, 36, 15, 22, 13])\n",
+    "print(sorted_list)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "a) (*) Erzeugen Sie einen Vektor $a$ mit Nullen der Länge 10 (10 Elemente) und setzen den Wert des 5. Elementes auf eine 1."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.zeros(10)\n",
+    "a[4] = 1\n",
+    "print(a)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "b) (*) Erzeugen Sie einen Vektor $b$ mit Ganzahl-Werten von 10 bis 49."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "b = np.arange(10,50)\n",
+    "print(b)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "c) (*) Erzeugen Sie einen Vektor mit 8 Einträgen zwischen -1 und 1 bei dem alle Werte die gleichen Abstände haben und sowohl -1 als auch 1 enthalten sind."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "c = np.linspace(-1,1,8)\n",
+    "print(c)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "d) (*) Geben Sie nur das Stück (slice) von Vektor $b$ aus, das die Zahlen 21 bis 38 (Stellen 11 bis 28) beinhaltet."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(b[11:29])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "e) (*) Ändern Sie den Vektor b) indem sie das Stück (slice) von Stelle 15 bis einschließlich Stelle 25 mit den Werten negierten Werten von Stelle 1 bis einschließlich Stelle 11 überschreiben."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "b[15:26] = -b[1:12]\n",
+    "print(b)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "f) (*) Drehen Sie die Werte des Vektors aus $b$ um."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "b[::-1]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "g) (*) Summieren Sie alle Werte in einem Array."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "b.sum()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "h) (*) Erzeugen Sie eine 4x4 Matrix mit den Werte 0 (links oben) bis 15 (rechts unten)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "np.arange(16).reshape(4,4)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "i) (*) Erzeugen Sie eine 5x3 Matrix mit Zufallswerteintegers zwischen 0-100."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "np.random.randint(100, size=(5,3))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "j) (*) Multiplizieren Sie eine 4x3 Matrix mit einer 3x2 Matrix."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "j1 = np.arange(12).reshape(4,3)\n",
+    "j2 = np.arange(6).reshape(3,2)\n",
+    "j1@j2"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "k) (*) Erzeugen Sie eine 7x7 Matrix und geben Sie jeweils die geraden und die ungeraden Zeile in Form eines Slices aus (geht jeweils in einer Zeile)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "k = np.arange(7*7).reshape(7,7)\n",
+    "print(k[::2])\n",
+    "print(k[1::2])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "l) (**) Erzeuge eine 5x5 Matrix mit Zufallswerteintegers zwischen 0-100 und finde deren Maximum und Minimum und normalisieren Sie die Werte (sodass alle Werte zwischen 0 und 1 liegen - das bedeutet ein Wert wird 1 sein und einer 0)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "l = np.random.randint(100, size=(5,3))\n",
+    "print((l-l.min())/(l.max()-l.min()))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "m) (**) Extrahieren Sie den Integer-Anteil jeder zahl eines float Arrays von zufälliger Zahlen zwischen 0-10 auf 3 verschiedene Arten."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Extrahieren Sie den Integer-Anteil eine Arrays von zufälliger Zahlen zwischen 0-10 auf 3 verschiedene Arten.\n",
+    "m = np.random.rand(5)*10\n",
+    "print(m.astype(int))\n",
+    "print(np.modf(m)[1])\n",
+    "print(np.floor(m))\n",
+    "print(m-m%1)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "n) (**) Erzeugen Sie eine Matrix $M$ der Größe 4x3 und einen Vektor $v$ mit Länge 3. Multiplizieren Sie jeden Eintrag aus $v$ mit der kompletten Spalte aus $M$. Nutzen Sie dafür [Broadcasting](https://numpy.org/doc/stable/user/basics.broadcasting.html)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "n1 = np.random.rand(4,3)\n",
+    "n2 = np.random.rand(3)\n",
+    "n1*n2"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "o) (***) Erzeugen Sie eine Matrix $M$ der Größe 4x3 und einen Vektor $v$ mit Länge 4. Multiplizieren Sie jeden Eintrag aus $v$ mit der kompletten Zeile aus $M$. Nutzen Sie dafür [Broadcasting](https://numpy.org/doc/stable/user/basics.broadcasting.html)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "n1 = np.random.rand(4,3)\n",
+    "n2 = np.random.rand(4)\n",
+    "n1*n2[:,None]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "p) (***) Erzeugen Sie einen Zufallsmatrix der Größe 10x2, die Sie als Kartesische Koordinateninterpretieren können ([[x0, y0],[x1, y1],[x2, y2]]). Konvertieren Sie diese in Polarkoordinaten <a href=\"https://de.wikipedia.org/wiki/Polarkoordinaten\">https://de.wikipedia.org/wiki/Polarkoordinaten</a>."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "p = np.random.rand(10,2)\n",
+    "print(p)\n",
+    "print(np.array([np.sqrt(np.sum(p**2,axis=1)),np.arctan2(p[:,1], p[:,0])]).T)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "q) (***) Erzeugen Sie einen Matrix der Größe 6x2, die Sie als Kartesische Koordinaten interpretieren können ([[x0, y0],[x1, y1],[x2, y2]]). Schreiben Sie eine Funktion, die alle Punkt-Punkt Abstände berechnet und in einer 6x6 Matrix ausgibt."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "q = np.random.rand(6,2)\n",
+    "print(np.linalg.norm(q[:, None, :] - q[None, :, :], axis=-1))"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}