Complexități
Introducere
Atunci când elaborăm un algoritm, vrem să știm cât de rapid va fi, pentru a putea decide dacă are sens să trecem la implementarea lui propriu-zisă. Un mod de a măsura calitatea unui algoritm este dat de complexitatea pe care îl are. În cele ce urmează vom discuta acest concept și modul în care îl putem aplica în probleme.
Complexitatea unui algoritm reprezintă o măsură a eficienței acestuia în funcție de dimensiunea intrării. O bună înțelegere a complexității este esențială pentru a putea alege cel mai potrivit algoritm pentru o problemă dată.
Imaginați-vă că trebuie să căutați un cuvânt într-un dicționar. Dacă îl căutați pagină cu pagină, procesul este lent. Dacă folosiți metoda căutării binare (deschizând cartea pe la mijloc și eliminând jumătate din pagini la fiecare pas), veți ajunge mult mai repede la rezultat.
Complexitatea de timp
Pentru a calcula complexitatea de timp a unui algoritm, trebuie să avem în vedere că în practică, procesoarele moderne pot procesa aproximativ operații simple pe secundă, acest număr depinzând în funcție de contextul unde trebuie rezolvată problema (anumite site-uri sunt mai rapide decât altele și anumite evaluatoare de la concursurile oficiale sunt mai rapide decât altele).
Sfat
În concursuri, folosirea valorii de operații pe secundă este o estimare precisă, care este folosită de regulă și de propunătorii de probleme atunci când se decid limitele de timp.
Prin operație „simplă” înțelegem concret o operație care durează un timp constant, indiferent de mărimea intrării. Exemple de operații simple includ operațiile aritmetice simple, incrementările, operațiile pe biți etc., iar exemple care nu sunt simple includ aflarea radicalului, aflarea restului împărțirii etc.
În general, constantele mici pot fi ignorate în calculul complexitatilor. De exemplu, este echivalent cu pentru un . Mai jos puteți găsi exemple de cod, împreună cu complexitățile lor.
Acest cod are complexitatea , operațiile fiind constante.
int a = 5;
int b = 7;
int c = 4;
int d = a + b + c + 153;Aceste coduri au complexitatea , numărul de operații fiind cel făcut în structura repetitivă.
for (int i = 1; i <= n; i++) {
// Cod în timp constant
}int i = 0;
while (i < n) {
// Cod în timp constant
i++;
}În ciuda constantelor care apar, codurile au din nou complexitatea . Aceste coduri au complexitatea , numărul de operații fiind cel făcut în structura repetitivă.
for (int i = 1; i <= 5 * n + 17; i++) {
// Cod în timp constant
}for (int i = 1; i <= n + 758458; i++) {
// Cod în timp constant
}Dacă avem de-a face cu mai multe structuri repetitive imbricate, complexitatea se va înmulți, complexitatea codului de mai jos este .
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
// Cod în timp constant
}
}Dacă avem de-a face cu diverse repetitive imbricate în diferite blocuri de cod, complexitatea va deveni egală cu cea mai costisitoare structură de acest gen, complexitatea se va înmulți, complexitatea codului de mai jos este , în ciuda bucății care are complexitate .
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
// Cod în timp constant
}
}
for (int i = 1; i <= m; i++) {
// Cod în timp constant
}Atenție
O greșeală care se face deseori este să presupunem că o complexitate se menține pentru valori mici. Când calculăm complexitățile, ignorăm constantele pentru că vrem să analizăm algoritmul când devine din ce în ce mai mare. Ne interesează deci rata în care crește timpul de execuție, dar asta nu ne zice nimic legat de timpul concret. Pentru valori mici, nu avem voie să ignorăm constantele și alți termeni, deoarece constantele contează enorm. Din acest punct de vedere, putem vedea ca fiind cazul cel mai rău al unui algoritm. Notația nu ne zice nimic de cum rulează algoritmul în medie sau în cel mai bun caz.
De pildă, dacă avem un algoritm pentru care fiecare operație durează 50ms (am putea reprezenta asta ca ), acesta va fi mai încet decât un algoritm , unde fiecare operație durează 5ms pentru . Fiecare operație are un cost și există foarte mulți factori care pot influența cum rulează un algoritm (performanța procesorului, memoria disponibilă, cum accesează programul memoria, ce operații au loc etc.). Un algoritm va fi de 2 ori mai rapid decât unul , deși ele cresc în același fel. Deci nu vă bazați pe complexități dacă vreți să comparați concret doi algoritmi sau două structuri de date; faceți teste.
Exemple de complexități de timp
Aici prezentăm câteva exemple de complexități, care vor fi utile pe parcurs. Nu este nevoie să știți algoritmii de aici încă, ei vor fi prezentați și învățați de-a lungul parcursului vostru în lumea algoritmicii.
- Formule matematice care calculează un răspuns:
- Căutarea binară:
- Folosirea unor structuri de date precum set, map: per operație
- Aflarea divizorilor unui număr:
- Citirea sau parcurgerea a valori:
- Sortarea unui vector cu valori: de obicei
- Parcurgerea tuturor submulțimilor de lungime 2: .
- Parcurgerea tuturor submulțimilor:
- Parcurgerea tuturor permutărilor:
Complexitatea de memorie
În cazul complexității de memorie, trebuie să avem în vedere și tipul de date folosit.
Dintre cele mai frecvente tipuri de date, putem enumera următoarele:
- tipul
int: 4 octeți, limite între si ( si ). - tipul
short: 2 octeți, limite între si ( si ). - tipul
char: 1 octet, limite între si 127. - tipul
bool: 1 octet, accepta doar 0 sau 1. - tipul
long long: 8 octeți, limite între si ( si ) — numere de maxim 19 cifre.
În privința tipurilor reale, putem enumera următoarele:
- tipul
float: 4 octeți, limite între aproximativ și . - tipul
double: 8 octeți, limite între aproximativ și . - tipul
long double: în funcție de standardul de compilare, cel puțin 8 octeți, limite mai mari decât cele de ladouble.
De exemplu, dacă avem un vector de elemente de tipul int și altul de elemente de tipul long long, vom folosi octeți = 4.8 MB.
Este foarte important în cazul complexităților de memorie să aveți în vedere faptul că în general la concursuri, se ia în considerare memoria așa cum e declarată la început, și nu ce folosești pe parcurs. Astfel, este foarte important pe cât posibil să nu declarați mai multă memorie decât folosiți și să aveți grijă la câtă memorie alocați, pentru a evita și situația în care alocați mai puțin decât trebuie.
Pe scurt, e important să citiți cu atenție restricțiile din enunțurile problemelor.
Complexități acceptabile pentru diverse restricții
Acestea sunt aproximări pentru diverse clase de complexități, trebuie să aveți în vedere limita de timp și sfaturile date anterior, împreună cu particularitățile problemei.
| Complexități posibile | |
|---|---|
| , , | |
| , | |
| , , |
Concluzii
Complexitățile reprezintă o parte fundamentală din rezolvarea fiecărei probleme și înțelegerea principiilor din spatele modului în care se calculează este necesară pentru oricine dorește să aprofundeze studiul algoritmicii. Aceste informații prezentate aici vor fi esențiale pentru rezolvarea tuturor problemelor de algoritmică.