Complexidade de Algoritmos e Análise de Desempenho

Recursão, Método Master e Árvores Binárias de Pesquisa

Última ocorrência: 2023-09-11 em Universidade LaSalle Canoas

Assunto

  1. Recursão
  2. Divisão e Conquista
    • Muitos algoritmos úteis são recursivos na sua estrutura. Para resolver um problema eles chamam a si mesmos resolvendo instâncias mais simples do mesmo problema, resolvendo os subproblemas recursivamente e combinando as soluções para resorver o preblema original
    • Passos do paradigma Divisão e Conquista:
      1. Divisão
        Divide o problema em subproblemas que são instâncias menore que o problema original
      2. Conquista
        Resolve os problemas menores, onde, em alguns casos, a solução é trivial.
      3. Combinação
        Combina as soluções dos subproblemas para resolver o problema original.
    • Exemplo: Merge Sort
      • Divisão
        Divide a sequência de n elementos a ser ordenada em duas subsequências de n/2 elementos cada.
      • Conquista
        Ordena as duas subsequências recursivamente, utilizando o merge sort.
      • Combinação
        Junta as duas sequências, mantendo o resultado ordenado, para produzir a resposta.
      • Note que a recursão termina quando a ordenação do array é trivial e não precisa ser executada (array de um elemento).
      • A operação chave é a combinação dos dois arrays ordenados, onde a complexidade de tempo é Θ(n)
    • Análise de complexidade em algoritmos de divisão e conquista
      • Podemos descrever o tempo de execução do algoritom utilizando uma equação de recorrência, que descreve o tempo de execução do problema de tamanho n em termos do tempo de execução dos problemas menores.
      • Dado que T(n) o tempo de execução do problema de tamanho n.
      • Dado que para um problema pequeno o suficiente, quando nc para uma constante c, a solução trivial é executada em tempo constante, Θ(1)
      • Se dividirmos o problema em a subproblemas, cada um com tamanho 1b do tamanho original, e levando D(n) tempo para a divisão e C(n) tempo para combinar as soluções, obtemos então a equação de recorrência: T(n)={Θ(1)sencaT(n/b)+D(n)+C(n)casocontrário
    • Análise do Merge Sort
      • Para simplificar a análise do merge sort, assumimos que o tamanho da entrada é uma potência de 2 (n=2Θ(1)).
      • A cada divisão os subproblemas tem os n elementos divididos em n2 elementos.
      • Quando o subproblema tem n=1 elementos, a ordenação é trivial, não necessitando de nenhuma operação, logo Θ(1) se n=1.
      • Para $n \gt 1:
        • Divisão
          Basta calcular o índice do elemento do meio do subarray, que pode ser feito em tempo constante, logo D(n)=Θ(1)
        • Conquista
          Recursivamente se resolve dois subprobleams (a=2) de tamanho n2 (b=2), que contribui com 2T(n2) para o tempo de execução
        • Combinação
          Já vimos que a complexidade de tempo para combinar os elementos é Θ(n), logo, C(n)=Θ(n)
      • Logo a recorrência para o tempo de execução do pior caso do merge sorte é dada por: T(n)={Θ(1)sen=12T(n/2)+Θ(n)sen>1

      • Intuitivamente, podemos resolver essa recorrência. Reescrevemos a recorrência como: T(n)={csen=12T(n/2)+cnsen>1

      • Sabemos que o tempo total de execução é T(n), que é o tempo da combinação cn somado aos tempos dos subproblemas de tamanho n2, ou seja, T(n2).
      • Cada subproblema T(n/2) leva cn2 mais T(n/4), e assim sucessivamente até que o problema seja trivial (n=1) onde o custo de resolver cada um dos problemas é c
      • Se representarmos esse problema como uma árvore de recursão, a soma dos tempos de cada nível da árvore é cn, e a altura da árvore é 1+log(n) (mais especificamente 1+log2n)
      • Logo o tempo total de execução é cnlogn+cn, onde, pela análise assintótica, temos Θ(nlogn).
  3. Método Master
    • Podemos resolver as recorrências dos algoritmos recursivos através de:
      • Método da Substituição
        Escolhemos um limite e utilizamos indução matemática para provar que nossa escolha estava correta.
      • Método da árvore de recursão
        Convertemos a recorrência numa árvore onde os nós representam os custos de execução em cada um dos níveis da recursão e utilizamos técnicas para encotrar os limites dos somatórias para resolver a recorrência
      • Método master
        Que provê limites para recorrências do tipo T(n)=aT(nb)+f(n), quando a1, b>1.
    • Para utilizar o método master é preciso decorar três casos, e, com isso, você consegue determinar os limites assintóticos para diversas recorrências.
    • O método master depende do Teorema Master:
      • Sejam a1 e b>1 constantes, seja f(n) uma função, e T(n) definido para inteiros não-negativos pela recorrência T(n)=aT(nb)+f(n), onde nb significa nb ou nb,
      • Então T(n) tem os seguintes limites assintóticos:
        • Caso 1
          Se f(n)=O(nlogbaϵ) para alguma constante ϵ>0, então T(n)=Θ(nlogba)
          ou seja, o tempo de resolver os subproblemas se sobropõe ao tempo de dividir/combinar.
        • Caso 2
          Se f(n)=Θ(nlogbalogkn), então T(n)=Θ(nlogbalogk+1n)
          ou seja, os tempos de resolver os subproblemas e de dividir/combinar são semelhantes. T(n)=Θ(nlogbalogn)=Θ(f(n)logn)
        • Caso 3
          Se f(n)=Ω(nlogba+ϵ) para alguma constante ϵ>0, e se af(nb)cf(n) para alguma constante c<1 e todo n suficientemente grande, então T(n)=Θ(f(n))
          ou seja, o tempo de dividir/combinar se sobrepõe ao tempo de resolver os subproblemas.
    • Para os casos entre o caso 2 e 3, temos as seguintes extensões:
      • Caso 2a
        Se f(n)=Θ(nlogbaϵlogkn) para k>1, então T(n)Θ(nlogbaϵlogk+1n)
      • Caso 2b
        Se f(n)=Θ(nlogbaϵlogkn) para k=1, então T(n)Θ(nlogbaϵloglogn)
      • Caso 2c
        Se f(n)=Θ(nlogbaϵlogkn) para k<1, então T(n)Θ(nlogbaϵ)
    • Exemplos de aplicação do método master
      • T(n)=9T(n3)+n
        • a = 9
        • b = 3
        • f(n)=n
        • Onde temos que nlogba=nlog39=Θ(n2)
        • Como f(n)=O(nlog39), com ϵ=1, podemos aplicar o caso 1
        • Logo T(n)=Θ(n2)
      • T(n)=T(2n3)+1
        • a = 1
        • b = 32
        • f(n)=1
        • Onde temos que nlogba=nlog321=n0=1
        • Como f(n)=Θ(1) podemos aplicar o caso 2
        • Logo T(n)=Θ(logn)
      • T(n)=3T(n4)+nlogn
        • a = 3
        • b = 4
        • f(n)=nlogn
        • Onde temos que nlogba=nlog43=O(n0.793)
        • Como f(n)=Ω(nlog43+ϵ), onde ϵ0.2 podemos aplicar o caso 3, caso possamos mostrar que a condição de regularidade é valida para f(n).
        • Para um n suficientemente largo, temos af(nb)=3(n4)log(n4)(34)nlogn=cf(n) para c=34.
        • Logo, podemos aplicar o caso 3, e T(n)=Θ(nlogn)
      • T(n)=2T(n2)+nlogn
        • a = 2
        • b = 2
        • f(n)=nlogn
        • nlogba=n
        • Aparentemente, poderíamos aplicar o caso 3, uma vez que f(n)=nlogn é assintóticamente maior que nlogba=n, porém não é polinomialmente maior.
        • A razão f(n)nlogba=(nlogn)n=logn, que é assintoticamente menor que nϵ para qualquer ϵ positivo e constante, porém não é polinomialmente maior.
        • Porém, podemos aplicar o caso 2a, uma vez que k=1, logo temos T(n)=Θ(nlog22log1+1n)=Θ(nlog2n)
  4. Árvores Binárias de Pesquisa
    • Regra de criação:
      • insert(node.left, k) if k < node.key else insert(node.right, k)
    • Complexidade do pior caso:
      • Inserção: O(n)
      • Exclusão: O(n)
      • Busca: O(n)
    • Complexidade do melhor caso:
      • Inserção: O(logn)
      • Exclusão: O(logn)
      • Busca: O(logn)
    • Árvores Binárias de Pesquisa auto-balanceáveis
      • Árvores AVL
        • Algoritmos:
          • Inserção
            após a inserção na BST, os nós, a partir do nó-pai do nó inserido, tem os fatores de balanceamento corrigidos até que a altura do nó não se altere, ou seja corrigida a raiz da árvore.
          • Exclusão
            após a exclusão na BST, os nós, a partir do nó-pai do nó removido, tem os fatores de balanceamento corrigidos até que a altura do nó não se altere, ou seja corrigida a raiz da árvore.
          • Validação de um nó AVL:
            • Fator de Balanceamento: FB=H(left)H(right)
            • Fator de balanceamento válido: $ FB \lt 2$
          • Correção do nó AVL:
            if FB(node) == -2:
              if FB(node.left) == +1:
              rotate_left(node.left)
              rotate_right(node)
            if FB(node) == +2:
              if FB(node.right) == -1:
              rotate_right(node.right)
              rotate_left(node)
            
        • Complexidade:
          • Busca: Θ(logn)
          • Inserção: Θ(logn)
          • Exclusão: Θ(logn)
        • Rotações: O(logn)
      • Outros modelos:
        • Árvores Red-Black
        • B-Trees
    • Binary Search Sorting
      • Complexidade de tempo quando se utiliza uma árvore auto-balanceável…
      • Assumindo uma máquina de ponteiros:Θ(nlogn)
        • Utiliza ponteiros, e cada nó deve armazenar meta dados da estruturas
      • Assumindo uma máquina de acesso aleatório: O(nlogn),amortizado
        • Cada posiçõa deve armazenar meta dados da estruturas

Questões

  1. Implemente uma árvore binária de pesquisa.
  2. Implemente uma árvore binária de pesquisa do tipo AVL.
  3. Implemente uma função que verifica se uma sub-árvore é uma árvore AVL válida, com a interface is_valid_avl(node). Qual a complexidade de tempo dessa função?

Recursos para essa aula

Bibliografia

  1. Cormen, E. et al. Introduction to Algorithms. Caps. 2 e 3.
  2. Master Theorem (Analysis of Algorithms)
  3. Árvores AVL (Wikipedia)
  4. AVL Trees (Geeks For Geeks)