Explanation of insertion sort
Most people intuitively try to order items in ascending order this way. Let’s see how.
How do we sort?
Imagine that we need to order some numbers randomly written in a line from smallest to greatest.
Say that these numbers are the following: 5, 3, 10, 4 and 7.
What most people intuitively do is that they start to look at the numbers from the left and if they find a number that is less than those before, they will look for the correct position of the current number.
The first two numbers are 5 and 3. 3, which is at the second position is smaller than 5, therefore we need to move 3 to the left, so the order of the numbers are now 3, 5, 10, 4, 7.
The first two numbers are in the right order, let’s move on to the third one (10). 10 is greater than both 3 and 5, so the order of the first three numbers (3, 5, 10) is already correct.
The fourth number is 4. Unfortunately it’s smaller than the third one (10), so we definitely need to shift it to the left, hence we get 3, 5, 4, 10 for the first four numbers. But that’s still not good, since 4 is greater than 5 and because it’s on the right of 5, we will need to move 4 further to the left. This way the order of the first four numbers will become correct (3, 4, 5, 10).
Let’s incorporate the 5th number (7) as well and find its position compared to the first four numbers. 7 is smaller than 10 (the fourth number in the line), so we need to shift it to the left. The numbers are now 3, 4, 5, 7 and 10. If we quickly compare 7 to the number on its left (5), we will see that 7 is at the right position as it’s greater than 5. We don’t have worry about the rest of the numbers on the left as they are already sorted. We are done, all numbers are correct order.
Description of the algorithm
Let’s inspect the algorithm in general. Say that we have n numbers and call our current number (the one we want to put in the right position) x. We compare x to the ones on its left until we find a number that is smaller than x or we get to the start of the line. When we find such a number (or get to the start of the line), we will insert x in that position and will shift all other numbers to the right by one.
We need to repeat this process with each number and eventually we will get a nicely sorted set of numbers.
It seems to be a good idea to include the numbers we want to sort in an array. x is always the current number and sooner or later each number has to be the current one as we need to inspect each element, which means that we need to iterate over the array and compare each element to the ones on their left. This implies that we will need another iteration: We should to loop over the elements to the left of the current number (x) in order to determine its correct position relative to the numbers on its left.
As we keep moving to the right number by number, the order of the numbers to the left of the current number will always be correct (we have already sorted them in the previous iterations) and our job at each iteration is to find the right position for the current number among the already ordered numbers on the left. Eventually we will get to the rightmost (n_th) number and will find that the rest of the numbers on the left (_n - 1 numbers) have been already sorted. We need to find the correct position for the n_th number, so we iterate over the _n - 1 numbers of the left and find the first number smaller than the _n_th one.
I know that it might sound a bit confusing but once it clicks, it will be very logical to you.
JavaScript code
Let’s move on to the code. I will show one possible solution and will then go over the code line by line.
Our function is called - you can guess - insertionSort
and we also have the numbers
array:
const numbers = [4, 1, 3, 5, 6, 2];
function insertionSort(arr) {
for (let i = 1; i < arr.length; i++) {
const current = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > current) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = current;
}
return arr;
}
Yeah, it’s a lots of i
-s and j
-s but what’s going on here?
We start with a for
loop, which always gives us the current number. The iteration starts at the second element of numbers
(i = 1
) and mark that element (1 in our case) as current
.
Now we need to compare current
to the number on its left and this is where the second iteration comes in the picture. Our second iteration is a while
loop, where j
, which also refers to the indexes of numbers
, can’t be lower than 0 (since the first element of the array has index 0). The initial value of j
will be the index of the left neighbour of current
.
The magic is just about to happen. If the arr[j]
, which is 4 in our case, is greater than the current
number (1), we shift 4 to the right by one. The arr[j + 1] = arr[j]
assignment does exactly that. At this very moment we have the following numbers in the array: 4, 4, 3, 5, 6, 2.
Where is 1? It’s stored in current
and will get back to it a bit later.
While still inside the while
statement, we decrease the value of j
by one on each iteration, in this case j
will be -1.
The while
loop won’t continue because j >= 0
won’t fulfil, so we move on to the last part of the code, where the arr[j + 1] = current
assignment happens. Here we add back that 1
to j
, hence arr[j + 1]
will be the first element of the array (index 0) and we make this element equal to current
, where we initially stored the value of 1. This is how 1 returns to the array. The order of the numbers after this step is [1, 4, 3, 5, 6, 2]
.
The for
loop continues with the next element, i = 2
on this iteration. current
will be 3, the third element (index 2) of numbers
and j
is initially 1. Let’s have a look at the most difficult part of the code, which is the while
loop.
Since j >= 0
and arr[1]
(which is 4 now) is greater than current
(3), both conditions are met, so the statements inside the while
block will execute.
arr[j + 1] = arr[j]
assigns the third element (index 2 as j
equals 1) of numbers
the value of arr[1]
(4), so we shift 4 to the right again. The numbers at this moment are 1, 4, 4, 5, 6, 2, the missing number 3 is stored in current
. Let’s decrease j
by 1, so it will become 0
. Back to the condition of the while
loop: j >= 0
is met but arr[j] > current
is not as arr[0]
equals 1 and current
is 3.
So we jump to the statement after the while
block. arr[j + 1]
will refer to arr[1]
as the value of j
is 0 at the moment, so we assing the stored value of 3 (current
) to the second element (arr[1]
) of numbers
. After this step our array is the following: [1, 3, 4, 5, 6, 2]
.
Almost there! The next two elements, 5 and 6 are in the correct position compared to the numbers on the left (1, 3, and 4), so the second condition of the while
statement (arr[j] > current
) won’t be met. arr[j + 1] = current
will leave the numbers unchanged as it simply adds 1 back to j
, so it simply neutralizes the effect of subtracting 1 at the j = i - 1
step.
We arrive at the last value of numbers
, which is 2. This will be the value of current
and j
will initially be 4.
The conditions of the while
loop are both met (4 >= 0 and arr[6]
, which is 6 is greater than 2). Again, we shift 6 to the right by one, so it will go to its final, last position. The order of the numbers will be 1, 3, 4, 5, 6, 6 now. After decreasing j
by 1, arr[j]
will refer to the index 3 element (5), which is also greater than current
(2), so we shift 5 to the right by one as well, therefore we will get 1, 3, 4, 5, 5 and 6.
We keep shifting numbers greater than 2 to the right until we get to the first number that is less than 2. This will be the first element of the array (1, j
will be 0 here). At this very moment the numbers will be in the order of 1, 3, 3, 4, 5, 6. The conditions of the while
loop are not met anymore, so we make the final correction and make arr[j + 1]
(i.e. the second element as j
is 0) equal to the stored value of current
and finally our numbers are in ascending order.
As a last step we return arr
in which the numbers are correctly ordered.
Considerations
As I mentioned above, insertion sort is not the most efficient sorting algorithm. It contains two iterations and iterations are expensive.
The best case scenario is when the numbers are already ordered. This time we simply iterate over the numbers (for
loop) and the conditions of the while
loop will never be met as the elements are already in the right order. We have to iterate over the numbers once, so the time the function needs to run for only depends on the number of the elements in the array.
On the contrary, when the numbers are in reverse order (worst case), both the for
loop (it will always run no matter what the order of the numbers is) and the while
loop will run every time. This time we need to deal with nested iterations with one loop being inside the other and nesting will make insertion sort an expensive algorithm. It’s true though that the probability of having the numbers in reverse order is quite small (1 / 720) and in most cases the while
loop won’t run on every for
iteration as the example above proves.
A more detailed analysis of insertion sort in terms of time complexity will be covered in a later post.
Conclusion
Insertion sort is the most natural way of sorting elements in ascending order. Unfortunately, it’s not the most efficient sorting algorithm, for example quicksort is faster most of the time.
The code itself is not easy to digest in my opinion and it takes some time to understand. This is not the only correct solution though. I challenge the reader to find another valid insertion sort solution but the code might be longer or more difficult than the one above.
I hope that this post could help you understand insertion sort. If so, see you next time.