Ciência da computação

Conheça os principais algoritmos de ordenação

Imagine como seria buscar um número em um catálogo telefônico se os nomes das pessoas não estivessem listados em ordem alfabética? Seria muito complicado. A ordenação ou classificação de registros consiste em organizá-los em ordem crescente ou decrescente e assim facilitar a recuperação desses dados. A ordenação tem como objetivo facilitar as buscas e pesquisas de ocorrências de determinado elemento em um conjunto ordenado.

Como preza a estratégia algorítmica: “Primeiro coloque os números em ordem. Depois decidimos o que fazer.”

Na computação existe uma série de algoritmos que utilizam diferentes técnicas de ordenação para organizar um conjunto de dados, eles são conhecidos como Métodos de Ordenação ou Algoritmos de Ordenação. Vamos conhecer um pouco mais sobre eles.

Os métodos de ordenação se classificam em:

  • Ordenação Interna: onde todos os elementos a serem ordenados cabem na memória principal e qualquer registro pode ser imediatamente acessado.

  • Ordenação Externa: onde os elementos a serem ordenados não cabem na memória principal e os registros são acessados sequencialmente ou em grandes blocos.

Hoje veremos apenas os métodos de ordenação interna.

Dentro da ordenação interna temos os Métodos Simples e os Métodos Eficientes:

C Básico
Curso de C Básico
CONHEÇA O CURSO

Métodos Simples

Os métodos simples são adequados para pequenos vetores, são programas pequenos e fáceis de entender. Possuem complexidade C(n) = O(n²), ou seja, requerem O(n²) comparações. Exemplos: Insertion Sort, Selection Sort, Bubble Sort, Comb Sort.

Dica: Veja uma breve introdução à análise de algoritmos

Nos algoritmos de ordenação as medidas de complexidade relevantes são:

  • Número de comparações C(n) entre chaves.
  • Número de movimentações M(n) dos registros dos vetores.

Onde n é o número de registros.

Insertion Sort

Insertion Sort ou ordenação por inserção é o método que percorre um vetor de elementos da esquerda para a direita e à medida que avança vai ordenando os elementos à esquerda. Possui complexidade C(n) = O(n) no melhor caso e C(n) = O(n²) no caso médio e pior caso. É considerado um método de ordenação estável.

Um método de ordenação é estável se a ordem relativa dos itens iguais não se altera durante a ordenação.

O funcionamento do algoritmo é bem simples: consiste em cada passo a partir do segundo elemento selecionar o próximo item da sequência e colocá-lo no local apropriado de acordo com o critério de ordenação.


(Imagens: Wikipedia.org)

Vejamos a implementação:

void insercao (int vet, int tam){
int i, j, x;
for (i=2; i<=tam; i++){
    x = vet[i];
    j=i-1;
    vet[0] = x; 
    while (x < vet[j]){
        vet[j+1] = vet[j];
        j--;
    }
    vet[j+1] = x;
}

Para compreender melhor o funcionamento do algoritmo veja este vídeo:

Selection Sort

A ordenação por seleção ou selection sort consiste em selecionar o menor item e colocar na primeira posição, selecionar o segundo menor item e colocar na segunda posição, segue estes passos até que reste um único elemento. Para todos os casos (melhor, médio e pior caso) possui complexidade C(n) = O(n²) e não é um algoritmo estável.


(Imagens: Wikipedia.org)

Veja este vídeo para conhecer o passo a passo de execução do algoritmo:

void selecao (int vet, int tam){
    int i, j, min, x;
    for (i=1; i<=n-1; i++){
        min = i;
    for (j=i+1; j<=n; j++){
            if (vet[j] < vet[min])
            min = j;
    }
    x = vet[min];
    vet[min] = vet[i];
    vet[i] = x;
    }
}

Métodos Eficientes

Os métodos eficientes são mais complexos nos detalhes, requerem um número menor de comparações. São projetados para trabalhar com uma quantidade maior de dados e possuem complexidade C(n) = O(n log n). Exemplos: Quick sort, Merge sort, Shell sort, Heap sort, Radix sort, Gnome sort, Count sort, Bucket sort, Cocktail sort, Timsort.

Quick Sort

O Algoritmo Quicksort, criado por C. A. R. Hoare em 1960, é o método de ordenação interna mais rápido que se conhece para uma ampla variedade de situações.

Provavelmente é o mais utilizado. Possui complexidade C(n) = O(n²) no pior caso e C(n) = O(n log n) no melhor e médio caso e não é um algoritmo estável.

É um algoritmo de comparação que emprega a estratégia de “divisão e conquista”. A ideia básica é dividir o problema de ordenar um conjunto com n itens em dois problemas menores. Os problemas menores são ordenados independentemente e os resultados são combinados para produzir a solução final.

Basicamente a operação do algoritmo pode ser resumida na seguinte estratégia: divide sua lista de entrada em duas sub-listas a partir de um pivô, para em seguida realizar o mesmo procedimento nas duas listas menores até uma lista unitária.

Funcionamento do algoritmo:

  • Escolhe um elemento da lista chamado pivô.
  • Reorganiza a lista de forma que os elementos menores que o pivô fiquem de um lado, e os maiores fiquem de outro. Esta operação é chamada de “particionamento”.
  • Recursivamente ordena a sub-lista abaixo e acima do pivô.

Dica: Conheça um pouco sobre algoritmos recursivos

(Imagem: Wikipedia.org)

Veja este vídeo para conhecer o passo a passo de execução do algoritmo:

void quick(int vet[], int esq, int dir){
    int pivo = esq, i,ch,j;         
    for(i=esq+1;i<=dir;i++){        
        j = i;                      
        if(vet[j] < vet[pivo]){     
            ch = vet[j];               
            while(j > pivo){           
                vet[j] = vet[j-1];      
                j--;                    
            }
            vet[j] = ch;               
            pivo++;                    
        }
    }
    if(pivo-1 >= esq){              
        quick(vet,esq,pivo-1);      
    }
    if(pivo+1 <= dir){              
        quick(vet,pivo+1,dir);      
    }
 }

A principal desvantagem deste método é que ele possui uma implementação difícil e delicada, um pequeno engano pode gerar efeitos inesperados para determinadas entradas de dados.

Mergesort

Criado em 1945 pelo matemático americano John Von Neumann o Mergesort é um exemplo de algoritmo de ordenação que faz uso da estratégia “dividir para conquistar” para resolver problemas. É um método estável e possui complexidade C(n) = O(n log n) para todos os casos.

Esse algoritmo divide o problema em pedaços menores, resolve cada pedaço e depois junta (merge) os resultados. O vetor será dividido em duas partes iguais, que serão cada uma divididas em duas partes, e assim até ficar um ou dois elementos cuja ordenação é trivial.

Para juntar as partes ordenadas os dois elementos de cada parte são separados e o menor deles é selecionado e retirado de sua parte. Em seguida os menores entre os restantes são comparados e assim se prossegue até juntar as partes.


(Imagens: Wikipedia.org)

Veja o vídeo para conhecer o passo a passo da execução do algoritmo:

void mergeSort(int *vetor, int posicaoInicio, int posicaoFim) {
    int i, j, k, metadeTamanho, *vetorTemp;
    if(posicaoInicio == posicaoFim) return;
    metadeTamanho = (posicaoInicio + posicaoFim ) / 2;

    mergeSort(vetor, posicaoInicio, metadeTamanho);
    mergeSort(vetor, metadeTamanho + 1, posicaoFim);

    i = posicaoInicio;
    j = metadeTamanho + 1;
    k = 0;
    vetorTemp = (int *) malloc(sizeof(int) * (posicaoFim - posicaoInicio + 1));

    while(i < metadeTamanho + 1 || j  < posicaoFim + 1) {
        if (i == metadeTamanho + 1 ) { 
            vetorTemp[k] = vetor[j];
            j++;
            k++;
        }
        else {
            if (j == posicaoFim + 1) {
                vetorTemp[k] = vetor[i];
                i++;
                k++;
            }
            else {
                if (vetor[i] < vetor[j]) {
                    vetorTemp[k] = vetor[i];
                    i++;
                    k++;
                }
                else {
                    vetorTemp[k] = vetor[j];
                    j++;
                    k++;
                }
            }
        }

    }
    for(i = posicaoInicio; i <= posicaoFim; i++) {
        vetor[i] = vetorTemp[i - posicaoInicio];
    }
    free(vetorTemp);
}

Shell Sort

Criado por Donald Shell em 1959, o método Shell Sort é uma extensão do algoritmo de ordenação por inserção. Ele permite a troca de registros distantes um do outro, diferente do algoritmo de ordenação por inserção que possui a troca de itens adjacentes para determinar o ponto de inserção. A complexidade do algoritmo é desconhecida, ninguém ainda foi capaz de encontrar uma fórmula fechada para sua função de complexidade e o método não é estável.

Os itens separados de h posições (itens distantes) são ordenados: o elemento na posição x é comparado e trocado (caso satisfaça a condição de ordenação) com o elemento na posição x-h. Este processo repete até h=1, quando esta condição é satisfeita o algoritmo é equivalente ao método de inserção.

A escolha do salto h pode ser qualquer sequência terminando com h=1. Um exemplo é a sequencia abaixo:

h(s) = 1, para s = 1
h(s) = 3h(s - 1) + 1, para s > 1

A sequência corresponde a 1, 4, 13, 40, 121, …

Knuth (1973) mostrou experimentalmente que esta sequencia é difícil de ser batida por mais de 20% em eficiência.

Veja o vídeo que demonstra o passo a passo da execução do algoritmo:

void shellSort(int *vet, int size) {
    int i , j , value;
    int gap = 1;
    while(gap < size) {
        gap = 3*gap+1;
    }
    while ( gap > 1) {
        gap /= 3;
        for(i = gap; i < size; i++) {
            value = vet[i];
            j = i - gap;
            while (j >= 0 && value < vet[j]) {
                vet [j + gap] = vet[j];
                j -= gap;
            }
            vet [j + gap] = value;
        }
    }
}

Conclusão

Neste artigo foi apresentado os principais métodos de ordenação com foco no conceito e funcionamento de cada um deles.

Você pode ver na tabela abaixo a comparação entre eles:

(Imagem: DECOM – UFOP)

Um abraço e até a próxima!

Python - Estrutura de dados - Parte 1
Curso de Python - Estrutura de dados - Parte 1
CONHEÇA O CURSO

Desmistificando os algoritmos recursivos

Os algoritmos recursivos são fundamentais na solução de muitos problemas envolvendo a computação, ainda assim, muitos programadores os veem como algo complexo e de difícil implementação.

Como disse o prof. Siang Wun Song:

Para fazer um procedimento recursivo é preciso ter fé.

Calma! Apesar da fala do prof. Siang, implementar um algoritmo recursivo não é um bicho de sete cabeças. Veremos alguns passos para compreendê-los de vez. o/

C Básico
Curso de C Básico
CONHEÇA O CURSO

Primeiro devemos entender o que é a recursividade. Uma função recursiva chama a si mesma dentro do próprio escopo. Pode ser uma recursão direta onde uma função A chama a própria função A ou uma recursão indireta onde uma função A chama uma função B que por sua vez chama a função A. Além de chamar a si mesma, a função recursiva deve possuir uma condição de parada que a impedirá de entrar em loop infinito.

Antes de criar uma função recursiva para determinado problema, é preciso saber se ele possui uma estrutura recursiva. Neste caso, devemos verificar se parte do problema possui o mesmo tipo do original, assim podemos dizer que a solução para ele é a recursividade. Para compreender melhor, vamos analisar o caso do somatório onde temos um número natural n >= 0 e queremos descobrir a soma de todos os números de n até 0.

Antes de criar o algoritmo devemos extrair dois elementos do problema: O caso base que se tornará a condição de parada e o passo recursivo. Vejamos algumas possíveis iterações do problema:

No caso do somatório, é possível identificar que para cada valor de n o último elemento sempre é 0, assim podemos confirmar que este é o caso base.

Analisando um pouco mais as iterações temos que para cada valor de n vamos diminuindo em 1 até que chegue ao valor 0.

Assim, nós temos que o somatório de um número natural n é o próprio n adicionado ao somatório do número antecessor (Somatório de n = n + (n-1) + (n-2) + ... + 0). Logo, o nosso passo recursivo é n + soma(n-1).

Sabemos que o caso base é 0 e o somatório de 0 é ele mesmo, estão essa será a nossa condição de parada. O nosso passo recurso é n + soma(n-1), assim já temos os elementos da nossa função recursiva e podemos criá-la:

int soma(int n){
    if(n == 0)
        return 0;
    return n + soma(n-1);
}

Esta é a implementação em C para resolver o problema de forma recursiva. Para um número natural n informado, a função irá executar até que seja encontrado o caso base e assim retornar o valor do somatório.

Vamos analisar outro problema clássico que pode ser resolvido com recursividade: o fatorial de um número natural n >= 1. O fatorial é bem semelhante ao somatório, porém é feito com multiplicação sucessiva.

Vamos analisar as possíveis iterações:

No exemplo do fatorial, o nosso caso base é 1, portanto, a nossa condição de parada será verificar se o número é 1 e, caso seja, a função deverá retorná-lo. Também é possível definir o nosso passo recursivo: fatorial de n = n x (n - 1) x (n - 2) x ... x 1 logo n! = n x fatorial(n - 1). Por fim, chegamos à seguinte implementação em C:

int fatorial(int n){
    if(n == 1)
        return 1;
    return n * fatorial(n-1);
}

Uma grande vantagem da recursividade é o fato de gerar uma redução no tamanho do algoritmo, permitindo descrevê-lo de forma mais clara e concisa. Porém, todo cuidado é pouco ao se fazer módulos recursivos. Basta seguir estes passos e você não terá grandes problemas. Como disse L. Peter Deutsch:

Iterar é humano, fazer recursão é divino.

Um abraço e até a próxima!

Laravel - Desenvolvimento de APIs REST
Curso de Laravel - Desenvolvimento de APIs REST
CONHEÇA O CURSO

Processo de execução de um código no .NET Framework

Quando estamos desenvolvendo, o único momento em que pensamos na compilação do código é quando algum erro ocorre. Fora isso, essa é só mais uma etapa do processo de execução (que costuma passar despercebida).

Só que um bom programador precisa entender como este processo funciona, para criar códigos melhores, entender mais a linguagem que se está utilizando e compreender melhor os erros gerados.

No geral as linguagens possuem um processo de compilação parecido, mas aqui veremos o processo de compilação de uma aplicação desenvolvida para o .NET Framework.

Nele, separamos o processo de compilação em quatro etapas:

  • Escolhendo um compilador;
  • Compilando o código para a linguagem intermediária;
  • Compilando o código da linguagem intermediário para a nativa;
  • Executando o código.

Essas etapas são ilustradas na imagem abaixo:

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Escolhendo um compilador

O .NET Framework possui um importante recurso que permite a interoperabilidade das linguagens suportadas por ele, que é o Common Language Runtime
(CLR).

Seguindo as especificações definidas no padrão ECMA-335 (Obs: Esse link leva a um arquivo PDF) – que define as especificações do CLR – qualquer linguagem pode definir um compilador suportado pelo .NET. Com isso, é possível desenvolver aplicações para o .NET utilizando C#, F#, Perl, Cobol, Ruby etc.

Assim, na compilação, a primeira coisa que o .NET faz é selecionar o compilador definido para a linguagem do código que será compilado.

É função do compilador verificar o código que está sendo analisado. Ele verificará se algum token está sendo utilizado de forma errônea e se todas as regras da linguagem estão sendo obedecidas. Basicamente ele verificará se há algum erro de digitação no código.

Compilando o código para a linguagem intermediária

Agora que o .NET começa a fazer a sua “mágica”. Ao final do processo de compilação, o compilador converte o código-fonte para uma linguagem intermediária, que no .NET, é chamada de Microsoft Intermediate Language (MSIL), ou apenas Intermediate Language (IL).

O IL é um conjunto de instruções independente da CPU, que pode ser convertido de forma mais eficiente para código nativo.

Nele temos instruções para carregar, armazenar, iniciar e chamar métodos em objetos, bem como, instruções para operações lógicas e matemáticas, controle de fluxo, acesso direto a memória, manipulação de erros, entre outros recursos.

Antes deste código ser gerado, ele precisa ser convertido em tempo de execução para um código de máquina que é entendido pela CPU do computador. Normalmente este processo é realizado por um compilador Just-in-Time (JIT). Como o CLR fornece um ou mais compiladores de acordo com a arquitetura do computador, o mesmo conjunto de instruções IL pode ser compilado e executado em qualquer arquitetura suportada pelo .NET

Quando o código intermediário é produzido, também são gerados metadados. Esses metadados descrevem os tipos utilizados no código incluindo as suas definições, as assinaturas dos seus membros, as referências a outros códigos e qualquer coisa que será utilizada em tempo de execução.

O IL e os metadados são contidos em um arquivo executável portátil (PE – portable executable), que é baseado e que estende o Microsoft PE e Common Object File Format (COFF), historicamente utilizado para conteúdo executável. Este formato de arquivo, que possui o IL ou código nativo, bem como metadados, permite que o sistema operacional reconheça características do CLR. A presença dos metadados com o código intermediário permite que o código se descreva, o que significa que não há necessidade de incluir bibliotecas de tipos ou interfaces de definição da linguagem (IDL).

Durante a execução, o sistema localiza e extrai dos metadados as informações que necessitar.

Compilando o código da linguagem intermediária para a nativa

Antes de executar o código IL, é necessário compilá-lo para a linguagem nativa, suportada pela arquitetura da máquina alvo. Para fazer este processo, o .NET fornece duas formas de conversão:

  • Um compilador Just-in-Time (JIT);
  • O Ngen.exe (Native Image Generator).

Compilação com um compilador JIT

A compilação JIT converte o código intermediário em código nativo em tempo de execução. Conforme um código IL for requisitado, o JIT o converte para código nativo.

Como o CLR fornece um compilador JIT para cada arquitetura suportada pelo .NET, um código IL pode ser compilado pelo JIT e executado em diferentes arquiteturas. No entanto, se o código chamar APIs nativas de uma plataforma, ou algum recurso específico, o código IL só poderá ser executado nesta plataforma.

A compilação JIT leva em conta a possibilidade de algum código nunca ser chamado durante a execução. Assim, em vez de usar tempo e memória para converter todo o código, em um arquivo PE nativo, ele converte apenas o que for necessário para a execução, e armazena este código nativo gerado, na memória, para ele ficar acessível para as chamadas subsequentes.

Por exemplo, na primeira vez que um método é invocado durante a execução, o JIT irá convertê-lo para código nativo e armazená-lo na memória. Quando este código for chamado novamente, o JIT irá diretamente na memória obter o código nativo, em vez de tentar convertê-lo novamente.

Compilação com o Ngen.exe

Como o compilador JIT gera o código nativo em tempo de execução, isso pode impactar um pouco na performance da aplicação. Na maioria dos casos, esta perda é aceitável ou irrisória. Além disso, o código gerado pelo compilador JIT fica vinculado ao processo que desencadeou a compilação, assim ele não pode ser compartilhado entre vários processos.

Para permitir que o código gerado possa ser compartilhado entre múltiplas chamadas ou entre vários processos que compartilham um conjunto de códigos nativos, o .NET possui o compilador Ngen.exe, também chamado de compilador Ahead-of-Time (AOT).

Este compilador gera o código nativo de uma forma muito parecida que o JIT, mas eles se diferenciam em três pontos:

  • O código IL é convertido antes do arquivo ser executado, no lugar de convertê-lo durante a execução.
  • Todo o código é convertido de uma única vez.
  • O código convertido é salvo no disco, e não na memória.

Verificação de código

Se não for desabilitado, durante a compilação para o código nativo, tanto com JIT ou com o AOT, o código IL passa por uma verificação que analisa se o código é “type safe”, o que significa que o código só acessa locais da memória que ele está autorizado a acessar.

Isso ajuda a isolar um objeto do outro e ajuda a protegê-lo contra corrupção indevida ou mal-intencionada. Ele também garante que as restrições de segurança podem ser aplicadas de forma confiável.

Para verificar se o código é “type safe”, a verificação se baseia em três afirmações (que precisam ser verdadeiras):

  • Uma referência a um tipo é estritamente compatível com o tipo a ser referenciado;
  • Somente operações apropriadamente definidas são invocadas em um objeto;
  • As identidades são de quem afirmam ser.

Se o requisito “type safe” estiver ativo e o código não passar na verificação acima, é gerando um erro de execução.

Executando o código

O processo final de execução, é a execução do código propriamente dito. Como já dito nos tópicos anteriores, durante esta execução o CLR e os metadados irão fornecer as informações que forem necessárias para a execução da aplicação.

Lendo assim, é possível notar que o processo não é simples, mas não é tão complexo, por isso que é importante o seu entendimento.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Introdução à análise de algoritmos

Os algoritmos fazem parte do dia a dia das pessoas. Seja uma receita culinária ou instruções para uso de medicamentos. Ou seja, é uma sequência de ações executáveis para chegar à solução de um determinado tipo de problema. Segundo Edsger Dijkstra, um algoritmo corresponde a uma descrição de um padrão de comportamento, expresso em termos de um conjunto finito de ações.

Desta forma, a análise de algoritmos (descrita e difundida por D.E. Knuth) tem como função determinar os recursos necessários para executar um dado algoritmo. Ela estuda a correção e o desempenho através da análise de correção e da análise de complexidade. Dados dois algoritmos para um mesmo problema, a análise permite decidir qual dos dois é mais eficiente.

C Básico
Curso de C Básico
CONHEÇA O CURSO

Análise da correção

A análise da correção procura entender porque um dado algoritmo funciona. É preciso ter certeza de que o algoritmo está correto. Como algoritmos prometem resolver problemas, é esperado que ele cumpra essa premissa.

Análise da complexidade

A Análise da complexidade procura estimar a velocidade do algoritmo. Prever o tempo que o algoritmo vai consumir. Verificar qual é o custo de usar um dado algoritmo para resolver um problema específico. Porém, neste aspecto, encontram-se duas dificuldades:

  1. O tempo gasto depende dos dados de entrada.
  2. O tempo também depende da capacidade de hardware do computador utilizado.

Mas, mesmo assim, é possível definir a complexidade de determinado algoritmo.

Para medir o custo de execução de um algoritmo é comum definir uma função de custo ou função de complexidade, f; f(n) é a medida do tempo necessário para executar um algoritmo para um problema de tamanho n;

Para entendermos melhor, vejamos o trecho de código abaixo:

int soma = 0;
for (i=0; i<n; i++)
    soma = soma + vet[i];

O custo para execução deste algoritmo é f(n) = n. Pois o laço executa de acordo com o tamanho de n. Se n=10, logo o laço executará 10 vezes, se for igual a 100 executará 100 vezes.

Podemos tornar este algoritmo mais eficiente fazendo a seguinte modificação:

int soma = vet[0]; 
for (i=1; i<n; i++)
    soma = soma + vet[i];

Assim, temos uma nova função de complexidade, f(n) = n - 1.

Os dois trechos de códigos resolvem o mesmo problema, somar os elementos de um vetor. Porém, cada um tem um custo de execução diferente do outro, a partir da análise, podemos definir qual é o algoritmo mais eficiente para resolver este problema específico.

Melhor caso, Pior caso, Caso Médio.

Na análise da complexidade, definimos para um algoritmo, o melhor, o pior e o caso médio, como forma de mensurar o custo do algoritmo de resolver determinado problema diante de diferentes entradas.

  • Melhor caso: é quando o algoritmo representa o menor tempo de execução sobre todas as entradas de tamanho n;

  • Pior caso: maior tempo de execução sobre todas as entradas de tamanho n;

  • Caso médio (ou caso esperado): é a média dos tempos de execução de todas as entradas de tamanho n.

Desta forma, considere o problema de acessar um determinado registro de um vetor de inteiro; cada registro contém um índice único que é utilizado para acessar o elemento do vetor. O problema: dada uma chave qualquer, localize o registro que contenha esta chave; O algoritmo de pesquisa mais simples é o que faz a pesquisa sequencial.

Seja f uma função de complexidade tal que f(n) é o número de registros consultados no arquivo, temos:

  • Melhor caso: f(n) = 1 (registro procurado é o primeiro consultado);

  • Pior caso: f(n) = n (registro procurado é o último consultado ou não está presente no arquivo);

  • Caso médio: f(n) = (n+1)/2 (a média aritmética entre o melhor e o pior caso);

Além disso, a análise de algoritmos estuda certos paradigmas como divisão e conquista, programação dinâmica, gula, busca local, aproximação, entre outros que se mostraram úteis na criação de algoritmos para vários problemas computacionais.

Conclusão

Neste artigo, vimos uma breve introdução do que se trata a análise de algoritmos. Como ela é útil para definir o algoritmo mais eficiente em determinados problemas. Assim, o objetivo final não é apenas fazer códigos que funcionem, mas que sejam também eficientes.

“Um bom algoritmo, mesmo rodando em uma máquina lenta, sempre acaba derrotando (para instâncias grandes do problema) um algoritmo pior rodando em uma máquina rápida. Sempre.” – S. S. Skiena, The Algorithm Design Manual

Um abraço e até a próxima!

C++ Básico
Curso de C++ Básico
CONHEÇA O CURSO