Skip to content

Commit

Permalink
Refinamento da explicação do MergeSort
Browse files Browse the repository at this point in the history
[x] adicionei introdução, detalhando caso médio
[x] adicionei informações sobre a estratégia
[x] refinamento das ilustrações
[x] adicionei análise de complexidade
[x] refinamento do resumo final
  • Loading branch information
eduardolfalcao authored Oct 6, 2023
1 parent 037b9fe commit d388bb3
Showing 1 changed file with 66 additions and 27 deletions.
93 changes: 66 additions & 27 deletions conteudos/ordenacao/MergeSort.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
# MergeSort

## Introdução

Os algoritmos SelectionSort, BubbleSort e InsertionSort são O(n²) no caso médio.
Quando nos referimos ao caso médio, significa que a complexidade de tempo leva em consideração o comportamento médio/esperado do algoritmo quando ele executa sobre uma variedade de entradas.
Em contraste com a complexidade de tempo de pior caso, que descreve o desempenho do algoritmo na condição menos favorável.

Agora vamos estudar um algoritmo que é O(nlog(n)) no pior caso, ou seja, um algoritmo que no pior caso já é bem melhor do que o caso médio dos algoritmos Selection, Bubble e Insertion.

O algoritmo do MergeSort é classificado como um algoritmo de Divisão e Conquista.
A ideia é dividir recursivamente o array pela metade, até que ele seja indivisível.
E por fim, juntar cada pedaço do array de forma ordenada.
**Observação:** gravei um [vídeo](https://www.youtube.com/watch?v=BZjwEvzH_fQ) e disponibilizei no YouTube sobre o algoritmo MergeSort.

### Estratégia do algoritmo de acordo com seu nome

- Ordenação por mesclagem
- É classificado como um algoritmo de Divisão e Conquista.
- Divisão: dividimos recursivamente o array pela metade, até que ele seja indivisível
- Conquista: mesclamos (juntamos/combinamos) os arrays de forma ordenada

## Ilustração do funcionamento do algoritmo

Primeiro, vejamos a ilustração simplificada do MergeSort para o array: [9,4,3,6,3,2,5,7,1,8].
Na verdade, aqui vamos focar na operação de **merge** entre dois arrays ordenados.
Para simplificar a compreensão do algoritmo como um todo, começaremos a discussão pelo procedimento da conquista, consistindo na mesclagem ordenada dos arrays.
Note, no entanto, que a conquista é executada somente depois da divisão.
Em seguida discutiremos a divisão, a primeira etapa do algoritmo.

- Introdução:
### Conquista

Na verdade, aqui vamos discutir o funcionamento da operação de **merge** entre dois arrays ordenados.
Passos 0, 1, e 2 representam de forma simplificada a divisão e abstrai a forma como os arrays da esquerda e direita foram ordenados.

- Passo 0:
- ![](../imgs/ordenacao/merge/MergeSort1.png)
- Passo 1:
- ![](../imgs/ordenacao/merge/MergeSort2.png)
Expand Down Expand Up @@ -45,6 +64,28 @@ Na verdade, aqui vamos focar na operação de **merge** entre dois arrays ordena
- Passo 16:
- ![](../imgs/ordenacao/merge/MergeSort17.png)

### Divisão

Uma vez que tenhamos entendido a operação **merge**, ainda tem um detalhe que precisa ser esclarecido.
Nos passos 2 e 3, supomos que os arrays da direita e esquerda estariam ordenados.
Mas para que eles estejam ordenados, algum algoritmo de ordenação deveria ter sido executado sobre eles, e aí não faria muito sentido o que o algoritmo MergeSort, que é um algoritmo de ordenação, usasse algum outro algoritmo de ordenação.

Uma forma de ter arrays ordenados é dividindo o vetor original em vetores menores até que esses vetores possuam apenas um elemento, sendo portanto considerado ordenado.
Uma vez que um vetor v de tamanho n seja quebrado em n vetores de tamanho 1, podemos utilizar a operação de merge para reconstruirmos os arrays de tamanho intermediário de forma ordenada.

Vejamos a ilustração desta parte (divisão) do algoritmo, a seguir.

![](../imgs/ordenacao/merge/MergeSort-Divisao.png)

Ao olhar o código que construiremos vocês vão perceber que a divisão é executada recursivamente primeiro em todos os vetores da esquerda, até chegarmos a vetores de tamanho 1.
Posteriormente, a divisão é executada recursivamente em todos os vetores da direita.
Mas perceba que a ilustração, por uma questão de simplicidade, não representa esse aspecto do algoritmo.
Para melhor entender isso, podemos visualizar o MergeSort passo a passo nesse [simulador](https://www.hackerearth.com/practice/algorithms/sorting/merge-sort/visualize/).

## Código

### Conquista

Agora vamos implementar a função de combinação, ou seja, a função **merge**:

```c
Expand Down Expand Up @@ -77,20 +118,10 @@ void merge(int* v, int tamV, int* e, int tamE, int* d, int tamD){
}
```
Uma vez que tenhamos entendido a operação **merge**, ainda tem um detalhe que precisa ser esclarecido.
Nos passos 2 e 3, supomos que os arrays da direita e esquerda estariam ordenados.
Mas para que eles estejam ordenados, algum algoritmo de ordenação deveria ter sido executado sobre eles, e aí não faria muito sentido o que o algoritmo MergeSort, que é um algoritmo de ordenação, usasse algum outro algoritmo de ordenação.
### Divisão
Uma forma de ter arrays ordenados é dividindo o vetor original em vetores menores até que esses vetores possuam apenas um elemento, sendo portanto considerado ordenado.
Uma vez que um vetor v de tamanho n seja quebrado em n vetores de tamanho 1, podemos utilizar a operação de merge para reconstruirmos os arrays de tamanho intermediário de forma ordenada.
Vejamos a ilustração desta parte (divisão) do algoritmo, a seguir.
![](../imgs/ordenacao/merge/MergeSort-Divisao.png)
Podemos visualizar o MergeSort passo a passo em https://www.hackerearth.com/practice/algorithms/sorting/merge-sort/visualize/.
Agora vamos implementar a função **MergeSort**, encarregade de dividir o vetor em vetores menores de um elemento, e depois aplicar a função **merge** para combinar esses vetores de forma ordenada.
Agora vamos implementar a função encarregada de dividir o vetor em vetores menores de tamanho 1.
A função **MergeSort** executa essa divisão recursivamente, e depois aplica recursivamente a função **merge** para combinar esses vetores de forma ordenada.
```c
void mergeSort(int* v, int tamV){
Expand Down Expand Up @@ -120,14 +151,22 @@ void mergeSort(int* v, int tamV){
}
```

Agora podemos simular como a execução do algoritmo acontece do início até o fim, e em qual ordem, usando este vetor como exemplo: [9,4,3,6,3,2,5,7,1,8].
## Análise Assintótica

O MergeSort é um algoritmo recursivo.
Portanto, quando analisamos linha por linha, existirá uma chamada recursiva à própria função mergeSort, e isso torna a análise que discutimos inviável.
Para algoritmos recursivos é preciso estabelecer o que chamamos de relação de recorrência.
Existem diversos métodos para descobrir a complexidade de algoritmos recursivos: iteração, árvore de recursão, substituição e método mestre.
A seguir eu ilustro a análise assintótica do mergeSort através da combinação entre os métodos iterativos e da árvore de recursão.

![](../imgs/ordenacao/merge/arvore-recursao.png)

Categorizamos o MergeSort como:
## Resumo

- um algoritmo de divisão e conquista
- estável
- recursivo
- out-of-place: O(n), se apagarmos os vetores que não são mais usados (sendo mais específico, θ(n))
- complexidade de tempo: O(nlogn).
- a altura da recursão até chegar no caso base log(n)
- em cada nível executamos o merge em estruturas menores, que somados custam O(n)
- Algoritmo de divisão e conquista
- Recursivo
- Out-of-place
- Estável
- O(nlogn), Ômega(nlogn), portanto, Theta(nlogn)
- a altura da recursão até chegar no caso base é log(n)
- em cada nível executamos o merge em estruturas menores, que somados custam O(n)

0 comments on commit d388bb3

Please sign in to comment.