- 1. Overview
- 2. Etymology
- 3. Cultural Impact
Insertion sort. Itâs a thing. A simple one, at that. Builds a sorted array piece by piece, comparing each new element to the ones already settled. Don’t expect it to dance circles around the heavy hitters like quicksort or heapsort when youâve got a mountain of data. But for a small, manageable pile? It has its charms.
For starters, itâs ridiculously easy to implement. Jon Bentley himself managed to condense a version into three lines of C-like gibberish. Five if youâre being a bit more thorough. Itâs efficient for those smaller data sets, much like other algorithms that take a quadratic, O (n²) approach. And surprisingly, it often outperforms its simpler quadratic cousins like selection sort or bubble sort . Though, mind you, the dataâs initial state and the cost of reading/writing matter. If things are a complete mess and writing is expensive, selection sort might just leave it in the dust.
Itâs also adaptive . If your data is already mostly in order, it doesn’t break a sweat. Itâs quick, with a time complexity of O (kn) if each element is only a few spots away from its final resting place. And itâs stable ; the order of equal elements remains undisturbed. Plus, itâs in-place , meaning it barely uses any extra memoryâjust a constant O(1) auxiliary space. Oh, and it’s online , capable of sorting a stream of data as it arrives.
Think about how youâd sort a hand of bridge cards. Most people naturally use something akin to insertion sort. Itâs intuitive, really.
Algorithm
Picture this: a graphical representation of insertion sort. Youâve got a partially sorted list, initially just the first element. With each pass, an element is plucked from the unsorted pile and slotted into its correct position within the growing sorted sequence. This continues until the input is exhausted.
The sorting usually happens right there in the array, working from left to right. As it progresses, it expands the sorted section. At each position, it glances at the value and compares it to the largest value already sorted (which is conveniently right next to it). If the new value is larger, itâs left alone, and the algorithm moves on. If it’s smaller, it hunts for the right spot in the sorted section, nudges all the larger values over to make room, and then drops the new value into place.
After k iterations, the first k + 1 elements are sorted. The algorithm takes the next available element from the input and inserts it into its proper home within the already sorted portion.
The most common way this plays out with arrays can be described like this:
- Imagine a function, letâs call it
Insert. Its job is to take a value and wedge it into a sorted sequence at the beginning of an array. It does this by starting at the end of the sequence and shifting elements one by one to the right until it finds the perfect spot for the new value. This process overwrites whatever was sitting immediately after the sorted sequence. - To perform the insertion sort, you start at the far left of the array. For each element you encounter, you call
Insertto place it correctly within the sorted sequence that’s accumulating at the start of the array. Each insertion replaces a single value: the one being inserted.
Hereâs a taste of the pseudocode , assuming zero-based indexing:
| |
The outer loop marches through every element but the first, because a single element is inherently sorted. The inner loopâs mission is to move A[i] to its rightful place, ensuring that the first i+1 elements are sorted after it finishes. Crucially, the and operator needs short-circuit evaluation
. Otherwise, if j happens to be 0, youâll hit an array bounds error
trying to access A[-1].
If you expand the swap operation, replacing it with a temporary variable, you can shave off a bit of time. This version moves A[i] to its spot in one go, performing only one assignment in the inner loop:
| |
This revised inner loop meticulously shifts elements to the right, carving out the space needed for x, which was originally A[i].
Itâs also possible to implement this recursively. The recursion essentially takes the place of the outer loop. It calls itself, pushing progressively smaller values of n onto the stack. Once n hits 0, the function unwinds, executing the code after each recursive call, starting with n = 1 and incrementing. The initial call would look something like insertionSortR(A, length(A)-1).
| |
This recursive approach doesn’t make the code any shorter, nor does it improve execution speed. What it does do is increase the memory overhead from O(1) to O(N) due to the call stack.
Best, Worst, and Average Cases
When the input array is already sorted, that’s the best case for insertion sort, and it runs in linear time, O(n). Each element is compared only with the last element of the sorted portion.
The worst case? An array sorted in reverse order. Or any array where each element is smaller than or equal to the second-smallest of the preceding elements. In these scenarios, every inner loop iteration has to scan and shift the entire sorted portion before inserting the next element. This brings the running time to quadratic, O(n²).
The average case is also quadratic, making insertion sort impractical for large datasets. However, itâs surprisingly quick for very small arrays, often beating even quicksort . In fact, many efficient quicksort implementations switch to insertion sort for subarrays below a certain size threshold (typically around ten elements, though this needs empirical testing).
Letâs trace an example: sorting {3, 7, 4, 9, 5, 2, 6, 1}. The underlined number is the one being considered in each step. An asterisk marks the element that was just moved or left in place.
| Step | Array | Notes |
|---|---|---|
| 1 | 3 7 4 9 5 2 6 1 | Initial state |
| 2 | 3 7* 4 9 5 2 6 1 | 7 is larger than 3, stays put. |
| 3 | 3 7 4* 9 5 2 6 1 | 4 is smaller than 7, shift 7. 4 < 3? No. Insert 4. |
| 4 | 3 4 7 9* 5 2 6 1 | 9 is larger than 7, stays put. |
| 5 | 3 4 7 9 5* 2 6 1 | 5 < 9, shift 9. 5 < 7, shift 7. 5 < 4? No. Insert 5. |
| 6 | 2* 3 4 5 7 9 6 1 | 2 < 5, shift 5. 2 < 4, shift 4. 2 < 3, shift 3. 2 < 2? No. Insert 2. |
| 7 | 2 3 4 5 6* 7 9 1 | 6 is larger than 5, stays put. |
| 8 | 1* 2 3 4 5 6 7 9 | 1 < 6, shift 6. 1 < 5, shift 5. 1 < 4, shift 4. 1 < 3, shift 3. 1 < 2, shift 2. 1 < 1? No. Insert 1. |
Relation to Other Sorting Algorithms
Insertion sort shares similarities with selection sort . In both, after k passes, the first k elements are sorted. The key difference lies in their scanning strategy. Selection sort scans the entire unsorted portion to find the absolute smallest element, while insertion sort scans backward from the current element. This means selection sort guarantees the first k elements are the k smallest overall, whereas insertion sort simply ensures the first k elements of the original input are now sorted amongst themselves.
The main advantage of insertion sort over selection sort is its ability to be more efficient when the data is partially sorted. Selection sort always has to examine every remaining element. Insertion sort, however, can finish quickly if the current element is larger than the last sorted element. In many cases, insertion sort performs roughly half the comparisons of selection sort on average.
In the worst case (reverse-sorted array), insertion sort performs just as many comparisons as selection sort. But here’s the catch: insertion sort requires significantly more writes. Each insertion involves shifting multiple elements, leading to O(n²) writes. Selection sort, on the other hand, performs only one swap per pass, resulting in O(n) writes. This makes selection sort a better choice when memory writes are costly, like with EEPROM or flash memory .
While divide-and-conquer algorithms like quicksort and mergesort dominate for larger datasets, simpler sorts like insertion and selection sort are often faster for very small arrays (typically between 7 and 50 elements). This leads to hybrid approaches where these algorithms are used for small subarrays generated by the more complex ones.
Variants
D.L. Shell introduced significant enhancements, resulting in Shell sort . This algorithm compares elements separated by a decreasing gap. Shell sort offers much improved practical performance, with common variants achieving O(nÂł/²) and O(nâ´/Âł) running times.
If comparing elements is more expensive than swapping themâthink string keys or human judgmentâthen binary insertion sort might be the way to go. It uses binary search to pinpoint the insertion spot, requiring only âlogânâ comparisons in the worst case. For the entire sort, this leads to O(n log n) comparisons. However, the overall algorithm remains O(n²) on average due to the shifting required for each insertion.
The number of swaps can be reduced by calculating the final positions of multiple elements before moving them. This approach, similar in spirit to merge sort , can cut down swaps by about 25% for random data.
Binary merge sort is another variant. It uses binary insertion sort for small chunks (e.g., 32 elements) and then applies merge sort for the final pass. This cleverly combines the strengths of both algorithms.
To bypass the costly element shifting in arrays, data can be stored in a linked list . Splicing elements in or out takes constant time once the position is known. The downside? Searching a linked list is sequential, meaning it can’t leverage faster methods like binary search. This keeps the search time at O(n) and the overall sorting time at O(n²). More advanced data structures like heaps or binary trees can speed up searching and insertion, forming the basis of heap sort and binary tree sort .
In 2006, Bender, Martin Farach-Colton , and Mosteiro proposed library sort , or gapped insertion sort. This method intentionally leaves unused spaces (gaps) within the array. Insertions only need to shift elements until a gap is encountered, significantly reducing the work. Their research suggests this algorithm achieves O(n log n) time with high probability.
Using a skip list can further reduce insertion time to O(log n) and eliminates the need for swaps due to its linked-list implementation. The overall insertion sort time then becomes O(n log n).
List Insertion Sort Code in C
If your items are chained together in a linked list, you can sort it using only O(1) extra space. The process begins with an empty, sorted list. Items are taken one by one from the input list and inserted into their correct position in the sorted list. Once the input list is empty, the sorted list holds the final result.
| |
The algorithm below utilizes a trailing pointer for more efficient insertion into the sorted list. A simpler recursive method rebuilds the list at each step, consuming O(n) stack space.
| |