Data Structures and Algorithms examples in JavaScript
How many primitive operations are executed?
Nested loops: O(N^2) time complexity Two separate loops: O(2N) time complexity
Constant time complexity: O(2) AKA O(1)
Constant: O(1)
Logarithmic: O(logN)
Linear: O(N)
Quadratic: O(N^2)
Exponential: O(K^N)
Drop constants, describe it as the highest complexity involved
const arr = [1,2,3]
arr.pop(): Constant
arr[0]: Constant
arr.shift(): Linear
const obj = {a: 1}
obj.a: Constant
Multiply loops inside of loops: O(N^2)
Add separate loops: O(2N)
If every time you loop, you cut your problem by a set amount: O(log N)
A standard linear loop that has another loop performed inside of it cut by a set amount each time: O(Nlog N)
O(1) Running a statement
O(1) Value look-up on an array, object, variable
O(logn) Loop that cuts problem in half every iteration
O(n) Looping through the values of an array
O(n^2) Double nested loops
O(n^3) Triple nested loops
Calculated by determining the size and whether you are creating new objects when your code executes
var countChars = function(str){
var count = 0;
for(var i = 0; i < str.length; i++) {
count++;
}
return count;
};
countChars("dance");
countChars("walk");
Single loop, so the complexity is O(N), where N is the length of str
var countChars = function(str){
return str.length;
};
countChars("dance");
countChars("walk");
// How much more work would it take
// to get the length of 1 million
//char string?
Because the length is a property of the string object, the complexity is O(1). Property lookups are instant.
var myList = ["hello", "hola"];
myList.push("bonjour");
myList.unshift();
myList.shift();
//calculate the time complexity for the
//native methods above (separately)
The push method is constant time, O(1), while shift and unshift are linear time, O(N), because you must modify all elements of the array.
You can save the value of your loop to optimize time complexity at the cost of space complexity. See isUnique.js
Caching the value that a function returns
const factorial = (n) => {
// Calculations: n * (n-1) * (n-2) * ... (2) * (1)
return factorial;
}
factorial(35);
factorial(36); // factorial(36) = factorial(35) * 36
See memoization.js for example
Recursion is when a function calls itself
var callMe = function() {
callMe();
callMe();
callMe('anytime');
};
-
Push called Fn on stack
-
Execute Fn body until...
...another fn is called: Pause the current execution and start at step 1.
...a return is hit: Pop the current Fn off the stack. Resume executing the previous Fn.
var callMyself = function() {
if() {
// base case
return;
} else {
// recursive case
callMyself();
}
return;
};
- Identify base case(s)
- Identify recursive case(s)
- Return where appropriate
- Write procedures for each case that bring you closer to the base case(s)
Recursion can always be implemented as a loop, but in some situations it is simpler to use recursion
- Wrapper functions (wrapper.js)
- Accumulators (accumulator.js)
Search for a value in a sorted array by cutting the side of the search area in half (binarySearch.js)
Search for a value in an array by checking each value in order. (linearSearch.js)
Recursive calls to a subset of the problem 0. Recognize base case
- Divide: Break problem down during each call
- Conquer: Do work on each subset
- Combine: Solutions
Bubble Sort, insertion sort, selection sort
Divide and Conquer Sorts: Recursively divide lists and sort smaller parts of list until entire list is sorted
Mergesort and Quicksort
Loop through an array, comparing adjacent indices and swapping the greater value to the end
Recursively merge sorted sub-lists
The merge step takes two sorted lists and merges them into 1 sorted list
Divide input array into 'n' single element subarrays
Repeatedly merge subarrays and sort on each merge
mergeSort(list)
base case: if list.length < 2, return
break the list into halves L & R
Lsorted = mergeSort(L)
Rsorted = mergeSort(R)
return merge(Lsorted, Rsorted)
mergeSort(list)
initialize n to the length of the list
base case is if n < 2, just return
initialize mid to n/2
left = left slice of array to mid - 1
right = right slice of array mid to n - 1
mergeSort(left)
mergeSort(right)
merge(left, right)
This results in O(N logN) time complexity (bubblesort.js)
Greedy algorithms always make the locally optimal choice (greedyMakeChange.js)
Calculate every single combination possible and keep track of the minimum
Dynamic approach: Cache values to avoid repeated calculations
- Optimal substructure (tends to be recursive)
- Overlapping Subproblems
Top Down (recursive) Vs. Bottom Up (iterative)