Σημειωματάριο Πέμπτης 26 Φεβ. 2015

Σήμερα δεν είδαμε νέες δυνατότητες της γλώσσας python αλλά ασχοληθήκαμε με το να λύσουμε συγκεκριμένα προβλήματα με κάποιο ενδιαφέρον.

Μια συνάρτηση για εύρεση ελαχίστου και μεγίστου

Αρχίζουμε με την κατασκευή μιας συνάρτησης minmax(L) η οποία παίρνει ως είσοδο μια λίστα αριθμών και επιστρέφει το ελάχιστο και το μέγιστο της λίστας.

Η ιδέα είναι πολύ απλή. Χρησιμοποιούμε δύο μεταβλητές m και M στις οποίες θα υπολογίσουμε το ελάχιστο και το μέγιστο. Η αρχική τιμή και των δύο είναι το πρώτο στοιχείο της λίστας. Έπειτα διατρέχουμε τη λίστα με το for και φροντίζουμε να ισχύει το εξής: σε κάθε επανάληψη του loop να είναι m και M οι τρέχουσες τιμές του ελαχίστου και του μεγίστου, το ελάχιστο δηλ. και το μέγιστο όσων στοιχείων έχουμε δει μέχρι στιγμής. Για να το πετύχουμε αυτό, για κάθε νέο στοιχείο της λίστας που κοιτάμε ενημερώνουμε της τιμές m και M αν χρειάζεται με τα δύο if που είναι μέσα στο for.

In [1]:
def minmax(L):
    "Επιστρέφει το min και το max της λίστας αριθμών L"
    m = L[0]
    M = L[0]
    for x in L:
        if x<m:
            m = x
        if x>M:
            M = x
    return m, M

L=input("Δώσε λίστα αριθμών:")
print minmax(L)
Δώσε λίστα αριθμών:[1, -1, 4, -2, 0, 10]
(-2, 10)

Σχέδια με αστεράκια

Στα επόμενα παραδείγμα σχεδιάζουμε διάφορα σχήματα, ολοένα και πιο πολύπλοκα τυπώνοντας κατάλληλα τους χαρακτήρες " " και "*" (λευκός ή κενός χαρακτήρας και αστεράκι) στην οθόνη.

Αρχίζουμε με ένα πρόγραμμα που τυπώνει ένα τετράγωνο N x N στην οθόνη. (Το N παίρνει τιμή στην αρχή του προγράμματος.)

Η λύση είναι ένα διπλό for loop όπου η εξωτερική ανακύκλωση (με δείκτη τη μεταβλητή i) εκτελείται N φορές και σε κάθε εκτέλεσή της είναι υπεύθυνη για την εκτύπωση μιας γραμμής με N αστεράκια, πράγμα που επιτυγχάνουμε με την εσωτερική ανακύκλωση (με δείκτη τη μεταβλητή j). Μέσα στην εσωτερική ανακύκλωση τυπώνουμε ένα αστεράκι N φορές (προσέχοντας να μην αλλάξουμε γραμμή σε κάθε τέτοιο αστεράκι, αλλά μόνο μια φορά μέσα σε κάθε εξωτερική ανακύκλωση, με την τελευταία εντολή print).

In [2]:
N=5
for i in range(N):
    for j in range(N):
        print "*",
    print
    
* * * * *
* * * * *
* * * * *
* * * * *
* * * * *

Ένα λεπτό σημείο στο προηγούμενο πρόγραμμα είναι ότι η εντολή print "*", δεν αλλάζει μεν γραμμή μετά την εκτέλεσή της αλλά τυπώνει ένα κενό χαρακτήρα με αποτέλεσμα τα αστεράκια του προηγούμενου τετραγώνου να είναι χωρισμένα οριζόντια με ένα λευκό χαρακτήρα. Αυτό τελικά δεν είναι και τόσο αντιαισθητικό μια και ο κάθε χαρακτήρας που τυπώνεται στην οθόνη είναι στενόμακρος με τη μακριά πλευρά του να είναι η κατακόρυφη, οπότε με το να τυπώνουνται τα κενά ανάμεσα στα αστεράκια το τελικό αποτέλεσμα έχει διαστάσει που προσεγγίζουν καλύτερα αυτές ενός τετραγώνου απ' ό,τι στο επόμενο πρόγραμμα όπου φροντίζουμε τα κενά αυτά να μη τυπωθούν.

Είναι μάλλον αδύνατο στην python να κάνουμε την print να μη τυπώνει τα κενά, οπότε καταφεύγουμε σε μια άλλη λύση. Αντί να καλούμε την print Ν φορές για κάθε γραμμή την καλούμε μόνο μία αφού φροντίζουμε όμως να έχουμε ετοιμάσει μια λέξη s που να περιέχει και τα N αστεράκια (χωρίς κενά ανάμεσά τους). Η κατασκευή της s επιτυγχάνεται με το πρώτο for του προγράμματος. Το δεύτερο for απλά τυπώνει το string s N φορές.

In [3]:
N=5
s=""
for i in range(N):
    s = s+"*"
for i in range(N):
    print s
    
*****
*****
*****
*****
*****

Στο επόμενο τυπώνουμε και πάλι το τετράγωνο με τα αστεράκια αλλά τυπώνουμε αστεράκια μόνο στο περίγραμμα. Στο εσωτερικό τυπώνουμε κενούς χαρακτήρες.

Αν ονομάσουμε i τη γραμμή ενός σημείου (στο οποίο θέλουμε να δούμε αν θα τυπώσουμε "*" ή " ") και j τη στήλη του, με \(0 \le i, j \le N-1\), παρατηρούμε ότι το σημείο είναι στο εσωτερικό του τετραγώνου αν και μόνο αν ισχύει \(1 \le i \le N-2\) και \(1 \le j \le N-2\).

Η αλλαγή λοιπόν που κάνουμε στο προηγούμενο πρόγραμμα για να τυπώνουμε αστεράκια μόνο στο περίγραμμα είναι ότι ελέγχουμε με τη συνθήκη if που είναι μέσα στο for αν είμαστε στο εσωτερικό ή όχι και ανάλογα τυπώνουμε αστεράκι ή κενό.

In [4]:
N=5
for i in range(N):
    for j in range(N):
        if (1<=i) and (i<=N-2) and (1<=j) and (j<=N-2):
            print " ",
        else:
            print "*",
    print
* * * * *
*       *
*       *
*       *
* * * * *

Η επόμενη παραλλαγή μας είναι ότι θέλουμε να τυπώνουμε κι ένα μοναδικό αστεράκι στο κέντρο του τετραγώνου όπως φαίνεται παρακάτω.

Παρατηρούμε κατ' αρχήν ότι για να υπάρχει αυτό το κέντρο πρέπει αναγκαστικά το N να είναι περιττός αριθμός. Δεν υπάρχει κεντρικό τετραγωνάκι αν το N είναι άρτιο.

Στη λογική μεταβλητή center (τέτοιες είναι οι μεταβλητές που παίρνουν μόνο δύο τιμές, True και False) υπολογίζουμε πρώτα αν όντως υπάρχει τέτοιο κεντρικό τετραγωνάκι, θέτοντας τη μεταβλητή αυτή σε True αν το N είναι περιττός (το υπόλοιπο της διαίρεσής του διά 2 είναι 1) και σε False αλλιώς.

Η τροποποίηση που κάνουμε στο εσωτερικό for loop του προηγούμενου προγράμματος είναι ότι πριν τυπώσουμε τον κενό χαρακτήρα ελέγχουμε τη συνθήκη center and i==N/2 and j==N/2 η οποία ελέχει αν υπάρχει κεντρικό τετράγωνο και αν είμαστε πάνω σε αυτό (οι συντεταγμένες του κεντρικού τετραγώνου είναι \(i=j=N/2\) και παρατηρείστε ότι στην περίπτωση που υπάρχει κεντρικό τετράγωνο το \(N/2\) είναι ο αριθμός \(k\) όπου \(N=2k+1\))

In [3]:
N=9 # Αν εδώ βάλουμε άρτιο αριθμό τότε δεν τυπώνεται κεντρικό αστεράκι

if N%2==1:
    center=True
else:
    center=False
    
for i in range(N):
    for j in range(N):
        if (1<=i) and (i<=N-2) and (1<=j) and (j<=N-2):
            if center and i==N/2 and j==N/2:
                print "*",
            else:
                print " ",
        else:
            print "*",
    print
* * * * * * * * *
*               *
*               *
*               *
*       *       *
*               *
*               *
*               *
* * * * * * * * *

Στο επόμενο πρόγραμμα θέλουμε να τυπώσουμε ένα δέντρο, όπως αυτό που φαίνεται παρακάτω. Οι παράμετροι που περιγράφουν το δέντρο μας είναι δύο, το \(k\) που μας λέει πόσες γραμμές έχει το τρίγωνο που παριστάνει το φύλλωμα του δέντρου και το \(l\) που παριστάνει το ύψος του κορμού κάτω από το μέσο της βάσης του τριγώνου.

Φτιάχνουμε λοιπόν δύο συναρτήσεις, τη συνάρτηση triangle(k) που τυπώνει το τρίγωνο με \(k\) σειρές, και τη συνάρτηση kormos(l, k) που τυπώνει τον κορμό με ύψος l κάτω από το τρίγωνο με \(k\) γραμμές. (Η παράμετρος \(l\) δεν είναι απαραίτητη στην triangle αλλά η παράμετρος \(k\) είναι απαραίτητη στην kormos γιατί πρέπει να ξέρουμε όχι μόνο το ύψος του κορμού αλλά και πόσο δεξιά να τον τυπώσουμε. Στο κυρίως πρόγραμμα καλούμε πρώτα τη συνάρτηση triangle και μετά τη συνάρτηση kormos προσέχοντας να δώσουμε την ίδια τιμή \(k\) και στις δύο.

Η σημαντική δουλειά εδώ είναι στην προετοιμασία, όπου πρέπει να μετρήσουμε για κάθε μια από τις \(k\) γραμμές του φυλλώματος πόσα κενά και πόσα αστεράκια να τυπώσουμε. Με ένα προσεκτικό μέτρημα και ονομάζοντας \(0\) την πάνω γραμμή (με το ένα μόνο αστεράκι) και \(k-1\) την κάτω γραμμή του φυλλώματος (με τα περισσότερα αστεράκια) βρίσκουμε ότι στη γραμμή με δείκτη \(i\) (όπου \(0 \le i \le k-1\)) πρέπει να τυπώσουμε \(k-1-i\) κενά ακολουθούμενα από \(2i+1\) αστεράκια. Έτσι η συνάρτηση triangle είναι ένα απλό for loop που εκτελείται \(k\) φορές και τυπώνονται ακριβώς αυτός ο αριθμός από κενά και αστεράκια.

Η συνάρτηση kormos είναι πολύ πιο απλή γιατί απλά τυπώνουμε \(l\) φορές την ίδια γραμμή που αποτελείται από \(k-1\) κενά και ένα αστεράκι.

In [13]:
def triangle(k):
    "Τυπώνει τρίγωνο βάθους k, το φύλλωμα του δέντρου"
    for i in range(k):
        for j in range(k-1-i):
            print " ",
        for j in range(2*i+1):
            print "*",
        print

def kormos(l, k):
    "Τυπώνει τον κορμό του δέντρου ύψους l, για δέντρο με φύλλωμα με k σειρές"
    for j in range(l):
     for i in range(k-1):
        print " ",
     print "*"
        
triangle(11)
kormos(5, 11)
                    *
                  * * *
                * * * * *
              * * * * * * *
            * * * * * * * * *
          * * * * * * * * * * *
        * * * * * * * * * * * * *
      * * * * * * * * * * * * * * *
    * * * * * * * * * * * * * * * * *
  * * * * * * * * * * * * * * * * * * *
* * * * * * * * * * * * * * * * * * * * *
                    *
                    *
                    *
                    *
                    *

Το δεκαδικό και το δεκαεξαδικό ανάπτυγμα ενός αριθμού

Ένα φυσικό αριθμό \(x\) συνήθως τον γράφουμε στο δεκαδικό σύστημα \(x = (a_n a_{n-1} \cdots a_1 a_0)_{10}\), όπου \[ a_n, a_{n-1}, \cdots, a_1, a_0 \in \{0, 1, \ldots, 9\} \] είναι τα δεκαδικά ψηφία του αριθμού \(x\) με \(a_0\) να είναι το ψηφίο των δεκάδων, \(a_1\) να είναι το ψηφίο των δεκάδων, κλπ. Βάζουμε το δείκτη 10 έξω από την παρένθεση για να ξεκαθαρίσουμε ότι αυτά είναι τα ψηφία του αριθμού γραμμένα στο 10δικό σύστημα. Έτσι η περιγραφή \(x = (a_n a_{n-1} \cdots a_1 a_0)_{10}\) δε σημαίνει τίποτε άλλο παρά την ισότητα \[ x = a_n 10^n + a_{n-1} 10^{n-1} + \cdots + a_2 10^2 + a_1 10 + a_0. \] Δεν υπάρχει κάποια ιδιαιτερότητα στο 10 (πέρα από το ότι οι άνθρωποι έχουν 10 δάκτυλα, και αυτός είναι ο λόγος που επικράτησε το δεκαδικό σύστημα) και μπορεί κανείς να εκφράσει τον \(x\) και σε οποιαδήποτε άλλη βάση. Αν θέλουμε να γράψουμε τον \(x\) στο σύστημα με βάση το \(D\) (ένας φυσικός \(\ge 2\)) τότε ψάχνουμε να βρούμε ψηφία \[ a_n, a_{n-1}, \cdots, a_1, a_0 \in \{0, 1, \ldots, D-1\} \] ώστε να ισχύει \[ x = a_n D^n + a_{n-1} D^{n-1} + \cdots + a_2 D^2 + a_1 D + a_0. \] Στην ειδική περίπτωση όπου \(D=2\) (δυαδικό σύστημα) οι μόνες τιμές που μπορεί να πάρει ένα ψηφίο είναι το 0 και το 1, ο ελάχιστος δυνατός αριθμός, και αυτός είναι ο λόγος που το δυαδικό σύστημα είναι αυτό που χρησιμοποιείται στα ψηφιακά ηλεκτρονικά: είναι πολύ πιο εύκολο για μια ηλεκτρονική διάταξη να ξεχωρίσει μια τάση, π.χ. 0V, από μια άλλη, π.χ. 5V, παρά να ξεχωρίσει 10 διαφορετικές τάσεις μεταξύ τους.

Πώς μπορεί κανείς τώρα να βρεί τα ψηφία ενός φυσικού αριθμού με βάση αρίθμησης το \(D\);

Η σημαντική παρατήρηση είναι ότι αν εξαιρέσουμε το \(a_0\), που ικανοποιεί \(0\le a_0 < D\), όλες οι άλλες ποσότητες στο δεξί μέρος της παραπάνω ισότητας διαιρούνται με το \(D\), και άρα το \(a_0\) είναι το υπόλοιπο της διαίρεσης του \(x\) δια \(D\). Έτσι υπολογίζουμε το \(a_0\).

Αφού βρούμε το \(a_0\) μπορούμε να το αφαιρέσουμε από το αριστερό μέλος και έπειτα να διαιρέσουμε δια \(D\) για να πάρουμε \[ \frac{x-a_0}{D} = a_n D^{n-1}+ a_{n-1} D^{n-2} + \cdots + a_2 D + a_1. \] Για να βρούμε τώρα το ψηφίο \(a_1\) δεν έχουμε παρά να πάρουμε το υπόλοιπο της διαίρεσης του αριστερού μέλους δια \(D\), και προχωράμε παρόμοια μέχρις ότου το αριστερό μέλος μηδενιστεί.

Η παρακάτω συνάρτηση tobinary παίρνει ένα φυσικό αριθμό x ως όρισμα και επιστρέφει μια λίστα αριθμών η οποία έχει μέσα τα δυαδικά ψηφία του x με τη σειρά που γράφονται, δηλ. με το ψηφίο των μονάδων τελευταίο στη λίστα.

Η λίστα ld, η οποία και επιστρέφεται, ξεκινά ως μια κενή λίστα. Πρώτα μπαίνει μέσα το ψηφίο των μονάδων και κάθε νέο ψηφίο y που υπολογίζουμε μπαίνει μπροστά στη λίστα με την εντολή ld = [y] + ld. Το ψηφίο y υπολογίζεται παίρνοντας το υπόλοιπο του αριθμού δια 2 και στην τελευταία γραμμή του loop x = (x-y)/2 πραγματοποιείται η αφαίρεση του ψηφίου που μόλις υπολογίσαμε από το αριστερό μέλος και η διαίρεση δια 2.

Έτσι, με το παράδειγμα που υπολογίζουμε παρακάτω, βλέπουμε ότι \[ (153)_{10} = (10011001)_2. \]

In [14]:
def tobinary(x):
    "Επιστρέφει μια λίστα που είναι η δυαδική γραφή του φυσικού αριθμού x"
    ld = []
    while x>0:
        y = x%2
        ld = [y] + ld
        x = (x-y)/2
    return ld

print tobinary(153)
[1, 0, 0, 1, 1, 0, 0, 1]

Με τον ίδιο ακριβώς τρόπο μπορεί κανείς να υπολογίσει το 16αδικό ανάπτυγμα (\(D=16\)) ενός αριθμού, μια άλλη πολύ χρήσιμη περίπτωση. Στον παραπάνω κώδικα αρκεί απλώς να αλλάξει κανείς τις δύο γραμμές y = x%2 και x = (x-y)/2 σε y = x%16 και x = (x-y)/16 για να δουλέψει το πρόγραμμα μια χαρά και να μας επιστρέψει το πρόγραμμα τη λίστα των 16αδικών ψηφίων του x.

Τα 16αδικά ψηφία ενός αριθμού μπορούν να πάρουν όλες τις τιμές από 0 έως 15 και συνηθίζεται για τα ψηφία που είναι ίσα με 10, 11, 12, 13, 14 ή 15 να γράφουμε αντί αυτών των αριθμών τα γράμματα του λατινικού αλφαβήτου A, B, C, D, E, F.

Για να το υλοποιήσουμε αυτό στο πρόγραμμά μας παρακάτω ορίζουμε μια λίστα dn που περιέχει τα γράμματα αυτά με αυτή τη σειρά και αν το ψηφίο που υπολογίσαμε προκύψει να είναι > 9 τότε αντί για το y καταχωρούμε το dn[y-10] στη λίστα των ψηφίων.

Προκύπτει έτσι με το παράδειγμα που υπολογίζουμε παρακάτω ότι \[ (200)_{10} = (C8)_{16}. \]

In [19]:
def tohex(x):
    "Επιστρέφει μια λίστα που είναι η δεκαεξαδική γραφή του φυσικού αριθμού x"
    dn = ['A', 'B', 'C', 'D', 'E', 'F']
    ld = []
    while x>0:
        y = x%16
        x = (x-y)/16
        if y>9:
            y = dn[y-10]
        ld = [y] + ld
    return ld

print tohex(200)
['C', 8]