Analisador de Frequência em Zig — Tutorial Passo a Passo

Analisador de Frequência em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um analisador de frequência de caracteres que lê texto e produz estatísticas detalhadas sobre a distribuição de letras, dígitos e símbolos. Este projeto é usado em criptoanálise, linguística computacional e compressão de dados.

O Que Vamos Construir

Nosso analisador vai:

  • Contar a frequência de cada caractere em um texto
  • Gerar histogramas visuais no terminal
  • Calcular entropia de Shannon (medida de aleatoriedade)
  • Comparar distribuições com idiomas conhecidos
  • Identificar possíveis idiomas com base na frequência de letras
  • Processar texto de stdin ou entrada interativa

Por Que Este Projeto?

A análise de frequência é fundamental em criptografia (como vimos na Cifra de César), compressão de dados e processamento de linguagem natural. Em Zig, podemos implementar isso de forma eficiente usando arrays de tamanho fixo para contagem — sem hash maps ou alocação dinâmica.

Pré-requisitos

Passo 1: Estrutura do Projeto

mkdir analisador-frequencia
cd analisador-frequencia
zig init

Passo 2: Estrutura de Dados para Frequência

const std = @import("std");
const io = std.io;
const fmt = std.fmt;
const mem = std.mem;
const math = std.math;

/// Tabela de frequência para todos os 256 valores de byte possíveis.
/// Usamos um array de 256 posições indexado pelo valor do byte.
/// Isso é mais eficiente que um HashMap para este caso —
/// O(1) para inserção e consulta, sem colisões, sem alocação.
const TabelaFrequencia = struct {
    contagem: [256]u64,
    total: u64,

    // Contadores por categoria
    letras: u64,
    digitos: u64,
    espacos: u64,
    pontuacao: u64,
    controle: u64,
    outros: u64,

    const Self = @This();

    pub fn init() Self {
        return .{
            .contagem = [_]u64{0} ** 256,
            .total = 0,
            .letras = 0,
            .digitos = 0,
            .espacos = 0,
            .pontuacao = 0,
            .controle = 0,
            .outros = 0,
        };
    }

    /// Processa um único byte, atualizando contadores.
    pub fn processar(self: *Self, byte: u8) void {
        self.contagem[byte] += 1;
        self.total += 1;

        // Categorizar o byte
        if ((byte >= 'a' and byte <= 'z') or (byte >= 'A' and byte <= 'Z')) {
            self.letras += 1;
        } else if (byte >= '0' and byte <= '9') {
            self.digitos += 1;
        } else if (byte == ' ' or byte == '\t' or byte == '\n' or byte == '\r') {
            self.espacos += 1;
        } else if (byte >= 33 and byte <= 47 or byte >= 58 and byte <= 64 or
            byte >= 91 and byte <= 96 or byte >= 123 and byte <= 126)
        {
            self.pontuacao += 1;
        } else if (byte < 32 or byte == 127) {
            self.controle += 1;
        } else {
            self.outros += 1;
        }
    }

    /// Processa um slice de bytes.
    pub fn processarTexto(self: *Self, texto: []const u8) void {
        for (texto) |byte| {
            self.processar(byte);
        }
    }

    /// Retorna a frequência relativa (0.0-1.0) de um byte.
    pub fn frequenciaRelativa(self: *const Self, byte: u8) f64 {
        if (self.total == 0) return 0.0;
        return @as(f64, @floatFromInt(self.contagem[byte])) / @as(f64, @floatFromInt(self.total));
    }

    /// Retorna a frequência percentual de um byte.
    pub fn frequenciaPercentual(self: *const Self, byte: u8) f64 {
        return self.frequenciaRelativa(byte) * 100.0;
    }

    /// Retorna a frequência percentual considerando apenas letras.
    pub fn frequenciaLetra(self: *const Self, byte: u8) f64 {
        if (self.letras == 0) return 0.0;
        // Agrupa maiúsculas e minúsculas
        var total_letra: u64 = self.contagem[byte];
        if (byte >= 'a' and byte <= 'z') {
            total_letra += self.contagem[byte - 32]; // maiúscula
        } else if (byte >= 'A' and byte <= 'Z') {
            total_letra += self.contagem[byte + 32]; // minúscula
        }
        return @as(f64, @floatFromInt(total_letra)) / @as(f64, @floatFromInt(self.letras)) * 100.0;
    }
};

Passo 3: Cálculo de Entropia

/// Calcula a entropia de Shannon do texto.
/// A entropia mede a "imprevisibilidade" do texto.
/// - Texto com todos os caracteres iguais: entropia = 0 (totalmente previsível)
/// - Texto com distribuição uniforme: entropia máxima
/// - Texto em linguagem natural: entropia intermediária (~4 bits para português)
///
/// Fórmula: H = -sum(p(x) * log2(p(x))) para cada símbolo x
fn calcularEntropia(tabela: *const TabelaFrequencia) f64 {
    if (tabela.total == 0) return 0.0;

    var entropia: f64 = 0.0;
    const total_f: f64 = @floatFromInt(tabela.total);

    for (tabela.contagem) |cnt| {
        if (cnt > 0) {
            const p: f64 = @as(f64, @floatFromInt(cnt)) / total_f;
            entropia -= p * @log2(p);
        }
    }

    return entropia;
}

/// Calcula o Índice de Coincidência (IC).
/// O IC é usado em criptoanálise para determinar se um texto
/// é cifrado por monoalfabético ou polialfabético.
/// Português: IC ~ 0.072-0.074
/// Texto aleatório: IC ~ 0.038
fn calcularIndiceCoincidencia(tabela: *const TabelaFrequencia) f64 {
    if (tabela.letras <= 1) return 0.0;

    var soma: f64 = 0.0;
    const n_f: f64 = @floatFromInt(tabela.letras);

    // Considera apenas letras a-z (agrupando maiúsculas/minúsculas)
    var i: usize = 0;
    while (i < 26) : (i += 1) {
        const lower: u64 = tabela.contagem['a' + i];
        const upper: u64 = tabela.contagem['A' + i];
        const total = lower + upper;
        const fi: f64 = @floatFromInt(total);
        soma += fi * (fi - 1.0);
    }

    return soma / (n_f * (n_f - 1.0));
}

Passo 4: Referências de Idiomas

/// Frequência de letras em diferentes idiomas (percentual).
const FrequenciaIdioma = struct {
    nome: []const u8,
    frequencias: [26]f64,
    ic_esperado: f64, // Índice de coincidência esperado
};

const idiomas = [_]FrequenciaIdioma{
    .{
        .nome = "Portugues",
        .frequencias = .{
            14.63, 1.04, 3.88, 4.99, 12.57, 1.02, 1.30, 1.28, 6.18, 0.40,
            0.02, 2.78, 4.74, 5.05, 10.73, 2.52, 1.20, 6.53, 7.81, 4.34,
            4.63, 1.67, 0.01, 0.21, 0.01, 0.47,
        },
        .ic_esperado = 0.074,
    },
    .{
        .nome = "Ingles",
        .frequencias = .{
            8.17, 1.49, 2.78, 4.25, 12.70, 2.23, 2.02, 6.09, 6.97, 0.15,
            0.77, 4.03, 2.41, 6.75, 7.51, 1.93, 0.10, 5.99, 6.33, 9.06,
            2.76, 0.98, 2.36, 0.15, 1.97, 0.07,
        },
        .ic_esperado = 0.067,
    },
    .{
        .nome = "Espanhol",
        .frequencias = .{
            12.53, 1.42, 4.68, 5.86, 13.68, 0.69, 1.01, 0.70, 6.25, 0.44,
            0.01, 4.97, 3.15, 6.71, 8.68, 2.51, 0.88, 6.87, 7.98, 4.63,
            3.93, 0.90, 0.02, 0.22, 0.90, 0.52,
        },
        .ic_esperado = 0.077,
    },
};

/// Identifica o idioma mais provável com base na distribuição de frequências.
fn identificarIdioma(tabela: *const TabelaFrequencia) struct { nome: []const u8, score: f64 } {
    var melhor_nome: []const u8 = "Desconhecido";
    var melhor_score: f64 = math.floatMax(f64);

    for (idiomas) |idioma| {
        var chi2: f64 = 0.0;
        var i: usize = 0;
        while (i < 26) : (i += 1) {
            const obs = tabela.frequenciaLetra('a' + @as(u8, @intCast(i)));
            const esp = idioma.frequencias[i];
            if (esp > 0.0) {
                const diff = obs - esp;
                chi2 += (diff * diff) / esp;
            }
        }
        if (chi2 < melhor_score) {
            melhor_score = chi2;
            melhor_nome = idioma.nome;
        }
    }

    return .{ .nome = melhor_nome, .score = melhor_score };
}

Passo 5: Visualização

/// Exibe um histograma horizontal das letras mais frequentes.
fn exibirHistograma(tabela: *const TabelaFrequencia, writer: anytype) !void {
    try writer.print("\n  --- Histograma de Letras ---\n\n", .{});

    // Encontra a frequência máxima para escalar as barras
    var max_freq: f64 = 0.0;
    var i: usize = 0;
    while (i < 26) : (i += 1) {
        const freq = tabela.frequenciaLetra('a' + @as(u8, @intCast(i)));
        if (freq > max_freq) max_freq = freq;
    }

    if (max_freq == 0.0) {
        try writer.print("  (nenhuma letra encontrada)\n", .{});
        return;
    }

    const largura_max: f64 = 40.0;

    i = 0;
    while (i < 26) : (i += 1) {
        const letra: u8 = 'a' + @as(u8, @intCast(i));
        const freq = tabela.frequenciaLetra(letra);

        try writer.print("  {c} {d:>5.1}% ", .{ letra, freq });

        const barras: usize = @intFromFloat(freq / max_freq * largura_max);
        var b: usize = 0;
        while (b < barras) : (b += 1) {
            try writer.print("#", .{});
        }
        try writer.print("\n", .{});
    }
}

/// Exibe o resumo estatístico completo.
fn exibirResumo(tabela: *const TabelaFrequencia, writer: anytype) !void {
    const entropia = calcularEntropia(tabela);
    const ic = calcularIndiceCoincidencia(tabela);
    const idioma = identificarIdioma(tabela);

    try writer.print(
        \\
        \\  === Resumo Estatistico ===
        \\
        \\  Total de bytes:    {d}
        \\  Letras:            {d} ({d:.1}%)
        \\  Digitos:           {d} ({d:.1}%)
        \\  Espacos:           {d} ({d:.1}%)
        \\  Pontuacao:         {d} ({d:.1}%)
        \\  Caracteres ctrl:   {d}
        \\  Outros:            {d}
        \\
        \\  Entropia de Shannon: {d:.3} bits/byte
        \\  Indice de Coincidencia: {d:.4}
        \\  Idioma provavel: {s} (score: {d:.2})
        \\
    , .{
        tabela.total,
        tabela.letras,    if (tabela.total > 0) @as(f64, @floatFromInt(tabela.letras)) / @as(f64, @floatFromInt(tabela.total)) * 100.0 else 0.0,
        tabela.digitos,   if (tabela.total > 0) @as(f64, @floatFromInt(tabela.digitos)) / @as(f64, @floatFromInt(tabela.total)) * 100.0 else 0.0,
        tabela.espacos,   if (tabela.total > 0) @as(f64, @floatFromInt(tabela.espacos)) / @as(f64, @floatFromInt(tabela.total)) * 100.0 else 0.0,
        tabela.pontuacao, if (tabela.total > 0) @as(f64, @floatFromInt(tabela.pontuacao)) / @as(f64, @floatFromInt(tabela.total)) * 100.0 else 0.0,
        tabela.controle,
        tabela.outros,
        entropia,
        ic,
        idioma.nome, idioma.score,
    });
}

/// Exibe os N caracteres mais frequentes.
fn exibirTopCaracteres(tabela: *const TabelaFrequencia, n: usize, writer: anytype) !void {
    // Ordenar índices por frequência (selection sort simples)
    var indices: [256]u8 = undefined;
    for (&indices, 0..) |*idx, i| {
        idx.* = @intCast(i);
    }

    // Selection sort descendente por contagem
    var i: usize = 0;
    while (i < @min(n, 256)) : (i += 1) {
        var max_idx = i;
        var j = i + 1;
        while (j < 256) : (j += 1) {
            if (tabela.contagem[indices[j]] > tabela.contagem[indices[max_idx]]) {
                max_idx = j;
            }
        }
        const tmp = indices[i];
        indices[i] = indices[max_idx];
        indices[max_idx] = tmp;
    }

    try writer.print("\n  --- Top {d} Caracteres ---\n\n", .{n});
    i = 0;
    while (i < n and i < 256) : (i += 1) {
        const byte = indices[i];
        const cnt = tabela.contagem[byte];
        if (cnt == 0) break;

        // Representação legível do caractere
        const repr = if (byte >= 33 and byte <= 126) blk: {
            var buf: [1]u8 = .{byte};
            break :blk buf;
        } else blk: {
            break :blk [1]u8{'.'};
        };

        const nome: []const u8 = switch (byte) {
            ' ' => "ESPACO",
            '\n' => "NEWLINE",
            '\t' => "TAB",
            '\r' => "CR",
            else => &[_]u8{repr[0]},
        };

        try writer.print("  {d:>3} (0x{X:0>2}) {s:<8} {d:>8} ({d:.2}%)\n", .{
            byte, byte, nome, cnt, tabela.frequenciaPercentual(byte),
        });
    }
}

Passo 6: Loop Principal

pub fn main() !void {
    const stdout = io.getStdOut().writer();
    const stdin = io.getStdIn().reader();

    try stdout.print(
        \\
        \\  ==========================================
        \\    ANALISADOR DE FREQUENCIA - Zig
        \\  ==========================================
        \\
    , .{});

    var buf: [4096]u8 = undefined;

    while (true) {
        try stdout.print(
            \\
            \\  [1] Analisar texto digitado
            \\  [2] Comparar com idiomas
            \\  [3] Sair
            \\
            \\  Opcao:
        , .{});

        const opcao_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse break;
        const opcao = mem.trim(u8, opcao_raw, " \t\r\n");

        if (mem.eql(u8, opcao, "3")) break;

        if (mem.eql(u8, opcao, "1") or mem.eql(u8, opcao, "2")) {
            try stdout.print("\n  Digite o texto (ENTER para finalizar):\n  ", .{});
            const texto_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            const texto = mem.trim(u8, texto_raw, "\r\n");

            if (texto.len == 0) {
                try stdout.print("  Texto vazio.\n", .{});
                continue;
            }

            var tabela = TabelaFrequencia.init();
            tabela.processarTexto(texto);

            try exibirResumo(&tabela, stdout);
            try exibirHistograma(&tabela, stdout);
            try exibirTopCaracteres(&tabela, 15, stdout);

            if (mem.eql(u8, opcao, "2")) {
                try stdout.print("\n  --- Comparacao com Idiomas ---\n", .{});
                for (idiomas) |idioma| {
                    var chi2: f64 = 0.0;
                    var i: usize = 0;
                    while (i < 26) : (i += 1) {
                        const obs = tabela.frequenciaLetra('a' + @as(u8, @intCast(i)));
                        const esp = idioma.frequencias[i];
                        if (esp > 0.0) {
                            const diff = obs - esp;
                            chi2 += (diff * diff) / esp;
                        }
                    }
                    try stdout.print("  {s:<12} chi2 = {d:.2}\n", .{ idioma.nome, chi2 });
                }
                try stdout.print("  (menor chi2 = melhor correspondencia)\n", .{});
            }
        } else {
            try stdout.print("  Opcao invalida.\n", .{});
        }
    }

    try stdout.print("\n  Ate logo!\n", .{});
}

Passo 7: Testes

test "tabela frequencia - contagem basica" {
    var tabela = TabelaFrequencia.init();
    tabela.processarTexto("aab");

    try std.testing.expectEqual(@as(u64, 2), tabela.contagem['a']);
    try std.testing.expectEqual(@as(u64, 1), tabela.contagem['b']);
    try std.testing.expectEqual(@as(u64, 3), tabela.total);
    try std.testing.expectEqual(@as(u64, 3), tabela.letras);
}

test "tabela frequencia - categorias" {
    var tabela = TabelaFrequencia.init();
    tabela.processarTexto("abc 123 !?");

    try std.testing.expectEqual(@as(u64, 3), tabela.letras);
    try std.testing.expectEqual(@as(u64, 3), tabela.digitos);
    try std.testing.expectEqual(@as(u64, 2), tabela.espacos);
    try std.testing.expectEqual(@as(u64, 2), tabela.pontuacao);
}

test "entropia - texto uniforme" {
    var tabela = TabelaFrequencia.init();
    tabela.processarTexto("aaaa");
    const entropia = calcularEntropia(&tabela);
    try std.testing.expectApproxEqAbs(@as(f64, 0.0), entropia, 0.001);
}

test "entropia - dois simbolos iguais" {
    var tabela = TabelaFrequencia.init();
    tabela.processarTexto("ab");
    const entropia = calcularEntropia(&tabela);
    try std.testing.expectApproxEqAbs(@as(f64, 1.0), entropia, 0.001);
}

test "frequencia relativa" {
    var tabela = TabelaFrequencia.init();
    tabela.processarTexto("aaab");
    try std.testing.expectApproxEqAbs(@as(f64, 0.75), tabela.frequenciaRelativa('a'), 0.001);
    try std.testing.expectApproxEqAbs(@as(f64, 0.25), tabela.frequenciaRelativa('b'), 0.001);
}

test "indice coincidencia - texto repetido" {
    var tabela = TabelaFrequencia.init();
    tabela.processarTexto("aaaa");
    const ic = calcularIndiceCoincidencia(&tabela);
    try std.testing.expectApproxEqAbs(@as(f64, 1.0), ic, 0.001);
}

Compilando e Executando

zig build test
zig build run

Conceitos Aprendidos

  • Arrays de tamanho fixo como tabelas de lookup O(1)
  • Cálculo de entropia de Shannon
  • Índice de coincidência para criptoanálise
  • Categorização de caracteres ASCII
  • Histogramas visuais no terminal
  • Comparação estatística de distribuições (qui-quadrado)

Próximos Passos

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.