Εισαγωγή Λύσεις για τις ασκήσεις του lab5 Επειδή φάνηκε να υπάρχουν αρκετά προβλήματα σχετικά με τον τρόπο σκέψης για την επίλυση των προβλημάτων του lab5, θα συνοδεύσουμε τις λύσεις με αρκετές επεξηγήσεις, ειδικά για την άσκηση της Πέμπτης που μπορούσε να γίνει με πολλούς τρόπους. Μελετήστε τις ασκήσεις και απαντήσεις και των δύο τμημάτων, κι αν δεν είχατε καταφέρει να τις ολοκληρώσετε, προσπαθήστε ξανά. Άσκηση Πέμπτης SIZE Σε όλες τις αναφορές στο μέγεθος του πίνακα, δε δίνεται ένα συγκεκριμένο νούμερο, αλλά το πιο γενικό SIZE, διότι θέλουμε το πρόγραμμα να είναι γραμμένο με τέτοιο τρόπο ώστε να λειτουργεί σωστά ανεξαρτήτως του μεγέθους του πίνακα. Επομένως θα χρησιμοποιήσουμε #define για να ορίσουμε το SIZE με τρόπο που θα είναι εύκολο να αλλαχθεί. Επειδή τα αρχεία ελέγχου είναι γραμμένα για πίνακες των 10 ή 15 στοιχείων, επιλέγουμε μια από αυτές τις τιμές για τον ορισμό του SIZE. Αν θέλουμε να ελέγξουμε το πρόγραμμα για διαφορετικό μέγεθος πίνακα, θα αλλάξουμε απλά την τιμή του SIZE και θα ξανακάνουμε compile. #define SIZE 10 int main (int argc, char *argv[]) { int source[size], destination[size/2]; Παρατηρήσεις: Δεν ξεχνάμε ότι κατά σύμβαση, σταθερές και ονόματα ορισμένα με #define γράφονται με όλα τα γράμματα κεφαλαία. Η άσκηση δε φαίνεται να λύνει κάποιο πρόβλημα του πραγματικού κόσμου και ίσως αυτό μας κάνει να δυσκολευτούμε να βρούμε περιγραφικά ονόματα για τις μεταβλητές. Αλλά, η ίδια η εκφώνηση "προτείνει" source και destination. Είσοδος δεδομένων Ζητείται να διαβαστούν SIZE ακέραιοι, επομένως χρειαζόμαστε δομή επανάληψης. Εφόσον γνωρίζουμε τον πλήθος των επαναλήψεων, η πιο κατάλληλη δομή είναι for. Παρατηρήσεις Προσοχή να μη βγούμε από τα όρια του πίνακα. Το πρώτο στοιχείο του πίνακα βρίσκεται στη θέση 0 και το τελευταίο στη θέση SIZE-1 Μετατόπιση στοιχείων Το τέταρτο βήμα ζητά να αφαιρεθεί κάθε δεύτερο στοιχείο από τον πρώτο πίνακα, μετατοπίζοντας τα επόμενα στοιχεία αριστερά ώστε να "γεμίσουν" οι κενές θέσεις που δημιουργούνται, και να προστεθεί κάθε ένα από τα αφαιρεμένα στοιχεία στον πίνακα 2. Κάθε φορά που έχουμε ένα σύνθετο πρόβλημα, το σπάμε σε υποπροβλήματα:
> Εντοπισμός κάθε δεύτερου στοιχείου Δεδομένου ότι ξεκινάμε από τη θέση 1, τα στοιχεία που θέλουμε να αφαιρέσουμε είναι σε ζυγές θέσεις, επομένως ένας τρόπος να τα εντοπίσουμε είναι ελέγχοντας τη θέση τους. Εναλλακτικά, μπορούμε απλά να διατρέξουμε τον πίνακα αυξάνοντας κάθε φορά το μετρητή ανά δύο. Δείτε υλοποίηση και των δύο μεθόδων στην επόμενη ενότητα. > Μεταφορά στοιχείων στον πίνακα destination Ο πίνακας έχει μέγεθος SIZE/2 και θα μεταφέρουμε ακριβώς τόσα στοιχεία σε αυτόν. Πρέπει να προσέξουμε το μετρητή που θα χρησιμοποιήσουμε. Μπορούμε είτε να χρησιμοποιήσουμε ένα νέο μετρητή, ή να εκφράσουμε τις θέσεις σε σχέση με τον μετρητή που διατρέχει τον πίνακα source. Παραλλαγή 1: /* έλεγχος άρτιας/περιττής θέσης και χρήση νέου μετρητή */ for (i=1, j=0; i<size; i++) { if ( i%2 == 1) { destination[j] = source[i]; j++; Προσοχή Θέλουμε να αντιγράψουμε στο destination το 2ο, 4ο, 6ο κτλ. στοιχείο αλλά δεν πρέπει να ξεχνάμε πως η αρίθμηση θέσεων σε ένα πίνακα ξεκινά από το 0, επομένως μας ενδιαφέρουν οι μονές θέσεις 1, 3, 5, κτλ. Το i θα μπορούσε να ξεκινά κι από 0. Τότε απλά θα γινόταν μια επιπλέον επανάληψη, αλλά το τελικό αποτέλεσμα δε θα άλλαζε. Το j πρέπει να αυξάνεται μόνο όταν προστίθεται νέο στοιχείο στον πίνακα destination κι όχι μαζί με το i. Τα i και j σε αυτή την περίπτωση θα μπορούσαν να έχουν πιο περιγραφικά ονόματα: from και to αντίστοιχα. Παραλλαγή 2: /* Προσαύξηση i ανά δύο και υπολογισμός θέσεις στο destination με βάση τον υπάρχοντα μετρητή */ for (i=1; i<size; i+=2) { destination[i/2] = source[i]; > Γέμισμα κενών Για να γεμίσουν οι θέσεις που περιέχουν τα στοιχεία που αντιγράψαμε πρέπει να μετατοπίσουμε όλα τα επόμενα στοιχεία προς τα αριστερά:
σχήμα 1 * Μέθοδος 1 (πολύ εύκολη) Ο πιο απλός τρόπος είναι να κάνουμε τη μετατόπιση αφού έχουν αντιγραφεί όλα τα στοιχεία στον πίνακα destination, χρησιμοποιώντας ένα loop με δύο μετρητές: o ένας, ας τον πούμε from, θα διατρέχει τις θέσεις από τις οποίες θέλουμε να πάρουμε στοιχεία. Ο άλλος, ας τον πούμε to, θα διατρέχει τις θέσεις στις οποίες θέλουμε να βάλουμε στοιχεία: /* Αντιγραφή στο destination */ for (i=1; i<size; i++) { destination[i/2] = source[i]; /* Μετατόπιση στοιχείων αριστερά, αφότου ολοκληρωθεί η αντιγραφή στο destination */ for (from = 2, to = 1; from < SIZE; from+=2, to++) { source[to]=source[from]; *Μέθοδος 2 (λίγο πιο δύσκολη) Εναλλακτικά, μπορούμε να κάνουμε τη μετατόπιση αφότου έχουμε αντιγράψει όλα τα στοιχεία που θέλουμε στον πίνακα destination υπολογίζοντας την απόσταση που πρέπει να μετατοπιστεί κάθε στοιχείο. Δείτε το σχήμα 1. Τα βελάκια δείχνουν πού θέλουμε να μετατοπιστεί κάθε ένα στοιχείο. Παρατηρούμε πως η απόσταση μετατόπισης αυξάνεται κάθε φορά κατά ένα. Εδώ πρέπει να προσέξουμε ιδιαιτέρως τα όρια του πίνακα. Εφόσον προσπελαύνουμε το source[i+distance] θα πρέπει το i+distance να μη γίνει ποτέ μεγαλύτερο του SIZE-1. /* Αντιγραφή στο destination */ for (i=1; i<size; i++) { destination[i/2] = source[i]; /* Μετατόπιση στοιχείων αριστερά, αφότου ολοκληρωθεί η αντιγραφή στο destination */ for (i=1, distance=1; i+distance < SIZE; i++, distance++) { source[i]=source[i+distance]; *Μέθοδος 3 (αρκετά δύσκολη) Ένας άλλος, κλασικός τρόπος είναι κάθε φορά που αντιγράφεται ένα στοιχείο στον πίνακα destination και "ανοίγει" κενή θέση στον πίνακα source, να μετατοπίζουμε όλα τα υπόλοιπα στοιχεία μια θέση αριστερά. Τι ξέρουμε:
Το στοιχείο που βρίσκεται στη θέση i του πίνακα source αντιγράφηκε στο destination Η θέση i θεωρείται "άδεια" (προσοχή: ακόμη βρίσκεται εκεί ο αριθμός, αλλά δε θεωρείται έγκυρος) Τι θέλουμε: Θέλουμε κάθε στοιχείο από τη θέση i+1 και πέρα να πάει μια θέση αριστερά. Τι πρέπει να προσέξουμε: Να μη βγούμε εκτός ορίων πίνακα Πώς πρέπει να ανανεώνεται το i. Αυτό είναι ιδιαιτέρως σημαντικό. Δείτε το παρακάτω σχήμα : σχήμα 2 Ας υποθέσουμε ότι μόλις αντιγράψαμε το 6 στον πίνακα destination. Το i μας είναι 1. Αμέσως μετά, θέλουμε να μετατοπίσουμε μια θέση αριστερά όλα τα επόμενα στοιχεία (το 9 στη θέση 1, το 8 στη θέση 2 κ.ο.κ.): σχήμα 3 Παρατηρούμε πως το επόμενο στοιχείο που θα αντιγραφεί στον πίνακα destination, το 8, τώρα πια βρίσκεται στη θέση 2. Επομένως, το i για την επόμενη επανάληψη δεν πρέπει πια να αυξάνεται κατά 2, αλλά κατά 1. /* Μετατόπιση στοιχείων αριστερά, από τη θέση i+1 και πέρα, παράλληλα με την αντιγραφή στο dest. */ for (i=1, j=0; i<=size/2; i++) { destination[j] = source[i]; j++; for (k=i; k<size-1; k++) { source[k] = source[k+1]; Προσοχή: Το i πηγαίνει μόνο μέχρι και SIZE/2 γιατί μετά από κάθε αντιγραφή στο destination, μεταφέρουμε τα υπόλοιπα στοιχεία του source αριστερά, με αποτέλεσμα ο πίνακας source να μικραίνει κάθε φορά. Το εσωτερικό loop πρέπει να χρησιμοποιήσει διαφορετικό μετρητή από το i. Το εσωτερικό loop πηγαίνει μέχρι και SIZE-2. Αυτό οφείλεται στο ότι προσπελαύνουμε το
source[k+1]. Για να μείνουμε εντός ορίων πίνακα θα πρέπει το k+1 να πάρει το πολύ την τιμή SIZE-1. Γέμισμα με μηδενικά Αφού μετατοπιστούν τα στοιχεία, πρέπει να γεμίσουμε τις δεξιές θέσεις με μηδενικά. Αν χρησιμοποιήσουμε τη μέθοδο 3, αυτό είναι εύκολο. Αρκεί κάθε φορά να βάζουμε μηδέν στην τελευταία θέση του πίνακα, και με κάθε μετατόπιση (δηλαδή με κάθε εκτέλεση του εσωτερικού loop) θα μεταφέρεται κι αυτό αριστερά: /* Μετατόπιση στοιχείων αριστερά, από τη θέση i+1 και πέρα, παράλληλα με την αντιγραφή στο destination και γέμισμα των δεξιών θέσεων με μηδενικά */ for (i=1, j=0; i<=size/2; i++) { destination[j] = source[i]; j++; for (k=i; k<size-1; k++) { source[k] = source[k+1]; source[size-1] = 0; Αν χρησιμοποιήσουμε τη μέθοδο 1 ή 2, μπορούμε να εκμεταλλευτούμε τη γνώση ότι ο πίνακας έχει έγκυρα στοιχεία μέχρι και τη μέση ή μέχρι το distance του loop μετατόπισης. Αν χρησιμοποιήσουμε τη μέση, πρέπει να λάβουμε υπόψη ότι ο πίνακας μπορεί να έχει άρτιο ή περιττό μέγεθος και να γράψουμε τον κώδικά μας ώστε να δουλεύει σωστά και για τις δύο περιπτώσεις. Ένας γρήγορος τρόπος είναι υπολογίζοντας το άνω ακέραιο μέρος της πράξης SIZE/2.0. Η C παρέχει μια συνάρτηση γι' αυτό, με όνομα ceil. /* Μετατόπιση στοιχείων αριστερά, αφότου ολοκληρωθεί η αντιγραφή στο destination */ for (i=1, distance=1; i+distance < SIZE; i++, distance++) { source[i]=source[i+distance]; /* Γέμισμα πίνακα με μηδενικά */ for (i = ceil(size/2.0); i < SIZE; i++) { source[i]=0; Είναι σαφώς πιο απλό να εκμεταλλευτούμε την τιμή της μεταβλητής distance: /* Μετατόπιση στοιχείων αριστερά, αφότου ολοκληρωθεί η αντιγραφή στο destination */ for (i=1, distance=1; i+distance < SIZE; i++, distance++) { source[i]=source[i+distance]; /* Γέμισμα πίνακα με μηδενικά */ for (i = distance; i < SIZE; i++) { source[i]=0;
Έξοδος προγράμματος Τέλος, ζητείται να εκτυπωθούν τα περιεχόμενα του κάθε πίνακα, στην ίδια γραμμή, με ένα κενό ανάμεσα σε διαδοχικά στοιχεία, κι ένα χαρακτήρα αλλαγής γραμμής στο τέλος. for (i=0; i < SIZE; i++) { printf("%d ", source[i]); printf("\n"); Παρατήρηση: Έχουμε βάλει ένα χαρακτήρα κενό μετά το %d Άσκηση Παρασκευής SIZE Σε όλες τις αναφορές στο μέγεθος του πίνακα, δε δίνεται ένα συγκεκριμένο νούμερο, αλλά το πιο γενικό SIZE, διότι θέλουμε το πρόγραμμα να είναι γραμμένο με τέτοιο τρόπο ώστε να λειτουργεί σωστά ανεξαρτήτως του μεγέθους του πίνακα. Επομένως θα χρησιμοποιήσουμε #define για να ορίσουμε το SIZE με τρόπο που θα είναι εύκολο να αλλαχθεί. Επειδή τα αρχεία ελέγχου είναι γραμμένα για πίνακες των 7 ή 8 στοιχείων, επιλέγουμε μια από αυτές τις τιμές για τον ορισμό του SIZE. Αν θέλουμε να ελέγξουμε το πρόγραμμα για διαφορετικό μέγεθος πίνακα, θα αλλάξουμε απλά την τιμή του SIZE και θα ξανακάνουμε compile. #define SIZE 7 int main (int argc, char *argv[]) { char letters[size], to_insert; Παρατηρήσεις: Δεν ξεχνάμε ότι κατά σύμβαση, σταθερές και ονόματα ορισμένα με #define γράφονται με όλα τα γράμματα κεφαλαία. Η άσκηση δε φαίνεται να λύνει κάποιο πρόβλημα του πραγματικού κόσμου και ίσως αυτό μας κάνει να δυσκολευτούμε να βρούμε περιγραφικά ονόματα για τις μεταβλητές. Αλλά, εφόσον ο πίνακας περιέχει γράμματα, ένα καλό όνομα για αυτόν είναι letters ή characters (όχι chars γιατί είναι πολύ κοντά στη λέξη-κλειδί char και υπάρχει κίνδυνος τυπογραφικού λάθους). Το όνομα του χαρακτήρα προς τοποθέτηση είναι το αρκετά περιγραφικό to_insert. Εντοπισμός κεντρικής θέσης πίνακα Πρέπει να προσέξουμε δύο πράγματα κατά τον εντοπισμός της κεντρικής θέσης: Αν ο πίνακας έχει άρτιο μέγεθος, τότε έχει δύο κεντρικές θέσεις Η πρώτη θέση του πίνακα είναι 0 κι όχι 1 Στην περίπτωση που ο πίνακας έχει περιττό μέγεθος, η κεντρική θέση είναι SIZE/2. Στην περίπτωση που ο πίνακας έχει άρτιο μέγεθος, οι κεντρικές θέσεις είναι SIZE/2-1 και SIZE/2
Είσοδος δεδομένων Ζητείται να διαβαστούν SIZE χαρακτήρες, επομένως θα χρειαστούμε δομή επανάληψης. Εφόσον γνωρίζουμε τον πλήθος των επαναλήψεων, η πιο κατάλληλη δομή είναι for. Όταν διαβάζουμε χαρακτήρες πρέπει να είμαστε ιδιαίτερα προσεκτικοί γιατί ακόμη το enter ή κενό που θα πατήσουμε μετά την εισαγωγή ενός χαρακτήρα είναι κι αυτά χαρακτήρες, οπότε στην επόμενη επανάληψη θα διαβαστεί το enter (ή το κενό) 1. Στην περίπτωση της scanf το πρόβλημα λύνεται βάζοντας ένα κενό ανάμεσα στο " και στο %c. Στην περίπτωση της getchar το πρόβλημα λύνεται αν βάλουμε μια ακόμη getchar ώστε να διαβάσει και να "καταναλώσει" το enter ή κενό που πατήσαμε μετά το χαρακτήρα. Τοποθέτηση χαρακτήρων δεξιά / αριστερά Αν εξαιρέσουμε την τοποθέτηση του πρώτου χαρακτήρα, ο οποίος μπορεί να πάει σε μία ή δύο θέσεις ανάλογα με το αν το μέγεθος του πίνακα είναι περιττό ή άρτιο, η διαδικασία είναι ίδια και για τις δύο περιπτώσεις. Αρκεί να ξέρουμε σε ποια θέση αριστερά και ποια θέση δεξιά προσθέσαμε ένα χαρακτήρα στην προηγούμενη επανάληψη. Επομένως, η πιο πρακτική λύση είναι να χρησιμοποιήσουμε δύο μεταβλητές, μία για την αριστερή θέση και μία για τη δεξιά θέση. Απλά, στην περίπτωση περιττού μήκους, έχουν και οι δύο την ίδια τιμή. Από εκεί και πέρα, αρκεί να βάζουμε κάθε φορά τον χαρακτήρα που διαβάστηκε σε κάθε μία από αυτές τις δύο θέσεις. Εδώ είναι κατάλληλη η χρήση while : θέλουμε να τοποθετούμε χαρακτήρες μέχρι να φτάσουμε στα άκρα του πίνακα. Εφόσον η τοποθέτηση γίνεται συμμετρικά, αρκεί να κάνουμε έλεγχο για το ένα άκρο. if (SIZE%2) { /* περιττό μέγεθος πίνακα */ leftpos = rightpos = SIZE/2; else { /* άρτιο μέγεθος πίνακα */ leftpos = SIZE/2-1; rightpos = leftpos + 1; while (leftpos >= 0) { scanf(" %c", &to_insert); letters[leftpos] = letters[rightpos] = to_insert; leftpos--; rightpos++; Εκτύπωση πίνακα σε κάθε στάδιο Η εκφώνηση ζητά να εκτυπώνεται ο πίνακας μετά από κάθε εισαγωγή νέου χαρακτήρα, και να εμφανίζεται μια παύλα σε όσες θέσεις είναι ακόμη "άδειες". Αρχικά, είναι όλος ο πίνακας άδειος, επομένως είναι λογικό, πριν κάνουμε οτιδήποτε στο πρόγραμμα να αρχικοποιήσουμε το πίνακα letters με ένα for loop ώστε να περιέχει μια παύλα σε κάθε θέση. Μετά, όπως θα διαβάζονται οι χαρακτήρες θα αντικαθιστούν μία-μία τις παύλες. 1 Η είσοδος από το πληκτρολόγιο γίνεται με τρόπο παρεμφερή με αυτόν της εξόδου στην οθόνη, όπως εξηγήθηκε στο εργαστήριο. Ότι γράφουμε μπαίνει σε ένα προσωρινό πινακάκι, και κάθε φορά που εκτελείται scanf ή getchar διαβάζει το επόμενο πράγμα που βρίσκει στο πινακάκι. Έτσι το enter που πιθανώς είχαμε πατήσει μετά από την εισαγωγή ενός χαρακτήρα, εξακολουθεί να "ζει" και να περιμένει να διαβαστεί από κάποια επόμενη scanf ή getchar. Αυτό είναι πρόβλημα όταν διαβάζουμε χαρακτήρες. Αν η scanf ζητήσει ένα ακέραιο, τότε ακόμη κι αν υπάρχει ήδη κάποιο enter στο "πινακάκι", θα αγνοηθεί.
Τα περιεχόμενα του πίνακα θέλουμε να τυπώνονται κάθε φορά στην ίδια γραμμή, με ένα κενό ανάμεσα σε διαδοχικά στοιχεία, κι ένα χαρακτήρα αλλαγής γραμμής στο τέλος. for (i=0; i < SIZE; i++) { printf("%c ", letters[i]); printf("\n"); Παρατήρηση: Έχουμε βάλει ένα χαρακτήρα κενό μετά το %d Προσοχή: Το loop που εκτυπώνει τον πίνακα πρέπει να βρίσκεται μέσα στο while!