Διάλεξη 12ης Μαΐου 2015 - Αλγόριθμοι ταξινόμησης
Οι αλγόριθμοι ταξινόμησης είναι από τους σημαντικότερους αλγορίθμους στην επιστήμη των υπολογιστών, έχουν μακρά ιστορία, ίση τουλάχιστον με την ιστορία των μοντέρνων υπολογιστών και, φυσικά, τεράστια πρακτική σημασία. Για να παρουσιάσουμε μερικούς από τους πιο δημοφιλής αλγόριθμους ταξινόμησης θα υποθέσουμε ότι τα δεδομένα που θέλουμε να ταξινομήσουμε είναι αποθηκευμένα σε ένα πίνακα και ότι μπορούμε να συγκρίνουμε δύο οποιαδήποτε στοιχεία του. Οι συχνότερες λειτουργίες κατά την ταξινόμηση είναι η σύγκριση δύο στοιχείων (comparison) και η εναλλαγή δύο στοιχείων (swap) και τον αριθμό αυτών των λειτουργιών υπολογίζουμε κατά τη σύγκριση δύο αλγορίθμων ταξινόμησης.
Ο απλούστερος, ίσως αλγόριθμος ταξινόμησης είναι ο αλγόριθμος της ταξινόμησης με εισαγωγή (insertion sort). Ο συγκεκριμένος αλγόριθμος εισάγει ένα-ένα τα στοιχεία του πίνακα που εξετάζει στη σωστή του θέση. Στο βήμα $i$ της μεθόδου υποθέτουμε ότι τα στοιχεία $A[1\ldots i-1]$ είναι ταξινομημένα. Εισάγουμε το στοιχεία $A[i]$ στη σωστή θέση μετακινώντας όλα τα στοιχεία που είναι μεγαλύτερα του $A[i]$ μια θέση προς τα δεξιά. Μια υλοποίηση της μεθόδου ταξινόμησης με εισαγωσή φαίνεται παρακάτω:
for i in range(1,len(A)):
t = A[i]
j = i
while j > 0 and A[j-1] > t:
A[j] = A[j-1]
j = j - 1
A[j] = t
Αν εφαρμόσουμε τον παραπάνω αλγόριθμο στην πίνακα με στοιχεία 54, 26, 93, 17, 77, 31, 44, 55, 20 θα έχουμε διαδοχικά (με γκρίζο φόντο τα στοιχεία του πίνακα που είναι ήδη ταξινομημένα):
54 | 26 | 93 | 17 | 77 | 31 | 44 | 55 | 20 |
26 | 54 | 93 | 17 | 77 | 31 | 44 | 55 | 20 |
26 | 54 | 93 | 17 | 77 | 31 | 44 | 55 | 20 |
17 | 26 | 54 | 93 | 77 | 31 | 44 | 55 | 20 |
17 | 26 | 54 | 77 | 93 | 31 | 44 | 55 | 20 |
17 | 26 | 31 | 54 | 77 | 93 | 44 | 55 | 20 |
17 | 26 | 31 | 44 | 54 | 77 | 93 | 55 | 20 |
17 | 26 | 31 | 44 | 54 | 55 | 77 | 93 | 20 |
17 | 20 | 26 | 31 | 44 | 54 | 55 | 77 | 93 |
Το κόστος εκτέλεσης του αλγορίθμου ταξινόμησης με εισαγωγή είναι εύκολο να υπολογιστεί: στην χρειρότερη περίπτωση,
η εσωτερική ανκύκλωση while
θα εκτελεστεί $i$ φορές για $i=0,\ldots,n$, όπου $n = \mathrm{len}(A)$. Το κόστος λοιπόν της ταξινόμησης με εισαγωγή είναι
$$
\sum_{i=1}^{n-1} i = 1 + 2 + \cdots + n-1 = \frac{n(n-1)}{2},
$$
ανάλογο δηλαδή του $n^2$.
Ένας ακόμα αλγόριθμος ταξινόμησης του οποίου ο χρόνος εκτέλεσης είναι ανάλογος του $n^2$ είναι ο αλγόριθμος ταξινόμησης με επιλογή (selection sort). Στο βήμα $i$ του αλγορίθμου επιλέγεται το ελάχιστο από τα στοιχεία στις θέσεις $i, i-1,\ldots$ και μετακινείται στη θέση $i$ του πίνακα. Μια υλοποίηση του συγκεκριμένου αλγόριθμου φαίνεται παρακάτω:
for i in range(0,len(A)-1):
k = i
for j in range(i+1,len(A)):
if A[j] < A[k]: k = j
t = A[i]
A[i] = A[k]
A[k] = t
Είναι δυνατόν να βρούμε αλγόριθμους ταξινόμησης των οποίων ο χρόνος εκτέλεσης να είναι καλύτερος από $n^2$; Η απάντηση είναι ναι, αλλά χρειαζόμαστε μια νέα ιδέα. Ο αλγόριθμος της ταξινόμησης με συγχώνευση (merge sort) βασίζεται στην στρατηγική του διαίρει και βασίλευε (divide and conquer), δηλαδή σε μια αναδρομική διαδικασία όπου το πρόβλημα μοιράζεται σε μέρη τα οποία λύνονται χωριστά και μετά οι λύσεις τους συνδιάζονται. Κατά την ταξινόμηση με συγχώνευση, μοιράζουμε τον πίνακα σε δύο μέρη, αναδρομικά ταξινομούμε τα δύο μέρη και συγχωνεύουμε τα αποτελέσματα. Επειδή κατά την $i$-στή κλήση της αναδρομικής διαδικασίας κομμάτια μεγέθους $2^i$ είναι ταξινομημένα, η διαδικασία της ταξινόμησης με στγχώνευση πρέπει να κληθεί $\log_2 n$ φορές, όπου $n = \mathrm{len}(A)$, μέχρις ότου ταξινομηθεί ολόκληρος ο πίνακας. Στη διπλανή εικόνα φαίνεται η διαδικασία κατά την οποία η αρχική λίστα χωρίζεται σε διαδοχικά μικρότερες λίστες.
Όταν το μήκος της κάθε λίστας είναι ένα, και επομένως όλες οι λίστες είναι ταξινομημένες, ακολουθεί η διαδικασία της συγχώνευσης, κατά την οποία δύο ταξινομημένες λίστες συγχωνεύονται σε μια ταξινομημένη λίστα σε χρόνο ανάλογο του αθροίσματος των μεγεθών των δύο λίστών. Η διαδικασία συνοψίζεται στην εικόνα στα δεξιά. Η υλοποίηση του αλγορίθμου της ταξινόμησης φαίνεται παρακάτω. Παρατηρήστε ότι μια και η συγχώνευση δύο ταξινομημένων λιστών σε μια ταξινομημένη λίστα απαιτεί γραμμικό χρόνο και η συγχωνευτική ταξινόμηση καλείται αναδρομικά $\log_2 n$ φορές, το κόστος της συγχωνευτικής ταξινόμησης είναι ανάλογο του $n\log_2 n$, πολύ καλύτερο δηλαδή του κόστους του αλγορίθμου ταξινόμησης με επιλογή, για παράδειγμα.
def mergeSort(A):
if len(A) > 1:
mid = len(A) / 2
left = A[:mid]
right = A[mid:]
mergeSort(left)
mergeSort(right)
i = 0
j = 0
k = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
A[k] = left[i]
i = i + 1
else:
A[k] = right[j]
j = j + 1
k = k + 1
while i < len(left):
A[k] = left[i]
i = i + 1
k = k + 1
while j < len(right):
A[k] = right[j]
j = j + 1
k = k + 1
Αξίζει να σημειώσουμε ότι η Python χρησιμοποιεί ένα υβριδικό αλγόριθμο ταξινόμησης με το όνομα timsort, ο οποίος αποτελεί συνδιασμό των αλγορίθμων ταξινόμησης με συγχώνευση και ταξινόμησης με εισαγωγή. Γράφτηκε το 2002 από τον Tim Peters και ο χρόνος εκτέλεσής του είναι ανάλογος του $n\log_2 n$.