From 4c5d27e8d1501ec3ae0c9d50deee9bbf8b4e1114 Mon Sep 17 00:00:00 2001
From: Nils Pukropp <nils@narl.io>
Date: Wed, 14 Feb 2024 18:44:59 +0100
Subject: [PATCH] added some recursive datastructures

---
 recursion/recursive_datastructure/README.md   | 87 +++++++++++++++++++
 recursion/recursive_datastructure/lists.py    | 56 ++++++++++++
 .../recursive_datastructure/solution/lists.py | 62 +++++++++++++
 .../solution/search_trees.py                  | 45 ++++++++++
 .../solution/test_lists.py                    | 77 ++++++++++++++++
 .../solution/test_search_trees.py             | 24 +++++
 .../solution/test_trees.py                    | 13 +++
 .../recursive_datastructure/solution/trees.py | 33 +++++++
 recursion/recursive_datastructure/trees.py    | 19 ++++
 9 files changed, 416 insertions(+)
 create mode 100644 recursion/recursive_datastructure/README.md
 create mode 100644 recursion/recursive_datastructure/lists.py
 create mode 100644 recursion/recursive_datastructure/solution/lists.py
 create mode 100644 recursion/recursive_datastructure/solution/search_trees.py
 create mode 100644 recursion/recursive_datastructure/solution/test_lists.py
 create mode 100644 recursion/recursive_datastructure/solution/test_search_trees.py
 create mode 100644 recursion/recursive_datastructure/solution/test_trees.py
 create mode 100644 recursion/recursive_datastructure/solution/trees.py
 create mode 100644 recursion/recursive_datastructure/trees.py

diff --git a/recursion/recursive_datastructure/README.md b/recursion/recursive_datastructure/README.md
new file mode 100644
index 0000000..ea692a1
--- /dev/null
+++ b/recursion/recursive_datastructure/README.md
@@ -0,0 +1,87 @@
+# Rekursive Datenstrukturen
+
+
+## [LinkedList](./lists.py)
+
+In der Vorlesung wurden uns rekursive Datenstrukturen vorgestellt. Und zwar in der einfachsten Form, einer Liste. Hierbei modellieren wir eine *Node* Datenstruktur die unseren Wert *Value* trägt und auf die nächste *Node* in der Liste verweist durch *next_node*.
+
+```python
+from dataclasses import dataclass
+from typing import Optional
+
+@dataclass
+class Node[T]:
+    value: T
+    next_node: Optional['Node[T]']
+```
+
+Jetzt wollen wir eine Liste um unsere Node modellieren, hierbei interessiert uns nur die erste Node *__head* und die Länge *__length*.
+
+```python
+@dataclass
+class LinkedList[T]:
+
+    def __post_init__(self):
+        self.__head: Optional[Node[T]] = None
+        self.__length: int = 0
+```
+
+Jetzt geht es darum alle Standardmethoden die eine Liste braucht zu implementieren.
+
+## [BinaryTree](./trees.py)
+
+Jetzt wollen wir eine andere rekursive Datenstruktur definieren, den binary Tree. Diese besteht immer aus zwei Abzweigungen *right* und *left*.
+
+```python
+@dataclass
+class Node[T]:
+    value: T
+    left: Optional['Node[T]']
+    right: Optional['Node[T]']
+```
+
+Statt jetzt eine eigene Klasse zu erstellen die `Node` intern benutzt wollen wir einfach einen Typalias erstellen und imperative mit Funktionen statt Methoden arbeiten.
+
+```python
+type BinaryTree[T] = Optional[Node[T]]
+```
+
+### `traverse`
+
+Man kann einen BinaryTree auf drei typische arten ablaufen
+
+- Inorder
+  - Erst laufen wir Links ab
+  - Dann schauen wir uns die Node an
+  - Dann laufen wir Rechts ab
+- Preorder
+  - Erst schauen wir uns die Node an
+  - Dann laufen wir Links ab
+  - Dann laufen wir Rechts ab
+- Postorder
+  - Erst laufen wir Links ab
+  - Dann laufen wir Rechts ab
+  - Dann schauen wir uns die Node an
+
+Hierfür erstellen wir einen `Enum` um zu unterscheiden wie wir gerade ablaufen
+
+```python
+class TraversalType(Enum):
+    INORDER, POSTORDER, PREORDER = auto(), auto(), auto()
+```
+
+Und jetzt implementieren wir einen Generator der uns einen Iterator erzeugt welcher den BinaryTree in der gegebenen Order abläuft.
+
+```python
+def traverse[T](tree: BinaryTree[T], order: TraversalType = TraversalType.INORDER) -> Iterator[T]:
+    pass
+```
+
+Tipp:
+
+Mit `yield from ...` kann man einen ganzen Iterator *yield*en.
+
+```python
+def my_range(start: int, end: int) -> Iterator[int]:
+    yield from range(start, end)
+```
\ No newline at end of file
diff --git a/recursion/recursive_datastructure/lists.py b/recursion/recursive_datastructure/lists.py
new file mode 100644
index 0000000..6794837
--- /dev/null
+++ b/recursion/recursive_datastructure/lists.py
@@ -0,0 +1,56 @@
+from dataclasses import dataclass
+from typing import Iterator, Optional
+
+@dataclass
+class Node[T]:
+    value: T
+    next_node: Optional['Node[T]']
+   
+
+@dataclass
+class LinkedList[T]:
+    
+    def __post_init__(self):
+        # unser erstes Element
+        self.__head: Optional[Node[T]] = None
+        # Die Länge der Liste
+        self.__length = 0
+    
+    def __eq__(self, other: 'LinkedList[T]') -> bool:
+        """Hier wollen wir alle Elemente von dieser Liste mit allen Elementen von einer anderen Liste vergleichen.
+        Hierbei kann man von ausgehen dass T vergleichbar ist. 
+        Das Programm sollte nicht abstürzen bei unterschiedlichen längen etc. sondern einfach False zurückgeben"""
+        pass
+        
+    def __iter__(self) -> Iterator[T]:
+        """Hier wollen wir einen Iterator über alle Elemente haben, der Iterator kann leer sein
+        Tipp: Generator"""
+        pass
+        
+    def append(self, value: T) -> None:
+        """Hier wollen wir einfach `value` an das Ende der Liste setzen und die Länge erhöhen"""
+        pass
+        
+    def remove(self, value: T):
+        """Hier wollen wir den Wert `value` aus unserer Liste entfernen, wir können davon ausgehen dass T vergleichbar ist. 
+        Wenn wir `value` nicht finden soll einfach nichts passieren"""
+        pass
+                
+    def __len__(self) -> int:
+        """Hier soll die Länge der Liste zurückgegeben werden"""
+        pass
+    
+    def __getitem__(self, index: int) -> T:
+        """_summary_
+        
+        __getitem__ definiert das verhalten vom [] index operator wobei `index` der Wert ist der in [] übergeben wird
+        
+        Hierbei sollen wir das Item `T` an Stelle `index` zurückgeben und sonst einen IndexError raisen
+
+        Args:
+            index (int): der Index von dem Item was wir suchen
+
+        Returns:
+            T: das Item an stelle `index`
+        """
+        pass
\ No newline at end of file
diff --git a/recursion/recursive_datastructure/solution/lists.py b/recursion/recursive_datastructure/solution/lists.py
new file mode 100644
index 0000000..bd24ae9
--- /dev/null
+++ b/recursion/recursive_datastructure/solution/lists.py
@@ -0,0 +1,62 @@
+from dataclasses import dataclass
+from typing import Iterator, Optional
+
+@dataclass
+class Node[T]:
+    value: T
+    next_node: Optional['Node[T]']
+   
+
+@dataclass
+class LinkedList[T]:
+    
+    def __post_init__(self):
+        self.__head: Optional[Node[T]] = None
+        self.__length = 0
+    
+    def __eq__(self, other: 'LinkedList[T]') -> bool:
+        if len(self) != len(other):
+            return False
+        for i in range(0, len(self)):
+            if self[i] != self[i]:
+                return False
+        return True
+        
+    def __iter__(self) -> Iterator[T]:
+        node = self.__head
+        while node:
+            yield node.value
+            node = node.next_node
+        
+    def append(self, value: T):
+        if not self.__head:
+            self.__head = Node(value, None)
+        else:
+            node = self.__head
+            while node.next_node:
+                node = node.next_node
+            node.next_node = Node(value, None)
+        self.__length += 1
+        
+    def remove(self, value: T):
+        if self.__head and self.__head.value == value:
+            self.__head = self.__head.next_node
+            self.__length -= 1
+        elif self.__head:
+            node = self.__head
+            while node.next_node and node.next_node.value != value:
+                node = node.next_node
+            if node.next_node and node.next_node.value == value:
+                node.next_node = node.next_node.next_node
+                self.__length -= 1
+                
+    def __len__(self) -> int:
+        return self.__length
+    
+    def __getitem__(self, index: int) -> T:
+        if index >= len(self):
+            raise IndexError
+        node = self.__head
+        for _ in range(0, index):
+            node = node.next_node
+        return node.value
\ No newline at end of file
diff --git a/recursion/recursive_datastructure/solution/search_trees.py b/recursion/recursive_datastructure/solution/search_trees.py
new file mode 100644
index 0000000..847014e
--- /dev/null
+++ b/recursion/recursive_datastructure/solution/search_trees.py
@@ -0,0 +1,45 @@
+from typing import Iterator, Optional
+from trees import Node, traverse
+from dataclasses import InitVar, dataclass
+
+@dataclass
+class BinarySearchTree[T]:
+    
+    def __post_init__(self):
+        self.__root: Optional[Node[T]] = None
+        
+        
+    def insert(self, value: T):
+        prev = None
+        curr = self.__root
+        while curr:
+            prev = curr
+            if curr.value > value:
+                curr = curr.left
+            else:
+                curr = curr.right
+        if prev is None:
+            self.__root = Node(value, None, None)
+        elif value < prev.value:
+            prev.left = Node(value, None, None)
+        else:
+            prev.right = Node(value, None, None)
+            
+    def __delete(prev: Node[T], to_delete: Node[T]) -> Node[T]:
+        # TODO: implement
+        pass
+    
+    def remove(self, value: T):
+        prev = None
+        curr = self.__root
+        while curr:
+            prev = curr
+            if curr.value > value:
+                curr = curr.left 
+            elif curr.value < value:
+                curr = curr.right
+            else:
+                curr = self.__delete(prev, curr)
+            
+    def __iter__(self) -> Iterator[T]:
+        yield from traverse(self.__root)
\ No newline at end of file
diff --git a/recursion/recursive_datastructure/solution/test_lists.py b/recursion/recursive_datastructure/solution/test_lists.py
new file mode 100644
index 0000000..23c0823
--- /dev/null
+++ b/recursion/recursive_datastructure/solution/test_lists.py
@@ -0,0 +1,77 @@
+from lists import LinkedList
+
+def test_append():
+    lst = LinkedList[int]()
+    lst.append(1)
+    lst.append(2)
+    lst.append(3)
+    lst.append(4)
+    lst2 = LinkedList[int]()
+    lst2.append(1)
+    lst2.append(2)
+    lst2.append(3)
+    lst2.append(4)
+    assert lst == lst2
+
+def test_remove():
+    lst = LinkedList[int]()
+    lst.remove(0)
+    lst.append(1)
+    lst.append(2)
+    lst.append(3)
+    lst.append(4)
+    lst.remove(2)
+    lst.remove(5)
+    lst2 = LinkedList[int]()
+    lst2.append(1)
+    lst2.append(3)
+    lst2.append(4)
+    assert lst == lst2
+
+def test_length():
+    lst = LinkedList[int]()
+    lst.append(0)
+    lst.append(1)
+    lst.append(2)
+    lst.append(3)
+    lst.append(4)
+    assert len(lst) == 5
+    lst.remove(0)
+    assert len(lst) == 4
+    lst.remove(2)
+    assert len(lst) == 3
+    lst.remove(1)
+    assert len(lst) == 2
+    lst.remove(3)
+    assert len(lst) == 1
+    lst.remove(4)
+    assert len(lst) == 0
+    lst.remove(4)
+    lst.remove(5)
+    assert len(lst) == 0
+    
+
+def test_index():
+    lst = LinkedList[int]()
+    lst.append(0)
+    lst.append(1)
+    lst.append(2)
+    lst.append(3)
+    lst.append(4)
+    assert lst[0] == 0
+    assert lst[1] == 1
+    assert lst[2] == 2
+    assert lst[3] == 3
+    assert lst[4] == 4
+
+def test_iter():
+    lst = LinkedList[int]()
+    lst.append(0)
+    lst.append(1)
+    lst.append(2)
+    lst.append(3)
+    lst.append(4)
+    j = 0
+    for i in iter(lst):
+        assert i == j
+        j += 1
\ No newline at end of file
diff --git a/recursion/recursive_datastructure/solution/test_search_trees.py b/recursion/recursive_datastructure/solution/test_search_trees.py
new file mode 100644
index 0000000..7b70672
--- /dev/null
+++ b/recursion/recursive_datastructure/solution/test_search_trees.py
@@ -0,0 +1,24 @@
+from search_trees import BinarySearchTree
+from random import randint as random
+
+MAX= 1000
+MIN = 0
+LENGTH = 100
+
+def test_insert():
+    nums = [random(MIN, MAX) for _ in range(0, LENGTH)]
+    bst = BinarySearchTree()
+    for num in nums:
+        bst.insert(num)
+    assert list(iter(bst)) == sorted(nums)
+    
+def test_remove():
+    nums = {random(MIN, MAX) for _ in range(0, LENGTH)}
+    fil = {random(MIN, MAX) for _ in range(0, LENGTH // 4)}
+    bst = BinarySearchTree()
+    for num in nums:
+        bst.insert(num)
+    for num in fil:
+        bst.remove(num)
+        
+    assert list(iter(bst)) == sorted(filter(lambda x: x not in fil, nums))
\ No newline at end of file
diff --git a/recursion/recursive_datastructure/solution/test_trees.py b/recursion/recursive_datastructure/solution/test_trees.py
new file mode 100644
index 0000000..4534c4e
--- /dev/null
+++ b/recursion/recursive_datastructure/solution/test_trees.py
@@ -0,0 +1,13 @@
+from trees import Node, traverse, TraversalType
+
+
+def test_traverse():
+    tree = Node(1, Node(2, Node(3, None, None), Node(4, Node(
+        5, None, None), None)), Node(6, Node(7, None, None), Node(8, None, None)))
+
+    assert ", ".join(map(lambda x: str(x), traverse(tree))
+                     ) == "3, 2, 5, 4, 1, 7, 6, 8"
+    assert ", ".join(map(lambda x: str(x), traverse(
+        tree, TraversalType.PREORDER))) == "1, 2, 3, 4, 5, 6, 7, 8"
+    assert ", ".join(map(lambda x: str(x), traverse(
+        tree, TraversalType.POSTORDER))) == "3, 5, 4, 2, 7, 8, 6, 1"
\ No newline at end of file
diff --git a/recursion/recursive_datastructure/solution/trees.py b/recursion/recursive_datastructure/solution/trees.py
new file mode 100644
index 0000000..e737a87
--- /dev/null
+++ b/recursion/recursive_datastructure/solution/trees.py
@@ -0,0 +1,33 @@
+from dataclasses import dataclass
+from enum import Enum, auto
+from typing import Iterator, Optional
+
+
+@dataclass
+class Node[T]:
+    value: T
+    left: Optional['Node[T]']
+    right: Optional['Node[T]']
+
+
+type BinaryTree[T] = Optional[Node[T]]
+
+class TraversalType(Enum):
+    INORDER, POSTORDER, PREORDER = auto(), auto(), auto()
+
+def traverse[T](tree: BinaryTree[T], order: TraversalType = TraversalType.INORDER) -> Iterator[T]:
+    match (tree, order):
+        case (Node(value, left, right), TraversalType.INORDER):
+            yield from traverse(left, order)
+            yield value
+            yield from traverse(right, order)
+        case (Node(value, left, right), TraversalType.POSTORDER):
+            yield from traverse(left, order)
+            yield from traverse(right, order)
+            yield value
+        case (Node(value, left, right), TraversalType.PREORDER):
+            yield value
+            yield from traverse(left, order)
+            yield from traverse(right, order)
+        case _:
+            return
\ No newline at end of file
diff --git a/recursion/recursive_datastructure/trees.py b/recursion/recursive_datastructure/trees.py
new file mode 100644
index 0000000..b9cc1d7
--- /dev/null
+++ b/recursion/recursive_datastructure/trees.py
@@ -0,0 +1,19 @@
+from dataclasses import dataclass
+from enum import Enum, auto
+from typing import Iterator, Optional
+
+
+@dataclass
+class Node[T]:
+    value: T
+    left: Optional['Node[T]']
+    right: Optional['Node[T]']
+
+
+type BinaryTree[T] = Optional[Node[T]]
+
+class TraversalType(Enum):
+    INORDER, POSTORDER, PREORDER = auto(), auto(), auto()
+
+def traverse[T](tree: BinaryTree[T], order: TraversalType = TraversalType.INORDER) -> Iterator[T]:
+    pass
\ No newline at end of file