Algoritmos para Ordenação de Listas Ligadas: Merge Sort e Fila de Prioridade

Ordenar uma lista simplesmente ligada de forma eficiente requer algoritmos que minimizem o acesso aleatório, priorizando o acesso sequencial. A seguir, exploramos as abordagens de Merge Sort (Top-down e Bottom-up) e o uso de Filas de Prioridade.

1. Merge Sort Top-down (Recursivo)

Esta abordagem utiliza a estratégia de "dividir para conquistar". O processo consiste em encontrar o meio da lista usando a técnica de ponteiros rápido e lento (fast/slow pointers), dividir a lista em duas metades e ordená-las recursivamente antes de entercalá-las.

/**
 * Definição para lista ligada simples.
 * public class NoLista {
 *     int valor;
 *     NoLista proximo;
 *     NoLista(int x) { valor = x; }
 * }
 */
class SolucaoMergeSortTopDown {
    public NoLista sortList(NoLista inicio) {
        if (inicio == null || inicio.proximo == null) {
            return inicio;
        }

        // Encontrar o ponto médio
        NoLista anterior = null, lento = inicio, rápido = inicio;
        while (rápido != null && rápido.proximo != null) {
            anterior = lento;
            lento = lento.proximo;
            rápido = rápido.proximo.proximo;
        }

        // Desconectar as duas metades
        anterior.proximo = null;

        NoLista l1 = sortList(inicio);
        NoLista l2 = sortList(lento);

        return intercalar(l1, l2);
    }

    private NoLista intercalar(NoLista a, NoLista b) {
        NoLista auxiliar = new NoLista(0);
        NoLista atual = auxiliar;

        while (a != null && b != null) {
            if (a.valor < b.valor) {
                atual.proximo = a;
                a = a.proximo;
            } else {
                atual.proximo = b;
                b = b.proximo;
            }
            atual = atual.proximo;
        }

        if (a != null) atual.proximo = a;
        if (b != null) atual.proximo = b;

        return auxiliar.proximo;
    }
}

2. Merge Sort Bottom-up (Iterativo)

Diferente da versão recursiva, esta técnica elimina o overhead da pilha de chamadas, garantindo complexidade de espaço O(1). O algoritmo itera sobre a lista em tamanhos de subgruppos crescentes (1, 2, 4, 8...), fundindo segmentos adjacentes.

class SolucaoMergeSortBottomUp {
    public NoLista sortList(NoLista head) {
        if (head == null || head.proximo == null) return head;

        int total = 0;
        NoLista temp = head;
        while (temp != null) {
            total++;
            temp = temp.proximo;
        }

        NoLista dummy = new NoLista(0);
        dummy.proximo = head;

        for (int tam = 1; tam < total; tam <<= 1) {
            NoLista anterior = dummy;
            NoLista atual = dummy.proximo;

            while (atual != null) {
                NoLista esq = atual;
                NoLista dir = dividir(esq, tam);
                atual = dividir(dir, tam);
                anterior.proximo = fundir(esq, dir);
                while (anterior.proximo != null) {
                    anterior = anterior.proximo;
                }
            }
        }
        return dummy.proximo;
    }

    private NoLista dividir(NoLista inicio, int passo) {
        for (int i = 1; inicio != null && i < passo; i++) {
            inicio = inicio.proximo;
        }
        if (inicio == null) return null;
        NoLista resto = inicio.proximo;
        inicio.proximo = null;
        return resto;
    }

    private NoLista fundir(NoLista l1, NoLista l2) {
        NoLista fake = new NoLista(0);
        NoLista p = fake;
        while (l1 != null && l2 != null) {
            if (l1.valor < l2.valor) {
                p.proximo = l1;
                l1 = l1.proximo;
            } else {
                p.proximo = l2;
                l2 = l2.proximo;
            }
            p = p.proximo;
        }
        p.proximo = (l1 != null) ? l1 : l2;
        return fake.proximo;
    }
}

3. Ordenação com Fila de Prioridade (Min-Heap)

Esta abordagem utiliza uma estrutura de dados de Heap para gerenciar os nós. Embora simples de implementar, a complexidade de espaço aumenta para O(n) devido ao armazenamento de todos os nós na fila.

import java.util.PriorityQueue;

class SolucaoPriorityQueue {
    public NoLista sortList(NoLista head) {
        if (head == null) return null;

        // Fila de prioridade comparando os valores dos nós
        PriorityQueue<NoLista> heap = new PriorityQueue<>((n1, n2) -> n1.valor - n2.valor);

        NoLista marcador = head;
        while (marcador != null) {
            heap.offer(marcador);
            marcador = marcador.proximo;
        }

        NoLista sentinela = new NoLista(0);
        NoLista rastro = sentinela;

        while (!heap.isEmpty()) {
            NoLista menor = heap.poll();
            rastro.proximo = menor;
            rastro = rastro.proximo;
            // Importante limpar o ponteiro para evitar ciclos
            rastro.proximo = null;
        }

        return sentinela.proximo;
    }
}

A escolha entre estes métodos depende das restrições de memória: o Merge Sort Bottom-up é ideal quando o espaço é limitado, enquanto a Fila de Prioridade oferece uma implementação mais direta e legível.

Tags: mergesort linked-list priority-queue algorithms data-structures

Publicado em 6-26 00:08