29 Stimmen

Listenverständnis für laufende Summe

Ich möchte eine laufende Summe aus einer Liste von Zahlen erhalten.

Zu Demonstrationszwecken beginne ich mit einer fortlaufenden Liste von Zahlen mit range

a = range(20)

runningTotal = []
for n in range(len(a)):
    new = runningTotal[n-1] + a[n] if n > 0 else a[n]
    runningTotal.append(new)

# This one is a syntax error
# runningTotal = [a[n] for n in range(len(a)) if n == 0 else runningTotal[n-1] + a[n]]

for i in zip(a, runningTotal):
    print "{0:>3}{1:>5}".format(*i)

ergibt

  0    0
  1    1
  2    3
  3    6
  4   10
  5   15
  6   21
  7   28
  8   36
  9   45
 10   55
 11   66
 12   78
 13   91
 14  105
 15  120
 16  136
 17  153
 18  171
 19  190

Wie Sie sehen können, initialisiere ich eine leere Liste [] entonces append() in jeder Schleifeniteration. Gibt es einen eleganteren Weg zu diesem, wie eine Liste Verständnis?

7voto

April Arcus Punkte 89

Wenn wir die Summe einer Liste bilden, bezeichnen wir einen Akkumulator ( memo ) und gehen dann durch die Liste, wobei sie die Binärfunktion "x+y" auf jedes Element und den Akkumulator anwenden. Verfahrenstechnisch sieht das so aus:

def mySum(list):
    memo = 0
    for e in list:
        memo = memo + e
    return memo

Dies ist ein gängiges Muster und nicht nur für Summen nützlich - wir können es auf jede binäre Funktion verallgemeinern, die wir als Parameter übergeben, und dem Aufrufer auch erlauben, einen Anfangswert anzugeben. Damit erhalten wir eine Funktion, die als reduce , foldl , oder inject [1] :

def myReduce(function, list, initial):
    memo = initial
    for e in list:
        memo = function(memo, e)
    return memo

def mySum(list):
    return myReduce(lambda memo, e: memo + e, list, 0)

In Python 2, reduce war eine eingebaute Funktion, aber in Python 3 wurde sie in die functools Modul:

from functools import reduce

Wir können alle möglichen coolen Sachen machen mit reduce abhängig von der Funktion, die wir als erstes Argument angeben. Wenn wir "Summe" durch "Listenverkettung" und "Null" durch "leere Liste" ersetzen, erhalten wir die (oberflächliche) copy Funktion:

def myCopy(list):
    return reduce(lambda memo, e: memo + [e], list, [])

myCopy(range(10))
> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Fügen wir eine transform Funktion als weiteren Parameter für copy und wendet es vor der Verkettung an, so erhält man map :

def myMap(transform, list):
    return reduce(lambda memo, e: memo + [transform(e)], list, [])

myMap(lambda x: x*2, range(10))
> [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Fügen wir eine predicate Funktion, die die e als Parameter verwendet und einen booleschen Wert zurückgibt und diesen verwendet, um zu entscheiden, ob verkettet werden soll oder nicht, erhalten wir filter :

def myFilter(predicate, list):
    return reduce(lambda memo, e: memo + [e] if predicate(e) else memo, list, [])

myFilter(lambda x: x%2==0, range(10))
> [0, 2, 4, 6, 8]

map y filter sind eine etwas unschöne Art und Weise, Listenauffassungen zu schreiben - wir hätten auch sagen können [x*2 for x in range(10)] o [x for x in range(10) if x%2==0] . Es gibt keine entsprechende Syntax für das Verstehen von Listen für reduce denn reduce muss überhaupt keine Liste zurückgeben (wie wir es bei sum die Python zufällig auch als eingebaute Funktion anbietet).

Es hat sich herausgestellt, dass für die Berechnung einer laufenden Summe die Fähigkeiten zur Listenerstellung von reduce sind genau das, was wir wollen, und wahrscheinlich der eleganteste Weg, um dieses Problem zu lösen, trotz seines Rufs (zusammen mit lambda ) als eine Art unpythonisches Schibboleth. Die Version von reduce das bei seiner Ausführung Kopien seiner alten Werte hinterlässt, heißt reductions o scanl [1] und es sieht so aus:

def reductions(function, list, initial):
    return reduce(lambda memo, e: memo + [function(memo[-1], e)], list, [initial])

So ausgerüstet, können wir nun definieren:

def running_sum(list):
    first, rest = list[0], list[1:]
    return reductions(lambda memo, e: memo + e, rest, first)

running_sum(range(10))
> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

Dieser präzise Ansatz ist zwar konzeptionell elegant, funktioniert aber in der Praxis mit Python nur schlecht. Weil Pythons list.append() eine Liste an Ort und Stelle verändert, sie aber nicht zurückgibt, können wir sie nicht effektiv in einem Lambda verwenden und müssen die + stattdessen den Operator. Dadurch wird eine völlig neue Liste erstellt, was Zeit in Anspruch nimmt, die proportional zur Länge der bisher aufgelaufenen Liste ist (also eine O(n)-Operation). Da wir uns bereits innerhalb der O(n) for Schleife von reduce Wenn wir dies tun, erhöht sich die gesamte Zeitkomplexität auf O(n 2 ).

In einer Sprache wie Ruby [2] , wobei array.push e gibt die mutierte array läuft das Äquivalent in O(n)-Zeit:

class Array
  def reductions(initial, &proc)
    self.reduce [initial] do |memo, e|
      memo.push proc.call(memo.last, e)
    end
  end
end

def running_sum(enumerable)
  first, rest = enumerable.first, enumerable.drop(1)
  rest.reductions(first, &:+)
end

running_sum (0...10)
> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

dasselbe in JavaScript [2] , dessen array.push(e) gibt zurück. e (nicht array ), aber deren anonyme Funktionen erlauben uns, mehrere Anweisungen einzuschließen, die wir verwenden können, um einen Rückgabewert separat anzugeben:

function reductions(array, callback, initial) {
    return array.reduce(function(memo, e) {
        memo.push(callback(memo[memo.length - 1], e));
        return memo;
    }, [initial]);
}

function runningSum(array) {
    var first = array[0], rest = array.slice(1);
    return reductions(rest, function(memo, e) {
        return x + y;
    }, first);
}

function range(start, end) {
    return(Array.apply(null, Array(end-start)).map(function(e, i) {
        return start + i;
    }
}

runningSum(range(0, 10));
> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

Wie können wir also dieses Problem lösen und gleichzeitig die konzeptionelle Einfachheit einer reductions Funktion, die wir gerade übergeben lambda x, y: x + y zu, um die Funktion der laufenden Summe zu erstellen? Lassen Sie uns umschreiben reductions verfahrenstechnisch. Wir können die zufällig quadratisch Problem, und wenn wir schon dabei sind, die Ergebnisliste vorab zuzuweisen, um Heap Thrashing zu vermeiden [3] :

def reductions(function, list, initial):
    result = [None] * len(list)
    result[0] = initial
    for i in range(len(list)):
        result[i] = function(result[i-1], list[i])
    return result

def running_sum(list):
    first, rest = list[0], list[1:]
    return reductions(lambda memo, e: memo + e, rest, first)

running_sum(range(0,10))
> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

Das ist für mich der richtige Punkt: O(n)-Leistung, und der optimierte prozedurale Code ist unter einem aussagekräftigen Namen versteckt, wo er wiederverwendet werden kann, wenn Sie das nächste Mal eine Funktion schreiben müssen, die Zwischenwerte in einer Liste akkumuliert.

  1. Die Namen reduce / reductions stammen aus der LISP-Tradition, foldl / scanl aus der ML-Tradition, und inject aus der Smalltalk-Tradition.
  2. Pythons List und Ruby's Array sind beide Implementierungen einer Datenstruktur mit automatischer Größenänderung, die als "dynamisches Array" (oder std::vector in C++). JavaScript ist Array ist ein wenig barocker, verhält sich aber identisch, vorausgesetzt, dass Sie nicht an Indizes außerhalb der Grenzen zuweisen oder mutieren Array.length .
  3. Das dynamische Array, das den Hintergrundspeicher der Liste in der Python-Laufzeitumgebung bildet, ändert seine Größe jedes Mal, wenn die Länge der Liste eine Zweierpotenz überschreitet. Die Größenänderung einer Liste bedeutet, dass eine neue Liste mit der doppelten Größe der alten Liste auf dem Heap zugewiesen wird, der Inhalt der alten Liste in die neue kopiert wird und der Speicher der alten Liste an das System zurückgegeben wird. Dies ist eine O(n)-Operation, aber da dies immer seltener geschieht, je größer die Liste wird, beträgt die Zeitkomplexität des Anhängens an eine Liste im Durchschnitt O(1). Das "Loch", das die alte Liste hinterlässt, kann jedoch je nach Position im Heap manchmal schwer zu recyceln sein. Selbst mit Garbage Collection und einem robusten Speicherallokator kann die Vorabzuweisung eines Arrays mit bekannter Größe den zugrunde liegenden Systemen einige Arbeit ersparen. In einer eingebetteten Umgebung ohne den Vorteil eines Betriebssystems wird diese Art von Mikro-Management sehr wichtig.

0 Stimmen

Sie haben gerade ein 5 Jahre altes Thema wiederbelebt, aber danke! Ich habe viel gelernt: vor allem, weil ich weiß, dass es sich um ein gängiges Muster handelt und dass es dafür bewährte Verfahren gibt.

1 Stimmen

Kleiner Fehler: Sie müssten Ihre Indizes um 1 korrigieren in reductions an einigen Stellen; oder Sie können Reduktionen so umschreiben, dass sie automatisch das erste Element der Liste als Anfangswert verwenden (wie bei der eingebauten reduce ). Alternativ können Sie auch einfach eine Funktion erstellen, die an eine Liste anhängt und diese zurückgibt, und Folgendes ersetzen .append in Ihrem Original O(N^2) mit dieser Funktion.

0 Stimmen

Denken Sie auch, dass itertools.accumulate ist im Wesentlichen dasselbe wie Ihr reductions oder gibt es sinnvolle Unterschiede zwischen den beiden (abgesehen von der Rückgabe eines Iterators gegenüber einer Liste)?

3voto

user1908017 Punkte 49

Ich wollte dasselbe tun, um kumulative Häufigkeiten zu erzeugen, die ich mit bisect_left bearbeiten kann - so habe ich die Liste erstellt;

[ sum( a[:x] ) for x in range( 1, len(a)+1 ) ]

6 Stimmen

Ich hoffe, Ihre Liste ist nicht sehr lang... das ist O(len(a)^2), genau das.

0 Stimmen

Etwas kürzere Version (und unter Verwendung von xrange): [ sum(a[:x+1]) for x in xrange(len(a)) ]

3voto

Xavier Guihot Punkte 42435

Start: Python 3.8 und die Einführung von Zuweisungsausdrücke (PEP 572) ( := Operator) können wir eine Variable innerhalb eines Listenverständnisses verwenden und inkrementieren:

# items = range(7)
total = 0
[(x, total := total + x) for x in items]
# [(0, 0), (1, 1), (2, 3), (3, 6), (4, 10), (5, 15), (6, 21)]

Dies:

  • Initialisiert eine Variable total a 0 die die laufende Summe symbolisiert
  • Für jedes Element, diese beiden:
    • Inkremente total durch das aktuelle Element in der Schleife ( total := total + x ) über eine Zuordnungsausdruck
    • und gibt gleichzeitig den neuen Wert von total als Teil des erzeugten gemappten Tupels

2voto

topkara Punkte 846

Hier ist ein Einzeiler zur Lösung der linearen Zeit:

list(reduce(lambda (c,s), a: (chain(c,[s+a]), s+a), l,(iter([]),0))[0])

Beispiel:

l = range(10)
list(reduce(lambda (c,s), a: (chain(c,[s+a]), s+a), l,(iter([]),0))[0])
>>> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

Kurz gesagt, das Reduzieren geht über die Liste, akkumuliert die Summe und konstruiert eine Liste. Die letzte x[0] gibt die Liste zurück, x[1] wäre der laufende Gesamtwert.

2voto

nickie Punkte 5388

Ein weiterer Einzeiler, in linearer Zeit und Raum.

def runningSum(a):
    return reduce(lambda l, x: l.append(l[-1]+x) or l if l else [x], a, None)

Ich betone hier den linearen Raum, weil die meisten der Einzeiler, die ich in den anderen Antwortvorschlägen gesehen habe --- die auf dem Muster list + [sum] oder mit chain Iteratoren --- erzeugen O(n)-Listen oder -Generatoren und belasten den Garbage Collector so sehr, dass sie im Vergleich dazu sehr schlecht abschneiden.

0 Stimmen

Das ist sehr elegant! Ich bin ein bisschen an dem 'oder l' Teil hängen geblieben, bis ich gemerkt habe, dass es eine Abkürzung für ...; return(l)

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