Compare commits

...

13 Commits

18 changed files with 18535 additions and 13 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea

View File

@@ -60,8 +60,12 @@ def rotate_left(node: Node) -> Node:
new_right_child = node.right.left new_right_child = node.right.left
node.right = new_right_child node.right = new_right_child
node.parent = new_parent if new_right_child is not None:
new_right_child.parent = node
new_parent.left = node new_parent.left = node
node.parent = new_parent
new_parent.parent = original_parent
if original_parent is not None: if original_parent is not None:
if original_parent.left == node: if original_parent.left == node:
@@ -81,8 +85,12 @@ def rotate_right(node: Node) -> Node:
new_left_child = node.left.right new_left_child = node.left.right
node.left = new_left_child node.left = new_left_child
node.parent = new_parent if new_left_child is not None:
new_left_child.parent = node
new_parent.right = node new_parent.right = node
node.parent = new_parent
new_parent.parent = original_parent
if original_parent is not None: if original_parent is not None:
if original_parent.left == node: if original_parent.left == node:
@@ -115,48 +123,86 @@ def trees_equal(a: Node | None, b: Node | None) -> bool:
if __name__ == "__main__": if __name__ == "__main__":
# note that matching is done by value for simplicity here
def test_rotate_right_example(): def test_rotate_right_example():
# The worksheet example: right rotate on 10. # The worksheet example: right rotate on 10.
root = Node( root = Node(
10, 10,
left=Node(5, left=Node(3, left=Node(2)), right=Node(7)), left=Node(
right=Node(15), 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( expected = Node(
5, 5,
left=Node(3, left=Node(2)), left=Node(3, left=Node(2, parent=Node(3)), parent=Node(5)),
right=Node(10, left=Node(7), right=Node(15)), right=Node(
10,
left=Node(7, parent=Node(10)),
right=Node(15, parent=Node(10)),
parent=Node(5),
),
) )
return rotate_right(root), expected return rotate_right(root), expected
def test_rotate_left_example(): def test_rotate_left_example():
# The worksheet example: left rotate on 16. # The worksheet example: left rotate on 16.
root = Node(16, right=Node(33, left=Node(22), right=Node(55))) root = Node(
expected = Node(33, left=Node(16, right=Node(22)), right=Node(55)) 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 return rotate_left(root), expected
def test_rotate_right_two_nodes(): def test_rotate_right_two_nodes():
# Smallest case: a root with only a left child. # Smallest case: a root with only a left child.
return rotate_right(Node(2, left=Node(1))), Node(1, right=Node(2)) 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(): def test_rotate_left_two_nodes():
# Smallest case: a root with only a right child. # Smallest case: a root with only a right child.
return rotate_left(Node(1, right=Node(2))), Node(2, left=Node(1)) return rotate_left(Node(1, right=Node(2, parent=Node(1)))), Node(2, left=Node(1, parent=Node(2)))
def test_round_trip(): def test_round_trip():
# A left rotate undone by a right rotate restores the original tree. # 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))) original = Node(1, left=Node(0, parent=Node(1)),
expected = Node(1, left=Node(0), right=Node(3, left=Node(2), right=Node(4))) 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 return rotate_right(rotate_left(original)), expected
def test_rotate_with_parent(): def test_rotate_with_parent():
# run it on 33 instead of the root # run it on 33 instead of the root
root = Node(16, right=Node(33, left=Node(22), right=Node(55))) 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 original = root.right
expected = Node(55, left=Node(33, left=Node(22))) 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] return rotate_left(original), expected # pyright: ignore[reportArgumentType]
tests = [ tests = [
("rotate_right on worksheet example (10)", test_rotate_right_example), ("rotate_right on worksheet example (10)", test_rotate_right_example),
("rotate_left on worksheet example (16)", test_rotate_left_example), ("rotate_left on worksheet example (16)", test_rotate_left_example),

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

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

View 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.
![Rotations](rotations.png)
![Red-black trees](red-black-trees.png)
## 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

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

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?