Trova un "buco" in un elenco di numeri


14

Qual è il modo più veloce per trovare il primo numero intero (più piccolo) che non esiste in un determinato elenco di numeri interi non ordinati (e che è maggiore del valore più piccolo dell'elenco)?

Il mio approccio primitivo è quello di ordinarli e di sfogliare l'elenco, c'è un modo migliore?


6
@Jodrell Penso che ordinare una progressione infinita sarebbe difficile ;-)
maple_shaft

3
@maple_shaft concordato, potrebbe richiedere del tempo.
Jodrell,

4
Come si definisce prima per un elenco non ordinato?
Jodrell,

1
Ho appena realizzato che probabilmente appartiene a StackOverflow, dal momento che non è davvero un problema concettuale.
JasonTrue,

2
@JasonTrue Dalle FAQ, If you have a question about… •algorithm and data structure conceptsè sull'argomento IMHO.
maple_shaft

Risposte:


29

Supponendo che intendi "numero intero" quando dici "numero", puoi usare un bitvector di dimensione 2 ^ n, dove n è il numero di elementi (supponi che il tuo intervallo includa numeri interi compresi tra 1 e 256, quindi puoi usare un 256- bit o 32 byte, bitvector). Quando ti imbatti in un numero intero nella posizione n del tuo intervallo, imposta l'ennesimo bit.

Quando hai finito di enumerare la raccolta di numeri interi, esegui l'iterazione sui bit nel tuo bitvector, cercando la posizione di tutti i bit impostati 0. Ora corrispondono alla posizione n dei tuoi numeri interi mancanti.

Questo è O (2 * N), quindi O (N) e probabilmente più memoria efficiente rispetto all'ordinamento dell'intero elenco.


6
Bene, come confronto diretto, se avessi tutti numeri interi a 32 bit senza segno positivi ma 1, potresti risolvere il problema dei numeri interi mancanti in circa mezzo gigabyte di memoria. Se invece ordinassi, dovresti utilizzare oltre 8 gigabyte di memoria. E l'ordinamento, tranne in casi speciali come questo (il tuo elenco viene ordinato una volta che hai un bitvector) è quasi sempre n log o peggio, quindi tranne nei casi in cui la costante supera la complessità dei costi, l'approccio lineare vince.
JasonTrue,

1
Cosa succede se non si conosce la gamma a priori?
Blrfl,

2
Se hai un tipo di dati intero, Blrfl, sicuramente conosci le estensioni massime dell'intervallo, anche se non hai abbastanza informazioni per restringere ulteriormente. Se ti capita di sapere che è un piccolo elenco, ma non conosci la dimensione esatta, l'ordinamento potrebbe essere una soluzione più semplice.
JasonTrue,

1
Oppure esegui prima un altro ciclo nell'elenco per trovare l'elemento più piccolo e più grande. Quindi è possibile allocare un array di dimensioni esatte con il valore più piccolo come offset di base. Ancora O (N).
Sicuro il

1
@JPatrick: non i compiti, gli affari, mi sono laureato CS anni fa :).
Fabian Zeindl,

4

Se si ordina prima l'intero elenco, si garantisce il tempo di esecuzione nel caso peggiore. Inoltre, la scelta dell'algoritmo di ordinamento è fondamentale.

Ecco come affrontare questo problema:

  1. Utilizzare un ordinamento heap , concentrandosi sugli elementi più piccoli nell'elenco.
  2. Dopo ogni scambio, vedi se hai un gap.
  3. Se trovi un vuoto, allora return: hai trovato la tua risposta.
  4. Se non trovi un divario, continua a scambiare.

Ecco una visualizzazione di un tipo di heap .


Una domanda, come si identificano gli elementi "più piccoli" dell'elenco?
Jodrell,

4

Solo per essere esoterici e "intelligenti", nel caso speciale dell'array con un solo "buco", puoi provare una soluzione basata su XOR:

  • Determinare l'intervallo dell'array; questo viene fatto impostando una variabile "max" e "min" sul primo elemento dell'array, e per ogni elemento successivo, se quell'elemento è inferiore al minimo o maggiore del massimo, imposta il minimo o il massimo sul valore nuovo valore.
  • Se l'intervallo è uno in meno rispetto alla cardinalità del set, esiste solo un "buco" per poter usare XOR.
  • Inizializza una variabile intera da X a zero.
  • Per ogni numero intero compreso tra min e max inclusivamente, XOR quel valore con X e memorizza il risultato in X.
  • Ora XOR ogni numero intero nella matrice con X, memorizzando ogni risultato successivo su X come prima.
  • Al termine, X sarà il valore del tuo "buco".

Questo verrà eseguito in circa 2N tempo simile alla soluzione bitvector, ma richiede meno spazio di memoria per qualsiasi dimensione N> di (int). Tuttavia, se l'array ha più "fori", X sarà la "somma" XOR di tutti i fori, che sarà difficile o impossibile separare nei valori effettivi dei fori. In tal caso si ritorna ad altri metodi come gli approcci "pivot" o "bitvector" da altre risposte.

Potresti ricorrere anche a questo usando qualcosa di simile al metodo pivot per ridurre ulteriormente la complessità. Riorganizza l'array in base a un punto di rotazione (che sarà il massimo del lato sinistro e il minimo di destra; sarà banale trovare il massimo e il minimo dell'intero array durante il pivot). Se il lato sinistro del perno presenta uno o più fori, ricorrere solo in quel lato; altrimenti ricorrere nell'altra parte. In qualsiasi punto in cui è possibile determinare che esiste un solo foro, utilizzare il metodo XOR per trovarlo (che dovrebbe essere complessivamente più economico rispetto al continuare a ruotare fino a una raccolta di due elementi con un foro noto, che è il caso base per l'algoritmo pivot puro).


È ridicolmente intelligente e fantastico! Ora puoi trovare un modo per farlo con un numero variabile di fori? :-D

2

Qual è l'intervallo di numeri che incontrerai? Se l'intervallo non è molto ampio, potresti risolverlo con due scansioni (tempo lineare O (n)) utilizzando un array con tanti elementi quanti sono i tuoi numeri, scambiando spazio per il tempo. È possibile trovare l'intervallo in modo dinamico con un'altra scansione. Per ridurre lo spazio, è possibile assegnare 1 bit a ciascun numero, ottenendo 8 numeri di memoria per byte.

L'altra opzione che potrebbe essere migliore per i primi scenari e che sarebbe possibile inserire invece di copiare la memoria è quella di modificare l'ordinamento di selezione per uscire anticipatamente se il minimo trovato in un passaggio di scansione non è 1 in più rispetto all'ultimo minuto trovato.


1

No, non proprio. Dato che qualsiasi numero non ancora scansionato potrebbe sempre essere uno che riempie un determinato "buco", non puoi evitare di scansionare ogni numero almeno una volta e poi confrontarlo con i possibili vicini. Probabilmente potresti accelerare le cose costruendo un albero binario o giù di lì e poi attraversandolo da sinistra a destra fino a trovare un buco, ma è essenzialmente della stessa complessità temporale dell'ordinamento, poiché è l'ordinamento. E probabilmente non ti verrà in mente nulla di più veloce di Timsort .


1
Stai dicendo che attraversare un elenco è la stessa complessità temporale dell'ordinamento?
maple_shaft

@maple_shaft: No, sto dicendo che costruire un albero binario da dati casuali e poi attraversarlo da sinistra a destra equivale all'ordinamento e quindi passare da piccolo a grande.
pillmuncher

1

La maggior parte delle idee qui non sono altro che un semplice ordinamento. La versione bitvector è semplice Bucketsort. È stato anche menzionato l'ordinamento dell'heap. Fondamentalmente si riduce a scegliere il giusto algoritmo di ordinamento che dipende dai requisiti di tempo / spazio e anche dalla gamma e dal numero di elementi.

Dal mio punto di vista, l'utilizzo di una struttura heap è probabilmente la soluzione più generale (un heap sostanzialmente ti dà gli elementi più piccoli in modo efficiente senza un ordinamento completo).

Potresti anche analizzare approcci che trovano prima i numeri più piccoli e quindi scansionano per ogni numero intero più grande di quello. Oppure trovi i 5 numeri più piccoli sperando che possa avere un divario.

Tutti questi algoritmi hanno la loro forza a seconda delle caratteristiche di input e dei requisiti del programma.


0

Una soluzione che non utilizza memoria aggiuntiva né assume la larghezza (32 bit) di numeri interi.

  1. In un passaggio lineare trova il numero più piccolo. Chiamiamo questo "min". O (n) complessità temporale.

  2. Scegli un elemento pivot casuale ed esegui una partizione di stile quicksort.

  3. Se il pivot è finito nella posizione = ("pivot" - "min"), quindi ricorrere sul lato destro della partizione, altrimenti ricorrere sul lato sinistro della partizione. L'idea qui è che se non ci sono buchi dall'inizio, il perno sarebbe in posizione ("perno" - "min"), quindi il primo foro dovrebbe trovarsi a destra della partizione e viceversa.

  4. Il caso base è un array di 1 elemento e il foro si trova tra questo elemento e quello successivo.

La complessità del tempo di esecuzione totale prevista è O (n) (8 * n con le costanti) e il caso peggiore è O (n ^ 2). L'analisi della complessità temporale per un problema simile è disponibile qui .


0

Credo di aver escogitato qualcosa che dovrebbe funzionare in modo generale ed efficiente se si ha la certezza di non avere duplicati * (tuttavia, dovrebbe essere estendibile a qualsiasi numero di fori e qualsiasi intervallo di numeri interi).

L'idea alla base di questo metodo è come quicksort, in quanto troviamo un perno e una partizione attorno ad esso, quindi ricorrere sui lati con un buco. Per vedere quali lati hanno il buco, troviamo i numeri più bassi e più alti e li confrontiamo con il perno e il numero di valori su quel lato. Supponi che il perno sia 17 e il numero minimo sia 11. Se non ci sono buchi, dovrebbero esserci 6 numeri (11, 12, 13, 14, 15, 16, 17). Se ce ne sono 5, sappiamo che c'è un buco su quel lato e possiamo ricorrere su quel lato per trovarlo. Ho difficoltà a spiegarlo più chiaramente di così, quindi facciamo un esempio.

15 21 10 13 18 16 22 23 24 20 17 11 25 12 14

Perno:

10 13 11 12 14 |15| 21 18 16 22 23 24 20 17 25

15 è il perno, indicato da pipe ( ||). Ci sono 5 numeri sul lato sinistro del perno, come dovrebbero esserci (15 - 10), e 9 a destra, dove dovrebbero esserci 10 (25 - 15). Quindi ricerchiamo sul lato destro; noteremo che il limite precedente era 15 nel caso in cui il buco sia adiacente ad esso (16).

[15] 18 16 17 20 |21| 22 23 24 25

Ora ci sono 4 numeri sul lato sinistro, ma dovrebbero essercene 5 (21-16). Quindi ricontattiamo lì e di nuovo noteremo il limite precedente (tra parentesi).

[15] 16 17 |18| 20 [21]

La parte sinistra ha i 2 numeri corretti (18 - 16), ma la destra ha 1 invece di 2 (20 - 18). A seconda delle nostre condizioni finali, potremmo confrontare il numero 1 con i due lati (18, 20) e vedere che manca 19 o ricorrere ancora una volta:

[18] |20| [21]

Il lato sinistro ha una dimensione pari a zero, con uno spazio tra il perno (20) e il limite precedente (18), quindi 19 è il buco.

*: Se ci sono duplicati, probabilmente potresti usare un set di hash per rimuoverli nel tempo O (N), mantenendo il metodo generale O (N), ma ciò potrebbe richiedere più tempo rispetto all'uso di qualche altro metodo.


1
Non credo che l'OP abbia detto nulla sull'essere solo un buco. L'input è un elenco non numerato di numeri: potrebbero essere qualsiasi cosa. Dalla tua descrizione non è chiaro come determineresti quanti numeri ci dovrebbero "essere".
Caleb,

@caleb Non importa quanti buchi ci siano, solo nessun duplicato (che può essere rimosso in O (N) con un set di hash, anche se in pratica potrebbe avere più overhead rispetto ad altri metodi). Ho provato a migliorare la descrizione, vedere se è meglio.
Kevin,

Questo non è lineare, IMO. È più simile a (logN) ^ 2. Ad ogni passaggio, fai perno sul sottoinsieme della raccolta che ti interessa (la metà del sottoarray precedente che hai identificato con il primo "buco"), quindi ricicla nella parte sinistra se ha un "buco", o il lato destro se il lato sinistro no. (logN) ^ 2 è ancora meglio di linear; se N aumenta di dieci volte, prendi solo l'ordine di 2 (log (N) -1) + 1 altro passo.
KeithS

@Keith - sfortunatamente, devi guardare tutti i numeri di ogni livello per ruotarli, quindi ci vorranno circa n + n / 2 + n / 4 + ... = 2n (tecnicamente, 2 (nm)) confronti .
Kevin,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.