Compare commits
7 Commits
4ca03b5487
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a7ee5d06a0 | |||
| 059c848d3d | |||
| d488ac5575 | |||
| 95544077bc | |||
| 42a3430627 | |||
| c97e452c44 | |||
| 953ce7f2cb |
@@ -1,3 +1,5 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
class Node:
|
class Node:
|
||||||
def __init__(self, value, left=None, right=None, parent=None, is_red=False):
|
def __init__(self, value, left=None, right=None, parent=None, is_red=False):
|
||||||
self.value = value
|
self.value = value
|
||||||
@@ -84,17 +86,95 @@ class RedBlackTree:
|
|||||||
elif root.value < value:
|
elif root.value < value:
|
||||||
self.delete(value, root.right, root)
|
self.delete(value, root.right, root)
|
||||||
|
|
||||||
def rebalance_from_just_inserted(self, node: Node):
|
def rebalance_from_just_inserted(self, node: Node | None):
|
||||||
# This is what you'll be implementing.
|
if node is None:
|
||||||
#
|
return
|
||||||
# By the time this is called, `node` has already been inserted like a
|
|
||||||
# normal BST node, colored red, and had its `parent` pointer set (see
|
parent = node.parent
|
||||||
# insert()). Your job is to restore the red-black properties by
|
if parent is not None and not parent.is_red:
|
||||||
# rebalancing the subtree around `node`.
|
return
|
||||||
#
|
elif parent is None:
|
||||||
# Rebalancing is only needed when node.parent is red. Remember to keep
|
node.is_red = False
|
||||||
# the root black at the end.
|
return
|
||||||
pass
|
|
||||||
|
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:
|
def visualize(self) -> str:
|
||||||
# renders the tree top-down with the root on top and branches
|
# renders the tree top-down with the root on top and branches
|
||||||
@@ -373,8 +453,10 @@ def run_tests():
|
|||||||
print(" Your rebalance likely created a cycle or otherwise broke the")
|
print(" Your rebalance likely created a cycle or otherwise broke the")
|
||||||
print(" tree structure (a child pointing back up at an ancestor).\n")
|
print(" tree structure (a child pointing back up at an ancestor).\n")
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f" RESULT: ERROR - {type(e).__name__}: {e}\n")
|
print(f" RESULT: ERROR - see stack trace below\n")
|
||||||
|
print(traceback.format_exc())
|
||||||
|
print("")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Guard the author (you) against a typo when adding a new case: the
|
# Guard the author (you) against a typo when adding a new case: the
|
||||||
|
|||||||
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