Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 11

Understanding Linear Time Non-Comparison Sorts

Recommended resources

Hello! Welcome back.

In our last lesson, we dissected three major comparison-based sorting algorithms: Heapsort, Merge Sort, and Quicksort. We saw that they all have an average-case time complexity of and differ in their performance guarantees, space usage, and stability. We ended with a question: is it possible to sort faster than ?

Today, we answer that question with a resounding "yes," provided we can make certain assumptions about our data. This lesson dives into the world of non-comparison sorting. Our goal is to describe how non-comparison sorts, like Counting Sort, can achieve linear time complexity. We will explore the clever trick they use to bypass the limitations of comparison-based sorting.

Why Comparison Sorts are Limited

Before we see how to break the barrier, it's helpful to understand why that barrier exists for algorithms like Quicksort and Merge Sort. At their core, they rely on a compare(a, b) operation that has only two outcomes (e.g., a < b or a >= b).

To sort unique items, there are (n-factorial) possible correct orderings (permutations). Any comparison-based algorithm must perform enough comparisons to distinguish the correct ordering from all other possibilities. This can be visualized as a decision tree where each node is a comparison, and each leaf is a permutation. To have enough leaves for all permutations, the tree must have a height of at least , which can be shown to be in .

So, how do we do better? We need to use an operation more powerful than a simple two-way comparison.

Linear Time Sorting: Counting Sort, Radix Sort, and Bucket Sort

The video 'Linear Time Sorting' explains this fundamental difference. It contrasts the '2-way fork' of comparison sorts with the 'n-way fork' that allows linear time sorting algorithms to operate more efficiently.

Watch the segment from 17:06 to 18:30. Pay close attention to the decision tree analogy and how using the numeric value of an element directly allows for a much wider 'fork' in the decision process.

This idea of using an element's value to directly determine its position, rather than just comparing it to other elements, is the key insight behind the algorithm we'll study today: Counting Sort.


Counting Sort: The Core Idea

Counting Sort is a non-comparison sorting algorithm that is efficient when the input consists of items (like integers) within a known, relatively small range.

Imagine you need to sort an array of integers where you know all values are between 0 and 9. Instead of comparing the numbers, you could simply:

  1. Create 10 "bins," one for each possible value (0 through 9).
  2. Go through the input array and place each number in its corresponding bin.
  3. Concatenate the contents of the bins in order (all the 0s, then all the 1s, etc.) to get the sorted array.

This is the essence of Counting Sort. It avoids comparisons by using the element values themselves as addresses into an auxiliary data structure.

Counting Sort: The Linear Time Sorting Algorithm Every Developer Should Know

The article 'Counting Sort: The Linear Time Sorting Algorithm Every Developer Should Know' provides a great high-level overview of this concept.

Read the 'Introduction' and 'Understanding Counting Sort' sections. This will solidify the core concept and the four key steps of the algorithm before we dive into the implementation details.


The Mechanism of Counting Sort

Now let's walk through the full, stable version of the algorithm. A stable sort preserves the original relative order of equal-valued elements. This is a desirable property, and as you'll see, it requires a bit more sophistication than the simple "bin" idea.

The algorithm assumes we are sorting integers in the range [0, k]. It involves three main passes. Let's use a detailed video walkthrough to understand each step.

7.12 Counting Sort (Analysis and Code) | Easiest Explanation | Data Structure Tutorials

The video from Jenny's Lectures CS IT provides a fantastic, detailed walkthrough of Counting Sort. We will watch it in segments corresponding to the main steps of the algorithm.

First, watch from 01:09 to 03:51 to understand the preconditions for using Counting Sort, such as having non-negative integers in a range 0 to k. Then, watch from 03:51 to 10:32 to see the first major step: creating a count array and populating it with the frequency of each element from the input array.

Step 1: Count Frequencies

As shown in the video, the first step is to create a count array of size . We then iterate through the input array (let's call it A) of size , and for each element A[j], we increment count[A[j]]. After this pass, count[i] holds the number of elements in A equal to i.

Step 2: Calculate Cumulative Positions

This is the key step that helps place elements correctly and ensures stability. We transform the count array so that count[i] stores the number of elements less than or equal to i.

7.12 Counting Sort (Analysis and Code) | Easiest Explanation | Data Structure Tutorials

Let's continue with the same video to see how the frequency counts are transformed into positions.

Watch the segment from 10:32 to 15:17. Focus on understanding how modifying the count array to store cumulative sums tells us the final position (or rather, one-past-the-end position) for each group of elements in the sorted output.

After this step, count[i] now tells us the index where the last element with value i should be placed in the sorted output array (assuming 1-based indexing, or the index after the last element in 0-based indexing).

Step 3: Build the Output Array

Finally, we build the sorted output array (let's call it B). To ensure stability, we iterate through the input array A backwards, from the last element to the first.

For each element A[j]:

  1. Find its target position using the cumulative count array: pos = count[A[j]].
  2. Place A[j] into the output array B at B[pos - 1] (for 0-based indexing).
  3. Decrement count[A[j]] to ensure the next identical element (which we encounter because we're iterating backwards) is placed in the preceding slot.

7.12 Counting Sort (Analysis and Code) | Easiest Explanation | Data Structure Tutorials

This final step puts everything together. The video explains why iterating backwards is crucial for stability.

Watch from 15:17 to 25:15. This is the most complex part of the algorithm. Pay close attention to how the count array is used and decremented as each element from the original array is placed into the new sorted array. The explanation of stability around the 17:30 mark is particularly important.


Analysis: Achieving Linear Time

Now we can clearly see how Counting Sort achieves its impressive performance.

  • Step 1 (Count Frequencies): We initialize a count array of size k+1 ( time) and iterate through the input array of size n once ( time). Total: .
  • Step 2 (Calculate Cumulative Positions): We iterate through the count array once ( time).
  • Step 3 (Build Output Array): We iterate through the input array of size n once ( time).

The overall time complexity is the sum of these steps: .

This is a linear time complexity! If the range k is proportional to the number of elements n (i.e., ), the complexity becomes .

Space Complexity and Limitations

The efficiency of Counting Sort comes at a cost: space. The algorithm requires an auxiliary count array of size and an output array of size . Therefore, the space complexity is .

This leads us to the primary limitation of Counting Sort:

  • It is only efficient when the range of input values, k, is not significantly larger than the number of elements, n. If you have n=100 elements but their values can go up to k=1,000,000, you would need an enormous count array, making the algorithm highly impractical in both time and space.

Other limitations include:

  • It is not a general-purpose sorting algorithm, as it's designed for integers or data with integer keys.
  • It doesn't directly handle negative numbers or floating-point numbers without modifications (like shifting all values to be non-negative).

[PDF] Lower bound on runtime of comparison sorts

This PDF from a Stanford CS course provides a concise summary of the runtime and the key warning about using Counting Sort.

First, read the 'Counting sort' section (page 2) to see the formal statement of its runtime and the crucial warning about the dependency on k. Then, read the 'Runtime' analysis for the full version (page 6) which confirms the Θ(n+k) complexity.


Conclusion

We have successfully shown how a non-comparison sort can achieve linear time complexity. By sidestepping comparisons and using element values directly as array indices, Counting Sort provides an extremely fast way to sort data under the right conditions.

Key Takeaways:

  • Non-comparison sorts can beat the lower bound of comparison sorts by using information about the values of the elements, not just their relative order.
  • Counting Sort works by counting the occurrences of each element in a given range [0, k] and using those counts to calculate the final positions of the elements.
  • Its time complexity is , which is linear () if the range k is on the order of n.
  • Its main drawback is the space complexity of , which makes it impractical for data with a very large range of values.
  • The full version of Counting Sort is stable, a property that is valuable in its own right and makes it a suitable subroutine for other algorithms like Radix Sort.

Preview of the Next Lesson:

We started today by briefly touching upon the lower bound for comparison-based sorting. Now that we've seen a practical way to circumvent it, our next lesson will circle back and take a more formal, rigorous look at that barrier. We will mathematically prove why algorithms like Merge Sort and Quicksort cannot perform better than in the general case.