Quicksort com Python
Na vida real, devemos sempre usar a classificação embutida fornecida pelo Python. No entanto, compreender o algoritmo de classificação rápida é instrutivo.
Meu objetivo aqui é quebrar o assunto de forma que seja facilmente entendido e replicável pelo leitor sem ter que retornar aos materiais de referência.
O algoritmo quicksort é essencialmente o seguinte:
- Selecione um ponto de dados pivô.
- Mova todos os pontos de dados abaixo (abaixo) do pivô para uma posição abaixo do pivô - mova aqueles maiores ou iguais (acima) do pivô para uma posição acima dele.
- Aplique o algoritmo às áreas acima e abaixo do pivô
Se os dados forem distribuídos aleatoriamente, selecionar o primeiro ponto de dados como o pivô é equivalente a uma seleção aleatória.
Exemplo legível:
Primeiro, vamos olhar um exemplo legível que usa comentários e nomes de variáveis para apontar para valores intermediários:
def quicksort(xs):
"""Given indexable and slicable iterable, return a sorted list"""
if xs: # if given list (or tuple) with one ordered item or more:
pivot = xs[0]
# below will be less than:
below = [i for i in xs[1:] if i < pivot]
# above will be greater than or equal to:
above = [i for i in xs[1:] if i >= pivot]
return quicksort(below) + [pivot] + quicksort(above)
else:
return xs # empty list
Para reafirmar o algoritmo e o código demonstrado aqui - movemos os valores acima do pivô para a direita e os valores abaixo do pivô para a esquerda e, em seguida, passamos essas partições para a mesma função para serem classificados posteriormente.
Jogado golfe:
Isso pode ser definido para 88 caracteres:
q=lambda x:x and q([i for i in x[1:]if i<=x[0]])+[x[0]]+q([i for i in x[1:]if i>x[0]])
Para ver como chegamos lá, primeiro pegue nosso exemplo legível, remova comentários e docstrings e encontre o pivô no local:
def quicksort(xs):
if xs:
below = [i for i in xs[1:] if i < xs[0]]
above = [i for i in xs[1:] if i >= xs[0]]
return quicksort(below) + [xs[0]] + quicksort(above)
else:
return xs
Agora encontre abaixo e acima, no local:
def quicksort(xs):
if xs:
return (quicksort([i for i in xs[1:] if i < xs[0]] )
+ [xs[0]]
+ quicksort([i for i in xs[1:] if i >= xs[0]]))
else:
return xs
Agora, sabendo que and
retorna o elemento anterior se falso, senão, se for verdadeiro, avalia e retorna o seguinte elemento, temos:
def quicksort(xs):
return xs and (quicksort([i for i in xs[1:] if i < xs[0]] )
+ [xs[0]]
+ quicksort([i for i in xs[1:] if i >= xs[0]]))
Já que lambdas retornam uma única epressão, e nós simplificamos para uma única expressão (embora esteja ficando mais ilegível), agora podemos usar um lambda:
quicksort = lambda xs: (quicksort([i for i in xs[1:] if i < xs[0]] )
+ [xs[0]]
+ quicksort([i for i in xs[1:] if i >= xs[0]]))
E para reduzir ao nosso exemplo, reduza os nomes das funções e variáveis para uma letra e elimine os espaços em branco desnecessários.
q=lambda x:x and q([i for i in x[1:]if i<=x[0]])+[x[0]]+q([i for i in x[1:]if i>x[0]])
Observe que este lambda, como a maioria dos códigos de golfe, é um estilo bastante ruim.
Quicksort no local, usando o esquema de particionamento Hoare
A implementação anterior cria muitas listas extras desnecessárias. Se pudermos fazer isso no local, evitaremos o desperdício de espaço.
A implementação abaixo usa o esquema de particionamento Hoare, sobre o qual você pode ler mais na wikipedia (mas aparentemente removemos até 4 cálculos redundantes por partition()
chamada usando a semântica while-loop em vez de do-while e movendo as etapas de redução para o final de o loop while externo.).
def quicksort(a_list):
"""Hoare partition scheme, see https://en.wikipedia.org/wiki/Quicksort"""
def _quicksort(a_list, low, high):
# must run partition on sections with 2 elements or more
if low < high:
p = partition(a_list, low, high)
_quicksort(a_list, low, p)
_quicksort(a_list, p+1, high)
def partition(a_list, low, high):
pivot = a_list[low]
while True:
while a_list[low] < pivot:
low += 1
while a_list[high] > pivot:
high -= 1
if low >= high:
return high
a_list[low], a_list[high] = a_list[high], a_list[low]
low += 1
high -= 1
_quicksort(a_list, 0, len(a_list)-1)
return a_list
Não tenho certeza se testei exaustivamente:
def main():
assert quicksort([1]) == [1]
assert quicksort([1,2]) == [1,2]
assert quicksort([1,2,3]) == [1,2,3]
assert quicksort([1,2,3,4]) == [1,2,3,4]
assert quicksort([2,1,3,4]) == [1,2,3,4]
assert quicksort([1,3,2,4]) == [1,2,3,4]
assert quicksort([1,2,4,3]) == [1,2,3,4]
assert quicksort([2,1,1,1]) == [1,1,1,2]
assert quicksort([1,2,1,1]) == [1,1,1,2]
assert quicksort([1,1,2,1]) == [1,1,1,2]
assert quicksort([1,1,1,2]) == [1,1,1,2]
Conclusão
Esse algoritmo é freqüentemente ensinado em cursos de ciência da computação e solicitado em entrevistas de emprego. Isso nos ajuda a pensar sobre recursão e dividir para conquistar.
Quicksort não é muito prático em Python, pois nosso algoritmo timsort embutido é bastante eficiente e temos limites de recursão. Esperaríamos classificar as listas no local com list.sort
ou criar novas listas classificadas com sorted
- ambas as quais recebem um argumento key
e reverse
.
my_list = list1 + list2 + ...
. Ou descompacte listas para uma nova listamy_list = [*list1, *list2]