ECE250 Notes
ECE250 Notes
Asymptotic Analysis
• Ignore machine-dependent constants
• Analyze growth of T(n) as n approaches ∞
o Drop lower order terms and ignore leading constants
Asymptotic Notation
O-notation: Upper Bound
There exists positive constants 𝑐 and 𝑛0 such that 0 ≤ 𝑓(𝑛) ≤ 𝑐𝑔(𝑛) for all 𝑛 ≥ 𝑛0
1. 2𝑛2 = O(𝑛3 )
2. √𝑛 = Ω(log 𝑛)
1
3. 2 𝑛2 − 3𝑛 = Θ(𝑛2 )
Solving Recurrences
Recurrence: an equation/inequality that describes a function in terms of its own value on smaller inputs
Repeated Substitution
• Substitute for n until you find a pattern
• Write a formula in terms of n and i (the number of substitutions)
• Choose i so that it references the base case
• Solve the resulting summation
Master Theorem
Can be applied to recurrences in the form:
𝑛
𝑇(𝑛) = 𝑎𝑇 ( ) + 𝑓(𝑛) where 𝑎 ≥ 1, 𝑏 > 1 and 𝑓(𝑛) is asymptotically positive
𝑏
Case 1
𝑓(𝑛) = O(𝑛log𝑏 𝑎−𝜀 ) for some 𝜀 > 0
Case 2
𝑓(𝑛) = Θ(𝑛log𝑏 𝑎 )
Case 3
𝑛
𝑓(𝑛) = Ω(𝑛log𝑏 𝑎+𝜀 ) for some 𝜀 > 0 and satisfies regularity condition 𝑎𝑓 (𝑏 ) ≤ 𝑐𝑓(𝑛) for some 𝑐 < 1
Stacks
• Objects are inserted/removed according to the last-in-first-out (LIFO) principle
• Array implementation with internal array S of size N, and pointer t to top of stack:
o All methods run in O(1) time
o push(x) – inserts x on top of S
if size() = N
return Error
t=t+1
S[t] = x
o pop() – removes top object from S
if size() = N
return Error
top = S[t]
S[t] = null
t = t-1
return top
o top() – returns top object of S
if isEmpty()
return Error
return S[t]
o size() – returns current number of objects in stack
return t+1
Queues
• Objects are inserted/removed according to the first-in-first-out (FIFO) principle
• Array implementation with internal array Q of size N, and pointers f, r for the front and rear
elements:
o All methods run in O(1) time
o enqueue(x) – inserts x at back of Q
if size() = N
return Error
Q[r] = x
r = (r+1) mod N
o dequeue() – removes front object of Q
front = Q[f]
Q[f] = null
f = (f+1) mod N
return front
o size() – returns current number of objects in Q
return (N-f+r) mod N
o front() – returns object at the front of Q
if isEmpty()
return Error
return Q[f]
Hash Tables
Hash function: used to map a set of unique keys into an array
Collision Resolution
Chaining
• Array of links (a table) indexed by keys, to lists of items with the same key
• Simple Uniform Hashing: Assume each key is equally likely to be hashed into any slot of the
table, independent of where other keys are hashed
• Load factor: Given a hash table with 𝑚 slots holding 𝑛 elements, the load factor is 𝑛/𝑚 – the
average number of keys per slot
• Search, insertion, and deletion all take O(1) time when lists are doubly linked
Open Addressing
• All elements are stored in the hash table
• Probe the table until an empty slot is found
• Hash function takes in probe number
Linear Probing
• If the current location is filled, try the next location
o h(k,i) = (h(k) + i) mod m
• Uses less memory than chaining – don’t need to store links to lists
• Slower than chaining – primary clustering means may need to walk along table for a long time
Double Hashing
• Use two hash functions
o h(k,i) = (h1(k) + i*h2(k)) mod m
• Distributes keys more evenly than linear probing
• h2(k) must always be co-prime to m to guarantee that probe sequences will return all slots
from 0 to m-1
o If m is prime, have 1 < h2(k) < m
o Let m = 2j and have h2(k) always return an odd number
Hash Function
• A good hash function should distribute keys uniformly and compute quickly
• Division method
o h(k) = k mod m
o k = key, m = array size
o Choose m to be prime to help ensure even distribution
• Multiplication method
o h(k) = floor( m * (k*A) mod 1 )
o k = key, m = array size, 0 < A < 1 (a constant)
o Typically, choose m = 2j
Trees
A collection of nodes
• Parent/Child: every node except the root has one parent; a node can have an arbitrary number
of children
• Leaves: nodes with no children
• Siblings: nodes with the same parent
Terminology
• Path: a sequence of nodes where each node is the parent of its successive node
• Length: the number of edges in a path
• Depth of a node: the length of the path from root to the node
o Depth of tree = depth of deepest leaf
• Height of a node: length of the longest path from a node to a leaf
o All leaves have height 0
o Height of tree = height of root
Binary Tree
A tree where each node doesn’t have more than two children
ADT
Accessors Mutators
• key() • setKey()
• parent() • setParent()
• left() • setLeft()
• right() • setRight()
Tree Traversal
• Pre-order traversal: Node, Left, Right
• In-order traversal: Left, Node, Right
• Post-order traversal: Left, Right, Node
Binary Search Trees
A binary tree where each key in the left subtree of a node is smaller than the node’s key, and each key in
the right subtree is larger than the node’s key
Searching
Iterative
Search(T, k)
x = T
while (x != null)
if (x.key() == k) return x
if (x.key() < k) x = x.right()
else x = x.left()
return null
Recursive
Search(T, k)
if (T == null) return null
if (T.key() == k) return T
if (T.key() < k) return Search(T.right(), k)
return Search(T.left(), k)
Minimum/Maximum
TreeMin(x)
while x.left() != null
x = x.left()
return x
TreeMax(x)
while x.right() != null
x = x.right()
return x
Successor/Predecessor
Case 1: right subtree of x is non-empty
p = x.parent()
while (p != null && p.right() == x)
x = p
p = p.parent()
return p
Insertion
• Find where z belongs
• Insert z by setting parent and left/right pointers
TreeInsert(T, z)
x = T
y = null
while (x != null)
y = x
if (x.key() < z.key()) x = x.right()
else x = x.left()
z.setParent(y)
if (y != null)
if (y.key() < z.key()) y.setRight(z)
else y.setLeft(z)
else T = z
InOrderTreeWalk(T)
if (T == null) return
InOrderTreeWalk(T.left())
print T.key()
InOrderTreeWalk(T.right())
Deletion
Case 1: No children
• Remove x
AVL Trees
Balanced BSTs
• The height of a binary tree is at least Θ(log n)
o Worst-case execution time of insertion and deletion in BST is Θ(n)
o Balanced binary search trees guarantee small height
• AVL trees are BSTs where for every node, the height of the left and right subtrees differ by at
most 1
o Height of subtree = maximum number of edges to a leaf
o Height of an empty subtree = -1
o Height of one node = 0
B-Tree Definitions
• Every leaf has the same depth – the tree’s height h
• In a B-Tree of degree t:
o Every node except the root has at least t-1 keys; thus, every node except the root has at
least t children
o Every node contains at most 2t-1 keys; therefore every node has at most 2t children
o The root node has between 0 and 2t-1 keys (0 and 2t children)
• For a B-tree of height h, containing n (>= 1) keys and degree t (>= 2):
o 𝑛 ≥ 2𝑡 ℎ − 1
𝑛+1
o ℎ ≤ log 𝑡 2
• Searching: typical binary search tree search algorithm, with added disk reading
• Creating an empty tree: create a root and write it to the disk
Splitting Nodes
• When nodes reach their maximum capacity 2t-1, each full node must be split before insertion of
new keys
• One (middle) key of node moves up to the parent, leaving two nodes each with t-1 keys
• Insertion is only done in leaf nodes
• Splitting the root node requires creating a new node – tree grows at the top instead of bottom
• Running time: 𝑂(𝑡)
Inserting Keys
• Starting from root, recursively traverse down the tree to leaf level
• Ensure that every node has < 2t-1 keys, splitting when necessary
• Insert key into appropriate node
• Running time:
o Disk I/O: 𝑂(ℎ)
o CPU: 𝑂(𝑡ℎ) = 𝑂(𝑡 log 𝑡 𝑛)
Deleting Keys
• Starting from root, recursively traverse down the tree to leaf level
• Ensure that every node has >= t keys
• Three cases (k = key to delete):
o Case 1: k in leaf node x and x has >= t keys
▪ Just delete k from x
o Case 2: k in internal node x
▪ If the child preceding k has >= t keys:
• Find the predecessor of k, j
• Recursively delete j
• Replace k with j in x
▪ If both children preceding/succeeding k have t-1 keys:
• Merge the two children and delete k from x
o Case 3: k suspected in lower level node ci[x]
▪ Distribution: If ci[x] only has t-1 leys but one of its immediate siblings has >= t
keys, move a key from x to ci[x], and move a key from ci[x]’s immediate sibling
up to x
▪ Merging: If ci[x] and its immediate siblings only have t-1 leys, merge ci[x] with
one sibling by moving a key from x down to the merged node, as the median key
of that node
• Running time:
o Disk I/O: 𝑂(ℎ)
o CPU: 𝑂(𝑡ℎ) = 𝑂(𝑡 log 𝑡 𝑛)
• Parent(i) = floor(i/2)
• Left(i) = 2i
• Right(i) = 2i + 1
• A[Parent(i)] >= A[i]
Heapify
• Heapify(A, i) makes A into a heap by moving A[i] down the heap until the heap property
A[Parent(i)] >= A[i] is satisfied again
o Compares A[i] with both of its children (e.g. A[j] and A[k])
o If a larger child is found (e.g. A[j]), swap A[i] and A[j]
o Call Heapify(A, j)
• Running time: O(log n)
Building a Heap
• Given an array A[1 … n], convert it into a heap
• The elements in the second half of the array are leaf nodes, which are 1-element heaps
• Hence, only need to call Heapify(A, i) on i = floor(n/2) down to i = 1
• Running time: O(n)
Heapsort
Running time: O(n) + O(n log n) = O(n log n)
Heapsort(A):
BuildHeap(A);
Heapify(A, 1);
}
Priority Queue
• A data structure for maintaining a set of elements that each have an associated key
• A max heap can be used to implement a max PQ with the following operations:
o Insert(S, x): insert element x into set S
▪ Add empty spot in PQ
▪ Traverse up the parents while the parent’s key < x’s key
▪ Insert x in its position
▪ Running time: O(log n)
Quicksort
• In-place sort – doesn’t require additional array
• Divide and conquer algorithm:
o Partition array into 2 subarrays, such that elements in the lower part <= elements in the
higher part
▪ Select a pivot element x around which to partition
▪ One variable i starts from beginning of array, the other j starts from end
• Increment i until you find an element >= x
• Decrement j until you find an element <= x
• If i < j, swap elements and continue incrementing/decrementing
• Otherwise, you are done, so return j
o Recursively sort the 2 subarrays
o Combine the sorted subarrays (trivial, since already sorted)
• Running time: depends on the distribution of partitions
o Average (balanced) case: O(n log n)
o Worst case: O(n2) - input is already sorted or reverse sorted
CountingSort(A, k)
C = new int[k]; // aux array; k = max number
B = new int[n]; // output array; n = A.length
Radix Sort
• Sorts each input from least significant to most significant digit with a stable sort
• Given n d-digit numbers where each digit takes up to k possible values, radix sort correctly sorts
these number in O(d(n + k)) time if the stable sort that it uses takes O(n + k) time
• Given n b-digit numbers and positive integers r <= b, radix correctly sorts these numbers in
O((b/r)(n + 2r)) time if the stable sort that it uses takes O(n + k) time
o If we’re sorting n words with b bits each, each word can be seen having b/r base-2r digits
o Example: 32-bit word
▪ If r = 8, b/r = 4 passes of counting sort on base-28 digits
▪ If r = 16, b/r = 2 passes of counting sort on base-216 digits
o Want to choose r to minimize total running time
▪ Choosing r = log n implies T = O(bn/log n)
▪ For numbers in the range 0 to nd – 1, b = d log n so radix sort runs in O(dn) time
• Is radix sort preferable to comparison sorts?
o If b [number of digits] = O(log n [number of elements]), then if we choose r as close as
possible to log n, radix sort’s running time will be O(n), better than comparison sorts
Dynamic Programming
• Remember solution to solved sub-problems; build complete solution bottom-up
Optimization Problems
• Choose most optimal solution of many – one with the max/min value
• Solution exhibits a structure – a string of the most optimal choices
Steps:
1. Show optimal substructure – an optimal solution to the problem contains optimal solutions to
sub-problems
2. Write recurrence for the value of an optimal solution
3. Compute the value of an optimal solution in a bottom-up fashion, so that necessary sub-results
are always computed
4. Construct optimal solution from computed information (make sequence of choices leading to
optimal solution)
Step 2: Recurrence
LCS(Xi, Yj) =
• 0 if i = 0 or j = 0
• LCS(Xi-1, Yj-1) + 1 if i, j > 0 and xi = yj
• max { LCS(Xi, Yj-1), LCS(Xi-1, Yj) } if i, j > 0 and xi != yj
Adjacency List
• For a vertex v, a sequence of vertices adjacent to v
• Space required: O(V + sum of degree(V)) = O(V + E)
Adjacency Matrix
• Matrix M with entries for all pair of vertices
o M[i, j] = true – there is an edge (i, j) in the graph
o M[i, j] = false – there isn’t an edge (i, j) in the graph
• Space = O(V2)
Breadth-First Search
• Traverse a connected component of a graph, defining a spanning tree
• Looks at all vertices on the same level before going to the next
o Initialize all vertices with distance = infinity, color = white
o The starting vertex s is assigned distance = 0, color = gray, and added to the queue
o While the queue isn’t empty:
▪ Increment distance
▪ Get front of queue u
▪ For each of u’s adjacent vertices, if its color = white, set distance, color = gray,
and enqueue it
▪ Dequeue u and set its color to black
o At the end, the distance of a vertex v corresponds to the length of the shortest path
from s to v
• Running time: O(V + E)
o Discovers all reachable vertices from starting vertex s
o Computes shortest distance from s to each vertex
o Computes a breadth-first tree containing all reachable vertices
Depth-First Search
• Looks at all the descendants of a vertex before backtracking to other vertices on the same level
o Initialize all vertices with distance = infinity, color = white
o For each vertex u in the graph’s vertices, if u’s color = white, call DFSVisit(u)
▪ Increment distance
▪ Set u’s distance, color = gray
▪ For each vertex v adjacent to u, if v’s color = white, call DFSVisit(v)
▪ Set u’s color = black
• Running time: O(V + E)
DFS Edge Classifications
• Gray -> white = Tree edge: edge from ancestor -> descendant in a depth-first forest
• Gray -> gray = Back edge: descendant -> ancestor in depth-first tree
• Gray -> black = Forward edge: non-tree edge from ancestor -> descendant in depth-first tree
Cross edge: edges between vertices of same depth, between trees or subtrees
MST/Prim’s Algorithm
• Spanning tree: a subgraph of a graph G that is a tree and contains all vertices of G
o For a graph with V vertices, there are V-1 edges in its spanning tree
• For a graph G with weight function w that assigns cost to all edges, the minimum spanning tree
is a spanning tree that minimizes the sum of w(u, v) for all edges
o Must make V-1 choices to arrive at optimization goal (one choice per edge)
o After selecting each edge, the problem becomes a sub-problem with one less vertex
Growing an MST
• Build a set of edges, A while maintaining a set S of vertices in A
• Each edge (u, v) added to A must be a “safe edge” that maintains A as a subset of some MST
o Add u and v to S
o Look for light edge – edge connecting a vertex in S to a vertex in V – S with minimum
weight
Prim-Jarnik Algorithm
• Vertex-based algorithm – grows a tree T, one vertex at a time
• Set A covers the portion of T already computed
• For each vertex v outside of A, set v.key() to the minimum weight of an edge connecting v to an
edge in A (v.key() = infinity if no edge exists)
• Running time: O(V * T(extractMin) + E * T(modifyKey))
o If Q is an array, O(V * O(V) + E * O(1)) = O(V2)
o If Q is a max heap, O(V * O(log V) + E * O(log V)) = O(E log V)
PrimsMST(G, s)
for each (Vertex v in G.V()) {
v.setKey(infinity);
v.setParent(null);
}
s.setKey(0);
Q.init(G.V());
while (!Q.empty()) {
u = Q.extractMin();
for each (Vertex v in u.adjacent()) {
if (v in Q and G.w(u, v) < v.key()) {
v.setKey(G.w(u, v));
Q.modifyKey(v);
v.setParent(u);
}
}
}
Kruskal’s Algorithm
• Edge-based algorithm – adds edges one at a time in increasing weight order
• Maintains a forest of trees (a set of sets) S, and a set A covering edges in the MST
• Sort edges by weight
• Add minimum weight safe edge (u, v) to A, and merge sets containing u and v; an edge is “safe”
if it connects vertices of distinct trees (within separate sets in S)
• Running time: O(E log V)
KruskalsMST(G)
A = null
S.init()
Dijkstra’s Algorithm
• Like BFS, but considers edge weights
• Can’t handle negative weight edges
• Basic idea:
o Maintain set S of solved vertices
o At each step, look for closest vertex u, add u to S, and relax all edges from u
• Running time: O(V * T(extractMin) + E*T(modifyKey)) just like Prim’s
o If Q is an array: O(V2)
o If Q is a max heap: O(E log V)
Dijkstra(G, s)
for each (Vertex v in G.V()) {
v.setd(infinity);
v.setParent(null);
}
s.setd(0);
Q.init(G.V());
while (!Q.empty()) {
u = Q.extractMin();
for each (Vertex v in u.adjacent()) {
Relax(u, v, G);
Q.modifyKey(v);
}
}
Relax(u, v, G)
if (v.d() > u.d() + G.w(u, v)) {
v.setd(u.d() + G.w(u, v));
v.setParent(u);
}
BellmanFord(G, s)
for each (Vertex v in G.V()) {
v.setd(infinity);
v.setParent(null);
}
s.setd(0);
for (i = 1 to i = G.V().length - 1) {
for each (Edge (u, v) in G.E()) {
Relax(u, v, G);
}
}
Topological Sort
• A linear ordering of the vertices in a DAG, such that for any edge (u, v), u appears before v in the
ordering
• Vertices are arranged left to right in the order of decreasing finishing time
o Call DFS to compute finishing time for each vertex
o When each vertex is completed, insert it into the front of a linked list
o Return the linked list
• Running time: O(V + E)
DAGShortestPath(G, s)
for each (Vertex v in G.V()) {
v.setd(infinity);
v.setParent(null);
}
s.setd(0);
topologically sort G
for each (Vertex u in topological order) {
for each (Vertex v in u.adjacent()) {
Relax(u, v, G);
}
}
Floyd-Warshall Algorithm
• A dynamic programming algorithm
• Considers the intermediate vertices of a path
• Sub-problem: find the shortest path through a subset of vertices
• Running time: O(n3)
For a directed weighted graph, let W be the adjacency matrix, where each edge (u, v) is either its weight
if it exists, or infinity if it doesn’t
• dij(n) in D(n) stores the length of the shortest path between two vertices i and j
• πij(n) in Π (n) stores the predecessor (vertex before j in the shortest path from i to j)
FloydWarshall(W)
n = W.rows().length; // number of vertices
D(0) = W;
Π(0) = { null if i = j or dij(0) = ∞
{ i if i != j and dij(0) != ∞
return D(n);
NP Completeness
Towers of Hanoi
• Recurrence: T(n) = 2T(n-1) + 1
• Running time: O(2n) exponential
Decision Problems
• Optimization problems can generally be transformed into true/false decision problems
o For an optimization problem O, with a related decision problem D, if we show that D is
hard to solve, O must also be hard to solve
P Complexity Class
• Set of all decision problems that can be solved in polynomial time on real computers
• Examples include: shortest path, relative prime, LCS
NP-Hard
• A problem that, if it can be solved efficiently, can be used (as a subroutine) to solve any other
NP problem efficiently
NP-Complete
• NP problems that are NP-hard – if solved efficiently, can solve any other NP problem efficiently
o Finding a polynomial time algorithm for one NP-complete problem would automatically
yield a polynomial time algorithm for all NP problems
o Proving that one NP-complete problem has an exponential lower bound would
automatically prove that all NP-complete problems have exponential lower bounds
A language L1 is PT reducible to a language L2, i.e. L1 <p= L2, if there exists a PT computable function f(x)
over a range {0, 1}* such that for all x in {0, 1}*, x exists in L1 if and only if f(x) exists in L2
NP-Completeness
• PT reduction shows that one problem is at least as hard as another, within a PT factor
o If L1 <p= L2, then L1 is no more than a polynomial factor harder than L2
• A language L, subset of {0, 1}* is NP-complete if:
1. L exists in NP
2. L’ <p= L for every L’ that exists in NP
• A language L is NP-hard if 2 is satisfied but not necessarily 1