# Analysis of Algorithms

Performance analysis of algorithms is an essential aspect of computer science. As algorithms help in solving various complex problems, it is essential to analyze the performance of the algorithms to determine the efficiency and effectiveness of the algorithm in solving the problem.

Performance analysis of algorithms helps in understanding how the algorithm works and the resources required by the algorithm. The analysis of algorithms helps in determining the time and space complexity, which helps in selecting the best algorithm to solve a particular problem.

## Why We Need Efficient Algorithms

In today's world, where we are dealing with large amounts of data, it is crucial to have efficient algorithms. The use of inefficient algorithms can lead to slow and unresponsive systems, which can have significant negative impacts on user experience and business productivity.

Let's consider an example to understand this concept better. Suppose you have a task of finding a particular book in a library. You have two options:

• Option 1: Start at the first shelf and scan each bookshelf one-by-one until you find the book.
• Option 2: Use the library's computerized catalog system to locate the book's shelf number and go directly to the shelf.
Option 1 is an inefficient algorithm, as it requires a lot of time and effort to find the book, especially if the library is very large. On the other hand, option 2 is an efficient algorithm, as it leverages technology to quickly locate the book, saving you time and effort.

Similarly, when we are dealing with large datasets, using an inefficient algorithm can lead to significant performance issues. For example, if we use a brute-force algorithm to search for an element in an unsorted array, it would require O(n) time in the worst case, where n is the size of the array. However, if we use a binary search algorithm, which requires a sorted array, it would only take O(logn) time in the worst case, which is much faster.

In conclusion, by using efficient algorithms, we can significantly improve the performance of our systems, making them faster, more reliable, and more user-friendly. Efficient algorithms enable us to handle large amounts of data quickly and efficiently, which is crucial in today's data-driven world.

### Why Performance Analysis of Algorithms

The performance analysis of algorithms is essential for following reasons:
• It helps in determining the best algorithm to solve a particular problem.

• It helps in understanding the efficiency and effectiveness of the algorithm in solving the problem.

• It helps in understanding the resources required by the algorithm.

• It helps in comparing the performance of different algorithms.

### Time and Space Complexity Analysis of Algorithms

The performance analysis of algorithms involves the analysis of time and space complexity. Time complexity is the amount of time required by the algorithm to solve a particular problem, and space complexity is the amount of memory required by the algorithm to solve a particular problem.

## Asymptotic Analysis of Algorithms

Asymptotic analysis of algorithms is a method of analyzing the performance of algorithms by determining the growth rate of the algorithm's time or space complexity as the input size approaches infinity. The growth rate of the time or space complexity is denoted by Big-O notation.

### Big-O Notation

Big-O notation is a notation used to describe the upper bound of the growth rate of the time or space complexity of an algorithm. It gives an idea of how the time or space complexity of the algorithm grows as the input size increases.

### Omega Notation

Omega notation is a notation used to describe the lower bound of the growth rate of the time or space complexity of an algorithm. It gives an idea of how the time or space complexity of the algorithm is at least as much as the growth rate.

### Theta Notation

Theta notation is a notation used to describe the tight bound of the growth rate of the time or space complexity of an algorithm. It gives an idea of how the time or space complexity of the algorithm is not more than the upper bound and not less than the lower bound.

## Worst, Average and Best Case Analysis of Algorithms

The performance of an algorithm can be analyzed based on the worst-case, average-case, and best-case scenarios. The worst-case scenario is when the algorithm takes the maximum time or space to solve the problem. The best-case scenario is when the algorithm takes the minimum time or space to solve the problem. The average-case scenario is the average of the time or space taken by the algorithm to solve the problem for all possible inputs.

## Worst, Average and Best Case Analysis of Algorithms

The performance of an algorithm can be analyzed based on the worst-case, average-case, and best-case scenarios. The worst-case scenario is when the algorithm takes the maximum time or space to solve the problem. The best-case scenario is when the algorithm takes the minimum time or space to solve the problem. The average-case scenario is the average of the time or space taken by the algorithm to solve the problem for all possible inputs.

## Comparison between different asymptotic notations

When analyzing the time complexity of an algorithm, it's essential to understand how different notations compare in terms of their execution time. Here are some common notations and their execution time comparison:

• O(n): Algorithms with a time complexity of O(n) have an execution time proportional to the input size, n. For example, iterating over an array of n elements has a time complexity of O(n).
• O(nlogn): Algorithms with a time complexity of O(nlogn) have an execution time that grows logarithmically with the input size. Examples of such algorithms include quicksort and mergesort, both of which have a time complexity of O(nlogn).
• O(logn): Algorithms with a time complexity of O(logn) have an execution time that grows logarithmically with the input size. For example, binary search has a time complexity of O(logn).
• O(n^2): Algorithms with a time complexity of O(n^2) have an execution time proportional to the square of the input size. Examples include bubble sort and selection sort, both of which have a time complexity of O(n2).
• O(An): Algorithms with a time complexity of O(A^n) have an execution time that grows exponentially with the input size. Examples of such algorithms include brute-force algorithms that enumerate all possible solutions.

To understand the performance difference between these notations, let's compare the execution time of sorting algorithms with different complexities on a dataset of 100,000 randomly generated integers. Here are the results:

• Bubble Sort (O(n^2)): 37.25 seconds
• Quick Sort (O(nlogn)): 0.051 seconds
• Merge Sort (O(nlogn)): 0.063 seconds
As you can see, the difference in execution time is significant, with bubble sort taking over 700 times longer than quick sort and merge sort. These results illustrate why understanding the time complexity of an algorithm is crucial for selecting the right algorithm for a particular task.

## Performance Analysis of Bubble Sort and Quick Sort

In this section, we will analyze the performance of two popular sorting algorithms, Bubble Sort and Quick Sort, using the concepts we learned in the previous sections.

### Bubble Sort

Bubble Sort is a simple sorting algorithm that repeatedly steps through the list, compares adjacent elements and swaps them if they are in the wrong order. The pass through the list is repeated until the list is sorted.

```/*
* arr[] : input Array of integers.
* n : number of integers in input array.
*/
void bubbleSort(int arr[], int n) {
int i, j, temp;

// Outer loop to iterate through the array
for (i = 0; i < n - 1; i++) {

// Inner loop to perform the comparison and swap
for (j = 0; j < n - i - 1; j++) {

// Swap if the current element is greater than the next element
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
```

Let's consider the worst-case scenario, where the input array is in reverse order. In this case, the algorithm will require n^2 comparisons and n^2 swaps, where n is the size of the input array. Therefore, the time complexity of Bubble Sort in the worst case is O(n^2).

The space complexity of Bubble Sort is O(1), as it only requires a constant amount of extra space to store temporary variables.

### Quick Sort

Quick Sort is a widely used sorting algorithm based on the divide-and-conquer approach. It picks an element as a pivot and partitions the given array around the picked pivot.

```void quickSort(int arr[], int low, int high) {
int i = low, j = high, temp;
int pivot = arr[(low + high) / 2];

// Traverse input array to find partition
while (i <= j) {
while (arr[i] < pivot) {
i++;
}
while (arr[j] > pivot) {
j--;
}
if (i <= j) {
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++;
j--;
}
}
// Recursively sort two sub-arrays
if (low < j) {
quickSort(arr, low, j);
}
if (i < high) {
quickSort(arr, i, high);
}
}
```

Let's consider the worst-case scenario, where the pivot is always chosen as the largest or smallest element in the array. In this case, the algorithm will require n^2 comparisons and n^2 swaps, where n is the size of the input array. Therefore, the time complexity of Quick Sort in the worst case is O(n^2).

However, in practice, the average case time complexity of Quick Sort is O(nlogn). The space complexity of Quick Sort is O(logn) for the average and worst cases, as it requires logn levels of recursion.