TIME AND SPACE COMPLEXITY
Big O is about growth, not actual time or space
/* Let's start out easy */
function foo (arr) {
let sum = 0
let product = 1
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
for (let j = 0; j < arr.length; j++) {
product *= arr[i]
}
console.log(sum * product)
}
/* Piece of cake */
function foo (arr) {
let sum = 0 // O(1)
let product = 1 // O(1)
for (let i = 0; i < arr.length; i++) { // O(arr)
sum += arr[i] // O(1)
}
for (let j = 0; j < arr.length; j++) { // O(arr)
product *= arr[i] // O(1)
}
console.log(sum * product); // O(1)
}
// O(1) + O(1) + (O(arr) * O(1)) + (O(arr) * O(1)) + O(1)
// O(3 + 2arr) => O(arr) => O(n)
/*
- Measure the complexity of the algorithm at each and every step.
Ask yourself: would this change if the input got larger?
- Be concrete: you don't need to use 'n', 'm' etc if you don't want to.
Use the name of the inputs of the algorithm, or whatever makes the most sense to you!
- Add the complexity for each line at the same level of indentation.
Multiply inner levels by their outer levels.
- When you've reduced as far as you can, drop everything but the largest term!
*/
/* Another softball */
function bar (arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length; j++) {
console.log(arr[i] + arr[j])
}
}
}
/* All good! */
function bar (arr) {
for (let i = 0; i < arr.length; i++) { // O(arr)
for (let j = 0; j < arr.length; j++) { // O(arr)
console.log(arr[i] + arr[j]) // O(1)
}
}
}
// O(arr) * O(arr) * O(1) => O(arr^2) => O(n^2)
/* Don't worry, you got this */
function baz (arrA, arrB) {
for (let i = 0; i < arrA.length; i++) {
for (let j = 0; j < arrB.length; j++) {
console.log(arrA[i] + arrB[j])
}
}
}
/* See that wasn't so bad */
function baz (arrA, arrB) {
for (let i = 0; i < arrA.length; i++) { // O(arrA)
for (let j = 0; j < arrB.length; j++) { // O(arrB)
console.log(arrA[i] + arrB[j]) // O(1)
}
}
}
// O(arrA) * O(arrB) * O(1) => O(nm)
/*
7
/ \
4 9
/ \ / \
1 6 8 12
Seven elements altogether
Three levels deep
For an algorithm that visits each node:
O(2^log(n)) ==> MATH ==> O(n)
*/
/* Let's take it to the limit! */
function fib (n) {
if (n === 1 || n === 0) return n;
else return fib(n - 1) + fib(n - 2);
}
/* This is why fib is so slow! */
/*
fib(4)
/ \
fib(3) fib(2)
/ \ / \
fib(2) fib(1) fib(1) fib(0)
/ \
fib(1) fib(0)
our input is equal to 4: n = 4
we go four levels deep, so depth = n
we branch twice with each recursive call
therefore, runtime is O(2^n)!
*/
/* Let's get the memo! */
function fib (n, memo = {}) {
if (n === 1 || n === 0) return n;
else if (memo[n]) return memo[n];
else memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
return memo[n];
}
/* Such quicker, much dynamic, wow! */
/*
fib(4)
/ \
fib(3) fib(2)
/ \ / \
fib(2) fib(1) fib(1) fib(0)
/ \
fib(1) fib(0)
1. fib(4) = fib(3) + fib(2)
/
2. fib(3) = fib(2) + fib(1)
/
3. fib(2) = fib(1) + fib(0) = memo[2] at this point, we've had to do O(n) work
4. fib(3) = memo[2] + fib(1) = memo[3] but now, every calculation is constant time!
5. fib(4) = memo[3] + fib(2) => memo[3] + memo[2]
*/
/*
That entire second branch got taken out of the picture!
Every step after we reach the bottom of the tree is O(1), thanks to the memo!
Using a memo cuts runtime down to O(n)!
*/
/* Memoized fibonacci revisited */
function fib (n, memo) {
if (!memo) var memo = {};
if (n === 1 || n === 0) return n;
else if (memo[n]) return memo[n];
else memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
return memo[n];
}
/*
Call Stack:
fib(4) fib(4) fib(4) fib(4) fib(4) fib(4)
fib(3) fib(3) fib(3) fib(3) fib(3) etc...
fib(2) fib(2) fib(2) fib(1)
fib(1) >> >> fib(0) >> >> >>
This is a lot quicker than the non-memoized version, but remember that it's still recursive,
so we will eventually have n calls on the call stack. We also have the memo,
but it ends up not mattering much. It will always contain a little less than n items.
Therefore, space complexity is O(n + n-ish) => O(n)
...which is the same as the non-memoized version!
*/
/* Sorting an array of strings */
function sortedStrings (arr) {
for (let i = 0; i < arr.length; i++) {
arr[i].sort(); // Let's say that .sort is O(n log n)
}
}
// Fun fact: different browsers have different implementations for Array.prototype.sort!
/* Sorting an array of strings */
/* The key to understanding this is that we have two algorithms with two different inputs
sortedStrings takes an array with an array input, with a length of say 'n'
Array.prototype.sort takes a string input, with a length of say 's'
*/
function sortedStrings (arr) {
for (let i = 0; i < arr.length; i++) { // O(n)
arr[i].sort(); // O(s log s)
}
}
// O(n) * O(s log s) => O(n * s log s)