1 of 79

Note

This lecture is the last lecture in scope for Spring 2021 midterm 2.

2 of 79

CS61B, 2021

Lecture 22: Graphs II: Graph Traversal Implementations

  • BreadthFirstPaths
  • Graph API
  • Graph Representations and Graph Algorithm Runtimes
  • Graph Traversal Runtimes
  • Layers of Abstraction

3 of 79

Tree and Graph Traversals

Just as there are many tree traversals:

  • Preorder: DBACFEG
  • Inorder: ABCDEFG
  • Postorder: ACBEGFD
  • Level order: DBFACEG

A

C

B

D

E

F

G

1

2

3

4

5

6

7

8

0

s

So too are there many graph traversals, given some source:

  • DFS Preorder: 012543678 (dfs calls).
  • DFS Postorder: 347685210 (dfs returns).
  • BFS order: Act in order of distance from s.
    • BFS stands for “breadth first search”.
    • Analogous to “level order”. Search is wide, not deep.
    • 0 1 24 53 68 7

4 of 79

Shortest Paths Challenge

Goal: Given the graph above, find the shortest path from s to all other vertices.

  • Give a general algorithm.
  • Hint: You’ll need to somehow visit vertices in BFS order.
  • Hint #2: You’ll need to use some kind of data structure.
  • Hint #3: Don’t use recursion.

0

1

2

3

4

5

6

7

s

5 of 79

BFS Answer

Breadth First Search.

  • Initialize a queue with a starting vertex s and mark that vertex.
    • A queue is a list that has two operations: enqueue (a.k.a. addLast) and dequeue (a.k.a. removeFirst).
    • Let’s call this the queue our fringe.
  • Repeat until queue is empty:
    • Remove vertex v from the front of the queue.
    • For each unmarked neighbor n of v:
      • Mark n.
      • Set edgeTo[n] = v (and/or distTo[n] = distTo[v] + 1).
      • Add n to end of queue.

Demo: Breadth First Paths

A queue is the opposite of a stack. Stack has push (addFirst) and pop (removeFirst).

Do this if you want to track distance value.

6 of 79

One use of BFS: Kevin Bacon

Graph with two types of vertices:

  • Movies
  • Actors

Perform BFS from s=Kevin Bacon.

7 of 79

BreadthFirstSearch for Google Maps

Would breadth first search be a good algorithm for a navigation tool (e.g. Google Maps)?

  • Assume vertices are intersection and edges are roads connecting intersections.

8 of 79

BreadthFirstSearch for Google Maps

Would breadth first search be a good algorithm for a navigation tool (e.g. Google Maps)?

  • Assume vertices are intersection and edges are roads connecting intersections.

Some roads are longer than others.

  • BAD!

Will discuss how to deal with this in the next lecture.

  • First, we should talk about how graphs and graph algorithms are actually implemented in a programming language.

9 of 79

Graph API

10 of 79

Graph Representations

To Implement our graph algorithms like BreadthFirstPaths and DepthFirstPaths, we need:

  • An API (Application Programming Interface) for graphs.
    • For our purposes today, these are our Graph methods, including their signatures and behaviors.
    • Defines how Graph client programmers must think.
  • An underlying data structure to represent our graphs.

Our choices can have profound implications on:

  • Runtime.
  • Memory usage.
  • Difficulty of implementing various graph algorithms.

11 of 79

Graph API Decision #1: Integer Vertices

Common convention: Number nodes irrespective of “label”, and use number throughout the graph implementation. To lookup a vertex by label, you’d need to use a Map<Label, Integer>.

Austin

Dallas

Houston

0

1

2

Intended graph.

How you’d build it.

Map<String, Integer>:

Austin: 0

Dallas: 1

Houston: 2

12 of 79

Graph API

The Graph API from our optional textbook.

Some features:

  • Number of vertices must be specified in advance.
  • Does not support weights (labels) on nodes or edges.
  • Has no method for getting the number of edges for a vertex (i.e. its degree).

public class Graph {

public Graph(int V): Create empty graph with v vertices

public void addEdge(int v, int w): add an edge v-w

Iterable<Integer> adj(int v): vertices adjacent to v

int V(): number of vertices

int E(): number of edges

...

13 of 79

Graph API

The Graph API from our optional textbook.

Example client:

public class Graph {

public Graph(int V): Create empty graph with v vertices

public void addEdge(int v, int w): add an edge v-w

Iterable<Integer> adj(int v): vertices adjacent to v

int V(): number of vertices

int E(): number of edges

...

/** degree of vertex v in graph G */

public static int degree(Graph G, int v) {

int degree = 0;

for (int w : G.adj(v)) {

degree += 1;

}

return degree; }

(degree = # edges)

degree(G, 1) = 2

0

2

1

3

14 of 79

Graph API

The Graph API from our optional textbook.

Challenge: Try to write a client method called print that prints out a graph.

public class Graph {

public Graph(int V): Create empty graph with v vertices

public void addEdge(int v, int w): add an edge v-w

Iterable<Integer> adj(int v): vertices adjacent to v

int V(): number of vertices

int E(): number of edges

...

0

2

1

3

$ java printDemo

1 - 2

1 - 4

2 - 1

2 - 3

3 - 2

4 - 1

public static void print(Graph G) {

???

}

15 of 79

Graph API

The Graph API from our optional textbook.

Print client:

public class Graph {

public Graph(int V): Create empty graph with v vertices

public void addEdge(int v, int w): add an edge v-w

Iterable<Integer> adj(int v): vertices adjacent to v

int V(): number of vertices

int E(): number of edges

...

public static void print(Graph G) {

for (int v = 0; v < G.V(); v += 1) {

for (int w : G.adj(v)) {

System.out.println(v + “-” + w);

}

}

}

0

2

1

3

$ java printDemo

0 - 1

0 - 3

1 - 0

1 - 2

2 - 1

3 - 0

16 of 79

Graph API and DepthFirstPaths

Our choice of Graph API has deep implications on the implementation of DepthFirstPaths, BreadthFirstPaths, print, and other graph “clients”.

  • Will come back to this in more depth, but first...

17 of 79

Graph Representation and Graph Algorithm Runtimes

18 of 79

Graph Representations

To Implement our graph algorithms like BreadthFirstPaths and DepthFirstPaths, we need:

  • An API (Application Programming Interface) for graphs.
    • For our purposes today, these are our Graph methods, including their signatures and behaviors.
    • Defines how Graph client programmers must think.
  • An underlying data structure to represent our graphs.

Our choices can have profound implications on:

  • Runtime.
  • Memory usage.
  • Difficulty of implementing various graph algorithms.

19 of 79

Graph Representations

Just as we saw with trees, there are many possible implementations we could choose for our graphs.

Let’s review briefly some representations we saw for trees.

20 of 79

Tree Representations

We’ve seen many ways to represent the same tree. Example: 1a.

w

x

y

z

1a: Fixed Number of Links (One Per Child)

w

z

x

y

21 of 79

Tree Representations

We’ve seen many ways to represent the same tree. Example: 3.

w

z

x

y

w

x

y

z

Key[] keys

3: Array of Keys

Uses much less memory and operations will tend to be faster.

… but only works for complete trees.

22 of 79

Graph Representations

0

1

2

Graph Representation 1: Adjacency Matrix.

0

1

2

0

0

1

1

1

0

0

1

2

0

0

0

0

1

3

2

0

1

2

3

0

0

1

0

0

1

1

0

1

0

2

0

1

0

1

3

0

0

1

0

For undirected graph: Each edge is represented twice in the matrix. Simplicity at the expense of space.

s

t

v

w

23 of 79

Graph Representations

Graph Representation 1: Adjacency Matrix.

  • G.adj(2) would return an iterator where we can call next() up to two times
    • next() returns 1
    • next() returns 3
  • Total runtime to iterate over all neighbors of v is Θ(V).
    • Underlying code has to iterate through entire array to handle next() and hasNext() calls.

0

1

3

2

0

1

2

3

0

0

1

0

0

1

1

0

1

0

2

0

1

0

1

3

0

0

1

0

v

w

G.adj(2) returns an iterator that will ultimately provide 1, then 3.

24 of 79

Graph Printing Runtime: http://yellkey.com/allow

What is the order of growth of the running time of the print client from before if the graph uses an adjacency-matrix representation, where V is the number of vertices, and E is the total number of edges?

  1. Θ(V)
  2. Θ(V + E)
  3. Θ(V2)
  4. Θ(V*E)

Runtime to iterate over v’s neighbors?

How many vertices do we consider?

0

1

2

3

0

0

1

0

0

1

1

0

1

0

2

0

1

0

1

3

0

0

1

0

0

1

3

2

for (int v = 0; v < G.V(); v += 1) {

for (int w : G.adj(v)) {

System.out.println(v + “-” + w);

}

}

25 of 79

Graph Printing Runtime: http://yellkey.com/allow

What is the order of growth of the running time of the print client from before if the graph uses an adjacency-matrix representation, where V is the number of vertices, and E is the total number of edges?

  • Θ(V)
  • Θ(V + E)
  • Θ(V2)
  • Θ(V*E)

Runtime to iterate over v’s neighbors?

  • Θ(V).

How many vertices do we consider?

  • V times.

0

1

2

3

0

0

1

0

0

1

1

0

1

0

2

0

1

0

1

3

0

0

1

0

0

1

3

2

for (int v = 0; v < G.V(); v += 1) {

for (int w : G.adj(v)) {

System.out.println(v + “-” + w);

}

}

26 of 79

More Graph Representations

Representation 2: Edge Sets: Collection of all edges.

  • Example: HashSet<Edge>, where each Edge is a pair of ints.

0

1

2

{(0, 1), (0, 2), (1, 2)}

27 of 79

More Graph Representations

Representation 3: Adjacency lists.

  • Common approach: Maintain array of lists indexed by vertex number.
  • Most popular approach for representing graphs.

0

1

2

0

1

2

[1, 2]

[2]

28 of 79

Graph Printing Runtime: http://yellkey.com/just

What is the order of growth of the running time of the print client if the graph uses an adjacency-list representation, where V is the number of vertices, and E is the total number of edges?

  • Θ(V)
  • Θ(V + E)
  • Θ(V2)
  • Θ(V*E)

Runtime to iterate over v’s neighbors?

How many vertices do we consider?

a

b

c

0

1

2

[1, 2]

[2]

for (int v = 0; v < G.V(); v += 1) {

for (int w : G.adj(v)) {

System.out.println(v + “-” + w);

}

}

29 of 79

Graph Printing Runtime: http://yellkey.com/just

What is the order of growth of the running time of the print client if the graph uses an adjacency-list representation, where V is the number of vertices, and E is the total number of edges?

  • Θ(V)
  • Θ(V + E)
  • Θ(V2)
  • Θ(V*E)

Runtime to iterate over v’s neighbors? List can be between 1 and V items.

  • Ω(1), O(V).

How many vertices do we consider?

  • V.

a

b

c

0

1

2

[1, 2]

[2]

Best case: Θ(V) Worst case: Θ(V2)

for (int v = 0; v < G.V(); v += 1) {

for (int w : G.adj(v)) {

System.out.println(v + “-” + w);

}

}

30 of 79

Graph Printing Runtime: http://yellkey.com/effort

What is the order of growth of the running time of the print client if the graph uses an adjacency-list representation, where V is the number of vertices, and E is the total number of edges?

  • Θ(V)
  • Θ(V + E)
  • Θ(V2)
  • Θ(V*E)

Runtime to iterate over v’s neighbors? List can be between 0 and V items.

  • Ω(1), O(V).

How many vertices do we consider?

  • V.

a

b

c

0

1

2

[1, 2]

[2]

Best case: Θ(V) Worst case: Θ(V2)

?

?

?

?

?

for (int v = 0; v < G.V(); v += 1) {

for (int w : G.adj(v)) {

System.out.println(v + “-” + w);

}

}

31 of 79

Graph Printing Runtime: http://yellkey.com/effort

What is the order of growth of the running time of the print client if the graph uses an adjacency-list representation, where V is the number of vertices, and E is the total number of edges?

  • Θ(V)
  • Θ(V + E)
  • Θ(V2)
  • Θ(V*E)

�Best case: Θ(V) Worst case: Θ(V2)

All cases: Θ(V + E)

  • Create V iterators.
  • Print E times.

a

b

c

0

1

2

[1, 2]

[2]

for (int v = 0; v < G.V(); v += 1) {

for (int w : G.adj(v)) {

System.out.println(v + “-” + w);

}

}

32 of 79

Graph Printing Runtime

Runtime: Θ(V + E)

How to interpret: No matter what “shape” of increasingly complex graphs we generate, as V and E grow, the runtime will always grow exactly as Θ(V + E).

  • Example shape 1: Very sparse graph where E grows very slowly, e.g. every vertex is connected to its square: 2 - 4, 3 - 9, 4 - 16, 5 - 25, etc.
    • E is Θ(sqrt(V)). Runtime is Θ(V + sqrt(V)), which is just Θ(V).
  • Example shape 2: Very dense graph where E grows very quickly, e.g. every vertex connected to every other.
    • E is Θ(V2). Runtime is Θ(V + V2), which is just Θ(V2).

V is total number of vertices.

E is total number of edges in the entire graph.

for (int v = 0; v < G.V(); v += 1) {

for (int w : G.adj(v)) {

System.out.println(v + “-” + w);

}

}

33 of 79

Graph Representations

Runtime of some basic operations for each representation:

In practice, adjacency lists are most common.

  • Many graph algorithms rely heavily on adj(s).
  • Most graphs are sparse (not many edges in each bucket).

idea

addEdge(s, t)

for(w : adj(v))

print()

hasEdge(s, t)

space used

adjacency matrix

Θ(1)

Θ(V)

Θ(V2)

Θ(1)

Θ(V2)

list of edges

Θ(1)

Θ(E)

Θ(E)

Θ(E)

Θ(E)

adjacency list

Θ(1)

Θ(1) to Θ(V)

Θ(V+E)

Θ(degree(v))

Θ(E+V)

Note: These operations are not part of the Graph class’s API.

34 of 79

Bare-Bones Undirected Graph Implementation

Cannot create array of anything involving generics, so have to use weird cast as with project 1A.

public class Graph {

private final int V; private List<Integer>[] adj;

public Graph(int V) {

this.V = V;

adj = (List<Integer>[]) new ArrayList[V];

for (int v = 0; v < V; v++) {

adj[v] = new ArrayList<Integer>();

}

}

public void addEdge(int v, int w) {

adj[v].add(w); adj[w].add(v);

}

public Iterable<Integer> adj(int v) {

return adj[v];

}

}

35 of 79

Graph Traversal Implementations and Runtime

36 of 79

Depth First Search Implementation

Common design pattern in graph algorithms: Decouple type from processing algorithm.

  • Create a graph object.
  • Pass the graph to a graph-processing method (or constructor) in a client class.
  • Query the client class for information.

public class Paths {

public Paths(Graph G, int s): Find all paths from G

boolean hasPathTo(int v): is there a path from s to v?

Iterable<Integer> pathTo(int v): path from s to v (if any)

}

Paths.java

37 of 79

Example Usage

Start by calling: Paths P = new Paths(G, 0);

  • P.hasPathTo(3); //returns true
  • P.pathTo(3); //returns {0, 1, 4, 3}

Paths.java

public class Paths {

public Paths(Graph G, int s): Find all paths from G

boolean hasPathTo(int v): is there a path from s to v?

Iterable<Integer> pathTo(int v): path from s to v (if any)

}

38 of 79

DepthFirstPaths

Let’s review DepthFirstPaths by running the demo again.

Will then discuss:

  • Implementation.
  • Runtime.�

39 of 79

DepthFirstPaths, Recursive Implementation

public class DepthFirstPaths {

private boolean[] marked;

private int[] edgeTo;

private int s;

public DepthFirstPaths(Graph G, int s) {

...

dfs(G, s);

}

private void dfs(Graph G, int v) {

marked[v] = true;

for (int w : G.adj(v)) {

if (!marked[w]) {

edgeTo[w] = v;

dfs(G, w);

}

}

}

...

}

marked[v] is true iff v connected to s

edgeTo[v] is previous vertex on path from s to v

find vertices connected to s.

not shown: data structure initialization

recursive routine does the work and stores results in an easy to query manner!

Question to ponder: How would we write pathTo(v) and hasPathTo(v)?

  • Answer on next slide.

40 of 79

DepthFirstPaths, Recursive Implementation

public class DepthFirstPaths {

private boolean[] marked;

private int[] edgeTo;

private int s;

...

public Iterable<Integer> pathTo(int v) {

if (!hasPathTo(v)) return null;

List<Integer> path = new ArrayList<>();

for (int x = v; x != s; x = edgeTo[x]) {

path.add(x);

}

path.add(s);

Collections.reverse(path);

return path;

}

public boolean hasPathTo(int v) {

return marked[v];

}

}

marked[v] is true iff v connected to s

edgeTo[v] is previous vertex on path from s to v

41 of 79

Runtime for DepthFirstPaths

Give a O bound for the runtime for the DepthFirstPaths constructor.

Assume graph uses adjacency list!

public class DepthFirstPaths {

private boolean[] marked;

private int[] edgeTo;

private int s;

public DepthFirstPaths(Graph G, int s) {

...

dfs(G, s);

}

private void dfs(Graph G, int v) {

marked[v] = true;

for (int w : G.adj(v)) {

if (!marked[w]) {

edgeTo[w] = v;

dfs(G, w);

}

}

} ...

}

42 of 79

Runtime for DepthFirstPaths

Give a O bound for the runtime for the DepthFirstPaths constructor.

O(V + E)

  • Each vertex is visited at most once (O(V)).
  • Each edge is considered at most twice (O(E)).�

Assume graph uses adjacency list!

public class DepthFirstPaths {

private boolean[] marked;

private int[] edgeTo;

private int s;

public DepthFirstPaths(Graph G, int s) {

...

dfs(G, s);

}

private void dfs(Graph G, int v) {

marked[v] = true;

for (int w : G.adj(v)) {

if (!marked[w]) {

edgeTo[w] = v;

dfs(G, w);

}

}

} ...

}

edge considerations, each constant time

(no more than 2E calls)

vertex visits (no more than V calls)

Cost model in analysis above is the sum of:

  • Number of dfs calls.
  • marked[w] checks.

43 of 79

Various Cost Models for DFS

Four cost models for DFS:

  • Number of DFS calls: O(V)
    • Is an underestimate for families of graphs with tons of edges, e.g. we have genGraph(N) with N vertices and an edge between all pairs of vertices. Runtime would be worse than V.
  • Number of DFS + marked checks: O(V + E), but can simplify to O(E)
    • Also underestimates for families of graphs with very few edges.
  • Number of marked checks: O(E)
    • Is an underestimate for families of graphs with very few edges. So imagine, we have genGraph(N) which generates a graph with N nodes and sqrt(N) edges. As N grows, runtime would be worse than E.
  • Initialization of marked to false + marked checks: O(V + E)
    • This one does not underestimate.

We never formally defined asymptotics on multiple variables, and it turns out to be somewhat poorly defined.

44 of 79

Runtime for DepthFirstPaths

Very hard question: Could we say the runtime is O(E)?

Argument: Can only visit a vertex if there is an edge to it.

  • # of DFS calls is bounded above by E.
  • So why not just say O(E)?

Assume graph uses adjacency list!

public class DepthFirstPaths {

private boolean[] marked;

private int[] edgeTo;

private int s;

public DepthFirstPaths(Graph G, int s) {

...

dfs(G, s);

}

private void dfs(Graph G, int v) {

marked[v] = true;

for (int w : G.adj(v)) {

if (!marked[w]) {

edgeTo[w] = v;

dfs(G, w);

}

}

} ...

}

45 of 79

Runtime for DepthFirstPaths

Very hard question: Could we say the runtime is O(E)? No.

Can’t say O(E)!

  • Constructor has to create an all false marked array.
  • This marking of all vertices as false takes Θ(V) time.�

Our cost model earlier (dfs calls + marked checks) does not provide a tight bound.

Assume graph uses adjacency list!

public class DepthFirstPaths {

private boolean[] marked;

private int[] edgeTo;

private int s;

public DepthFirstPaths(Graph G, int s) {

...

dfs(G, s);

}

private void dfs(Graph G, int v) {

marked[v] = true;

for (int w : G.adj(v)) {

if (!marked[w]) {

edgeTo[w] = v;

dfs(G, w);

}

}

} ...

}

46 of 79

Graph Problems

Runtime is O(V+E)

  • Based on cost model: O(V) dfs calls and O(E) marked[w] checks.
  • Can’t say O(E) because creating marked array
  • Note, can’t say Θ(V+E), example: Graph with no edges touching source.

Space is Θ(V).

  • Need arrays of length V to store information.

Problem

Problem Description

Solution

Efficiency (adj. list)

s-t paths

Find a path from s to every reachable vertex.

DepthFirstPaths.java

Demo [update]

O(V+E) time

Θ(V) space

47 of 79

BreadthFirstPaths Implementation

marked[v] is true iff v connected to s

edgeTo[v] is previous vertex on path from s to v

set up starting vertex

for freshly dequeued vertex v, for each neighbor that is unmarked:

  • Enqueue that neighbor to the fringe.
  • Mark it.
  • Set its edgeTo to v.

public class BreadthFirstPaths {

private boolean[] marked;

private int[] edgeTo;

...

private void bfs(Graph G, int s) {

Queue<Integer> fringe =

new Queue<Integer>();

fringe.enqueue(s);

marked[s] = true;

while (!fringe.isEmpty()) {

int v = fringe.dequeue();

for (int w : G.adj(v)) {

if (!marked[w]) {

fringe.enqueue(w);

marked[w] = true;

edgeTo[w] = v;

}

}

}

}

48 of 79

Graph Problems

Runtime for shortest paths is also O(V+E)

  • Based on same cost model: O(V) .next() calls and O(E) marked[w] checks.

Space is Θ(V).

  • Need arrays of length V to store information.

Problem

Problem Description

Solution

Efficiency (adj. list)

s-t paths

Find a path from s to every reachable vertex.

DepthFirstPaths.java

Demo

O(V+E) time

Θ(V) space

s-t shortest paths

Find a shortest path from s to every reachable vertex.

BreadthFirstPaths.java

Demo

O(V+E) time

Θ(V) space

49 of 79

Layers of Abstraction

50 of 79

Clients and Our Graph API

Our choice of Graph API has deep implications on the implementation of DepthFirstPaths, BreadthFirstPaths, print, and other graph “clients”.

51 of 79

Our Graph API and Implementation

Our choice of how to implement the Graph API has profound implications on runtime.

  • Example: Saw that DepthFirstPaths on Adjacency Lists was O(V + E).

52 of 79

Our Graph API and Implementation

Our choice of how to implement the Graph API has profound implications on runtime.

  • What happens if to DepthFirstPaths runtime if we use an adjacency matrix?

53 of 79

Runtime for DepthFirstPaths

Give a tight O bound for the runtime for the DepthFirstPaths constructor.

Assume graph uses adjacency matrix!

public class DepthFirstPaths {

private boolean[] marked;

private int[] edgeTo;

private int s;

public DepthFirstPaths(Graph G, int s) {

...

dfs(G, s);

}

private void dfs(Graph G, int v) {

marked[v] = true;

for (int w : G.adj(v)) {

if (!marked[w]) {

edgeTo[w] = v;

dfs(G, w);

}

}

} ...

}

54 of 79

Runtime for DepthFirstPaths

Give a tight O bound for the runtime for the DepthFirstPaths constructor.

O(V2)

  • In the worst case, we iterate over the neighbors of all vertices.

Assume graph uses adjacency matrix!

public class DepthFirstPaths {

private boolean[] marked;

private int[] edgeTo;

private int s;

public DepthFirstPaths(Graph G, int s) {

...

dfs(G, s);

}

private void dfs(Graph G, int v) {

marked[v] = true;

for (int w : G.adj(v)) {

if (!marked[w]) {

edgeTo[w] = v;

dfs(G, w);

}

}

} ...

}

We create ≤ V iterators.

  • Each one takes a total of Θ(V) time to iterate over.

Essentially, iterating over the entire adjacency matrix takes O(V2) time.

55 of 79

Graph Problems for Adjacency Matrix Based Graphs

If we use an adjacency matrix, BFS and DFS become O(V2).

  • For sparse graphs (number of edges << V for most vertices), this is terrible runtime.
  • Thus, we’ll always use adjacency-list unless otherwise stated.

Problem

Problem Description

Solution

Efficiency (adj. matrix)

s-t paths

Find a path from s to every reachable vertex.

DepthFirstPaths.java

Demo

O(V2) time

Θ(V) space

s-t shortest paths

Find a shortest path from s to every reachable vertex.

BreadthFirstPaths.java

Demo

O(V2) time

Θ(V) space

56 of 79

Summary

57 of 79

Summary

BFS: Uses a queue instead of recursion to track what work needs to be done.

Graph API: We used the Princeton algorithms book API today.

  • This is just one possible API. We’ll see other APIs in this class.
  • Choice of API determines how client needs to think in order to write code.
    • e.g. Getting the degree of a vertex requires many lines of code with this choice of API.
    • Choice may also affect runtime and memory of client programs.

public class Graph {

public Graph(int V): Create empty graph with v vertices

public void addEdge(int v, int w): add an edge v-w

Iterable<Integer> adj(int v): vertices adjacent to v

int V(): number of vertices

int E(): number of edges ...

58 of 79

Summary

Graph Implementations: Saw three ways to implement our graph API.

  • Adjacency matrix.
  • List of edges.
  • Adjacency list (most common in practice).

Choice of implementation has big impact on runtime and memory usage!

  • DFS and BFS runtime with adjacency list: O(V + E)
  • DFS and BFS runtime with adjacency matrix: O(V2)

59 of 79

Citations

60 of 79

Live Lecture Exercise

61 of 79

Switching Things Up

Breadth First Search.

  • Initialize a queue with a starting vertex s and mark that vertex.
    • A queue is a list that has two operations: enqueue (a.k.a. addLast) and dequeue (a.k.a. removeFirst).
    • Let’s call this the queue our fringe.
  • Repeat until queue is empty:
    • Remove vertex v from the front of the queue.
    • For each unmarked neighbor n of v:
      • Mark n.
      • Set edgeTo[n] = v (and/or distTo[n] = distTo[v] + 1).
      • Add n to end of queue.

Demo: Breadth First Paths

Do this if you want to track distance value.

62 of 79

Switching Things Up

??? First Search.

  • Initialize a queuestack with a starting vertex s and mark that vertex.
    • A stack is a list that has two operations: push (a.k.a. addLast) and pop (a.k.a. removeLast).
    • Let’s call this the queuestack our fringe.
  • Repeat until queue is empty:
    • Pop vertex v from the top of the stack.
    • For each unmarked neighbor n of v:
      • Mark n.
      • Set edgeTo[n] = v (and/or distTo[n] = distTo[v] + 1).
      • Push n to the top of of the queuestack.

63 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order: []

0

64 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order: []

0

65 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order: [0]

66 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order: [0]

1

67 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order: [0, 1]

68 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order: [0, 1]

2

4

69 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order:

[0, 1, 4]

2

3

5

70 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order:

[0, 1, 4, 5]

2

3

71 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order:

[0, 1, 4, 5]

2

3

6

8

72 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order:

[0, 1, 4, 5, 8]

2

3

6

*

73 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order:

[0, 1, 4, 5, 8, 6]

2

3

7

74 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order:

[0, 1, 4, 5, 8, 6, 7]

2

3

75 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order:

[0, 1, 4, 5, 8, 6, 7, 3]

2

76 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order:

[0, 1, 4, 5, 8, 6, 7, 3, 2]

77 of 79

Try running ??? first search. Assume adjacency lists are in numerical order.

  • Dives down a rabbit hole. Then makes its way back.
    • Depth first!!

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order:

[0, 1, 4, 5, 8, 6, 7, 3, 2]

DFS (Pre-order):

[0, 1, 2, 5, 4, 3, 6, 7, 8]

78 of 79

Try running R???R first search, which uses reverse order of adjacency lists.

  • In other words, 5’s adjacency list is: 2->4->6->8, so let’s do it backwards
  • EVEN THIS IS NOT classic DFS

1

2

3

4

5

6

7

8

0

s

Stack:

??? First Search Visit Order:

[0, 1, 4, 5, 8, 6, 7, 3, 2]

DFS (Pre-order):

[0, 1, 2, 5, 4, 3, 6, 7, 8]

R???R First Search Visit Order:

[0, 1, 2, 5, 6 RRRHRHH (buzzer sound)]

79 of 79

Interesting fact:

  • ANY recursive algorithm can be implemented using iteration and a stack.
  • So DFS (which we implemented recursively) can be implemented using a stack data structure.
    • So far we’ve uncovered ??? first search and R???R first search.
    • These two algorithms are close cousins of DFS-breaking-ties-alphabetically search, but are NOT quite the same.
    • R???R is a closer cousin.
    • Tweaking the algorithm so that our iterative algorithm yields the exact same output as our recursive one will require more work.

In 61C, you’ll learn how recursive calls are implemented at a low level using a stack, i.e. in REAL recursive code, there is an explicit stack being utilized (but below the level of abstraction that you usually think about).�