Ordenação e Busca1
Ordenação
A maioria das linguagens de programação modernas implementa uma função de ordenação eficiente. Em C++, tem-se a função sort
da biblioteca <algorithm>
. Veja alguns exemplos de uso desta função:
- [2, 3, 3, 4, 5, 5, 8]
- [2, 3, 3, 4, 5, 5, 8]
- Por padrão, um
pair
sempre é ordenado pelo campofirst
. {123, Paulo} {456, Ana} {789, Paulo}
A ordenação padrão é a não-decrescente, mas pode-se obter a ordem inversa da seguinte forma:
{789, Paulo} {456, Ana} {123, Paulo}
Para ordernarmos um struct
ou class
ou outra coleção de objetos (por exemplo, um pair
pelo campo second
), tem-se duas alternativas: (\(i\)) definir uma função de comparação ou (\(ii\)) fazer a sobrecarga do operador <
.
Agora suponha que seja necessário ordenar objetos do tipo Pessoa
:
- Construtor padrão usado quando não passamos nenhum argumento para a classe. Sempre faça o construtor padrão. Caso contrário, não será possivel fazer, por exemplo:
Pessoa p; //Chama o construtor padrão
.
Para ordernarmos um struct
ou class
ou outra coleção de objetos (por exemplo, um pair
pelo campo second
), tem-se duas alternativas: (\(i\)) definir uma função de comparação ou (\(ii\)) fazer a sobrecarga do operador <
.
Usando uma função de comparação
A primeira alternativa é definir uma função que compara dois objetos e retorna true
caso o primeiro seja considerado menor que o segundo e false
, caso contrário. Por exemplo,
- O
const
diz ao compilador que os objetos passados para a função não serão alterados internamento. Já o&
representa passagem por referência, uma alternativa a passagem por ponteiro. Dessa forma, não passamos uma cópia dos objetos e sim referências (endereços) dos mesmos, o que torna o código mais eficiente. Por quê?
Com a função de comparação definida, basta passá-la como argumento na função sort
:
- Inclui o elemento no fim do
vector
. Leia mais sobrevector
aqui. - Usa a função
compara
para fazer a ordenação.
Também é possivel usar uma função lambda como função de comparação. Por exemplo, para ordenar um pair
pelo campo second
(ou usando-o como critério de desempate), pode-se fazer:
- Usando a função
compara
(-1, 10) (0, -5) (0, 0) (0, 1) (0, 2) (1, 2)
- Usando função lambda
Fazando a sobrecarga do operador <
Ao invés de ser difinidas funções de comparações, pode-se fazer a sobrecarga do operador <
(operator<
). Isso é comum ao se usar class
ou struct
. Veja como ficaria ao ser considerado a classe Pessoa
:
- Sobrecarga/definição do operador
<
para a classePessoa
. Dessa forma, é possivel fazer a comparaçãop1 < p2
, considerando quep1
ep2
são objetos do tipoPessoa
. - Usa o operador
<
para fazer a ordenação.
Complemente sua leitura e seu conhecimento
- Competitive Programmer’s Handbook
- Sorting (CS Academy)
- std::sort
- std::partial_sort
- std::stable_sort
- std::nth_element
Busca
Busca sequencial
A forma mais intuitiva para procurar um elemento em um array é usar um loop que percorre todos os elementos do array e parando assim que o elemento buscado é encontrado. No pior caso, é necessário percorrer todos os elementos, logo a complexidade será \(O(n)\). O código abaixo verifica se x
está no array:
Em C++, pode-se usar a função search
.
Busca binária
Se os elementos do array estiverem ordenados, pode-se usar uma estratégia diferente e mais eficiente para realizar a busca: verifique se o elemento do meio do array é o elementos buscado, se for, a busca termina. Caso não seja, verique se o elemento do meio é menor que elemento buscaso, se for, repita o processo considerando apenas a segunda metade do array. Senão, considere a primeira metade do array. Assim, a cada passo da busca, o tamanho do array é reduzido a metade, logo, a complexidade do algoritmo é \(O(\log n)\).
- Evite usar
meio = (ini + fim) / 2;
, já queini + fim
pode gerar integer overflow. x
não está no vetorv
.
Em C++, pode-se usar a função std::binary_search. As funções abaixo também são úteis e baseadas na busca binária:
- std::lower_bound: retorna um ponteiro para o primeiro elemento do array cujo valor é pelo menos
x
; - std::upper_bound: retorna um ponteiro para o primeiro elemento do array cujo valor é maior que
x
;
As funções assumem que o array está ordenado. Se o valor procurado não for encontrado, é retornado um ponteiro para o elemento após o último elemento do array. Por exemplo, o código a seguir verifica se o array contém um elemento com valor x
:
- Pode-se usar a função std::distance ao invés de
k - v.begin()
, ou seja,distance(v.begin(), k)
.
Complemente sua leitura e seu conhecimento:
- Binary Search tutorial (C++ and Python)
- Busca Binária
- Binary Search
- Binary Search (ITMO Academy) 🤯
- Binary Search (CS Academy)
Busca binária em funções monotônicas2
Considere uma função booleana \(f(x)\) e se deseja encontrar o valor máximo (ou mínimo) de \(x\) tal que \(f(x)\) seja true
. Da mesma forma que a busca binária só funciona se o array estiver ordenado, só é possivel aplicar a busca binária em uma função monótona, ou seja, é sempre não-decrescente ou sempre não-crescente.
Seja check(x)
uma função que verifica uma propriedade de x
. Se para todo x
, check(x) = true
implica check(x+1) = true
, ou para todo x
, check(x) = false
implica check(x+1) = false
, então a função check
é monótona.
Suponha a função check
abaixo que verifica se um elemento é maior ou igual a x
.. Se x = 11
e o vetor v = [1,2,3,5, 8, 11, 12, 14, 16]
, então teremos o seguinte vetor de saída ao aplicarmos check
em v
: [0,0,0,0,0,1,1,1,1,1]
.
Dessa forma, a função check
para essa situação é monótona e isso é relevante porque se um valor do vetor satisfizer a condição, todos os valores a direita também vão satisfazê-la, e de forma análoga, todos os valores a esquerda de um índice que não satisfaz a condição, também não vão satisfazer, e é isso que nos permite aplicar busca binária. Além disso, a função check
só se torna monótona nesse exemplo quando o vetor está ordenado, por isso a busca binária só é feita em vetores ordenados.
Como encontrar o menor valor que torna check
verdadeiro? R. inicia-se o processo "chutanto" um intervalo onde a resposta com certeza estará. Para cada intevalo, checa-se o meio e, dependendo da resposta, descarta-se os elementos a direita ou a esquerda, mas sempre divide-se o tamanho do intervalo por 2, até que o intervalo tenha tamanho 1. Veja uma solução:
Exemplo: Encontrar o maior valor de \(x \in [0, 10]\) tal que \(x^2 \leq 30\).
- Se
check(m)
étrue
, então todos os números menores quem
também serãotrue
. - Se
check(m)
éfalse
, então todos os números maiores quem
também serãofalse
.
Two-Pointers
Na técnica chamada Two-Pointers dois "aponstadores" caminham pelo vetor. Normalmente, esses apontadores são ``colocados'' nas extremidades opostas do vetor e caminham um em direção ao outro, como mostra a figura abaixo.
Você consegue pensar em como usar esse técnica para resolver o problema de inverter os elementos de um vetor sem usar um vetor auxiliar? A ideia é simples: coloque cada apontador (digamos i
e j
) em uma extremidade do vetor, ou seja, i = 0
e j = n - 1
, e, a cada iteração, troque os elementos que estão nas posições i
e j
(ou seja, v[i] <-> v[j]
). Após a troca, incremente o apontador i
e decremente o apontador j
. Repita esse processo enquanto i < j
. A figura abaixo ilustra parte desse processo.
O código abaixo ilustra essa estratégia (note a simplicidade):
- A std::reverse também pode ser usada com o mesmo objetivo. O intuito é mostrar com a técnica Two-Pointers funciona.
- std::swap
Complemente sua leitura e seu conhecimento: