Two Pointers — Uma Técnica para Resolver Problemas com Arrays
Two Pointers (dois ponteiros) é uma das técnicas mais importantes para resolver problemas envolvendo arrays e strings.
A ideia é simples:
Utilizar duas variáveis para acompanhar diferentes posições dentro de uma estrutura de dados.
Em vez de comparar todos os elementos entre si, podemos utilizar dois ponteiros para percorrer os dados de forma inteligente.
Essa técnica aparece frequentemente em problemas do LeetCode e é muito utilizada para resolver problemas com complexidade:
O(n)
em vez de:
O(n²)
1. O conceito básico
Imagine o seguinte array:
[1, 2, 3, 4, 5]
Podemos ter dois ponteiros:
left
↓
[1, 2, 3, 4, 5]
↑
right
Inicialmente:
left = 0
right = 4
Ou seja:
left → primeiro elemento
right → último elemento
Podemos movimentar os ponteiros:
left++
ou:
right--
Dependendo do problema.
2. O exemplo clássico: Two Sum II
Um problema muito conhecido do LeetCode é o:
Two Sum II — Input Array Is Sorted
Temos um array ordenado:
numbers = [2, 7, 11, 15]
target = 9
Precisamos encontrar dois números cuja soma seja 9.
Sabemos que:
2 + 7 = 9
Então o resultado é:
[1, 2]
Nesse problema, os índices começam em 1.
3. A solução com Two Pointers
Como o array está ordenado:
[2, 7, 11, 15]
↑ ↑
left right
Começamos com:
left = 0
right = 3
Pegamos:
2 + 15 = 17
Como:
17 > 9
Precisamos diminuir a soma.
Como o array está ordenado, movemos:
right--
Agora:
[2, 7, 11, 15]
↑ ↑
left right
Temos:
2 + 11 = 13
Ainda é maior:
13 > 9
Movemos novamente:
right--
Agora:
[2, 7, 11, 15]
↑ ↑
left right
Temos:
2 + 7 = 9
Encontramos a resposta.
4. Implementação em TypeScript
function twoSum(
numbers: number[],
target: number
): number[] {
let left = 0;
let right = numbers.length - 1;
while (left < right) {
const sum = numbers[left] + numbers[right];
if (sum === target) {
return [left + 1, right + 1];
}
if (sum > target) {
right--;
} else {
left++;
}
}
return [];
}
Uso:
const result = twoSum(
[2, 7, 11, 15],
9
);
console.log(result);
Resultado:
[1, 2]
5. Por que funciona?
A chave está no fato de que o array está ordenado.
Temos:
[2, 7, 11, 15]
↑ ↑
left right
Se:
2 + 15 > target
sabemos que precisamos diminuir a soma.
Como o array está ordenado, podemos mover:
right--
Se:
2 + 7 < target
precisamos aumentar a soma.
Movemos:
left++
A lógica é:
Soma muito grande
↓
right--
Soma muito pequena
↓
left++
Soma correta
↓
Encontramos
6. Complexidade
A solução percorre o array apenas uma vez.
Cada ponteiro pode avançar, mas nunca volta.
Portanto:
Time Complexity: O(n)
E não precisamos de uma estrutura auxiliar:
Space Complexity: O(1)
Isso é melhor do que a solução de força bruta:
O(n²)
e também não utiliza a memória adicional:
O(n)
da solução com HashMap.
7. Two Pointers em strings
A técnica também funciona muito bem com strings.
Um exemplo clássico é verificar se uma string é um palíndromo.
Um palíndromo é uma palavra que permanece igual quando lida de trás para frente.
Por exemplo:
radar
Temos:
r a d a r
↑ ↑
L R
Comparamos:
r === r
Movemos:
↑ ↑
L R
Comparamos:
a === a
Movemos novamente:
↑
d
Chegamos ao centro.
É um palíndromo.
8. Implementando um Palíndromo
function isPalindrome(s: string): boolean {
let left = 0;
let right = s.length - 1;
while (left < right) {
if (s[left] !== s[right]) {
return false;
}
left++;
right--;
}
return true;
}
Podemos testar:
console.log(isPalindrome("radar"));
Resultado:
true
Outro exemplo:
console.log(isPalindrome("hello"));
Resultado:
false
9. Visualizando o algoritmo
Para:
radar
Começamos:
r a d a r
↑ ↑
L R
Comparamos:
r === r
Movemos:
↑ ↑
L R
Comparamos:
a === a
Movemos:
↑
L/R
Chegamos ao centro.
Resultado:
true
10. Outro exemplo: Reverse String
Também podemos utilizar Two Pointers para inverter uma string ou array.
Temos:
["h", "e", "l", "l", "o"]
Começamos:
["h", "e", "l", "l", "o"]
↑ ↑
left right
Trocamos:
h ↔ o
Resultado:
["o", "e", "l", "l", "h"]
Movemos os ponteiros:
["o", "e", "l", "l", "h"]
↑ ↑
left right
Trocamos:
e ↔ l
Resultado:
["o", "l", "l", "e", "h"]
Agora terminamos.
11. Implementação
function reverseString(chars: string[]): void {
let left = 0;
let right = chars.length - 1;
while (left < right) {
[chars[left], chars[right]] = [
chars[right],
chars[left]
];
left++;
right--;
}
}
Uso:
const chars = ["h", "e", "l", "l", "o"];
reverseString(chars);
console.log(chars);
Resultado:
["o", "l", "l", "e", "h"]
A vantagem é que fazemos a operação in-place, sem precisar criar outro array.
Complexidade:
Time: O(n)
Space: O(1)
12. Two Pointers não precisa ser Left e Right
É comum pensar que Two Pointers sempre significa:
left →
← right
Mas não necessariamente.
Podemos ter dois ponteiros que percorrem a estrutura na mesma direção.
Por exemplo:
slow
↓
[1, 2, 3, 4, 5]
↑
fast
O slow avança mais devagar.
O fast avança mais rapidamente.
Esse padrão é conhecido como:
Slow and Fast Pointers
13. Exemplo: remover duplicatas
Imagine um array ordenado:
[1, 1, 2, 2, 3]
Queremos remover as duplicatas.
Podemos usar dois ponteiros:
slow
↓
[1, 1, 2, 2, 3]
↑
fast
O fast percorre todos os elementos.
O slow representa a posição onde devemos colocar o próximo valor único.
Implementação:
function removeDuplicates(
numbers: number[]
): number {
if (numbers.length === 0) {
return 0;
}
let slow = 0;
for (
let fast = 1;
fast < numbers.length;
fast++
) {
if (numbers[fast] !== numbers[slow]) {
slow++;
numbers[slow] = numbers[fast];
}
}
return slow + 1;
}
Depois:
[1, 2, 3, 2, 3]
Os primeiros elementos representam:
[1, 2, 3]
O tamanho válido é:
3
14. Two Pointers e Arrays Ordenados
Um dos principais sinais de que podemos utilizar Two Pointers é quando o problema envolve:
Um array ordenado.
Por exemplo:
[1, 2, 3, 5, 7, 9]
Podemos começar:
left → 1
right → 9
E usar a ordenação para decidir qual ponteiro mover.
Se a soma for muito alta:
right--
Se for muito baixa:
left++
Essa propriedade permite eliminar rapidamente grandes partes do espaço de busca.
15. Exemplo prático: encontrar dois preços
Imagine um e-commerce com produtos ordenados por preço:
[10, 20, 30, 40, 50, 60]
Queremos encontrar dois produtos cujo preço combinado seja:
70
Começamos:
10 + 60 = 70
Encontramos imediatamente.
Agora imagine:
target = 80
Começamos:
10 + 60 = 70
É menor que 80.
Movemos:
left++
Agora:
20 + 60 = 80
Encontramos.
O algoritmo poderia ser:
function findProducts(
prices: number[],
target: number
): [number, number] | null {
let left = 0;
let right = prices.length - 1;
while (left < right) {
const total = prices[left] + prices[right];
if (total === target) {
return [
prices[left],
prices[right]
];
}
if (total < target) {
left++;
} else {
right--;
}
}
return null;
}
Uso:
const result = findProducts(
[10, 20, 30, 40, 50, 60],
80
);
console.log(result);
Resultado:
[20, 60]
16. Quando pensar em Two Pointers?
Existem alguns sinais comuns.
1. O array está ordenado
[1, 2, 3, 4, 5]
Pode ser possível usar:
left
right
2. Você precisa encontrar um par
Por exemplo:
a + b = target
ou:
a + b < target
ou:
a + b > target
3. Você precisa comparar extremos
Por exemplo:
Primeiro ↔ Último
Isso aparece em:
- Palíndromos
- Strings
- Arrays
- Reverse
- Comparações
4. Você precisa percorrer duas partes simultaneamente
Por exemplo:
slow
fast
Esse padrão aparece em:
- Remoção de duplicatas
- Linked Lists
- Detecção de ciclos
- Encontrar o meio de uma lista
17. Two Pointers vs HashMap
No problema Two Sum, podemos ter diferentes soluções.
HashMap
Funciona mesmo quando o array não está ordenado:
[3, 2, 4]
Complexidade:
O(n)
Memória:
O(n)
Two Pointers
Normalmente exige que os dados estejam ordenados:
[2, 3, 4]
Complexidade:
O(n)
Memória:
O(1)
Mas existe um detalhe.
Se o array original não estiver ordenado, precisamos ordená-lo.
Isso pode custar:
O(n log n)
Portanto, a escolha depende do problema.
18. A grande ideia
O principal benefício do Two Pointers é evitar comparações desnecessárias.
Uma abordagem de força bruta poderia fazer:
Elemento 1
↓
Comparar com todos
Elemento 2
↓
Comparar com todos
Elemento 3
↓
Comparar com todos
Complexidade:
O(n²)
Com Two Pointers:
left →
← right
Os ponteiros se aproximam.
Cada elemento é visitado no máximo algumas vezes.
Resultado:
O(n)
Conclusão
Two Pointers é uma técnica utilizada para resolver problemas percorrendo uma estrutura de dados com dois índices ou referências.
O padrão mais conhecido é:
left → ← right
E funciona especialmente bem quando:
- O array está ordenado.
- Precisamos encontrar pares.
- Precisamos comparar extremos.
- Podemos decidir qual ponteiro mover com base no resultado atual.
Os exemplos mais comuns incluem:
Two Sum II
Valid Palindrome
Reverse String
Remove Duplicates
A ideia pode ser resumida em:
Problema
│
▼
Usar dois ponteiros
│
┌──────────┴──────────┐
▼ ▼
left/right slow/fast
│ │
▼ ▼
Extremos do array Velocidades
A principal lição é reconhecer quando o problema permite eliminar partes da busca sem precisar testá-las individualmente.
Se você consegue determinar, com base no estado atual, qual dos dois ponteiros deve avançar, existe uma boa chance de que Two Pointers seja a solução.