Goals:
Complexity classes in increasing order:
 
| 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! | 
Constant complexity class
Logarithmic complexity class
Searching in a list
def search(L, e):
  for i in range(len(L)):
    if L[i] == e:
      return True
    if L[i] > e:
      return False
  return False
This method is called a sequential search.
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)
for i in range(N * N):
  for j in range(3, 219):
    print("sum: ", i + j)
print("Enjoy COL100");
Show the working of findBigO on this code snippet.
Binary search
e in list LL[i] == eL[i] is larger or smaller than eL for e.Binary search complexity analysis
 
log n.O(log n) where n is len(L).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:
O(log n) calls to binsearch1
n, in worst case in case of range of size 1 when n/2k=1, or k=log nO(n) for each binsearch1 call to copy list:
O(log n)*O(n) indicates that the complexity can be as high as O(n log n).O(n/2i). Thus, the total cost of copying is n + n/2 + n/22 + ... + 1, which is equal to 2n.A better implementation of binary search
n=len(L). 
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:
O(log n) calls to binsearch_helper
n, in worst case in case of range of size 1 when n/2k=1, or k=log nO(1) for each binsearch_helper execution (including the cost to set up the call)O(log n)*O(1) indicates that the complexity is O(log n).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:
n by 10 such that it remains greater than 0?O(log(n))O(1)O(len(result)) as it involves a copy, which is O(log n) in the worst case and average case.O(log n).O(log2 n).How can you change the implementation so it takes O(log n) time?
result be a list, initialized to [] and appended-to using the append() function (which is O(1))O(len(result))) and join it to form a string (again O(len(result))).Linear complexity
Analyzing the complexity of an iterative implementation of the factorial function
def fact_iter(n):
  prod = 1
  for i in range(1, n+1):
    prod *= i
  return prod
O(n). n times around loop, constant time each time.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)
O(n) because the number of function calls is linear in n, and constant effort to set up call.Log-linear O(n log n) complexity
Polynomial complexity
isSubset function:
def isSubset(L1, L2):
  for e1 in L1:
    matched = False
    for e2 in L2:
      if e1 == e2:
        matched = True
        break
    if not matched:
      return False
  return True
Exponential complexity
Complexity of Towers of Hanoi
tn denote time to solve tower of size n.tn = 2tn-1 + 1 = 2(2tn-2 + 1) + 1 = 4tn-2+ 2 + 14tn-2+ 2 + 1 = 4(2tn-3+1)+2 + 1 = 8tn-3 + 4 + 2 + 1k expansions: 2ktn-k+2k-1 + 2k-2 + ... + 4 + 2 + 1n-k=1: 2n-1 + 2n-2 + 2n-3 + ... + 4 + 2 + 12n-1.2n.Exponential complexity example: Power set
{1, 2, 3, 4} would generate (order does not matter):
{}, {1}, {2}, {1,3}, {1,4}, {2,3}, {2,4}, {3,4}, {3}, {4}, {1,2}, {1,2,3}, {1,2,4}, {1,3,4}, {2,3,4}, {1,2,3,4}
Power set --- concept
n); and all of those subsets with n
added to each of them belong to the bigger power set.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:
append() is constant timesmall+extra can be O(n) time.k, the solution is of size 2k.tn denote time to solve problem of size n.sn denote size of solution to a problem of size n.tn=tn-1+c3*n*sn-1+c1*sn-1+c2 (where c1,c2,c3 are some constants)tn>=tn-1+c1*2n-1+c2>=tn-2+c1*2n-2+c1*2n-1+2*c2>=tn-k+c1*2n-k+c1*2n-k-1+...+2n-1+k*c2>=t0+c1*20+c1*21+...+c1*2n-1+n*c2>=1+c1*2n+n*c2>O(2n)Summary of examples of complexity classes
O(1): code does not depend on size of the inputO(log n): reduce problem in half with constant effort at each stepO(n): simple iterative or recursive programs with a single loop or linear recursionO(nlog n): will see next timeO(nc): nested loops or recursive callsO(cn): multiple recursive calls at each level, where the number of levels is linear in nExample: 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:
O(1)c1*O(1) + O(n)*c2*O(1) = O(n)
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
Lists: n is len(L)
Dictionaries: n is len(d)