CodeGym /Blogue Java /Random-PT /Complexidade algorítmica
John Squirrels
Nível 41
San Francisco

Complexidade algorítmica

Publicado no grupo Random-PT
Oi! A lição de hoje será um pouco diferente das demais. Será diferente porque está apenas indiretamente relacionado ao Java. Complexidade algorítmica - 1 Dito isto, este tópico é muito importante para todo programador. Nós vamos falar sobre algoritmos . O que é um algoritmo? Em termos simples, é uma sequência de ações que devem ser concluídas para atingir um resultado desejado . Usamos algoritmos frequentemente na vida cotidiana. Por exemplo, todas as manhãs você tem uma tarefa específica: ir à escola ou ao trabalho e, ao mesmo tempo, ser:
  • vestido
  • Limpar
  • federal
Qual algoritmo permite alcançar esse resultado?
  1. Acorde usando um despertador.
  2. Tome um banho e lave-se.
  3. Faça o café da manhã e um pouco de café ou chá.
  4. Comer.
  5. Se você não passou suas roupas na noite anterior, passe-as.
  6. Vestir-se.
  7. Saia de casa.
Essa sequência de ações certamente permitirá que você obtenha o resultado desejado. Na programação, estamos constantemente trabalhando para concluir tarefas. Uma parte significativa dessas tarefas pode ser executada usando algoritmos já conhecidos. Por exemplo, suponha que sua tarefa seja esta: classificar uma lista de 100 nomes em uma matriz . Esta tarefa é bastante simples, mas pode ser resolvida de diferentes maneiras. Aqui está uma solução possível: Algoritmo para classificar nomes em ordem alfabética:
  1. Compre ou baixe a edição de 1961 do Webster's Third New International Dictionary.
  2. Encontre todos os nomes da nossa lista neste dicionário.
  3. Em um pedaço de papel, escreva a página do dicionário em que o nome está localizado.
  4. Use os pedaços de papel para classificar os nomes.
Essa sequência de ações realizará nossa tarefa? Sim, certamente será. Essa solução é eficiente ? Dificilmente. Aqui chegamos a outro aspecto muito importante dos algoritmos: sua eficiência . Existem várias maneiras de realizar essa tarefa. Mas tanto na programação quanto na vida cotidiana, queremos escolher a maneira mais eficiente. Se sua tarefa é fazer uma torrada com manteiga, você pode começar plantando trigo e ordenhando uma vaca. Mas isso seria ineficientesolução — levará muito tempo e custará muito dinheiro. Você pode realizar sua tarefa simples simplesmente comprando pão e manteiga. Embora permita que você resolva o problema, o algoritmo envolvendo trigo e vaca é muito complexo para ser usado na prática. Na programação, temos uma notação especial chamada notação O grande para avaliar a complexidade dos algoritmos. Big O permite avaliar o quanto o tempo de execução de um algoritmo depende do tamanho dos dados de entrada . Vejamos o exemplo mais simples: transferência de dados. Imagine que você precisa enviar algumas informações na forma de um arquivo a uma longa distância (por exemplo, 5.000 milhas). Qual algoritmo seria mais eficiente? Depende dos dados com os quais você está trabalhando. Por exemplo, suponha que temos um arquivo de áudio de 10 MB. Complexidade algorítmica - 2Nesse caso, o algoritmo mais eficiente é enviar o arquivo pela Internet. Não poderia demorar mais do que alguns minutos! Vamos reformular nosso algoritmo: "Se você deseja transferir informações na forma de arquivos a uma distância de 5.000 milhas, envie os dados pela Internet". Excelente. Agora vamos analisá-lo. Cumpre nossa tarefa?Bem, sim, ele faz. Mas o que podemos dizer sobre sua complexidade? Hmm, agora as coisas estão ficando mais interessantes. O fato é que nosso algoritmo depende muito dos dados de entrada, ou seja, do tamanho dos arquivos. Se tivermos 10 megabytes, está tudo bem. Mas e se precisarmos enviar 500 megabytes? 20 GB? 500 terabytes? 30 petabytes? Nosso algoritmo vai parar de funcionar? Não, todas essas quantidades de dados podem de fato ser transferidas. Vai demorar mais? Sim vai! Agora sabemos uma característica importante do nosso algoritmo: quanto maior a quantidade de dados a enviar, mais tempo levará para executar o algoritmo. Mas gostaríamos de ter um entendimento mais preciso dessa relação (entre o tamanho dos dados de entrada e o tempo necessário para enviá-los). No nosso caso, a complexidade algorítmica é linear . "Linear" significa que, à medida que a quantidade de dados de entrada aumenta, o tempo necessário para enviá-los aumentará aproximadamente proporcionalmente. Se a quantidade de dados dobrar, o tempo necessário para enviá-los será o dobro. Se os dados aumentarem 10 vezes, o tempo de transmissão aumentará 10 vezes. Usando a notação O grande, a complexidade do nosso algoritmo é expressa como O(n). Você deve se lembrar dessa notação para o futuro — ela é sempre usada para algoritmos com complexidade linear. Observe que não estamos falando de várias coisas que podem variar aqui: velocidade da Internet, poder computacional do nosso computador e assim por diante. Ao avaliar a complexidade de um algoritmo, simplesmente não faz sentido considerar esses fatores — de qualquer forma, eles estão fora de nosso controle. A notação Big O expressa a complexidade do próprio algoritmo, não o "ambiente" em que ele é executado. Vamos continuar com o nosso exemplo. Suponha que acabemos descobrindo que precisamos enviar arquivos totalizando 800 terabytes. Podemos, é claro, realizar nossa tarefa enviando-os pela Internet. Há apenas um problema: em taxas de transmissão de dados domésticas padrão (100 megabits por segundo), levará aproximadamente 708 dias. Quase 2 anos! :O Nosso algoritmo obviamente não é um bom ajuste aqui. Precisamos de outra solução! Inesperadamente, a gigante de TI Amazon vem em nosso socorro! O serviço Snowmobile da Amazon nos permite carregar uma grande quantidade de dados para armazenamento móvel e depois entregá-los no endereço desejado de caminhão! Complexidade algorítmica - 3Então, temos um novo algoritmo! "Se você deseja transferir informações na forma de arquivos a uma distância de 5.000 milhas e isso levaria mais de 14 dias para ser enviado pela Internet, você deve enviar os dados em um caminhão da Amazon." Escolhemos 14 dias arbitrariamente aqui. Digamos que este seja o período mais longo que podemos esperar. Vamos analisar nosso algoritmo. E a velocidade dele? Mesmo que o caminhão viaje a apenas 50 mph, cobrirá 5.000 milhas em apenas 100 horas. Isso é um pouco mais de quatro dias! Isso é muito melhor do que a opção de enviar os dados pela Internet. E quanto à complexidade desse algoritmo? Também é linear, ou seja, O(n)? Não não é. Afinal de contas, o caminhão não se importa com o peso que você carrega - ele ainda irá dirigir na mesma velocidade e chegar na hora certa. Quer tenhamos 800 terabytes ou 10 vezes isso, o caminhão ainda chegará ao seu destino em 5 dias. Em outras palavras, o algoritmo de transferência de dados baseado em caminhão tem complexidade constante. Aqui, "constante" significa que não depende do tamanho dos dados de entrada. Coloque um pen drive de 1 GB no caminhão, ele chegará em 5 dias. Coloque em discos contendo 800 terabytes de dados, chegará em 5 dias. Ao usar a notação O grande, a complexidade constante é denotada por O(1) . Nós nos familiarizamos com O(n) e O(1) , então agora vamos ver mais exemplos no mundo da programação :) Suponha que você receba uma matriz de 100 números e a tarefa seja exibir cada um deles em o console. Você escreve um forloop comum que executa esta tarefa

int[] numbers = new int[100];
// ...fill the array with numbers

for (int i: numbers) {
   System.out.println(i);
}
Qual é a complexidade desse algoritmo? Linear, ou seja, O(n). O número de ações que o programa deve executar depende de quantos números são passados ​​para ele. Se houver 100 números na matriz, haverá 100 ações (declarações para exibir strings na tela). Se houver 10.000 números na matriz, 10.000 ações deverão ser executadas. Nosso algoritmo pode ser melhorado de alguma forma? Não. Não importa o que aconteça, teremos que fazer N passagens pelo array e executar N instruções para exibir strings no console. Considere outro exemplo.

public static void main(String[] args) {

   LinkedList<Integer> numbers = new LinkedList<>();
   numbers.add(0, 20202);
   numbers.add(0, 123);
   numbers.add(0, 8283);
}
Temos um vazio LinkedListno qual inserimos vários números. Precisamos avaliar a complexidade algorítmica de inserir um único número em LinkedListnosso exemplo e como isso depende do número de elementos na lista. A resposta é O(1), ou seja, complexidade constante . Por que? Observe que inserimos cada número no início da lista. Além disso, você deve se lembrar que quando você insere um número em um LinkedList, os elementos não se movem para lugar algum. Os links (ou referências) estão atualizados (se você esqueceu como funciona o LinkedList, veja uma de nossas aulas antigas ). Se o primeiro número da nossa lista for x, e inserirmos o número y no início da lista, tudo o que precisamos fazer é o seguinte:

x.previous  = y;
y.previous = null;
y.next = x;
Quando atualizamos os links, não nos importamos quantos números já estão noLinkedList , se um ou um bilhão. A complexidade do algoritmo é constante, ou seja, O(1).

Complexidade logarítmica

Não entrar em pânico! :) Se a palavra "logarítmico" fizer você querer fechar esta lição e parar de ler, espere alguns minutos. Não haverá nenhuma matemática maluca aqui (há muitas explicações como essa em outros lugares), e vamos separar cada exemplo. Imagine que sua tarefa seja encontrar um número específico em uma matriz de 100 números. Mais precisamente, você precisa verificar se está lá ou não. Assim que o número necessário for encontrado, a pesquisa será encerrada e você exibirá o seguinte no console: "O número necessário foi encontrado! Seu índice na matriz = ...." Como você realizaria essa tarefa? Aqui a solução é óbvia: você precisa iterar os elementos do array um a um, começando do primeiro (ou do último) e verificar se o número atual corresponde ao que você está procurando. De acordo, o número de ações depende diretamente do número de elementos no array. Se tivermos 100 números, poderemos precisar ir para o próximo elemento 100 vezes e realizar 100 comparações. Se houver 1.000 números, poderá haver 1.000 comparações. Esta é obviamente uma complexidade linear, ou seja,O(n) . E agora adicionaremos um refinamento ao nosso exemplo: a matriz onde você precisa encontrar o número é classificada em ordem crescente . Isso muda alguma coisa em relação à nossa tarefa? Ainda poderíamos realizar uma busca de força bruta pelo número desejado. Mas, alternativamente, poderíamos usar o conhecido algoritmo de busca binária . Complexidade algorítmica - 5Na linha superior da imagem, vemos uma matriz classificada. Precisamos encontrar o número 23 nele. Em vez de iterar sobre os números, simplesmente dividimos o array em 2 partes e verificamos o número do meio no array. Encontre o número que está localizado na célula 4 e verifique-o (segunda linha da imagem). Esse número é 16 e estamos procurando 23. O número atual é menor do que estamos procurando. O que isso significa? Significa quetodos os números anteriores (aqueles localizados antes do número 16) não precisam ser verificados : eles certamente serão menores que o que estamos procurando, porque nossa matriz está classificada! Continuamos a busca entre os 5 elementos restantes. Observação:fizemos apenas uma comparação, mas já eliminamos metade das opções possíveis. Temos apenas 5 elementos restantes. Repetiremos nossa etapa anterior dividindo novamente o subarray restante ao meio e novamente pegando o elemento do meio (a 3ª linha da imagem). O número é 56 e é maior do que o que estamos procurando. O que isso significa? Isso significa que eliminamos outras 3 possibilidades: o próprio número 56, bem como os dois números depois dele (já que eles são garantidos como maiores que 23, porque a matriz é classificada). Temos apenas 2 números restantes para verificar (a última linha da imagem) — os números com índices de matriz 5 e 6. Verificamos o primeiro deles e encontramos o que procurávamos — o número 23! Seu índice é 5! Vejamos os resultados do nosso algoritmo e, em seguida, analisaremos sua complexidade. A propósito, agora você entende por que isso é chamado de pesquisa binária: ela se baseia na divisão repetida dos dados pela metade. O resultado é impressionante! Se usássemos uma busca linear para buscar o número, precisaríamos de até 10 comparações, mas com uma busca binária, realizamos a tarefa com apenas 3! No pior caso, haveria 4 comparações (se no último passo o número que queríamos fosse a segunda das possibilidades restantes, ao invés da primeira. E a sua complexidade? Este é um ponto muito interessante :) O algoritmo de busca binária é muito menos dependente do número de elementos na matriz do que o algoritmo de busca linear (isto é, iteração simples). Com 10 elementos na matriz, uma pesquisa linear precisará de no máximo 10 comparações, mas uma pesquisa binária precisará de no máximo 4 comparações. Essa é uma diferença de um fator de 2,5. Mas para uma matriz de 1.000 elementos , uma pesquisa linear precisará de até 1.000 comparações, mas uma pesquisa binária exigiria apenas 10 ! A diferença agora é 100 vezes! Observação:o número de elementos na matriz aumentou 100 vezes (de 10 para 1.000), mas o número de comparações necessárias para uma pesquisa binária aumentou em um fator de apenas 2,5 (de 4 para 10). Se chegarmos a 10.000 elementos , a diferença será ainda mais impressionante: 10.000 comparações para busca linear e um total de 14 comparações para busca binária. E, novamente, se o número de elementos aumentar 1.000 vezes (de 10 para 10.000), o número de comparações aumentará em um fator de apenas 3,5 (de 4 para 14). A complexidade do algoritmo de busca binária é logarítmica ou, se usarmos a notação O grande, O(log n). Por que isso é chamado assim? O logaritmo é como o oposto da exponenciação. O logaritmo binário é a potência à qual o número 2 deve ser elevado para obter um número. Por exemplo, temos 10.000 elementos que precisamos pesquisar usando o algoritmo de pesquisa binária. Complexidade algorítmica - 6Atualmente, você pode consultar a tabela de valores para saber que isso exigirá no máximo 14 comparações. Mas e se ninguém tiver fornecido essa tabela e você precisar calcular o número máximo exato de comparações? Você só precisa responder a uma pergunta simples: a que potência você precisa elevar o número 2 para que o resultado seja maior ou igual ao número de elementos a serem verificados? Para 10.000, é a 14ª potência. 2 elevado a 13 é muito pequeno (8192), mas 2 elevado a 14 = 16384, e esse número satisfaz nossa condição (é maior ou igual ao número de elementos no array). Encontramos o logaritmo: 14. Isso é quantas comparações podemos precisar! :) Algoritmos e complexidade algorítmica é um tópico muito amplo para caber em uma lição. Mas saber disso é muito importante: muitas entrevistas de emprego envolverão questões envolvendo algoritmos. Para teoria, posso recomendar alguns livros para você. Você pode começar com " Algoritmos de Grokking ". Os exemplos neste livro são escritos em Python, mas o livro usa linguagem e exemplos muito simples. É a melhor opção para iniciantes e, além do mais, não é muito grande. Entre leituras mais sérias, temos livros de Robert Lafore e Robert Sedgewick. Ambos são escritos em Java, o que tornará o aprendizado um pouco mais fácil para você. Afinal, você está bastante familiarizado com esse idioma! :) Para alunos com boas habilidades matemáticas, a melhor opção é o livro de Thomas Cormen . Mas a teoria sozinha não vai encher sua barriga! Conhecimento!= Habilidade . Você pode praticar a resolução de problemas envolvendo algoritmos no HackerRank e no LeetCode . As tarefas desses sites são frequentemente usadas até mesmo durante entrevistas no Google e no Facebook, então você definitivamente não ficará entediado :) Para reforçar o material desta lição, recomendo que você assista a este excelente vídeo sobre a notação O grande no YouTube. Nos vemos nas próximas aulas! :)
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION