Όχι, μόνο N strings αλλά n(n+1) chars καθώς χρησιμοποιούνται οι καταλήξεις κι όχι όλα τα δυνατά substrings. Στόχος δεν είναι η ελάχιστη μνήμη αλλά η μέγιστη ταχύτητα. Γι αυτό και είπα ότι είναι για μεγάλα strings και αλλεπάλληλες αναζητήσεις. Για μία αναζήτηση μόνο είναι εντελώς περιττό. Αν όμως θέλει κανείς να ψάξει επανηλλημένα για ένα substring η αναζήτηση είναι ένα πολύ γρήγορο binary search. Η γραμμική αναζήτηση έχει πολυπλοκότητα Ο(m+n) όπου m το μέγεθος του substring και n το μέγεθος του μεγάλου string. Η πολυπλοκότητα του binary search είναι O(m*logn). Αν το substring είναι μικρό σε σχέση με το αρχικό string η διαφορά ταχύτητας είναι σημαντική. Ψάχνοντας για ένα substring των 10 χαρακτήρων σε ένα string των 1000 χαρακτήρων θα δώσει στην πρώτη περίπτωση 1010 ενώ στη δεύτερη 30. Και μετά πολλαπλασιάζουμε το νούμερο με τον αριθμό queries που θέλουμε να κάνουμε.
Το ίδιο πράγμα συμβαίνει με οποιαδήποτε δομή η οποία χρησιμοποιείται για indexing, παίρνουν συνήθως πολύ περισσότερη μνήμη αλλά επιταχύνουν τόσο πολύ τις αναζητήσεις ώστε το κόστος της δημιουργίας του index θεωρείται ότι μοιράζεται και είναι αμελητέο. Αν η πληροφορία που αναζητούμε είναι μικρή σε σχέση με το συνολικό όγκο πληροφορίας, ένα index επιταχύνει τη διαδικασία. Διαφορετικά είναι καλύτερη η γραμμική αναζήτηση. Αυτό γίνεται και με τον SQL Server, όταν κάποιες φορές επιλέγει να κάνει table scan αντί να χρησιμοποιήσει κάποιο index.
Τον κώδικα που έγραψα τον ψιλοέκλεψα από ένα απλό implementation σε Ruby με τη διαφορά ότι χρησιμοποίησα τα έτοιμα functions του .ΝΕΤ. H List<T>.Sort() χρησιμοποιεί QuickSort οπότε δεν είχα τις ανησυχίες του αρθρογράφου για το Enumerable#sort της Ruby, ενώ το BinarySearch ήταν κι αυτό έτοιμο. Η γραμμική αναζήτηση μετά για να βρώ όλα τα substrings που ξεκινούν με το substring που ψάχνω είναι λίγο χαζή, αλλά βαρέθηκα να ψάξω και για κάποιο καλύτερο αλγόριθμο!
Η ταχύτητα, ειδικά για το substring count, μπορεί να βελτιωθεί αρκετά αν χρησιμοποιηθούν suffix trees, στην οποία περίπτωση το substring count είναι ο αριθμός των πεδιών του πρώτου κόμβου που ξεκινάει με το substring που ψάχνουμε. Αυτός ο υπολογισμός μπορεί να γίνει άνετα τη στιγμή που δημιουργείται ένα δέντρο οπότε η αναζήτηση για ένα substring μπορεί να γίνει απλά O(logn). Βρήκα μία υλοποίηση σε C++ στο http://blog.lizhao.net/2005/01/c-implementation-of-ukkonens-suffix.html η οποία φτιάχνει το δέντρο σε Ο(n) αλλά ... κάτι δεν πήγε καλά στη μετατροπή σε C# και το δέντρο που παίρνω είναι λίγο μυστήριο.
Τα suffix trees και τα suffix arrays δημιουργούνται για να επιταχύνουν πολλαπλά queries π.χ. σε εφαρμογές βιοπληροφορικής όπου μπορεί να έχεις πολύ μεγάλα strings που αντιπροσωπεύουν γονίδια και ψάχνεις να βρεις μία συγκεκριμένη αλληλλουχία δηλαδή ... ένα substring. Επίσης χρησιμοποιούνται από ... προγράμματα antivirus για να ψάξουν για κάποια substrings μέσα σε προγράμματα ή τη μνήμη και τέλος σε μηχανής αναζήτησης. Αλήθεια, ο Δημήτρης τί θέλει να το κάνει?
Παναγιώτης Καναβός, Freelancer
Twitter: http://www.twitter.com/pkanavos