Compare commits

...

7 Commits

9 changed files with 17966 additions and 13 deletions

View File

@@ -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

View 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.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

View File

@@ -0,0 +1,18 @@
## Outline
- Go over the homework
- Go over the basics of graphs, see [Introduction to graphs](./graphs-intro.md)
![Graphs introduction whiteboard](./graphs.png)
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)

View 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?

View 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?

View 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)

View 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?