Counting Sort in Python: Linear-Time Sorting with Range Checks

Quick answer: Counting sort counts occurrences of bounded integer keys and reconstructs sorted output. It can run in O(n + k) time with O(k) auxiliary space, where k is the value range, so range size and memory limits determine whether it beats comparison sorting.

Python Pool infographic showing counting sort mapping bounded integer values into a frequency array before rebuilding sorted output
Counting sort counts occurrences in a bounded integer range, then reconstructs the ordered result; its memory cost depends on that range, not only input length.

Counting sort is a non-comparison sorting algorithm for integers in a known, limited range. It counts how many times each value appears, then rebuilds the sorted output from those counts.

The main references are the collections.Counter documentation, Python’s sorting HOWTO, and the array module documentation.

Counting sort can be very fast when the number of input items is large and the value range is small. It is a poor fit when values can be huge, sparse, or non-integer.

The time complexity is O(n + k), where n is the number of items and k is the size of the value range.

The memory cost also depends on k. That is the tradeoff: counting sort avoids comparisons, but it pays for a count table that covers the full range.

Use it when values are integers such as grades, small ids, byte values, categories encoded as numbers, or bounded scores.

Before choosing it, estimate the smallest and largest input values. A list of one million numbers between 0 and 99 is a strong match. A list of ten numbers spread between 0 and 10_000_000 is not.

That range check is part of the algorithm design, not an optional optimization. Without it, counting sort can turn a small input into a large memory allocation.

Simple Counting Sort

This version works for non-negative integers.

def counting_sort(values):
    if not values:
        return []

    largest = max(values)
    counts = [0] * (largest + 1)

    for item in values:
        counts[item] += 1

    output = []
    for item, count in enumerate(counts):
        output.extend([item] * count)
    return output

print(counting_sort([4, 2, 2, 8, 3, 3, 1]))

The list index represents the integer value, and the stored count records how often that value appears.

This is the easiest version to understand, but it assumes every input value is zero or greater.

If a negative number appears in this simple version, Python would use a negative list index and corrupt the count logic. Use the offset version for signed integers.

Handle Negative Numbers

Use an offset when the input can contain negative integers.

def counting_sort_with_negatives(values):
    if not values:
        return []

    smallest = min(values)
    largest = max(values)
    offset = -smallest
    counts = [0] * (largest - smallest + 1)

    for item in values:
        counts[item + offset] += 1

    output = []
    for index, count in enumerate(counts):
        output.extend([index - offset] * count)
    return output

print(counting_sort_with_negatives([3, -1, 2, -1, 0]))

The offset shifts the smallest value to index 0.

This keeps the count list compact when the range is still reasonably small.

The offset does not change the sorted values. It only changes where those values are stored in the count table.

Python Pool infographic showing counting sort minimum, maximum, counts, and memory
Value range: Counting sort minimum, maximum, counts, and memory.

Build Cumulative Counts

A stable counting sort uses cumulative counts to place items in their final positions.

def cumulative_counts(values):
    counts = [0] * (max(values) + 1)
    for item in values:
        counts[item] += 1

    total = 0
    for index, count in enumerate(counts):
        total += count
        counts[index] = total
    return counts

print(cumulative_counts([2, 0, 2, 1]))

After this step, each count says how many values are less than or equal to that index.

Cumulative counts are the key to stable placement.

Stability is not important when sorting plain integers, but it becomes important when each integer is attached to a larger record.

Stable Counting Sort

Stable sorting keeps equal items in their original relative order. This matters when sorting records by one integer key.

def stable_counting_sort(records, key):
    if not records:
        return []

    largest = max(key(record) for record in records)
    counts = [0] * (largest + 1)

    for record in records:
        counts[key(record)] += 1

    for index in range(1, len(counts)):
        counts[index] += counts[index - 1]

    output = [None] * len(records)
    for record in reversed(records):
        item_key = key(record)
        counts[item_key] -= 1
        output[counts[item_key]] = record
    return output

students = [("Ada", 2), ("Grace", 1), ("Linus", 2)]
print(stable_counting_sort(students, key=lambda row: row[1]))

The reverse loop places later equal items later, preserving the original order among equal keys.

Use this form when sorting records rather than plain integers.

The algorithm walks the records backward so equal keys keep their original order in the final output.

Use Counter For Counting

collections.Counter is convenient when you only need counts, not a full counting sort implementation.

from collections import Counter

values = [4, 2, 2, 8, 3, 3, 1]
counts = Counter(values)

output = []
for item in range(min(values), max(values) + 1):
    output.extend([item] * counts[item])

print(output)

This is readable and handles missing values naturally.

For teaching the algorithm, the list-based count table is more explicit. For small scripts, Counter may be clearer.

Counter also avoids allocating empty buckets for values that do not appear, but rebuilding sorted order still requires deciding which range to iterate.

Python Pool infographic showing frequency counts, prefix sums, offsets, and placement
Count array: Frequency counts, prefix sums, offsets, and placement.

Know When Not To Use It

Counting sort is not a general replacement for Python’s built-in sorted().

values = [42, 10_000_000, 7]
range_size = max(values) - min(values) + 1

if range_size > len(values) * 10:
    result = sorted(values)
else:
    result = values

print(result)

If the range is much larger than the input size, a count array wastes memory.

The practical rule is to use counting sort for integers with a compact known range. Use sorted() for general Python objects, strings, floats, and sparse integer ranges.

Python’s built-in sorter is stable, highly optimized, and easier to read for normal application code. Counting sort should earn its place by matching the data shape: integer keys, tight bounds, and enough repeated values to make the count table worthwhile.

Always check the input range before allocating the count table. That one check prevents surprising memory use on unexpected data.

For interviews and algorithm practice, explain both the speed advantage and the range limitation. Counting sort is powerful because it uses information about the input domain.

For everyday Python code, start with sorted(). Reach for counting sort only when profiling or problem constraints show that the integer range is compact enough to justify it.

Build The Frequency Range

Find the minimum and maximum key, map each value to value – minimum, and allocate counts for the range. Check the span before allocation so a few distant values cannot create an unsafe array.

Python Pool infographic showing input order, duplicates, keys, and stable output
Stable output: Input order, duplicates, keys, and stable output.

Reconstruct The Values

For each frequency, emit the corresponding value the recorded number of times. This simple form is clear and efficient for integers when stability of associated records is not required.

Support Negative Keys

An offset lets negative values use non-negative indices. Keep the offset with the operation and test the minimum, maximum, and mixed-sign cases explicitly.

Choose Stable Sorting When Needed

To sort records by an integer key while preserving equal-key order, compute cumulative counts and place items into an output array from the correct direction. Stability costs extra space and bookkeeping.

Python Pool infographic checking negative values, sparse ranges, complexity, and tests
Sort checks: Python Pool infographic checking negative values, sparse ranges, complexity, and tests.

Compare The Range To Input Size

Counting sort is attractive when k is small relative to n. If the range is huge, Python’s built-in Timsort or another bounded representation may be faster and much more memory efficient.

Test The Contract

Test empty input, one value, duplicates, negative keys, wide ranges, already sorted and reverse-sorted data, stable records, invalid non-integers, and the memory guard. Compare against sorted for trusted reference cases.

Use the official Python sorting HOWTO when comparing a counting implementation with built-in sorting. Related Python Pool references include Python lists and tests.

For related sorting work, compare Python list operations, range and stability tests, and frequency mappings before choosing counting sort.

Frequently Asked Questions

What is counting sort?

Counting sort counts how often each bounded integer occurs and uses those frequencies to emit values in sorted order without comparing pairs.

When is counting sort efficient?

It is useful when the value range k is reasonably small compared with the number of items n; typical time is O(n + k) with O(k) auxiliary space.

Can counting sort handle negative numbers?

Yes. Use the minimum value as an offset so each value maps to a non-negative frequency index, while checking the range before allocation.

Is counting sort stable by default?

A simple frequency reconstruction is not a stable record sort. A stable version uses cumulative counts and places records in input order, which requires additional bookkeeping.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted