Compare commits
10 Commits
9f04971c20
...
e2dcba1600
| Author | SHA1 | Date | |
|---|---|---|---|
| e2dcba1600 | |||
| 093e2e980d | |||
| d5b078210f | |||
| dc637640c8 | |||
| 10b15d3515 | |||
| 873f3bfa83 | |||
| 7b407b4eb1 | |||
| 3e978c5dcd | |||
| c8fe899770 | |||
| 93f5702d5f |
@@ -4,6 +4,14 @@ class Student:
|
||||
self.year = year
|
||||
|
||||
|
||||
class Node:
|
||||
next = None
|
||||
previous = None
|
||||
|
||||
def __init__(self, student: Student):
|
||||
self.student = student
|
||||
|
||||
|
||||
# homework: implement this Queue which orders students by year.
|
||||
# students with a higher year are placed in front of students with a lower year
|
||||
class Queue:
|
||||
|
||||
130
notes-and-examples/2026.05.13/homework.py
Normal file
130
notes-and-examples/2026.05.13/homework.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# Also do the 2026.05.06 homework
|
||||
#
|
||||
# Implement insertion, removal and search in a binary search tree.
|
||||
# Properties of binary search trees:
|
||||
# - Each node has at most two children
|
||||
# - The left subtree of a node contains only nodes with values less than the node's value
|
||||
# - The right subtree of a node contains only nodes with values greater than the node's value
|
||||
#
|
||||
# Example of what such a tree would look like:
|
||||
# 10
|
||||
# / \
|
||||
# 5 15
|
||||
# / \ \
|
||||
# 3 7 20
|
||||
#
|
||||
# Example of an *invalid* tree:
|
||||
# 10
|
||||
# / \
|
||||
# 5 15
|
||||
# / \ \
|
||||
# 3 7 20
|
||||
# \
|
||||
# 12
|
||||
#
|
||||
# Don't worry about balancing the tree for now, we will cover that later
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, value, left=None, right=None):
|
||||
self.value = value
|
||||
self.left: Node | None = left
|
||||
self.right: Node | None = right
|
||||
|
||||
|
||||
class BinarySearchTree:
|
||||
def __init__(self, root=None):
|
||||
self.root: Node | None = root
|
||||
|
||||
def find_prev_node(self, value):
|
||||
prev_node: Node | None = None
|
||||
node = self.root
|
||||
while node is not None:
|
||||
prev_node = node
|
||||
if node.value >= value:
|
||||
node = node.left
|
||||
else:
|
||||
node = node.right
|
||||
|
||||
return prev_node
|
||||
|
||||
def find_existing_node_and_parent(self, value):
|
||||
prev_node: Node | None = None
|
||||
node = self.root
|
||||
|
||||
while node is not None:
|
||||
if value == node.value:
|
||||
return node, prev_node
|
||||
|
||||
prev_node = node
|
||||
if value < node.value:
|
||||
node = node.left
|
||||
else:
|
||||
node = node.right
|
||||
|
||||
return None, None
|
||||
|
||||
def insert(self, value):
|
||||
prev_node = self.find_prev_node(value)
|
||||
if prev_node is None:
|
||||
self.root = Node(value)
|
||||
return
|
||||
|
||||
if prev_node.value >= value:
|
||||
prev_node.left = Node(value)
|
||||
else:
|
||||
prev_node.right = Node(value)
|
||||
|
||||
def remove(self, value):
|
||||
node, prev_node = self.find_existing_node_and_parent(value)
|
||||
if prev_node is None and node is not None:
|
||||
self.root = None
|
||||
return
|
||||
elif node is not None:
|
||||
if prev_node.value >= value:
|
||||
prev_node.left = None
|
||||
else:
|
||||
prev_node.right = None
|
||||
|
||||
def search(self, value):
|
||||
node = self.root
|
||||
|
||||
while node is not None:
|
||||
if value == node.value:
|
||||
return node
|
||||
elif value < node.value:
|
||||
node = node.left
|
||||
else:
|
||||
node = node.right
|
||||
return None
|
||||
|
||||
|
||||
print("================ Inserts ================")
|
||||
|
||||
test_tree = BinarySearchTree()
|
||||
test_tree.insert(5)
|
||||
test_tree.insert(3)
|
||||
print(test_tree.root.left.value)
|
||||
|
||||
test_tree.insert(4)
|
||||
test_tree.insert(6)
|
||||
print(test_tree.root.left.right.value)
|
||||
print(test_tree.root.right.value)
|
||||
|
||||
print("================ Searches ================")
|
||||
|
||||
# is the search result giving us the expected node?
|
||||
print(test_tree.search(4) == test_tree.root.left.right)
|
||||
print(test_tree.search(6) == test_tree.root.right)
|
||||
print(test_tree.search(20) is None)
|
||||
|
||||
print("================ Removals ================")
|
||||
|
||||
test_tree.remove(4)
|
||||
print(test_tree.root.left.right)
|
||||
test_tree.remove(6)
|
||||
print(test_tree.root.right)
|
||||
test_tree.remove(3)
|
||||
print(test_tree.root.left)
|
||||
test_tree.remove(5)
|
||||
print(test_tree.root)
|
||||
121
notes-and-examples/2026.05.13/trees.py
Normal file
121
notes-and-examples/2026.05.13/trees.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# Example: expression tree + evaluation
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class TokenType(Enum):
|
||||
NUMBER = 1
|
||||
OPERATOR = 2
|
||||
|
||||
|
||||
# either store an operator (+, -, *, /) or a number
|
||||
class Token:
|
||||
def __init__(self, type: TokenType, value: str):
|
||||
self.type = type
|
||||
self.value = value
|
||||
|
||||
|
||||
# each individual element in the tree
|
||||
class Node:
|
||||
left = None
|
||||
right = None
|
||||
|
||||
def __init__(self, token: Token, left=None, right=None):
|
||||
self.token = token
|
||||
self.left = left
|
||||
self.right = right
|
||||
|
||||
|
||||
# holder for the tree
|
||||
class ExpressionTree:
|
||||
def __init__(self, root: Node | None):
|
||||
self.root = root
|
||||
|
||||
|
||||
def evaluate(tree: ExpressionTree) -> int:
|
||||
return _evaluate(tree.root)
|
||||
|
||||
|
||||
# evaluate a tree recursively starting from the node
|
||||
def _evaluate(node: Node | None) -> int:
|
||||
if node is None:
|
||||
return 0
|
||||
|
||||
if node.token.type == TokenType.NUMBER:
|
||||
return int(node.token.value)
|
||||
|
||||
elif node.token.type == TokenType.OPERATOR:
|
||||
if node.left is None or node.right is None:
|
||||
raise ValueError(f"Operator {node.token.value} requires two operands")
|
||||
|
||||
left = _evaluate(node.left)
|
||||
right = _evaluate(node.right)
|
||||
|
||||
if node.token.value == "+":
|
||||
return left + right
|
||||
elif node.token.value == "-":
|
||||
return left - right
|
||||
elif node.token.value == "*":
|
||||
return left * right
|
||||
elif node.token.value == "/":
|
||||
return left // right
|
||||
|
||||
raise ValueError(f"Unknown operator: {node.token.value}")
|
||||
|
||||
|
||||
# test cases
|
||||
test_cases = [
|
||||
(ExpressionTree(Node(Token(TokenType.NUMBER, "5"))), 5),
|
||||
(
|
||||
ExpressionTree(
|
||||
Node(
|
||||
Token(TokenType.OPERATOR, "+"),
|
||||
Node(Token(TokenType.NUMBER, "3")),
|
||||
Node(Token(TokenType.NUMBER, "2")),
|
||||
)
|
||||
),
|
||||
5,
|
||||
),
|
||||
(
|
||||
ExpressionTree(
|
||||
Node(
|
||||
Token(TokenType.OPERATOR, "+"),
|
||||
Node(Token(TokenType.NUMBER, "3")),
|
||||
Node(
|
||||
Token(TokenType.OPERATOR, "*"),
|
||||
Node(Token(TokenType.NUMBER, "2")),
|
||||
Node(Token(TokenType.NUMBER, "3")),
|
||||
),
|
||||
)
|
||||
),
|
||||
9,
|
||||
),
|
||||
(
|
||||
ExpressionTree(
|
||||
Node(
|
||||
Token(TokenType.OPERATOR, "+"),
|
||||
Node(
|
||||
Token(TokenType.OPERATOR, "-"),
|
||||
Node(Token(TokenType.NUMBER, "3")),
|
||||
Node(Token(TokenType.NUMBER, "2")),
|
||||
),
|
||||
Node(
|
||||
Token(TokenType.OPERATOR, "*"),
|
||||
Node(Token(TokenType.NUMBER, "2")),
|
||||
Node(Token(TokenType.NUMBER, "3")),
|
||||
),
|
||||
)
|
||||
),
|
||||
7,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_evaluate() -> None:
|
||||
for tree, expected in test_cases:
|
||||
evaluation = evaluate(tree) == expected
|
||||
print(f"Test case: evaluate({tree}) == {expected} -> {evaluation}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_evaluate()
|
||||
36
notes-and-examples/2026.05.27/find_depth.py
Normal file
36
notes-and-examples/2026.05.27/find_depth.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Tree example:
|
||||
# 10
|
||||
# / \
|
||||
# 5 15
|
||||
# / \ \
|
||||
# 3 7 20
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, value, left=None, right=None):
|
||||
self.value = value
|
||||
self.left: Node | None = left
|
||||
self.right: Node | None = right
|
||||
|
||||
|
||||
root_node = Node(10)
|
||||
root_node.left = Node(5)
|
||||
root_node.left.left = Node(3)
|
||||
root_node.left.right = Node(7)
|
||||
root_node.right = Node(15)
|
||||
root_node.right.right = Node(20)
|
||||
|
||||
|
||||
def find_depth(root: Node | None, subtree_depth=0) -> int:
|
||||
if root is None:
|
||||
return subtree_depth
|
||||
|
||||
left_depth = find_depth(root.left, subtree_depth + 1)
|
||||
right_depth = find_depth(root.right, subtree_depth + 1)
|
||||
return max(left_depth, right_depth)
|
||||
|
||||
|
||||
print(find_depth(root_node))
|
||||
|
||||
root_node.right.right.right = Node(1)
|
||||
print("After adding another level:", find_depth(root_node))
|
||||
183
notes-and-examples/2026.05.27/homework.py
Normal file
183
notes-and-examples/2026.05.27/homework.py
Normal file
@@ -0,0 +1,183 @@
|
||||
# Implement left rotates and right rotates for trees.
|
||||
#
|
||||
# Example right rotate on 10:
|
||||
#
|
||||
# 10
|
||||
# / \
|
||||
# 5 15
|
||||
# / \
|
||||
# 3 7
|
||||
# /
|
||||
# 2
|
||||
#
|
||||
# Turns into:
|
||||
#
|
||||
# 5
|
||||
# / \
|
||||
# 3 10
|
||||
# / / \
|
||||
# 2 7 15
|
||||
#
|
||||
# Notice how the right child of 10's left child (7) becomes the left child of 10.
|
||||
#
|
||||
# Left rotates are the reverse. See this example on 16:
|
||||
# 16
|
||||
# \
|
||||
# 33
|
||||
# / \
|
||||
# 22 55
|
||||
#
|
||||
# Turns into:
|
||||
#
|
||||
# 33
|
||||
# / \
|
||||
# 16 55
|
||||
# \
|
||||
# 22
|
||||
#
|
||||
# where the left child of 16's right child (22) turns into 16's right child.
|
||||
#
|
||||
# Copy this entire file, implement rotate_left and rotate_right,
|
||||
# and run the tests using `python3 <name-of-file>.py`.
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, value, left=None, right=None, parent=None):
|
||||
self.value = value
|
||||
self.left: Node | None = left
|
||||
self.right: Node | None = right
|
||||
|
||||
# required to re-set the child later
|
||||
self.parent: Node | None = parent
|
||||
|
||||
|
||||
def rotate_left(node: Node) -> Node:
|
||||
if node.right is None:
|
||||
return node
|
||||
|
||||
original_parent = node.parent
|
||||
new_parent = node.right
|
||||
new_right_child = node.right.left
|
||||
|
||||
node.right = new_right_child
|
||||
node.parent = new_parent
|
||||
new_parent.left = node
|
||||
|
||||
if original_parent is not None:
|
||||
if original_parent.left == node:
|
||||
original_parent.left = new_parent
|
||||
elif original_parent.right == node:
|
||||
original_parent.right = new_parent
|
||||
|
||||
return new_parent
|
||||
|
||||
|
||||
def rotate_right(node: Node) -> Node:
|
||||
if node.left is None:
|
||||
return node
|
||||
|
||||
original_parent = node.parent
|
||||
new_parent = node.left
|
||||
new_left_child = node.left.right
|
||||
|
||||
node.left = new_left_child
|
||||
node.parent = new_parent
|
||||
new_parent.right = node
|
||||
|
||||
if original_parent is not None:
|
||||
if original_parent.left == node:
|
||||
original_parent.left = new_parent
|
||||
elif original_parent.right == node:
|
||||
original_parent.right = new_parent
|
||||
|
||||
return new_parent
|
||||
|
||||
|
||||
# --- Tests ---------------------------------------------------------------
|
||||
# Both rotate functions are expected to return the new root of the rotated
|
||||
# subtree (the parent would then point its child link at that new root).
|
||||
|
||||
|
||||
def to_tuple(node: Node | None):
|
||||
"""Turn a tree into nested (value, left, right) tuples for easy comparison."""
|
||||
if node is None:
|
||||
return None
|
||||
return (
|
||||
node.value,
|
||||
to_tuple(node.left),
|
||||
to_tuple(node.right),
|
||||
node.parent.value if node.parent is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def trees_equal(a: Node | None, b: Node | None) -> bool:
|
||||
return to_tuple(a) == to_tuple(b)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
def test_rotate_right_example():
|
||||
# The worksheet example: right rotate on 10.
|
||||
root = Node(
|
||||
10,
|
||||
left=Node(5, left=Node(3, left=Node(2)), right=Node(7)),
|
||||
right=Node(15),
|
||||
)
|
||||
expected = Node(
|
||||
5,
|
||||
left=Node(3, left=Node(2)),
|
||||
right=Node(10, left=Node(7), right=Node(15)),
|
||||
)
|
||||
return rotate_right(root), expected
|
||||
|
||||
def test_rotate_left_example():
|
||||
# The worksheet example: left rotate on 16.
|
||||
root = Node(16, right=Node(33, left=Node(22), right=Node(55)))
|
||||
expected = Node(33, left=Node(16, right=Node(22)), right=Node(55))
|
||||
return rotate_left(root), expected
|
||||
|
||||
def test_rotate_right_two_nodes():
|
||||
# Smallest case: a root with only a left child.
|
||||
return rotate_right(Node(2, left=Node(1))), Node(1, right=Node(2))
|
||||
|
||||
def test_rotate_left_two_nodes():
|
||||
# Smallest case: a root with only a right child.
|
||||
return rotate_left(Node(1, right=Node(2))), Node(2, left=Node(1))
|
||||
|
||||
def test_round_trip():
|
||||
# A left rotate undone by a right rotate restores the original tree.
|
||||
original = Node(1, left=Node(0), right=Node(3, left=Node(2), right=Node(4)))
|
||||
expected = Node(1, left=Node(0), right=Node(3, left=Node(2), right=Node(4)))
|
||||
return rotate_right(rotate_left(original)), expected
|
||||
|
||||
def test_rotate_with_parent():
|
||||
# run it on 33 instead of the root
|
||||
root = Node(16, right=Node(33, left=Node(22), right=Node(55)))
|
||||
original = root.right
|
||||
expected = Node(55, left=Node(33, left=Node(22)))
|
||||
return rotate_left(original), expected # pyright: ignore[reportArgumentType]
|
||||
|
||||
tests = [
|
||||
("rotate_right on worksheet example (10)", test_rotate_right_example),
|
||||
("rotate_left on worksheet example (16)", test_rotate_left_example),
|
||||
("rotate_right on two nodes", test_rotate_right_two_nodes),
|
||||
("rotate_left on two nodes", test_rotate_left_two_nodes),
|
||||
("rotate_left then rotate_right restores the tree", test_round_trip),
|
||||
("rotate_left works when there is a parent", test_rotate_with_parent),
|
||||
]
|
||||
|
||||
passed = 0
|
||||
for name, test in tests:
|
||||
try:
|
||||
result, expected = test()
|
||||
if trees_equal(result, expected):
|
||||
print(f"PASS: {name}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f"FAIL: {name}")
|
||||
print(f" expected {to_tuple(expected)}")
|
||||
print(f" got {to_tuple(result)}")
|
||||
except Exception as e: # noqa: BLE001 - surface any bug as a failed test
|
||||
print(f"ERROR: {name}: {type(e).__name__}: {e}")
|
||||
|
||||
print(f"\n{passed}/{len(tests)} tests passed")
|
||||
40
notes-and-examples/2026.05.27/invert_tree.py
Normal file
40
notes-and-examples/2026.05.27/invert_tree.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Tree example:
|
||||
# 10
|
||||
# / \
|
||||
# 5 15
|
||||
# / \ \
|
||||
# 3 7 20
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, value, left=None, right=None):
|
||||
self.value = value
|
||||
self.left: Node | None = left
|
||||
self.right: Node | None = right
|
||||
|
||||
|
||||
root_node = Node(10)
|
||||
root_node.left = Node(5)
|
||||
root_node.left.left = Node(3)
|
||||
root_node.left.right = Node(7)
|
||||
root_node.right = Node(15)
|
||||
root_node.right.right = Node(20)
|
||||
|
||||
|
||||
def invert_tree(root: Node | None):
|
||||
if root is None:
|
||||
return
|
||||
|
||||
invert_tree(root.left)
|
||||
invert_tree(root.right)
|
||||
|
||||
root.left, root.right = root.right, root.left
|
||||
|
||||
|
||||
invert_tree(root_node)
|
||||
|
||||
print(root_node.left.value)
|
||||
print(root_node.right.value)
|
||||
print(root_node.left.left.value)
|
||||
print(root_node.right.right.value)
|
||||
print(root_node.right.left.value)
|
||||
BIN
notes-and-examples/2026.05.27/overview.pdf
Normal file
BIN
notes-and-examples/2026.05.27/overview.pdf
Normal file
Binary file not shown.
BIN
notes-and-examples/2026.06.03/overview.pdf
Normal file
BIN
notes-and-examples/2026.06.03/overview.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user