1

Aprender DSA é chato? Vamos usar o DSA View View 👀👀 (Two Sum, Binary Search e Bubble Sort)

Oi oi!

Eu sou @nyaomaru, engenheiro frontend que não gosta muito de lugares cheios, então estou planejando tirar umas férias tranquilas em setembro. 🏝️

Você já usou o DSA View View? 👀👀

https://dev.to/nyaomaru/i-built-a-tool-to-visualize-dsa-lets-learn-together-dsa-view-view--djo

O DSA View View permite entender DSA visualizando como sua implementação realmente roda.

Mas só apresentar a ferramenta não é suficiente.

Ela realmente ajuda a entender DSA?

Vamos testar!

Neste artigo, vamos passar por três problemas clássicos:

  • Two Sum
  • Binary Search
  • Bubble Sort

Primeiro vamos entender o algoritmo e, depois, ver o que realmente acontece com o DSA View View.

Eu também ainda estou aprendendo DSA, então vamos aprender juntos! 😸


🗺️ Two Sum

Vamos começar com um problema bem famoso.

Dado um array de números e um valor target, precisamos encontrar os índices de dois números cuja soma seja igual ao target.

Por exemplo:

nums = [2, 7, 11, 15];
target = 9;

A resposta é:

[0, 1];

Porque:

2 + 7 = 9

Simples!

Então, como podemos encontrar esses dois números? 🤔

Brute Force

A abordagem mais simples provavelmente é verificar todos os pares possíveis.

function twoSum(nums: number[], target: number): number[] {
  for (let i = 0; i < nums.length; i++) {
    for (let j = i + 1; j < nums.length; j++) {
      if (nums[i] + nums[j] === target) {
        return [i, j];
      }
    }
  }

  return [];
}

Isso funciona.

Mas, se o array ficar grande, talvez seja necessário comparar muitos pares, certo?

A complexidade de tempo é O(n²).

Será que podemos evitar verificar os mesmos valores várias vezes?

Sim.

Vamos usar um Map.

function twoSum(nums: number[], target: number): number[] {
  const seen = new Map<number, number>();

  for (let i = 0; i < nums.length; i++) {
    const current = nums[i];
    const need = target - current;

    if (seen.has(need)) {
      return [seen.get(need)!, i];
    }

    seen.set(current, i);
  }

  return [];
}

A parte importante é esta 👇

const need = target - current;

Em vez de perguntar:

Quais dois números eu preciso combinar?

perguntamos:

Qual número eu preciso para completar o target?

Vamos acompanhar o exemplo.

No começo:

current = 2
target = 9

need = 9 - 2
     = 7

Já vimos 7?

Não.

Então guardamos 2.

seen = {
  2 → 0
}

Depois:

current = 7
target = 9

need = 9 - 7
     = 2

Já vimos 2?

Sim! 👀👀

seen = {
  2 → 0
}

Então:

return [0, 1];

Pronto!

Como precisamos percorrer o array apenas uma vez:

Time:  O(n)
Space: O(n)

👀 Vamos visualizar

A implementação é pequena.

Mas, quando eu estava aprendendo esse pattern, esta parte ainda parecia um pouco mágica:

if (seen.has(need))
  • De onde veio need?
  • O que existe dentro de seen neste momento?
  • Por que verificar os valores anteriores resolve o problema?

É exatamente aqui que a visualização ajuda.

Two Sum no DSA View View

https://dsa-view-view.vercel.app/#s=j.eyJlIjoidHdvLXN1bSIsImwiOiJ0eXBlc2NyaXB0IiwibSI6InZlcmlmaWNhdGlvbiIsInYiOjF9

Com o DSA View View, podemos avançar pelo runtime um step de cada vez e observar como os valores mudam.

2
↓
Need 7
↓
Remember 2
↓
7
↓
Need 2
↓
Found 2!
🎉

Agora o Map não parece mais algum truque misterioso.

Podemos realmente acompanhar a ideia:

Lembre o que já vimos e verifique se o valor de que precisamos está lá.

Nice! 😸


Agora vamos para Binary Search.

Imagine que temos este array ordenado:

[1, 3, 5, 7, 9, 11, 13]

E queremos encontrar:

11

Claro, poderíamos começar em 1 e verificar cada número.

1 → 3 → 5 → 7 → 9 → 11

Isso funciona.

Mas Binary Search faz algo mais esperto.

Em vez de verificar desde o começo, ele olha para o meio.

[1, 3, 5, 7, 9, 11, 13]
          ↑
         mid

O valor do meio é 7.

Estamos procurando 11.

11 > 7

Como o array está ordenado, já sabemos algo muito útil.

Tudo à esquerda de 7 também é menor que 11.

Então não precisamos mais dessa metade. 👋

[1, 3, 5, 7, 9, 11, 13]
             └───────┘
               search

Agora verificamos o meio do range restante.

[9, 11, 13]
     ↑
    mid

E:

11 === 11

Encontramos! 🎉

Aqui está a implementação:

function binarySearch(nums: number[], target: number): number {
  let left = 0;
  let right = nums.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (nums[mid] === target) {
      return mid;
    }

    if (nums[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return -1;
}

Temos três variáveis importantes:

left
right
mid

Elas representam o search range atual.

No nosso exemplo, começam assim:

left = 0
right = 6
mid = 3

[1, 3, 5, 7, 9, 11, 13]
 ↑        ↑          ↑
left     mid       right

Como:

nums[mid] < target;

movemos left.

left = mid + 1;

Agora:

[1, 3, 5, 7, 9, 11, 13]
             ↑   ↑   ↑
           left mid right

E encontramos 11.

Por que Binary Search é rápido?

Essa é a parte interessante.

A cada step, removemos aproximadamente metade dos candidatos restantes.

Se houver 1.000 valores, não precisamos necessariamente fazer 1.000 verificações.

Fica aproximadamente assim:

1000
↓
500
↓
250
↓
125
↓
...

Por isso Binary Search possui:

Time:  O(log n)
Space: O(1)

Mas existe uma condição muito importante.

Os dados precisam estar ordenados.

Sem dados ordenados, não podemos descartar metade do search range com segurança.

👀 Vamos visualizar

Binary Search é um dos algoritmos que me fizeram querer uma ferramenta de visualização desde o começo.

O code em si é curto:

left = mid + 1;

ou:

right = mid - 1;

Easy.

Mas, enquanto eu aprendia, às vezes pensava:

Espera... qual parte estamos procurando agora? 😿

Binary Search no DSA View View

https://dsa-view-view.vercel.app/#s=j.eyJlIjoiYmluYXJ5LXNlYXJjaCIsImwiOiJ0eXBlc2NyaXB0IiwibSI6InZlcmlmaWNhdGlvbiIsInYiOjF9

Quando visualizamos left, mid e right, a ideia fica muito mais fácil de acompanhar.

Não estamos alterando três números aleatoriamente.

Estamos diminuindo continuamente o search range.

███████████████

       ↓

        ███████

       ↓

          ███

       ↓

           █

Isso é Binary Search!

Corte a metade desnecessária.

Depois corte novamente.

E novamente.

E novamente.

Até encontrar a resposta. ✂️😸


🫧 Bubble Sort

Por fim, vamos ordenar alguma coisa!

Considere este array:

[5, 1, 4, 2, 8];

Queremos:

[1, 2, 4, 5, 8];

Bubble Sort compara repetidamente dois valores vizinhos.

Se eles estiverem na ordem errada, fazemos swap.

Vamos olhar o começo.

[5, 1, 4, 2, 8]
 ↑  ↑

Comparamos:

5 > 1

Então fazemos swap.

[1, 5, 4, 2, 8]

Próximo:

[1, 5, 4, 2, 8]
    ↑  ↑

De novo:

5 > 4

Swap!

[1, 4, 5, 2, 8]

E continuamos.

[1, 4, 5, 2, 8]
       ↑  ↑

5 > 2

Swap!

[1, 4, 2, 5, 8]

Eventualmente, os valores maiores vão se movendo para o final do array.

Eles meio que...

sobem como bolhas. 🫧

É por isso que se chama Bubble Sort.

Aqui está uma implementação simples:

function bubbleSort(nums: number[]): number[] {
  for (let i = 0; i < nums.length - 1; i++) {
    for (let j = 0; j < nums.length - i - 1; j++) {
      if (nums[j] > nums[j + 1]) {
        [nums[j], nums[j + 1]] = [nums[j + 1], nums[j]];
      }
    }
  }

  return nums;
}

Comparamos repetidamente:

nums[j];

e:

nums[j + 1];

e fazemos swap quando necessário.

Depois de um pass completo, o maior valor restante chega à posição correta perto do final.

Então, no próximo pass, não precisamos verificar essa posição novamente.

É por isso que o inner loop contém:

nums.length - i - 1;

Complexidade

Bubble Sort não é muito rápido para arrays grandes.

Sua complexidade é:

Time:  O(n²)
Space: O(1)

Então provavelmente não vou começar a substituir o sorting de production por Bubble Sort amanhã. 😸

Mas, como exemplo de aprendizado, eu gosto muito dele.

Por quê?

Porque podemos ver o algoritmo funcionando.

👀 Vamos visualizar

Esse provavelmente é o mais satisfatório visualmente dos três.

Bubble Sort no DSA View View

https://dsa-view-view.vercel.app/#s=j.eyJlIjoiYnViYmxlLXNvcnQiLCJsIjoidHlwZXNjcmlwdCIsIm0iOiJ2ZXJpZmljYXRpb24iLCJ2IjoxfQ

Em vez de apenas ler:

[nums[j], nums[j + 1]] = [nums[j + 1], nums[j]];

podemos acompanhar os valores se movendo pelo array.

[5, 1, 4, 2, 8]

 ↓ swap

[1, 5, 4, 2, 8]

    ↓ swap

[1, 4, 5, 2, 8]

       ↓ swap

[1, 4, 2, 5, 8]

Depois começa outro pass.

O code contém nested loops, indexes, comparisons e swaps.

Mas, visualmente, a regra básica é extremamente simples:

Compare os vizinhos. Se o valor da esquerda for maior, faça swap.

Repita.

Repita.

Repita.

Sorted! 🎉


🧠 O que realmente aprendemos?

Esses três problemas parecem bem diferentes.

Mas cada um apresenta uma forma útil de pensar.

Two Sum

Guarde informações dos steps anteriores.

Já vi o valor de que preciso?

Use o que já sabemos para remover candidatos impossíveis.

Posso descartar com segurança metade do search space?

Bubble Sort

Quebre um problema maior em várias comparações pequenas.

Esses dois valores estão na ordem correta?

Essa é uma das coisas que acho interessantes em aprender DSA.

No começo, a implementação pode parecer uma coleção de indexes, loops, conditions e variáveis misteriosas.

Mas, por trás do code, geralmente existe uma ideia muito mais simples.

E às vezes eu não entendo completamente essa ideia apenas olhando para o code.

Eu quero ver. 👀👀


🎯 Conclusão

Neste artigo, vimos três algoritmos clássicos:

  • Two Sum com Map
  • Binary Search
  • Bubble Sort

E, mais importante, vimos como os dados mudam durante a execução.

Acho que é justamente aqui que a visualização pode ser especialmente útil.

  • Ler a implementação final nos diz o que o code é.
  • Percorrer a execução step-by-step ajuda a entender por que ele funciona.

Foi exatamente por isso que criei o DSA View View.

https://dsa-view-view.vercel.app

Você pode escrever ou carregar uma implementação em TypeScript, executá-la com seus próprios inputs e avançar ou voltar pelo runtime.

Se você também está aprendendo DSA, tente pegar um problema que já resolveu e visualizá-lo step-by-step.

Talvez você perceba algo que não tinha percebido apenas lendo o code. 👀

E, se houver algum problema de DSA que você gostaria que eu abordasse no próximo artigo, me conta nos comentários!

Eu ainda tenho muitos algoritmos para aprender também. 😸

Vamos treinar nossos músculos de DSA juntos! 💪

Se você gostar do DSA View View, deixe uma star ⭐

https://github.com/nyaomaru/dsa-view-view

Até o próximo artigo!

Carregando publicação patrocinada...