web-dev-qa-db-de.com

Identifizieren Sie Gruppen fortlaufender Nummern in einer Liste

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.

70
mikemaccana

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.

19
pylang

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]))
104
Nadia Alramli

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)]
15
truppo

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)]
12
SilentGhost

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
8
Andrea Ambu

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]))
6
Mark Lawrence

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.

3
Mark Byers

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]]
1
mikemaccana
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]
1
user5049920

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).

0
coldfix

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. 

0
Nir

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]]
0