Understanding Time Complexity in JavaScript
Time complexity is the total time taken by an algorithm to run as a function of the length of the input. It's an important concept to evaluate the efficiency of algorithms.
Types of Time Complexities
Best Case (Ω, Omega)
The best-case scenario is the minimum time required for the algorithm to complete. It represents the lower bound of the algorithm.
Average Case (Θ, Theta)
The average case represents the expected time an algorithm would take to complete, considering all possible inputs.
Worst Case (O, Big O)
The worst-case scenario is the maximum time required for the algorithm to complete. It represents the upper bound of the algorithm.
Common Time Complexities
- O(1): Constant time
- O(log n): Logarithmic time
- O(√n): Square root time
- O(n): Linear time
- O(n*m): Bilinear Time
- O(log₂n): Logarithmic time
- O(n log n): Linearithmic time
- O(n²): Quadratic time
- O(n³): Cubic time
- O(2ⁿ): Exponential time
O(1): Constant Time
An algorithm with O(1)
complexity takes the same amount of time regardless of the input size.
function printConstant(n) {
for (let i = 1; i <= 10; i++) {
console.log('Print');
}
}
Explanation:
- Input: None (the function runs a fixed number of times regardless of input size)
- Operation: The loop runs a fixed 10 times
- Reason for Time Complexity: Since the number of iterations is constant and does not depend on the input size, the time complexity is
O(1)
. Here, the loop runs a fixed 10 times, no matter the input sizen
. Therefore, the time complexity isO(1)
.
function print(n) { // n input
console.log('Hello'); // Single operation
}
Explanation:
- Input: None (the function runs once regardless of input size)
- Operation: A single console log operation
- Reason for Time Complexity: This function has a single operation which runs once, regardless of the input size n. Thus, its time complexity is O(1).
console.log(n*(n+1)/2);
O(n): Linear Time
An algorithm with O(n) complexity grows directly with the input size.
function printNumber(n) {
for (let i = 1; i <= n; i++) { // The loop runs n times
console.log(i); // Operation inside loop
}
}
Explanation:
- Input:
n
(the size of the input) - Operation: The loop runs
n
times, printing each number from1
ton
- Reason for Time Complexity: The loop runs
n
times, and the time taken grows linearly withn
. Hence, the time complexity isO(n)
. Here, the loop runsn
times, and the time taken grows linearly withn
. Hence, the time complexity isO(n)
.
O(n*m): Bilinear Time
An algorithm with O(n*m) complexity grows proportionally with the product of two inputs.
function printNM(n, m) {
for (let i = 1; i <= n; i++) { // Outer loop runs n times
for (let j = 1; j <= m; j++) { // Inner loop runs m times for each iteration of the outer loop
console.log(n, m); // Operation inside inner loop
}
}
}
Explanation:
- Input: n and m (the sizes of the two inputs)
- Operation: The outer loop runs n times, and for each iteration of the outer loop, the inner loop runs m times.
- Reason for Time Complexity: The outer loop runs n times, and for each iteration of the outer loop, the inner loop runs m times. Therefore, the total number of operations is n _ m, resulting in O(n _ m) time complexity.
O(log n): Logarithmic Time
An algorithm with O(log n)
complexity grows slowly as the input size increases.
function repeatedDivision(n) {
while (n > 1) {
n = Math.floor(n / 2); // The value of n is halved each iteration
console.log(n); // Operation inside loop
}
}
function repeatedLogarithmicPrint(n) {
for (let i = 1; i <= n; i = i * 2) { // The value of i is doubled each iteration
console.log('Print'); // Operation inside loop
}
}
Explanation:
- Input:
n
(the size of the input) - Operation: The loop starts with
i
initialized to1
, and in each iteration,i
is multiplied by2
. This means thati
takes the values 1, 2, 4, 8, 16, 32, ... until it exceedsn
. - Reason for Time Complexity: The number of iterations is proportional to the logarithm of
n (base 2)
. This is because each iteration effectively halves the problem size (in terms of the remaining values ofi
that need to be processed). Therefore, the time complexity isO(log₂n)
.In Big O notation, O(log n) and O(log₂n) essentially represent the same class of complexity.
for (let i = n; i >= 1; i = Math.floor(i / 2)) {
console.log(i);
}
Explanation:
- Input:
n
(the size of the input) - Operation: The loop runs while
n
is greater than1
, halvingn
each time - Reason for Time Complexity: In each iteration,
n
is halved, so the number of iterations is logarithmic relative ton
. Thus, the time complexity isO(log n)
. In this example,n
is halved each time, so the number of iterations is logarithmic relative ton
. Thus, the time complexity isO(log n)
.
function printSquareRoot(n) {
while (n > 1) {
n = Math.sqrt(n); // The value of n is updated to its square root
console.log(n); // Print the updated value of n
}
}
Explanation:
- Input:
n
(the size of the input) - Operation: The function repeatedly updates
n
to its square root and prints it - Reason for Time Complexity: In each iteration,
n
is replaced with its square root. The number of iterations required to reducen
to a value less than or equal to1
grows logarithmically with respect to n. Hence, the time complexity isO(log₂(log₂n))
, but it’s often simplified toO(log n)
in discussions.
O(√n): Square Root Time
An algorithm with O(√n) complexity grows with the square root of the input size.
function printFactors(n) {
for (let i = 1; i * i <= n; i++) { // Loop runs approximately √n times
if (n % i === 0) { // Check if i is a factor of n
console.log(i); // Print factor
if (i !== n / i) { // Print complementary factor
console.log(n / i);
}
}
}
}
Explanation:
- Input:
n
(the size of the input) - Operation: The loop runs from
1
to the square root ofn
, checking ifi
is a factor ofn
and printing it - Reason for Time Complexity: The loop runs approximately √n times because i goes up to the square root of n. Hence, the time complexity is
O(√n)
.
O(log₂n): Logarithmic Time
Logarithmic time complexity, O(log₂n)
, describes an algorithm that reduces the problem size by a constant fraction each step. This type of complexity is typical for algorithms that divide the input in half every time.
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
return mid; // Target found
} else if (arr[mid] < target) {
left = mid + 1; // Search in the right half
} else {
right = mid - 1; // Search in the left half
}
}
return -1; // Target not found
}
Explanation:
- Input:
arr
(the array to search),target
(the value to search for) - Operation: The loop divides the search interval in half each time, checking if the target is in the left or right half
- Reason for Time Complexity: Binary search reduces the problem size by half in each iteration. Therefore, the time complexity is
O(log₂n)
. Binary search is a classic example of an algorithm withO(log₂n)
complexity. It works by repeatedly dividing the search interval in half.
O(n log n): Linearithmic Time
An algorithm with O(n log n)
complexity is a bit more than linear but not too much.
function sumAllPairsLogStep(n) {
let sum = 0;
for (let i = 1; i <= n; i++) { // Outer loop runs n times
for (let j = i; j <= n; j *= 2) { // Inner loop runs log(n) times
sum += i * j; // Operation inside inner loop
}
}
return sum;
}
Explanation:
- Input:
n
(the size of the input) - Operation: The outer loop runs
n
times and the inner loop runslog(n)
times for each iteration of the outer loop - Reason for Time Complexity: The outer loop runs n times and the inner loop runs
log(n)
times for each iteration of the outer loop. Therefore, the time complexity isO(n log n)
. The outer loop runsn
times and the inner loop runslog(n)
times for each iteration of the outer loop. Therefore, the time complexity isO(n log n)
.
O(n²): Quadratic Time
An algorithm with O(n²)
complexity grows much faster than the input size.
function bubbleSort(arr) {
for (let i = 0; i < arr.length; i++) { // Outer loop runs n times
for (let j = 0; j < arr.length - i - 1; j++) { // Inner loop runs n times for each outer loop iteration
if (arr[j] > arr[j + 1]) { // Compare adjacent elements
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // Swap if out of order
}
}
}
return arr;
}
Explanation:
- Input:
arr
(the array to sort) - Operation: The outer loop runs
n
times, and the inner loop runs(n-i-1)
times for each iteration of the outer loop, performing comparisons and swaps if necessary. - Reason for Time Complexity: The outer loop runs
n
times and the inner loop runsn
times for each iteration of the outer loop, resulting inO(n²)
time complexity.
function printPattern(n) {
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= i * 2; j++) { // Inner loop runs 2*i times for each iteration of the outer loop
console.log('Print');
}
}
}
Explanation:
- Input:
n
(the size of the input) - Operation: The outer loop runs
n
times, and the inner loop runs2*i
times for each iteration of the outer loop - Reason for Time Complexity: The number of operations performed by the inner loop grows linearly with
i
for each iteration of the outer loop. Summing up these operations over all iterations of the outer loop results in a quadratic growth with respect ton
. Hence, the time complexity isO(n²)
.
O(n² √n): Quadratic Square Root Time
An algorithm with O(n² √n) complexity grows with the square of the input size, multiplied by the square root of the input size.
function tripleNestedLoopSquareRootStep(n) { // n input
for (let i = 1; i <= n; i++) { // Outer loop runs n times
for (let j = 1; j <= n; j++) { // Middle loop runs n times
for (let k = 1; k * k <= n; k++) { // Inner loop runs approximately √n times
console.log(i, j, k);
}
}
}
}
Explanation:
- Input: n (the size of the input)
- Operation: The outer loop and middle loop each run n times, while the inner loop runs approximately √n times
- Reason for Time Complexity: The outer and middle loops each run n times, and the inner loop runs approximately √n times. Therefore, the time complexity is O(n² √n).
O(n³): Cubic Time
An algorithm with O(n³) complexity grows even faster than quadratic time.
function printComplexPatterns(n) {
for (let i = 1; i <= n; i++) { // Outer loop: runs n times
for (let j = 1; j <= i * 2; j++) { // Middle loop: runs i * 2 times
for (let k = 1; k <= n / 2; k++) { // Inner loop: runs n / 2 times
console.log('Print');
}
}
}
}
Explanation:
- Input:
n
(the size of the input) - Operation: The outer loop runs
n
times. For each iteration of the outer loop, the middle loop runs up to2 * i
times (which sums toO(n²)
over all iterations). The inner loop runsn / 2
times for each iteration of the middle loop (simplified toO(n)
). - Reason for Time Complexity: The outer loop runs
n
times, the middle loop runsO(n²)
times in total, and the inner loop runsO(n)
times for each iteration of the middle loop. Thus, the overall time complexity isO(n³)
.
function tripleNestedLoop(n) {
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= n; j++) {
for (let k = 1; k <= n; k++) {
console.log(i, j, k);
}
}
}
}
Explanation:
- Input:
n
(the size of the input) - Operation: The outer loop runs
n
times, the middle loop runsn
times for each iteration of the outer loop, and the inner loop runsn
times for each iteration of the middle loop. - Reason for Time Complexity: The outer loop runs
n
times, the middle loop runsn
times for each iteration of the outer loop, and the inner loop runsn
times for each iteration of the middle loop. Therefore, the time complexity isO(n³)
.
function printNestedLoops(n) { // Input: `n`
for (let i = 1; i <= n; i++) { // Outer loop: Runs `n` times, iterating over each value from 1 to `n`.
for (let j = 1; j <= i * i; j++) { // Inner loop: For each value of `i`, runs `i * i` times (i.e., `i` squared).
console.log('Print');
}
}
}
Explanation:
- Input:
n
(the size of the input) - Operation: The outer loop runs
n
times, and for each iteration of the outer loop, the inner loop runsi²
times, where i is the current value of the outer loop variable - Reason for Time Complexity: The inner loop runs up to
i²
times for each iteration of the outer loop, resulting in a total time complexity ofO(n³)
, since the total number of operations is proportional to the sum of squares from1
ton
.
function sumPairsWithLogLoop(n) {
let sum = 0;
for (let i = 1; i <= n; i++) { // Outer loop runs n times
for (let j = 1; j <= n; j++) { // Middle loop runs n times for each outer loop iteration
for (let k = j; k <= n; k *= 2) { // Inner loop runs log(n) times for each middle loop iteration
sum += i + j + k; // Accumulate the sum of i, j, and k
}
}
}
return sum;
}
Explanation:
- Input: n (the size of the input)
- Operation: The outer loop runs
n
times, the middle loop runsn
times for each iteration of the outer loop, and the inner loop runs approximatelylog(n)
times for each iteration of the middle loop (due to the multiplication by 2). - Reason for Time Complexity: The outer and middle loops each run
n
times, leading to a cubic time complexity. The inner loop runslog(n)
times, which affects the multiplicative constant, but the dominant term remainsO(n³)
.
function printAllPairs(n) {
for (let i = 1; i <= n; i++) { // Outer loop runs n times
for (let j = 1; j <= n; j++) { // Inner loop runs n times for each outer loop iteration
console.log(i, j); // Print pair (i, j)
}
}
}
Explanation:
- Input:
n
(the size of the input) - Operation: The outer loop runs
n
times, and for each iteration of the outer loop, the inner loop also runsn
times. - Reason for Time Complexity: The outer loop and the inner loop each run
n
times, resulting inn * n
operations. Thus, the time complexity isO(n²)
. In this example, the outer loop runsn
times and the inner loop runsn
times for each iteration of the outer loop, leading toO(n³)
time complexity.
O(n √n): Square Root Linear Time
An algorithm with O(n √n) complexity grows with the square root of the input size, multiplied by the linear factor.
function printPairsSquareRootStep(n) {
for (let i = 1; i <= n; i++) { // Outer loop runs n times
for (let j = 1; j * j <= n; j++) { // Inner loop runs approximately √n times
console.log(i, j); // Operation inside inner loop
}
}
}
Explanation:
- Input:
n
(the size of the input) - Operation: The outer loop runs
n
times, and the inner loop runs whilej * j
is less than or equal ton
, which is approximately√n
times. - Reason for Time Complexity: The inner loop runs approximately
√n
times for each iteration of the outer loop. Therefore, the overall time complexity isO(n √n)
.
O(2ⁿ): Exponential Time
An algorithm with O(2ⁿ) complexity grows extremely quickly.
function fibonacci(n) {
if (n <= 1) { // Base case
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2); // Recursive calls
}
Explanation:
- Input:
n
(the size of the input) - Operation: The function calls itself twice for each value of
n
, resulting in an exponential number of calls - Reason for Time Complexity: The function makes two recursive calls for each value of
n
, resulting in an exponential growth of function calls. Hence, the time complexity isO(2ⁿ)
. In this example, each call to fibonacci results in two more calls, leading to an exponential growth of function calls. Hence, the time complexity isO(2ⁿ)
.
Conclusion
Understanding time complexity helps in evaluating and optimizing the performance of algorithms. By analyzing the complexity of our code, we can make better decisions and write more efficient programs. The examples above illustrate how different algorithms perform as the input size grows, helping us choose the best approach for our needs.
Happy coding! 😊