Quando il compito di selezione comporta grandi array di piccoli interi, come gradi, età o codici categoriali, gli algoritmi classici basati su confronti come QuickSort o MergeSort possono sentirsi come overkill. Questi algoritmi funzionano nel tempo O(n log n), ma se la gamma di possibili valori è limitata, è possibile ordinare in lineare O(n + k) con

Come Contare le Opere Ordinate

Counting Sort sfrutta la consapevolezza che i valori di input sono interi tratti da una piccola gamma []. Invece di comparazioni bidimensionali, costruisce un istogramma di frequenza dei valori e quindi utilizza che l'istogramma per posizionare ogni elemento nella sua corretta posizione ordinata.

L'approccio di base: Ricostruzione diretta

La versione più semplice di Counting Sort funziona in due passaggi:

  1. Conta frequenze[[]] – Iterate attraverso l'array di input e incrementate un contatore per ogni valore che vedete.
  2. Overwrite l'ingresso[[] – Passare attraverso il contatore dal più piccolo al più grande e, per ogni valore, riscriverlo nella matrice di input quante volte il suo conteggio.

Questo rende un output ordinato ma non ]non] preservare l'ordine relativo dei duplicati (non è stabile). La stabilità conta quando si seleziona una chiave mantenendo l'ordine originale dei record con chiavi uguali. La variante stabile, descritta in seguito, è quella più comunemente utilizzata nella pratica.

La variabile stabile: conteggi cumulativi

Per rendere il Counting Sort stabile, aggiungiamo un terzo passaggio:

  1. Conta le frequenze come prima.
  2. Trasformare l'array di frequenza in un array di conteggio cumulativo. Dopo questo passaggio, ] contiene il numero di elementi ≤ i[].
  3. Iterare l'array di input inverso (da ultimo elemento a primo). Per ogni elemento, utilizzare il suo conteggio cumulativo per trovare la sua posizione nell'array di uscita, posizionarlo e decrementare il conteggio.

Poiché si attraversa inverso, viene conservato l'ordine relativo di elementi uguali. L'array di uscita è separato dall'ingresso, quindi questa versione utilizza lo spazio aggiuntivo O(n) per l'output, mentre la versione di base può ordinare in-place sovrascrivendo l'ingresso.

Attuazione di conteggio Ordina in C#

Di seguito sono riportate due implementazioni C#: la versione base in-place (per scenari in cui la stabilità è inutile) e la versione stabile che utilizza un array ausiliario. Entrambi richiedono di conoscere il valore massimo in anticipo.

Basic (Non-Stable) Contare

Questa variante ordina direttamente l'array di input senza un buffer di uscita aggiuntivo, ma non è stabile.

public static void CountingSortBasic(int[] array, int maxValue)
{
 int[] counts = new int[maxValue + 1];

 // Count each element's frequency
 for (int i = 0; i < array.Length; i++)
 {
 counts[array[i]]++;
 }

 // Overwrite the original array in sorted order
 int index = 0;
 for (int value = 0; value <= maxValue; value++)
 {
 while (counts[value]-- > 0)
 {
 array[index++] = value;
 }
 }
}

Ordina per la contesa stabile

La versione stabile richiede un array di output della stessa dimensione dell'ingresso, e utilizza anche conteggi cumulativi per posizionare correttamente gli elementi.

public static int[] CountingSortStable(int[] array, int maxValue)
{
 int[] counts = new int[maxValue + 1];
 int[] output = new int[array.Length];

 // Step 1: Count occurrences
 foreach (int num in array)
 {
 counts[num]++;
 }

 // Step 2: Transform counts to cumulative counts
 for (int i = 1; i <= maxValue; i++)
 {
 counts[i] += counts[i - 1];
 }

 // Step 3: Build the output array (iterate input in reverse for stability)
 for (int i = array.Length - 1; i >= 0; i--)
 {
 int value = array[i];
 output[counts[value] - 1] = value;
 counts[value]--;
 }

 return output;
}

In entrambe le implementazioni, è il più grande interi che appare nell'array. Se il vero massimo è sconosciuto, è possibile calcolare con una scansione preparatoria (O(n)). La versione stabile restituisce una nuova matrice ordinata, lasciando l'originale invariato.

Analisi della complessità

n]] sia il numero di elementi e k[] = max – min + 1 (la gamma di valori possibili).

  • Tempo:[]] La fase di conteggio è O(n)], il prefisso cumulativo è O(k), e la ricostruzione è O(n). Quando k è O(n), l'algoritmo è lineare.
  • Spazio:[[] La versione base utilizza lo spazio extra O(k) per l'array di conteggio. La versione stabile utilizza O(n + k) perché alloca anche l'array di uscita. Ciò rende Counting Sort inadattabile quando la gamma è grande rispetto al numero di elementi.
  • Comparison con altri tipi:[] I tipi basati sul confronto come QuickSort e MergeSort richiedono almeno confronti O(n log n).

Variazioni e estensioni

Gestione di Integer negativi

Per gestire i valori negativi, spostare l'intera gamma in modo che il minimo diventi zero. Ad esempio, se i numeri variano da -1000 a 1000, compensano ogni elemento di +1000. L'array di conteggio ha poi dimensioni .

public static int[] CountingSortWithNegative(int[] array)
{
 if (array.Length == 0) return array;

 int min = array.Min();
 int max = array.Max();
 int range = max - min + 1;

 int[] counts = new int[range];
 int[] output = new int[array.Length];

 foreach (int num in array)
 counts[num - min]++;

 for (int i = 1; i < range; i++)
 counts[i] += counts[i - 1];

 for (int i = array.Length - 1; i >= 0; i--)
 {
 int value = array[i];
 output[counts[value - min] - 1] = value;
 counts[value - min]--;
 }

 return output;
}

Mapping chiavi non-Integer

Se i dati sono costituiti da caratteri (byte), o enumerazioni che possono essere gettati in interi, puoi applicarlo. Per gli oggetti più grandi, puoi estrarre una chiave integer e ordinare gli oggetti di conseguenza—questo è esattamente il modo in cui Radix Sort utilizza spesso Counting Sort come subroutine interna.

Radix Ordina Combo

Radix Sort elabora le cifre (o bit) singolarmente, e Counting Sort è la scelta naturale per ogni passaggio quando la base (ad esempio, 10 o 256) è piccola, permettendo una selezione lineare-time di interi arbitrari, non solo piccoli.

Considerazioni pratiche in C#

Memoria Footprint e Large k

Per esempio, ordinare 1.000 elementi con una gamma di 1.000.000 di rifiuti di spazio. Verificare sempre che ]k] non sia ordini di grandezza più grande di n[]]]]]—altrimenti utilizzare una sorta di confronto o un approccio ibrido.

Parallelismo e Span< T>

Per array estremamente grandi, è possibile parallelizzare la fase di conteggio dividendo l'ingresso attraverso i fili. Ogni thread conta il suo segmento in una matrice privata, e poi i risultati parziali sono aggregati.

Bordo caso

  • Ottura vuota[] – ritorna immediatamente.
  • L'elemento singolo[] – la selezione è banale.
  • Tutti i valori identici[ – l'array di conteggio ha una voce non zero; la ricostruzione funziona in O(n).
  • Grande gamma ma dati radi[[[] – Counting Sort diventa inefficiente perché la maggior parte delle voci di conteggio sono zero.

Raccomandazioni sulle prestazioni

Usare Counting Sort quando si sa che gli interi di input cadono in una piccola gamma (ad esempio, voti 0–100, età 0–120, o codici di errore 0–255). Per i range più grandi, considerare Radix Sort o un ibrido che rientra a QuickSort per partizioni ad alta gamma.

Quando usare il conteggio di tipo ordinario (e quando non a)

SituationRecommendation
Small integer range (k ~ n)Excellent choice – linear time, simple code.
Large integer range (k >> n)Avoid – memory waste and O(k) overhead.
Need stabilityUse the stable variant (cumulative counts).
Strings or objectsConsider Radix Sort or a comparison sort.
Extremely large datasetsCounting Sort can be parallelized; but watch memory.

Benchmarking e prestazioni

In un punto di riferimento tipico con n = 1,000,000 e k = 1.000, Counting Sort completa in circa il 20-30% del tempo preso da [ (che utilizza introssort). Il divario si allarga come k diminuisce. Di seguito è un confronto approssimativo (tempi di esecuzione su una CPU moderna con .NET 8):

n = 1,000,000 | k = 1,000
Array.Sort (QuickSort variant) : 68 ms
CountingSortBasic : 12 ms
CountingSortStable : 18 ms

Quando la gamma cresce a 10.000, Counting Sort vince ancora, ma il margine si restringe. Per k = 100.000, la memoria overhead (≈ 400 KB per il conteggio array) inizia a danneggiare la cache della CPU e le prestazioni possono degradarsi.

Conclusioni

Counting Sort è un algoritmo ingannevole che offre prestazioni lineari quando i dati si adattano ai suoi vincoli. Per gli sviluppatori C# che trattano di grandi array di piccoli interi, è uno strumento prezioso che può ridurre drasticamente il tempo di selezione.

Per ulteriori informazioni, consultare l'articolo Wikipedia sulla Contazione di Sort, il ]Microsoft docs su Array.Sort, e una guida pratica da GeeksforGeeks.