Ich möchte Gruppen von fortlaufenden Zahlen in einer Liste identifizieren, so dass:
myfunc([2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20])
Kehrt zurück:
[(2,5), (12,17), 20]
Und fragte mich, was der beste Weg war, dies zu tun (vor allem, wenn in Python etwas eingebaut ist).
Bearbeiten: Hinweis Ich habe ursprünglich vergessen zu erwähnen, dass einzelne Zahlen als individuelle Zahlen zurückgegeben werden sollten und nicht als Bereiche.
more_itertools.consecutive_groups
wurde in Version 4.0 hinzugefügt.
Demo
import more_itertools as mit
iterable = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20]
[list(group) for group in mit.consecutive_groups(iterable)]
# [[2, 3, 4, 5], [12, 13, 14, 15, 16, 17], [20]]
Code
Mit diesem Werkzeug erstellen wir eine Generatorfunktion, die Bereiche aufeinanderfolgender Zahlen findet.
def find_ranges(iterable):
"""Yield range of consecutive numbers."""
for group in mit.consecutive_groups(iterable):
group = list(group)
if len(group) == 1:
yield group[0]
else:
yield group[0], group[-1]
iterable = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20]
list(find_ranges(iterable))
# [(2, 5), (12, 17), 20]
Die source - Implementierung emuliert ein klassisches Rezept (wie von @Nadia Alramli gezeigt).
Hinweis: more_itertools
ist ein Paket eines Drittanbieters, das über pip install more_itertools
installiert werden kann.
EDIT 2: Zur Beantwortung der neuen Anforderung des OP
ranges = []
for key, group in groupby(enumerate(data), lambda (index, item): index - item):
group = map(itemgetter(1), group)
if len(group) > 1:
ranges.append(xrange(group[0], group[-1]))
else:
ranges.append(group[0])
Ausgabe:
[xrange(2, 5), xrange(12, 17), 20]
Sie können xrange durch range oder jede andere benutzerdefinierte Klasse ersetzen.
Python-Dokumente haben ein sehr ordentliches Rezept dafür:
from operator import itemgetter
from itertools import groupby
data = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17]
for k, g in groupby(enumerate(data), lambda (i,x):i-x):
print map(itemgetter(1), g)
Ausgabe:
[2, 3, 4, 5]
[12, 13, 14, 15, 16, 17]
Wenn Sie genau dieselbe Ausgabe erhalten möchten, können Sie Folgendes tun:
ranges = []
for k, g in groupby(enumerate(data), lambda (i,x):i-x):
group = map(itemgetter(1), g)
ranges.append((group[0], group[-1]))
ausgabe:
[(2, 5), (12, 17)]
EDIT: Das Beispiel ist bereits in der Dokumentation erklärt, aber vielleicht sollte ich es näher erläutern:
Der Schlüssel zur Lösung ist Differenzierung mit einem Bereich, so dass fortlaufende Nummern erscheinen alle in derselben Gruppe.
Wenn die Daten: [2, 3, 4, 5, 12, 13, 14, 15, 16, 17]
.__ waren, entspricht groupby(enumerate(data), lambda (i,x):i-x)
Folgendes:
groupby(
[(0, 2), (1, 3), (2, 4), (3, 5), (4, 12),
(5, 13), (6, 14), (7, 15), (8, 16), (9, 17)],
lambda (i,x):i-x
)
Die Lambda-Funktion subtrahiert den Elementindex vom Elementwert. Also, wenn Sie das Lambda auf jeden Artikel anwenden. Sie erhalten die folgenden Schlüssel für die Gruppierung:
[-2, -2, -2, -2, -8, -8, -8, -8, -8, -8]
groupby gruppiert Elemente nach gleichem Schlüsselwert, so dass die ersten 4 Elemente zusammen gruppiert werden und so weiter.
Ich hoffe, das macht es lesbarer.
Die python 3
-Version kann für Anfänger hilfreich sein
importieren Sie zuerst die erforderlichen Bibliotheken
from itertools import groupby
from operator import itemgetter
ranges =[]
for k,g in groupby(enumerate(data),lambda x:x[0]-x[1]):
group = (map(itemgetter(1),g))
group = list(map(int,group))
ranges.append((group[0],group[-1]))
Die "naive" Lösung, die ich zumindest etwas lesbar finde.
x = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 22, 25, 26, 28, 51, 52, 57]
def group(L):
first = last = L[0]
for n in L[1:]:
if n - 1 == last: # Part of the group, bump the end
last = n
else: # Not part of the group, yield current group and start a new
yield first, last
first = last = n
yield first, last # Yield the last group
>>>print list(group(x))
[(2, 5), (12, 17), (22, 22), (25, 26), (28, 28), (51, 52), (57, 57)]
Angenommen, Ihre Liste ist sortiert:
>>> from itertools import groupby
>>> def ranges(lst):
pos = (j - i for i, j in enumerate(lst))
t = 0
for i, els in groupby(pos):
l = len(list(els))
el = lst[t]
t += l
yield range(el, el+l)
>>> lst = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17]
>>> list(ranges(lst))
[range(2, 6), range(12, 18)]
Hier sollte etwas funktionieren, ohne dass ein Import erforderlich ist:
def myfunc(lst):
ret = []
a = b = lst[0] # a and b are range's bounds
for el in lst[1:]:
if el == b+1:
b = el # range grows
else: # range ended
ret.append(a if a==b else (a,b)) # is a single or a range?
a = b = el # let's start again with a single
ret.append(a if a==b else (a,b)) # corner case for last single/range
return ret
Bitte beachten Sie, dass der Code, der groupby
verwendet, nicht wie in Python 3 angegeben funktioniert. Verwenden Sie dies also.
for k, g in groupby(enumerate(data), lambda x:x[0]-x[1]):
group = list(map(itemgetter(1), g))
ranges.append((group[0], group[-1]))
Dies verwendet keine Standardfunktion - es wird nur über die Eingabe geschrieben, aber es sollte funktionieren:
def myfunc(l):
r = []
p = q = None
for x in l + [-1]:
if x - 1 == q:
q += 1
else:
if p:
if q > p:
r.append('%s-%s' % (p, q))
else:
r.append(str(p))
p = q = x
return '(%s)' % ', '.join(r)
Beachten Sie, dass die Eingabe nur positive Zahlen in aufsteigender Reihenfolge enthält. Sie sollten die Eingabe überprüfen, dieser Code wird jedoch der Übersichtlichkeit halber weggelassen.
Hier ist die Antwort, die ich mir ausgedacht habe. Ich schreibe den Code, damit andere Leute ihn verstehen können, also bin ich ziemlich ausführlich mit Variablennamen und Kommentaren.
Zunächst eine schnelle Hilfsfunktion:
def getpreviousitem(mylist,myitem):
'''Given a list and an item, return previous item in list'''
for position, item in enumerate(mylist):
if item == myitem:
# First item has no previous item
if position == 0:
return None
# Return previous item
return mylist[position-1]
Und dann der eigentliche Code:
def getranges(cpulist):
'''Given a sorted list of numbers, return a list of ranges'''
rangelist = []
inrange = False
for item in cpulist:
previousitem = getpreviousitem(cpulist,item)
if previousitem == item - 1:
# We're in a range
if inrange == True:
# It's an existing range - change the end to the current item
newrange[1] = item
else:
# We've found a new range.
newrange = [item-1,item]
# Update to show we are now in a range
inrange = True
else:
# We were in a range but now it just ended
if inrange == True:
# Save the old range
rangelist.append(newrange)
# Update to show we're no longer in a range
inrange = False
# Add the final range found to our list
if inrange == True:
rangelist.append(newrange)
return rangelist
Beispiellauf:
getranges([2, 3, 4, 5, 12, 13, 14, 15, 16, 17])
kehrt zurück:
[[2, 5], [12, 17]]
import numpy as np
myarray = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20]
sequences = np.split(myarray, np.array(np.where(np.diff(myarray) > 1)[0]) + 1)
l = []
for s in sequences:
if len(s) > 1:
l.append((np.min(s), np.max(s)))
else:
l.append(s[0])
print(l)
Ausgabe:
[(2, 5), (12, 17), 20]
Eine kurze Lösung, die ohne zusätzliche Importe funktioniert. Es akzeptiert alle Iterationen, sortiert unsortierte Eingaben und entfernt doppelte Elemente:
def ranges(nums):
nums = sorted(set(nums))
gaps = [[s, e] for s, e in Zip(nums, nums[1:]) if s+1 < e]
edges = iter(nums[:1] + sum(gaps, []) + nums[-1:])
return list(Zip(edges, edges))
Beispiel:
>>> ranges([2, 3, 4, 7, 8, 9, 15])
[(2, 4), (7, 9), (15, 15)]
>>> ranges([-1, 0, 1, 2, 3, 12, 13, 15, 100])
[(-1, 3), (12, 13), (15, 15), (100, 100)]
>>> ranges(range(100))
[(0, 99)]
>>> ranges([0])
[(0, 0)]
>>> ranges([])
[]
Dies ist dasselbe wie @dansalmos solution , was ich erstaunlich fand, wenn auch etwas schwer zu lesen und anzuwenden (da es nicht als Funktion angegeben ist).
Man beachte, dass er leicht modifiziert werden kann, um "herkömmliche" offene Bereiche [start, end)
auszuspucken, z. die return-Anweisung ändern:
return [(s, e+1) for s, e in Zip(edges, edges)]
Ich habe diese Antwort von einer anderen Frage kopiert, die als Duplikat dieser mit der Absicht gekennzeichnet war, sie leichter auffindbar zu machen (nachdem ich gerade erst wieder nach diesem Thema gesucht habe und nur die Frage hier zuerst finde und nicht zu sein) zufrieden mit den gegebenen Antworten).
Verwenden von numpy + Verständnislisten:
Mit der Funktion "numpy diff" können nachfolgende Eingabevektoreingaben identifiziert werden, bei denen ihre Differenz nicht gleich eins ist. Der Anfang und das Ende des Eingabevektors müssen berücksichtigt werden.
import numpy as np
data = np.array([2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20])
d = [i for i, df in enumerate(np.diff(data)) if df!= 1]
d = np.hstack([-1, d, len(data)-1]) # add first and last elements
d = np.vstack([d[:-1]+1, d[1:]]).T
print(data[d])
Ausgabe:
[[ 2 5]
[12 17]
[20 20]]
Hinweis: Die Anforderung, dass einzelne Zahlen unterschiedlich behandelt werden sollten (als Einzelwert und nicht als Bereich zurückgegeben), wurde ausgelassen. Dies kann durch weitere Nachbearbeitung der Ergebnisse erreicht werden. Normalerweise macht dies die Dinge komplexer, ohne einen Nutzen daraus zu ziehen.
Wenn Sie groupby
und count
aus itertools
verwenden, erhalten Sie eine kurze Lösung. Die Idee ist, dass die Differenz zwischen Index und Wert in einer aufsteigenden Reihenfolge gleich bleibt.
Um den Index zu verfolgen, können wir ein itertools.count verwenden, wodurch der Code sauberer wird als mit enumerate
:
from itertools import groupby, count
def intervals(data):
out = []
counter = count()
for key, group in groupby(data, key = lambda x: x-next(counter)):
block = list(group)
out.append([block[0], block[-1]])
return out
Einige Beispielausgaben:
print(intervals([0, 1, 3, 4, 6]))
# [[0, 1], [3, 4], [6, 6]]
print(intervals([2, 3, 4, 5]))
# [[2, 5]]