Deep Dive into Data Structures and Algorithms
Published: 2022-06-29 20:27:35

One important thing to note is that for an algorithm to be considered effective, it should take the least time to execute and use the least amount of memory or space. There are different ways of calculating the performance of an algorithm:

##Space complexity##
In simple terms, whenever a solution to a problem is written, some memory is required to complete it.This is the amount of space required by an algorithm in order to execute. The input determines the space needed for execution- a larger input size means more memory is required. 

###Components that require space###

**Instruction space**. This is the space required to store the executable version of the program. It is fixed but varies depending on the number of lines of code in the program. It is used to store the compiled version of instructions.

**Data space**. The space required to store all the constants and variables including temporary variables.

**Environment space**. The space required to store the environment information needed to resume the suspended function.
 
**Calculating Space Complexity**

To calculate space complexity, it is important to know the value of memory used by different types of datatype variables for example; a _boolean, character and int8_ have 1 byte each while _float_ has 4 bytes.

```
let d = a+b+c;
console.log(d);
```
Variables _a,b,c_ and _d_ are **integer**  types hence they will take up 4 bytes each, so the total memory will be _(4(4) + 4)_ where the extra 4 bytes is for the console.log. This is an example of **constant space complexity**.

An example of **Linear Space Complexity** is illustrated below.

```
function sum (a[],n) {
  let y= 0;
  for(let i=0; i<n; i++) {
   y = y + a[i];
 }
return y;
}
sum();
```
In the above example, the space required is `4*n` bytes for the array `a[]` elements. 4 bytes is then required for `y,n,i` and the return value. The total requirement is `
(4n+12)` which is increasing linearly with the increase of the input value `n`.


##Time complexity##

This is the total time required by a program to run until its completion. It is estimated by counting the number of elementary steps performed by an algorithm from start to finish execution.

Time complexity is commonly expressed using the **Big O Notation**. This is the asymptotic(pertaining values approached at infinity) notation that represents the time complexity. Since an algorithm's performance may change depending on the input, the complexity that is commonly used is **Worst-case time complexity**. This is the maximum time taken for any input size.

**Calculating Time Complexity**

There are different types of time complexity.

- **Constant Time Complexity**

The running time does not change in relation to the input. It is denoted by **O(1)** where O is Big O. This is the fastest time complexity. It is considered the best case scenario for an algorithm because no matter how big the input size is, it takes the same amount of time to execute.

_Example:Array look up_

```
let arr1 = [1,2,4,5];
arr1.index(2);
```

- **Linear Time Complexity**

The running time is directly proportional to the input `N`. Performance grows in direct proportion to the size of the input data set. It is denoted by **O(n)**.

_Example_

```
let arr2 = [10,20,30,40,50];
let printValue
for (let i = 0; i < arr2.length; i++) {
  let printValue += arr2[i] ;
}
```
The program has to loop through the array to print each element.

- **Quadratic Time Complexity** 
 
The running time is directly proportional to the square of the input `N`. It is denoted by **O(n*n) or O(n^2)**. This is used because the complexity completes both O(1) and O(n).  

_Example_

```
let text = "";
for(let i = 0; i < N; i++) {
  for(let j = 0; j < N; j++) {
    text += `The number is ${i} + ${j};`
  }
}
```

-  **Logarithmic Time Complexity**

The running time is proportional to the number of times `N` can be divided by 2. The working area is divided by 2 with each iteration. It is denoted by **O(log n)**.

_Example_

```
function logTime(arr) {
  let numberOfLoops = 0
  for (let i = 1; i < arr.length; i *= 2) {
    numberOfLoops++
  }
  return numberOfLoops
}

let loop1 = logTime([1, 2, 3, 4]) // 2 loops
```
From the example above, every time the input length is doubled, the number of operations increases linearly by 1 which makes it half of the input length.

Understanding time and space complexity is important for every developer because it helps one to choose the best case to use for execution and hence ensure that the algorithm is performing at its best state while using the least memory.