Merge sort is a stable divide-and-conquer sorting algorithm that splits an array into halves until each part has one element, and merges the sorted parts back together in order. It always runs in O(n log n) time, whatever the input looks like, and it needs an extra array of the same size for merging.
Merge sort is the choice in Java when the order of equal elements must survive the sort, when we sort linked lists, or when the data is too big for memory and we merge sorted chunks from disk. The JDK sorts objects with TimSort, which is a tuned merge sort.
The following example sorts delivery times in minutes with the mergeSort() method from section 3.
int[] deliveryMinutes = {5, 2, 7, 1, 8, 3, 6, 4};
mergeSort(deliveryMinutes);
int[] sorted = deliveryMinutes; // [1, 2, 3, 4, 5, 6, 7, 8]
The method sorts the caller’s array, although it uses an O(n) buffer internally. Next come the merge step in detail, recursive and bottom-up code, two speed-ups, and a comparison with the sorts that Java uses.
1. What Is Merge Sort?
Merge sort works in two phases. The split phase cuts the array in the middle again and again until every part has one element, and a single element is always sorted. The merge phase combines neighboring sorted parts into larger sorted parts until one sorted array is left.
All the real work happens while merging. Splitting only computes a middle index, so it costs almost nothing.

The array of 8 elements needs log2(8) = 3 levels, and every level merges all 8 elements once. That gives n elements times log n levels, which is where the O(n log n) running time comes from.
2. Merging Two Sorted Halves
The merge step takes two sorted halves and builds one sorted result. It keeps one index at the front of each half, compares the two front elements, copies the smaller one to the output and moves that index forward. When one half runs out, the rest of the other half is copied as it is.

The comparison must use <=, not <, because taking the left element on a tie is what makes merge sort stable. With <, two equal records would swap their order every time they meet in a merge.
Say a delivery app that keeps each driver’s stops sorted by time. To show one combined timeline, the app merges the sorted lists instead of sorting everything again, which costs O(n) instead of O(n log n).
3. Merge Sort Implementation in Java
The textbook version creates two new arrays in every merge call. Our version allocates one buffer of size n at the start and reuses it in every merge, which avoids creating about two million small arrays when we sort a million elements. The full source with tests lives in the Core-Java repository (Java 25).
static void mergeSort(int[] a) {
if (a == null || a.length < 2) {
return;
}
int[] buffer = new int[a.length];
sort(a, buffer, 0, a.length - 1);
}
static void sort(int[] a, int[] buffer, int low, int high) {
if (low >= high) {
return;
}
int mid = low + (high - low) / 2; // no int overflow
sort(a, buffer, low, mid);
sort(a, buffer, mid + 1, high);
merge(a, buffer, low, mid, high);
}
static void merge(int[] a, int[] buffer, int low, int mid, int high) {
System.arraycopy(a, low, buffer, low, high - low + 1);
int i = low; // front of the left half
int j = mid + 1; // front of the right half
int k = low; // next output position
while (i <= mid && j <= high) {
if (buffer[i] <= buffer[j]) { // <= keeps it stable
a[k++] = buffer[i++];
} else {
a[k++] = buffer[j++];
}
}
while (i <= mid) {
a[k++] = buffer[i++];
}
while (j <= high) {
a[k++] = buffer[j++];
}
}
The middle index uses low + (high – low) / 2 instead of (low + high) / 2. For arrays with more than about one billion elements, low + high is larger than Integer.MAX_VALUE and becomes negative, a bug that Joshua Bloch found in the JDK’s own binary search in 2006.
int[] none = {};
mergeSort(none);
int[] stillNone = none; // []
int[] repeats = {4, 1, 4, 1, 2};
mergeSort(repeats);
int[] sortedRepeats = repeats; // [1, 1, 2, 4, 4]
3.1. Sorting Objects with a Comparator
Stability matters when we sort objects by one field after they were already ordered by another. The following example sorts bakery pickups, which arrive in time order, by their pickup slot. Pickups in the same slot must keep their arrival order.
record Pickup(String customer, int slot) {}
static <T> void mergeSort(T[] a, Comparator<? super T> cmp) {
if (a == null || a.length < 2) {
return;
}
T[] buffer = a.clone();
sort(a, buffer, cmp, 0, a.length - 1);
}
static <T> void sort(T[] a, T[] buffer, Comparator<? super T> cmp, int low, int high) {
if (low >= high) {
return;
}
int mid = low + (high - low) / 2;
sort(a, buffer, cmp, low, mid);
sort(a, buffer, cmp, mid + 1, high);
System.arraycopy(a, low, buffer, low, high - low + 1);
int i = low, j = mid + 1, k = low;
while (i <= mid && j <= high) {
a[k++] = cmp.compare(buffer[i], buffer[j]) <= 0 ? buffer[i++] : buffer[j++];
}
while (i <= mid) {
a[k++] = buffer[i++];
}
while (j <= high) {
a[k++] = buffer[j++];
}
}
Pickup[] pickups = {
new Pickup("Mia", 3), new Pickup("Noah", 1), new Pickup("Liam", 3), new Pickup("Emma", 1)
};
mergeSort(pickups, Comparator.comparingInt(Pickup::slot));
String order = Arrays.stream(pickups).map(Pickup::customer).collect(Collectors.joining(", ")); // "Noah, Emma, Mia, Liam"
Noah still comes before Emma and Mia before Liam, so the arrival order inside each slot survived. The quicksort version of the same code does not give that guarantee.
3.2. Bottom-Up Merge Sort Without Recursion
The bottom-up version skips the split phase. It merges neighboring parts of width 1, after that of width 2, 4, 8 and so on, until one part covers the whole array. It needs no recursive calls, so the stack depth stays constant, and it reuses the same merge() method.
static void mergeSortBottomUp(int[] a) {
if (a == null || a.length < 2) {
return;
}
int n = a.length;
int[] buffer = new int[n];
for (int width = 1; width < n; width *= 2) {
for (int low = 0; low < n - width; low += 2 * width) {
int mid = low + width - 1;
int high = Math.min(low + 2 * width - 1, n - 1);
merge(a, buffer, low, mid, high);
}
}
}
int[] times = {9, 3, 7, 1, 5};
mergeSortBottomUp(times);
int[] bottomUp = times; // [1, 3, 5, 7, 9]
The odd length 5 works because Math.min() cuts the last part at the end of the array, and the inner loop condition low < n – width skips a part that has no right half.
4. Making Merge Sort Faster
Two small changes add most of the speed that library implementations have. Both keep the algorithm stable and correct.
- Below a size of about 16 elements, an insertion sort is faster than splitting further, because it has no recursion overhead.
- When the last element of the left half is less than or equal to the first element of the right half, the two halves are already in order, so we skip the merge.
static void sortFast(int[] a, int[] buffer, int low, int high) {
if (high - low < 16) {
for (int i = low + 1; i <= high; i++) { // insertion sort
int value = a[i];
int j = i - 1;
while (j >= low && a[j] > value) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = value;
}
return;
}
int mid = low + (high - low) / 2;
sortFast(a, buffer, low, mid);
sortFast(a, buffer, mid + 1, high);
if (a[mid] <= a[mid + 1]) {
return; // already in order
}
merge(a, buffer, low, mid, high);
}
int[] arrivals = new Random(7).ints(100, 0, 1_000).toArray();
int[] expected = arrivals.clone();
Arrays.sort(expected);
sortFast(arrivals, new int[arrivals.length], 0, arrivals.length - 1);
boolean same = Arrays.equals(expected, arrivals); // true
We timed one million random integers on Java 25, and every version produced the same result as Arrays.sort().
| Version | 1 million random ints | 1 million sorted ints |
|---|---|---|
| New arrays in every merge | 160 to 300 ms | not measured |
| One reused buffer | about 165 ms | about 42 ms |
| Buffer, cutoff and skip check | about 145 ms | about 2 ms |
| Arrays.sort(int[]) | about 10 ms (50 to 80 ms on first calls) | not measured |
The skip check helps most on data that is already sorted or nearly sorted, such as a list of events that only gets a few new entries.
5. Merge Sort Time and Space Complexity
The running time follows the recurrence T(n) = 2T(n/2) + n, because each call sorts two halves and merges n elements. The recursion has log2(n) levels with n work each, so the total is n log n. The input order does not change the number of levels, so the best, average and worst cases are the same for the basic version.
| Property | Merge sort |
|---|---|
| Best time | O(n log n), or O(n) with the skip check on sorted input |
| Average time | O(n log n) |
| Worst time | O(n log n) |
| Extra space | O(n) buffer plus O(log n) stack, or no stack for bottom-up |
| Stable | Yes, with <= in the merge |
| In place | No |
The O(n) buffer is the main cost. Sorting 100 million int values needs 400 MB for the array and another 400 MB for the buffer, whereas quicksort needs only its stack.
6. Merge Sort in the JDK and Other Sorting Algorithms
Java does not expose a method called merge sort, but every object sort in the JDK uses a merge sort variant. Arrays.sort(Object[]), Collections.sort() and List.sort() use TimSort, a stable, adaptive and iterative merge sort that finds existing sorted runs in the data and merges them. On nearly sorted input it needs far fewer than n log n comparisons.
Primitive arrays such as int[] use Dual-Pivot Quicksort instead, because stability means nothing for plain numbers. Arrays.parallelSort() splits large arrays into parts, sorts them on the common fork/join pool and merges the results, which is merge sort at a higher level.
| Algorithm | Worst time | Extra memory | Stable | Used in the JDK for |
|---|---|---|---|---|
| Merge sort (TimSort) | O(n log n) | O(n) | Yes | Objects, List.sort() |
| Quicksort (Dual-Pivot) | O(n log n) with heap sort fallback | O(log n) | No | Primitive arrays |
| Heap sort | O(n log n) | O(1) | No | Fallback inside Dual-Pivot Quicksort |
| Insertion sort | O(n2) | O(1) | Yes | Small ranges inside both |
7. Merge Sort FAQs
Stability, memory and speed raise the most questions once merge sort works.
7.1. Is merge sort stable?
Yes, as long as the merge takes the left element when two elements are equal. The bakery example in section 3.1 shows equal slots keeping their order.
7.2. Can merge sort work in place?
Not in the simple form. In-place merge algorithms exist, but they are complex and slower. The usual trade-off is the O(n) buffer, and linked lists avoid it because merging relinks nodes instead of copying them.
7.3. Which is faster, merge sort or quicksort?
On primitive arrays, quicksort is faster in most measurements because it moves less data and needs no buffer. Merge sort wins when stability matters, when the worst case must stay O(n log n), or when data comes as sorted streams. The quicksort tutorial measures both against Arrays.sort().
7.4. How does merge sort handle data that does not fit in memory?
Through external merge sort. We read chunks that fit in memory, sort each chunk and write it to a temporary file, and at the end merge all files by keeping one current line from each file in a PriorityQueue. Databases and tools such as Unix sort use this approach for large files.
8. Conclusion
Merge sort splits the array down to single elements and merges sorted parts back together, so it always does O(n log n) work. Taking the left element on ties keeps it stable, which is why Java sorts objects with TimSort, a merge sort variant.
Its cost is the O(n) buffer. One reused buffer, an insertion sort cutoff and the skip check for halves that are already in order make a hand-written version much faster, and the bottom-up form removes recursion completely.
9. References
- List.sort() (Java SE 25 API)
- Arrays (Java SE 25 API)
- Joshua Bloch, Nearly All Binary Searches and Mergesorts are Broken
Happy Learning !!
why use comparable? since Integer implement this interface, if you defines all Comparable array to Integer array, it works well too.
Wow this is insanely easy. BTW things weren’t very clear at first. Then I simplified the version and saw how System.arrayCopy works and everything was crystal clear. I’ve added a simplified version of the code which doesn’t involve comparable and Wrapper objects just to make it clear.
package core.algos.sort; import java.util.Arrays; /* * <Note on java.lang.System.arraycopy();> * * The java.lang.System.arraycopy() method copies an array from the specified * source array, beginning at the specified position, to the specified position * of the destination array. A subsequence of array components are copied from * the source array referenced by src to the destination array referenced by * dest.The number of components copied is equal to the length argument. * * The components at positions srcPos through srcPos + length - 1 in the source * array are copied into positions destPos through destPos + length - 1, * respectively, of the destination array. * * <Parameters> * src - This is the source array. * srcPos - This is the starting position in the source array. * dest - This is the destination array. * destPos - This is the starting position in the destination data. * length - This is the number of array elements to be copied. */ /** * * In computer science, merge sort (also commonly spelled mergesort) is an * O(nlog n) comparison-based sorting algorithm. Most implementations produce a * stable sort, which means that the implementation preserves the input order of * equal elements in the sorted output. Mergesort is a divide and conquer * algorithm. Divide and conquer algorithms divide the original data into * smaller sets of data to solve the problem. * * During the Mergesort process the object in the collection are divided into * two collections. To split a collection, Mergesort will take the middle of the * collection and split the collection into its left and its right part. The * resulting collections are again recursively splitted via the Mergesort * algorithm until they are broke to single element in each collection. * * After splitting each collection, mergesort algorithm start combining all * collections obtained via above procedure. To combine both collections * Mergesort start at each collection at the beginning. It pick the object which * is smaller and inserts this object into the new collection. For this * collection it now selects the next elements and selects the smaller element * from both collection by comparing one element from each collection at a time. * * This process create a collection of sorted elements (subset of all elements * which needs to be sorted). This process is recursively done for all available * collections obtained in first step i.e. splitting the collections. * * Once all elements from both collections have been inserted in the new * collection, Mergesort has successfully sorted the collection. * * <When to use Merge Sort> * 1. Merge sort is used when the data structure doesn’t support random access, * since it works with pure sequential access (forward iterators, rather than * random access iterators). It’s also widely used for external sorting, where * random access can be very, very expensive compared to sequential access. * For example, When sorting a file which doesn’t fit into memory, you might * break it into chunks which fit into memory, sort these chunks independently, * writing each out to a file, then merge sort the generated files. * 2. Also, you can use merge sort when you need a stable sort. It’s very important * feature of merge sort. * 3. Mergesort is quicker when dealing with linked lists. This is because pointers * can easily be changed when merging lists. It only requires one pass (O(n)) through * the list. * 4. If there is a lot of parallelization occurs then parallelizing Mergesort is * simpler than other sort algorithms. * * * <Steps> * 1. Divide the unsorted list into two sublists of about half the size. * 2. Sort each of the two sublists. * 3. Merge the two sorted sublists back into one sorted list. * */ public class MergeSort { public static void main(String[] args) { // Unsorted array int[] a = { 2, 6, 3, 5, 1, 4 }; // Call merge sort mergeSort(a); // Check the output which is sorted array System.out.println(Arrays.toString(a)); } /** * @param list * @return */ public static int[] mergeSort(int[] list) { // If list is empty; no need to do anything if (list.length <= 1) { return list; } // Create 2 lists to hold 1st half and 2nd half of original list. int[] first = new int[list.length / 2]; int[] second = new int[list.length - first.length]; // Split the array in half and populate above lists. System.arraycopy(list, 0, first, 0, first.length); System.arraycopy(list, first.length, second, 0, second.length); // Sort each half recursively mergeSort(first); mergeSort(second); // Merge both halves together, overwriting original array merge(first, second, list); return list; } /** * @param first * @param second * @param result */ private static void merge(int[] first, int[] second, int[] result) { // Index Position in first array - starting with first element int iFirst = 0; // Index Position in second array - starting with first element int iSecond = 0; // Index Position in merged array - starting with first position int iMerged = 0; // Compare elements at iFirst and iSecond, // and move smaller element at iMerged while (iFirst < first.length && iSecond < second.length) { if (first[iFirst] < second[iSecond]) { result[iMerged] = first[iFirst]; iFirst++; } else { result[iMerged] = second[iSecond]; iSecond++; } iMerged++; } // copy remaining elements from both halves - each half will have already sorted // elements System.arraycopy(first, iFirst, result, iMerged, first.length - iFirst); System.arraycopy(second, iSecond, result, iMerged, second.length - iSecond); } }Thanks for sharing.
It’s awesome dude,,,,
I am getting a recursive infinite at line mergeSort(first)
the code is the same as you have posted what am I missing?
public static void main(String[] args) { Integer[] input = { 23, 31, 1, 21, 36, 72, 20, 73, 19}; mergeSort(input); //Check the output which is sorted array System.out.println(Arrays.toString(input)); } public static Comparable[] mergeSort(Comparable[] list) { //Split the array in half in two parts Comparable[] first = new Comparable[list.length / 2]; Comparable[] second = new Comparable[list.length - first.length]; System.arraycopy(list, 0, first, 0, first.length); System.arraycopy(list, first.length, second, 0, second.length); //Sort each half recursively mergeSort(first); mergeSort(second); //Merge both halves together, overwriting to original array merge(first, second, list); return list; } public static void merge(Comparable[] first, Comparable[] second, Comparable[] result) { //Index Position in first array - starting with first element int iFirst = 0; //Index Position in second array - starting with first element int iSecond = 0; //Index Position in merged array - starting with first position int iMerged = 0; //Compare elements at iFirst and iSecond, //and move smaller element at iMerged while (iFirst < first.length && iSecond < second.length) { if (first[iFirst].compareTo(second[iSecond]) < 0) { result[iMerged] = first[iFirst]; iFirst++; } else { result[iMerged] = second[iSecond]; iSecond++; } iMerged++; } //copy remaining elements from both halves - each half will have already sorted elements System.arraycopy(first, iFirst, result, iMerged, first.length - iFirst); System.arraycopy(second, iSecond, result, iMerged, second.length - iSecond); }You missed the code section:
if (list.length <= 1) {
return list;
}
This is important to stop the recursive path.
Thanks for you inputs. it clear and easy to understand. but in mergeSort method we are always creating new arrays for first,second these will be created each time in recursion happened. this will more costly(less performance). I think there are other ways to do the sorting without creating the arrays each time by passing the indexes. refer https://aviatordao.com/java2novice/. but it will be more difficult to understand compared to this Tutorial.
package com.ds; import java.util.Arrays; public class MergeSortUsingIndex { public static void main(String[] args) { //Unsorted array int[] arr = { 4, 2, 9, 6, 23, 12, 34, 0, 1 };//{ 2, 6, 3, 5, 1 };//{ 5,4,3,1,2,6}; //Call merge sort mergeSort(arr,arr.length); //Check the output which is sorted array System.out.println(Arrays.toString(arr)); } public static void mergeSort(int[] list,int end) { if(end<=1) return; int middle = end/2; mergeSort(list,middle); sortUpTo(list,end); } public static void sortUpTo(int[] list,int end){ int j=0; for (int i = 0; i 0){ if(list[j]<list[j-1]){ swap(list,j,j-1); } j--; } } } public static void swap(int[] list,int from,int to){ int temp = list[from]; list[from]=list[to]; list[to] = temp; } }doesn’t seems to work :)
//copy remaining elements from both halves – each half will have already sorted elements
System.arraycopy(first, iFirst, result, iMerged, first.length – iFirst);
System.arraycopy(second, iSecond, result, iMerged, second.length – iSecond);
while copying you are starting both with index=iMerged, so it will overwrite. no?
Thansk for your post, but since result already have the sorted elements in the merge function, why still need these below steps?
System.arraycopy(first, iFirst, result, iMerged, first.length – iFirst);
System.arraycopy(second, iSecond, result, iMerged, second.length – iSecond);
I have written explanation in comments.
Hi Lokesh,
Thanks for all tutorial. They are very helpful.
Description, diagrammatic explanation of mergesort and code are different.
It’s bottom up mergesort in diagram and in code it’s top down mergesort in code.
Sorry for comment’s got misunderstood initially.