Recap

Why do we want to understand the efficiency of programs?

Goals:

Complexity classes in increasing order:

Complexity classes
Class n=10 n=100 n=1000 n=1000000
O(1) 1 1 1 1
O(log n) 1 2 3 6
O(n) 10 100 1000 1000000
O(n log(n)) 10 200 3000 6000000
O(n^2) 100 10000 1000000 1000000000000
O(2^n) 1024 1.2*10^31 1.07*10^311 forget it!

Complexity classes in more detail

Constant complexity class

Logarithmic complexity class

Searching in a list

Recursive Big-O

Below is the "pseudocode" for finding BigO of a non-recursive function. Note that this is not real code; this is to show the recursive nature of BigO

Self-similarity: find BigO of smaller code blocks and combine them The BigO pseudocode does not cover function calls and some other cases (to keep the discussion simple) --- though experiment to expand this

findBigO(codeSnippet):
  if codeSnippet is a single statement:
    return O(1)
  if codeSnippet is a loop:
    return number_of_times_loop_runs * findBigO(loop_inside)
  for codeBlock in codeSnippet:
    return the_sum_of findBigO(codeBlock)

Binary search

  1. Interested in search for element e in list L
  2. Pick an index i that divides list in half
  3. Ask if L[i] == e
  4. If not, ask if L[i] is larger or smaller than e
  5. Depending on answer, search left or right half of L for e.
  6. Answer to smaller problem is the answer to the original problem

Binary search complexity analysis
Binary search complexity analysis

Binary search implementation 1

def binsearch1(L, e):
  if L == []:             # this check and return is constant-time-complexity, or O(1)
    return False    
  elif len(L) == 1:       # this check is also O(1)
    return L[0] == e      # this check is also O(1)
  else:
    half = len(L)//2      # constant O(1)
    if L[half] > e:    # Is L[half] O(1)?  Let's assume for now it is.
      return binsearch1(L[:half], e)   # involves copying the list, not constant
    else:
      return binsearch1(L[half:], e)   # involves copying the list, not constant
      

Complexity of binsearch1:

A better implementation of binary search

Binary search without copying
def binsearch_helper(L, e, low, high):
  if high == low:
    return L[low] == e
  mid = (low + high)//2
  if L[mid] == e:
    return True
  elif L[mid] > e:
    if low == mid:   #nothing left to search
      return False
    else:
      return binsearch_helper(L, e, low, mid - 1)    #constant to set up the call.  The actual call takes longer
  else:
    return binsearch_helper(L, e, mid + 1, high)  #constant to set up the call.  The actual call takes longer

def binsearch(L, e):
  if len(L) == 0:
    return False
  else:
    return binsearch_helper(L, e, 0, len(L) - 1)  #constant to set up the call.  The actual call takes longer

Complexity of binsearch:

Another example of logarithmic complexity: int2str

def int2str(n):
  digits = '0123456789'
  if n == 0:
    return '0'
  result = ''
  i = n
  while i > 0:
    result = digits[i%10] + result
    i = i//10
    # can you place an assertion here?
  return result
What is the correctness argument? What assertion can you put at the end of the body of the while loop? Ans: assert(10**(len(result))*i+int(result)==n). This is an invariant.

Analyzing the complexity of the program above:

How can you change the implementation so it takes O(log n) time?

Linear complexity

Analyzing the complexity of an iterative implementation of the factorial function

Analyzing the complexity of a recursive implementation of the factorial function

def fact_rec(n):
  """ assume n >= 0 """
  if n <= 0:
    return 1
  else:
    return n*fact_rec(n-1)

Log-linear O(n log n) complexity

Polynomial complexity

Exponential complexity

Complexity of Towers of Hanoi

Exponential complexity example: Power set

Power set --- concept

Exponential complexity

def genSubsets(L):
  res = []
  if len(L) == 0:
    return [[]]  # list of empty list
  smaller = genSubsets(L[:-1]) #all elements without last element
  extra = L[-1:]
  new = []
  for small in smaller:
    new.append(small+extra)  # for all smaller solutions, add one with last element
  return smaller+new  # combine those with last element and those without
Assumptions: Method: Let's analyze:

Summary of examples of complexity classes

Example: Iterative Fibonacci

def fib_iter(n):
  if n == 0:    # O(1)
    return 0    # O(1)
  elif n == 1:  # O(1)
    return 1    # O(1)
  else:         # O(1)
    fib_i = 0   # O(1)
    fib_ii = 1  # O(1)
    for i in range(n-1):      # the loop executes O(n) iterations
      tmp = fib_i             # O(1)
      fib_i = fib_ii          # O(1)
      fib_ii = tmp + fib_ii   # O(1)
    return fib_ii             # O(1)
Analysis:

Example: Recursive Fibonacci

def fib_rec(n):
  """ assumes n an int >= 0 """
  if n == 0:    # O(1)
    return 0    # O(1)
  elif n == 1:  # O(1)
    return 1    # O(1)
  else:         # O(1)
    return fib_rec(n-1) + fib_rec(n-2)
Draw tree. Show the growth for small values of n. Claim (without proof) that the complexity is exponential, slightly lower than 2n.

Big-O summary

Complexity of common Python functions

Lists: n is len(L)

Dictionaries: n is len(d)