As a software developer, it is important to be mindful of different aspects of your code. Such as: how correct the code is, ease of understanding, robustness to handle edge cases, how efficient it is, among other aspects. This last one is what we will focus on in this lesson.
Due to the importance of being able to measure how efficient a program is, specific notation has been developed to measure it. We can measure the best case scenario, the average case scenario and the worst case scenario. In most cases we are most interested in knowing how efficient our program is in a worst case scenario. Why is this? Because we as developers have no way of knowing how others will use our code, nor how it will handle at a much larger scale. We must assume that at some point, a worst case will be encountered and do our best to enable our code to run in even the most strenuous of cases.
There are two parts to complexity we can measure, being time complexity and space complexity. It is important to optimize for both of these as ignoring either might lead to disastrous consequences.
Time complexity measures how long the program takes to complete.
Space complexity measures how much of a systems resources a program takes.
The notation mentioned earlier in regards to worst case scenario is termed Big O notation.
Big O {phonetic: big-oh} represents the upper bound or worst-case scenario of a program.
That's all well and good, but how do we actually figure out what this might be for a program? By going through the program line-by-line, operation-by-operation and computing how long or how much space it takes. For most of this lesson we will focus on time complexity, however do know that everything mentioned holds true for space complexity as well.
To an interpreter, an operation will take differing amounts of time. Some
operations are easier than others. If an operation can be performed in
constant time, that means that no matter what any variables might be in the
operation, it won't affect how long it takes to perform. This would be if
any variables in the operation have fixed size, such as integer or string. For example,
assigning a value to a variable is easy, it can do this in constant time. If
the operation is a conditional, this is also constant time. If an operation
has constant time, then it has big O notation
If we don't know the size of an input, we don't know how long it will take
to perform some operation on it. This will be the case with datatypes such
as array's and dictionaries. In this case, we represent this unknown with a
random variable. A random variable could be something like x
or n. Lets look at a few examples. If the operation is a for
loop over an array, it will need to iterate through the array up
to n times. If the operation is a while loop that breaks when
it finds an element in a dictionary, this may take in the worst
case n when the element is not in the dictionary. In both cases
we may refer to them with big O notation
Now how do we find the big O notation for a program? For each operation we either sum or multiply. If the current operation is independent of the previous, then we sum. If the next operation is dependent on the previous, then we multiply.
def func(vals: list[int]):
a = 2 # 1
res = [] # 2
for v in vals: # 3
if v % a == 0: # 4
res.append(v) # 5
return res # 6
vals = [1,2,3,4]
print(func(vals))
> [2, 4] n. Iterating through this array will take linear
time. Since space has already been allocated for it, iterating through
it won't take any additional space, thus constant space. Time: From this we should sum or multiply our complexity to find what the overall complexities are for this program.
Time:
Space:
Neat, so our program has linear time and linear space complexity. What happened to some of the terms? Why did we approximate at the end? Let's find out in the next lesson!