5 Stimmen

Warum ist der Geschwindigkeitsunterschied zwischen diesen beiden Funktionen so groß?

Ich habe mir einige der MIT Opencourseware-Quizfragen durchgelesen, und sie hatten eine Frage, die wie folgt lautet:

6) Betrachten Sie die beiden unten angegebenen Funktionen, die zum Spielen eines "Zahlenratespiels" verwendet werden.

def cmpGuess(guess):
"""Assumes that guess is an integer in range(maxVal). returns -1 if guess is < than the magic number, 0 if it is equal to the magic number and 1 if it is greater than the magic number."""
def findNumber(maxVal):
"""Assumes that maxVal is a positive integer. Returns a number, num, such that cmpGuess(num) == 0."""
Write a Python implementation of findNumber that guesses the magic number defined by cmpGuess. Your program should have the lowest time complexity possible. """

Hier ist meine Umsetzung:

def find(maxVal):
    ceiling = maxVal
    floor = 0

    while 1:
        med = ((ceiling + floor) / 2)
        res = cmp(med)
        if res == 1:
            ceiling = med
        elif res == -1:
            floor = med
        else:
            return med

Und hier ist der Antwortbogen, den die Lehrerin oder der Lehrer zur Verfügung gestellt hat:

def findNumber(maxVal): 
    """ Assumes that maxVal is a positive integer. Returns a number, num, such that cmpGuess(num) == 0 """ 
    s = range(0, maxVal) 
    return bsearch(s, 0, len(s) -1)

def bsearch(s, first, last): 
    if (last-first) < 2: 
        if cmp(s[first]) == 0: 
            return first 
    else:
        return last

    mid = first + (last -first)/2
    if cmp(s[mid]) == 0:
        return s[mid]
    if cmp(s[mid]) == -1:
        return bsearch(s, first, mid -1)
    return bsearch(s, mid + 1, last)

Dies ist die cmp-Funktion, die laut Spezifikation von unseren beiden Funktionen verwendet wird:

def cmp(guess):
    if guess > num:
        return 1
    elif guess < num:
        return -1
    else: return 0

Ein großer Unterschied besteht darin, dass meine Lösung iterativ ist und die des Lehrers rekursiv. Ich habe 1000 Durchläufe unserer beiden Funktionen mit einem maxVal = 1.000.000 gemessen. Hier ist ein Ausschnitt der Zeitmessung:

t = timeit.Timer('find(1000000)', 'from __main__ import find,cmp')
t1 = timeit.Timer('findNumber(1000000)', 'from __main__ import findNumber,bsearch')
print str(t.timeit(1000))
print str(t1.timeit(1000))

Mein Lauf dauerte: 0.000621605333677s Der Lauf des Lehrers dauerte: 29.627s
Das kann unmöglich richtig sein. Ich habe die Zeit mehrmals hintereinander gemessen, und in allen Fällen kam die zweite Funktion mit lächerlichen 30s-Ergebnissen an. Ich habe die Lösungsfunktion direkt aus dem vom MIT zur Verfügung gestellten Dokument kopiert und eingefügt. Hat jemand eine Idee?

7voto

Duncan Punkte 85702

Das Offensichtlichste, was ich sehen kann, ist, dass jedes Mal, wenn man die Funktion des Lehrers aufruft, eine Liste mit 1.000.000 ganzen Zahlen erstellt wird (unter der Annahme, dass Python 2.x verwendet wird), und dann, wenn sie zurückkehrt, diese Liste wieder vernichtet wird.

Das wird eine Weile dauern.

4voto

Justin Peel Punkte 46114

Sie sagten, Sie hätten beide Skripte getestet, um sicherzugehen, dass sie die gleiche Antwort geben, aber das tun sie nicht. Das Skript des Lehrers, so wie Sie es geschrieben haben, wird immer das letzte Element zurückgeben. Das liegt an diesen Zeilen:

def bsearch(s, first, last): 
    if (last-first) < 2: 
        if cmp(s[first]) == 0: 
            return first 
    else:
        return last

sollte sein

def bsearch(s, first, last): 
    if (last-first) < 2: 
        if cmp(s[first]) == 0: 
            return first 
        else:
            return last

Mit anderen Worten, es gibt einen Einrückungsfehler, der sich auch in der PDF-Datei auf der MIT-Website findet. Zweitens funktioniert das Skript des Lehrers immer noch nicht richtig (es gibt immer die letzte Zahl zurück), weil seine binäre Suche mit dem Ergebnis von cmp in die falsche Richtung geht. Dies ist ein Fehler in der Lösung des Lehrers gemäß der Spezifikation und kann leicht behoben werden, indem man die folgende Zeile ändert

    if cmp(s[mid]) == -1:

à

    if cmp(s[mid]) == 1:

Dies war nicht wirklich eine Antwort auf die Frage nach der Langsamkeit, die offensichtlich (wie bereits erwähnt) auf die Zuweisung von O(N)-Speicher (das ist das große Problem), die Rekursion, das Nachschlagen in Listen, den potenziell zweimaligen Aufruf von cmp für jeden bsearch-Aufruf anstatt nur einmal und das Speichern des Ergebnisses sowie die explizite Prüfung, ob (last - first) < 2 für jeden Aufruf von bsearch denn er verwendet die Variable last als im Bereich der möglichen Werte enthalten und nicht als 1 mehr als der höchstmögliche Wert. Übrigens kann Ihr eigener Code ein bisschen schneller werden, indem Sie die Zeile ändern:

            floor = med

à

            floor = med + 1

so dass Sie med aus dem Suchbereich ausschließen. Anhand des Ergebnisses von cmp wissen Sie bereits, dass es sich nicht um den Wert handelt. Übrigens, ich würde nicht verwenden cmp als meinen eigenen Funktionsnamen, weil es sich um eine in Python eingebaute Funktion handelt (ich weiß, dass es eine cmpGuess in der Spezifikation).

3voto

Bolo Punkte 11234

Lassen Sie mich meinen Kommentar in Form einer Antwort wiederholen: range(maxval) die gesamte Liste zugewiesen, so dass der Algorithmus des Lehrers (maxval) Raumkomplexität und damit (maxval) Zeitaufwand (die Erstellung einer solchen Liste erfordert Zeit). Daher ist die Lösung des Lehrers nicht die "geringstmögliche zeitliche Komplexität" haben.

Wenn xrange(maxval) verwendet wird, wird nicht mehr die gesamte Liste zugewiesen. Sowohl Ihre Lösung als auch die des Lehrers haben (log(maxval)) Zeitkomplexität.

Außerdem hat Ihre Lösung (1) Raumkomplexität, während die (optimierte) Lösung des Lehrers eine (log(maxval)) Raumkomplexität - rekursive Aufrufe verbrauchen Stack-Speicher.

3voto

John Machin Punkte 78125

Die Lehrerversion weist drei Fehler auf, die in der OP-Version behoben wurden.

  1. s = range(maxVal) erstellt eine Liste in Python 2. Die Verwendung von xrange() spart die Zeit für das Erstellen und Löschen der Liste. Die ganze Idee, s zu verwenden, ist jedoch unsinnig, weil s[i] == i für alle relevanten i, so dass s einfach weggeworfen werden kann, was das Nachschlagen erspart.

  2. Rekursion: Die Rekursionstiefe beträgt etwa math.log(1000000.0, 2.0) ... etwa 20. Das sind also etwa 20 Funktionsaufrufe pro Aufruf von findNumber(), und Funktionsaufrufe sind sehr teuer im Vergleich zum Sprung zurück an den Anfang einer while-Schleife.

  3. Sie fordert cmp(s[mid]) zweimal pro Schätzung statt einmal, das sind also weitere 20 verschwendete Funktionsaufrufe pro Aufruf von findNumber().

0voto

anta40 Punkte 6289

Ich habe im Moment kein Python installiert, so von einem Blick vielleicht, weil der Lehrer verwendet Rekursion (in der bsearch) ?

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X