Skip to main content

Welcome to a Grasp lesson

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

Lesson 18

Counting Sort for Limited-Range Integers

Recommended resources

Hello! Welcome to the next lesson in your journey to mastering Python for competitive programming.

In our last session, we focused on leveraging Python's powerful built-in sorting capabilities, using custom comparators and lambda functions. You learned that these methods are typically based on comparison sorts like Timsort, which have an excellent average-case performance of .

Today, we're going to explore a completely different approach that can, under the right circumstances, be even faster. We will be learning to apply Counting Sort for integer arrays with a limited range. This is a non-comparison-based algorithm that can achieve linear time complexity, making it a valuable and powerful tool in your competitive programming arsenal for specific types of problems.

What is Counting Sort and Why Use It?

All the sorting algorithms we've discussed so farโ€”Merge Sort, Quicksort, and Python's built-in sortโ€”are comparison-based. They work by comparing pairs of elements to decide their order. It's a proven fact in computer science that any comparison-based sorting algorithm has a lower bound of time complexity in the average and worst case.

Counting Sort breaks this barrier. It doesn't compare elements at all. Instead, it works by counting the number of occurrences of each unique element in the input array. This allows it to sort in linear time, but it comes with a major condition: it's only efficient for sorting integers that fall within a limited range.

For a great animated introduction to why Counting Sort is useful and the scenarios where it excels, watch the beginning of the following video.

Counting Sort explained with animations and example | Full implementation and code

This video from Nikhil Lohia provides an excellent visual introduction to Counting Sort, explaining its linear time complexity and the specific conditions under which it's most effective.

Watch from the beginning to 03:33. Focus on the core idea: instead of comparing elements, we use the values themselves as indices to count their frequencies.


How Counting Sort Works: A Step-by-Step Guide

The core idea is simple: if we know how many elements are less than a given element x, we can place x directly into its correct sorted position.

Let's break down the algorithm into two main versions: a simple, intuitive approach and a more robust, stable version. A stable sorting algorithm maintains the relative order of equal elements, which can be important in some problems.

Version 1: The Simple (In-place) Approach

This version is easy to understand and implement. It involves two main passes over the data.

  1. Count Frequencies: Create a count array to store the frequency of each number in the input array. To do this, you first need to find the maximum value in the input array to determine the size of the count array.
  2. Reconstruct the Array: Iterate through the count array from the smallest index to the largest. For each number i, append i to the result count[i] times. This overwrites the original array with the sorted elements.

This approach is demonstrated very clearly in the following video, which is geared towards competitive programming.

Sorting: Bubble, Insertion, Selection, Merge, Quick, Counting Sort - DSA Course in Python Lecture 10

In this video, Greg Hogg explains the mechanics of Counting Sort using a practical example and analyzes its complexity.

Watch from 00:30:32 to 00:34:14. Pay attention to how the 'counts' array is created and then used to rewrite the original array in sorted order. This demonstrates the simple, non-stable version of the algorithm.

While this method works for sorting simple integers, it is not stable. For example, if you were sorting objects based on an integer key, you wouldn't be able to preserve their original relative order if they had the same key. That's where the stable version comes in.

Version 2: The Stable Approach

This version is slightly more complex but is the standard, more powerful implementation. It ensures stability.

  1. Find the maximum value (max_val) in the input array.
  2. Create a count array of size max_val + 1 and store the frequency of each element, just like before.
  3. Calculate cumulative counts: Modify the count array so that each element at index i stores the sum of counts up to i. After this step, count[i] tells you the position where the last occurrence of element i should go in the sorted output.
  4. Build the output array: Create a new output array of the same size as the input. Iterate through the input array in reverse. For each element x, place it at output[count[x] - 1], and then decrement count[x]. Iterating in reverse is the key to maintaining stability.

The Programiz article below provides excellent diagrams illustrating this process.

Counting Sort Algorithm

This article from Programiz provides a fantastic step-by-step visual breakdown of the stable Counting Sort algorithm.

Focus on the section 'Working of Counting Sort'. Carefully examine the four diagrams that show the transition from the initial array to the count array, then the cumulative count, and finally how the output array is constructed.

This image from the article perfectly captures the final step of placing an element into the output array using the cumulative counts.

Counting Sort: Placing an Element
This image illustrates the process of placing an element from the original array into the sorted `output` array. For the element `4`, its cumulative count is `6`. It is placed at index `6-1=5`, and the count for `4` is then decremented.

Python Implementation

Now, let's translate the stable algorithm into Python code. The following implementation is clear and directly follows the steps we just outlined.

Counting Sort - CSU083 | Shoolini University

This resource from Shoolini University provides a well-commented Python implementation of the stable Counting Sort algorithm. It's broken down into steps that match our discussion.

Please read the section 'Basic Implementation in Python'. Note how each of the five steps (Find max, Create count array, Count occurrences, Transform to positions, Build output) is implemented in the code.

Here's the code from the article for your reference:

def counting_sort(arr):
    # Step 1: Find the maximum value in the array
    max_val = max(arr)

    # Step 2: Create a count array of size (max_val + 1) initialized to zero
    count = [0] * (max_val + 1)

    # Step 3: Count occurrences of each element
    for num in arr:
        count[num] += 1

    # Step 4: Transform count array to store actual positions (cumulative sum)
    for i in range(1, len(count)):
        count[i] += count[i - 1]

    # Step 5: Build the sorted output array
    output = [0] * len(arr)
    # Iterate in reverse for stability
    for num in reversed(arr):
        output[count[num] - 1] = num
        count[num] -= 1
        
    return output

# Example usage
arr = [4, 2, 2, 8, 3, 3, 1]
sorted_arr = counting_sort(arr)
print("Sorted array:", sorted_arr)
# Output: Sorted array: [1, 2, 2, 3, 3, 4, 8]
Test your understanding!

Given the input array arr = [3, 1, 3, 4, 1] and using the stable counting_sort function above:

  1. What will the count array look like after Step 3 (counting occurrences)?
  2. What will the count array look like after Step 4 (cumulative sum)?
Show answer
  1. After Step 3 (Frequencies): The max value is 4, so the count array has size 5. The frequencies are:

    • count[0] = 0
    • count[1] = 2 (two 1s)
    • count[2] = 0
    • count[3] = 2 (two 3s)
    • count[4] = 1 (one 4)
      So, count = [0, 2, 0, 2, 1]
  2. After Step 4 (Cumulative Sum):

    • count[0] remains 0.
    • count[1] becomes count[0] + count[1] = 0 + 2 = 2.
    • count[2] becomes count[1] + count[2] = 2 + 0 = 2.
    • count[3] becomes count[2] + count[3] = 2 + 2 = 4.
    • count[4] becomes count[3] + count[4] = 4 + 1 = 5.
      So, count = [0, 2, 2, 4, 5]

Complexity Analysis and Trade-offs

Understanding the performance of Counting Sort is key to knowing when to use it in a competition.

  • Time Complexity: , where is the number of elements in the input array, and is the range of the input values (specifically, max_val).
    • Finding the max:
    • Counting frequencies:
    • Calculating cumulative sums:
    • Building the output array:
    • Total: , which simplifies to .
  • Space Complexity: . We need for the count array and for the output array.

This complexity analysis reveals the algorithm's main strength and weakness. If is small or on the same order as (i.e., ), the time complexity is effectively , which is fantastically fast. However, if you have an array like [1, 10000000], then would be enormous, making the algorithm incredibly slow and memory-hungry. This makes it unsuitable for sorting data with a large range of values.

Counting Sort - CSU083 | Shoolini University

For a clear summary of these trade-offs and a helpful guide on when to choose Counting Sort, refer back to the Shoolini University resource.

Read sections 'Time & Space Complexity Analysis' and 'Trade-offs of Counting Sort'. The table in 'When to Use and When to Avoid' is particularly useful for competitive programming.


Conclusion

Great work today! You've added a non-comparison-based sorting algorithm to your toolkit. Counting Sort is a prime example of how understanding the properties of your data can lead to significant performance gains, a crucial insight for competitive programming.

Key Takeaways:

  • Counting Sort is a non-comparison sorting algorithm that achieves linear time complexity, .
  • It is only effective for sorting integers within a limited range (k).
  • It works by counting element frequencies and using those counts to determine the final sorted positions.
  • The stable version uses a cumulative count array and is generally preferred.
  • Its primary weakness is high time and space complexity when the range of numbers (k) is very large compared to the number of elements (n).

Up Next:
Now that you have a solid foundation in various sorting algorithms, we'll move on to a new class of techniques that often operate on sorted data. In our next lesson, we'll dive into the Two-Pointers and Sliding Window module, starting with how to "Apply the two-pointer technique to solve problems on sorted arrays." You'll see how a sorted array can be processed very efficiently using this clever pattern.