Implementing the Merge Sort Algorithm in Python
Sorting is one of the most fundamental problems in computer science. Whether it’s organizing a to-do list, ranking search results, or arranging numbers in order, efficient sorting is important for handling large amounts of data. In this tutorial, we will explore how to implement merge sort in Python, a powerful sorting algorithm that uses a divide-and-conquer approach. We’ll learn how it works and how to implement it in Python and discuss its real-world applications.
What is merge sort?
Merge sort is a popular sorting algorithm that follows the divide-and-conquer approach. Instead of sorting the entire list at once, it breaks the problem into smaller pieces. The algorithm works by:
- Dividing the array into two halves recursively until each piece has just one element.
- Sorting the smaller parts.
- Merging the sorted halves back together in order, ensuring the result is a fully sorted array.
One of the reasons merge sort is widely used is its predictable efficiency. Unlike some other sorting algorithms that struggle in the worst case, merge sort consistently operates with a time complexity of O(n log n)
across all inputs. We will compare merge sort with other algorithms in the following sections.
While merge sort may not always be the fastest option for small datasets due to its additional memory usage, it excels in sorting large datasets and linked lists where other algorithms might slow down.
But how exactly does the merge sort achieve it’s consistent performance? Let’s break down the step-by-step process that makes merge sort work.
Learn Sorting Algorithms with Python
Learn about the usefulness and efficiency of computational sorting by implementing different sorting algorithms yourself.Try it for freeHow does merge sort work?
As we discussed, merge sort follows a divide-and-conquer approach. At its core, merge sort relies on two key operations:
- Dividing the array into smaller subarrays
- Merging those subarrays back together in sorted order
Let’s walk through these steps in detail:
Divide phase
The algorithm starts by repeatedly splitting the input array into halves. First, the main array is divided into two equal (or nearly equal) parts. Then, each of those parts is further divided into two. This division continues recursively until we reach subarrays containing only a single element. By definition, a one-element array is already sorted.
Conquer phase (Merging)
Once we have our single-element subarrays, we combine them in a sorted manner. The merging process compares elements from two sorted subarrays and builds a new, larger sorted array from them. For each pair of subarrays, we:
- Compare the first elements of both arrays
- Select the smaller element and add it to our result array
- Move to the next element in the array that contributed the smaller element
- Repeat until all elements from both arrays have been processed
Combine phase
As the merging continues, we work our way back up through the recursion tree. Each merge operation produces larger and larger sorted subarrays. What starts as merging pairs of single elements grows to merging arrays of 2 elements, then 4, then 8, and so on. Each merged result maintains its sorted property, ensuring that by the time we reach the final merge, we have a completely sorted array.
Example
Let’s visualize this process with a simple 5-element array:
[38, 27, 43, 3, 9]
Step 1: Divide the array
We start by splitting the array in half and continue splitting until we have individual elements:
At this point, we’ve broken our array down into subarrays with single elements. A single-element array is already sorted by definition.
Step 2: Merge subarrays in sorted order
Now we start merging these single elements back together in sorted order:
First, we merge [38] and [27]. To merge these arrays:
- Compare 38 and 27
- 27 is smaller, so it goes first
- 38 remains, so it goes next
Result:
[3] and [9] are already sorted. Next, we merge [43] with [3, 9]:
Here’s how this merge works:
- Compare 43 and 3: 3 is smaller, the result so far: [3]
- Compare 43 and 9: 9 is smaller, the result so far: [3, 9]
- Only 43 remains, so add it to the result
Result:
Finally, we merge the remaining two sorted subarrays. For this final merge:
- Compare 27 and 3: 3 is smaller, the result so far: [3]
- Compare 27 and 9: 9 is smaller, the result so far: [3, 9]
- Compare 27 and 43: 27 is smaller, the result so far: [3, 9, 27]
- Compare 38 and 43: 38 is smaller, the result so far: [3, 9, 27, 38]
- Only 43 remains, so add it to the result
Final result:
And that’s it! Our array is now fully sorted.
Time and space complexity
Let’s understand the efficiency of merge sort by analyzing its time and space complexity.
Time complexity of merge sort
Merge sort consistently operates with time complexity of O(n log n)
, making it more efficient than algorithms like bubble sort or insertion sort, which can take O(n²)
in the worst case. Here’s how it’s time complexity plays out in different scenarios:
- Best Case (
O(n log n)
): Even if the input is already sorted, merge sort still follows the same divide-and-conquer approach, recursively splitting and merging the array. The operations remain the same, leading toO(n log n)
complexity. - Average Case (
O(n log n)
): Regardless of the input order, the algorithm always divides the array into halves and merges them in a structured manner. Since this process occurs inlog n
levels, with each level requiringO(n)
operations to merge elements, the total complexity remainsO(n log n)
. - Worst Case (
O(n log n)
): When the input is in the worst possible order (such as reverse order), merge sort still executes in the same structured way. Unlike quick sort, which can degrade toO(n²)
in the worst case, merge sort maintains a steadyO(n log n)
performance.
Space complexity of merge sort
While merge sort is efficient in terms of time, it comes with an extra cost in terms of space. The algorithm requires additional memory to store the divided subarrays during merging, leading to a space complexity of O(n)
. This extra space holds temporary arrays while merging elements back together.
For recursive implementations, merge sort also has an O(log n)
stack space overhead due to recursive function calls. However, the major space usage comes from the auxiliary arrays needed during merging. This makes merge sort less ideal for memory-constrained environments compared to in-place sorting algorithms like Quick Sort or Heap Sort.
Merge sort implementation in python
Let’s break down the merge sort algorithm into smaller, easy-to-understand parts. We’ll walk through each function and explain exactly what it does.
def merge_sort(arr):# Base case: a list of 0 or 1 elements is already sortedif len(arr) <= 1: return arr# Divide the list into two halvesmid = len(arr) // 2left_half = arr[:mid]right_half = arr[mid:]# Recursively sort both halvesleft_half = merge_sort(left_half)right_half = merge_sort(right_half)# Merge the sorted halvesreturn merge(left_half, right_half)
The above function is our primary function that implements the merge sort algorithm. Let’s understand each part:
- Base case: The first thing we do is check if our list has 0 or 1 elements. If it does, we simply return it since a list with a single element is already sorted! This is where our recursion will eventually stop.
- Dividing the array: We find the middle point of our array using integer division (
//
). Then we split our original array into two halves - everything before the middle point goes intoleft_half
, and everything from the middle point onwards goes intoright_half
. - Recursive sorting: We call the same
merge_sort
function on each half. This means each half will be further divided until we reach lists with single elements; then, those will be merged back up in sorted order. - Merging: Once both halves are sorted, we combine them using our merge function to get a completely sorted array.
Next is the merge()
function:
def merge(left, right):result = []i = j = 0# Compare elements from both lists and add the smaller one to the resultwhile i < len(left) and j < len(right):if left[i] <= right[j]:result.append(left[i])i += 1else:result.append(right[j])j += 1
This first part of our merge
function sets up the comparison logic:
- We create an empty list called
result
that will hold our merged, sorted elements. - We initialise two pointers,
i
andj
, to keep track of our position in the left and right lists. - The
while
loop continues until we reach the end of either list. - Inside the loop, we compare the current elements from both lists (
left[i]
andright[j]
). - We take the smaller element, add it to our result, and move the corresponding pointer forward.
Now for the second part of the merge()
function:
def merge(left, right):result = []i = j = 0# Compare elements from both lists and add the smaller one to the resultwhile i < len(left) and j < len(right):if left[i] <= right[j]:result.append(left[i])i += 1else:result.append(right[j])j += 1# Second Partwhile i < len(left):result.append(left[i])i += 1# Add any remaining elements from the right listwhile j < len(right):result.append(right[j])j += 1return result
After our first loop, one of the lists might still have elements left. These remaining elements are already sorted among themselves. The first while
loop adds any remaining elements from the left list. The second while
loop adds any remaining elements from the right list.
Finally, we return our completely merged and sorted list.
Now that we’ve explored the implementation details of merge sort, let’s step back and look at the bigger picture. Understanding when to use merge sort compared to other algorithms is crucial for making informed decisions in real-world programming scenarios.
Merge sort vs other sorting algorithms
Let’s compare merge sort with other sorting algorithms to understand its strengths and limitations:
Algorithm | Best Time Complexity | Average Time Complexity | Worst Time Complexity | Space Complexity |
---|---|---|---|---|
Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) |
Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) |
Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) |
Insertion Sort | O(n) | O(n²) | O(n²) | O(1) |
Bubble Sort | O(n) | O(n²) | O(n²) | O(1) |
As we’ve examined the inner workings of merge sort and compared it with other sorting algorithms, let’s now turn our attention to real-world applications.
Applications of merge sort
Merge sort is widely used in many real-world applications because of its consistent performance and unique properties:
- Sorting Large Datasets
- Used in big data frameworks for massive dataset processing.
- Implemented in financial systems for chronological order transactions.
- External Sorting
- Ideal for data too large for memory.
- Applied in log processing for large server logs.
- Sorting Linked Lists
- Best for linked list sorting due to sequential access.
- Rearranged pointers efficiently without extra memory.
- Database Algorithms
- Powers index creation in database systems.
- Employed in merge joins to combine sorted data.
Conclusion
Merge sort is a fundamental algorithm that plays a crucial role in the software industry. In this article, we learned that:
- Merge sort illustrates the benefits of breaking problems into smaller parts for efficient solutions.
- It guarantees
O(n log n)
time complexity, ensuring reliable performance regardless of input order. - It requires
O(n)
additional memory but offers stability and efficiency on large datasets. - Merge sort is essential for various applications, including database systems and big data processing.
If you’re interested in learning about algorithms, consider taking this free course called Learn Data Structures and Algorithms with Python on Codecademy.
'The Codecademy Team, composed of experienced educators and tech experts, is dedicated to making tech skills accessible to all. We empower learners worldwide with expert-reviewed content that develops and enhances the technical skills needed to advance and succeed in their careers.'
Meet the full teamRelated articles
- Article
Sorting Algorithms
A brief overview of the more common sorting algorithms. - Article
How to Abort a Merge in Git
Learn how to cancel merge in Git using git merge --abort and git reset --merge commands. Explore when and why to abort a merge, key differences between both the commands and best practices for aborting a merge. - Article
How to sort a list in Python
Learn how to sort lists in Python using `.sort()` and `sorted()`. Explore custom sorting with keys, descending order sorting, and apply real-world sorting techniques efficiently.
Learn more on Codecademy
- Course
Learn Sorting Algorithms with Python
Learn about the usefulness and efficiency of computational sorting by implementing different sorting algorithms yourself.With CertificateIntermediate3 hours - Free course
Java: Algorithms
Learn the basics of recursion and how to implement and analyze important algorithms in Java.Beginner Friendly3 hours - Skill path
Pass the Technical Interview with Python
Learn about the computer science concepts of data structures and algorithms and build implementations of each from scratch in modern Python.Includes 8 CoursesWith CertificateIntermediate25 hours