Tim sort is the algorithm behind Java and Python's inbuilt `sort()` function. It is a hybrid algorithm, which uses concepts behind insertion sort and merge sort.
## Why to learn Tim Sort?
As we know, asymptotic notation hides some information in it, which is the constant factor associated with it. Constant factor depends upon the type of hardware and the amount of resources used by the algorithm for the provided input data. It is necessary to consider constant factor to evaluate the real time complexity.
**For example**, Insertion sort outperforms Merge sort for a small list of elements, even if asymptotically Insertion sort and Merge sort works with complexity $\mathcal{O}(N^2)$ and $\mathcal{O}(N\log{N})$ respectively. Why?
Merge sort uses recursive calls and $\mathcal{O}(N)$ extra space. Due to this memory overhead and overhead of recursive calls, the constant factor for merge sort turned out to be higher than insertion sort for a small list of elements. Therefore, $C_{is}(N^2) <C_{ms}(N\log{N})$,forsmallN.
- Here, **Overhead** is any combination of excess or indirect computation time, memory or other resources that are required to perform a specific task on computer.
- Suffix $is$ and $ms$ stands for insertion sort and merge sort respectively.
- For the shake of simplicity, overhead is considered in constant factors $C_{is}$ and $C_{ms}$.
As we know Quick sort, Merge sort and Heap sort has the time complexity of $\mathcal{O}(N\log{N})$. Tim sort also has the same time complexity, but it is designed such that the associated constant factor is as low as possible.
Tim sort is an **adaptive algorithm**, which means that it changes its behavior based on the patterns observed in the input data. That is why it is an **intelligent algorithm** and more optimal than other sorting algorithms.
1. The whole array is splitted into subarrays such that these subarrays are sorted. If a subarray is sorted in descending manner, then it is reversed.
**Note:** We use some criteria regarding the minimum size of these subarrays.
2. Then we use merge operation to merge these sorted subarrays. This merge operation is an advanced version of the merge routine used in the standard merge sort procedure.
Minimum run length is the minimum length of such run. Its value is decided based on the number of elements($n$) in the list. Let's see the algorithm to find it out.
Here, 64 is a standard value decided by the inventor such that the **min-run length**, for a list of size greater than 63, will turned out to be in the range 32 to 64 inclusive.
The main reason for this is that we are going to use modified insertion sort to sort this small chunk of data and insertion sort performs better on an array of small size.
A **Run** can be increasing or decreasing, so minimum length of a **Run** is $2$, because any sequence of length 2 is either increasing or decreasing.
Whether the run is increasing or decreasing, can be decided based on the first two elements. After finding a type(increasing or decreasing) of a run, we find its length by running a loop until the corresponding condition is satisfied.
In the case of decreasing run, we reverse the list in the end.
```cpp
// Find run and return its length
// @param start: Start position for the next run
int find_Runandmake_Ascending(vector<int>& data, int start)
{
int end = data.size();
if (start + 1 == end)
return 1;
int runHi = start + 1;
/// Ascending
if (data[start] <data[runHi])
while (runHi <end&&data[runHi-1]<data[runHi])
runHi++;
/// Descending
else {
while (runHi <end&&data[runHi-1]> data[runHi])
runHi++;
reverseRange(data, start, runHi - 1);
}
return runHi - start;
}
// To reverse elements from the range lo to hi
void reverseRange(vector<int>& data, int lo, int hi) {
while (lo <hi)
swap(data[lo++], data[hi--]);
}
```
If the length of the run is less than the constant **min-run length**, then we use **binary insertion sort** to add elements until the length becomes min-run length.
## Binary Insertion Sort
As we know the main idea of Insertion sort is to take an element and insert it at the correct position. Now, we are going to use binary search to find correct position, rather than a simple loop.
While we merging runs, we take too much care to about stability. In order to maintain **stability** we always merge consecutive runs, because otherwise it may result in instability.
For example, [2 3 4], [1 2 5], [2 4 5] are three consecutive runs, so if we merge first and third run first, then 2 of third run will end up before 2 of second run in the later merge operation.
Now, to maintain information about runs, we are going to use an array, which is used as a stack, so whenever a new run comes, we insert it at the top. Now, let's discuss some criteria about merging this runs.
Criterion 1: If the stack-size is greater than or equal to 3 and $|Z| <= |Y| + |X|$ is true, then if $|Z|<|X|$, then merge $|Z|$ and $|Y|$ otherwise merge $|X|$ and $|Y|$.
1. In order to have balanced merge, we are delaying the merge by waiting for a next run.
2. If we want to take advantage of cache memory, that is fresh runs are already in the cache therefore merging them have less memory overhead, then we should merge the fresh runs as soon as possible.
Certainly no, when we are not achieving a sufficient index jump by performing exponential search, then we might end up having more comparisons than classical merge procedure. But what is a sufficient jump?
It is decided to be a constant $7$, it is called **minimum gallop**(MIN_GALLOP). Other than that we have a new variable called _current_min_gallop_, which is assigned to constant MIN_GALLOP(7) at the start of the algorithm.
1. If we find that we are taking elements from one array more than or equal to _current_min_gallop_ times consistently, then we enter into galloping mode.
2. After entering into galloping mode, we continue to remain into galloping mode if and only if we find that we have a jump of more than or equal to MIN_GALLOP(7). Otherwise we exit gallop mode. If galloping mode is a success, then we decrease _current_min_gallop_ by $1$ per one success. It promotes galloping mode.
**Note:** In galloping mode we are using MIN_GALLOP constant to check for the success of galloping mode and to enter into galloping mode we are using _current_min_gallop_ variable.
3. After we exit galloping mode, we increase _current_min_gallop_ by one, to discourage a return to galloping mode again. This is to make sure that next we recover the lost of more comparisons.
So, **we are trying to balance the whole situation by taking the advantage of galloping.** Sometimes, the value of _current_min_gallop_ becomes so large that we never enter into galloping mode.
The whole procedure discussed above will be helpful in merge procedure. Now, we come back to pure galloping.
Note that we can do galloping(exponential search) from any side of the array, either left or right, because we are just intended to find a position and that can be approached by doing exponential search from any side.
The starting position for the search is called a "$hint$". Sometimes it is better to search from left and sometimes from right. For the given array below,
If we want to find position for $3$, then starting from index 0($hint = 0$) is efficient, but if we are looking for position of $13$ then starting from index $len-1$($hint = len-1$) is more efficient.
We have two types of galloping function `gallopRight` and `gallopLeft`, the main difference between them is, `gallopRight` and `gallopLeft` returns rightmost index and leftmost index respectively in case if there are equal elements.
If you find a position of 13 by using galloping, then `gallopLeft` will return 6, but `gallopRight` will return 9.
In simple mode it is trivial to maintain stability, but to maintain stability while merging in galloping mode, we sometimes use `gallopRight` and sometimes `gallopLeft`. Just to get the basic idea, see the below example, it will be more clear when we will the merge procedure.
Now, if we are finding a position of run2[0] in run1, then we will use `gallopRight`, but if we are finding a position for run1[3] in run2, then we will use `gallopLeft`.
After performing this operation you notice that all elements of $run_2$ are less than last element of $run_1$ and first element fo $run_1$ is greater than first element of $run_2$, i.e. $run_1[base_1] > run_2[base_2]$. These implies two things:
Conclusion 1. The last element of $run_1$ is the largest element.
Conclusion 2. The first element of $run_2$ is the smallest element.
Now, Let say we are merging two sorted arrays of size _$len_1$_ and _$len_2$_. In traditional merge procedure, we create a new array of size $len_1$+$len_2$. But in Tim sort's merge procedure, we just create a new temporary array of size $min(len1,len2)$ and we copy the smaller array into this temporary array.
Now, suppose the $len_1$ is less than $len_2$, then we will create a temporary copy of $run_1$. To merge them, we are not going to allocate any more memory, but we will merge them directly into the main array, in **left-to-right** direction. In the other case($len_2$ < $len_1$), we will merge them in **right-to-left** direction.
**The reason for different directions is that, by doing this we are able to do merging in the main list itself.** You will be able to see this when we will see `merge_LtoR` and `merge_RtoL`.
2. Start by merging them in a classical way, until one run seems to be contributing elements consistently, let's call it a win. If one run wins for more than _cur_minGallop_ times consistently, then we enter into galloping mode.
3. We stay into galloping mode as far as it is performing good.
4. If we exit galloping mode, then we penalize by incrementing _cur_minGallop_. Then start from step-2 again.
**There are two degenerate cases, we check again and again while executing the function, which shows that merge procedure is at its end.** They are based on the two conclusions we have alredy discussed: