Merg Sort Algorithm in C++

Merge Sort is a fundamental sorting algorithm that belongs to the divide-and-conquer family of algorithms. It is renowned for its efficiency and reliability and is often used as a benchmark against which other sorting algorithms are measured. The essence of Merge Sort lies in its ability to efficiently sort an array or list of elements, making it a vital tool in computer science, data science, and various other fields where data manipulation is a fundamental task.

A Brief History of Sorting Algorithms

Sorting is a fundamental operation in computer science, and the development of sorting algorithms has a rich history dating back to the early days of computing. Sorting algorithms have evolved over time, driven by advancements in hardware, algorithms, and the need for efficient data processing. This historical overview will provide insight into the key milestones and developments in the history of sorting algorithms.

1940s and 1950s: Early Beginnings

The earliest sorting algorithms were simple and relied on basic operations. One of the first algorithms was the bubble sort, which involved comparing and swapping adjacent elements. Early computers, such as the ENIAC and UNIVAC, used bubble sort and other similar methods for sorting data. These algorithms were not particularly efficient and had time complexities of O (n^2).

1950s and 1960s: Algorithm Refinements

As computers became more powerful, the need for more efficient sorting algorithms became evident. In the 1950s, American mathematician and computer scientist John Mauchly introduced the concept of merge sort. Merge sort is a divide-and-conquer algorithm that divides the data into smaller parts, sorts them, and then merges them back together. It offered better performance with a time complexity of O(n log n) and was a significant step forward in sorting.

1960s and 1970s: Quick Sort and Heap Sort

In the early 1960s, British computer scientist Tony Hoare introduced quick sort, another divide-and-conquer sorting algorithm. Quick sort became highly popular due to its speed and efficiency, often outperforming merge sort in practice. However, it has a worst-case time complexity of O(n^2), which led to further research into optimizing pivot selection.

Around the same time, heap sort was developed. This algorithm is based on the concept of binary heaps, a data structure that allows efficient retrieval of the maximum (or minimum) element. Heap sort provided an alternative approach to sorting with a time complexity of O(n log n) and worked well on larger datasets.

1970s and 1980s: Radix Sort and Hybrid Algorithms

In the 1970s, radix sort gained attention as a non-comparative sorting algorithm that processed data one digit at a time. It was particularly efficient for sorting integers or strings with fixed-length keys. Radix sort offered linear time complexity in many cases and found its applications in various data processing tasks.

The 1980s saw the emergence of hybrid sorting algorithms that combined the strengths of different sorting methods. IntroSort was one such hybrid algorithm, which used quick sort until a certain recursion depth and then switched to heap sort. This prevented the worst-case scenario for quick sort and combined the efficiency of both algorithms.

Late 20th Century: Introduction of Modern Sorting Algorithms

Towards the end of the 20th century and into the early 21st century, sorting algorithms like Timsort and Introspective Sort were developed. Timsort, used in Python and Java, combines merge sort and insertion sort to efficiently handle real-world data. Introspective Sort is a variation of quick sort that uses insertion sort to improve performance on small arrays.

Recent Advances: Parallel and External Sorting

Recent advancements in hardware and the need to process massive datasets have led to research in parallel and external sorting. Parallel sorting algorithms leverage multi-core processors and distributed computing to sort data faster. External sorting algorithms handle data that doesn't fit entirely in memory by utilizing external storage, such as hard drives or SSDs, to manage large-scale sorting tasks.

In summary, the history of sorting algorithms is a testament to the evolution of computer science and its interplay with hardware capabilities and real-world data processing needs. From the humble beginnings of bubble sort to the highly efficient and specialized sorting algorithms used today, this journey reflects the relentless pursuit of improving efficiency in one of the most fundamental operations in computing: sorting. Sorting algorithms continue to be a topic of research and innovation, ensuring that data can be organized and processed effectively in an ever-expanding digital world.

The origins of Merge Sort can be traced back to the early days of computer science and the development of algorithms for sorting data. John von Neumann, a renowned mathematician and computer scientist, is often credited with introducing the concept of merging and sorting in the mid-1940s. Merge Sort is built on the principle of divide and conquer, which was formalized by John von Neumann, paving the way for its development as an efficient sorting algorithm.

The algorithm's refinement continued over the years, with significant contributions from various computer scientists and mathematicians. One of the first published descriptions of the Merge Sort algorithm was in "A Method for Merging Information" by John Mauchly in 1945. Grace Hopper also contributed to the development of Merge Sort during her work on the UNIVAC I computer in the 1950s.

In the 1960s, Donald Knuth included Merge Sort in the first volume of his influential book series, "The Art of Computer Programming," solidifying its place in the canon of sorting algorithms. Since then, Merge Sort has remained a prominent and widely used sorting algorithm due to its efficiency and stability.

Algorithmic Overview:

• Merge Sort is a divide-and-conquer algorithm, which means it breaks down a problem into smaller subproblems, solves them, and then combines the solutions to solve the original problem. The primary steps of the Merge Sort algorithm can be summarized as follows:
• Divide: The unsorted array is divided into two halves (or more) until each subarray contains only one element. This is the base case of the recursion.
• Conquer: The subarrays are recursively sorted. This is typically done by applying the Merge Sort algorithm to each subarray, which involves further division and sorting.
• Merge: The sorted subarrays are then merged back together to produce a single, sorted array. This step is where the algorithm gets its name.

The key to Merge Sort's efficiency is the merging step, which combines already sorted subarrays into a single sorted array. This merging process is performed by comparing the elements from the two subarrays and placing them in the correct order in the merged result. This ensures that the elements are in the correct order within each subarray and, consequently, within the final sorted array.

Recursive Nature:

Merge Sort's recursive nature is a defining feature of the algorithm. It repeatedly divides the problem into smaller subproblems until it reaches a base case, where each subarray contains only one element. At this point, a subarray with a single element is already considered sorted, and the algorithm begins merging the subarrays.

The recursive structure of Merge Sort can be visualized as a binary tree. At the root of the tree, you have the original unsorted array. With each level of recursion, the array is divided into smaller subarrays, creating a binary tree structure. The leaves of this tree represent the one-element subarrays, which are considered sorted by definition.

Merge Sort Time Complexity:

One of the most attractive aspects of Merge Sort is its consistent and efficient time complexity. Merge Sort exhibits a time complexity of O(n log n) in all cases, making it a dependable choice for sorting large datasets. Let's explore why this is the case:

• Divide: The divide step involves repeatedly dividing the array into two halves until we have one-element subarrays. This process takes O(log n) time since the array is divided in half at each step.
• Conquer: In the conquer step, each subarray is sorted independently. Since every element is visited once during the merge process, the time complexity for sorting the subarrays is O(n) for each level of recursion.
• Merge: The merge step takes O(n) time to combine two subarrays of size n/2 into a single sorted subarray. The merging process efficiently combines the two halves while preserving the order of the elements.

Considering all these steps together, the overall time complexity of Merge Sort is O(n log n). This consistent and efficient performance is one of the main reasons Merge Sort is favored in practice.

Merge Sort Space Complexity:

The space complexity of an algorithm refers to the amount of memory it uses during its execution. Merge Sort, while highly efficient in terms of time complexity, does require additional memory for the merging step. This makes it a stable and adaptive sorting algorithm but not an in-place sorting algorithm.

The space complexity of Merge Sort is O(n), meaning it requires auxiliary memory to store the two halves during the merge step. This auxiliary memory is necessary to temporarily hold the merged results as they are combined. In practical terms, this means that Merge Sort consumes additional memory proportional to the size of the input data, which can be a concern when dealing with very large datasets.

However, it's important to note that the additional memory used by Merge Sort does not depend on the specific arrangement of elements in the input array, which makes it stable and adaptive. Stability is a desirable property in sorting algorithms because it ensures that equal elements retain their relative order in the sorted output.

To mitigate the space complexity issue, an alternative approach known as "bottom-up" or "iterative" Merge Sort can be used. This approach reduces the space complexity to O(1) by carefully managing the merging process and swapping elements in the original array. While it offers improved space efficiency, it can be less intuitive and somewhat more complex to implement.

Merge Sort Implementation:

Merge Sort is a versatile algorithm that can be implemented in various programming languages. To illustrate its implementation, let's take a look at a high-level example in Python. This C++ implementation follows the algorithmic steps of Merge:

Output:

```Given array is
12 11 13 5 6 7
Sorted array is
5 6 7 11 12 13
................
Process executed in 1.11 seconds
Press any key to continue
```

Explanation

1. #include <bits/stdc++.h> and using namespace std;: These lines include the necessary C++ standard libraries and specify that the code will use the standard namespace for convenience.
2. void merge(int array[], int const left, int const mid, int const right): This function is responsible for merging two subarrays of the input array. It takes three parameters:
• array[]: The input array to be sorted.
• left: The left index of the first subarray.
• mid: The middle index that separates the two subarrays.
• right: The right index of the second subarray.
3. The next lines in the merge function:
• Calculate the sizes of the two subarrays.
• Create temporary arrays leftArray and rightArray to store the data from the two subarrays.
• Copy the data from the original array into these temporary arrays.
4. The while loop in the merge function merges the two subarrays by comparing elements from both subarrays and placing them in the correct order in the original array.
5. After merging, there are two while loops that copy any remaining elements from the leftArray and rightArray to the original array.
6. Finally, delete[] is used to free the memory allocated for the temporary arrays.
7. void mergeSort(int array[], int const begin, int const end): This is the merge sort function. It takes three parameters:
• array[]: The input array to be sorted.
• begin: The left index of the subarray to be sorted.
• end: The right index of the subarray to be sorted.
8. In the mergeSort function, if begin is greater than or equal to end, it returns, as there's nothing to sort in this case.
9. The function calculates the middle index mid and then recursively calls itself on the left and right subarrays and finally merges them using the merge function.
10. The printArray function is a utility function to print the elements of an array.
11. In the main function:
• An example integer array arr is defined.
• The size of the array is calculated using sizeof.
• The original array is printed.
• mergeSort is called to sort the array.
• The sorted array is printed.

The code demonstrates the process of Merge Sort to sort an array of integers.

Time and Space Complexity

Time Complexity:

Merge Sort is a divide-and-conquer sorting algorithm, and its time complexity can be analyzed as follows:

• Divide Step: In this step, the array is divided into two equal-sized subarrays. This process continues recursively until we reach subarrays of size 1. The number of divisions required is approximately O(log n), where n is the number of elements in the array.
• Conquer Step: In the conquer step, we merge two subarrays of size n/2 each, which takes O(n) time. Since we have O(log n) levels of recursion, and each level takes O(n) time, the total time complexity of the merge operation is O(n * log n).
• Overall Time Complexity: The divide and conquer steps result in a time complexity of O(n * log n) for the Merge Sort algorithm in the worst, best, and average cases. This makes Merge Sort very efficient for large datasets.

Space Complexity:

The space complexity of an algorithm refers to the amount of additional memory required to perform the algorithm. In the case of the Merge Sort code, we use additional memory for the following purposes:

• Temporary Arrays: The code uses two temporary arrays, leftArray and rightArray, to store the elements of the subarrays during the merge step. Each of these arrays has a size of approximately n/2. Therefore, the space complexity due to these temporary arrays is O(n).
• Recursive Call Stack: Merge Sort is a recursive algorithm, and each recursive call creates a new set of local variables and memory for function call data. The maximum depth of the recursion tree is O(log n), and at each level, we have space used for function parameters and local variables. The total space used in the call stack is also O(log n).
• Overall Space Complexity: The space complexity of Merge Sort is O(n) for the temporary arrays plus O(log n) for the call stack. Therefore, the overall space complexity is O(n + log n), which can be simplified to O(n).

In summary, the Merge Sort algorithm has a time complexity of O(n * log n) and a space complexity of O(n) in the worst, best, and average cases. The use of temporary arrays and the recursive nature of the algorithm contribute to its space complexity. Merge Sort is efficient in terms of time complexity, making it suitable for sorting large datasets, but it does require additional memory to operate.

Advantages of Merge Sort:

• Stable Sorting: Merge Sort is a stable sorting algorithm, which means that the relative order of equal elements is preserved during the sorting process. This is essential when sorting data based on multiple criteria or when the original order of equal elements needs to be maintained.
• Efficient for Large Datasets: Merge Sort has a time complexity of O(n log n) in the worst, best, and average cases. This property makes it particularly efficient for sorting large datasets. Unlike other algorithms with quadratic time complexity, such as Bubble Sort or Insertion Sort, Merge Sort's performance does not degrade significantly with increasing input size.
• Guaranteed Performance: Unlike some other sorting algorithms, Merge Sort's time complexity remains constant regardless of the data distribution. Its divide-and-conquer strategy ensures that it consistently divides the data into balanced partitions, leading to a predictable performance.
• Parallelizability: Merge Sort can take advantage of parallel processing to speed up sorting, making it suitable for modern multi-core and distributed computing environments. By dividing the sorting process into smaller subproblems, these sub problems can be sorted concurrently.
• No Worst-Case Scenario: Merge Sort does not have a worst-case scenario like Quick Sort, where the algorithm's performance can degrade significantly for specific data distributions. Its time complexity remains stable at O (n log n) across all cases.

Disadvantages of Merge Sort:

• Space Complexity: Merge Sort requires additional memory to store temporary arrays during the merging phase. This means it has a space complexity of O(n), which could be a concern when sorting very large datasets with limited memory resources.
• Slower for Small Datasets: While Merge Sort is highly efficient for large datasets, its divide-and-conquer nature and the overhead of function calls make it relatively slower for small datasets compared to simpler algorithms like Insertion Sort or Selection Sort.
• Non-Adaptive: Merge Sort does not take advantage of any existing order in the dataset. Even if the array is partially sorted, Merge Sort will still perform its standard divide-and-conquer approach, resulting in a higher number of operations.
• Recursive Overhead: Merge Sort is typically implemented using recursion, which adds some overhead due to function calls and maintaining the call stack. In some programming languages or platforms, recursion might not be the most efficient way to implement sorting algorithms.

In summary, Merge Sort's advantages lie in its efficiency, stability, and predictable performance, making it an excellent choice for sorting large datasets and scenarios where stability is crucial. However, its space complexity, non-adaptiveness, and slower performance for small datasets are notable limitations. In practice, the choice of sorting algorithm depends on the specific requirements and characteristics of the dataset being sorted.

Applications of merge sort

Merge Sort is a versatile sorting algorithm with various applications across different domains due to its efficiency and stability. Its O (n log n) time complexity and ability to maintain the relative order of equal elements in the input make it well-suited for numerous tasks. Here are some of the prominent applications of Merge Sort:

• General Sorting: Merge Sort is commonly used for sorting large datasets in various applications, such as database management systems, file processing, and data analysis. Its stable nature ensures that the original order of equal elements remains intact in the sorted output.
• External Sorting: Merge Sort is ideal for sorting large files that do not fit entirely in memory, a scenario known as external sorting. By dividing the data into manageable chunks, sorting each segment, and then merging them together, Merge Sort minimizes the need for excessive memory usage.
• Parallel Processing: Merge Sort's divide-and-conquer nature lends itself well to parallelization. Different subarrays can be sorted concurrently on separate processors or threads, taking advantage of multi-core architectures to improve sorting performance.
• Inversion Counting: Merge Sort is efficient for counting inversions in an array, where an inversion represents a pair of elements in the wrong order. This is useful in applications such as measuring the similarity between two sequences or identifying data anomalies.
• Searching Algorithms: Merge Sort is a fundamental component in various searching algorithms, such as binary search in sorted arrays. By ensuring that the array remains sorted, binary search can efficiently find specific elements.
• Merge Operations in Data Structures: Merge Sort's merging step is also widely used in data structure operations like merging two sorted linked lists or merging intervals in interval trees.
• Merge-Based Divide-and-Conquer Algorithms: Several advanced algorithms, like Merge Sort for sorting networks, use the merging technique as a building block. These algorithms often exhibit superior performance for specific tasks.
• External Memory Applications: Merge Sort is prevalent in applications that handle data stored on external storage devices, like hard drives or solid-state drives. Its ability to reduce the number of disk I/O operations makes it an essential component in external memory algorithms.
• Merge Join in Database Operations: In database systems, Merge Sort is used in merge join operations to combine and retrieve data efficiently from two sorted tables based on certain conditions.
• Offline Algorithms: Merge Sort is used in various offline algorithms, where the entire input is available at the start, allowing for efficient preprocessing before executing the main algorithm.

In summary, Merge Sort's efficiency, stability, and adaptability to various scenarios make it a widely used and valuable sorting algorithm in multiple fields, spanning from general-purpose sorting to complex data processing and storage applications.

For Videos Join Our Youtube Channel: Join Now