In questo articolo, impareremo perché ogni programmatore dovrebbe imparare le strutture di dati e gli algoritmi con l'aiuto di esempi.
Questo articolo è per coloro che hanno appena iniziato ad apprendere algoritmi e si sono chiesti quanto sarà d'impatto aumentare le proprie capacità di carriera / programmazione. È anche per coloro che si chiedono perché grandi aziende come Google, Facebook e Amazon assumono programmatori eccezionalmente bravi nell'ottimizzazione degli algoritmi.
Cosa sono gli algoritmi?
Informalmente, un algoritmo non è altro che un accenno ai passaggi per risolvere un problema. Sono essenzialmente una soluzione.
Ad esempio, un algoritmo per risolvere il problema dei fattoriali potrebbe essere simile a questo:
Problema: trova il fattoriale di n
Inizializza fact = 1 Per ogni valore v nell'intervallo da 1 a n: Moltiplica il fatto per v fact contiene il fattoriale di n
Qui l'algoritmo è scritto in inglese. Se fosse scritto in un linguaggio di programmazione, lo chiameremmo invece codice . Ecco un codice per trovare il fattoriale di un numero in C ++.
int factorial(int n) ( int fact = 1; for (int v = 1; v <= n; v++) ( fact = fact * v; ) return fact; )
La programmazione riguarda le strutture dati e gli algoritmi. Le strutture dati vengono utilizzate per contenere i dati mentre gli algoritmi vengono utilizzati per risolvere il problema utilizzando quei dati.
Le strutture dati e gli algoritmi (DSA) analizzano dettagliatamente le soluzioni ai problemi standard e forniscono una panoramica di quanto sia efficiente utilizzare ciascuno di essi. Ti insegna anche la scienza per valutare l'efficienza di un algoritmo. Ciò consente di scegliere la migliore tra le varie scelte.
Utilizzo di strutture dati e algoritmi per rendere scalabile il codice
Il tempo è prezioso.
Supponiamo che Alice e Bob stiano cercando di risolvere un semplice problema di trovare la somma dei primi 10 11 numeri naturali. Mentre Bob scriveva l'algoritmo, Alice lo ha implementato dimostrando che è semplice come criticare Donald Trump.
Algoritmo (di Bob)
Inizializza sum = 0 per ogni numero naturale n nell'intervallo da 1 a 1011 (inclusi): aggiungi n alla somma sum è la tua risposta
Code (di Alice)
int findSum() ( int sum = 0; for (int v = 1; v <= 100000000000; v++) ( sum += v; ) return sum; )
Alice e Bob si sentono euforici di se stessi all'idea di poter costruire qualcosa di loro in pochissimo tempo. Entriamo di nascosto nel loro spazio di lavoro e ascoltiamo la loro conversazione.
Alice: eseguiamo questo codice e scopriamo la somma. Bob: Ho eseguito questo codice qualche minuto fa ma non mostra ancora l'output. Che cosa c'è che non va?
Oops! Qualcosa è andato storto! Un computer è la macchina più deterministica. Tornare indietro e provare a eseguirlo di nuovo non aiuta. Quindi analizziamo cosa c'è di sbagliato in questo semplice codice.
Due delle risorse più preziose per un programma per computer sono il tempo e la memoria .
Il tempo impiegato dal computer per eseguire il codice è:
Tempo per eseguire codice = numero di istruzioni * tempo per eseguire ciascuna istruzione
Il numero di istruzioni dipende dal codice utilizzato e il tempo necessario per eseguire ogni codice dipende dalla macchina e dal compilatore.
In questo caso, il numero totale di istruzioni eseguite (diciamo x) è , che èx = 1 + (1011 + 1) + (1011) + 1
x = 2 * 1011 + 3
Supponiamo che un computer possa eseguire istruzioni in un secondo (può variare in base alla configurazione della macchina). Il tempo necessario per eseguire il codice sopra èy = 108
Tempo impiegato per eseguire il codice = x / y (maggiore di 16 minuti)
È possibile ottimizzare l'algoritmo in modo che Alice e Bob non debbano attendere 16 minuti ogni volta che eseguono questo codice?
Sono sicuro che hai già indovinato il metodo giusto. La somma dei primi N numeri naturali è data dalla formula:
Somma = N * (N + 1) / 2
La conversione in codice avrà un aspetto simile a questo:
int sum (int N) (return N * (N + 1) / 2;)
This code executes in just one instruction and gets the task done no matter what the value is. Let it be greater than the total number of atoms in the universe. It will find the result in no time.
The time taken to solve the problem, in this case, is 1/y
(which is 10 nanoseconds). By the way, the fusion reaction of a hydrogen bomb takes 40-50 ns, which means your program will complete successfully even if someone throws a hydrogen bomb on your computer at the same time you ran your code. :)
Note: Computers take a few instructions (not 1) to compute multiplication and division. I have said 1 just for the sake of simplicity.
More on Scalability
Scalability is scale plus ability, which means the quality of an algorithm/system to handle the problem of larger size.
Consider the problem of setting up a classroom of 50 students. One of the simplest solutions is to book a room, get a blackboard, a few chalks, and the problem is solved.
But what if the size of the problem increases? What if the number of students increased to 200?
The solution still holds but it needs more resources. In this case, you will probably need a much larger room (probably a theater), a projector screen and a digital pen.
What if the number of students increased to 1000?
The solution fails or uses a lot of resources when the size of the problem increases. This means, your solution wasn't scalable.
What is a scalable solution then?
Consider a site like Khanacademy, millions of students can see videos, read answers at the same time and no more resources are required. So, the solution can solve the problems of larger size under resource crunch.
If you see our first solution to find the sum of first N natural numbers, it wasn't scalable. It's because it required linear growth in time with the linear growth in the size of the problem. Such algorithms are also known as linearly scalable algorithms.
Our second solution was very scalable and didn't require the use of any more time to solve a problem of larger size. These are known as constant-time algorithms.
Memory is expensive
Memory is not always available in abundance. While dealing with code/system which requires you to store or produce a lot of data, it is critical for your algorithm to save the usage of memory wherever possible. For example: While storing data about people, you can save memory by storing only their age not the date of birth. You can always calculate it on the fly using their age and current date.
Examples of an Algorithm's Efficiency
Here are some examples of what learning algorithms and data structures enable you to do:
Example 1: Age Group Problem
Problems like finding the people of a certain age group can easily be solved with a little modified version of the binary search algorithm (assuming that the data is sorted).
The naive algorithm which goes through all the persons one by one, and checks if it falls in the given age group is linearly scalable. Whereas, binary search claims itself to be a logarithmically scalable algorithm. This means that if the size of the problem is squared, the time taken to solve it is only doubled.
Suppose, it takes 1 second to find all the people at a certain age for a group of 1000. Then for a group of 1 million people,
- the binary search algorithm will take only 2 seconds to solve the problem
- the naive algorithm might take 1 million seconds, which is around 12 days
The same binary search algorithm is used to find the square root of a number.
Example 2: Rubik's Cube Problem
Imagine you are writing a program to find the solution of a Rubik's cube.
This cute looking puzzle has annoyingly 43,252,003,274,489,856,000 positions, and these are just positions! Imagine the number of paths one can take to reach the wrong positions.
Fortunately, the way to solve this problem can be represented by the graph data structure. There is a graph algorithm known as Dijkstra's algorithm which allows you to solve this problem in linear time. Yes, you heard it right. It means that it allows you to reach the solved position in a minimum number of states.
Example 3: DNA Problem
DNA is a molecule that carries genetic information. They are made up of smaller units which are represented by Roman characters A, C, T, and G.
Imagine yourself working in the field of bioinformatics. You are assigned the work of finding out the occurrence of a particular pattern in a DNA strand.
It is a famous problem in computer science academia. And, the simplest algorithm takes the time proportional to
(number of character in DNA strand) * (number of characters in pattern)
A typical DNA strand has millions of such units. Eh! worry not. KMP algorithm can get this done in time which is proportional to
(number of character in DNA strand) + (number of characters in pattern)
The * operator replaced by + makes a lot of change.
Considering that the pattern was of 100 characters, your algorithm is now 100 times faster. If your pattern was of 1000 characters, the KMP algorithm would be almost 1000 times faster. That is, if you were able to find the occurrence of pattern in 1 second, it will now take you just 1 ms. We can also put this in another way. Instead of matching 1 strand, you can match 1000 strands of similar length at the same time.
And there are infinite such stories…
Final Words
Generally, software development involves learning new technologies on a daily basis. You get to learn most of these technologies while using them in one of your projects. However, it is not the case with algorithms.
Se non conosci bene gli algoritmi, non sarai in grado di identificare se puoi ottimizzare il codice che stai scrivendo in questo momento. Ci si aspetta che tu le conosca in anticipo e le applichi ove possibile e in modo critico.
Abbiamo parlato in particolare della scalabilità degli algoritmi. Un sistema software è costituito da molti di questi algoritmi. L'ottimizzazione di uno qualsiasi di essi porta a un sistema migliore.
Tuttavia, è importante notare che questo non è l'unico modo per rendere scalabile un sistema. Ad esempio, una tecnica nota come calcolo distribuito consente a parti indipendenti di un programma di essere eseguite su più macchine insieme, rendendolo ancora più scalabile.