Organização de dados, análise de operações, implementação de algoritmos eficientes, buscas lineares e binárias, avaliação de complexidade com notação Big O e ordenação com Bubble Sort em arrays.
A "list comprehension" é uma forma concisa e elegante de criar listas em Python. A sintaxe é mais curta e, muitas vezes, mais legível do que usar um loop `for` tradicional com `append`.
Por que este código?
Utilizamos o método `char.isspace()` que é mais robusto do que `char != ' '`. Ele identifica todos os tipos de caracteres de espaço em branco, incluindo espaços, tabulações (`\t`) e quebras de linha (`\n`), garantindo uma filtragem completa e precisa conforme solicitado.
A proposta desse exercício 2, à primeira vista, suscita reflexões interessantes sobre restrições de acesso a dados e seus impactos na complexidade de algoritmos. Quando transposto para execução manual — ordenar um pequeno baralho sob regras rígidas de manipulação — o problema se revela mais profundo: o algoritmo resultante apresenta, de forma inevitável, uma complexidade quadrática $O(n^2)$.
Essa constatação não decorre de detalhes de implementação, mas das próprias restrições impostas: acesso sequencial às cartas, movimentação unitária e inserção apenas nas extremidades. Em ciência da computação, tais limitações equivalem ao uso de pilhas e deques, estruturas que impedem a aplicação de métodos mais eficientes como Quicksort ou Mergesort. O resultado é um processo que se aproxima conceitualmente de algoritmos clássicos como Insertion Sort e Selection Sort, ambos notoriamente quadráticos no pior caso.
Esse modelo de acesso restrito inviabiliza algoritmos mais sofisticados, que dependem de acesso aleatório ou de divisão eficiente de listas.
Dentro dessas limitações, o processo lembra dois métodos conhecidos:
Cada carta retirada de A deve ser colocada em sua posição correta em uma pilha auxiliar (B/C/D). No pior caso, inserir exige deslocar todas as cartas de uma pilha, resultando no custo acumulado: $1 + 2 + \dots + n = O(n^2)$
Outra estratégia é escolher, a cada rodada, a menor carta restante e transferi-la para a pilha final. Encontrar a menor implica percorrer todas as n cartas restantes, repetido n vezes: $n + (n-1) + (n-2) + \dots + 1 = O(n^2)$
Para ilustrar, segue uma simulação didática que respeita as restrições descritas:
import collections
def encontrar_lugar_para_inserir(numero, listas):
for nome, lista in listas.items():
if not lista:
return nome, 'fim'
if numero < lista[0]:
return nome, 'inicio'
if numero > lista[-1]:
return nome, 'fim'
return None, None
def mover_entre_listas_para_abrir_espaco(listas, historico):
listas_ativas = {k: v for k, v in listas.items() if v}
if len(listas_ativas) < 2:
return False
nome_destino = max(listas_ativas, key=lambda k: listas_ativas[k][0])
lista_destino = listas_ativas[nome_destino]
candidatos = []
for nome_origem, lista_origem in listas_ativas.items():
if nome_origem == nome_destino:
continue
num = lista_origem[0]
if num < lista_destino[0] and (nome_destino, nome_origem, num) not in historico:
candidatos.append((nome_origem, num, 'inicio'))
elif num > lista_destino[-1] and (nome_destino, nome_origem, num) not in historico:
candidatos.append((nome_origem, num, 'fim'))
if not candidatos:
return False
origem, num, pos = min(candidatos, key=lambda c: abs(c[1] - lista_destino[0]))
listas[origem].pop(0)
if pos == 'inicio':
lista_destino.insert(0, num)
else:
lista_destino.append(num)
historico.add((origem, nome_destino, num))
return True
def consolidar_listas(listas):
historico = set()
while sum(1 for l in listas.values() if l) > 1:
if not mover_entre_listas_para_abrir_espaco(listas, historico):
break
for l in listas.values():
if l: return l
return []
# Demonstração
A = [3, 5, 6, 2, 4, 7, 8, 9, 13, 1, 12, 11, 10]
listas = collections.OrderedDict([('B', []), ('C', []), ('D', [])])
while A:
num = A.pop(0)
nome, pos = encontrar_lugar_para_inserir(num, listas)
if nome:
if pos == 'inicio': listas[nome].insert(0, num)
else: listas[nome].append(num)
else:
mover_entre_listas_para_abrir_espaco(listas, set())
resultado = consolidar_listas(listas)
print("Lista final ordenada:", resultado)
O processo pode ser dividido em duas fases:
As restrições impostas pelo problema conduzem inevitavelmente a um algoritmo de complexidade $O(n^2)$ no pior caso. Mesmo com otimizações de implementação (como o uso de deque para inserções em tempo constante nas extremidades), o limite teórico permanece.
Esse comportamento aproxima o exercício de um Insertion Sort ou Selection Sort em ambiente restrito, demonstrando que a ineficiência não resulta da técnica escolhida, mas do modelo de acesso limitado que define as regras do jogo.
Escrevi um artigo sobre isso. Se tiver interesse acesse: aqui.
A busca linear é o método de busca mais simples. Ela percorre sequencialmente cada elemento do array, um por um, desde o início, até encontrar o valor desejado ou chegar ao final do array.
Portanto, seriam necessários 4 passos para realizar a busca linear.
A busca binária é um algoritmo muito mais eficiente, mas requer que o array esteja ordenado. A cada passo, ela elimina metade dos elementos restantes, comparando o alvo com o elemento do meio da seção de busca.
Neste caso específico e favorável, a busca binária levaria apenas 1 passo.
Vamos considerar um alvo diferente, como o número 12, para ilustrar melhor:
A função original tem uma complexidade de $O(N^2)$ porque, para cada elemento do array (`for i in array`), ela percorre o array inteiro novamente (`for j in array`). Isso é extremamente ineficiente.
Por que este código?
Esta nova versão percorre o array apenas uma vez. Ela inicializa uma variável `greatest_so_far` com o primeiro elemento e, em seguida, itera pelo resto do array. Se encontrar um número maior, atualiza a variável. Ao final do único loop, a variável conterá o maior número do array. Isso reduz a complexidade de quadrática ($O(N^2)$) para linear ($O(N)$), tornando-a drasticamente mais rápida para grandes conjuntos de dados.
O número de grãos em cada quadrado segue uma progressão geométrica de base 2. O quadrado 1 tem $2^0=1$ grão, o quadrado 2 tem $2^1=2$ grãos, o quadrado 3 tem $2^2=4$ grãos, e assim por diante. O quadrado `n` terá $2^{(n-1)}$ grãos.
Para encontrar o número do quadrado (`n`) a partir da quantidade de grãos (`g`), precisamos resolver a equação $g = 2^{(n-1)}$. Isso é um problema de logaritmo.
A complexidade de tempo da função é $O(1)$, ou tempo constante.
Por quê? A função executa um número fixo de operações matemáticas (`math.log2`, adição, etc.), independentemente do tamanho do número de grãos de entrada. Seja para 16 grãos ou para um trilhão de grãos, o número de cálculos é o mesmo. Portanto, o tempo de execução não cresce com o tamanho da entrada, caracterizando a complexidade $O(1)$.
A complexidade de tempo desta função é $O(N)$, ou tempo linear, onde N é o número de strings no array de entrada.
O algoritmo solicitado é conhecido como Cocktail Shaker Sort. Ele é uma variação do Bubble Sort que otimiza o processo ordenando em ambas as direções em cada passagem. Em uma passagem, ele move o maior elemento para o final (como o Bubble Sort) e, na passagem de volta, move o menor elemento para o início. Isso pode resolver o problema de "tartarugas" (elementos pequenos no final da lista) que tornam o Bubble Sort lento.
# Supondo que a função está dentro de uma classe com self._a (o array)
# e self.swap(i, j) como no exercício original.
def bidirectionalBubbleSort(self):
left_index = 0
right_index = len(self._a) - 1
swapped = True
while swapped:
# Primeiro, passamos da esquerda para a direita (Bubble Sort padrão)
swapped = False
for i in range(left_index, right_index):
if self._a[i] > self._a[i+1]:
self.swap(i, i+1)
swapped = True
# Se nenhuma troca ocorreu, o array está ordenado.
if not swapped:
break
# O elemento na `right_index` agora é o maior, então podemos diminuir o limite
right_index = right_index - 1
# Agora, passamos da direita para a esquerda, movendo o menor para o início
swapped = False
for i in range(right_index, left_index, -1):
if self._a[i] < self._a[i-1]:
self.swap(i, i-1)
swapped = True
# O elemento na `left_index` agora é o menor, podemos aumentar o limite
left_index = left_index + 1
# Exemplo de como a classe poderia ser para teste:
class Sorter:
def __init__(self, arr):
self._a = arr
def swap(self, i, j):
self._a[i], self._a[j] = self._a[j], self._a[i]
bidirectionalBubbleSort = bidirectionalBubbleSort # Atribui a função à classe
# Teste
my_array = [5, 1, 4, 2, 8, 0, 3, 9, 6, 7]
sorter = Sorter(my_array)
sorter.bidirectionalBubbleSort()
print(sorter._a)
O Bubble Sort é um algoritmo de ordenação simples que repetidamente percorre a lista, compara elementos adjacentes e os troca se estiverem na ordem errada. As passagens pela lista são repetidas até que nenhuma troca seja necessária, o que indica que a lista está ordenada.
def bubble_sort_numeros(lista):
n = len(lista)
# Percorre todos os elementos da lista
for i in range(n):
# A flag `trocou` otimiza o algoritmo. Se não houver trocas em uma passagem,
# a lista já está ordenada e podemos parar.
trocou = False
# A última i posições já estão no lugar certo
for j in range(0, n-i-1):
# Percorre a lista da esquerda para a direita
# Troca se o elemento encontrado for maior que o próximo
if lista[j] > lista[j+1]:
lista[j], lista[j+1] = lista[j+1], lista[j]
trocou = True
if not trocou:
break
return lista
# --- Testes da Função ---
lista1 = [64, 34, 25, 12, 22, 11, 90]
lista2 = [5, 1, 4, 2, 8]
lista3 = [1, 2, 3, 4, 5] # Já ordenada
lista4 = [9, 8, 7, 6, 5] # Ordem reversa
lista5 = [] # Lista vazia
print(f"Lista original 1: {lista1}")
print(f"Lista ordenada 1: {bubble_sort_numeros(lista1.copy())}\n")
print(f"Lista original 2: {lista2}")
print(f"Lista ordenada 2: {bubble_sort_numeros(lista2.copy())}\n")
print(f"Lista original 3: {lista3}")
print(f"Lista ordenada 3: {bubble_sort_numeros(lista3.copy())}\n")
print(f"Lista original 4: {lista4}")
print(f"Lista ordenada 4: {bubble_sort_numeros(lista4.copy())}\n")
print(f"Lista original 5: {lista5}")
print(f"Lista ordenada 5: {bubble_sort_numeros(lista5.copy())}\n")
A beleza do Python é que a mesma lógica do Bubble Sort pode ser aplicada a strings sem modificação alguma. Os operadores de comparação (`>`, `<`, etc.) funcionam nativamente para strings, comparando-as em ordem lexicográfica (alfabética).
# A função é exatamente a mesma do exercício anterior.
# Vamos renomeá-la para clareza.
def bubble_sort_strings(lista):
n = len(lista)
for i in range(n):
trocou = False
for j in range(0, n-i-1):
# A única linha que importa: a comparação.
# Em Python, `>` funciona para strings alfabeticamente.
if lista[j] > lista[j+1]:
lista[j], lista[j+1] = lista[j+1], lista[j]
trocou = True
if not trocou:
break
return lista
# --- Testes da Função com Strings ---
lista_frutas = ["banana", "maçã", "uva", "abacaxi", "laranja"]
lista_nomes = ["Carlos", "Beatriz", "Ana", "Daniel"]
lista_mista = ["Python", "java", "C++", "python"] # 'p' minúsculo vem depois de 'J' maiúsculo
print(f"Lista original de frutas: {lista_frutas}")
print(f"Lista ordenada de frutas: {bubble_sort_strings(lista_frutas.copy())}\n")
print(f"Lista original de nomes: {lista_nomes}")
print(f"Lista ordenada de nomes: {bubble_sort_strings(lista_nomes.copy())}\n")
print(f"Lista original mista: {lista_mista}")
print(f"Lista ordenada mista: {bubble_sort_strings(lista_mista.copy())}\n")
Observação sobre a ordenação mista: A ordenação padrão em Python é sensível a maiúsculas e minúsculas (case-sensitive). Caracteres maiúsculos vêm antes dos minúsculos na tabela ASCII. Para uma ordenação que ignore essa diferença, poderíamos modificar a comparação para `if lista[j].lower() > lista[j+1].lower():`.