Διάλεξη 2ας/7ης Νοεμβρίου 2016

Συναρτήσεις. Μερικά ακόμα παραδείγματα. Αναδρομικές συναρτήσεις. Καθολικές μεταβλητές

Πρόβλημα 1. Γράψτε μια συνάρτηση η οποία με όρισμα μια ακολουθία χαρακτήρων s επιστρέφει την ίδια αλλά χωρίς τα φωνήεντα. Για παράδειγμα, αν δοθεί ως όρισμα η ακολουθία 'maria' η συνάρτηση θα πρέπει να επιστρέψει την ακολουθία 'mr'.


def delVowels(s):
    t = ''
    for c in s:
        if c in 'aeiouAEIOU': continue
        t += c
    return t

# Test the function

s = input('Enter some string: ')
print('Here is the string', s, 'without the vowels:', delVowels(s))

Πρόβλημα 2. Γράψτε τη συνάρτηση noduplicate η οποία με όρισμα μια ακολουθία χαρακτήρων s επιστρέφει True αν η συμβολοσειρά δεν περιέχει το ίδιο γράμμα δύο ή περισσότερες φορές, False διαφορετικά.


def noduplicate(s):
    for i in range(len(s)):
        if s[i] in s[i+1:]:
            return False
    return True

# Test the function

s = input('Enter some string: ')
if noduplicate(s):
    print('The string', s, 'does not contain duplicate letters')
else:
    print('The string', s, 'contains duplicate letters')

Πρόβλημα 3. Γράψτε μια συνάρτηση η οποία μετατρέπει θερμοκρασίες από την κλίμακα Fahrenheit στην κλίμακα Celsius ή αντίστροφα, ανάλογα με την τιμή ενός δεύτερου, προαιρετικού ορίσματος. Αν αυτό έχει την τιμή 'F2C' η μετατροπή γίνεται από την κλίμακα Fahrenheit στην κλίμακα Celsius. Αν το όρισμα αυτό αυτό έχει την τιμή 'C2F' η μετατροπή γίνεται από την κλίμακα Celsius στην κλίμακα Fahrenheit. Αν το όρισμα απουσιάζει ή έχει οποιαδήποτε άλλη τιμή η μετατροπή γίνεται από την κλίμακα Fahrenheit στην κλίμακα Celsius.


def tempConv(temp, way='F2C'):
    if way == 'C2F':
        return 32+9*temp/5
    else:
        return 5*(temp-32)/9

# Test the function

# Convert 0, 5, 10, 15,...,50 degrees Celsius to degrees Fahrenheit

for t in range(0,55,5):
    print(t, 'degrees Celsius correspond to', tempConv(t, 'C2F'), 'degrees Fahrenheit')

# Convert -50, -45, -40,...,35 degrees Fahrenheit to degrees Celsius

for t in range(-50,40,5):
    print(t, 'degrees Fahrenheit correspond to', tempConv(t), 'degrees Celsius')

Πρόβλημα 4. Γράψτε τη συνάρτηση nameScore η οποία δέχεται ως όρισμα μια ακολουθία χαρακτήρων και επιστρέφει το βαθμό της, ένα φυσικό αριθμό, ο οποίος υπολογίζεται ως εξής: κάθε φωνήεν συνεισφέρει 2 στο βαθμό και κάθε σύμφωνο -1.


def nameScore(s):
    score = 0
    for c in s:
        if c in 'aeiou':
            score += 2
        else:
            score -= 1
    return score

# Test the function

s = input('Enter some string: ')
print('The score of the string', s, 'is', numScore(s))

Πρόβλημα 5. Γράψτε μια συνάρτηση η οποία κωδικοποιεί ακολουθίες χαρακτήρων αντικαθιστώντας κάθε γράμμα με το γράμμα που βρίσκεται 13 θέσεις δεξιότερα στο αλφάβητο. Για παράδειγμα, η λέξη 'cat' θα κωδικοποιηθεί ως 'png'.


def rot13(s):
    chars = 'abcdefghijklmnopqrstuvwxyz'
    codes = 'nopqrstuvwxyzabcdefghijklm'  # codes = chars[13:] + chars[:13]

    msg = ''

    for c in s:
        i = 0
	while i < 26 and chars[i] != c:
            i += 1
        if i == 26:
            msg += c 
	else:
            msg += codes[i]
    return msg

# Test the function

s = input('Enter some string: ')
print('Original message:', s)
print('Encoded message: ', rot13(s))

Πρόβλημα 6. Γράψτε τη συνάρτηση minFib η οποία με όρισμα κάποιον πραγματικό αριθμό Μ επιστρέφει τον μικρότερο όρο της ακολουθίας Fibonacci ο οποίος είναι μεγαλύτερος του Μ. Υπενθυμίζουμε ότι οι όροι της ακολουθίας Fibonacci είναι $F_0 = 0, F_1 = 1$ και $F_n = F_{n-1} + F_{n-2}$ για $n\ge 2$.


def minFib(M):
    if M < 0:
        return 0
    elif M < 1:
        return 1
    else:
        x = 0
        y = 1
        while 1:
           z = x + y
           if z > M: break
           x = y
           y = z
    return z
        
# Test the function

M = float(input('Enter some real number: '))
print('The smallest Fibonacci number gretater than', M, 'is', minFib(M))

Πρόβλημα 7. Γράψτε μια συνάρτηση η οποία υπολογίζει τον μικρότερο αριθμό κερμάτων που χρειαζόμαστε για να κάνουμε «ψιλά» ένα συγκεκριμένο ποσό σε ευρώ και λεπτά.


def makeChange(amount):
    amount = int(100*amount)

    ncoins = 0
    while amount >= 50:
        amount -= 50
        ncoins += 1
    while amount >= 20:
        amount -= 20
        ncoins += 1
    while amount >= 10:
        amount -= 10
        ncoins += 1
    while amount >= 5:
        amount -= 5
        ncoins += 1
    while amount >= 2:
        amount -= 2
        ncoins += 1

    return ncoins + amount

# Test the function

amount = float(input('Enter an amount [euros and cents]: '))
print('Number of coins to break', amount, 'euros is', makeChange(mount))

Πρόβλημα 8. Το Τμήμα Μαθηματικών απαιτεί από τους χρήστες των υπολογιστικών συστημάτων του να χρησιμοποιούν κωδικούς (passwords) οι οποίοι να ικανοποιούν τα παρακάτω κριτήρια:

  1. Να αποτελούνται από 6 ως 12 χαρακτήρες
  2. Να περιέχουν τουλάχιστον ένα πεζό και ένα κεφαλαίο γράμμα
  3. Να περιέχουν τουλάχιστον ένα ψηφίο και τουλάχιστον ένα από τους χαρακτήρες '$#@'.

Γράψτε μια συνάρτηση η οποία βαθμολογεί έναν κωδικό πρόσβασης με 100 αν πληροί όλες τις παραπάνω προϋποθέσεις και αφαιρεί 10 βαθμούς για κάθε κριτήριο που δεν ικανοποιείται.


def checkPassword(p):
    score = 100

#   Check password length

    if len(p) < 6 or len(p) > 12:
        score -= 10

#   Check for a capital letter

    found = False

    for c in p:
	if c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
            found = True
            break

    if not found:
         score -= 10

#   Check for a small letter

    found = False

    for c in p:
        if c in 'abcdefghijklmnopqrstuvwxyz':
            found = True
            break

    if not found:
         score -= 10

#   Check for a digit

    found = False

    for c in p:
        if c in '0123456789':
            found = True
            break

    if not found:
         score -= 10

#   Check for the characters '$#@'
     
    found = False

    for c in p:
        if c in '$#@':
            found = True
            break

    if not found:
         score -= 10

    return score

# Test the function

passwd = input('Check your password: ')
score = checkPassword(passwd)
print('Your password score is', score, 'out of a possible 100')

Αναδρομικές συναρτήσεις

Ο πιο γνωστός, ίσως, αναδρομικός ορισμός τον οποίο γνωρίζουμε είναι αυτός του $n! = 1\cdot 2\cdot \cdots n$ ($n$ παραγοντικό). Συγκεκριμένα, μπορούμε να ορίσουμε $1! = 1$ και $(n+1)! = (n+1) \cdot n!$ για $n\ge 1$. Εύκολα μπορεί να βεβαιωθεί κανείς ότι οι δύο ορισμοί είναι ισοδύναμοι. Στην Python μπορούμε να γράψουμε δύο εκδοχές της συνάρτησης παραγοντικό, με την πρώτη να χρησιμοποιεί τον ορισμό $n! = 1\cdot 2\cdot \cdots n$ και η δεύτερη τον αναδρομικό ορισμό:


def factI(n):
    if n <= 1:
        return 1
    p = 1
    while n > 1:
        p *= n
        n -= 1
    return p

def factR(n):
    if n <= 1:
        return 1
    return n * factR(n-1)

# Test both functions

n = int(input('Enter a positive integer: '))

print(str(n) + '! using the iterative definition: ', factI(n))
print(str(n) + '! using the recursive definition: ', factR(n))

Θα μπορούσαμε ακόμα να γράψουμε και μια αναδρομική συνάρτηση για τον υπολογισμό των όρων της ακολουθίας Fibonacci, η οποία υλοποιεί με προφανή τρόπο τον ορισμό $F_n = F_{n-1} + F_{n-2},\, n\ge 2$.


def fibR(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibR(n-1) + fibR(n-2)

Εδώ, αξίζει τον κόπο να παρατηρήσει κανείς ότι ο υπολογισμός του όρου $F_{10}$ απαιτεί την αναδρομική κλήση της συνάρτησης fibR 177 φορές (βεβαιωθείτε ότι το καταλαβαίνετε). Ο αριθμός των αναδρομικών κλήσεων της fibR για τον υπολογισμό του $F_{30} = 1346269$ είναι 2692537, και όπως καταλαβαίνει κανείς ο υπολογισμός των όρων της ακολουθίας Fibonacci με τον συγκεκριμένο τρόπο γίνεται εξαιρετικά αργός για μεγάλες τιμές του $n$.


import time

def fibR(x):
        if x == 0 or x == 1:
                return 1
        else:
                return fibR(x-1) + fibR(x-2)

def fibI(n):
        if n == 0 or n == 1:
                return 1
        else:
                x = 1
                y = 1
                for i in range(2,n+1):
                        z = x + y
                        x = y
                        y = z
                return z

# Compute time it takes to compute F_20 using both methods

t0 = time.time()
f20 = fibI(20)
t1 = time.time()
print('It took', 1.0e6*(t1-t0), 'micro-seconds to compute F_20 using iterative method')

t0 = time.time()
f20 = fibR(20)
t1 = time.time()
print('It took', 1.0e6*(t1-t0), 'micro-seconds to compute F_20 using recursive method')

Στον προσωπικό μου υπολογιστή, ο υπολογισμός του $F_{20}$ με χρήση της συνάρτησης fibI χρειάστηκε περίπου $6\,\mu sec$ ενώ η fibR χρειάστηκε περίπου $3900\,\mu sec$, δηλαδή 650 φορές περισσότερο χρόνο!

Οι αναδρομικές συναρτήσεις είναι χρήσιμες και για μη μαθηματικά προβλήματα. Ως παράδειγμα, ας σκεφτούμε πως θα ελέγξουμε αν μια ακολουθία χαρακτήρων είναι παλινδρομική ή όχι. Θα μπορούσαμε, για παράδειγμα να ορίσουμε τη συνάρτηση


def isPalindrome1(s):
    return s == s[::-1]

ή την


def isPalindrome2(s):
    i = 0
    j = len(s)-1
    while i < j and s[i] == s[j]:
        i += 1
        j -= 1
    return i == j

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


def isPalindrome3(s):
    if len(s) <= 1:
	return True
    else:
        return s[0] == s[-1] and isPalindrome3(s[1:-1])

Παρατηρήστε ότι ο παραπάνω κώδικας ελέγχει πρώτα αν ο πρώτος και ο τελευταίος χαρακτήρας είναι ίσοι και μετά κατά πόσο η ακολουθία χαρακτήρων εκτός του πρώτου και του τελευταίου όρου είναι παλινδρομική. Η τεχνική της αναγωγής της λύσης του προβλήματος σε ένα απλούστερο, γνωστή με το όνομα διαίρει και βασίλευε (divide and conquer), είναι μια πολύ σημαντική τεχνική στη θεωρία των αλγορίθμων και οδηγεί σε εξαιρετικά κομψές λύσεις δύσκολων προβλημάτων.

Καθολικές μεταβλητές (global variables)

Μέχρι στιγμής είδαμε ότι η συναρτήσεις επικοινωνούν με το υπόλοιπο μέρος ενός προγράμματος χρησιμοποιώντας τα ορίσματά τους και τις τιμές που επιστρέφουν. Αυτό, συνήθως, είναι αρκετό για να καλύψει τις ανάγκες των περισσοτέρων προγραμμάτων, αλλά η Python παρέχει και ένα δεύτερο τρόπο, αυτό των καθολικών μεταβλητών. Όπως μαρτυρεί το όνομά τους πρόκειται για μεταβλητές οι οποίες είναι γνωστές σε όλους τους χώρους ονομάτων. Ας θεωρήσουμε για παράδειγμα το το πρόβλημα του υπολογισμού των όρων της ακολουθίας Fibonacci χρησιμοποιώντας την αναδρομική συνάρτηση fibR που γράψαμε παραπάνω. Αν θα θέλαμε να μετρήσουμε τον συνολικό αριθμό των αναδρομικών κλήσεων της συνάρτησης fibR θα μπορούσαμε να εισάγουμε μαι καθολική μεταβλητή την οποία να αυξάνουμε κατά ένα κάθε φορά που καλείται εκ νέου η συνάρτηση fibR.


def fibR(n):
    global numCalls
    numCalls += 1
    if n == 0 or n == 1:
        return 1
    else:
        return fibR(n-1) + fibR(n-2)

# Test the function fibR

global numCalls
numCalls = 0
print('fibR called', numCalls, 'times')