Compare commits
23 Commits
9f04971c20
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a7ee5d06a0 | |||
| 059c848d3d | |||
| d488ac5575 | |||
| 95544077bc | |||
| 42a3430627 | |||
| c97e452c44 | |||
| 953ce7f2cb | |||
| 4ca03b5487 | |||
| 732159838d | |||
| 94f913a52b | |||
| 40db395a85 | |||
| 14c14d1797 | |||
| 0fd98a46ca | |||
| e2dcba1600 | |||
| 093e2e980d | |||
| d5b078210f | |||
| dc637640c8 | |||
| 10b15d3515 | |||
| 873f3bfa83 | |||
| 7b407b4eb1 | |||
| 3e978c5dcd | |||
| c8fe899770 | |||
| 93f5702d5f |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.idea
|
||||
|
||||
|
||||
@@ -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))
|
||||
229
notes-and-examples/2026.05.27/homework.py
Normal file
229
notes-and-examples/2026.05.27/homework.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# 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
|
||||
if new_right_child is not None:
|
||||
new_right_child.parent = node
|
||||
|
||||
new_parent.left = node
|
||||
node.parent = new_parent
|
||||
new_parent.parent = original_parent
|
||||
|
||||
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
|
||||
if new_left_child is not None:
|
||||
new_left_child.parent = node
|
||||
|
||||
new_parent.right = node
|
||||
node.parent = new_parent
|
||||
new_parent.parent = original_parent
|
||||
|
||||
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__":
|
||||
# note that matching is done by value for simplicity here
|
||||
|
||||
def test_rotate_right_example():
|
||||
# The worksheet example: right rotate on 10.
|
||||
root = Node(
|
||||
10,
|
||||
left=Node(
|
||||
5,
|
||||
left=Node(3, left=Node(2, parent=Node(3)), parent=Node(5)),
|
||||
right=Node(7, parent=Node(5)),
|
||||
parent=Node(10),
|
||||
),
|
||||
right=Node(15, parent=Node(10)),
|
||||
)
|
||||
expected = Node(
|
||||
5,
|
||||
left=Node(3, left=Node(2, parent=Node(3)), parent=Node(5)),
|
||||
right=Node(
|
||||
10,
|
||||
left=Node(7, parent=Node(10)),
|
||||
right=Node(15, parent=Node(10)),
|
||||
parent=Node(5),
|
||||
),
|
||||
)
|
||||
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, parent=Node(33)),
|
||||
right=Node(55, parent=Node(33)),
|
||||
parent=Node(16),
|
||||
),
|
||||
)
|
||||
expected = Node(
|
||||
33,
|
||||
left=Node(16, right=Node(22, parent=Node(16)), parent=Node(33)),
|
||||
right=Node(55, parent=Node(33)),
|
||||
)
|
||||
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, parent=Node(2)))), Node(1, right=Node(2, parent=Node(1)))
|
||||
|
||||
|
||||
def test_rotate_left_two_nodes():
|
||||
# Smallest case: a root with only a right child.
|
||||
return rotate_left(Node(1, right=Node(2, parent=Node(1)))), Node(2, left=Node(1, parent=Node(2)))
|
||||
|
||||
|
||||
def test_round_trip():
|
||||
# A left rotate undone by a right rotate restores the original tree.
|
||||
original = Node(1, left=Node(0, parent=Node(1)),
|
||||
right=Node(3, left=Node(2, parent=Node(3)), right=Node(4, parent=Node(3)), parent=Node(1)))
|
||||
expected = Node(1, left=Node(0, parent=Node(1)),
|
||||
right=Node(3, left=Node(2, parent=Node(3)), right=Node(4, parent=Node(3)), parent=Node(1)))
|
||||
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)))
|
||||
root.right.parent = root
|
||||
root.right.left.parent = root.right
|
||||
root.right.right.parent = root.right
|
||||
original = root.right
|
||||
expected = Node(
|
||||
55,
|
||||
left=Node(33, left=Node(22, parent=Node(33)), parent=Node(55)),
|
||||
parent=Node(16),
|
||||
)
|
||||
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)
|
||||
16
notes-and-examples/2026.05.27/overview.md
Normal file
16
notes-and-examples/2026.05.27/overview.md
Normal file
@@ -0,0 +1,16 @@
|
||||
## Outline
|
||||
|
||||
- Review homework and do some tree exercises
|
||||
- Introduce tree balancing
|
||||
- Introduce red-black trees
|
||||
- Basic properties
|
||||
- Root property: the root is black
|
||||
- External property: every nil child pointer is considered a black node
|
||||
- Internal property: children and parents of a red node are black
|
||||
- Depth property: all nodes have the same black depth
|
||||
- Insertion and balancing algorithms
|
||||
- Assignment: implement red-black tree balancing
|
||||
|
||||
## Resources
|
||||
|
||||
- [Red-black tree - Wikipedia](https://en.wikipedia.org/wiki/Red–black_tree)
|
||||
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.
41
notes-and-examples/2026.06.12/overview.md
Normal file
41
notes-and-examples/2026.06.12/overview.md
Normal file
@@ -0,0 +1,41 @@
|
||||
## Outline
|
||||
|
||||
- Write a red-black tree with rebalancing
|
||||
- Assignment: start on the red-black tree (moved to 2026.07.01)
|
||||
- Requirement: you must use the left- and right-rotation code in the previous assignment
|
||||
- You may change the code to fit the class
|
||||
- Next week: introduce graphs and graph algorithms
|
||||
|
||||
## Git reference
|
||||
|
||||
There are two repositories to keep track of:
|
||||
|
||||
- [My code](https://gitea.bchen.dev/brendan/dsa-tutoring)
|
||||
- Get notes and sample code
|
||||
- [Your code](https://gitea.bchen.dev/ethan/dsa-homework)
|
||||
- Submit your homework
|
||||
|
||||
To submit homework, add all new files and changes to your code:
|
||||
|
||||
```
|
||||
git add .
|
||||
```
|
||||
|
||||
Commit with a message:
|
||||
|
||||
```
|
||||
git commit -m "Hello World"
|
||||
```
|
||||
|
||||
Push changes:
|
||||
|
||||
```
|
||||
git push
|
||||
```
|
||||
|
||||
|
||||
To pull new changes to the notes and sample code, run this in the `dsa-tutoring` directory:
|
||||
|
||||
```
|
||||
git pull
|
||||
```
|
||||
41
notes-and-examples/2026.07.01/overview.md
Normal file
41
notes-and-examples/2026.07.01/overview.md
Normal file
@@ -0,0 +1,41 @@
|
||||
## Outline
|
||||
|
||||
- Review tree rotations and red-black trees
|
||||
- Properties of a red-black tree
|
||||
- Top (root) node is black
|
||||
- The children and parent of a red node are black
|
||||
- Null nodes are colored black
|
||||
- The path from any particular node to a null node must contain the
|
||||
same number of black nodes
|
||||
- Dive into the steps for rebalancing a red-black tree
|
||||
|
||||
Also see the accompanying whiteboard pictures.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Assignment
|
||||
|
||||
**Clarifications on the red-black tree example:**
|
||||
|
||||
- In your assignment, you should first color a node _red_ when inserting it.
|
||||
- Then, check for red-red violations (the second rule). Then, rebalance if necessary.
|
||||
- If the uncle of the inserted node (parent → parent → right child) is red,
|
||||
you'll want to do two things:
|
||||
- Color the parent, uncle, and grandparent in a way which preserves all the rules
|
||||
(which ones are colored which, are an exercise left to you)
|
||||
- _Recursively_ run the rebalancing algorithm on the grandparent
|
||||
- Otherwise, your task is to figure out the correct conditions in which
|
||||
each of the rebalancing algorithms apply.
|
||||
|
||||
Copy 2026.07.01/homework.py into your own [Git repository](https://gitea.bchen.dev/ethan/dsa-homework),
|
||||
with a new folder for the date. Implement the `rebalance_from_just_inserted`
|
||||
function.
|
||||
|
||||
Don't change the test cases, but use them to inform how you implement your
|
||||
code. Try to pass all the test cases.
|
||||
|
||||
Follow the instructions from last session (2026.06.12) to commit and push
|
||||
your changes to Gitea.
|
||||
|
||||
BIN
notes-and-examples/2026.07.01/red-black-trees.png
Normal file
BIN
notes-and-examples/2026.07.01/red-black-trees.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 486 KiB |
494
notes-and-examples/2026.07.01/red_black_tree.py
Normal file
494
notes-and-examples/2026.07.01/red_black_tree.py
Normal file
@@ -0,0 +1,494 @@
|
||||
import traceback
|
||||
|
||||
class Node:
|
||||
def __init__(self, value, left=None, right=None, parent=None, is_red=False):
|
||||
self.value = value
|
||||
self.left: Node | None = left
|
||||
self.right: Node | None = right
|
||||
self.is_red = is_red
|
||||
|
||||
# required to traverse the tree and perform various checks
|
||||
self.parent: Node | None = parent
|
||||
|
||||
class RedBlackTree:
|
||||
def __init__(self):
|
||||
self.root: Node | None = None
|
||||
|
||||
def insert(self, value):
|
||||
# assumption: inserting the same value twice will
|
||||
# insert nothing the second time
|
||||
|
||||
node = Node(value)
|
||||
if self.root is None:
|
||||
# the root is always black (Node defaults to black)
|
||||
self.root = node
|
||||
return
|
||||
|
||||
# insert like a normal binary search tree
|
||||
prev: Node | None = None
|
||||
current = self.root
|
||||
while current:
|
||||
if current.value == value:
|
||||
return
|
||||
prev = current
|
||||
if current.value > value:
|
||||
current = current.left
|
||||
elif current.value < value:
|
||||
current = current.right
|
||||
|
||||
# a newly inserted (non-root) node is always red, and it needs a
|
||||
# parent pointer so rebalancing can walk back up toward the root
|
||||
node.is_red = True
|
||||
node.parent = prev
|
||||
if prev and prev.value > value:
|
||||
prev.left = node
|
||||
elif prev and prev.value < value:
|
||||
prev.right = node
|
||||
|
||||
self.rebalance_from_just_inserted(node)
|
||||
|
||||
def delete(self, value, root=None, parent=None):
|
||||
# does nothing if the value doesn't exist
|
||||
if parent is None:
|
||||
root = self.root
|
||||
if root is None:
|
||||
return
|
||||
|
||||
if root.value == value:
|
||||
new_root: Node | None
|
||||
if root.left is None:
|
||||
new_root = root.right
|
||||
elif root.right is None:
|
||||
new_root = root.left
|
||||
else:
|
||||
# both left and right subtrees exist
|
||||
# make left subtree root the tree root, and re-attach
|
||||
# the right subtree
|
||||
right_subtree_root = root.right
|
||||
new_root = root.left
|
||||
|
||||
prev = None
|
||||
current = new_root
|
||||
while current:
|
||||
prev = current
|
||||
current = current.right
|
||||
prev.right = right_subtree_root
|
||||
|
||||
if parent is None:
|
||||
self.root = new_root
|
||||
else:
|
||||
if parent.value > root.value:
|
||||
parent.left = new_root
|
||||
else:
|
||||
parent.right = new_root
|
||||
elif root.value > value:
|
||||
self.delete(value, root.left, root)
|
||||
elif root.value < value:
|
||||
self.delete(value, root.right, root)
|
||||
|
||||
def rebalance_from_just_inserted(self, node: Node | None):
|
||||
if node is None:
|
||||
return
|
||||
|
||||
parent = node.parent
|
||||
if parent is not None and not parent.is_red:
|
||||
return
|
||||
elif parent is None:
|
||||
node.is_red = False
|
||||
return
|
||||
|
||||
grandparent = parent.parent
|
||||
if grandparent is None:
|
||||
return
|
||||
|
||||
uncle = grandparent.right if grandparent.left == parent else grandparent.left
|
||||
|
||||
if uncle is not None and uncle.is_red:
|
||||
uncle.is_red = False
|
||||
parent.is_red = False
|
||||
grandparent.is_red = True
|
||||
|
||||
self.rebalance_from_just_inserted(grandparent)
|
||||
return
|
||||
|
||||
if grandparent.left == parent and parent.right == node:
|
||||
node, parent = parent, self.rotate_left(parent)
|
||||
elif grandparent.right == parent and parent.left == node:
|
||||
node, parent = parent, self.rotate_right(parent)
|
||||
|
||||
new_grandparent: Node | None = None
|
||||
if grandparent.left == parent and parent.left == node:
|
||||
new_grandparent = self.rotate_right(grandparent)
|
||||
elif grandparent.right == parent and parent.right == node:
|
||||
new_grandparent = self.rotate_left(grandparent)
|
||||
|
||||
if grandparent == self.root and new_grandparent is not None:
|
||||
self.root = new_grandparent
|
||||
self.root.is_red = False
|
||||
grandparent.is_red = True
|
||||
|
||||
|
||||
def rotate_left(self, 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
|
||||
if new_right_child is not None:
|
||||
new_right_child.parent = node
|
||||
|
||||
new_parent.left = node
|
||||
node.parent = new_parent
|
||||
new_parent.parent = original_parent
|
||||
|
||||
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(self, 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
|
||||
if new_left_child is not None:
|
||||
new_left_child.parent = node
|
||||
|
||||
new_parent.right = node
|
||||
node.parent = new_parent
|
||||
new_parent.parent = original_parent
|
||||
|
||||
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 visualize(self) -> str:
|
||||
# renders the tree top-down with the root on top and branches
|
||||
# ( / and \ ) drawn down to each child. spacing is computed so that
|
||||
# subtrees never overlap, no matter the shape of the tree.
|
||||
if self.root is None:
|
||||
return "<empty tree>"
|
||||
|
||||
# render() returns, for the subtree rooted at `node`:
|
||||
# lines - the block of text drawing the subtree
|
||||
# width - how many characters wide that block is
|
||||
# height - how many lines tall that block is
|
||||
# middle - the column where this node's value is centred (so the
|
||||
# caller knows where to attach its branch)
|
||||
def render(node: Node) -> tuple[list[str], int, int, int]:
|
||||
# label shows the value plus its color: R (red) or B (black)
|
||||
label = f"{node.value}{'R' if node.is_red else 'B'}"
|
||||
label_width = len(label)
|
||||
|
||||
# leaf: just the value on a single line
|
||||
if node.left is None and node.right is None:
|
||||
return [label], label_width, 1, label_width // 2
|
||||
|
||||
# only a left child
|
||||
if node.right is None:
|
||||
lines, width, height, mid = render(node.left)
|
||||
first = (mid + 1) * " " + (width - mid - 1) * "_" + label
|
||||
second = mid * " " + "/" + (width - mid - 1 + label_width) * " "
|
||||
shifted = [line + label_width * " " for line in lines]
|
||||
return [first, second] + shifted, width + label_width, height + 2, width + label_width // 2
|
||||
|
||||
# only a right child
|
||||
if node.left is None:
|
||||
lines, width, height, mid = render(node.right)
|
||||
first = label + mid * "_" + (width - mid) * " "
|
||||
second = (label_width + mid) * " " + "\\" + (width - mid - 1) * " "
|
||||
shifted = [label_width * " " + line for line in lines]
|
||||
return [first, second] + shifted, width + label_width, height + 2, label_width // 2
|
||||
|
||||
# two children: render each side, then place this node between them
|
||||
left_lines, left_w, left_h, left_mid = render(node.left)
|
||||
right_lines, right_w, right_h, right_mid = render(node.right)
|
||||
first = (
|
||||
(left_mid + 1) * " "
|
||||
+ (left_w - left_mid - 1) * "_"
|
||||
+ label
|
||||
+ right_mid * "_"
|
||||
+ (right_w - right_mid) * " "
|
||||
)
|
||||
second = (
|
||||
left_mid * " "
|
||||
+ "/"
|
||||
+ (left_w - left_mid - 1 + label_width + right_mid) * " "
|
||||
+ "\\"
|
||||
+ (right_w - right_mid - 1) * " "
|
||||
)
|
||||
# pad the shorter side so the two blocks line up row-for-row
|
||||
if left_h < right_h:
|
||||
left_lines += [left_w * " "] * (right_h - left_h)
|
||||
elif right_h < left_h:
|
||||
right_lines += [right_w * " "] * (left_h - right_h)
|
||||
merged = [l + label_width * " " + r for l, r in zip(left_lines, right_lines)]
|
||||
return (
|
||||
[first, second] + merged,
|
||||
left_w + right_w + label_width,
|
||||
max(left_h, right_h) + 2,
|
||||
left_w + label_width // 2,
|
||||
)
|
||||
|
||||
lines, _, _, _ = render(self.root)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# =============== TEST CASES ===================
|
||||
#
|
||||
# These run automatically: python red_black_tree.py
|
||||
#
|
||||
# Each case inserts ONE value into a hand-built red-black tree and checks the
|
||||
# result, printing the tree BEFORE, the ACTUAL tree after your rebalance, and
|
||||
# the EXPECTED tree. A case passes when the actual tree matches the expected
|
||||
# tree AND the result is still a valid red-black tree.
|
||||
#
|
||||
# HOW TO ADD YOUR OWN CASE:
|
||||
# Append a Case(...) to the CASES list below with four fields:
|
||||
# name - short description
|
||||
# before - a function returning the tree BEFORE the insert (None = empty)
|
||||
# insert - the value to insert
|
||||
# expected - a function returning the tree you expect AFTERWARD
|
||||
# Build trees with node(value, "R" or "B", left=..., right=...). Any child
|
||||
# you leave out is treated as a (black) nil leaf.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def node(value, color, left=None, right=None):
|
||||
"""Build a Node with an explicit color ("R"/"B"), wiring parent pointers."""
|
||||
n = Node(value, is_red=(color == "R"))
|
||||
n.left = left
|
||||
n.right = right
|
||||
if left is not None:
|
||||
left.parent = n
|
||||
if right is not None:
|
||||
right.parent = n
|
||||
return n
|
||||
|
||||
|
||||
def tree(root):
|
||||
"""Wrap a root Node (or None) in a RedBlackTree."""
|
||||
t = RedBlackTree()
|
||||
t.root = root
|
||||
return t
|
||||
|
||||
|
||||
@dataclass
|
||||
class Case:
|
||||
name: str
|
||||
before: Callable # () -> Node | None
|
||||
insert: int
|
||||
expected: Callable # () -> Node | None
|
||||
|
||||
|
||||
CASES = [
|
||||
Case(
|
||||
"Empty tree -> black root",
|
||||
before=lambda: None,
|
||||
insert=25,
|
||||
expected=lambda: node(25, "B"),
|
||||
),
|
||||
Case(
|
||||
"Black parent -> new red child, no rebalancing",
|
||||
before=lambda: node(25, "B"),
|
||||
insert=15,
|
||||
expected=lambda: node(25, "B", left=node(15, "R")),
|
||||
),
|
||||
Case(
|
||||
"Duplicate insert -> tree unchanged",
|
||||
before=lambda: node(25, "B", left=node(15, "R")),
|
||||
insert=15,
|
||||
expected=lambda: node(25, "B", left=node(15, "R")),
|
||||
),
|
||||
Case(
|
||||
"Red uncle -> recolor (grandparent is the root)",
|
||||
before=lambda: node(25, "B", left=node(15, "R"), right=node(35, "R")),
|
||||
insert=10,
|
||||
expected=lambda: node(
|
||||
25, "B",
|
||||
left=node(15, "B", left=node(10, "R")),
|
||||
right=node(35, "B"),
|
||||
),
|
||||
),
|
||||
Case(
|
||||
"LL -> right-rotate the grandparent",
|
||||
before=lambda: node(30, "B", left=node(20, "R")),
|
||||
insert=10,
|
||||
expected=lambda: node(20, "B", left=node(10, "R"), right=node(30, "R")),
|
||||
),
|
||||
Case(
|
||||
"RR -> left-rotate the grandparent",
|
||||
before=lambda: node(30, "B", right=node(40, "R")),
|
||||
insert=50,
|
||||
expected=lambda: node(40, "B", left=node(30, "R"), right=node(50, "R")),
|
||||
),
|
||||
Case(
|
||||
"LR -> left-rotate parent, then right-rotate grandparent",
|
||||
before=lambda: node(30, "B", left=node(20, "R")),
|
||||
insert=25,
|
||||
expected=lambda: node(25, "B", left=node(20, "R"), right=node(30, "R")),
|
||||
),
|
||||
Case(
|
||||
"RL -> right-rotate parent, then left-rotate grandparent",
|
||||
before=lambda: node(30, "B", right=node(40, "R")),
|
||||
insert=35,
|
||||
expected=lambda: node(35, "B", left=node(30, "R"), right=node(40, "R")),
|
||||
),
|
||||
Case(
|
||||
"Cascade: recolor propagates up, then rotate near the root",
|
||||
before=lambda: node(
|
||||
11, "B",
|
||||
left=node(
|
||||
2, "R",
|
||||
left=node(1, "B"),
|
||||
right=node(7, "B", left=node(5, "R"), right=node(8, "R")),
|
||||
),
|
||||
right=node(14, "B", right=node(15, "R")),
|
||||
),
|
||||
insert=4,
|
||||
expected=lambda: node(
|
||||
7, "B",
|
||||
left=node(
|
||||
2, "R",
|
||||
left=node(1, "B"),
|
||||
right=node(5, "B", left=node(4, "R")),
|
||||
),
|
||||
right=node(
|
||||
11, "R",
|
||||
left=node(8, "B"),
|
||||
right=node(14, "B", right=node(15, "R")),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def validate_rb(t):
|
||||
"""Return a list of red-black property violations ([] means valid)."""
|
||||
problems = []
|
||||
root = t.root
|
||||
if root is None:
|
||||
return problems
|
||||
if root.is_red:
|
||||
problems.append("root is red")
|
||||
if root.parent is not None:
|
||||
problems.append("root has a non-None parent pointer")
|
||||
|
||||
black_heights = set()
|
||||
seen = set()
|
||||
|
||||
def check(n, low, high, black_count):
|
||||
if n is None:
|
||||
black_heights.add(black_count + 1) # nil leaves count as black
|
||||
return
|
||||
if id(n) in seen:
|
||||
# a proper tree never reaches the same node twice
|
||||
problems.append(f"not a tree: node {n.value} reached twice (cycle or shared subtree)")
|
||||
return
|
||||
seen.add(id(n))
|
||||
if low is not None and n.value <= low:
|
||||
problems.append(f"BST order broken at {n.value}")
|
||||
if high is not None and n.value >= high:
|
||||
problems.append(f"BST order broken at {n.value}")
|
||||
if n.is_red and ((n.left and n.left.is_red) or (n.right and n.right.is_red)):
|
||||
problems.append(f"red node {n.value} has a red child")
|
||||
if n.left is not None and n.left.parent is not n:
|
||||
problems.append(f"broken parent pointer: left child {n.left.value} does not point back to {n.value}")
|
||||
if n.right is not None and n.right.parent is not n:
|
||||
problems.append(f"broken parent pointer: right child {n.right.value} does not point back to {n.value}")
|
||||
nb = black_count + (0 if n.is_red else 1)
|
||||
check(n.left, low, n.value, nb)
|
||||
check(n.right, n.value, high, nb)
|
||||
|
||||
check(root, None, None, 0)
|
||||
if len(black_heights) > 1:
|
||||
problems.append(f"unequal black-heights on paths to nil: {sorted(black_heights)}")
|
||||
return problems
|
||||
|
||||
|
||||
def _indent(text: str) -> str:
|
||||
return "\n".join(" " + line for line in text.split("\n"))
|
||||
|
||||
|
||||
def run_tests():
|
||||
passed = 0
|
||||
for i, case in enumerate(CASES, 1):
|
||||
print("=" * 64)
|
||||
print(f"CASE {i}: {case.name}")
|
||||
print("=" * 64)
|
||||
|
||||
t = tree(case.before())
|
||||
print("BEFORE:")
|
||||
print(_indent(t.visualize()))
|
||||
print(f"\n insert({case.insert})\n")
|
||||
|
||||
# Everything the student's code can affect is inside this guard, so a
|
||||
# buggy rebalance (even one that builds a cyclic/broken tree) fails just
|
||||
# this case instead of aborting the whole suite.
|
||||
try:
|
||||
t.insert(case.insert)
|
||||
actual = t.visualize()
|
||||
expected_tree = tree(case.expected())
|
||||
expected = expected_tree.visualize()
|
||||
violations = validate_rb(t)
|
||||
expected_problems = validate_rb(expected_tree)
|
||||
except RecursionError:
|
||||
print(" RESULT: ERROR - hit maximum recursion depth.")
|
||||
print(" Your rebalance likely created a cycle or otherwise broke the")
|
||||
print(" tree structure (a child pointing back up at an ancestor).\n")
|
||||
continue
|
||||
except Exception:
|
||||
print(f" RESULT: ERROR - see stack trace below\n")
|
||||
print(traceback.format_exc())
|
||||
print("")
|
||||
continue
|
||||
|
||||
# Guard the author (you) against a typo when adding a new case: the
|
||||
# 'expected' tree should itself be a valid red-black tree.
|
||||
if expected_problems:
|
||||
print(" RESULT: BAD TEST - the 'expected' tree is not a valid red-black tree:")
|
||||
for p in expected_problems:
|
||||
print(f" - {p}")
|
||||
print()
|
||||
continue
|
||||
|
||||
print("ACTUAL (what your code produced):")
|
||||
print(_indent(actual))
|
||||
print("\nEXPECTED:")
|
||||
print(_indent(expected))
|
||||
print()
|
||||
|
||||
if actual == expected and not violations:
|
||||
print(" RESULT: PASS")
|
||||
passed += 1
|
||||
else:
|
||||
print(" RESULT: FAIL")
|
||||
if actual != expected:
|
||||
print(" - actual tree does not match the expected tree")
|
||||
for v in violations:
|
||||
print(f" - red-black property broken: {v}")
|
||||
print()
|
||||
|
||||
print("=" * 64)
|
||||
print(f"{passed}/{len(CASES)} cases passed")
|
||||
print("=" * 64)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_tests()
|
||||
BIN
notes-and-examples/2026.07.01/rotations.png
Normal file
BIN
notes-and-examples/2026.07.01/rotations.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 571 KiB |
10
notes-and-examples/2026.07.08/overview.md
Normal file
10
notes-and-examples/2026.07.08/overview.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## Outline
|
||||
|
||||
Review the algorithm for a red-black tree rebalancing.
|
||||
|
||||
Next week, we will begin an introduction of graphs.
|
||||
|
||||
## Assignment
|
||||
|
||||
See the `red_black_tree` assignment from last week 2026.07.01.
|
||||
|
||||
70
notes-and-examples/2026.07.15/graphs-intro.md
Normal file
70
notes-and-examples/2026.07.15/graphs-intro.md
Normal file
@@ -0,0 +1,70 @@
|
||||
## Fundamentals
|
||||
|
||||
- A *node* or *vertex* contains a data point
|
||||
- An *edge* is what connects one node to another
|
||||
|
||||
A graph is just a collection of vertices and edges.
|
||||
|
||||
> [!QUESTION] What is a real-life example of a simple graph with only vertices and edges?
|
||||
|
||||
Some additional properties we can put on a graph:
|
||||
|
||||
- A *weight* is a numerical value that can be assigned to an edge
|
||||
- We can also assign a *direction* to an edge, such that it only points from one node to another, not the other way around
|
||||
|
||||
We can, of course, combine both of these properties too.
|
||||
|
||||
> [!QUESTION] What's something we can model with weights in a graph? What about with directional edges?
|
||||
|
||||
Going forward, we'll use $V$ to represent the number of vertices in a graph, and $E$ to represent the number of edges.
|
||||
|
||||
## Representing a graph
|
||||
|
||||
The *adjacency list* stores a list of connected vertices for each node, and it can fit in a dictionary or hash map structure.
|
||||
|
||||
```
|
||||
1 -> 2, 4
|
||||
2 -> 1, 3
|
||||
3 -> 2
|
||||
4 -> 1
|
||||
```
|
||||
|
||||
> [!QUESTION] How would you draw out this graph?
|
||||
|
||||
> [!QUESTION] What would this mapping look like if we wanted to add weights? What about directional edges?
|
||||
|
||||
The *adjacency matrix* is a 2D array where each position `arr[x][y]` represents an edge, and `x` and `y` each represent a node.
|
||||
|
||||
```
|
||||
[
|
||||
[0, 1, 0, 1],
|
||||
[1, 0, 1, 0],
|
||||
[0, 1, 0, 0],
|
||||
[1, 0, 0, 0]
|
||||
]
|
||||
```
|
||||
|
||||
> [!QUESTION] How would you draw out this graph? What would it look like as an adjacency list?
|
||||
|
||||
> [!QUESTION] How do we add weights and/or directional edges to this graph?
|
||||
|
||||
## Categorizing graphs
|
||||
|
||||
We say a graph is *directed and acyclic*, or a *directed acyclic graph (DAG)*, if there are no cycles formed using the directional edges.
|
||||
|
||||
> [!QUESTION] What's something we can model with a DAG?
|
||||
|
||||
> [!QUESTION] What's another data structure we went over that also classifies as a DAG?
|
||||
|
||||
A graph is *dense* if $E$ is closer to $V^2$, and *sparse* if $E$ is closer to $V$.
|
||||
|
||||
> [!QUESTION] What's the implication for the graph if $E$ is closer to $V^2$, i.e. how is it different compared to a sparse graph?
|
||||
|
||||
> [!QUESTION] What's the maximum number of edges we can have in a graph with no self-loops, relative to the number of vertices $V$?
|
||||
|
||||
A graph can contain *self-loops* (a loop from a vertex to itself). A graph can also have *multiple edges* going from one vertex to another.
|
||||
|
||||
> [!QUESTION] How do we represent a self-loop using an adjacency matrix?
|
||||
|
||||
Finally, a graph is *connected* if every vertex is reachable from every other vertex.
|
||||
|
||||
17688
notes-and-examples/2026.07.15/graphs.excalidraw
Normal file
17688
notes-and-examples/2026.07.15/graphs.excalidraw
Normal file
File diff suppressed because it is too large
Load Diff
BIN
notes-and-examples/2026.07.15/graphs.png
Normal file
BIN
notes-and-examples/2026.07.15/graphs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 509 KiB |
18
notes-and-examples/2026.07.15/overview.md
Normal file
18
notes-and-examples/2026.07.15/overview.md
Normal file
@@ -0,0 +1,18 @@
|
||||
## Outline
|
||||
|
||||
- Go over the homework
|
||||
- Go over the basics of graphs, see [Introduction to graphs](./graphs-intro.md)
|
||||
|
||||

|
||||
|
||||
To open the `.excalidraw` file:
|
||||
|
||||
- Launch https://excalidraw.com
|
||||
- Click the top left menu, then select "Open"
|
||||
- Select the `.excalidraw` file from the file picker
|
||||
|
||||
## Assignment
|
||||
|
||||
- Try to complete the red-black implementation from last week
|
||||
- Complete the exercise for the 2D array (see the picture)
|
||||
|
||||
13
notes-and-examples/2026.07.21/breadth-first-search.md
Normal file
13
notes-and-examples/2026.07.21/breadth-first-search.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## Breadth-first search (BFS)
|
||||
|
||||
In the notes on [depth-first search](./depth-first-search.md), we mention that the difference between DFS and BFS is the order in which nodes are traversed.
|
||||
|
||||
Using the same example, what would a breadth-first traversal look like if we start at vertex 0 in this graph?
|
||||
|
||||
```
|
||||
0 --- 1
|
||||
| |
|
||||
3 --- 2
|
||||
```
|
||||
|
||||
Can we also formalize an algorithm for breadth-first search?
|
||||
46
notes-and-examples/2026.07.21/depth-first-search.md
Normal file
46
notes-and-examples/2026.07.21/depth-first-search.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## Traversal introduction
|
||||
|
||||
Many algorithms that involve graphs must involve some way to traverse the elements of a graph. The two simplest ways of traversal are depth-first search (DFS) and [breadth-first search (BFS)](https://en.wikipedia.org/wiki/Breadth-first_search). The major difference here is the *order* in which nodes are traversed.
|
||||
|
||||
If we start from vertex 0 in the tree, in what order would you expect depth-first search to traverse the nodes? (There are multiple correct answers!)
|
||||
|
||||
```
|
||||
0
|
||||
/ \
|
||||
1 2
|
||||
/ \
|
||||
3 4
|
||||
```
|
||||
|
||||
Note that a single traversal step checks for already-visited nodes. So, if the path is `0 -> 1 -> 3`, the path cannot become `0 -> 1 -> 3 -> 1`.
|
||||
|
||||
What about starting from vertex 0 in this graph?
|
||||
|
||||
```
|
||||
0 --- 1
|
||||
| |
|
||||
3 --- 2
|
||||
```
|
||||
|
||||
What about this one?
|
||||
|
||||
```
|
||||
0
|
||||
/ \
|
||||
1 5
|
||||
/ \ \
|
||||
2 3 6
|
||||
\ / /
|
||||
4 7
|
||||
\ /
|
||||
8
|
||||
```
|
||||
|
||||
## Formalizing the algorithm
|
||||
|
||||
Based on these examples, can we create a formal algorithm that takes a starting node, producing a valid traversal path for all three graphs?
|
||||
|
||||
Some starter questions:
|
||||
|
||||
- Could recursion help us here?
|
||||
- What are some ways we can track already-visited nodes? What's the most *time-efficient* way to do so?
|
||||
25
notes-and-examples/2026.07.21/main.py
Normal file
25
notes-and-examples/2026.07.21/main.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Adjacency list
|
||||
first_graph = {
|
||||
1: [3, 20],
|
||||
2: [20],
|
||||
3: [1, 20],
|
||||
20: [2, 3]
|
||||
}
|
||||
|
||||
# First exercise: go from 1, to 20, to 2
|
||||
first_graph[first_graph[1][1]][0]
|
||||
|
||||
# pass visited by reference
|
||||
def depth_first_search(graph: dict, start, visited=set()):
|
||||
print(start)
|
||||
if len(visited)==len(graph):
|
||||
return visited
|
||||
|
||||
for x in graph[start]:
|
||||
if x not in visited:
|
||||
visited.add(x)
|
||||
return depth_first_search(graph, x, visited)
|
||||
pass
|
||||
|
||||
|
||||
depth_first_search(first_graph, 1)
|
||||
11
notes-and-examples/2026.07.21/overview.md
Normal file
11
notes-and-examples/2026.07.21/overview.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## Outline
|
||||
|
||||
To make our knowledge of graphs useful, we'll go over our first traversal method today: [depth-first search](./depth-first-search.md). We will also go over [breadth-first search](./breadth-first-search.md), and how both algorithms can be useful.
|
||||
|
||||
Assignment: write functions for depth-first search and breadth-first search. Because there is no starter file, the constraints are listed below:
|
||||
|
||||
- You may use either an adjacency list or adjacency matrix to represent your graph
|
||||
- You should demonstrate that your algorithm works by running it through some test cases
|
||||
- For each test case, specify the graph, starting point, and expected output(s)
|
||||
|
||||
Bonus: can you output *all* the valid paths for both DFS and BFS in a particular case?
|
||||
Reference in New Issue
Block a user