In the previous lesson we simplified O(n + 4) to O(n). Why did we do this? If one part of a program takes linear
time and another takes constant time, as the size of the variable grows, the
time spent performing any operation on the variable term will take a
proportionately longer time than anything with a constant. Image n equals
one million. one million plus four is indistinguishable from one million;
thus the larger term consumes the smaller. Thus giving us a complexity of
O(n).
Now how about O(3n)? Lets say n is one thousand. In this case the total
complexity would be three thousand. This is definitely larger than
before. But what if n is one million? Then the total complexity again is
three million. Comparing the two, we see the first value for n is magnitudes
smaller than the latter. This tells us the most important part is the power
of the term.
Larger complexities
Let's compute a few more complexities.
O(n log n + n + 55) ~= O(n log n)
O(n^2 + 50000n) ~= O(n^2)
O(3^n + n^3) ~= O(3^n)
In every case, big O notation retains the term with the largest power.
One function calls another
What should we do when one function calls another? How can we calculate the
total complexity of the original function? We can calculate the complexity
of the called function and take it's worst case time and space complexities,
then substitute them in where it's called in the original function.
Another example
Let's look at a more complex program. In this example we will look at a
simple grid search. The objective of find_abundant_grid is to
find the grid that has the most of some target. We use a helper
function grid_search to iterate through the grid and count how
many match the target.
def grid_search(grid, target):
n, m = len(grid), len(grid[0]) # 4
count = 0 # 5
for i in range(0, n): # 6
for j in range(0, m): # 7
if grid[i][j] == target: # 8
count += 1 # 9
return count # 10
def find_abundant_grid(grids, target):
abundance = 0 # 1
for grid in grids: # 2
abundance = max(grid_search(grid, target), abundance) # 3, 11
return abundance # 12
grid01 = [[0,0,0,1],
[0,0,2,0],
[1,0,0,0]]
grid02 = [[0,0,0,0],
[0,0,2,0],
[0,0,0,0]]
grid03 = [[0,2,0,1],
[0,2,2,0],
[2,2,0,0]]
grids = [grid01, grid02, grid03]
print(fund_abundant_grid(grids, 1))
> 2
Variable assignment. Time: O(1) Space: O(1).
Iterate through an array. Time: O(x) Space: O(1).
Variable assigment and max are both constants. Since a function is
called, we need to step into it to compute it's complexities to see
how it may affect complexities of our program.
Length and width of a grid. Time: O(1) Space: O(1).
Variable assignment. Time: O(1) Space: O(1).
Iteration. Time: O(n) Space: O(1).
Iteration. Time: O(m) Space: O(1).
Conditional. Time: O(1) Space: O(1).
Addition. Time: O(1) Space: O(1).
Return statement. Time: O(1) Space: O(1).
As explained below, grid_search has
quadratic time complexity. Time: O(m*n) Space: O(1).
Return statement. Time: O(1) Space: O(1).
Let's calculate the complexity of grid_search first.
Time: O(1+ 1 + m * n * 1 * 1 + 1) = O(m*n + 3) ~= O(m*n)
Space: O(1)
With this we can substitute in these complexities when it's called in the
main program.
One final note. Because we don't know which term might be the largest, we
can make a further simplification by using a single term in the notation, so
that we can see the program has cubic time complexity.
Summary
Analyzing time complexity helps identify algorithms that are more efficient
for large input sizes and helps make informed decisions when selecting
algorithms for specific tasks.
Analyzing space complexity helps ensure efficient memory utilization and
allows for optimization and trade-offs between time and space requirements.
Both time complexity and space complexity are crucial considerations in
algorithm design and analysis, as they help determine the scalability,
efficiency, and resource usage of algorithms in different scenarios.
Practice question
What are the time and space complexities of the following program?
def transpose(grid):
n, m = len(grid), len(grid[0])
grid_t = [ [ 0 for _ in range(0, n) ] for _ in range(0, m) ]
for i in range(0, n):
for j in range(0, m):
grid_t[j][i] = grid[i][j]
return grid_t