Velocidade e Qualidade com Estruturas de Dados e Algoritmos

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.

Por Fábio Linhares • Instituto Infnet

EXERCÍCIO 01
List Comprehension para Filtrar Caracteres
Objetivo: Usar list comprehension em Python para retornar caracteres individuais de uma string que não são espaços em branco.

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`.

A estrutura básica é `[expressão for item in iterável if condição]`. No nosso caso, a `expressão` é o próprio `caractere`, o `iterável` é a `string_original`, e a `condição` é verificar se o caractere não é um espaço em branco (`caractere != ' '`).
string_original = "Sítio do pica-pau amarelo \n 2023"
caracteres_filtrados = [char for char in string_original if not char.isspace()]
print(caracteres_filtrados)
['S', 'í', 't', 'i', 'o', 'd', 'o', 'p', 'i', 'c', 'a', '-', 'p', 'a', 'u', 'a', 'm', 'a', 'r', 'e', 'l', 'o', '2', '0', '2', '3']

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.

EXERCÍCIO 02
Algoritmo de Ordenação de Cartas
Objetivo: Elaborar um algoritmo para ordenar 13 cartas de espadas sob restrições físicas específicas.

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.

1. O Problema
O desafio consiste em ordenar manualmente 13 cartas de um baralho, sob restrições físicas severas:
  • Todas as cartas começam em uma mão (lista A);
  • A outra mão só pode segurar uma carta por vez;
  • Ao transferir cartas para pilhas auxiliares, só é permitido inseri-las no início ou no fim;
  • Não há inserção direta no meio de nenhuma pilha.
👉 Essas restrições tornam o problema análogo à manipulação de pilhas (stacks) e filas duplamente terminadas (deques) em ciência da computação.
2. Interpretação das Restrições
O mapeamento entre as regras manuais e conceitos computacionais é imediato:
  • Acesso sequencial → só a carta do topo é visível, como em uma pilha.
  • Movimento unitário → apenas uma carta por vez, sem operações em blocos.
  • Inserção limitada → apenas nas extremidades de uma pilha, comportamento típico de deque.

Esse modelo de acesso restrito inviabiliza algoritmos mais sofisticados, que dependem de acesso aleatório ou de divisão eficiente de listas.

3. Mapeamento para Algoritmos Clássicos

Dentro dessas limitações, o processo lembra dois métodos conhecidos:

3.1. Insertion Sort

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)$

3.2. Selection Sort

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)$

👉 Em ambos os casos, o limite inferior é quadrático.
4. Implementação em Python

Para ilustrar, segue uma simulação didática que respeita as restrições descritas:

from collections import deque

# ============================================
# Procedimento operacional (amarrado às regras)
# ============================================
# Regras do enunciado (mapeamento):
# (a) Todas as cartas começam em A (a "mão")  -> representamos como deque A
# (b) Objetivo: ordenar as 13 cartas         -> retornamos a lista ordenada
# (c) Só pode pegar a PRIMEIRA carta de uma lista -> popleft()
# (d) Pode inserir no INÍCIO ou FIM de uma lista  -> appendleft() / append()
# (e) No máximo 4 listas                     -> usamos A, B, C (D não é necessário)
# (f) Pode inverter uma lista movendo para outra -> (neste método, a inversão aparece
#                                                   naturalmente ao usar C como buffer)

def mover_primeira(origem, destino, posicao="inicio"):
    """
    Operação atômica permitida:
    - Remove a primeira carta (topo) de 'origem' e insere no início ou fim de 'destino'.
    """
    carta = origem.popleft()  # (c)
    if posicao == "inicio":
        destino.appendleft(carta)  # (d)
    else:
        destino.append(carta)      # (d)

def ordenar_13_cartas(cartas):
    """
    Estratégia: INSERTION SORT usando 3 listas (A, B, C), respeitando (c) e (d).

    Invariante:
      B permanece SEMPRE ordenada do menor (frente) ao maior (fim).

    Passo de inserção de uma carta x:
      1) tire x do topo de A
      2) enquanto a primeira de B for menor que x:
           mova a primeira de B para o INÍCIO de C (isso inverte a ordem dos removidos)
      3) coloque x no INÍCIO de B (antes do primeiro >= x)
      4) devolva tudo de C para o INÍCIO de B (desfaz a inversão e restaura a ordem)
    """
    A = deque(cartas)  # (a) topo = esquerda
    B = deque()        # lista ordenada
    C = deque()        # buffer (auxiliar)

    while A:
        x = A.popleft()  # (c) pega a primeira carta de A

        # Move cartas menores para C (inverte)
        while B and B[0] < x:
            mover_primeira(B, C, "inicio")

        # Insere x no lugar correto (antes do primeiro >= x)
        B.appendleft(x)

        # Restaura as cartas movidas (desfaz a inversão)
        while C:
            mover_primeira(C, B, "inicio")

    # Se você quiser "devolver" para uma mão A_final com topo = menor carta:
    A_final = deque()
    while B:
        mover_primeira(B, A_final, "fim")  # preserva a ordem ascendente em A_final

    return list(A_final)

# Demonstração
cartas = [7, 2, 11, 1, 9, 4, 13, 5, 8, 3, 12, 6, 10]
print(ordenar_13_cartas(cartas))
# Saída esperada: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]

# Complexidade (por que é O(n^2)):
# Para cada carta retirada de A, no pior caso movemos várias cartas de B para C e de volta.
# Isso dá um número total de movimentos proporcional a 1 + 2 + ... + (n-1) = O(n^2).
5. Análise de Complexidade

O processo pode ser dividido em duas fases:

  • Distribuição:
    • Retirar cada carta de A → $O(n)$.
    • Inserções variam entre $O(1)$ (append) e $O(n)$ (insert no início).
    • Pior caso → $O(n^2)$.
  • Consolidação:
    • Movimentos entre listas → até $O(n)$ cada.
    • Total de movimentos → até $O(n^2)$.
Classificação final:
  • Melhor caso: $O(n)$
  • Pior caso: $O(n^2)$
  • Médio caso: tende ao quadrático.
6. Conclusão

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.

EXERCÍCIO 03
Passos da Busca Linear
Objetivo: Calcular os passos necessários para uma busca linear pelo número 8 no array [2, 4, 6, 8, 10, 12, 13].

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.

Pense na busca linear como procurar um livro específico em uma prateleira, começando pelo primeiro livro à esquerda e verificando um por um até encontrar o que você quer.
Contagem de Passos:
Array: `[2, 4, 6, 8, 10, 12, 13]`
Alvo: `8`
Passo 1: Compara 8 com o primeiro elemento, 2. Não são iguais.
Passo 2: Compara 8 com o segundo elemento, 4. Não são iguais.
Passo 3: Compara 8 com o terceiro elemento, 6. Não são iguais.
Passo 4: Compara 8 com o quarto elemento, 8. São iguais! O elemento foi encontrado.

Portanto, seriam necessários 4 passos para realizar a busca linear.

EXERCÍCIO 04
Passos da Busca Binária
Objetivo: Calcular os passos da busca binária para o mesmo array e alvo do exercício anterior.

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.

É como procurar uma palavra em um dicionário. Você não começa da primeira página. Você abre no meio e decide se a palavra que procura está na primeira ou na segunda metade, descartando 50% do dicionário a cada passo.
Contagem de Passos:
Array: `[2, 4, 6, 8, 10, 12, 13]`
Alvo: `8`
Passo 1: O array tem 7 elementos. O elemento do meio é o de índice 3, que é o valor `8`. Comparamos o alvo (8) com o valor do meio (8). Eles são iguais! O elemento foi encontrado.

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:

Exemplo com Alvo = 12:
Passo 1: Meio é 8. Como 12 > 8, descartamos a metade esquerda e o meio. O novo array de busca é `[10, 12, 13]`.
Passo 2: O meio do novo array é 12. Comparamos 12 com 12. Encontrado! (2 passos)
EXERCÍCIO 05
Otimizando uma Função de O(N²) para O(N)
Objetivo: Reescrever a função ineficiente greatestNumber para ter uma complexidade de tempo linear.
Nota: se o enunciado usar “maior número único” no sentido de “aparece exatamente uma vez”, veja a alternativa no final.

A função original é O(N²) porque faz dois laços aninhados: para cada elemento i (N vezes), ela percorre novamente o array inteiro para comparar com todos os j (mais N comparações). Em termos práticos, o número de comparações cresce como N × N.

A versão otimizada elimina o laço interno mantendo um invariante: após processar cada posição, greatest_so_far guarda o maior valor visto até agora. Assim, fazemos apenas um passeio pela lista: tempo O(N) e espaço extra O(1).

Padrão de ouro para ganhar desempenho: troque “comparar tudo com tudo” por “varrer uma vez e manter estado”. Aqui, o estado é só o maior valor encontrado até agora.
# Função Original Ineficiente - O(N²)
def greatestNumber_inefficient(array):
for i in array:
isValTheGreatest = True
for j in array:
if j > i:
isValTheGreatest = False
if isValTheGreatest:
return i

# Função Otimizada (maior número / máximo) - Tempo O(N), Espaço O(1)
def greatestNumber_efficient(array):
if not array: # Trata o caso de um array vazio
return None

greatest_so_far = array[0] # Invariante: maior valor visto até agora

for number in array[1:]:
if number > greatest_so_far:
greatest_so_far = number

return greatest_so_far

print(greatestNumber_efficient([2, 4, 6, 8, 10, 12, 13]))
13

Por que este código?

Ele percorre o array uma única vez. Começa assumindo que o primeiro elemento é o maior e, a cada novo elemento, faz apenas uma comparação: “este número é maior do que o maior que eu já vi?”. Se sim, atualiza greatest_so_far. Ao final, essa variável contém o maior valor do array. Isso reduz o tempo de O(N²) para O(N).

Casos de borda recomendados para teste:

print(greatestNumber_efficient([]))
print(greatestNumber_efficient([5]))
print(greatestNumber_efficient([-3, -10, -2]))
print(greatestNumber_efficient([7, 7, 7]))
Se “único” for literal (isto é, “aparece exatamente uma vez”): o problema muda. Exemplo: em [5, 1, 5, 4, 4, 3], o maior número é 5, mas o maior número único é 3. A solução abaixo continua em tempo O(N), mas usa memória extra O(N) para contar ocorrências.
# Alternativa: maior número "único" (frequência = 1) - Tempo O(N), Espaço O(N)
def greatestUniqueNumber(array):
if not array:
return None

counts = {}
for x in array:
counts[x] = counts.get(x, 0) + 1

best = None
for x, c in counts.items():
if c == 1 and (best is None or x > best):
best = x

return best

print(greatestUniqueNumber([5, 1, 5, 4, 4, 3]))
3

Resumo de complexidade:

  • Maior número (máximo): tempo O(N), espaço extra O(1).
  • Maior número “único” (frequência = 1): tempo O(N), espaço extra O(N) para contagem.
EXERCÍCIO 06
O Problema do Trigo e do Xadrez
Objetivo: Criar uma função para o problema dos grãos de arroz no tabuleiro de xadrez e analisar sua complexidade.

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) Função em Python
Aplicando logaritmo na base 2 em ambos os lados da equação $g = 2^{(n-1)}$, obtemos $\log_2(g) = n-1$. Portanto, $n = \log_2(g) + 1$. A biblioteca `math` do Python pode calcular isso diretamente.
import math

def eh_potencia_de_dois(n):
return n > 0 and (n & (n - 1)) == 0

def encontrar_quadrado(graos):
# Validação: o enunciado exige que 'graos' seja potência de 2
if not eh_potencia_de_dois(graos):
return "Quantidade de grãos inválida. Deve ser uma potência de 2."

# Versão EXATA para inteiros grandes (sem floats):
# 1->1, 2->2, 4->3, 8->4... então n = bit_length()
return graos.bit_length()

# Alternativa (didática) via log2: pode perder precisão em números enormes
def encontrar_quadrado_log2(graos):
if not eh_potencia_de_dois(graos):
return "Quantidade de grãos inválida. Deve ser uma potência de 2."
return int(math.log2(graos)) + 1

print(f"Para 16 grãos, o quadrado é: {encontrar_quadrado(16)}")
print(f"Para 1024 grãos, o quadrado é: {encontrar_quadrado(1024)}")
print(f"Entrada inválida (12): {encontrar_quadrado(12)}")
Para 16 grãos, o quadrado é: 5
Para 1024 grãos, o quadrado é: 11
Entrada inválida (12): Quantidade de grãos inválida. Deve ser uma potência de 2.
b) Notação Big O

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)$.

EXERCÍCIO 07
Complexidade de Tempo de Filtragem de Strings
Objetivo: Descrever a complexidade de tempo da função `selectAStrings` usando a Notação Big O.
def selectAStrings(array):
# startswith(prefixo) custa O(p), onde p é o tamanho do prefixo.
# Aqui p=1 ("a"), então o teste é efetivamente constante.
# No caso geral (prefixos maiores), a complexidade vira O(n * p).
# Com p constante (como aqui), fica O(n).
return [s for s in array if s.startswith("a")]

# Variante: ignorar maiúsculas/minúsculas (trade-off: cria string temporária)
def selectAStrings_casefold(array):
return [s for s in array if s.lower().startswith("a")]

A complexidade de tempo desta função é $O(N)$, ou tempo linear, onde N é o número de strings no array de entrada.

A chave para analisar a complexidade é identificar os loops. A função contém um único loop `for` que itera sobre cada um dos N elementos do `array` exatamente uma vez. Dentro do loop, as operações (`startswith`, `append`) levam um tempo que não depende do tamanho do array, mas sim do comprimento da string, que podemos considerar constante em média. Como o trabalho principal cresce em proporção direta ao número de elementos no array, a complexidade é linear. Se o array dobrar de tamanho, o tempo de execução também irá aproximadamente dobrar.
EXERCÍCIO 08
Bubble Sort Bidirecional
Objetivo: Modificar o método `bubbleSort()` para que ele seja bidirecional (Cocktail Shaker Sort).

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.

Pense nisso como um barman agitando um coquetel. Ele agita para cima e para baixo. O algoritmo faz o mesmo: uma passagem da esquerda para a direita "flutua" o maior item para o topo (final), e a passagem de volta, da direita para a esquerda, "afunda" o menor item para o fundo (início).

# 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)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
EXERCÍCIO 09
Implementação do Bubble Sort
Objetivo: Escrever uma função em Python que implemente o Bubble Sort para ordenar números e testá-la.

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.

O nome "Bubble Sort" (Ordenação por Bolha) vem da forma como os elementos maiores "borbulham" para o final da lista a cada passagem, como bolhas de ar subindo na água.

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")
Lista original 1: [64, 34, 25, 12, 22, 11, 90] Lista ordenada 1: [11, 12, 22, 25, 34, 64, 90] Lista original 2: [5, 1, 4, 2, 8] Lista ordenada 2: [1, 2, 4, 5, 8] Lista original 3: [1, 2, 3, 4, 5] Lista ordenada 3: [1, 2, 3, 4, 5] Lista original 4: [9, 8, 7, 6, 5] Lista ordenada 4: [5, 6, 7, 8, 9] Lista original 5: [] Lista ordenada 5: []
EXERCÍCIO 10
Bubble Sort para Strings
Objetivo: Modificar a função `bubbleSort()` para ordenar uma lista de strings em ordem alfabética.

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).

Quando você faz `"banana" > "abacaxi"`, o Python avalia isso como `True`, pois "b" vem depois de "a" no alfabeto. O algoritmo de Bubble Sort usa exatamente essa comparação para ordenar os elementos.

# 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")
Lista original de frutas: ['banana', 'maçã', 'uva', 'abacaxi', 'laranja'] Lista ordenada de frutas: ['abacaxi', 'banana', 'laranja', 'maçã', 'uva'] Lista original de nomes: ['Carlos', 'Beatriz', 'Ana', 'Daniel'] Lista ordenada de nomes: ['Ana', 'Beatriz', 'Carlos', 'Daniel'] Lista original mista: ['Python', 'java', 'C++', 'python'] Lista ordenada mista: ['C++', 'Python', 'java', 'python']

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():`.