Counting Sort em Zig — Implementação e Explicação
O Counting Sort (ordenação por contagem) é um algoritmo de ordenação não-comparativo que funciona contando o número de ocorrências de cada valor distinto. É extremamente eficiente quando o intervalo de valores (k) é pequeno em relação ao número de elementos (n).
Como Funciona
- Contar: conte quantas vezes cada valor aparece no array original
- Acumular: calcule as posições finais usando soma acumulada
- Posicionar: coloque cada elemento em sua posição correta no array de saída
Visualização do Processo
Array original: [4, 2, 2, 8, 3, 3, 1]
Valores de 0 a 8 (k = 9)
Passo 1 — Contagem:
Índice: 0 1 2 3 4 5 6 7 8
Contagem: [0, 1, 2, 2, 1, 0, 0, 0, 1]
↑ ↑ ↑ ↑ ↑ ↑
0× 1× 2× 2× 1× 1×
Passo 2 — Soma acumulada:
Contagem: [0, 1, 3, 5, 6, 6, 6, 6, 7]
Posição do último elemento de cada valor
Passo 3 — Posicionar (da direita para estabilidade):
Original[6] = 1 → contagem[1]=1 → saída[0] = 1, contagem[1]=0
Original[5] = 3 → contagem[3]=5 → saída[4] = 3, contagem[3]=4
Original[4] = 3 → contagem[3]=4 → saída[3] = 3, contagem[3]=3
...
Resultado: [1, 2, 2, 3, 3, 4, 8] ✓
Implementação em Zig
const std = @import("std");
const Allocator = std.mem.Allocator;
/// Counting Sort para inteiros não-negativos.
/// k é o valor máximo + 1 no array.
pub fn countingSort(
allocator: Allocator,
items: []u32,
k: usize,
) !void {
if (items.len <= 1) return;
// Array de contagem
const contagem = try allocator.alloc(usize, k);
defer allocator.free(contagem);
@memset(contagem, 0);
// Array de saída
const saida = try allocator.alloc(u32, items.len);
defer allocator.free(saida);
// Passo 1: contar ocorrências
for (items) |valor| {
contagem[valor] += 1;
}
// Passo 2: soma acumulada
var i: usize = 1;
while (i < k) : (i += 1) {
contagem[i] += contagem[i - 1];
}
// Passo 3: posicionar (percorrendo de trás para frente = estável)
var j: usize = items.len;
while (j > 0) {
j -= 1;
const valor = items[j];
contagem[valor] -= 1;
saida[contagem[valor]] = valor;
}
// Copia de volta para o array original
@memcpy(items, saida);
}
/// Counting Sort para inteiros com valores negativos.
/// Ajusta o intervalo para trabalhar com índices positivos.
pub fn countingSortComNegativos(
allocator: Allocator,
items: []i32,
) !void {
if (items.len <= 1) return;
// Encontra min e max
var min_val: i32 = items[0];
var max_val: i32 = items[0];
for (items[1..]) |v| {
if (v < min_val) min_val = v;
if (v > max_val) max_val = v;
}
const intervalo: usize = @intCast(@as(i64, max_val) - @as(i64, min_val) + 1);
const contagem = try allocator.alloc(usize, intervalo);
defer allocator.free(contagem);
@memset(contagem, 0);
const saida = try allocator.alloc(i32, items.len);
defer allocator.free(saida);
// Conta ocorrências (ajustando pelo mínimo)
for (items) |v| {
const idx: usize = @intCast(@as(i64, v) - @as(i64, min_val));
contagem[idx] += 1;
}
// Soma acumulada
var i: usize = 1;
while (i < intervalo) : (i += 1) {
contagem[i] += contagem[i - 1];
}
// Posiciona (estável)
var j: usize = items.len;
while (j > 0) {
j -= 1;
const idx: usize = @intCast(@as(i64, items[j]) - @as(i64, min_val));
contagem[idx] -= 1;
saida[contagem[idx]] = items[j];
}
@memcpy(items, saida);
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Exemplo com inteiros não-negativos
var numeros = [_]u32{ 4, 2, 2, 8, 3, 3, 1 };
try stdout.print("Original: ", .{});
for (numeros) |n| try stdout.print("{d} ", .{n});
try stdout.print("\n", .{});
try countingSort(allocator, &numeros, 9);
try stdout.print("Ordenado: ", .{});
for (numeros) |n| try stdout.print("{d} ", .{n});
try stdout.print("\n", .{});
// Exemplo com negativos
var negativos = [_]i32{ -5, 3, -1, 0, 2, -3, 1 };
try countingSortComNegativos(allocator, &negativos);
try stdout.print("Negativos: ", .{});
for (negativos) |n| try stdout.print("{d} ", .{n});
try stdout.print("\n", .{});
}
Análise de Complexidade
| Caso | Tempo | Espaço |
|---|---|---|
| Todos os casos | O(n + k) | O(n + k) |
Onde n é o número de elementos e k é o intervalo de valores.
Quando é Eficiente?
- k = O(n): complexidade total é O(n) — linear!
- k » n: o algoritmo se torna ineficiente (ex: ordenar 10 números entre 0 e 1.000.000)
Características
- Estável: Sim — preserva a ordem de elementos com mesmo valor
- In-place: Não — precisa de O(n + k) memória adicional
- Não-comparativo: não compara elementos entre si
- Limitação: funciona apenas com inteiros (ou valores mapeáveis a inteiros)
Quando Usar
- Inteiros com intervalo limitado: notas de 0 a 100, idades, caracteres ASCII
- Estabilidade necessária: como sub-rotina do Radix Sort
- Desempenho linear: quando k = O(n)
Recursos Relacionados
- Radix Sort em Zig — Usa Counting Sort como sub-rotina
- Bubble Sort em Zig — Algoritmo comparativo simples
- Quick Sort em Zig — Algoritmo comparativo eficiente
- Hash Map — Estrutura que também conta ocorrências
- Array Dinâmico — Alocação dinâmica de arrays