Aula 06 - Estruturas de Dados Homogêneas e Heterogêneas (Listas, Tuplas e Dicionários)

Navegação Rápida

Metadados da Aula

  • Área: 06 Computação e Telecomunicações
  • Carga Horária Equivalente: 4 horas-aula teóricas e práticas
  • Pré-requisitos: Aula 05 - Modularização e Funções
  • Palavras-chave: Vetores Dinâmicos, Arrays Contíguos, Tabelas Hash Compactas, SipHash, Tuplas Imutáveis, Conjuntos (Set), Complexidade Big-O, Matriz de Admitância Nodal (Ybus), NBR 5410

📌 Sumário da Aula

  1. 🧭 1. Contextualização e Objetivos de Aprendizagem
  2. 📋 2. Sumário Executivo dos Tópicos
  3. ⚡ 3. Fundamentação Teórica e Modelagem Matemática
  4. 🖼️ 4. Diagrama Técnico Integrado
  5. 💻 5. Implementação Prática em Engenharia Elétrica
  6. 📝 6. Banco de Questões de Fixação e Concursos Auditados
  7. 📚 7. Referências Bibliográficas e Normas Técnicas

🧭 1. Contextualização e Objetivos de Aprendizagem

O dimensionamento de condutores de uma planta industrial, a simulação estática de fluxo de potência em sistemas interligados com dezenas de milhares de barras ou o despacho centralizado de cargas no Operador Nacional do Sistema (ONS) dependem da organização eficiente de grandes volumes de dados heterogêneos. Selecionar a estrutura de dados inadequada para representar uma topologia de rede pode degradar a complexidade temporal de um algoritmo de para , inviabilizando a resposta de sistemas de supervisão em tempo real.

Esta aula final de Lógica de Programação estuda a arquitetura interna e a complexidade assintótica das estruturas de dados canônicas do Python 3: listas (vetores dinâmicos de referências contíguas), tuplas (registros imutáveis de alta velocidade), dicionários (tabelas de dispersão compactas com tempo médio de consulta constante) e conjuntos (álgebra booleana de pertinência). Conclui demonstrando a aplicação prática dessas estruturas na modelagem algorítmica da Matriz de Admitância Nodal () em Sistemas Elétricos de Potência.

Ao término desta aula, o engenheiro será capaz de:

  • Analisar quantitativamente o consumo de memória e a complexidade Big-O de operações de inserção, remoção, busca e fatiamento em listas e dicionários.
  • Compreender a mecânica interna de realocação geométrica com sobrealocação (over-allocation) que garante o custo amortizado para o método append().
  • Explicar o funcionamento de tabelas hash compactas (PEP 468) e a resolução de colisões por endereçamento aberto (open addressing) com dispersão criptográfica SipHash.
  • Modelar redes elétricas de potência complexas utilizando estruturas de dados esparsas em memória, mitigando em mais de o desperdício computacional em relação a matrizes densas tradicionais.

📋 2. Sumário Executivo dos Tópicos

  • Listas (list): Vetores dinâmicos de ponteiros indexados a partir de zero. Acesso posicional instantâneo , anexação ao final append() com custo amortizado e custo para inserções/deleções no início ou meio da estrutura.
  • Tuplas (tuple): Sequências contíguas imutáveis. Eliminam a sobrecarga de capacidade reservada excedente (over-allocation), consumindo menos memória que listas e atuando como chaves válidas de tabelas hash.
  • Dicionários (dict): Tabelas de dispersão compactas formadas por uma matriz esparsa de índices e uma matriz densa de entradas (hash, key, value). Busca, inserção e remoção com custo médio constante . Preservam a ordem de inserção deterministicamente desde o Python 3.7.
  • Conjuntos (set): Coleções não ordenadas de elementos únicos com busca baseada em hash. Implementam nativamente a teoria dos conjuntos: união (|), interseção (&), diferença (-) e diferença simétrica (^).
  • Matriz de Admitância Nodal (): Representação matricial . Elementos diagonais ; elementos fora da diagonal . Estruturada computacionalmente através de mapeamentos esparsos.

⚡ 3. Fundamentação Teórica e Modelagem Matemática

3.1 Vetores Dinâmicos em CPython: A Mecânica Interna das Listas

Em CPython, uma lista (list) não é implementada como uma lista encadeada (linked list) com nós dispersos ligados por ponteiros. Trata-se de um vetor dinâmico de ponteiros contíguos (dynamic array):

3.3.1 Acesso Posicional em Tempo Constante

Como o vetor ob_item é contíguo na memória primária, o endereço físico do elemento no índice é determinado instantaneamente por simples aritmética de ponteiros da CPU:

Essa operação consome exatamente uma instrução de soma e deslocamento no hardware, garantindo tempo de acesso estritamente constante .

3.3.2 Sobrealocação Geométrica e Amortização de append()

Quando uma lista atinge sua capacidade máxima (size == allocated) e o método .append() é invocado, o interpretador não aloca apenas espaço para 1 novo elemento (o que exigiria realocar e copiar todo o vetor em cada inserção, gerando complexidade quadrática para inserções). O CPython aplica a fórmula de Sobrealocação Geométrica (Over-allocation):

O fator de expansão assintótico é de aproximadamente ( de margem adicional).

Pelo Método Contábil de Amortização:

  • A imensa maioria das chamadas .append() consome apenas de escrita no índice livre subsequente e incremento de size ();
  • Raramente ocorre a realocação com cópia de memória (realloc via memmove), cujo custo é ;
  • Como o custo da realocação é diluído ao longo das inserções anteriores que não demandaram realocação, o custo médio por operação é matematicamente comprovado como Amortizado.

Em contrapartida, inserções ou remoções no início da lista (lista.insert(0, x) ou lista.pop(0)) exigem o deslocamento físico de todos os ponteiros subsequentes na memória, apresentando custo linear obrigatório de .


3.2 Tuplas: Imutabilidade, Pegada de Memória e Chaves de Hashing

Uma tupla (tuple) é uma sequência de referências a objetos que, após instanciada, não pode ter seus elementos adicionados, removidos ou reatribuídos.

Vantagens Técnicas frente às Listas:

  1. Pegada de Memória Mínima: Como o tamanho é invariante, o interpretador aloca rigorosamente o tamanho necessário (size == allocated), eliminando o desperdício da capacidade de sobrealocação das listas:

3.3.1 Algoritmo de Hashing e Tratamento de Colisões

  1. Geração do Hash: A chave é submetida à função criptográfica/aleatorizada SipHash-2-4 (PEP 456), gerando um número inteiro de 64 bits .
  2. Mapeamento de Índice: O índice inicial na tabela esparsa de tamanho é calculado por máscara binária rápida:
  3. Resolução de Colisão por Sondagem Aberta Perturbada: Se a posição calculada já estiver ocupada por uma chave distinta (), o CPython aplica a sequência de perturbação pseudo-aleatória: Essa fórmula garante que todos os índices da tabela sejam visitados ciclicamente sem cair em laços de repetição estéreis.

Graças ao fator de carga rigorosamente controlado (a tabela é redimensionada sempre que a ocupação atinge da capacidade total), o custo de busca, inserção e remoção no caso médio é rigorosamente constante .


3.4 Modelagem Matricial em Sistemas Elétricos de Potência: A Matriz

Em análise de redes elétricas industriais e de transmissão, as tensões nas barras e as correntes injetadas são relacionadas no domínio fasorial pela equação matricial de Kirchhoff:

Onde é a Matriz de Admitância Nodal de dimensão ( sendo o número total de barras do sistema).

Regras Sistemáticas de Formação Algorítmica:

  1. Elementos Diagonais ( — Auto-admitância da Barra ): É a soma de todas as admitâncias diretamente conectadas à barra , incluindo os elementos shunt (reatores, capacitores e admitâncias de magnetização de trafos) e todos os ramos que chegam à barra: Onde é o conjunto de todas as barras conectadas à barra por linhas de transmissão ou transformadores, e é a admitância série do ramo.
  2. Elementos Fora da Diagonal ( — Admitância Mútua entre as Barras e ): É o negativo da admitância série do ramo que conecta diretamente a barra à barra : Se não houver linha de transmissão direta entre as barras e , então .

O Desafio da Esparsidade em Redes Reais:

Em um sistema elétrico real de grande porte (como a rede do ONS com mais de barras), a imensa maioria dos nós conecta-se a apenas 2, 3 ou 4 vizinhos. Se implementada como uma matriz densa bidimensional tradicional (lista de listas em Python):

  • Consumo de memória: ;
  • Percentual de zeros: mais de da matriz é preenchida por zeros estéreis.

A solução da engenharia de computação é utilizar estruturas esparsas baseadas em dicionários:

# Mapeia exclusivamente os ramos existentes (Complexidade de espaco O(E), onde E << N^2)
Ybus_esparsa: dict[tuple[int, int], complex] = {}

Isso reduz o consumo de memória de para menos de e acelera o cálculo do produto em centenas de vezes.


🖼️ 4. Diagrama Técnico Integrado

O diagrama técnico a seguir sintetiza a organização das estruturas de dados: à esquerda, o contraste entre vetores dinâmicos contíguos (list) e tabelas de dispersão de tempo constante (dict); à direita, a aplicação direta em sistemas elétricos na formação da Matriz de Admitância Nodal :


💻 5. Implementação Prática em Engenharia Elétrica

5.1 Dimensionador Automatizado de Condutores por Ampacidade e Queda (NBR 5410)

O algoritmo a seguir automatiza o dimensionamento técnico de condutores de cobre com isolação de PVC segundo os critérios conjugados de capacidade de condução de corrente (ampacidade) e queda de tensão percentual admissível máxima ():

"""
Dimensionador Tecnico de Condutores Eletricos segundo a ABNT NBR 5410.
 
Metodo de Instalacao: B1 (Condutores isolados em eletroduto embutido em alvenaria).
Material: Cobre eletrolitico com isolacao em PVC (temperatura de operacao 70 °C).
"""
 
from typing import Dict, Tuple
 
# Banco de Dados Estruturado em Dicionario:
# Chave: Secao Nominal [mm2] -> Valor: Tupla (Capacidade Iz [A], Resistencia a 70°C [Ohm/km])
TABELA_CABOS_NBR5410: Dict[float, Tuple[float, float]] = {
    1.5:  (17.5, 14.80),
    2.5:  (24.0,  8.91),
    4.0:  (32.0,  5.57),
    6.0:  (41.0,  3.71),
    10.0: (57.0,  2.24),
    16.0: (76.0,  1.41),
    25.0: (101.0, 0.889),
    35.0: (125.0, 0.641),
    50.0: (151.0, 0.473),
    70.0: (192.0, 0.328),
    95.0: (232.0, 0.236),
}
 
 
def dimensionar_alimentador(
    corrente_projeto_ib: float,
    comprimento_metros: float,
    tensao_fase_v: float = 220.0,
    queda_max_percentual: float = 4.0,
    is_circuito_trifasico: bool = False,
) -> dict:
    """
    Determina a menor secao de condutor que atende simultaneamente ampacidade e queda.
    """
    secao_adotada = None
    laudo_detalhado = []
 
    # Fator multiplicativo da queda de tensao: 2 para monofasico, sqrt(3) para trifasico
    fator_fases = math.sqrt(3.0) if is_circuito_trifasico else 2.0
 
    for secao, (iz_nominal, r_km) in TABELA_CABOS_NBR5410.items():
        # Criterio 1: Capacidade de Corrente (Iz >= Ib)
        atende_ampacidade = iz_nominal >= corrente_projeto_ib
 
        # Criterio 2: Queda de Tensao
        resistencia_total_linha = (r_km / 1000.0) * comprimento_metros
        delta_v_volts = fator_fases * resistencia_total_linha * corrente_projeto_ib
        queda_percentual = (delta_v_volts / tensao_fase_v) * 100.0
        atende_queda = queda_percentual <= queda_max_percentual
 
        status = atende_ampacidade and atende_queda
 
        laudo_detalhado.append({
            "secao_mm2": secao,
            "iz_amperes": iz_nominal,
            "queda_v": delta_v_volts,
            "queda_pct": queda_percentual,
            "aprovado": status
        })
 
        if status and secao_adotada is None:
            secao_adotada = secao
 
    return {
        "corrente_ib": corrente_projeto_ib,
        "comprimento_m": comprimento_metros,
        "secao_recomendada_mm2": secao_adotada,
        "historico": laudo_detalhado
    }

5.2 Algoritmo de Montagem de Matriz Esparsa com Dicionários

A rotina a seguir implementa a formação algorítmica da Matriz de Admitância Nodal utilizando dicionários com chaves em tuplas imutáveis (barra_k, barra_m) para suportar sistemas com numeração arbitrária de barras:

"""
Modulo de Montagem Algoritmica da Matriz de Admitancia Nodal (Ybus).
Utiliza dicionarios esparsos para representacao computacional eficiente.
"""
 
from typing import Dict, List, Tuple
 
 
class RedeEletricaYbus:
    def __init__(self, lista_barras: List[int]):
        self.barras = sorted(lista_barras)
        # Dicionario esparso: Chave (k, m) -> Valor: Admitancia complexa Y_km
        self.ybus_esparsa: Dict[Tuple[int, int], complex] = {}
        # Elementos shunt de cada barra (capacitores ou reatores para o terra)
        self.admitancia_shunt: Dict[int, complex] = {b: 0.0 + 0.0j for b in self.barras}
 
    def adicionar_elemento_shunt(self, barra: int, y_shunt: complex):
        """Adiciona capacitor ou reator conectado entre a barra e a referencia (terra)."""
        self.admitancia_shunt[barra] += y_shunt
 
    def adicionar_linha_transmissao(self, barra_de: int, barra_para: int, r_ohm: float, x_ohm: float):
        """Insere ramo serie entre as barras k e m."""
        z_serie = complex(r_ohm, x_ohm)
        y_ramo = 1.0 / z_serie
 
        k, m = barra_de, barra_para
 
        # 1. Atualizacao dos termos fora da diagonal (Mutua): Y_km = Y_mk = - y_ramo
        self.ybus_esparsa[(k, m)] = self.ybus_esparsa.get((k, m), 0.0j) - y_ramo
        self.ybus_esparsa[(m, k)] = self.ybus_esparsa.get((m, k), 0.0j) - y_ramo
 
        # 2. Acumulacao da admitancia serie nos termos da diagonal (Auto-admitancia)
        self.ybus_esparsa[(k, k)] = self.ybus_esparsa.get((k, k), 0.0j) + y_ramo
        self.ybus_esparsa[(m, m)] = self.ybus_esparsa.get((m, m), 0.0j) + y_ramo
 
    def finalizar_matriz(self):
        """Incorpora os elementos shunt na diagonal principal da matriz."""
        for b in self.barras:
            self.ybus_esparsa[(b, b)] = self.ybus_esparsa.get((b, b), 0.0j) + self.admitancia_shunt[b]
 
    def obter_elemento(self, k: int, m: int) -> complex:
        """Consulta em tempo constante O(1) de qualquer posicao da matriz Ybus."""
        return self.ybus_esparsa.get((k, m), 0.0 + 0.0j)
 
    def imprimir_matriz_formatada(self):
        print("MATRIZ DE ADMITANCIA NODAL YBUS [Ohms^-1 ou pu]:")
        for k in self.barras:
            linha_str = ""
            for m in self.barras:
                val = self.obter_elemento(k, m)
                linha_str += f"{val.real:+7.3f}{val.imag:+7.3f}j  "
            print(f"Barra {k:2d}: [ {linha_str}]")
 
 
def main():
    # Sistema Teste de 3 Barras
    # Linha 1-2: Z = 0.02 + j0.08 pu
    # Linha 1-3: Z = 0.01 + j0.04 pu
    # Linha 2-3: Z = 0.02 + j0.06 pu
    # Capacitor shunt na Barra 2: Y_shunt = +j0.10 pu
    rede = RedeEletricaYbus(lista_barras=[1, 2, 3])
    rede.adicionar_linha_transmissao(1, 2, 0.02, 0.08)
    rede.adicionar_linha_transmissao(1, 3, 0.01, 0.04)
    rede.adicionar_linha_transmissao(2, 3, 0.02, 0.06)
    rede.adicionar_elemento_shunt(2, complex(0.0, 0.10))
    rede.finalizar_matriz()
 
    rede.imprimir_matriz_formatada()
 
 
if __name__ == "__main__":
    main()

5.3 Rastreamento Topológico de Ilhamento Elétrico com Conjuntos (set)

O algoritmo a seguir utiliza conjuntos (set) para determinar se a abertura de disjuntores provocou a desconexão ou ilhamento de barras em relação à barra de referência (slack bus):

def detectar_ilhamento(todas_barras: set[int], linhas_ativas: list[tuple[int, int]], barra_slack: int = 1) -> set[int]:
    """
    Identifica o conjunto de barras eletricamente isoladas utilizando busca em grafo com sets.
    """
    barras_energizadas = {barra_slack}
    houve_expansao = True
 
    # Algoritmo de alcance: propaga conectividade ate saturar
    while houve_expansao:
        tamanho_anterior = len(barras_energizadas)
        for de, para in linhas_ativas:
            if de in barras_energizadas:
                barras_energizadas.add(para)
            elif para in barras_energizadas:
                barras_energizadas.add(de)
        
        # Verifica se novas barras foram alcancadas
        houve_expansao = len(barras_energizadas) > tamanho_anterior
 
    # O conjunto de barras ilhadas e a diferenca de conjuntos: Todas - Energizadas
    barras_ilhadas = todas_barras - barras_energizadas
    return barras_ilhadas

📝 6. Banco de Questões de Fixação e Concursos Auditados

Questão 01 — Amortização Big-O e Alocação Geométrica de Memória em Listas

(Petrobras — Engenharia de Equipamentos / Eletrônica — Adaptada)
Um registrador digital de perturbações de subestação armazena amostras transitórias de tensão em tempo real. O engenheiro avalia duas estratégias de código em Python para acumular amostras sucessivas em memória:

  • Estratégia A: Inserir cada nova amostra no final através do método lista.append(amostra);
  • Estratégia B: Inserir cada nova amostra no início através do método lista.insert(0, amostra).
    a) Determine a complexidade assintótica de tempo total ( e ) para a inserção das amostras em cada estratégia.
    b) Justifique fisicamente a diferença de desempenho com base na organização contígua do vetor ob_item e na rotina memmove da biblioteca padrão C.

💡 Resolução Analítica Passo a Passo:

a) Determinação das Complexidades Big-O:

  • Estratégia A (append):
    Como o método append() aproveita a sobrealocação geométrica da lista em CPython, cada inserção ao final é realizada em tempo amortizado .
    Para amostras consecutivas: Para , a execução dura frações de milissegundo ().
  • Estratégia B (insert(0, x)):
    Para inserir um elemento na posição 0 de um vetor contíguo com elementos, todos os ponteiros já existentes devem ser fisicamente deslocados uma posição para a direita na memória.
    O tempo da -ésima inserção é proporcional a . Para amostras consecutivas: Para , o número de movimentações na memória é de aproximadamente: resultando em um tempo de execução de dezenas de segundos ou minutos, com saturação da CPU.

b) Justificativa em Nível de Hardware:
Em CPython, o array ob_item é um bloco linear contínuo de endereços de memória virtual.
Ao executar insert(0, x), a máquina virtual invoca a função memmove(destino, origem, bytes). Essa chamada bloqueia a CPU enquanto copia milhões de bytes para a direita para abrir espaço de 8 bytes no índice 0, provocando poluição de linhas de cache L1/L2 e invalidação de predições de branch. Na estratégia append(), o ponteiro é simplesmente gravado na próxima posição livre previamente reservada (allocated), sem deslocamento de dados existentes.


Questão 02 — Resolução de Colisões em Tabelas Hash e Ataques de Negação de Serviço

(Marinha do Brasil — Corpo de Engenheiros / Computação)
Sistemas supervisórios SCADA que utilizam tabelas de dispersão (hash tables) para indexar comandos de manobra podem ser vulneráveis a ataques de negação de serviço algorítmico (Hash Collision DoS Attacks).
a) Explique o que ocorre com o tempo de consulta e inserção de um dicionário se um invasor forjar intencionalmente centenas de milhares de chaves que resultem exatamente no mesmo valor de hash ().
b) Apresente o mecanismo de segurança incorporado a partir da PEP 456 no Python 3 para neutralizar esse vetor de ataque.

💡 Resolução Analítica Passo a Passo:

a) Degradação Assintótica por Colisões Múltiplas:
A complexidade de tempo de busca em uma tabela de dispersão é no caso médio sob a hipótese de distribuição uniforme e independente das chaves.
No entanto, se chaves colidirem no mesmo índice de hash, o mecanismo de resolução de conflitos por endereçamento aberto (open addressing) é forçado a percorrer exaustivamente a sequência completa de sondagem perturbada a cada inserção ou consulta:

  • A -ésima inserção demandará a verificação de posições colididas;
  • O tempo total para inserir chaves degrada de para: Um servidor SCADA que consumiria para processar pacotes legítimos consumirá vários minutos de de CPU para processar o lote forjado, causando paralisia operacional no controle da subestação e bloqueio de telemetrias críticas.

b) Mitigação Normativa via PEP 456 (SipHash com Semente Aleatória):
Para mitigar definitivamente essa vulnerabilidade, o Python 3.4+ implementou a especificação PEP 456, adotando a função pseudo-aleatória criptograficamente robusta SipHash-2-4.
A cada inicialização do processo Python, o interpretador gera uma semente secreta de 128 bits (hash seed) obtida do gerador de números aleatórios do sistema operacional (/dev/urandom): Como a semente varia imprevisivelmente a cada reinicialização e não é exposta externamente pela rede, um invasor remoto é matematicamente incapaz de prever quais sequências de strings colidirão entre si no interpretador do servidor, inviabilizando completamente a geração de ataques DoS por colisão de hash.


Questão 03 — Modelagem Esparsa de Topologia de Rede Elétrica: Dicionário versus Matriz Densa

(ENADE — Engenharia Elétrica / Computação)
Deseja-se modelar computacionalmente a rede de distribuição de uma concessionária de energia contendo e .
Considere que cada valor de admitância complexa ocupa de memória (dois float de 64 bits para parte real e imaginária) e que cada ponteiro em arquitetura de 64 bits consome .
a) Calcule o consumo teórico de memória da Matriz de Admitância Nodal implementada como uma matriz densa bidimensional ().
b) Calcule o consumo aproximado utilizando uma estrutura esparsa baseada em dicionário Python, onde apenas os ramos existentes e os termos da diagonal são registrados.
c) Determine o percentual de redução de memória obtido pela abordagem esparsa.

💡 Resolução Analítica Passo a Passo:

a) Memória da Matriz Densa Bidimensional:
Uma matriz densa de contém: Armazenando cada elemento como um objeto complexo de : Somando os ponteiros da lista de listas (), o consumo real totaliza:

b) Memória da Estrutura Esparsa (Dicionário):
Na matriz , os únicos elementos não nulos são:

  • A diagonal principal: ;
  • Os ramos mútuos bidirecionais das : .
    Total de entradas não nulas: Em um dicionário compacto moderno do CPython:
  • Cada entrada densa armazena o hash (8 bytes), ponteiro da tupla-chave (8 bytes) e ponteiro do objeto complexo (8 bytes) ;
  • A chave é uma tupla de 2 inteiros ();
  • O objeto complexo consome ;
  • Custo por entrada: .
    Para 34.000 entradas (com tabela esparsa redimensionada para a potência de dois mais próxima, ):

c) Redução Percentual: A modelagem esparsa com dicionários proporciona uma economia de mais de de memória RAM, permitindo carregar toda a rede elétrica nacional na memória cache de processadores embarcados.


📚 7. Referências Bibliográficas e Normas Técnicas

  1. CORMEN, Thomas H. et al. Algoritmos: Teoria e Prática. 3. ed. Rio de Janeiro: Elsevier, 2012.
  2. ABNT NBR 5410:2004 (Versão Corrigida 2008): Instalações Elétricas de Baixa Tensão. Rio de Janeiro: ABNT, 2008.
  3. GRAINGER, John J.; STEVENSON, William D. Power System Analysis. New York: McGraw-Hill, 1994.
  4. HETTINGER, Raymond. *PEP 468 – Preserving the Order of *kwargs in a Function. Python Enhancement Proposals, 2016.
  5. AUMASSON, Jean-Philippe; BERNSTEIN, Daniel J. SipHash: A Fast Short-Input PRF. In: Progress in Cryptology – INDOCRYPT 2012. Lecture Notes in Computer Science, v. 7668, p. 489-508, 2012.
  6. MONTICELLI, Alcir. Fluxo de Carga em Redes de Energia Elétrica. São Paulo: Edgard Blücher, 1983.