Understanding Program Efficiency

Time vs. Space Efficiency

Want to understand efficiency of programs. But there are challenges in understanding efficiency of solution to a computational problem:

How to evaluate efficiency of programs

Timing a program

Similarly,
time.perf_counter_ns()
reports the current physical time in nanoseconds, as an integer value.

Timing programs is inconsistent

Counting operations

import time
def c2f(c):
  return c*9/5 + 32  # 4 ops

def mysum(x):
  total = 0  # 1 op
  for i in range(x+1):  # loop x+1 times.  1 op for x+1
    total += i  # 2 ops
  return total  # 1 op

#mysum takes 2 + 3(x+1) ops

Counting operations is also inconsistent

Still need a better way

Ideas

Need to choose which input to use to evaluate a function

Different inputs change how the program runs

BEST, AVERAGE, WORST cases

Orders of growth. Goals:

Measuring Order of Growth: Big Oh Notation

Exact steps vs. O():

def fact_iter(n):
  """ assumes n an int >= 0 """
  answer = 1
  while n > 1:  # comparison is 1 step
    answer *= n #2 steps
    n -= 1  #2 steps
  return answer

What does O(n) measure?

Simplification examples

O(n^2): n^2 + 2n + 2
O(n^2): n^2 + 10000000n + 3^10000
O(n): log(n) + n + 4
O(n log(n)): 0.0001*n*log(n) + 300n
O(3^n): 2n^30 + 3^n

Examples of Types of Order of Growth
Order of Growth

Analyzing programs and their complexity

Analyzing programs and their complexity

Complexity Classes

Complexity classes ordered low to high
Complexity classes

Complexity growth
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!

Linear complexity: Simple iterative loop algorithms are typically linear in complexity.

Linear search on unsorted list

def linearSearch(L, e):
  found = False
  for i in range(len(L)):
    if e == L[i]:
      found = True  # can speed up a little by returning True here
                    # but speed up doesn't impact worst case
  return found

Constant-time list access Constant-time list access

Linear search on sorted 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

Linear algorithm: add characters of a string, assumed to be composed of decimal digits

def addDigits(s):
  val = 0
  for c in s:
    val += int(c)
  return val
This is O(len(s)).

Linear complexity example: complexity often depends on the number of iterations

def fact_iter(n):
  prod = 1
  for i in range(1, n+1):
    prod *= i
  return prod

Nested loops

Quadratic complexity: determine if one list is subset of second, i.e., every element of first, appears in second (assume no duplicates)

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

Quadratic complexity example: Find intersection of two lists, return a list with each element appearing only once.

def intersect(L1, L2):
  tmp = []
  for e1 in L1:
    for e2 in L2:
      if e1 == e2:
        tmp.append(e1) #collect all intersections in tmp
        break
  res = []
  for e in tmp:
    if not(e in res):
      res.append(e)  #eliminate duplicates
  return res