1. Big-O Analysis
Big-O analysis is a form of runtime analysis that measures the efficiency of an algorithm in terms of the time it takes for the algorithm to run as a function of the input size. It’s not a formal benchmark, just a simple way to classify algorithms by relative efficiency when dealing with very large input sizes.
1.0.1. How Big-O Analysis Works
In Big-O analysis, input size is assumed to be an unknown value n. In this example, n simply represents the number of elements in an array. In other problems, n may represent the number of nodes in a linked list, the number of bits in a data type, or the number of entries in a hash table. After determining what n means in terms of the input, you must determine how many operations are performed for each of the n input items. “Operation” is a fuzzy word because algorithms differ greatly.
Commonly, an operation is something that a real computer can do in a constant amount of time, like adding an input value to a constant, creating a new input item, or deleting an input value. In Big-O analysis, the times for these operations are all considered equivalent.
In both CompareToMax and CompareToAll, the operation of greatest interest is comparing an array value to another value.
In CompareToMax, each array element was compared once to a maximum value. Thus, the n input items are each examined once, resulting in n examinations. This is considered O(n), usually referred to as linear time: The time required to run the algorithm increases linearly with the number of input items. How to Do Big-O Analysis
The general procedure for Big-O runtime analysis is as follows:
- Figure out what the input is and what n represents.
- Express the number of operations the algorithm performs in terms of n.
- Eliminate all but the highest-order terms.
- Remove all constant factors.
For the algorithms you’ll encounter in interviews, Big-O analysis should be straightforward as long as you correctly identify the operations that are dependent on the input size.
1.0.2. Which is Better?
The performance of most algorithms depends on n, the size of the input. The algorithms can be classified as follows from best-to-worse performance:
- O(log n) — An algorithm is said to be logarithmic if its running time increases logarithmically in proportion to the input size.
- O(n) — A linear algorithm’s running time increases in direct proportion to the input size.
- O(n log n) — A superlinear algorithm is midway between a linear algorithm and a polynomial algorithm.
- O(nc) — A polynomial algorithm grows quickly based on the size of the input.
- O(cn) — An exponential algorithm grows even faster than a polynomial algorithm.
- O(n!) — A factorial algorithm grows the fastest and becomes quickly unusable for even small values of n.
The run times of different orders of algorithms separate rapidly as n gets larger. Consider the run time for each of these algorithm classes with n = 10:
- log 10 = 1
- 10 = 10
- 10 log 10 = 10
- 102 = 100
- 210= 1,024
- 10! = 3,628,800
Now double it to n = 20:
- log 20 = 1.30
- 20 = 20
- 20 log 20= 26.02
- 202 = 400
- 220 = 1,048,576
20! = 2.43×1018
Notation - Name
- O(1) - Constant
- O(log(n)) - Logarithmic
- O((log(n))c) - Poly-logarithmic
- O(n) - Linear
- O(n2) - Quadratic
- O(nc) - Polynomial
- O(cn) - Exponential
1.0.3. Big O(1)
Consider the following function:
function increment(num){
return ++num;
}
console.log(increment(2));
1.0.4. Big O(N)
Now, let's use the sequential search algorithm:
function sequentialSearch(array, item) {
for (var i = 0; i < array.length; i++) {
if (item === array[i]) { //{1}
return i;
}
}
return -1;
}
console.log(sequentialSearch([9, 5, 2, 4, 3, 7, 6], 4));
1.0.5. Big O(N2)
For the O(n2) example, let's use the bubble sort algorithm:
function swap(array, index1, index2) {
let aux = array[index1];
array[index1] = array[index2];
array[index2] = aux;
}
function bubbleSort(array) {
let length = array.length;
for (let i = 0; i < length; i++) { //{1}
for (let j = 0; j < length - 1; j++) { //{2}
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
}
}
}
return array;
}
console.log(bubbleSort([9, 5, 2, 4, 3, 7, 6]));
1.0.6. Big O Performance Comparisons
Order of growth of algorithms specified in Big O notation.
Source: Big O Cheat Sheet
Below is the list of some of the most used Big O notations and their performance comparisons against different sizes of the input data.
Big O Notation | Computations for 10 elements | Computations for 100 elements | Computations for 1000 elements |
---|---|---|---|
O(1) | 1 | 1 | 1 |
O(log N) | 3 | 6 | 9 |
O(N) | 10 | 100 | 1000 |
O(N log N) | 30 | 600 | 9000 |
O(N^2) | 100 | 10000 | 1000000 |
O(2^N) | 1024 | 1.26e+29 | 1.07e+301 |
O(N!) | 3628800 | 9.3e+157 | 4.02e+2567 |
1.0.7. Data Structure Operations Complexity
Data Structure | Access | Search | Insertion | Deletion | Comments |
---|---|---|---|---|---|
Array | 1 | n | n | n | |
Stack | n | n | 1 | 1 | |
Queue | n | n | 1 | 1 | |
Linked List | n | n | 1 | 1 | |
Hash Table | - | n | n | n | In case of perfect hash function costs would be O(1) |
Binary Search Tree | n | n | n | n | In case of balanced tree costs would be O(log(n)) |
B-Tree | log(n) | log(n) | log(n) | log(n) | |
Red-Black Tree | log(n) | log(n) | log(n) | log(n) | |
AVL Tree | log(n) | log(n) | log(n) | log(n) | |
Bloom Filter | - | 1 | 1 | - | False positives are possible while searching |
1.0.8. Array Sorting Algorithms Complexity
Name | Best | Average | Worst | Memory | Stable | Comments |
---|---|---|---|---|---|---|
Bubble sort | n | n2 | n2 | 1 | Yes | |
Insertion sort | n | n2 | n2 | 1 | Yes | |
Selection sort | n2 | n2 | n2 | 1 | No | |
Heap sort | n log(n) | n log(n) | n log(n) | 1 | No | |
Merge sort | n log(n) | n log(n) | n log(n) | n | Yes | |
Quick sort | n log(n) | n log(n) | n2 | log(n) | No | Quicksort is usually done in-place with O(log(n)) stack space |
Shell sort | n log(n) | depends on gap sequence | n (log(n))2 | 1 | No | |
Counting sort | n + r | n + r | n + r | n + r | Yes | r - biggest number in array |
Radix sort | n * k | n * k | n * k | n + k | Yes | k - length of longest key |
2. Searching
2.1. Binary search
In computer science, binary search, also known as half-interval search, logarithmic search, or binary chop, is a search algorithm that finds the position of a target value within a sorted array. Binary search compares the target value to the middle element of the array; if they are unequal, the half in which the target cannot lie is eliminated and the search continues on the remaining half until it is successful. If the search ends with the remaining half being empty, the target is not in the array.
Here's how algorithm works.
- At every step, consider the array between
low
andhigh
indices - Calculate the
mid
index. - If element at the
mid
index is the key, returnmid
. - If element at
mid
is greater than thekey
, then reduce the array size such thathigh
becomesmid - 1
. - Index at low remains the same. - If element at
mid
is less than thekey
, then reduce the array size such thatlow
becomesmid + 1
. Index at high remains the same. - When
low
is greater thanhigh
, key doesn't exist. Return -1.
Time Complexity: O(log(n))
- since we split search area by two for every
next iteration.
function binarySearch(arr, val) {
let lower_bound = 0;
let upper_bound = arr.length - 1;
while (lower_bound <= upper_bound) {
let midpoint = Math.floor(upper_bound + lower_bound / 2);
let value_at_midpoint = arr[midpoint];
console.log(midpoint, value_at_midpoint);
if (val < value_at_midpoint) {
upper_bound = midpoint - 1;
} else if (val > value_at_midpoint) {
lower_bound = midpoint + 1;
} else if (val == value_at_midpoint) {
return midpoint;
}
}
return null;
}
console.log(binarySearch([1, 3, 4, 5, 6, 21, 43, 54, 86], 21));
console.log(binarySearch([1, 3, 4, 5, 6, 21, 43, 54, 86], 1));
let array_for_binary_search = [10, 20, 47, 59, 63, 75, 88, 99, 107, 120, 133, 155, 162, 176, 188, 199, 200, 210, 222];
console.log("Key(47) found at: " + binarySearch(array_for_binary_search, 47));
console.log("Key(75) found at: " + binarySearch(array_for_binary_search, 75));
console.log("Key(175) found at: " + binarySearch(array_for_binary_search, 175));
Iterative Example
let binary_search_iterative = function(a, key) {
let low = 0;
let high = a.length - 1;
while (low <= high) {
let mid = low + Math.floor((high - low) / 2);
if (a[mid] === key) {
return mid;
}
if (key < a[mid]) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
};
let array_for_binary_search = [10, 20, 47, 59, 63, 75, 88, 99, 107, 120, 133, 155, 162, 176, 188, 199, 200, 210, 222];
console.log("Key(47) found at: " + binary_search_iterative(array_for_binary_search, 47));
console.log("Key(75) found at: " + binary_search_iterative(array_for_binary_search, 75));
console.log("Key(175) found at: " + binary_search_iterative(array_for_binary_search, 175));