Μετάβαση στο περιεχόμενο

Πολυπλοκότητα αλγορίθμων

Πόσο μεγαλώνει ο χρόνος όταν μεγαλώνουν τα δεδομένα: log n, n, n².

45 λεπτά

Θα μάθεις να

  1. Να μετράς τα βασικά βήματα ενός αλγορίθμου ως συνάρτηση του μεγέθους της εισόδου.
  2. Να απλοποιείς ένα πλήθος βημάτων στον συμβολισμό O, κρατώντας τον κυρίαρχο όρο.
  3. Να συγκρίνεις τις τάξεις O(1), O(log n), O(n) και O(n²) για μεγάλα n.
  4. Να προβλέπεις πώς αλλάζει ο χρόνος εκτέλεσης όταν πολλαπλασιάζεται το μέγεθος της εισόδου.

Γιατί δεν μετράμε δευτερόλεπτα

Ένα πρόβλημα μπορεί να είναι επιλύσιμο και να είναι πρακτικά άχρηστο: αν η λύση του χρειάζεται περισσότερο χρόνο από την ηλικία του σύμπαντος, η ύπαρξή της δεν βοηθά κανέναν. Η πολυπλοκότητα (complexity) μετρά πόσους πόρους χρειάζεται ένας αλγόριθμος, κυρίως χρόνο και μνήμη.

Τα δευτερόλεπτα δεν είναι καλή μονάδα. Το ίδιο πρόγραμμα τρέχει σε ένα παλιό λάπτοπ δέκα φορές πιο αργά απ' ό,τι σε έναν καινούργιο εξυπηρετητή, και ο αλγόριθμος δεν άλλαξε. Γι' αυτό μετράμε βασικά βήματα (συγκρίσεις, προσθέσεις, αναθέσεις) ως συνάρτηση του μεγέθους της εισόδου, που το συμβολίζουμε nn.

Μέτρηση βημάτων

Τρεις αλγόριθμοι πάνω σε μια λίστα nn στοιχείων:

def first(items):
    return items[0]  # ένα βήμα, όσο μεγάλη κι αν είναι η λίστα


def total(items):
    s = 0
    for x in items:  # n επαναλήψεις, μία πρόσθεση η καθεμία
        s += x
    return s


def count_pairs(n):
    steps = 0
    for i in range(n):
        for j in range(i + 1, n):  # κάθε ζεύγος συγκρίνεται μία φορά
            steps += 1
    return steps


for n in [10, 100, 1000]:
    print(n, count_pairs(n))

Το πρόγραμμα τυπώνει 10 45, 100 4950 και 1000 499500. Ο αριθμός των ζευγών είναι ακριβώς n(n−1)2\frac{n(n-1)}{2}: για δέκα φορές περισσότερα στοιχεία, περίπου εκατό φορές περισσότερα βήματα.

Ο συμβολισμός O

Δεν μας ενδιαφέρει ο ακριβής αριθμός βημάτων, αλλά πώς μεγαλώνει όταν μεγαλώνει το nn. Ο συμβολισμός OO («κεφαλαίο όμικρον», big-O) κρατά μόνο αυτό, με δύο κανόνες:

  1. Κρατάμε τον όρο που μεγαλώνει γρηγορότερα και πετάμε τους υπόλοιπους.
  2. Πετάμε τους σταθερούς συντελεστές.
n(n−1)2=12n2−12n⟶O(n2)\frac{n(n-1)}{2} = \frac{1}{2}n^2 - \frac{1}{2}n \quad \longrightarrow \quad O(n^2)

Για n=106n = 10^6, ο όρος 12n2\frac{1}{2}n^2 είναι 5⋅10115 \cdot 10^{11} και ο 12n\frac{1}{2}n μόλις 5⋅1055 \cdot 10^5. Ο δεύτερος δεν αλλάζει την εικόνα, γι' αυτό τον πετάμε. Ο συντελεστής 12\frac{1}{2} εξαρτάται από τις λεπτομέρειες υλοποίησης και τη μηχανή, γι' αυτό τον πετάμε κι εκείνον.

Οι βασικές τάξεις

ΤάξηΌνομαΤυπικό παράδειγμαn=10n = 10n=1000n = 1000n=106n = 10^6
O(1)O(1)σταθερήπρόσβαση σε στοιχείο πίνακα με τη θέση του111111
O(log⁡n)O(\log n)λογαριθμικήδυαδική αναζήτηση σε ταξινομημένο πίνακα≈3\approx 3≈10\approx 10≈20\approx 20
O(n)O(n)γραμμικήσειριακή αναζήτηση, άθροισμα στοιχείων10101000100010610^6
O(nlog⁡n)O(n \log n)—γρήγορες ταξινομήσεις, π.χ. με συγχώνευση≈33\approx 33≈104\approx 10^4≈2⋅107\approx 2 \cdot 10^7
O(n2)O(n^2)τετραγωνικήταξινόμηση φυσαλίδας, σύγκριση όλων των ζευγών10010010610^6101210^{12}
O(2n)O(2^n)εκθετικήδοκιμή όλων των υποσυνόλων10241024≈10301\approx 10^{301}αριθμός με 301 030301\,030 ψηφία

Ο λογάριθμος εδώ είναι με βάση 22: log⁡2n\log_2 n είναι το πόσες φορές μπορείς να κόψεις το nn στη μέση μέχρι να μείνει 11. Αφού 210=10242^{10} = 1024, είναι log⁡21000≈10\log_2 1000 \approx 10, και αφού 220≈1062^{20} \approx 10^6, είναι log⁡2106≈20\log_2 10^6 \approx 20.

Σε έναν υπολογιστή που κάνει 10910^9 βήματα το δευτερόλεπτο, για n=106n = 10^6: ο γραμμικός αλγόριθμος τελειώνει σε ένα χιλιοστό του δευτερολέπτου, ο τετραγωνικός σε 10001000 δευτερόλεπτα, δηλαδή περίπου 1717 λεπτά. Ο εκθετικός δεν τελειώνει ποτέ με οποιαδήποτε πρακτική έννοια.

Γραμμική εναντίον λογαριθμικής

Ανέβασε το μέγεθος από 100 σε 1000. Η σειριακή αναζήτηση χρειάζεται στη χειρότερη περίπτωση δέκα φορές περισσότερες συγκρίσεις· η δυαδική μόλις τρεις παραπάνω.

Η δυαδική αναζήτηση προϋποθέτει ταξινομημένο πίνακα. Αν πρέπει πρώτα να ταξινομήσεις, πληρώνεις και το κόστος της ταξινόμησης.

Συγκρίσεις για αυτή τη θέση

Σειριακή, O(n)73

χειρότερη περίπτωση: 100

Δυαδική, O(log n)6

χειρότερη περίπτωση: 7

Και η μνήμη

Με τον ίδιο συμβολισμό μετράμε και τη χωρική πολυπλοκότητα, δηλαδή πόση επιπλέον μνήμη χρειάζεται ένας αλγόριθμος. Η συνάρτηση total χρησιμοποιεί μία μεταβλητή όσο μεγάλη κι αν είναι η λίστα, άρα O(1)O(1) χώρο. Ένας αλγόριθμος που φτιάχνει αντίγραφο της λίστας χρειάζεται O(n)O(n) χώρο. Συχνά κερδίζεις χρόνο ξοδεύοντας μνήμη, και το ανάποδο.

Λυμένα παραδείγματα

Παράδειγμα 1

Ένας αλγόριθμος κάνει 3n2+50n+73n^2 + 50n + 7 βήματα. Ποια είναι η τάξη του;

Δοκίμασέ το πρώτα. Μετά δες τη λύση.

Παράδειγμα 2

Ένα πρόγραμμα O(n2)O(n^2) χρειάζεται 22 δευτερόλεπτα για n=1000n = 1000. Πόσο χρειάζεται για n=4000n = 4000; Πόσο θα χρειαζόταν αν ήταν O(n)O(n);

Δοκίμασέ το πρώτα. Μετά δες τη λύση.

Παράδειγμα 3

Πόσες συγκρίσεις χρειάζεται στη χειρότερη περίπτωση η δυαδική αναζήτηση σε ταξινομημένο πίνακα ενός εκατομμυρίου στοιχείων;

Δοκίμασέ το πρώτα. Μετά δες τη λύση.

Παράδειγμα 4

Ποια είναι η τάξη του βρόχου: «όσο το n είναι μεγαλύτερο του 1, κάνε n = n // 2 και μέτρα ένα βήμα»;

Δοκίμασέ το πρώτα. Μετά δες τη λύση.

Ασκήσεις

Πρώτα ερωτήσεις με επιλογές: διαλέγεις, πατάς «Έλεγξε» και μαθαίνεις αμέσως αν το βρήκες και γιατί. Στο τέλος, λίγες ασκήσεις χωρίς επιλογές — εκεί κρύβεται μόνο η απάντηση.

Πολλαπλή επιλογήΆσκηση 1 από 14
Γιατί η πολυπλοκότητα μετριέται σε βήματα και όχι σε δευτερόλεπτα;

Τα κατάφερες;

  • Να μετράς τα βασικά βήματα ενός αλγορίθμου ως συνάρτηση του μεγέθους της εισόδου.
  • Να απλοποιείς ένα πλήθος βημάτων στον συμβολισμό O, κρατώντας τον κυρίαρχο όρο.
  • Να συγκρίνεις τις τάξεις O(1), O(log n), O(n) και O(n²) για μεγάλα n.
  • Να προβλέπεις πώς αλλάζει ο χρόνος εκτέλεσης όταν πολλαπλασιάζεται το μέγεθος της εισόδου.

Λύσε τις ασκήσεις πιο πάνω και θα δεις εδώ πού στέκεσαι.