@ -28,7 +28,7 @@ Conceitualmente, seu funcionamento se dá da seguinte maneira:
3. resta apenas a lista original ordenada. A partir de então, para encontrar o $i$-ésimo menor elemento, basta referenciar o arranjo pelo índice $i$: `A[i]`.
Podemos aferir que o algoritmo acima é correto (isto é, adequado a produzir a solução) pois
1. Nas linhas 19 e 20 subdivide-se a sequência em pares recursivamente, procedimento este que se repetirá até que restem apenas subconjuntos de 1 elemento e a função retorne na linha 17.
1. Nas linhas 22 e 23 subdivide-se a sequência em pares recursivamente, procedimento este que se repetirá até que restem apenas subconjuntos de 1 elemento e a função retorne na linha 20.
2. Cada recursão anterior então acessa a função `merge` na linha 21. Esta integrará pares de conjuntos adjacentes ao índice `pivot` nas linhas 7 e 8, percorrendo-os cada qual desde seus respectivos índices iniciais `i` e `j` e posicionando o elemento de menor valor no índice $k$. Isso, seguro de que estes já estarão ordenados em ordem crescente dada a condição anterior. Este então retornará os elementos de ambos os conjuntos em ordem crescente na linha 10.
2. Cada recursão anterior então acessa a função `merge` na linha 24. Esta integra pares de conjuntos adjacentes ao índice `pivot` nas linhas 8 à 10, percorrendo-os cada qual desde seus respectivos índices iniciais `i` e `j` e posicionando o elemento de menor valor no índice `k`. Isso, seguro de que estes já estarão ordenados em ordem crescente dada a condição anterior. Este então retornará os elementos de ambos os conjuntos em ordem crescente na linha 11 e 12.
3. O procedimento anterior se repete $(n - 1)$ vezes até que todos os pares ordenados combinados produzem a sequência original ordenada.
@ -82,7 +85,7 @@ A correção do algoritmo foi testada manualmente para pequenos arranjos utiliza
#### Tempo de Execução
É possível afirmar que o tempo de execução $T(n)$ do *Merge Sort* para quaisquer entradas de mesmo tamanho $n$ é assintoticamente equivalente pois independentemente dos valores contidos na entrada, as mesmas operações de comparação (na linha 7) e atribuição (na linha 10) são executadas. Isto é, diferentemente do que é visto noutros algoritmos de ordenação tais quais o *Quick Sort* ou o *Insertion Sort*. Para estimar este tempo de execução, podemos recorrer ao *Teorema Mestre para recorrências de Divisão e Conquista*, por este ser um algoritmo do tipo *Divisão e Conquista*. Segundo Márcio Ribeiro (2021) o teorema mestre pode ser descrito nos seguintes termos:
É possível afirmar que o tempo de execução $T(n)$ do *Merge Sort* para quaisquer entradas de mesmo tamanho $n$ é assintoticamente equivalente pois independentemente dos valores contidos na entrada, o mesmo número de operações de comparação (na linha 9) e atribuição (na linha 12) são executadas. Isto é, diferentemente do que é visto noutros algoritmos de ordenação tais quais o *Quick Sort* ou o *Insertion Sort*. Para estimar este tempo de execução, podemos recorrer ao *Teorema Mestre para recorrências de Divisão e Conquista*, por este ser um algoritmo do tipo *Divisão e Conquista*. Segundo Márcio Ribeiro (2021) o teorema mestre pode ser descrito nos seguintes termos:
> Sejam $a \ge 1$, $b > 1$ e $T (n) = aT \left( \frac nb \right) + f (n)$, então:
>
@ -92,7 +95,7 @@ A correção do algoritmo foi testada manualmente para pequenos arranjos utiliza
>
> 3. se $f(n) \in \Omega \left(n^{\log_b (a + \varepsilon)} \right)$ para algum $\varepsilon > 0$ e se $af \left(\frac nb \right) \le cf(n)$ para algum $c <1$etodo$n$suficientementegrandeentão$T(n) \inΘ(f(n))$
Temos que o tempo de execução $T(n)$ em função do tamanho $n$ da entrada do algoritmo *Merge Sort* é constante das linhas 14 à 18 mas varia nas linhas 19 e 20 ($T\left(\frac n2 \right)$ cada), e 21 ($cn$, para uma constante $c > 0$). Assim temos que a forma geral do tempo de execução é:
Temos que o tempo de execução $T(n)$ em função do tamanho $n$ da entrada do algoritmo *Merge Sort* é constante das linhas 17 à 21 mas varia nas linhas 22 e 23 ($T\left(\frac n2 \right)$ cada), e 24 ($cn$, para uma constante $c > 0$). Assim temos que a forma geral do tempo de execução é:
$$
T(n) = 2T\left(\frac n2 \right) + cn
@ -122,13 +125,7 @@ O algoritmo `quickSelect`, tal qual o algoritmo *Quick Sort*, foi desenvolvido p
Conceitualmente, seu funcionamento se dá da seguinte maneira:
- Particiona-se a sequência à partir do valor de um elemento qualquer contido nessa, separando-a em dois subconjuntos contendo respectivamente
- valores maiores ou iguais;
- e menores que o pivô.
Portanto, o pivô passa a ocupar uma posição intermediária às partições;
- Particiona-se a sequência à partir do valor de um elemento qualquer contido nessa denominado o pivô, separando-a em dois subconjuntos contendo respectivamente valores maiores e menores que este. Assim, o pivô passa a ocupar uma posição intermediária às partições e é considerado ordenado;
- Se o pivô assume a posição do índice buscado ou resta apenas um único índice no conjunto particionado, a busca se encerra; senão repete-se o procedimento na partição aquela que contiver o índice $i$ (sendo este menor ou maior que o pivô).
@ -175,9 +172,9 @@ Podemos aferir que o algoritmo acima é correto pois
1. Se a a entrada contiver um único elemento, este é o elemento devolvido — Este é o caso base.
2. Senão, o conjunto é particionado na linha 24 e comparações são feitas nas linhas 25 para aferir se o índice foi encontrado no pivô ou, senão, na linha 27 determina-se em qual partição o procedimento de busca deve ser repetido até que as condições anteriores sejam satisfeitas.
2. Senão, o conjunto é particionado na linha 25 e comparações são feitas na linha 26 para aferir se o índice foi encontrado no pivô ou, senão, na linha 27 para determinar em qual partição o procedimento de busca deve ser repetido até que as condições anteriores sejam satisfeitas.
3. Como toda partição é menor que o conjunto que lhe deu origem, no pior caso eventualmente a partição avaliada será pequena o suficiente para corresponder ao caso base.
3. Como toda partição é menor que o conjunto que lhe deu origem, no pior caso a partição avaliada eventualmente será pequena o suficiente para corresponder ao caso base.
##### Teste empírico
@ -195,9 +192,13 @@ O melhor caso para a execução deste algoritmo é aquele em que o $i$-ésimo me
O pior caso para a execução deste algoritmo seria aquele onde o usuário busca o menor valor de um arranjo que encontra-se perfeitamente em ordem crescente **à partir do segundo elemento**. Assim, o primeiro elemento tem o maior valor e cada processo de partição se dá de forma em que o pivô é o $(n - (i - 1))$ maior elemento, produzindo assim partições de tamanho menor que a anterior em apenas uma unidade. Assim o arranjo é percorrido de maneira a realizar um número de $S_n$ de comparações na linha 14 equivalente à:
Ou seja, o tempo de execução do algoritmo restaria em ordem quadrática: $T(n) \in \Theta(n^2)$.
@ -205,7 +206,7 @@ Ou seja, o tempo de execução do algoritmo restaria em ordem quadrática: $T(n)
Aplicando-se o Teorema Mestre conseguimos avaliar o **caso médio**, pois este admite que recursões são feitas em conjuntos menores de igual tamanho entre si. Fosse sempre este o caso com este algoritmo o índice $i$ ser menor ou maior que aquele do pivô não afetaria o tempo de execução da recursão seguinte, o qual também não seria nem muito pequeno (com o tamanho da partição seguinte abaixo da média) ou muito grande (com o tamanho da partição seguinte acima da média).
Temos que o tempo de execução $T(n)$ em função do tamanho $n$ da entrada do algoritmo *quickSelect* é constante nas linhas 20 à 23, 25 à 26 e 27 mas varia na linha 24 ($cn$, para uma constante $c > 0$) e nas linhas 28 e 29 ($T\left(\frac n2 \right)$ cada), das quais apenas uma delas é executada condicionalmente. Assim temos que a forma geral do tempo de execução é:
Temos que o tempo de execução $T(n)$ em função do tamanho $n$ da entrada do algoritmo *quickSelect* é constante senão nas linhas 25 ($cn$, para uma constante $c > 0$), 29 e 31 ($T\left(\frac n2 \right)$ cada), das quais apenas uma delas é executada condicionalmente. Assim temos que a forma geral do tempo de execução é:
$$
T(n) = T\left(\frac n2 \right) + cn
@ -219,7 +220,11 @@ Ou seja, conforme a definição $aT \left( \frac nb \right) + f (n)$ tem-se que
- $f(n) = cn$, sendo $\Theta(cn) \equiv \Theta(n)$;
- e portanto $f(n) \in \Omega \left( n^{\log_b(a+ \varepsilon)} \right)$ pois $\Omega \left(n^{\log_2(1 + 1)} \right) = \Omega(n)$.
Vemos então que teoricamente o caso médio aproxima-se mais da situação observada no melhor caso que do pior caso e, para valores de $n$ suficientemente grandes, oferece melhor desempenho com relação a solução `mergeSelect`.
<divstyle="page-break-before: always;"></div>
## Objetivo
Iremos comparar o desempenho de ambas as soluções para uma mesma entrada de tamanho $n$, para valores de $n$ cada vez maiores. Para tal utilizaremos o seguinte equipamento:
OpenGL: renderer: Mesa Intel HD Graphics 5500 (BDW GT2)
v: 4.6 Mesa 21.2.3
Network:
Device-1: Realtek RTL810xE PCI Express Fast Ethernet driver: r8169
Device-2: Intel Wireless 7265 driver: iwlwifi
Device-3: Intel Bluetooth wireless interface type: USB
driver: btusb
Drives:
Local Storage: total: 931.51 GiB used: 916.26 GiB (98.4%)
Info:
@ -270,29 +271,40 @@ Info:
used: 3.66 GiB (23.6%) Shell: fish inxi: 3.3.08
```
Os valores a serem avaliados[^6] serão sorteados fazendo uso do programa `gerador`[^7] e o índice a ser buscado será sorteado fazendo uso uso do comando `random`, função do shell `fish` na versão `3.3.1`. Este é um exemplo do cabeçalho de um arquivo de texto gerado desta forma:
> *Output* do comando `inxi -b`, algumas informações foram omitidas.
### Método
Os valores a serem avaliados foram sorteados fazendo uso do programa `gerador`[^6] e depositados em um arquivo. Então, utilizando o comando `random`, uma função do shell `fish` em sua versão 3.3.1, sorteou-se um índice a ser buscado, inserindo-o ao final do arquivo. Finalmente, tais aquivos foram providos enquanto argumentos para os programas de teste[^7] [^8], e o desempenho destes foi aferido fazendo uso do comando `time`, também função do shell `fish`. À seguir vemos os comandos passados e um exemplo de saída:
```shell
> head -4 tests/1.txt
10000 4430
1747153665
1888832918
1550537203
>
```
> for n in 1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384
wrn "Teste para |n = "(math "$n * 10000")"|"
./gerador.out $n > $n.txt
random 0 (math "$n * 10000") >> $n.txt
reg "|Merge Select|"
time mergeSelect/automatic_test.out < $n.txt
reg "|Quick Select|"
time quickSelect/automatic_test.out < $n.txt
rm $n.txt
end
O primeiro valor (`10000`) descreve o tamanho da entrada, o segundo (`4430`) o índice buscado, os valores restantes são uma amostra daqueles a serem avaliados. tais aquivos serão providos enquanto argumentos para os programas de teste[^8][^9], e o desempenho destes será avaliado fazendo uso do comando `time`, também função do shell `fish`, da seguinte maneira:
! Teste para n = 10000
Merge Select
O 331º elemento de menor valor: 70930890
```shell
> time mergeSelect/automatic_test.out <tests/1.txt
<imgtitle=""src="file:///home/user/Public/USP/Sistemas de Informação/2º semestre/Introdução à Análise de Algoritmos/EP 1/Imagens/53ccaa02d734f310cfe4fca904e3cfd3d1a5a053.jpg" alt=""width="681" data-align="center">
| Tamanho da entrada | Tempo de execução — *mergeSelect* | Tempo de execução — *quickSelect* |