Algorithms_for_everyone
Algorithms_for_everyone
for Everyone
F r e e m a n , P h D
Eric
1
eric freeman
computer science phd, yale
adjunct faculty, UT austin
oreilly 5x author
advisor, head first series
former disney cto
MIT tr35
Your instructor
2
3
Take a deep breath, this class
takes an applied approach
not a theoretical one.
4
How class works
5
What will you get out of this course?
• Solid introduction to algorithms and data structures
• Understand the crucial Big O notation
• Practical knowledge of essential algorithms
• Insight into fundamental data structures
• Improved coding practices to write more efficient
and scalable code
• Prepare you to further explore algorithms and data
structures on your own
6
The MYTH of the
Tower of Hanoi
7
The MYTH of the
Tower of Hanoi
8
Tower of Hanoi Rules
Objective
• The goal is to move the entire stack of disks from the initial peg
(A) to the destination peg (C), following the rules.
Rules
• Move One Disk at a Time: Only one disk can be moved at a time.
• Top Disk Only: Only the top disk of any peg can be moved.
• No Larger Disk on Smaller Disk: A larger disk cannot be placed
on top of a smaller disk.
9
Tower of Hanoi — Give it a try
https://www.mathplayground.com/logic_tower_of_hanoi.html
10
According to the Tower of Hanoi Myth, how much
time do we have?
11
Doctor Who: The Celestial Toymaker (1966) - The Doctor faces
Tower of Hanoi Rules
the enigmatic Celestial Toymaker in a series of deadly games.
Objective
• The goal is to move the entire stack of disks from the initial peg
(A) to the destination peg (C), following the rules.
Rules
• Move One Disk at a Time: Only one disk can be moved at a time.
• Top Disk Only: Only the top disk of any peg can be moved.
• No Larger Disk on Smaller Disk: A larger disk cannot be placed
on top of a smaller disk.
12
Making a Peanut Butter and Jelly Sandwich
◦ Place the two slices of bread together, with the peanut butter and
jelly sides facing each other.
5. Serve:
13
What is an algorithm?
14
Donald Knuth says…
An algorithm:
• Is finite
• Is precisely defined
• Has input
• Has output
• Must be effective
Correctness
Efficiency Scalability
ality Simplic
ity
Optim Clarity
Robustness Reliability
Adapta
bility
ability
Maintain
Modularity
17
We're going to be concerned with
two of these Extensibility
Correctness
Efficiency Scalability
ality Simplic
ity
Optim Clarity
Robustness Reliability
Adapta
bility
ability
Maintain
Modularity
18
Two ways of measuring efficiency
• Time Complexity
Measures the amount of time an
algorithm takes to complete as a
function of the size of its input.
• Space Complexity
Measures the amount of memory an
algorithm uses as a function of the size
of its input.
19
Let's play a guessing game…
20
Let's play a guessing game…
Player 1: Jill
Player 2: Melissa
Jill: 3 Jill: 8
Too low Too low
Jill: 4 Jill: 9
Too low Too low
Jill: 5 Jill: 10
Too low Too low
22
Let's play a guessing game…
Melissa: 50
Too high
Melissa: 25
Too low
Melissa: 37
Too low
Melissa: 43
Too high
Melissa: 40
You got it!
23
We have a winner!
24
Question:
But what were Jill and
Melissa's algorithms?
Algorithm 1
def algorithm1(number):
for i in range(1, 101):
print(f"Guessing {i}")
if i == number:
print(f"You guessed the number {number} in {i} guesses")
break
num = 40 Guessing 1
Guessing 2
algorithm1(num) Guessing 3
Guessing 4
Guessing 5
Guessing 6
Guessing 7
.
.
.
.
Guessing 40
You guessed the number 40 in 40 guesses
26
Algorithm 1
def algorithm1(number):
for i in range(1, 101):
print(f"Guessing {i}")
if i == number:
print(f"You guessed the number {number} in {i} guesses")
break
num = 40
algorithm1(num)
1 100
27
Algorithm 2
def algorithm2(number):
low = 1
high = 100
found = False
guesses = 0;
if mid == number:
print(f"You guessed the number {number} in {guesses} guesses")
found = True
elif mid < number:
low = mid + 1 Guessing 50
else: Guessing 25
high = mid - 1 Guessing 37
Guessing 43
num = 40 Guessing 40
algorithm2(num) You guessed the number 40 in 5 guesses
28
How long does it take each of these algorithms?
Let's say 1 comparison takes a millisecond
Algorithm 1 Algorithm 2
Size Time (seconds) Time (seconds)
100 .1 0.00664
1000 1.0 0.00997
10,000 10.0 0.01329
100,000 100.0 0.01661
1,000,000 1000.0 0.01993
Algorithm 1 Algorithm 2
Size Time (seconds) Time (seconds)
100 .1 0.00664
1000 1.0 0.00997
10,000 10.0 0.01329
100,000 100.0 0.01661
1,000,000 1000.0 0.01993
1,000,000,000 1000000.0 0.02990
NO! When we have a billion numbers, the
difference is over 34 million times faster!
30
We need a better way
that timing to know how
good an algorithm is.
Big O Notation
32
Big O Notation
33
Big O Notation
34
Comparison of n and n2
Size
100 100 10,000
1000 1000 1,000,000
10,000 10,000 100,000,000
100,000 100,000 10,000,000,000
1,000,000 1,000,000 1,000,000,000,000
35
So what is the time
complexity of Jill's
Algorithm 1 and
Melissa's Algorithm 2?
Algorithm 1
37
Algorithm 1's time complexity
39
Algorithm 2's time complexity
40
Algorithm 2
41
For each guess, how many
possibilities does Jill rule
out? What about Melissa?
42
Each iteration of algorithm 2 removes half the
possibilities from consideration
{
100
50
7 guesses
25
for 100 13
numbers 7
4
2
1
43
Let's consider 512 possibilities
{
512
256
9 guesses 128
for 512
64
32
numbers
16
8
4
2
1
44
Notice how these are powers of 2?
{
512
256
9 guesses 128
for 512
64
32
numbers
16
8
4
2
1
45
A little handy math review
46
So we can compute the upper bound with log
{
512
256
9 guesses 128
for 512
64
32
numbers
16
8
4
2
1
47
Worst
Algorithm 2 worst case Problem case
size guesses
rop
We usually d nd
the base 2 ag"
just say "lo
48
Remembering Logarithms
x
2 = 16 x=4
Then
1
found = False
guesses = 0;
if mid == number:
print(f"You guessed the number {number} in {guesses} guesses")
found = True
elif mid < number:
low = mid + 1
else:
high = mid - 1
num = 40
algorithm2(num)
50
Algorithm 2's time complexity
Algorithm 1: guesses
Algorithm 2: guesses
52
Algorithm 2
53
Group Question:
What is the worst case
for guessing over 1
billion numbers?
Group Question:
What is the worst case for
a range of 1 billion?
log(1,000,000,000) is
only 30!
Comparing algorithms,
if a guess takes a microsecond…
56
Comparing algorithms,
if a guess takes a microsecond…
57
Common Time Complexities
58
Common Time Complexities
59
Simplifying Big O Notation
We study the algorithm and
come up with this expression
61
Ride Wait Times
VIP Pass Express O(1)
Logarithmic Spiral O(log n)
Linear Loop O(n)
Quadratic Twist O(n2)
Exponential Drop O(2n)
Factorial Frenzy O(n!)
62
Ride Wait Times
Logarithmic Spiral
Factorial Frenzy O(n!)
63
Ride Wait Times
VIP Pass Express O(1)
Logarithmic Spiral O(log n)
Linear Loop O(n)
Quadratic Twist O(n2)
Exponential Drop O(2n)
Factorial Frenzy O(n!)
Linear Loop 64
Ride Wait Times
VIP Pass Express O(1)
Logarithmic Spiral O(log n)
Linear Loop O(n)
Quadratic Twist O(n2)
Exponential Drop O(2n)
Factorial Frenzy O(n!)
Quadratic Twist 65
Ride Wait Times
VIP Pass Express O(1)
Logarithmic Spiral O(log n)
Linear Loop O(n)
Quadratic Twist O(n2)
Exponential Drop O(2n)
Factorial Frenzy O(n!)
Exponential Drop 66
Ride Wait Times
VIP Pass Express O(1)
Logarithmic Spiral O(log n)
Linear Loop O(n)
Quadratic Twist O(n2)
Exponential Drop O(2n)
Factorial Frenzy O(n!)
Factorial Frenzy 67
Searching
Algorithms
68
Group Question:
How hard is finding
phone number by
last name in a
phone book?
Group Question:
How hard is finding
a last name by
phone number in a
phone book?
Group Question:
Finding all people
with a given last
name, say,
"Johnson".
Group Question:
Finding all people
with a given first
name, say, "John".
Arrays
● A fixed-size sequence of elements of the
same type (generally)
● Contiguous memory locations
● Each element can be accessed directly by
index
● First element has an index of 0
● Storage and retrieval by index are O(1)
0 1 2 3 4 5
74
Linear Search
arr = [1937, 1950, 1953, 1955, 1959, 1961, 1967, 1970, 1989, 1991, 1992, 1994, 1995, 1998,
1999, 2000, 2001, 2002, 2003, 2004, 2005, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014,
2016, 2018, 2019, 2020, 2021, 2022]
target = 1967
result = linear_search(arr, target)
75
Linear Search Time Complexity
arr = [1937, 1950, 1953, 1955, 1959, 1961, 1967, 1970, 1989, 1991, 1992, 1994, 1995, 1998,
1999, 2000, 2001, 2002, 2003, 2004, 2005, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014,
2016, 2018, 2019, 2020, 2021, 2022]
target = 1967
result = linear_search(arr, target)
76
Binary Search
def binary_search(arr, target): # The given array
low = 0 arr = [1937, 1950, 1953, 1955, 1959, 1961, 1967,
high = len(arr) - 1 1970, 1989, 1991, 1992, 1994, 1995, 1998, 1999,
2000, 2001, 2002, 2003, 2004, 2005, 2007, 2008,
while low <= high: 2009, 2010, 2011, 2012, 2013, 2014, 2016, 2018,
mid = (low + high) // 2 2019, 2020, 2021, 2022]
mid_value = arr[mid]
if mid_value == target:
return mid
elif mid_value < target:
low = mid + 1
else:
high = mid - 1
target = 1967
result = binary_search(arr, target)
print(f"Element {target} found at index {result}")
77
[1937, 1950, 1953, 1955, 1959, 1961, 1967, 1970, 1989, 1991, 1992, 1994, 1995, 1998,
1999, 2000, 2001, 2002, 2003, 2004, 2005, 2007, 2008, 2009, 2010, 2011, 2012, 2013,
2014, 2016, 2018, 2019, 2020, 2021, 2022]
[1937, 1950, 1953, 1955, 1959, 1961, 1967, 1970, 1989, 1991, 1992, 1994, 1995, 1998,
1999, 2000, 2001]
[1967, 1970]
[1967] 78
Binary Search Time Complexity
def binary_search(arr, target): # The given array
low = 0 arr = [1937, 1950, 1953, 1955, 1959, 1961, 1967,
high = len(arr) - 1 1970, 1989, 1991, 1992, 1994, 1995, 1998, 1999,
2000, 2001, 2002, 2003, 2004, 2005, 2007, 2008,
while low <= high: 2009, 2010, 2011, 2012, 2013, 2014, 2016, 2018,
mid = (low + high) // 2 2019, 2020, 2021, 2022]
mid_value = arr[mid]
if mid_value == target:
return mid
elif mid_value < target:
low = mid + 1
else:
high = mid - 1
79
Divide and Conquer
A method for solving problems by breaking them into smaller
subproblems, solving each independently, and combining the results.
• Examples:
- Merge Sort
- Quick Sort
- Binary Search
80
Best, worst, average case?
81
We can also talk about the complexity of array operations
82
Dynamic Arrays
● We allocated more locations than needed
● We expand if we use all locations
● Contiguous memory locations
● Each element can be accessed directly by
index
● First element has an index of 0
● Storage and retrieval by index are O(1)
0 1 2 3 4 5
84
Linked Lists
● Dynamic memory allocation without
contiguous storage
● Efficient insertion and deletion operations
● Nodes contain data and a link/reference
● Use in implementation of other data
structures (queues, hash tables, etc.)
● Supports:
- insert()
- delete()
- update()
- search()
85
The data structure
Start
of list 537-8129 739-4851 359-2784 682-3475
86
Doubly-linked list
Start End
of list 537-8129 739-4851 359-2784 682-3475 of list
87
We can also talk about the complexity of array operations
88
Group Question:
How do arrays and
linked lists
compare?
For search, can we
do better in search
than O(log n)?
Hash tables
A hash table, also known as a hash map,
is a fundamental data structure that
provides efficient access to data through
the use of a key.
91
Hash table uses
• Spell checkers
• Caching
• Compilers
• Network routing
• Web servers
• Dictionaries, objects
92
How hash tables work
93
How hash tables work
value
0
key
1
Emma Johnson
2
Liam Smith
3
Olivia Brown 4
Noah Davis 5
6
7
94
Putting values in
hash table hash function value
0
key
1
Emma Johnson
2
Liam Smith
3 834-2109
Olivia Brown 4
Noah Davis 5
6
7
95
Putting values in
hash table hash function value
0 273-4816
key
1 658-2934
Emma Johnson
2
Liam Smith
3 834-2109
Olivia Brown 4
Noah Davis 5
6
7 903-1748
96
Getting a value
from hash table hash function value
0 273-4816
key
1 658-2934
Emma Johnson
2
3 834-2109
834-2109 4
5
6
7 903-1748
97
Collisions and
chaining hash function value
0 273-4816
key
1 658-2934
Emma Johnson
2
3 834-2109 511-9871
4
collision
5
6
Thomas Kay
7 903-1748
98
hash function
value
100
Hashing in Python
my_dict = {}
my_dict["key"] = "value"
print(my_dict["key"]) # Output: value
value
The hash value of the key 'key'
is: -4123338472047041157
101
Superman's powers: Super
Hashtables in Python strength, flight, x-ray vision
hero_powers = {} Wonder Woman's powers: Super
strength, agility, lasso of truth
hero_powers["Superman"] = "Super strength, flight, x-ray vision" Batman's powers: Intellect,
hero_powers["Wonder Woman"] = "Super strength, agility, lasso of truth" martial arts, high-tech gadgets
hero_powers["Batman"] = "Intellect, martial arts, high-tech gadgets" Spider-Man is not in the hash
table.
print("Superman's powers:", hero_powers["Superman"]) Updated Batman's powers:
print("Wonder Woman's powers:", hero_powers["Wonder Woman"]) Intellect, martial arts,
print("Batman's powers:", hero_powers["Batman"]) detective skills
Wonder Woman has been removed
if "Spider-Man" in hero_powers: from the hash table.
print("Spider-Man's powers:", hero_powers["Spider-Man"])
else: Final state of the hash table:
print("Spider-Man is not in the hash table.") Superman: Super strength, flight,
x-ray vision
hero_powers["Batman"] = "Intellect, martial arts, detective skills" Batman: Intellect, martial arts,
print("Updated Batman's powers:", hero_powers["Batman"]) detective skills
del hero_powers["Wonder Woman"]
print("Wonder Woman has been removed from the hash table.")
● Queues:
- FIFO (First In, First Out): The first element added is the first one to be removed.
- Common Operations: enqueue, dequeue, front.
- Use Cases: Scheduling tasks, managing requests in web servers, BFS.
- Complexity: enqueue and dequeue operations: O(1) O(1)
● Heaps:
- Binary Heaps: Complete binary tree used to implement priority queues.
- Types: Min-heap (smallest element at root) and max-heap (largest element at root).
- Use Cases: Priority queues, Dijkstra’s shortest path algorithm, heap sort.
- Complexity: insert: O(log n) O(logn), extract-min or extract-max: O(log n) O(logn), find-min or
find-max: O(1)O(1)
103
Stacks
● Last In, First Out (LIFO) data structure.
● Like restaurant plate warmer, the last plate
added is the first to be retrieved.
● Supports:
- push()
- pop()
- peek()
- isEmpty()
- size()
- All operations are O(1)
104
Superman arrived and joined the
Stack in Python stack.
stack = [] Wonder Woman arrived and joined
the stack.
stack.append("Superman")
print("Superman arrived and joined the stack.")
Wonder Woman is performing a task
stack.append("Wonder Woman") and leaves the stack.
print("Wonder Woman arrived and joined the stack.")
Batman arrived and joined the
last_hero = stack.pop() stack.
print(f"\n{last_hero} is performing a task and leaves the stack.")
Spider-Man arrived and joined the
stack.append("Batman") stack.
print("Batman arrived and joined the stack.")
stack.append("Spider-Man") Spider-Man is performing a task
print("Spider-Man arrived and joined the stack.") and leaves the stack.
last_hero = stack.pop() Batman is performing a task and
print(f"\n{last_hero} is performing a task and leaves the stack.") leaves the stack.
last_hero = stack.pop() Iron Man arrived and joined the
print(f"{last_hero} is performing a task and leaves the stack.") stack.
stack.append("Iron Man")
print("Iron Man arrived and joined the stack.") Iron Man is performing a task and
last_hero = stack.pop() leaves the stack.
print(f"\n{last_hero} is performing a task and leaves the stack.")
print("\nCurrent stack of superheroes:") Current stack of superheroes:
print(stack) ['Superman']
105
Queues
● First In, First Out (FIFO) data structure.
● Like a move theater line, the first person in
line is the first to be given service.
● Supports:
- enqueue()
- dequeue()
- peek()
- isEmpty()
- size()
106
Superman arrived and joined the
Queues in Python queue.
Wonder Woman arrived and joined
from collections import deque the queue.
queue = deque() Batman arrived and joined the
queue.append("Superman") queue.
print("Superman arrived and joined the queue.")
queue.append("Wonder Woman")
Current queue of superheroes:
print("Wonder Woman arrived and joined the queue.") deque(['Superman', 'Wonder
Woman', 'Batman'])
queue.append("Batman")
print("Batman arrived and joined the queue.")
Superman is performing a task and
print("\nCurrent queue of superheroes:") leaves the queue.
print(queue)
Wonder Woman is performing a task
first_hero = queue.popleft() and leaves the queue.
print(f"\n{first_hero} is performing a task and leaves the queue.")
second_hero = queue.popleft() Current queue of superheroes:
print(f"{second_hero} is performing a task and leaves the queue.")
deque(['Batman'])
print("\nCurrent queue of superheroes:") Spider-Man arrived and joined the
print(queue)
queue.
queue.append("Spider-Man")
print("Spider-Man arrived and joined the queue.") Final queue of superheroes:
print("\nFinal queue of superheroes:") deque(['Batman', 'Spider-Man'])
print(queue)
107
Recursion
108
Understanding Recursion
109
Introducing factorial
1! = 1 = 1
2! = 2 * 1 = 2
3! = 3 * 2 * 1 = 6
4! = 4 * 3 * 2 * 1 = 24
5! = 5 * 4 * 3 * 2 * 1 = 120
99! = 99 * 98 * 97 * … =
110
Thinking recursively
0! = 1 = 1
1! = 1 * 0! = 1
2! = 2 * 1! = 2
3! = 3 * 2! = 6
4! = 4 * 3! = 24
5! = 5 * 4! = 120
99! = 99 * 98!
111
Factorial gets large fast
99! = 9332621544394415268169923885626670
0490715968264381621468592963895217
5999932299156089414639761565182862
5369792082722375825118521091686400
0000000000000000000000
112
Implementing Factorial with iteration
def factorial(n):
result = 1
for i in range(2, n + 1):
result *= i
return result
number = 5
print("The factorial of {number} is
{factorial(number)}")
113
Implementing Factorial with Recursion
number = 5
print("The factorial of {number} is
{factorial(number)}")
114
Implementing Factorial with Recursion
number = 5
print(f"The factorial of {number} is
{factorial(number)}")
115
Another recursive algorithm: Fibonacci
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
117
fibonacci(6)
├─ fibonacci(5)
The recursive runtime │ ├─ fibonacci(4)
│ │ ├─ fibonacci(3)
for fibonacci(6) │
│
│
│
│
│
├─ fibonacci(2)
│ ├─ fibonacci(1)
│ │ │ │ └─ fibonacci(0)
│ │ │ └─ fibonacci(1)
│ │ └─ fibonacci(2)
│ │ ├─ fibonacci(1)
│ │ └─ fibonacci(0)
│ └─ fibonacci(3)
│ ├─ fibonacci(2)
│ │ ├─ fibonacci(1)
│ │ └─ fibonacci(0)
│ └─ fibonacci(1)
└─ fibonacci(4)
├─ fibonacci(3)
│ ├─ fibonacci(2)
│ │ ├─ fibonacci(1)
│ │ └─ fibonacci(0)
│ └─ fibonacci(1)
└─ fibonacci(2)
├─ fibonacci(1)
└─ fibonacci(0)
118
fibonacci(6)
├─ fibonacci(5)
The recursive runtime │ ├─ fibonacci(4)
│ │ ├─ fibonacci(3)
for fibonacci(6) │
│
│
│
│
│
├─ fibonacci(2)
│ ├─ fibonacci(1)
│ │ │ │ └─ fibonacci(0)
│ │ │ └─ fibonacci(1)
│ │ └─ fibonacci(2)
│ │ ├─ fibonacci(1)
│ │ └─ fibonacci(0)
│ └─ fibonacci(3)
│ ├─ fibonacci(2)
│ │ ├─ fibonacci(1)
│ │ └─ fibonacci(0)
│ └─ fibonacci(1)
└─ fibonacci(4)
├─ fibonacci(3)
│ ├─ fibonacci(2)
│ │ ├─ fibonacci(1)
Yikes!
│ │ └─ fibonacci(0)
│ └─ fibonacci(1)
└─ fibonacci(2)
├─ fibonacci(1)
└─ fibonacci(0)
119
Naive Fibonacci Runtime
.
.
.
120
RecursionError: maximum
recursion depth exceeded
Understanding the call stack
● The call stack is a data structure that tracks function calls and their execution order.
● Each function call adds a new frame to the call stack, tracking execution context.
● Recursive calls are just function calls, so each recursion adds a new stack frame; stack grows
with calls, shrinks on returns.
● Beware of stack overflow from excessive recursion.
def fact(n):
if n == 0 or n == 1:
return 1
else:
return n * fact(n - 1)
Call stack
fact(5)
4 * fact(3)
122
fact(1) = 1 fact(1) = 1
1
2 * fact(1) 2 * fact(1) 2 * fact(1)
def fact(n):
if n == 0 or n == 1:
return 1
else:
2 * fact(1) return n * fact(n - 1)
2 fact(5)
3 * fact(2) 3 * fact(2)
6
4 * fact(3) 4 * fact(3) 4 * 6 = 24
Dynamic programming
A method for solving complex problems by breaking them down into
simpler subproblems
124
Fibonacci with dynamic programming
def fibonacci(n, cache={}):
if n <= 1: Memoizing, dynamic
return n
programming technique,
if n in cache: significantly optimizes
return cache[n]
performance by storing
cache[n] = fibonacci(n - 1, cache)
+ fibonacci(n - 2, cache) and reusing previously
return cache[n]
computed values.
for i in range(40):
print(f"Fibonacci({i}) = {fibonacci(i)}")
125
Memoized Fibonacci Runtime
.
.
.
.
.
.
126
Sorting
Algorithms
127
bubblesort in action
https://www.hackerearth.com/practice/algorithms/sorting/bubble-sort/visualize/
128
bubblesort implementation
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
arr = [100, 110, 98, 11, 33, 99, 105, 110, 200, 0, 1, 54]
result = bubble_sort(arr)
print(f"{arr}")
129
Can we guess what its time
complexity is?
130
bubblesort time complexity
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
arr = [100, 110, 98, 11, 33, 99, 105, 110, 200, 0, 1, 54]
result = bubble_sort(arr)
print(f"{arr}")
131
merge sort
https://www.hackerearth.com/practice/algorithms/sorting/merge-sort/visualize/ 132
merge sort
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left_half = arr[:mid]
right_half = arr[mid:]
left_sorted = merge_sort(left_half)
right_sorted = merge_sort(right_half)
133
# Example usage
def merge(left, right): arr = [38, 27, 43, 3, 9, 82, 10]
sorted_array = [] sorted_arr = merge_sort(arr)
left_index = 0 print("Original array:", arr)
right_index = 0 print("Sorted array:", sorted_arr)
return sorted_array
134
merge sort time complexity
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left_half = arr[:mid]
right_half = arr[mid:]
left_sorted = merge_sort(left_half)
right_sorted = merge_sort(right_half)
135
what about space complexity?
136
Other sorts to know
137
Graph
Algorithms
138
Graphs - Undirected
A
An undirected graph B
has edges that
C
connect vertices
without any direction,
D
E
allowing bidirectional
G
traversal.
F
139
Graphs - Weights
A
2
An undirected B 34
42
weighted graph has
23 C
edges with no 15
10
direction, and each
D 22
E 17
edge has an 11
G
associated weight. 3
140
Graphs - Directed
A
141
Graphs - Cycle
A
A cycle is a path in a B
graph that starts and
C
ends at the same
vertex, forming a loop.
D
E
142
Graphs - Directed with Weights
2 A
34
A directed weighted B
9
graph has edges with 23 42
C
specified directions 15
10
and associated
D 22
E 17
weights, indicating the 11
17 3
G
cost of traversal.
F
143
Trees
A
A tree is a connected
acyclic graph with a B C D
hierarchical structure,
having one root node E F G H I J
and no cycles.
K L M
144
Graph representations
● Use adjacency lists
A
graph = { 'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'], B
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E'] }
C
● Or, an adjacency matrix
adj_matrix = [ D
[0, 1, 1, 0, 0, 0], E
[1, 0, 0, 1, 1, 0],
[1, 0, 0, 0, 0, 1],
[0, 1, 0, 0, 0, 0], F
[0, 1, 0, 0, 0, 1],
[0, 0, 1, 0, 1, 0]
]
145
Graph time and space complexity
146
Graph Traversal
The process of visiting all the vertices and
edges in a graph.
● Types of Traversals:
- Depth-First Search (DFS): Explores as far
down a branch as possible before
backtracking.
- Breadth-First Search (BFS): Explores all
neighbors at the present depth before
moving on to nodes at the next depth level.
● Applications:
- Finding connected components
- Pathfinding algorithms
- Solving puzzles and games
147
Depth first search (DFS)
DFS explores as far down a branch as
possible before backtracking, making it ideal
for scenarios requiring exhaustive
pathfinding like maze-solving.
148
Depth first search (DFS)
B
Visited: []
Stack: [B] C
D
E
F
149
Depth first search (DFS)
B
Visited: [B]
Stack: [D E A] C
D
E
F
150
Depth first search (DFS)
B
Visited: [B D]
Stack: [E A] C
D
E
F
151
Depth first search (DFS)
B
Visited: [B D E]
Stack: [F A] C
D
E
F
152
Depth first search (DFS)
B
Visited: [B D E F]
Stack: [A] C
D
E
F
153
Depth first search (DFS)
B
Visited: [B D E F A]
Stack: [C] C
D
E
F
154
Depth first search (DFS)
B
Visited: [B D E F A C]
Stack: [] C
D
E
F
155
Depth first implementation
visited.add(start)
for neighbor in graph.adj_list.get(start, []):
if neighbor not in visited:
dfs(graph, neighbor, visited)
return visited
156
Depth first implementation A
B
graph = {
'A': ['B', 'C'], C
dfs(graph, 'B')
157
Breadth first search (BFS)
BFS explores all neighbors of a node before
moving to the next level, making it ideal for
finding the shortest path in unweighted graphs.
158
Breadth first search (BFS)
B
Visited: []
Queue: [B] C
D
E
F
159
Breadth first search (BFS)
B
Visited: [B]
Queue: [A E D] C
D
E
F
160
Breadth first search (BFS)
B
Visited: [B D]
Queue: [A E] C
D
E
F
161
Breadth first search (BFS)
B
Visited: [B D E]
Queue: [F A] C
D
E
F
162
Breadth first search (BFS)
B
Visited: [B D E A]
Queue: [C F] C
D
E
F
163
Breadth first search (BFS)
B
Visited: [B D E A F]
Queue: [C] C
D
E
F
164
Breadth first search (BFS)
B
Visited: [B D E A F C]
Queue: [] C
D
E
F
165
Breadth first implementation A
C
def bfs(graph, start):
visited = set() D
E
queue = deque([start]) F
bfs_order = []
graph = {
'A': ['B', 'C'],
while queue: 'B': ['D', 'E', 'A'],
vertex = queue.popleft() 'C': ['A'],
if vertex not in visited: 'D': ['B', 'E'],
visited.add(vertex) 'E': ['D', 'F'],
bfs_order.append(vertex) 'F': ['E']
}
for neighbor in graph[vertex]:
bfs_result = bfs(graph, 'B')
if neighbor not in visited: print("BFS Order:", bfs_result)
queue.append(neighbor)
return bfs_order BFS Order: ['B', 'D', 'E', 'A', 'F', 'C']
166
Dijkstra's Algorithm
● Optimal Path Finding: Efficiently determines
the shortest path in weighted graphs.
● Real-World Applications: Crucial for GPS
navigation, network routing, and logistics.
● Weighted Graphs: Handles varying edge
weights, unlike BFS which only works for
unweighted graphs.
● Efficiency: Uses a greedy approach to build
the shortest path tree incrementally.
● Scalability: Suitable for large graphs with
numerous nodes and edges.
● Algorithm Foundation: Builds understanding
for more complex algorithms like A* and
Bellman-Ford.
167
Shortest path to pizza
3 A
Vertices Distance Previous 10
home 0
A infinity 2 6 D
B infinity
C
8
infinity B
D infinity C
5
pizza infinity 4
168
Shortest path to pizza
3 A
Vertices Distance Previous 10
home 0
A 3 home 2 6 D
B 2 home
C
8
6 home B
D infinity C
5
pizza infinity 4
169
Shortest path to pizza
3 A
Vertices Distance Previous 10
home 0
A 3 home 2 6 D
B 2 home
C
8
6 home B
D infinity C
5
pizza infinity 4
170
Shortest path to pizza
3 A
Vertices Distance Previous 10
home 0
A 3 home 2 6 D
B 2 home
C
8
6 home B
D infinity C
5
pizza 4 C 4
171
Shortest path to pizza
3 A
Vertices Distance Previous 10
home 0
A 3 home 2 6 D
B 2 home
C
8
6 home B
D 13 A C
5
pizza 4 C 4
172
Shortest path to pizza
3 A
Vertices Distance Previous 10
home 0
A 3 home 2 6 D
B 2 home
C
8
6 home B
D 13 A C
5
pizza 4 C 4
173
Dijkstra's Algorithm
import heapq
def dijkstra(graph, start):
priority_queue = [(0, start)]
distances = {vertex: float('inf') for vertex in graph.adj_list}
distances[start] = 0
visited = set()
while priority_queue:
current_distance, current_vertex = heapq.heappop(priority_queue)
if current_vertex in visited:
continue
visited.add(current_vertex)
for neighbor, weight in graph.adj_list.get(current_vertex, []):
distance = current_distance + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(priority_queue, (distance, neighbor))
174
return distances
First, let's understand a heap data structure
● Acts as a priority queue
● Like an airport checkin with priority for
class levels (first, business, economy)
● Supports:
- insert() O(log n)
- extractmin and max() O(log n)
- peek() O(1)
- delete() O(1)
175
Superman arrived and joined the
Heaps in Python priority queue.
Wonder Woman arrived and joined the
import heapq
priority queue.
priority_queue = [] Batman arrived and joined the
priority queue.
heapq.heappush(priority_queue, (2, 'Superman'))
print("Superman arrived and joined the priority queue.")
Priority Queue after adding
heapq.heappush(priority_queue, (1, 'Wonder Woman')) superheroes:
print("Wonder Woman arrived and joined the priority queue.") [(1, 'Wonder Woman'), (2,
heapq.heappush(priority_queue, (3, 'Batman')) 'Superman'), (3, 'Batman')]
print("Batman arrived and joined the priority queue.")
Wonder Woman is performing a task
print("\nPriority Queue after adding superheroes:") and leaves the queue.
print(priority_queue) Superman is performing a task and
highest_priority_hero = heapq.heappop(priority_queue) leaves the queue.
print(f"\n{highest_priority_hero[1]} is performing a task and leaves the queue.")
Spider-Man arrived and joined the
next_priority_hero = heapq.heappop(priority_queue) priority queue.
print(f"{next_priority_hero[1]} is performing a task and leaves the queue.")
heapq.heappush(priority_queue, (4, 'Spider-Man')) Priority Queue after adding another
print("\nSpider-Man arrived and joined the priority queue.") superhero:
print("\nPriority Queue after adding another superhero:") [(3, 'Batman'), (4, 'Spider-Man')]
print(priority_queue) Batman is performing a task and
leaves the queue.
while priority_queue: Spider-Man is performing a task and
hero = heapq.heappop(priority_queue)
print(f"{hero[1]} is performing a task and leaves the queue.")leaves the queue.
print("\nPriority Queue is now empty.") 176
Dijkstra's Algorithm
import heapq
def dijkstra(graph, start):
priority_queue = [(0, start)]
distances = {vertex: float('inf') for vertex in graph.adj_list}
distances[start] = 0
visited = set()
while priority_queue:
current_distance, current_vertex = heapq.heappop(priority_queue)
if current_vertex in visited:
continue
visited.add(current_vertex)
for neighbor, weight in graph.adj_list.get(current_vertex, []):
distance = current_distance + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(priority_queue, (distance, neighbor))
return distances
177
Greedy Algorithms
178
The return of the MYTH
of the
Tower of Hanoi
179
Tower of Hanoi Rules
Objective
• The goal is to move the entire stack of disks from the initial peg
(A) to the destination peg (C), following the rules.
Rules
• Move One Disk at a Time: Only one disk can be moved at a time.
• Top Disk Only: Only the top disk of any peg can be moved.
• No Larger Disk on Smaller Disk: A larger disk cannot be placed
on top of a smaller disk.
180
181
182
183
184
185
186
187
188
Implementing Tower of Hanoi
def tower_of_hanoi(n, source, target, auxiliary):
if n == 1:
print(f"Move disk 1 from {source} to {target}")
return
189
What is its time complexity?
Every call to tower_of_hanoi doubles the
computation until we reach the base case
c a ll d o u b le s our work by
Every
alling
recursively c
noi again
tower_of_ha
190
How long do we have?
191
How long do we have?
193
Wrapping things up, a cheat sheet
e
Constant tim
No loops
hms
Code with one loop Linear algorit
h
Each loop halves work Binary searc
s
Each loop halves work nqu er style algo
Divide and co
with combined results
195
Where to go next
196
Where to go next
197
Where to go next
Coming in 2025
198
Thank you
199