Patterns.

Sorting Algorithms: A Comprehensive Guide

Discover how sorting algorithms, from bubble to quick, merge, heap, shell, and non‑comparison methods like counting, radix, and bucket, organize data efficiently, highlighting speed, memory use, and stability.

Introduction to Sorting Algorithms

Sorting algorithms are fundamental to computer science and programming, used to arrange data in a particular order (usually ascending or descending). At a high level, sorting refers to reorganizing a list of elements so that they follow a sequence (like numerical or lexicographical order). There are many sorting algorithms, each with different performance characteristics and use cases. Some sorting algorithms work entirely in memory (internal sorting), while others are designed to handle data that can’t fit into memory (external sorting). Internal sorting takes place within a computer’s main memory and is usually faster due to quick access times. External sorting, on the other hand, deals with massive datasets by storing and sorting parts of data on slower storage (like disks) and then merging the results, which is common in database systems and large-scale data processing.

In this article, we will explore various sorting algorithms, dividing them into two categories: Comparison-Based Sorting Algorithms and Non-Comparison-Based Sorting Algorithms. Each sorting method will be explained with its key mechanics, typical use cases, and a clear understanding of how it works. Pseudocode snippets will be provided only when necessary to clarify the process. This guide aims to be tutorial-like in tone but sufficiently clear and formal for reference. We use American English spelling and ensure WordPress-compatible formatting, including HTML entities for special characters.

Deciding Which Sorting Algorithm to Use

Sorting algorithms are a rich area of study, with each algorithm offering different benefits and trade-offs. Comparison-based algorithms like Bubble Sort, Insertion Sort, and Selection Sort are simple and work by comparing elements, but can be slower on large inputs (typically O(n^2) time). More advanced comparison sorts like Merge Sort, Quick Sort, Heap Sort, and Shell Sort offer better performance on larger data sets, with merge and quick sorts often being the default choices for many programming libraries due to their efficiency in practice (merge sort offering stability and predictable performance, quick sort offering speed and in-place convenience on average). Non-comparison sorts like Counting Sort, Radix Sort, and Bucket Sort can achieve linear time sorting by leveraging the structure of the data values (like integer ranges or digit positions) rather than making direct comparisons. These are invaluable for special cases like sorting large numbers of integers or fixed-format data.

In practical programming, choosing the right sorting algorithm depends on the data at hand: its size, distribution, whether it fits in memory (internal vs external sorting needs), whether stability is needed, and what the performance considerations are (average case vs worst case). Understanding these algorithms allows a programmer or computer scientist to make informed decisions and to recognize opportunities to improve sorting performance in everyday tasks or specialized applications. Whether you’re implementing a sort from scratch or using a library function, knowing how these algorithms work gives insight into the cost and behavior of sorting operations in your code.

For a more technical sheet to help you implement the right sorting algorithm, check our Sorting Algorithms: A Technical Reference Sheet and Comparison Chart.


The Algorithms Guide

This article is part of a comprehensive guide about algorithms. To access the main article and read about other types of algorithms, from sorting to machine learning, follow the following link:

A Friendly Guide to Computer Science Algorithms by Category.


Comparison-Based Sorting Algorithms

Comparison-based sorting algorithms determine order based on pairwise comparisons between elements. These algorithms compare elements, often using operators like “less than” or “greater than”, and then swap or reposition elements accordingly. They generally have a minimum time complexity of O(n log n) for comparison sorting due to theoretical limits (as proven by decision tree models). The following are well-known comparison-based sorts:

Bubble Sort

How it works: Bubble Sort is one of the simplest sorting algorithms. It repeatedly steps through the list, compares adjacent pairs of elements, and swaps them if they are in the wrong order. This “swapping” step causes larger elements to gradually “bubble up” to the end of the list while smaller elements sink towards the beginning. The algorithm makes multiple passes through the list until no swaps are needed, which indicates the list is sorted.

  • Mechanics: On each pass, bubble sort starts at the beginning of the array and compares element i with element i+1. If the element at i is greater than the one at i+1, it swaps them. It then moves one position over and repeats this comparison and swap process for the next pair, continuing until it reaches the end of the array. With each full pass, the largest unsorted element is guaranteed to move to its correct sorted position at the end.
  • Use Cases: Bubble sort has very limited practical use due to its inefficiency on large lists (time complexity around O(n^2) in the worst and average cases). It is primarily used as an educational tool to introduce sorting concepts, since its behavior is easy to visualize and understand. It’s sometimes used in specific scenarios like small data sets or when the data is nearly sorted (where it can perform close to O(n) if only a few elements are out of place). There are also niche cases in parallel computing or hardware where a form of bubble sort can be implemented in parallel to achieve good results, but those are specialized situations.

Bubble Sort pseudocode (for clarity):

for i from 0 to n-2:
    for j from 0 to n-2-i:
        if array[j] > array[j+1]:
            swap(array[j], array[j+1])

This double loop ensures every pair is checked and swapped appropriately, with each outer loop pass placing the next largest element in its final position at the end.

Insertion Sort

How it works: Insertion sort builds a sorted list one element at a time. It works like sorting a hand of playing cards: take the next card (element) and insert it into the correct position among the cards already in hand (the sorted portion of the array). The array is conceptually divided into a “sorted” part and an “unsorted” part. Initially, the sorted part just contains the first element of the array, and the unsorted part contains the rest. The algorithm then picks the first element in the unsorted part and inserts it into the correct position in the sorted part, shifting larger elements to the right as needed. This process repeats for each element in the unsorted part until the entire array is sorted.

  • Mechanics: For each element in the array (starting from the second element), insertion sort compares it with elements to its left (the sorted portion). It keeps moving the element toward the left until it finds the proper spot where the element is no longer smaller than an element to its left. Every time an element is shifted to the right to make space, insertion sort is effectively “inserting” the element into its correct sorted position.
  • Use Cases: Insertion sort is efficient for small arrays or lists that are already mostly sorted. Many real-world libraries or sorting functions will use insertion sort for small data sets (often as a part of more complex sorts like Timsort or Introsort). It’s particularly useful when you have a continuous influx of data and want to keep it sorted in real-time (like sorting a hand of cards as new cards are dealt). For example, in certain online algorithms or streaming data scenarios, insertion sort can insert new elements into an already sorted list with minimal overhead.

Insertion sort is in-place (needing no extra array space for many implementations) and stable (it preserves the relative order of equal elements). Here’s a brief pseudocode outline:

for i from 1 to n-1:
    key = array[i]
    j = i - 1
    while j >= 0 and array[j] > key:
        array[j+1] = array[j]  // shift element right
        j = j - 1
    array[j+1] = key

This moves each element key into its correct position among the previously sorted elements.

Selection Sort

How it works: Selection sort sorts an array by repeatedly finding the minimum element from the unsorted portion and moving it to the sorted portion. It maintains two subarrays within the list: one sorted (initially empty) and one unsorted (initially the entire list). On each iteration, the smallest element (for ascending order sorting) from the unsorted subarray is selected and swapped with the first element of the unsorted subarray, expanding the sorted subarray by one and reducing the unsorted portion by one.

  • Mechanics: Starting from the first position in the array, selection sort looks for the smallest value in the array. Once found, it swaps this smallest value with the value at the first position. Then it moves to the second position, looks for the smallest value in the remainder of the array (positions 2 through n), and swaps it with the value at the second position. This process continues, placing each subsequent smallest element into its correct position in the sorted portion of the array.
  • Use Cases: Selection sort is noted for its simplicity and the fact that it makes at most n-1 swaps, which is beneficial when write operations (like swapping) are costly. For instance, when memory writes are at a premium (like in flash memory or EEPROM where write cycles are limited), the minimal swapping characteristic is useful. However, selection sort still performs O(n^2) comparisons, which makes it inefficient for large data sets. It’s sometimes used when memory is very limited because it requires only O(1) additional space. Outside of such special scenarios, selection sort is generally outperformed by other O(n^2) algorithms like insertion sort or by more efficient algorithms for larger inputs.

Note: Standard selection sort is not stable (equal elements might not preserve their input order after sorting) because it involves swaps that can move identical elements relative to each other. However, there are stable variants of selection sort that insert the element into the beginning of the unsorted array instead of swapping, though these variants typically require more complex data structures or additional memory.

Merge Sort

How it works: Merge sort is a classic divide-and-conquer sorting algorithm. It works by dividing the array into two halves, recursively sorting each half, and then merging the two sorted halves back together. The merging process is where the core work happens: given two sorted subarrays, it efficiently combines them into a single sorted array by repeatedly picking the smaller of the two “front” elements of the subarrays and adding it to a new array.

  • Mechanics: The algorithm splits the list into halves until single-element sublists are reached (a list of one element is naturally sorted). Then, the merge sort algorithm merges these small sorted lists into larger ones. During the merge step, it takes two sorted sublists and compares their front elements, choosing the smaller (or larger for descending order) to put next in the merged output, and moves the pointer forward in that sublist. This process continues until all elements from both sublists are merged into a fully sorted list.
  • Use Cases: Merge sort is known for its reliable O(n log n) performance on all data sets (best, average, and worst case are all O(n log n)). It’s especially useful for sorting linked lists (due to sequential access patterns) and for external sorting (when working with data on disk) because of its sequential read/write patterns which are cache-friendly. Many languages’ sorting functions use merge sort or variations of it (like Timsort, which is a combination of merge sort and insertion sort) because of its stable sorting (it does not reorder equal elements) and predictable runtime. Real-world applications include sorting large log files, data that is too large to load entirely into memory (external merge sort is often used in databases and big data processing frameworks), and any scenario where stability (preserving input order for equal keys) is important, such as sorting records by multiple fields (e.g., sort by last name, then first name – stability ensures that if last names are equal, the original order by first name is preserved unless changed in a subsequent sort phase).

Merge sort requires additional memory for the merging process (usually O(n) auxiliary space for arrays), although there are in-place merge sort variations that are quite complex. The straightforward implementation is not in-place.

Quick Sort

How it works: Quicksort is another divide-and-conquer algorithm and is often the go-to algorithm for general-purpose sorting due to its excellent average performance. The algorithm picks an element as a “pivot” and partitions the array around that pivot. The partition step rearranges the array so that all elements less than the pivot come before it and all elements greater than the pivot come after it. After partitioning, the pivot element is in its final sorted position. The algorithm then recursively sorts the subarrays on either side of the pivot.

  • Mechanics: There are different ways to choose the pivot (e.g., always pick the last element, pick a random element, pick the median-of-three, etc.). Once the pivot is chosen, the array elements are compared with the pivot and swaps are made to ensure that eventually the pivot sits between the two partitions (all small elements on the left and all large ones on the right). After partitioning, the pivot is at the correct sorted position. The algorithm then repeats this process for the left subarray (elements less than pivot) and right subarray (elements greater than pivot).
  • Use Cases: Quicksort’s average time complexity is O(n log n) and it tends to be faster in practice than other O(n log n) algorithms like merge sort due to low overhead and good cache performance (it has good locality of reference since it often works in-place). It’s widely used in system libraries and frameworks (with tweaks to avoid worst-case scenarios). For example, the C standard library qsort and many implementations of language-specific sort functions (like in C++ sort, older versions of Java’s sort) rely on quicksort or variations of it. However, a naive quicksort has a worst-case time complexity of O(n^2) (for instance, when the pivot is consistently chosen poorly such as always the smallest or largest element). In practice, this is mitigated by using random pivots or the “median-of-three” method, and by switching to other algorithms for small subarrays (hybrid algorithms like IntroSort start with quicksort and switch to heap sort if recursion depth is too high to avoid worst-case, and to insertion sort for tiny arrays). Real-world use cases include sorting any in-memory array or list of items by a certain key, such as sorting records by a particular field, ordering tasks by priority, etc. Quicksort is not stable by default (equal elements may be reordered relative to each other through the swapping operations).

Heap Sort

How it works: Heap sort leverages the heap data structure (often a binary heap) to sort an array. It visualizes the array as a binary tree (heap) and repeatedly extracts the largest element from the heap (for ascending order sort) and places it at the end of the array. The algorithm has two main phases: building a heap out of the input data, and then repeatedly removing the largest element from the heap and adjusting the heap to maintain the heap property (this step is often called “heapify”).

  • Mechanics: First, the input array is transformed into a max-heap, which is a binary tree where every parent node is greater than or equal to its children (ensuring the largest element is at the root of the tree, i.e., index 0 of the array). Building this heap can be done efficiently in-place. Once the max-heap is constructed, the algorithm repeatedly swaps the root of the heap (which is the largest element) with the last element in the current heap, and then reduces the considered heap size by one (effectively placing that largest element at the end of the array in its final sorted position). After each swap, the heap property might be violated (the new root might be smaller than its children), so a “heapify” operation is done to restore the max-heap order for the remaining elements. This continues until the heap size is reduced to 1.
  • Use Cases: Heap sort has a consistent O(n log n) time complexity for best, average, and worst cases. It’s very useful when you need a guaranteed upper bound on runtime (unlike quicksort’s worst-case O(n^2)). Heap sort is in-place (doesn’t need much additional memory beyond the original array) but it is typically not stable (the swapping can reorder equal elements). One common use case is in systems where worst-case performance guarantees are important. Also, heaps (and thus heap sort) are closely related to priority queues – for example, every time you poll the highest priority element from a priority queue you are essentially performing part of a heap sort. In practice, though, heap sort often isn’t used as a standalone sorting method in high-level libraries because quicksort (or introsort) tends to be faster on average due to better cache usage. However, understanding heap sort helps with understanding priority queues and vice versa. It is also used in cases where sorting is interwoven with other operations that naturally use a heap (like continuously receiving a stream of data and needing to emit sorted output up to the current point, etc.).

Shell Sort

How it works: Shell sort is an optimization of insertion sort that allows the exchange of far apart elements to speed up the sorting process. It works by initially sorting pairs of elements far apart from each other, then progressively reducing the gap between elements to be compared. By starting with widely spaced elements, shell sort can move out-of-place elements into correct position faster than simple adjacent swaps (as in insertion sort or bubble sort). The algorithm uses a sequence of gaps (like 5, 3, 1 or other sequences such as the Knuth sequence) and performs a gapped insertion sort for each gap value.

  • Mechanics: Choose an initial gap (for example, half the length of the list, rounded down). Then, for each element index i from the gap value to the end of the list, compare the element at position i with the element at position i-gap, i-2gap, i-3gap, and so on, moving the element at i back into the position where it is larger than the previous but smaller than the next, effectively doing a gapped insertion sort. After this first pass with a large gap, reduce the gap and repeat the process. Continue reducing the gap until it finally becomes 1, at which point a standard insertion sort is performed as the final step. By the time the gap is 1, the list is already “mostly sorted”, which makes the final insertion sort efficient.
  • Use Cases: Shell sort is useful in practice for medium-sized arrays where using O(n^2) algorithms would be too slow, but the overhead of more complex O(n log n) algorithms might not be worth it. Its performance heavily depends on the choice of gap sequence. In the best cases (with good gap choices), Shell sort can perform much better than O(n^2). For instance, using the Knuth sequence (gaps like 1, 4, 13, 40, … calculated by the formula 3^k – 1), the algorithm performs quite efficiently for many input sizes. Shell sort was widely used in older computing systems when memory was at a premium and dynamic allocation for recursion (like required by merge sort or quicksort) was difficult. Today, it’s less common in standard libraries, but still taught and occasionally used when a simple, in-place, and reasonably efficient sort is desired without the need for recursion. It’s also an interesting algorithm for demonstrating the impact of gap sequences and partially ordered data on sorting efficiency.

Shell sort is not guaranteed to run in O(n log n) for all gap sequences (in fact, finding an optimal gap sequence for all cases is still somewhat open). However, with good sequences it can approach O(n log^2 n) or better in practical scenarios.

Non-Comparison-Based Sorting Algorithms

Non-comparison sorts don’t compare elements directly to decide their order. Instead, they exploit the structure of the data (like the range of values, number of digits, etc.). These algorithms can achieve linear time complexity O(n) under certain conditions, making them extremely fast for specific use cases. The trade-off is that they often require additional memory and are not as general-purpose (they might only work for integers or have constraints on the data values). Key non-comparison sorts include Counting Sort, Radix Sort, and Bucket Sort.

Counting Sort

How it works: Counting sort is an integer sorting algorithm that operates by counting the occurrences of each value in the input array. It assumes the input consists of integers within a known range (for example, 0 to 100, or 1 to K). The algorithm creates an auxiliary array (often called count) of length equal to the range of input values. It then iterates through the input array and for each element, it increments the count at the index corresponding to that element’s value. After counting all elements, it then modifies the count array such that each element at each index in the count array contains the sum of the previous counts; this step effectively gives the positions of the values in the sorted order. Finally, it iterates through the input array again (often in reverse order to maintain stability) and places each element into its correct sorted position in an output array, decreasing the corresponding count each time an element is placed.

  • Mechanics: Suppose we have an array of numbers and we know they range from 1 to 50. We initialize a count array of size 50 (or 51, if we use 1-indexing for convenience). In the first pass, each time we see a number v, we do count[v]++. Now, count[i] tells us how many times i appears. The next step is to transform this count array into a prefix sum array: for each i from 1 to 50, do count[i] = count[i] + count[i-1]. After this, count[i] will tell us how many elements are <= i. If we have an output array, we can then place each element x from the input array to its sorted position by looking up count[x] (which tells us the last index where x should appear in the sorted array), placing x there, and then decrementing count[x] to indicate that position is now filled.
  • Use Cases: Counting sort excels in situations where the range of potential items (K) is not significantly larger than the number of items (n). For example, sorting grades (0-100) for a huge number of students, sorting age data (like ages 0-120), or sorting pixels in an image (intensities 0-255). It’s often used as a subroutine in other algorithms like Radix Sort. Counting sort is stable by nature if implemented carefully (preserving the order of equal elements as they appeared in the input, which is important when sorting items by one key among multiple attributes). It’s important to note that counting sort can be extremely space-heavy if the range of input values is much larger than the number of items to sort. For instance, sorting the values {1000, 1000000} directly with counting sort would be very inefficient because the range is large with few actual elements. But for dense distributions like sorting every number between 1 and 100, counting sort is optimal.

Also, counting sort operates in O(n + K) time where n is the number of elements and K is the range of input values. This can be O(n) if K is in the same order as n (for example, K = 100 and n = 1000, etc.). The absence of comparisons allows it to break the typical O(n log n) barrier that comparison sorts cannot cross.

Radix Sort

How it works: Radix sort sorts numbers (or other data that can be represented in digits, like strings or dates) by processing individual digits. It breaks the sorting task into sorting by each digit position. There are two main variants: Least Significant Digit (LSD) radix sort and Most Significant Digit (MSD) radix sort. The LSD approach sorts the array repeatedly by each digit from the least significant (rightmost) digit to the most significant digit, using a stable sort (like counting sort) at each digit position. The MSD approach starts from the most significant digit and works inward.

  • Mechanics (LSD example): Consider we want to sort decimal numbers in the range 0 to 999. We would start by sorting the numbers by their least significant digit (units place) using a stable algorithm like counting sort (effectively grouping numbers by their units digit). Next, we sort the numbers by the tens place, again using a stable sort, but crucially we preserve the order from the previous step for numbers with the same tens digit (this is why stability is necessary). Finally, we sort by the hundreds place. After these three passes, the list is completely sorted. Because each sorting pass is stable, the relative order of numbers with the same hundreds and tens is determined by their units, and the relative order of those with the same hundreds is determined by tens and units, etc., effectively resulting in a fully sorted list.
  • Use Cases: Radix sort is useful for sorting numbers, words (lexicographically sorting strings by characters), or fixed-length strings like dates (sort by year, then month, then day). It’s often used when we know the number of digits d is much smaller than n, the number of items. The typical time complexity of LSD radix sort is O(d * (n + b)) where d is the number of digits and b is the base of the numbers (for decimal numbers b is 10, for binary data b might be 2 or 256 for a byte). If the number of digits is constant or grows slowly relative to n, this is effectively linear time O(n). For example, sorting 32-bit integers using radix sort might treat each byte as a “digit” and use 4 passes (since 4 bytes = 32 bits). That’s d = 4, b = 256 possible values per byte, and so it runs in O(4 * (n + 256)), which is essentially O(n) for large n.

Radix sort relies on a stable intermediate sort (like counting sort or bucket sort) at each digit position to maintain correct ordering, and it also uses additional memory to store output of each pass. It’s not a comparison sort, and its efficiency comes from exploiting the representation of the keys. Radix sort is great for situations like:

  • Sorting large numbers of fixed-length strings (for example, sort 10 million 10-character names).
  • Sorting integers when the range is huge (e.g., 32-bit or 64-bit integers), where counting sort is impractical due to large range, but radix sort can handle them in multiple passes.
  • Any scenario where you have multiple keys to sort by: you can think of each digit as a key from least significant to most significant.

Bucket Sort

How it works: Bucket sort (sometimes called bin sort) works by distributing elements into several “buckets” and then sorting those buckets individually. It is typically used for sorting numbers that are uniformly distributed in a range. The algorithm divides the range of possible values into a number of intervals (buckets), then it assigns each element of the array to one of these buckets based on its value. After distributing all elements into buckets, each bucket is sorted (often using another sorting algorithm, which could be a simple method like insertion sort or even recursively applying bucket sort), and finally, the buckets are concatenated in order to form the sorted array.

  • Mechanics: Imagine you have an array of random real numbers between 0 and 1 (like probabilities or percentages). You could create, say, 10 buckets for the intervals [0.0-0.1), [0.1-0.2), … [0.9-1.0]. Then iterate through the list and put each number into the bucket of its range. Since each bucket now contains numbers within a small sub-range, those numbers can be sorted more easily (possibly using insertion sort which is efficient for small or nearly sorted lists). After sorting each bucket, the contents of the buckets are concatenated: bucket 0 followed by bucket 1, and so on, which yields the sorted array.
  • Use Cases: Bucket sort is excellent for data that is uniformly or fairly evenly distributed across a known range. The classic example is sorting floating-point numbers in the range [0,1) by their fractional part, as described. If the data is not uniformly distributed, some buckets may have many elements and others very few, leading to imbalanced work (and in worst cases the time degrades toward O(n^2) if one bucket contains most of the elements and you end up sorting that with a quadratic algorithm). When the assumptions hold, bucket sort runs in linear time O(n + k) where k is the number of buckets (and typically k is chosen to be on the order of n for balancing). It’s also used in graphics or simulation for things like sorting points by x or y coordinate when those are uniformly distributed, or sorting records by a key that has a uniform distribution.

Example: If you have to sort 1 million uniformly distributed random numbers between 0 and 100, you might choose 1000 buckets covering ranges of 0.1 each. Each bucket would on average get about 1000 elements (since 1,000,000/1000 = 1000). If you then sort each bucket with an O(m log m) algorithm (where m is 1000 in this average case), the total work is roughly 1000 * (1000 log 1000), which is much less than 1,000,000 log 1,000,000 for a typical comparison sort. Thus, bucket sort can be very efficient if the distribution is favorable.

One important note: bucket sort can be stable or unstable depending on how the buckets are handled and how the sort inside each bucket is performed. If stability matters (preserving input order for equal keys), one should use a stable sorting algorithm within the buckets or ensure that items going into buckets already maintain an order if necessary.


Back to the Main Algorithms Guide

To go to the main guide about algorithms follow this link: A Friendly Guide to Computer Science Algorithms by Category,

Leave a Reply

Your email address will not be published. Required fields are marked *