Abstract: This workshop teaches basic algorithms in whiteboarding interviews. All the code examples are in Python and the course has dual purpose teaching basic Python programming.
A
Andrew FerlitschSecurity Engineer/Consultant at AllMed Healthcare Management
3. Coding Challenge
• Python Language Constructs Covered:
1. Main body
2. Indentation
3. Print
4. Line Comments
5. For loop – Range and Steps
6. If / else
7. Integer conversion
Prime Numbers
4. Prime Numbers
• Prime numbers are numbers that are only divisible by one and itself.
• Iterative Algorithm
1. For each number, we attempt to divide it by every number less than it, except for one.
2. The integer modulo operator is used to test if there is a remainder from the integer
division.
3. If there is no remainder, it is divisible by the number and therefore not a prime.
4. If each of the numbers it is divided by has a remainder, it is a prime.
print(“1)
print(“2”)
# Primes for numbers above 2
for number in range(3, 101):
# Attempt to divide this number by every number between 2 and one less than itself
for div in range(2, number):
if (number % div) == 0:
break
else:
print(number)
Range is always one less than the endpoint
Else clause is triggered when for loop does not complete all loops (i.e., break)
5. Prime Numbers – Skip Even Numbers
• Improvement:
1. Skip all even numbers, since they are divisible by two.
2. Odd numbers are not divisible by two, so skip dividing by even numbers.
print(“1)
print(“2”)
Print(“3”)
# Primes for numbers above 5
for number in range(5, 101, 2):
# Attempt to divide this number by every number between 3 and one less than itself
for div in range(3, number, 2):
if (number % div) == 0:
break
else:
print(number)
Step thru range in increments of 2 (odd numbers)
6. Prime Numbers – Skip Even Numbers, Divide by only
1/3 of range
• Improvement:
1. Since we are skipping evens, each prime must be divisible by at least the number 3.
2. So a prime must be divisible by 1/3 of less of itself.
print(“1)
print(“2”)
Print(“3”)
# Primes for numbers above 5
for number in range(5, 101, 2):
third = int(number / 3) + 1
# Attempt to divide this number by every number between 3 and 1/3 of itself
for div in range(3, third, 2):
if (number % div) == 0:
break
else:
print(number)
Round result to an integer
7. Coding Challenge
• Python Language Constructs Covered:
1. Function Definition
2. Iterative vs. Recursion
3. Variable Initialization
4. Return values
Fibonacci Sequence
8. Fibonacci Sequence – Recursive
• A Fibonacci sequence is F(n) = F(n-1) + F(n-2)
• Recursive Algorithm
1. Where F(0) = 0, F(1) = 1
2. F(n) = F(n-1) + F(n-2), where n >= 2
def fibonacci(n):
# Where F(0) = 0, F(1) = 1
if n == 0:
return 0
if n == 1:
return 1
# Recursion when n >= 2
return fibonacci(n – 1) + fibonacci(n – 2)
Recursion
Function Definition
9. Fibonacci Sequence - Iterative
• Iterative Algorithm
1. Where F(0) = 0, F(1) = 1
2. F(n) = F(n-1) + F(n-2), where n >= 2
def fibonacci(n):
# Where F(0) = 0, F(1) = 1
if n == 0:
return 0
if n == 1:
return 1
fn = 0 # current value for F(n)
f_minus_1 = 1 # current value for F(n-1)
f_minus_2 = 0 # current value for F(n-2)
for i in range(2, n+1):
# F(n) = F(n-1) + F(n-2) for current iteration
fn = f_minus_1 + f_minus_2
# Calculate F(n-1) and F(n-2) for next iteration
f_minus_2 = f_minus_1
f_minus_1 = fn
return fn
Initialize accumulators with n = 2
Range is always one less than the endpoint
Next F(n-1), F(n-2) would equal the current
F(n), F(n-1)
10. Coding Challenge
• Python Language Constructs Covered:
1. Static vs. Dynamic Array
2. Linked Lists
3. Class Definition
4. Class Constructor
5. Class Methods
6. Null (None) pointer
7. List Traversal
Dynamic Arrays
11. Dynamic Array
• An Array which can dynamically resize itself (i.e., linked list).
1. Add or Insert into the array (i.e., expand).
2. Remove any element in the array (i.e., shorten).
Static Array
0
Dynamically added element
3
1 2 3
Array is a contiguous block of memory,
with each element of fixed size.
Index of Element
Address of element is:
Address of start of array + ( index * element_size )
Dynamic Array
0 1 2
Link Link Link
Array is a non-contiguous block of memory,
With each element chained together by a link.
12. Dynamic Array
class Entry(object): # Definition for an array element
def __init__(self, data):
self.data = data # Entry data
self.link = None # Link to next Entry in Array
class Array(object):
def __init__(self):
self.array= None # initialize array to empty
# Add element an element to the end of the array
def add(self, data):
# Empty Array
if self.array is None:
self.array = Entry(data)
# Non-empty array
else:
# Find the last entry in the array
prev = self.array
while prev.link is not None:
prev = prev.link
# Add the element to the end of the list
prev.link = Entry( data )
Definition for an entry in
the dynamic array to hold
the entry data and link.
None is the null pointer.
Set array to a single entry
Remember last entry visited
In the linked list of entries.
When the link member is
None (null), you are at the
end of the linked list.
13. Dynamic Array - Improvement
class Array(object):
last = None # last entry in array
def __init__( self ):
self.array= None # initialize array to empty
# Add element an element to the end of the array
def add( self, data ):
entry = Entry( data )
# Empty Array
if self.last is None:
self.array = entry
# Non-empty array
else:
self.last.link = entry
# Set last entry to the newly added entry
self.last = entry
Keep location of last
entry in array.
14. Dynamic Array – Time Complexity
• Add
1. By maintaining pointer to the end, does not need to traverse the array
O(1)
• Find
1. Must traverse the links in the array (index-1) times for the index, where 0 < index < n
O(n/2) = O(n)
• Delete
1. Must traverse the links in the array (index-1) times for the index, where 0 < index < n
O(n/2) = O(n)
In complexity notation (Omega), multiplication and
division by a constant is removed
Omega
In complexity notation (Omega), 1 is used to represent
constant time.
In complexity notation (Omega), n is the number of
elements.
15. Coding Challenge
• Python Language Constructs Covered:
1. First In, First Out
2. Python Dynamic (builtin) Arrays
3. append() array method
4. len() object method
5. Copying elements
6. del operator
Queue
16. Queue
• FIFO (First In, First Out) – Data Structure
1. Enqueue – add an element to the back of the list
2. Dequeue – remove an element from the front of the list
x1
x2
Enqueue
Each element
is added to the
end of the queue
x2
Dequeue
x3
x3
Each element is
removed from the
front of the queue
x3 x1
17. Queue
class Queue(object):
def __init__(self):
self.queue = [] # initialize queue to empty dynamic array
# Add element to queue
def enqueue( self, element ):
self.queue.append( element )
# Remove element from front of the queue
def dequeue( self ):
# Empty Queue condition
if len( self.queue ) == 0:
return None
# Get (remember) the first element from the queue
element = self.queue[ 0 ]
# Remove (drop) the first element in the queue
del self.queue[ 0 ]
# Return the first element
return element
append() method adds an element to end of array/list.
len() function returns the number of
elements in an array/list.
Makes a local copy of the first element.
The del operator will remove an element
in an array/list at the specified location.
Notation for empty list/array.
18. Coding Challenge
• Python Language Constructs Covered:
1. Last In, Last Out
2. insert() array method
Stack
19. Stack
• LIFO (Last In, First Out) – Data Structure
1. Push – add an element to the front of the list
2. Pop – remove an element from the front of the list
x3
x2
Push
Each element is
added to the front
of the stack
x2
Pop
x1
x3
Each element is
removed from the
front of the stack
x3 x3
20. Stack
class Stack(object):
def __init__( self ):
self.stack = [] # initialize stack to empty dynamic array
# Add element to the stack
def push( self, element ):
self.stack.insert( 0, element )
# Remove element from front of the stack
def pop( self ):
# Empty Stack condition
if len( self.stack ) == 0:
return None
# Get (remember) the first element from the stack
element = self.stack[ 0 ]
# Remove (drop) the first element in the stack
del self.stack[ 0 ]
# Return the first element
return element
insert() method inserts an element to an array/list
at the specified index.
21. Coding Challenge
• Python Language Constructs Covered:
1. Importing a library
2. Prioritized Queue : heappush(), heappop()
Heap
22. Heap
• Prioritized Queue (Sorted by value) – Data Structure
1. Push – add an element to the sorted position in the list
2. Pop – remove an element from the front of the list
x3
x3
Push
Each element is
added at it’s
sorted location
In the queue
x2
Pop
x1
x3
Each element is
removed from the
front of the queue
x3 x3
23. Heap
from heapq import heappush, heappop
class Heap(object):
def __init__( self ):
self.heap = [] # initialize heap to empty dynamic array
# Add element to the (sorted) heap
def push( self, element ):
heappush( self.heap, element )
# Remove element from front of the (sorted) heap
def pop( self ):
# Empty Heap condition
if len( self.heap ) == 0:
return None
# Get the first element from the heap
element = heappop( self.heap )
# Return the first element
return element
heappush() function inserts an element to a sorted array/list.
Heappop() function removes the first element from a
sorted array/list.
Import the classes heappush and heappop
from the library heapq.
24. Queue, Stack, Heap -Time Complexity
• Queue
Enqueue O(1)
Dequeue O(1)
• Stack
Push O(1)
Pop O(1)
• Heap
Push O(n)
Pop O(1)
Must traverse array to find location
to insert.
25. Coding Challenge
• Python Language Constructs Covered:
1. Arithmetic Assignment
2. Recurring back in Recursion
StackChain
26. StackChain
• Chain of Stacks of Fixed Size
1. When an element is pushed to a stack that has reached its maximum (fixed) size, the
bottom of the stack is removed and pushed to the top of the next stack on the chain.
2. Likewise, when an element is popped from the stack, the top of the next stack that is
chained to it is popped and added to the bottom of the stack, and so forth.
x4
x3
Push
x2
x4
x1
Fixed Size
(e.g., 3) Removed
from the
bottom and
pushed to
top of the
next stack.
x3
Pop
x2
x3
x1
Removed
from the
top and
pushed to
top of the
previous stack.
27. StackChain
class StackChain(object):
def __init__( self, max ):
self.max = max # maximum size of a stack
self.stack = [] # initialize this stack to empty list/array.
self.chain = None # next stack in chain
# Add element to the stack chain
def push( self, element ):
self.stack.insert( 0, element )
# stack is less than full
if len( self.stack ) < self.max:
self.max += 1
# stack is full
else:
# Allocate the next stack in chain, if there is not one already.
if self.chain == None:
self.chain = stackChain( self.max )
# Get (remember) the element from the bottom of the stack
element = self.stack[ self.max - 1 ]
# Remove the element
del self.stack[ self.max - 1 ]
# push the element to the next stack in the chain
self.chain.stack.push( element )
No increment/decrement in Python.
Recursive call, will cascade
until the last stack in chain is less
than full.
28. StackChain
class StackChain(object):
# Remove element from the stack chain
def pop( self ):
# stack is empty
if len( self.stack ) == 0:
return None
# Get (Remember) top element in stack
element = self.stack[ 0 ]
# Remove the element from the stack
del self.stack[ 0 ]
# There is another stack in the chain
if self.chain is not None:
# Pop the top element from the next stack in the chain
bottom = pop( self.chain.stack )
# Add the element to the bottom of this stack
self.stack.append( bottom )
# Return the top of the stack
return element
Recursive call, will cascade to the last stack
and recur backwards moving the top of the
stack to the bottom of the previous stack.
29. Coding Challenge
• Python Language Constructs Covered:
1. Initializing size of an array
2. Recursive Sub-Solutions
Towers of Hanoi
30. Towers of Hanoi - Recursion
• Three Towers, stack of disks on start Tower, each disk smaller than
proceeding disk. Move the disks to the end Tower.
1. You move one disc at a time from the top of one tower to the top of another tower.
2. You can not place a larger disc on top of a smaller disc.
Start Intermediate End
Solve for N-1
Discs
Move disc N to End
StartIntermediate End
Move disc N to EndSolve for N-1
Discs
Recursion
31. Towers of Hanoi
class Hanoi(object):
towers = [ None ] * 3 # Initialize 3 towers with no discs on them
for i in range( 0, 3 ):
towers[ i ] = Stack()
def __init__(self, ndiscs ):
self.ndiscs = ndiscs # the number of discs
# initialize the first tower with discs in ascending order.
for i in range ( ndiscs, 0, -1 ):
self.towers[ 0 ].push( i )
# Recursive method to move ndiscs from start to end
def move( self, ndiscs, start, intermediate, end ):
# if single disk left, move it from start to end
if ndiscs == 1:
end.push( start.pop() )
else:
# move the remainder of the tower to the intermediate tower
self.move( ndiscs - 1, start, end, intermediate )
# move the bottom disc to the end tower
end.push( start.pop() )
# move the remainder of the intermediate tower to the end tower
self.move( ndiscs - 1, intermediate, start, end )
def play( self ):
self.move( self.ndiscs, self.towers[ 0 ], self.towers[ 1 ], self.towers[ 2 ] )
Can initialize class member variables
Outside of the constructor (__init__)
Notation for initializing an array of fixed
size (e.g., 3).
Recursively
solve
sub-problems
33. Binary Tree
• A Binary Tree is a type of directed graph tree where each node
contains at most two branches (subtrees), commonly referred to as
the left and right branch.
1. The recursive definition is a binary tree is either empty, a single node, where the left
and right branches are binary subtrees.
A
B C
D E
Left
Left
Root
Right
Right
Nodes
Leaves
Binary
subtree
34. Binary Tree
class BinaryTree(object):
# Constructor: set the node data and left/right subtrees to null
def __init__(self, key):
self.left = None # left binary subtree
self.right = None # right binary subtree
self.key = key # node data
# Get ot Set Left Binary Subtree
def Left(self, left = None):
if left is None:
return self.left
self.left = left
# Get or Set Right Binary Subtree
def Right(self, right = None):
if right is None:
return self.right
self.right = right
# Get ot Set Node Data
def Key(self, key = None):
if key is None:
return self.key
self.key = key
Good Practice to have
parent classes explicitly
Inherit the object class,
which all classes
implicitly inherit.
Default parameter. If
not specified, will
default to None.
Technique to emulate (fake) method overloading
using default parameter. If parameter not
specified, then it’s a getter[ otherwise (if
specified), it’s a setter.
35. Coding Challenge
• Python Language Constructs Covered:
1. is operator vs. ==
2. Default Parameters
3. Recursion (more)
4. Returning multiple values and Assignment of multiple
variables
Binary Tree Traversals
36. Binary Tree Traversals
• Binary Trees can be traversed either breadth first (BFS) or
depth first (DFS).
• Breadth First Search – tree is traversed one level at a time.
• The root node (level 1) is first visited, then the left node, then
the right node (level 2) of the root, and then the left and right
nodes of the these subtrees (level 3), and so forth.
• Depth First Search – tree is searched either inorder,
preorder, or postorder.
• Inorder : left (node), root, right
• Preorder : root, left, right
• Postorder: left, right, root
37. BFS Traversal
# Breadth First Search
def BFS( root ):
# Check if tree is empty
if root is None:
return
# list of nodes to visit in node level order
visit = []
visit.append( root )
# sequentially visit each node in level order as it is dynamically added to the list
i = 0
while i < len( visit ):
# Add to the list the child siblings of this node
if visit[ i ].Left() is not None:
visit.append( visit[ i ].Left() )
if visit[ i ].Right() is not None:
visit.append( visit[ i ].Right() )
i += 1
Visit is a dynamic array
Good practice when comparing
to None to use ‘is’ instead of ==
Good practice when comparing
to None to use ‘is not’ instead of
!=
38. DFS Traversal
Recursive Algorithms
# InOrder Traversal
def InOrder(root):
if root is None:
return
InOrder( root.Left() )
root.Action()
InOrder( root.Right() )
# PreOrder Traversal
def PreOrder(root):
if root is None:
return
root.Action()
PreOrder( root.Left() )
PreOrder( root.Right() )
# PostOrder Traversal
def PostOrder(root):
if root is None:
return
PostOrder( root.Left() )
PostOrder( root.Right() )
root.Action() The action to take when a node is visited.
Pre, In and Post refer to when
you visit the root (parent) node:
Pre = First
In = Middle
Post = Last
39. Binary Tree Max/Min Depth
• Binary Tree – Maximum/Minimum depth algorithm
1. Do a preorder traversal of the tree starting from the root.
2. When a node is visited, increment the depth count by 1.
3. Recursively apply the algorithm to the left and right nodes.
4. When returning from a child to a parent node, pass back the node depth of the child.
5. Return the maximum and minimum depth from the left and right child (or the parent if
there are no children).
A
B C
D E
Left
Left
Root
Right
Right
depth = 3 depth = 3
max = 3,
min = 3
max =2,
min = 2
max = 3,
min = 2
40. Binary Tree Maximum Depth
class BinaryTree(object):
# Maximum/Minimum depth of a binary tree
def MaxMinDepth( self, max ):
max += 1 # Increment by one level for the node
# Initialize the left and right branch max/min depth to the current max depth
lmax = lmin = max
rmax = rmin = max
# Calculate the maximum depth along the left binary subtree
if self.Left() is not None:
lmax, lmin = self.Left().MaxMinDepth( max )
# Calculate the maximum depth along the left binary subtree
if self.Right() is not None:
rmax , rmin = self.Right().MaxMinDepth( max )
# return the greater (max) and lessor (min) of the left and right subtrees
return ( rmax if rmax > lmax else lmax ), ( rmin if rmin < lmin else lmin )
Ternary conditional operator:
true_clause if condition else false_clause
Visited the root first.
Return max and min as a list
Can assign multiple
values on LHS
when list is returned.
41. Binary Tree Max/Min Value
• Binary Tree – Maximum/Minimum depth algorithm
1. Use either BFS or DFS/postorder traversal
2. Postorder: Recursively apply the algorithm to the left and right nodes.
3. When returning from a child to a parent node, pass back the max/min of the child.
4. Return the maximum or minimum value from the left and right child and parent.
5
4 3
5 8
Left
Left
Root
Right
Right
max = 5,
min = 5
max = 8,
min = 8
max = 8,
min = 4
max = 5,
min = 3
max = 8,
min = 3
42. Binary Tree Max/Min Value
class BinaryTree(object):
# Maximum/Minimum of a binary tree
def MaxMinValue( self ):
# Initialize the left and right branch max/min values to +/- infinity
lmax = rmax = -2147483647
lmin = rmin = 2147483648
# Calculate the maximum depth along the left binary subtree
if self.Left() is not None:
lmax, lmin = self.Left().MaxMinValue( )
# Calculate the maximum depth along the left binary subtree
if self.Right() is not None:
rmax , rmin = self.Right().MaxMinValue( )
# Compare max/min of child with parent
if rmax < self.Value():
rmax = self.Value()
if rmin > self.Value():
rmin = self.Value()
# return the greater (max) and lessor (min) of the left and right subtrees
return ( rmax if rmax > lmax else lmax ), ( rmin if rmin < lmin else lmin )
Visited the root last
43. Coding Challenge
• Python Language Constructs Covered:
1. Ternary Conditional operator
2. Local Variables
3. elif statement
4. Removing Links
Binary Search Tree
44. Binary Search Tree - Insert
• Binary Search Tree – Insert algorithm
1. A sorted tree where at each node, the data value of the left branch (child) is less than
the data value of the node, and the right node is greater than, and where there are
no duplicate values.
1. Insert: if the root is null (None), add it as the root; otherwise, traverse the tree.
2. If the value is less than the node (branch), traverse left; otherwise traverse right.
3. If there is no node (branch) to traverse, then add it as a new node.
6
4 8
2 5
Left
Left
Root
Right
Right
7
Left
7
Insert
7 > 6, go right
7 < 8, go left
No node, insert
45. Binary Search Tree - Insert
class BinarySearchTree(object):
root = None # root of the search tree
# Insert a node into a binary search tree
def Insert( self, node ):
# If tree is empty, make the root the first node
if self.root is None:
self.root = node
return
# Follow a path to insert the node
curr = self.root
while True:
# if value is less than node, traverse left branch
if node.value < curr.value:
# if no left branch, set node as left branch
if curr.Left() is None:
curr.Left( node )
return
curr = curr.Left()
# otherwise, traverse right branch :
elif curr.Right() is None:
curr.Right( node )
return
else:
curr = curr.Right()
Local variable, created
on first use.
Loop forever
Format of:
else if statement
Add code here to
check for duplicate
46. Binary Search Tree - Find
• Binary Search Tree – Find algorithm
1. If the root is null (None), return None.
2. If value is equal to the node, return the node.
3. If the value is less than the node (branch), traverse left; otherwise traverse right.
4. If there is no node (branch) to traverse, then return None.
6
4 8
2 5
Left
Left
Root
Right
Right
7
Left
Find value = 7
7 > 6, go right
7 < 8, go left
Node Found
47. Binary Search Tree - Find
class BinarySearchTree(object):
root = None # root of the search tree
# Find a node into a binary search tree
def Find( self, value ):
# If tree is empty, return None
if self.root is None:
return None
# Follow a path to find the node
curr = self.root
while True:
# Node Found
if curr.value == value:
return curr
# if value is less than node, traverse left branch
if value < curr.value:
# if no left branch, return not found
if curr.Left() is None:
return None
curr = curr.Left()
# otherwise, traverse right branch :
elif curr.Right() is None:
return None
else:
curr = curr.Right()
48. Binary Search Tree - Delete
• Binary Search Tree – Delete algorithm
1. If the root is null (None), return.
2. While remembering the parent node, If the value is less than the node (branch),
traverse left; otherwise traverse right.
3. If the value equals the node value, set the parent left branch to null if left branch;
Otherwise set parent right branch to null.
4. Reinsert the left and right branches of the removed node.
6
4 8
2 5
Left
Left
Root
Right
Right
7
Left
Remove value = 8
7 > 6, go right
Parent = 6
Remove Link
6
4
2 5
Left
Left
Right
Right
7
Reinserted
Node
49. Binary Search Tree - Delete
class BinarySearchTree(object):
root = None # root of the search tree
# Find a node into a binary search tree
def Delete( self, value ):
# If tree is empty, return None
if self.root is None:
return None
# Follow a path to find the node to delete
curr = self.root
prev = self.root
while True:
# Node Found
if curr.value == value:
if left == True: # delete the left branch of the parent
prev.left = None
else: # delete the right branch of the parent
prev.right = None
if curr.Left(): # re-insert left branch of deleted node
self.Insert( curr.left )
if curr.Right(): # re-insert right branch of delete node
self.Insert( curr.right )
return
# if value is less than node, traverse left branch
if value < curr.value:
# if no left branch, return not found
if curr.Left() is None:
return None
curr = curr.Left()
left = True # traversed left branch
# otherwise, traverse right branch :
elif curr.Right() is None:
return None
else:
curr = curr.Right()
left = False # traversed right branch
51. Arithmetic - Multiply
• Algorithm to implement multiply with only addition
and equality operator.
1. Initialize accumulators for result and repeat to 0.
2. Loop indefinitely for ‘x * y’
1. If repeat accumulator equals zero, stop.
2. Increment the repeat accumulator by one.
3. Add x to the result.
Repeaty ->
y > 0
result
No
Yes y-- result += x
52. Arithmetic - Multiply
def Mul( x, y ):
# multiple by a negative number
if y < 0:
y = -y # turn y into positive number for loop increment
x = -x # turn x into negative number for result accumulator
# repeat adding x to result y times
result = 0
for i in range( 0, y ):
result += x
return result
53. Arithmetic - Exponent
• Algorithm to implement exponent with only addition
and equality operator.
1. If the exponent is zero, return 1.
2. Initialize accumulators for result and repeat to 0.
3. Loop indefinitely for 'x power e‘
1. If repeat accumulator equals zero, stop.
2. Increment the repeat accumulator by one.
3. Update the result to Mul( result, x ).
Repeaty ->, result = 1
y > 0
result
No
Yes y-- result = Mul(result, x)
54. Arithmetic - Exponent
def Exp( x, y ):
# initialize result to 1
result = 1
# repeat multiplying result by x, y times
for i in range( 0, y ):
result = Mul( result, x )
return result
Does not handle negative exponents.
55. GCD – Iterative Solution
• Euclid’s Algorithm for Greatest Common Denominator
1. Calculate the remainder of dividing the 2nd number by the first number (y % x).
2. Swap the value of the first number (x) with the second number (y).
3. Set the second number (y) to the remainder.
4. Iteratively repeat the process until the 2nd number (y) is zero.
5. Return the value of first number (x) when the 2nd number is zero (y).
Repeatx, y ->
y > 0
x
No
rem = y % x
x = y; y = rem
Yes
56. Arithmetic – GCD Iterative
def GCD( x, y ):
# continue until the division of of the two numbers does not leave a remainder (evenly divisible)
while y > 0:
# calculate the remainder of the division by between x and y
remainder = ( y % x )
# swap the value of x with y
x = y
# set y to the remainder
y = remainder
return x
57. GCD – Recursive Solution
• Euclid’s Algorithm for Greatest Common Denominator
1. When the 2nd number (y) is reduced to zero, return the current value of the first
number (x).
2. Otherwise, swap the first number (x) with the second number (y) and set the 2nd
number (y) to the remainder of the division of the 2nd number by the first number (x % y).
3. Recursively call GCD() with the updated first and second numbers.
GCDx, y ->
y > 0
x
No
Yes rem = y % x
x = y; y = rem
Call
GCD(x,
y)
58. Arithmetic – GCD Recursive
def GCDR( x, y ):
# return the current value of x when y is reduced to zero.
if y == 0:
return x
# recursive call, swapping x with y, and setting y to remainder of y divided by x
return GCDR( y, ( y % x ) )
59. LCM
• Euclid’s Algorithm for Least Common Multiple
1. Multiple the first and second number.
2. Divide the result by the GCD of the two numbers.
LCMx, y ->
x
mul = x * y
Result = mul / GCD(x,y)
62. Bubble Sort
• Bubble Sort Algorithm
1. Make a pass through the list of values.
• Compare adjacent values.
• If the first value is less than the second value, swap the values.
2. If one of more values were swapped, repeat the process of making a pass through the
list.
4 2 1 3
Forward Scan
2 4 1 3 2 1 4 3 2 1 3 4
2 1 3 4 1 2 3 4
Repeat Cycle
1 2 3 4 1 2 3 4
63. Bubble Sort
def BubbleSort( data ):
length = len( data )
swapped = True
# continue to repeat until no more adjacent values are swapped
while swapped:
swapped = False
# Make a scan through the list
for i in range( 0, length - 1 ):
# Compare adjacent values. If the first value > second value, swap the values.
if data[ i ] > data[ i + 1 ]:
swap = data[ i ]
data[ i ] = data[ i + 1 ]
data[ i + 1 ] = swap
swapped = True
return data
Since we are comparing index with
index + 1, we stop one entry short
of the end of the array.
Swap: 1. save the first element in a
temporary variable.
2. Set the first element to the
value of the second element.
3. Set the second element to the
value of the temporary variable
(which was the former value of
first element).
Space Complexity: O(1) – except for swap variable, no extra space needed
Time Complexity : O(n2) – n is number of elements, n scans * n elements.
65. Insertion Sort
• Insertion Sort Algorithm
1. Iterate through the list of elements.
• Scan through the list one element at a time.
• From current element, move backwards to the beginning, swapping adjacent
elements when not sorted.
4 3 2 1
Forward Scan
3 4 2 1 2 3 4 1 1 2 3 4
3 2 4 1Move Backwards 2 3 1 4
2 1 3 4
66. Insertion Sort
def InsertionSort( data ):
length = len( data )
# iterate through the list for each element except the first element
for i in range( 1, length ):
# starting with the current element, remove/insert proceeding
# elements so they are in sorted order
for j in range( i, 0, -1):
# swap adjacent elements
if data[ j ] < data[ j - 1 ]:
temp = data[ j ]
data[ j ] = data[ j -1 ]
data[ j - 1 ] = temp
return data
Start at position 1,
not 0 – nothing to
swap with at first
element.
Space Complexity: O(1) – except for swap variable, no extra space needed
Time Complexity : O(n2) – n is number of elements, n scans * n elements.
Descending count
in for loop. Loop
from ‘n’ to 0 in -1
(descend) steps.
67. Quick Sort
• Quick Sort Algorithm
1. Calculate a midpoint in the list. The value at the midpoint is the pivot value.
2. Move all values in the first partition that are not less than the pivot to the second
partition.
3. Move all values in the second partition that are not greater than or equal to the pivot
to the first partition.
4. Recursively apply the algorithm to the two partitions.
3 2 4 5 18 67
3 21 5 8 674
21 3 4 65 7 8
68. Quick Sort
def QuickSort( data ):
qSort( data, 0, len( data ) - 1 )
return data
def qSort( data, low, high ):
i = low # starting lower index
j = high # starting higher index
mid = int( low + ( high - low ) / 2 ) # midway index
pivot = data[ mid ] # pivot, value at the midway index
# Divide the array into two partitions
while i <= j:
# keep advancing (ascending) the lower index until we find a value that is not less than the pivot
# we will move this value to the right half partition.
while data[ i ] < pivot:
i += 1
# keep advancing (descending) the higher index until we find a value that is not greater than the pivot
# we will move this value to the left half partition.
while data[ j ] > pivot:
j -= 1
# if the lower index has past the higher index, there is no values to swap
# otherwise, swap the values and continue
if i <= j:
# swap the higher than pivot value on the left side with the lower than pivot value on the right side
temp = data[ i ]
data[ i ] = data[ j ]
data[ j ] = temp
# advance the lower and higher indexes accordingly and continue
i += 1
j -= 1
# recursively sort the two partitions if the index has not crossed over the pivot index
if low < j:
qSort( data, low, j )
if i < high:
qSort( data, i, high )
69. Merge Sort
• Merge Sort Algorithm
1. Allocate space for a temporary copy of the array.
2. Recursively split the data into two partitions (halves), until each partition is a
single element.
3. Merge and sort each pair of partitions.
2 3 4 5 61 87
3 42 1 5 768
83 2 4 51 6 7
Split into
halves
38 2 4 15 7 6
Merge halves
and sort
70. Merge Sort
def MergeSort( data ):
# allocate space for a temporary copy of the data
tdata = [ 0 ] * len( data );
# sort the data (pass in the temporary copy so routine is thread safe)
mSort( data, 0, len( data ) - 1, tdata )
return data
def mSort( data, low, high, tdata ):
# if the partition has more than one element, then recursively divide the partition and merge the parts back in
if low < high:
mid = int( low + ( high - low ) / 2 ) # midway index
# sort the lower (first) half partition
mSort( data, low, mid, tdata )
# sort the upper (second) half partition
mSort( data, mid + 1, high, tdata )
# merge the partitions together
merge( data, low, mid, high, tdata )
def merge( data, low, mid, high, tdata ):
# make a temporary copy of the two separately sorted partitions
for i in range( low, high + 1 ):
tdata[ i ] = data[ i ]
# starting from the beginning of the first partition, iteratively search for the next lowest
# number from the lower (first) and higher (second) and move into current position in the
# lower (first) partition
i = low
k = low
j = mid + 1
while i <= mid and j <= high:
if tdata[ i ] <= tdata[ j ]:
data[ k ] = tdata[ i ]
i += 1
else:
data[ k ] = tdata[ j ]
j += 1
k += 1
# Copy any remaining elements back into the first partition
while i <= mid:
data[ k ] = tdata[ i ]
k += 1
i += 1
Logical operators are the
keywords:
and
or
71. Quick / Sort Merge - Complexity
• Merge
Space Complexity: O(n)
Time Complexity : O(n * logn)
• Quick
Space Complexity: O(n)
Time Complexity : O(n * logn)
72. Coding Challenge
• Python Language Constructs Covered:
1. Modulo Operator
2. Squashing into a range
3. Collision Handling
4. Complexity
Hashing
73. Hashing
• Hashing is used to solve indexing lookups without the overhead of
sequential scanning and name comparisons. The most basic use of
hashing is insertion and lookup in a key-value(s) dictionary.
• Common Issues:
1. Handling of collisions when two keys mapped to the same index.
2. Efficiency of the hashing algorithm.
3. The frequency of collisions.
4. The sparseness of the memory allocated for the index.
74. Integer Hash
• Integer Hash Algorithm
1. Map each integer value into a smaller fixed size integer range using the modulo operator.
2. If there is no entry at the index, add the key/value to the index as the first entry in the
chain.
3. If there are entries there, and the key is the same as one of the entries (duplicate),
update the value.
4. If there are entries there, and the key is not the same (collision) as any entry, add the key
to the chain of entries.
0
1
2
3
4
n
***
Range
None (no entry at index)
A
A B
Single Entry (no collision)
Chain of Entries (collisions)
Index = (key % range)
75. Integer Hash (part 1)
class Hash( object ):
RANGE = 0 # the range of the index.
index = [] # the index
# constructor
def __init__( self, range ):
# set the index range and allocate the index
self.RANGE = range
self.index = [None] * self.RANGE
# Map the key into an index within the set range
def Index( self, key ):
return key % self.RANGE
Module operator, returns the
remainder of an integer division.
76. Integer Hash (part 2)
class Hash( object ):
# Add a key/value entry to the index
def Add( self, key, value ):
ix = self.Index( key )
# there is no entry at this index, add the key/value
if self.index[ ix ] is None:
self.index[ ix ] = Entry( key, value )
else:
# See if the key already exists in the chain
next = self.index[ ix ]
while next is not None:
# Entry found, update the value
if next.Compare( key ):
next.Value( value )
break
next = next.Next()
# no entry found, add one
if next is None:
# Add the entry to the front of the chain
add = Entry( key, value )
add.Next( self.index[ ix ] )
self.index[ ix ] = add
Class specific implementation
Of comparing two keys.
77. Integer Hash (part 3)
class Hash( object ):
# Get the value for the key
def Get( self, key ):
ix = self.Index( key )
# See if the key exists in the chain at this entry in the index
next = self.index[ ix ]
while next is not None:
# Entry found, update the value
if next.Compare( key ):
return next.Value()
next = next.Next()
# not found
return None
Time Complexity : O(1) – no collisions (sparse)
O(n) – collisions
78. String (Object) Hash
• String (Object) Hash Algorithm
1. Convert the key to an integer value, which then can be divided by the range to get the
remainder (modulo).
2. In Python, the equivalent is the builtin function hash().
0
1
2
3
4
n
***
Range
None (no entry at index)
A
A B
Single Entry (no collision)
Chain of Entries (collisions)
Index = hash( value )
“my name”
79. Hash Table with Linear Probing
• A hash table without linked lists and instead handle collisions with
linear probing. In this case, we assume that the size of the table will
be greater than or equal to the number of keys.
1. Map each value into a fixed size integer range using a hash function.
2. If there is no entry at the index, add the key/value to the index.
3. If there is an entry and the key matches the entry (duplicate), then update the value.
4. If there is an entry and the key does not match the entry, then probe downward in
a linear fashion.
• If the entry is empty, add the key/value pair to the entry.
• If a key matches (duplicate), then update the entry.
• If the key does not match, continue to the next entry.
100 (index = )
202 (index = 2)
302 (index = 2)
n
***
Range
(e.g., 100)
Probe, 302 collides with 202 (index = 2)
80. Hash Table with Linear Probing
class HashLP( object ):
# Add a key/value entry to the index
def Add( self, key, value ):
# Linear probe the entries for an empty or matching slot.
for ix in range( self.Index( key ), self.RANGE ):
# there is no entry at this index, add the key/value
if self.index[ ix ] is None:
self.index[ ix ] = Entry( key, value )
break
# Entry found, update the value
if self.index[ ix ].Compare( key ):
self.index[ ix ].Value( value )
break
# Get the value for the key
def Get( self, key ):
ix = self.Index( key )
# Linear probe the entries for an empty or matching slot.
for ix in range( self.Index( key ), self.RANGE ):
# there is no entry at this index, return not found
if self.index[ ix ] is None:
return None
# Entry found
if self.index[ ix ].Compare( key ):
return self.index[ ix ].Value();
# not found
return None
81. Coding Challenge
• Python Language Constructs Covered:
1. Ord() builtin function
2. Bitwise-Mask
String Manipulation
82. Reverse String
• Reverse String - Iterative
1. Create a StringBuffer (or character array) to hold the reversed string.
2. Starting at the end of the string and moving backwards, append each character to
the reversed string.
• Reverse String – Recursive
1. If the original string is one character, if so then return the string.
2. Otherwise, break the string into two parts: the first character and the remainder of
the string.
3. Make a recursive call with the remaining string and append the first character to the
return from the call.
S t r i n g
g n i r t S
Backward Scan
Forward Copy
Copy Buffer
Original
Iterative
83. Reverse String
def ReverseString( original ):
reversed = “” # copy for reversed string
length = len( original ) # length of original string
for ix in range( length-1, -1, -1 ):
reversed += original[ ix ]
return reversed
Iterative
def ReverseStringR( original ):
if len( original ) > 1:
return ReverseStringR( original[1:] ) + original[ 0 ]
return original
Recursive
84. Palindrome - Word
• Palindrome - the same word/phrase spelled forward or backwards,
such as: madam, civic, racecar.
• Solution without making a copy of the string.
1. Iterate through the first half of the string. When the string is an odd number of
characters, skip the middle character.
2. On each iteration, compare the current character at the front of the string to the
same position but from the rear of the string.
3. If they do not much, it is not a palindrome.
r a c e c a
Backward ScanForward Scan
middle
r
85. Palindrome - Phrase
• Solve the same for phrases, regardless of the positioning of spaces,
punctuation and capitalizing, such as: My gym.
• Solution without making a copy of the string.
1. Iterate simultaneously from the front (forward) and back (backward) of the string,
until the two iterators meet.
2. If current character of either iterator is a punctuation or space character, then
advance the corresponding iterator.
3. Otherwise, if the lowercase value of the current character from both iterators do
not equal, then it is not a palindrome.
4. Advance both iterators and repeat.
M y g y m
Backward ScanForward Scan
middle
.
m
86. Palindrome
def Palindrome( s ):
length = len( s )
for i in range( 0, int( length / 2 ) ):
if s[ i ] != s[ length - i - 1 ]:
return False
return True
def PalindromeP( s ):
length = len( s )
i = 0
j = length - 1
while i < j:
if isPunctOrSpace( s[ i ] ):
i += 1
continue
if isPunctOrSpace( s[ j ] ):
j -= 1
continue
if s[ i ].lower() != s[ j ].lower():
return False
i += 1
if j != i:
j -= 1
return True
Word
Phrase
Calculate the reverse (backward) distance
Builtin string routine to lowercase a character.
87. Character Count
• Count all occurrences of every character in a string.
1. Create a counter for the 96 ASCII printable characters.
2. Iterate through the string, incrementing the character specific index in the counter
for each character.
r a c e c a
Forward Scan
r
Counter
88. Duplicate Character Count
• Count all occurrences of duplicate characters in a string.
1. Make a pass over the string to generate a character count index.
2. Make a pass over the character count index using a bitwise mask to mask out
single character occurrences (leaving only duplicates with non-zero values).
r a c e c a
Forward Scan
r
a=2 c=2 e=1b=0 d=0 r=2***
Counter
a=1 c=1 e=0b=0 d=0 r=1***
Mask &= ~0x01
Duplicates: Non-Zero value
89. Character and Duplicate Counts
def CharOccur( s ):
counter = [0] * 96 # codes 32 .. 127 are printable (so skip first 32)
length = len(s)
# use counter as an accumulator while we count each character in string
for i in range( 0, length):
counter[ ord( s[ i ] ) - 32 ] += 1 # offset ascii code by 32
return counter
Character Count
Builtin function that returns the ASCII value of the character.
def DupChar( s ):
# Get the character occurrences
dup = CharOccur( s )
# Mask out all single count occurrences
length = len( dup )
for i in range( 0, length):
dup[ i ] &= ~0x01;
return dup
Duplicate Count
A mask is the complement (inverse) of the value to remove