Kth Largest Element in an Array
Você recebe um array de números inteiros nums e um número inteiro k. Retorne o k-ésimo maior valor em nums: o valor na posição k, contando a partir de 1, depois que o array for ordenado do maior para o menor.
Valores iguais são contados separadamente. Em [5, 5, 1], o maior valor é 5 e o segundo maior também é 5.
Função
- numsinteger-array
- os valores a serem ranqueados
- kinteger
- qual é o maior valor a retornar, 1 para o maior
- Retornainteger
- o k-ésimo maior valor, contando duplicatas
Restrições
1 ≤ k ≤ nums.length ≤ 104-104 ≤ nums[i] ≤ 104- Valores iguais contam como valores separados.
Exemplos
- Entrada
- nums = [7, 2, 9, 4, 9, 1]k = 2
- Saída
- 9
- Explicação
- Do maior para o menor, os valores são
9, 9, 7, 4, 2, 1. Os dois 9 são contados separadamente, então o segundo maior é9, não7.
- Entrada
- nums = [5, -3, 8, 0, 2]k = 4
- Saída
- 0
- Explicação
- Do maior para o menor, os valores são
8, 5, 2, 0, -3, e o quarto deles é0.
- Entrada
- nums = [6]k = 1
- Saída
- 6
- Explicação
- Com um valor e
k = 1, esse valor é o maior.
+15 testes ocultos ao enviar
Para ir além
Agora os valores chegam um de cada vez. Você consegue informar a mediana de todos os valores vistos até o momento após cada chegada, em O(log n) de tempo por valor?
Dicas
Abra uma de cada vez. Cada uma revela um pouco mais.
Ordenada do maior para o menor, a resposta está em uma posição conhecida. Qual delas? E você precisa de todos os outros valores para saber isso?
O k-ésimo maior valor é o menor dos
kmaiores valores. Se você mantiver apenas oskmaiores valores vistos até agora, com qual deles você compara um novo valor?Mantenha um min-heap com no máximo
kvalores. Um novo valor substitui o elemento do topo quando é maior, e o elemento do topo no final é a resposta. Para obter tempo médioO(n), particione em torno de um pivô aleatório, como o quicksort faz, e mantenha apenas o lado que contém o índicen-k.
Solução
Ordenar e ler uma posição responde à pergunta, e isso é rápido o suficiente aqui. O que um entrevistador quer ver é quanto dessa ordenação você consegue evitar, porque você precisa de uma posição, não de todas as n. Um min-heap de tamanho k mantém apenas os valores que ainda podem ser a resposta, e o quickselect particiona como o quicksort, mas segue apenas o lado que contém a resposta, reduzindo o tempo médio para O(n).
Ordene e leia uma posição
Intuição
O k-ésimo maior valor é definido pela ordem de classificação, então produza essa ordem. Em ordem decrescente, [7, 2, 9, 4, 9, 1] se torna [9, 9, 7, 4, 2, 1], e o k-ésimo maior fica no índice k-1. Para k = 2, esse é o índice 1, o segundo 9. Se sua ordenação colocar o menor primeiro, leia o índice n-k: o índice 4 de [1, 2, 4, 7, 9, 9] é o mesmo 9.
Duplicatas não exigem nenhum tratamento especial: a ordenação mantém cada cópia, e cada cópia ocupa sua própria posição.
Com n = 10^4, uma ordenação faz cerca de n log n ≈ 1.3 × 10^5 comparações, o que passa em todos os testes. O desperdício está em colocar todos os n valores em ordem quando apenas uma posição importa. As próximas duas abordagens fazem menos desse trabalho.
Algoritmo
- Copie
numspara que o array de quem chamou permaneça como estava. - Ordene a cópia. Use uma comparação numérica; algumas linguagens comparam números como texto por padrão.
- Retorne o índice
k-1em uma ordem do maior para o menor, ou o índicen-kem uma ordem do menor para o maior.
def findKthLargest(nums, k):
# Largest first: the k-th largest sits at index k-1.
ordered = sorted(nums, reverse=True)
return ordered[k - 1]Mantenha os k maiores em um min-heap
Intuição
O k-ésimo maior valor é o menor dos k maiores valores. Então percorra nums uma vez e mantenha apenas os k maiores valores vistos até o momento em um min-heap. O topo de um min-heap é seu menor valor, que é exatamente o candidato à resposta.
Quando um valor x chega e o heap contém menos de k valores, adicione-o. Caso contrário, compare x com o topo. Se x não for maior, pelo menos k valores que você manteve são maiores ou iguais a x, então x nunca poderá ser a resposta e você o ignora. Se x for maior, o topo deixou de fazer parte dos k maiores: substitua-o por x. No exemplo 2, com k = 4, os quatro primeiros valores preenchem o heap com 5, -3, 8, 0 e o topo é -3. Então 2 supera -3 e o substitui, o topo passa a ser 0, e 0 é a resposta.
Cada valor exige no máximo uma operação de heap de O(log k), então o total é de O(n log k) de tempo e O(k) de memória. Isso é melhor do que ordenar quando k é pequeno e funciona em um fluxo: você nunca precisa de todos os valores ao mesmo tempo. Python tem heapq, Java tem PriorityQueue, C++ tem priority_queue com greater, Go tem container/heap, Rust tem BinaryHeap com Reverse e PHP tem SplMinHeap. O código para as outras linguagens implementa o heap em um array, no qual os filhos do índice i ficam em 2i+1 e 2i+2, ou em 2i e 2i+1 em Lua e R, que contam a partir de 1.
Algoritmo
- Comece com um min-heap vazio.
- Para cada valor
x, adicione-o enquanto o heap tiver menos dekvalores. - Quando ele tiver
kvalores, substitua o elemento do topo porxsomente quandoxfor maior que o elemento do topo. - Após o último valor, retorne o elemento do topo do heap.
import heapq
def findKthLargest(nums, k):
# A min-heap of the k largest values so far; its top is the smallest of them.
heap = []
for x in nums:
if len(heap) < k:
heapq.heappush(heap, x)
elif x > heap[0]:
heapq.heapreplace(heap, x) # drop the top, add x
return heap[0]Quickselect com particionamento em três vias
Intuição
O Quicksort escolhe um pivô e particiona: valores menores à esquerda dele, valores maiores à direita. Após uma partição, o pivô fica em seu índice final na ordenação, embora nenhum dos lados esteja ordenado ainda. O Quickselect usa esse fato. Na ordem do menor para o maior, a resposta está no índice target = n-k. Após uma partição, target está à esquerda do pivô, no pivô ou à direita dele, então você continua em um dos lados e descarta o outro.
Para [7, 2, 9, 4, 9, 1] e k = 2, target é 6-2 = 4. Particione em torno de 4: 2 e 1 ficam nos índices 0 e 1, 4 fica no índice 2, e 7, 9, 9 ficam nos índices 3 a 5. O índice 4 está à direita, então você mantém apenas os índices 3 a 5. Particione esses valores em torno de 9: 7 fica no índice 3 e os dois 9s ficam nos índices 4 e 5. O índice 4 contém um 9, então a resposta é 9.
Use uma partição em três vias: valores menores que o pivô, depois valores iguais a ele e, por fim, valores maiores que ele, acompanhados por lt e gt. O bloco de valores iguais [lt, gt] já está em sua posição ordenada, então, se target estiver dentro dele, você terminou. Com uma partição simples em duas vias, um array de 10^4 cópias de 7 diminui em um valor por rodada, cerca de 5 × 10^7 passos; a versão em três vias resolve isso em uma passagem.
Escolha o pivô aleatoriamente. Em metade das vezes, ele cai na metade central do intervalo, o que reduz o intervalo para no máximo três quartos do tamanho anterior. Assim, o trabalho esperado corresponde a algumas passagens pelos n valores: O(n). O pior caso ainda é O(n²) se todo pivô for um valor extremo, e uma escolha fixa, como o primeiro elemento, resulta nesse caso quando a entrada está ordenada. O código trabalha sobre uma cópia, o que custa O(n) de memória; particionar o próprio nums reduz esse custo para O(1) se você puder alterar a entrada.
Algoritmo
- Copie
numsparaa, definatarget = n-k,lo = 0ehi = n-1. - Escolha um pivô aleatório de
a[lo..hi]. - Particione
a[lo..hi]em valores menores que, iguais a e maiores que o pivô, deixando os valores iguais ema[lt..gt]. - Se
target < lt, definahi = lt-1; setarget > gt, definalo = gt+1; caso contrário, retorne o pivô. - Repita a partir da etapa 2.
import random
def findKthLargest(nums, k):
a = list(nums)
target = len(a) - k # the answer's index once a is sorted smallest first
lo, hi = 0, len(a) - 1
while True:
pivot = a[random.randint(lo, hi)]
# Three-way partition of a[lo..hi]: < pivot, then == pivot, then > pivot.
lt, i, gt = lo, lo, hi
while i <= gt:
if a[i] < pivot:
a[lt], a[i] = a[i], a[lt]
lt += 1
i += 1
elif a[i] > pivot:
a[i], a[gt] = a[gt], a[i]
gt -= 1
else:
i += 1
# Now a[lt..gt] all equal pivot, and they are in their sorted places.
if target < lt:
hi = lt - 1
elif target > gt:
lo = gt + 1
else:
return pivot
Armadilhas e casos extremos
A maioria das respostas erradas ocorre por causa de duplicatas e da confusão entre as duas maneiras de contar posições.
- Remover as duplicatas primeiro. O problema conta cada ocorrência: em
[7, 2, 9, 4, 9, 1]comk = 2, a resposta é9, mas, depois de transformar o array em um conjunto, ela passa a ser7. - Ler o índice errado.
ké contado a partir de 1, então a resposta está no índicek-1em uma ordem decrescente e no índicen-kem uma ordem crescente, não emn-k-1. - Ordenar números como texto. Em JavaScript e TypeScript,
[10, 9, 2].sort()resulta em[10, 2, 9]. Passe(a, b) => a - b. - Usar um max-heap de tamanho
k. Remover o maior elemento mantém oskmenores valores e retorna o k-ésimo menor. - Usar Quickselect com uma partição de duas vias ou um pivô fixo. Muitos valores iguais ou um array ordenado levam a um custo de
O(n²), o que ocorre nos testes maiores.
Perguntas frequentes4
Qual é a complexidade de tempo de Kth Largest Element in an Array?
A ordenação leva O(n log n) de tempo. Um min-heap de tamanho k leva O(n log k) de tempo e usa O(k) de memória. O Quickselect com um pivô aleatório leva O(n) de tempo em média e O(n²) no pior caso, algo que um pivô aleatório torna muito improvável.
Por que usar um heap mínimo, e não um heap máximo, para encontrar o k-ésimo maior elemento?
O heap armazena os k maiores valores encontrados até agora, e aquele que você deve usar na comparação e remover é o menor deles. Um min-heap mantém esse valor no topo. Um max-heap só funciona se você inserir nele todos os n valores e remover o maior k-1 vezes, o que exige O(n) de memória.
Devo usar um heap ou o quickselect para encontrar o k-ésimo maior elemento?
Quickselect é mais rápido em média, O(n), mas precisa de todos os valores na memória e os reordena. O heap é O(n log k), sem um pior caso ruim, e funciona quando os valores chegam um de cada vez e você não pode armazenar todos eles. Em uma entrevista, explique os dois e implemente aquele que a pergunta complementar solicitar.
O k-ésimo maior elemento pode ser encontrado em tempo linear no pior caso?
Sim. A regra da mediana das medianas escolhe um pivô que garante descartar uma fração fixa dos valores, o que torna a seleção O(n) no pior caso, embora seja mais lenta na prática do que um pivô aleatório. Como os valores estão limitados de -10^4 a 10^4, você também pode contar quantas vezes cada valor ocorre e percorrer os valores de 10^4 para baixo até ter passado por k valores, em tempo O(n + 2 × 10^4).
Problemas parecidos
Problemas que usam as mesmas ideias. Resolver dois ou três é o que fixa um padrão.
Python
def findKthLargest(nums, k):
# Escreva o código aquiCaso 1
Caso 2
Caso 3
Entrada
nums = [7, 2, 9, 4, 9, 1] k = 2
Esperado
9