Top K Frequent Elements
Ti viene fornito un array di numeri interi nums e un numero intero k. Restituisci i k valori che compaiono più spesso in nums, in ordine decrescente di frequenza. Se due valori compaiono lo stesso numero di volte, viene prima il valore più piccolo.
Ogni valore compare una sola volta nella risposta, indipendentemente da quante volte compare in nums, e k non è mai maggiore del numero di valori distinti.
Funzione
- numsinteger-array
- i valori da contare
- kinteger
- quanti valori restituire
- Restituisceinteger-array
- i k valori più frequenti, in ordine decrescente di frequenza; a parità di frequenza, prima il valore più piccolo
Vincoli
1 ≤ nums.length ≤ 104-104 ≤ nums[i] ≤ 1041 ≤ kekè al massimo il numero di valori distinti innums.
Esempi
- Input
- nums = [4, 1, 4, 2, 1, 4, 3, 1, 4]k = 2
- Output
- [4, 1]
- Spiegazione
4compare quattro volte,1tre volte e2e3una volta ciascuno. I due valori più frequenti sono4, seguito da1.
- Input
- nums = [5, -2, 7, -2, 7, 5, 9]k = 2
- Output
- [-2, 5]
- Spiegazione
-2,5e7compaiono ciascuno due volte e9una volta. Tre valori sono a pari merito al primo posto, quindi i due più piccoli,-2e5, sono la risposta.
- Input
- nums = [8]k = 1
- Output
- [8]
- Spiegazione
- C'è un solo valore, quindi è il più frequente.
+16 test nascosti all’invio
Suggerimenti
Aprili uno alla volta. Ognuno rivela un po’ di più.
Inizia scoprendo con quale frequenza compare ciascun valore. Quale struttura dati associa un valore al suo conteggio in un solo passaggio?
Con i conteggi a disposizione, vuoi i
kvalori migliori secondo un ordinamento: prima quelli con conteggio più alto, e a parità di conteggio il valore più piccolo. Ordinare ogni valore distinto funziona. Un min-heap di dimensionekconserva solo i valori che possono ancora far parte della risposta.Un conteggio è un numero intero da 1 a
n. Crea un bucket per ogni conteggio: il bucketccontiene i valori che compaiono esattamentecvolte; poi leggi i bucket dal conteggio più alto al più basso. Riempi i bucket scorrendo i valori dal più piccolo al più grande: ogni bucket sarà già nell’ordine corretto in caso di parità.
Soluzione
Il conteggio è la parte più veloce: una sola passata con una mappa hash fornisce il conteggio di ogni valore. La vera domanda è come scegliere i k valori migliori senza fare più lavoro del necessario. Ordinare tutti gli d valori distinti in base al conteggio costa O(d log d), un min-heap di dimensione k riduce il costo a O(d log k) e, poiché un conteggio è un numero intero compreso tra 1 e n, un ordinamento per bucket ordina i valori in base al conteggio senza effettuare alcun confronto.
Conta, poi ordina per conteggio
Intuizione
Prima conta. Un solo passaggio con una mappa hash dai valori ai conteggi trasforma [4, 1, 4, 2, 1, 4, 3, 1, 4] in 4 → 4, 1 → 3, 2 → 1, 3 → 1.
Poi disponi i valori distinti nell’ordine della risposta: prima il conteggio più alto e, a parità di conteggio, prima il valore più piccolo. Fornisci all’ordinamento esattamente questo criterio di confronto, usando il conteggio come prima chiave e il valore come seconda; le prime k voci dell’elenco ordinato sono la risposta. In questo caso l’ordine è 4, 1, 2, 3 e k = 2 mantiene 4 e 1.
Il conteggio costa O(n). Ordinare i d valori distinti costa O(d log d), al massimo O(n log n) quando tutti i valori sono diversi: 10^4 valori richiedono circa 1.3 × 10^5 confronti, un’operazione rapida. Lo svantaggio è che l’ordinamento ordina tutti i valori, mentre ne servono solo i primi k.
Algoritmo
- Conta ogni valore in una mappa hash.
- Inserisci i valori distinti in un elenco.
- Ordina l'elenco per conteggio dal più alto al più basso e, quando i conteggi sono uguali, per valore dal più basso al più alto.
- Restituisci i primi
kvalori.
from collections import Counter
def topKFrequent(nums, k):
counts = Counter(nums)
# Most frequent first; equal counts put the smaller value first.
ordered = sorted(counts, key=lambda value: (-counts[value], value))
return ordered[:k]Mantieni i k migliori in un min-heap
Intuizione
Ti servono solo i k valori migliori, quindi conserva solo k candidati. Per ogni nuovo valore, la domanda è se supera il candidato più debole che conservi, dove «più debole» significa con un conteggio inferiore, oppure con lo stesso conteggio e un valore maggiore. Un min-heap ordinato secondo questa regola mantiene in cima il candidato più debole, che puoi leggere in O(1) e sostituire in O(log k).
Esamina i valori distinti. Finché l’heap contiene meno di k valori, aggiungi il valore. In seguito, un valore che supera quello in cima lo sostituisce, mentre un valore che non lo supera viene scartato, perché sono già conservati k valori migliori. Con un heap di libreria, è più breve inserire ogni valore ed estrarne uno ogni volta che l’heap supera k, mantenendo così gli stessi k valori.
Alla fine l’heap contiene la risposta, ma non nell’ordine desiderato: un heap è ordinato solo parzialmente. L’estrazione restituisce prima il valore più debole, quindi scrivi la risposta dalla posizione finale a quella iniziale.
Ognuno degli d valori distinti richiede al massimo un’operazione sull’heap con k elementi, quindi la selezione richiede O(d log k). È più efficiente dell’ordinamento quando k è molto più piccolo di d, per esempio per trovare i primi 10 tra 8000 valori distinti.
Algoritmo
- Conta ogni valore in una mappa hash.
- Per ogni valore distinto, inseriscilo finché l'heap contiene meno di
kvalori. - Quando l'heap è pieno, confronta il valore con quello in cima, il valore più debole tra quelli mantenuti. Se il nuovo valore è più forte, inseriscilo in cima e fallo scendere nell'heap.
- Estrai dall'heap
kvolte, scrivendo ogni valore nella risposta dalla posizione finale alla prima.
import heapq
from collections import Counter
def topKFrequent(nums, k):
counts = Counter(nums)
# Entries are (count, -value). heapq keeps the smallest entry on top, which is
# the weakest value kept: the lowest count, and on a tie the larger value.
heap = []
for value, count in counts.items():
heapq.heappush(heap, (count, -value))
if len(heap) > k:
heapq.heappop(heap)
# Pops come out weakest first, so fill the answer from the back.
result = [0] * k
for i in range(k - 1, -1, -1):
result[i] = -heapq.heappop(heap)[1]
return resultConta, poi ordina in bucket in base al conteggio
Intuizione
Un conteggio non è un numero qualsiasi: è un numero intero compreso tra 1 e n. Questo permette di usare il bucket sort. Crea un bucket per ogni conteggio: il bucket c contiene i valori che compaiono esattamente c volte; poi leggi i bucket dal bucket n verso il basso. I valori escono in ordine di frequenza decrescente e non si confrontano mai due conteggi.
La regola per i pareggi richiede un'altra cosa: all'interno di un bucket, il valore più piccolo deve venire prima. I valori sono compresi tra -10^4 e 10^4, quindi un array di R = 2 × 10^4 + 1 contatori può eseguire il conteggio, con il valore v all'indice v + 10^4. Scorri l'array dal valore più piccolo al più grande e aggiungi ogni valore al bucket corrispondente al suo conteggio. Ogni bucket si riempie in ordine crescente, che è l'ordine richiesto per i pareggi, quindi non serve mai ordinare.
Per [5, -2, 7, -2, 7, 5, 9], la scansione inserisce -2, 5, 7 nel bucket 2, in quest'ordine, e 9 nel bucket 1. Leggendo a ritroso dal bucket 7, il primo bucket non vuoto è il bucket 2 e k = 2 prende -2 e 5.
Il lavoro consiste in un passaggio su nums, un passaggio sui R contatori e un passaggio sui bucket, per un totale di O(n + R): lineare per un intervallo fisso di valori. Con una mappa hash al posto dell'array di conteggio, il conteggio resta lineare, ma i bucket si riempiono nell'ordine della mappa e dovresti ordinare ciascuno per rispettare la regola dei pareggi.
Algoritmo
- Conta ogni valore in un array indicizzato da
value + 10^4. - Crea i bucket da 1 a
n, una lista per ogni possibile conteggio. - Scorri l’array dei conteggi dal valore più piccolo a quello più grande e aggiungi ogni valore presente al bucket del suo conteggio.
- Leggi i bucket dal conteggio
nfino a 1, prendendo i valori finché non haik.
def topKFrequent(nums, k):
OFFSET = 10000 # values run from -10^4 to 10^4
counts = [0] * (2 * OFFSET + 1)
for x in nums:
counts[x + OFFSET] += 1
# buckets[c] lists the values that occur exactly c times. Walking the
# values from smallest to largest fills every bucket in ascending order.
buckets = [[] for _ in range(len(nums) + 1)]
for i, c in enumerate(counts):
if c > 0:
buckets[c].append(i - OFFSET)
# Read the buckets from the highest count down until k values are taken.
result = []
for c in range(len(nums), 0, -1):
for value in buckets[c]:
result.append(value)
if len(result) == k:
return result
return result
Trappole e casi limite
Il conteggio raramente è sbagliato. È l'ordine della risposta a esserlo.
- Risolvere le parità in base alla prima occorrenza o all'ordine della mappa hash. Nel secondo esempio,
-2,5e7compaiono tutti due volte, e solo la regola del valore più piccolo rende[-2, 5]l'unica risposta corretta. - Restituire l'array dell'heap così com'è. Un heap è ordinato solo parzialmente e il suo elemento in cima è il valore più debole, quello che deve venire per ultimo.
- Invertire la regola di spareggio dell'heap. Tra due valori con lo stesso conteggio, quello più grande è più debole, quindi un min-heap su
(count, value)elimina quello sbagliato. Usa(count, -value)oppure un confronto scritto appositamente per la regola. - Creare solo tanti bucket quanti sono i valori distinti. Un valore può comparire
nvolte, come in[3, 3, 3, 3], quindi deve esistere il bucketn. - In Java, confrontare due conteggi
Integercon!=. Questo confronta i riferimenti e causa problemi quando i conteggi superano 127. Prima convertili inintrimuovendo il boxing. - Prendere un intero bucket alla fine. Fermati non appena hai
kvalori, anche a metà di un bucket.
Domande frequenti4
Qual è la complessità temporale di Top K Frequent Elements?
Il conteggio richiede O(n). Selezionare i k valori più alti richiede quindi O(d log d) con un ordinamento dei d valori distinti, O(d log k) con un min-heap di dimensione k e O(n) più un passaggio sull’intervallo dei valori con l’ordinamento per distribuzione. Poiché d può raggiungere n, nel caso peggiore l’ordinamento richiede O(n log n), mentre l’ordinamento per distribuzione è lineare.
È possibile risolvere il problema dei Top K Frequent Elements in tempo O(n)?
Sì, con l’ordinamento per bucket. I conteggi sono numeri interi da 1 a n, quindi ogni valore va nel bucket corrispondente al suo conteggio e, leggendo i bucket dal conteggio più alto a quello più basso, si elencano i valori in base alla frequenza senza alcun ordinamento per confronti. Quickselect sui conteggi è anch’esso O(n) in media, ma nel caso peggiore ha complessità quadratica.
Perché usare un min-heap e non un max-heap?
Va bene anche un max-heap di tutti i valori d: crealo in O(d) ed estrai un elemento k volte, per un totale di O(d + k log d). Un min-heap di dimensione k contiene solo k elementi ed è adatto ai valori che arrivano uno alla volta, perché il suo elemento in cima è quello da eliminare. Lo svantaggio è che restituisce la risposta al contrario, quindi riempi il risultato partendo dalla fine.
Come si risolvono le parità in Top K Frequent Elements?
Scegli una regola e applicala ovunque; qui, a parità di conteggio, si mette prima il valore più piccolo, così la risposta è univoca. In un ordinamento, confronta i conteggi e poi i valori. In un heap, tra due conteggi uguali, il valore più grande è quello più debole. In un ordinamento per distribuzione, riempi i bucket in ordine crescente di valore, e ogni bucket è già ordinato per risolvere le parità.
Problemi simili
Problemi che usano le stesse idee. Risolverne due o tre è ciò che fissa uno schema.
Python
def topKFrequent(nums, k):
# Scrivi il codice quiCaso 1
Caso 2
Caso 3
Input
nums = [4, 1, 4, 2, 1, 4, 3, 1, 4] k = 2
Atteso
[4, 1]