Quindi eccoti. Sollevato. Esausto. Finalmente hai trovato un approccio per risolvere la delicata domanda di programmazione che il tuo intervistatore ti pone. Forse l'hai persino scritto sulla lavagna, riga per riga. E ti sei divertito! Ti mancano solo 20 minuti alla riunione. Il tuo intervistatore deve essere colpito.
Destra?
"Funzionerà, ma hai idee su come farlo in modo più efficiente?"
Il tuo cuore affonda. Pensavi di aver finito con la parte complicata del design dell'algoritmo! Cerchi di pensare a più modi per risolvere il problema, ma tutto ciò che riesci a pensare è l'unico approccio che hai già escogitato.
Questo succede a quasi tutti. E non è perché sono stupidi. È perché la maggior parte delle persone non ha un metodo per migliorare l'efficienza dei propri algoritmi.
Ma la verità è che ce ne sono molti. La prossima volta che sei perplesso, prova ad applicare questi tre approcci comuni.
1. Utilizzare una mappa hash
Giusto. Mappe hash / matrici / dizionari associativi (prendono molti nomi, a seconda del linguaggio di programmazione che stai usando) hanno una magica capacità di ridurre il tempo di esecuzione degli algoritmi.
Ad esempio, supponiamo che la domanda fosse trovare il numero più ripetuto in una matrice di numeri.
Il tuo primo pensiero potrebbe essere quello di saltare in alcuni loop. Per ciascuno dei nostri numeri, capire il suo conteggio e vedere se è il più grande. Come otteniamo il conteggio per ciascun numero? Passa attraverso l'array, contando quante volte si verifica! Quindi stiamo parlando di due loop nidificati. In pseudocodice:
def get_mode (nums): max_count = 0 mode = null per potenziale_mode in nums: count = 0 per numero in our_array: count + = 1 if count> = max_count: mode = potenziale_mode max_count = count return mode
In questo momento, stiamo eseguendo il ciclo dell'intero array una volta per ogni elemento dell'array, ma possiamo fare di meglio. In notazione O grande, questo è il tempo O (n 2 ) in totale.
Se memorizziamo i nostri conteggi in una mappa hash (mappando i numeri ai loro conteggi), possiamo risolvere il problema in una sola passeggiata nell'array (O (n) time!):
def get_mode (nums): max_count = 0 mode = null conte = new HashMap, iniziando ogni valore su 0 per potenziale_mode in nums: conteggi + = 1 se conta> max_count: mode = potenziale_mode max_count = conta modalità di ritorno
Più veloce!
2. Utilizzare la manipolazione dei bit
Questo ti distinguerà davvero dal pacchetto. Non si applica a tutti i problemi, ma se lo tieni nella tasca posteriore e lo rompi al momento giusto, sembrerai una rockstar.
Ecco un esempio: supponiamo di avere una matrice di numeri, in cui ogni numero appare due volte, ad eccezione di un numero che si verifica solo una volta. Stiamo scrivendo una funzione per trovare il numero solitario, non ripetuto.
Il tuo primo istinto potrebbe essere quello di utilizzare una mappa hash, poiché ne abbiamo appena parlato. È un buon istinto da avere! E funzionerà per questo. Possiamo creare una mappa dei "conteggi" molto simile e usarla per vedere quale numero finisce con un conteggio di 1.
Ma c'è un modo ancora migliore. Se hai familiarità con la manipolazione dei bit, potresti avere familiarità con XOR. Una cosa speciale di XOR è che se si XOR un numero con se stesso, i bit si "annullano" su 0. Per questo problema, se XOR ogni numero nell'array insieme, rimarremo con un numero che non ha non cancellare:
def find_unrepeated (nums): non ripetuto = 0 per num in nums: non ripetuto = non ripetuto XOR num return non ripetuto
3. Vai dal basso verso l'alto
Scrivi una funzione che emette il numero "nth" di Fibonacci, dato un numero n. Questo è un classico e si presta molto bene alla ricorsione:
def fib (n): se n è 0 o 1: restituisce 1 restituisce fib (n-1) + fib (n-2)
Ma la semplice risposta ricorsiva non è l'unica! Pensa attentamente a cosa fa questa funzione. Supponiamo che n sia 5. Per ottenere la risposta, chiama ricorsivamente fib (4) e fib (3). Cosa fa questa chiamata a fib (4)? Chiama fib (3) e fib (2). Ma abbiamo appena detto che avevamo già una chiamata a fib (3)! Questa simpatica funzione ricorsiva fa molto lavoro ripetitivo. Il costo del tempo totale risulta essere O (2 n ). È cattivo, molto peggio di O (n 2 ).
Invece di andare da n ricorsivamente verso il basso a 1, andiamo "dal basso verso l'alto", da 1 a n. Questo ci consente di saltare la ricorsione:
def fib (n): precedente = 0 previous_previous = 1 per i nell'intervallo da 1 a n: current = precedente + precedente_previous precedente_previous = precedente precedente = corrente di ritorno corrente
Il codice è più lungo, ma è molto più efficiente! Fino a O (n) tempo. Come bonus aggiuntivo con lo svolgimento di algoritmi ricorsivi, risparmiamo spazio. Tutte quelle chiamate ricorsive si accumulano nello stack di chiamate, che si trova in memoria e conta per il nostro costo dello spazio. La nostra funzione ricorsiva aveva un costo spaziale O (n), ma questa iterativa occupa spazio O (1).
La prossima volta che il tuo intervistatore ti chiede di migliorare l'efficienza della tua soluzione, prova a seguire queste strategie e vedere se aiutano. Con abbastanza pratica, probabilmente ti ritroverai a saltare direttamente alla soluzione ottimizzata, saltando la soluzione più ingenua. E questa è un'ottima cosa. Non significa solo che stai diventando un intervistatore migliore, ma che stai diventando un ingegnere migliore.