We have actually covered everything in this course - in this part we’ll do some exercises!
Order of growth of Functions
Let’s find out the complexity () of:
Since we seek the growth rate - we can use our rules about complexity. We can remove all constants:
The next rule we can apply is, “the most dominating factor ‘wins’” as I like to call it. Therefore:
Then we just multiply!
And since we usually write when using Big-O notation:
Now we can say that has a complexity!. Since we are talking about , this means this function has a lower bound of this. This means that also has a complexity of for example.
So what we really mean is that has a complexity.
Suppose an algorithm takes time t on an input of size n. How many times longer does it take on an input of size 10n if…
- If the algorithm is ?
- If the algorithm is ?
- If the algorithm is ?
- If the algorithm is ?
- If the algorithm is ?
This is quite easy! We just plug in our new , and see how much grows!
- If the algorithm is ?
- We get !
- If the algorithm is ?
- We get !
- If the algorithm is ?
- We get !
- If the algorithm is ?
- We get !
- This isn’t just $10 log(10)“ times more - it’s a little bit longer, or so called “logarithmic linear”.
- If the algorithm is ?
- We get !
- This means just a constant more time!
Complexity Analysis
Let’s analyze the following snippet of code and, it’s complexity.
found = falsefor x in list: for y in list: if x + y == 0: found = trueIf we say that the length of list is . In the first loop we will have a complexity of .
The inner loop will follow, using our previous rule of ‘nested loops means multiplication’.
This means our final program will have the complexity of .
Now let’s see over this code snippet:
found = falsefor i in 0 .. n - 1: for j in i .. n - 1: x = list[i] y = list[j] if x + y == 0: found = trueIn this snippet - we’ll have the first loop iterating times - however, the second loop, it will iterate
This means that the number of times the second loop will run is between 1 and times - using our definition of complexity. Let’s call the number of times our loop runs , which means m has a complexity of .
This finally means we have a total complexity of
Now let’s do the same, but for three numbers!
found = falsefor i in 0 .. n - 1: for j in i .. n - 1: for k in j .. n - 1: x = list[i] y = list[j] z = list[k]
if x + y + z == 0: found = trueExactly the same logic goes as from the last question to this, we can prove that each loop has a complexity of .
Which gives the total complexity of .
Now let’s look at a similar program:
pairs_list = []for i in 0 .. n - 1: for j in i .. n - 1: x = list[i] y = list[j] pairs_list.add(x + y)
found = falsefor xplusy in pairs_list: for zplusw in pairs_list: if xplusy + zplusw == 0: found = trueAs we’ve stated above, the first part of the program will have a complexity of .
Note that the add() function takes for dynamic arrays.
However, in the next block, the new array length is , since we have added all possible permutations of pairs. So the loops will now through elements. Which in total results a complexity of .
From our earlier rules, we ‘add’ blocks of codes, so the complexity is . Which means the resulting complexity becomes .
Data Structure Complexities
Let’s refresh our memory and state all the complexities for our data structures and their functions.
- Dynamic Arrays:
- Get/Set:
- Add/Remove at end:
- Add/remove elsewhere:
- Get/Set:
- Stacks/queues:
- Push/Pop:
- Enqueue/Dequeue:
- Push/Pop:
- Binary Heaps:
- Add/RemoveMin (or Max):
- getMin (or Max):
- Add/RemoveMin (or Max):
- BSTs:
- Add/Remove/Search (worst case, meaning it’s already sorted):
- Otherwise:
- Add/Remove/Search (worst case, meaning it’s already sorted):
- Stacks/queues:
- Add/Remove/Search (Always!):
- Add/Remove/Search (Always!):
- Hash Tables:
- Add/Remove/Search (Given that the hash function is ‘good’):
- Add/Remove/Search (Given that the hash function is ‘good’):
- General Tree:
- If you down in a tree, you’ll visit:
- nodes
- If you explore every node, you’ll visit:
- nodes
- A tree has the worst case height.
- A balanced tree is always height.
- If you down in a tree, you’ll visit:
Analyzing more complexities
Let’s take a look at program which utilizes different data structures now:
pairs_list = []for i in 0 .. n - 1: for j in i .. n - 1: x = list[i] y = list[j] pairs_list.add(x + y)
merge_sort(pairs_list)
found = falsefor xplusy in pairs_list: if binary_search(pairs_list, -xplusy): found = trueSo, we’ve seen that first part, we know it’s .
But now we see a merge_sort() - this has a complexity of .
As we stated before, the list after the first block has a length of .
Which means merge_sort() will have a complexity of
This will just sort it so, no length is added.
Then the next block, the for loop will have a complexity of . The binary search algorithm, has a complexity of .
So this block will in total have a complexity of .
So if we add these blocks together and apply our rules we will get a total complexity of: . We can apply some log rules to this:
So finally our answer is,
Let’s now look at a case using a tree:
pairs_set = empty AVL_treefor i in 0 .. n - 1: for j in i .. n - 1: x = pairs_set[i] y = pairs_set[j]
if pairs_set.contains(-(x+y)): return true
pairs_set.add(x+y)As we’ve seen before, the loops are - now the interesting part is the contains() to check if an element is present.
To check whether an element is present in a tree, has a complexity of .
The rest of the operations are constant so, we can ignore them (including the add() for the AVL tree).
So-therefore the final complexity is . In the absolute worst case the contains() will be , but let’s ignore that :).
Now let’s look at a hash table:
pairs_set = empty Hash_tablefor i in 0 .. n - 1: for j in i .. n - 1: x = pairs.set[i] y = pairs_set[j]
if pairs_set.contains(-(x + y)): return true
pairs_set.add(x + y)As per usual, the loops together create a complexity of , now, a search in a hash table is , if our hash function is ‘good’.
The rest of the operations are constant. Therefore, the overall complexity is .
Now let’s look at a BST example:
pairs_set = empty BSTfor i in 0 .. n - 1: for j in i .. n - 1: x = pairs_set[i] y = pairs_set[j]
if pairs_set.contains(-(x + y)): return true
pairs_set.add(x + y)The usual loops :). Now the contains() is the interesting part.
Since this is a BST, the contains() will have a complexity of in the worst case, since the BST can become unbalanced.
The same applies for add(). Since the rest of the operations are constant therefore it will be, .
Different kinds of complexities
We also need to consider the different cases
- Best-case
- This is not useful.
- Worst-case
- This is the most useful.
- Average-case
- Can be useful sometimes, mostly gives us a ‘indicator’.
Let’s now talk about expected and amortised complexity.
Expected Complexity
This is useful for randomized algorithms! It’s the average over all possible random choice for a particular input.
For example, if we choose a random pivot, we turn quicksort from average-case to expected
Amortised Complexity:
Amortised complexity is, the average over any sequence of operations, this is super useful!
For example, we use this to make dynamic arrays have an amortised complexity of .
However, when we’re calculating the total runtime of a program, it’s safe to forget about this amortised bit and just treat each operation as costing .
Conclusion
This was it for this part - and the final part in this series. I really enjoyed this DSA course, super fun :).