Identificar grupos de números consecutivos en una lista

6 minutos de lectura

avatar de usuario de mikemaccana
mikemaccana

Me gustaría identificar grupos de números consecutivos en una lista, de modo que:

myfunc([2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20])

Devoluciones:

[(2,5), (12,17), 20]

Y me preguntaba cuál era la mejor manera de hacer esto (particularmente si hay algo incorporado en Python).

Editar: tenga en cuenta que originalmente olvidé mencionar que los números individuales deben devolverse como números individuales, no como rangos.

  • ¿Ese valor de retorno es una cadena?

    –Mark Byers

    28 de enero de 2010 a las 11:59

  • Idealmente, preferiría algo que use un tipo separado para rangos frente a números independientes.

    – mikemaccana

    28 de enero de 2010 a las 13:47

Avatar de usuario de Nadia Alramli
nadia alramli

EDIT 2: Para responder al nuevo requisito 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])

Producción:

[xrange(2, 5), xrange(12, 17), 20]

Puede reemplazar xrange con range o cualquier otra clase personalizada.


Los documentos de Python tienen una muy clara receta para esto:

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

Producción:

[2, 3, 4, 5]
[12, 13, 14, 15, 16, 17]

Si desea obtener exactamente el mismo resultado, puede hacer esto:

ranges = []
for k, g in groupby(enumerate(data), lambda (i,x):i-x):
    group = map(itemgetter(1), g)
    ranges.append((group[0], group[-1]))

producción:

[(2, 5), (12, 17)]

EDITAR: El ejemplo ya está explicado en la documentación, pero tal vez debería explicarlo más:

La clave de la solución es diferenciar con un rango para que todos los números consecutivos aparezcan en el mismo grupo.

Si los datos fueran: [2, 3, 4, 5, 12, 13, 14, 15, 16, 17]
Después groupby(enumerate(data), lambda (i,x):i-x) es equivalente a lo siguiente:

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
)

La función lambda resta el índice del elemento del valor del elemento. Entonces, cuando aplicas la lambda en cada artículo. Obtendrá las siguientes claves para groupby:

[-2, -2, -2, -2, -8, -8, -8, -8, -8, -8]

groupby agrupa los elementos por el mismo valor clave, por lo que los primeros 4 elementos se agruparán y así sucesivamente.

Espero que esto lo haga más legible.

python 3 la versión puede ser útil para principiantes

importar las bibliotecas requeridas primero

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

  • casi funciona en py3k, excepto que requiere lambda x:x[0]-x[1].

    – Fantasma silencioso

    28 de enero de 2010 a las 12:41

  • ¿Podría usar nombres de variables de varios caracteres? Para alguien que no esté familiarizado con map() o groupby(), los significados de kg, i y x no están claros.

    – mikemaccana

    28 de enero de 2010 a las 13:58


  • Esto fue copiado de las documentaciones de Python con los mismos nombres de variables. Ahora cambié los nombres.

    – Nadia Alramli

    28 de enero de 2010 a las 14:00


  • Deberá incrementar el segundo número en xrange/range porque no es inclusivo. En otras palabras, [2,3,4,5] == xrange(2,6)no xrange(2,5). Puede valer la pena definir un nuevo tipo de datos de rango inclusivo.

    – Ardor de Hielo

    29 de abril de 2014 a las 16:37

  • Python 3 arroja un error de sintaxis en el primer ejemplo. Aquí están las primeras 2 líneas actualizadas para trabajar en python 3: for key, group in groupby(enumerate(data), lambda i: i[0] - i[1]): group = list(map(itemgetter(1), group))

    – derek73

    5 de agosto de 2016 a las 4:22


avatar de usuario de pylang
pilang

more_itertools.consecutive_groups se añadió en la versión 4.0.

Manifestación

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

Código

Aplicando esta herramienta, hacemos una función generadora que encuentra rangos de números consecutivos.

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]

Él fuente implementación emula un receta clasica (como lo demuestra @Nadia Alramli).

Nota: more_itertools es un paquete de terceros instalable a través de pip install more_itertools.

  • Alternativamente, usando more_itertools.groupby_transform: [v for k,v in more_itertools.groupby_transform(enumerate(iterable), keyfunc=lambda p: p[1]-p[0], valuefunc=operator.itemgetter(1), reducefunc=to_interval)] con to_interval = lambda g: (sublst[0], sublst[-1]) if len(sublst := list(g)) > 1 else sublst[0]

    – Stef

    16 sep 2021 a las 10:48


avatar de usuario de mthurlin
mthurlin

La solución “ingenua” que encuentro algo legible al menos.

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

  • Me gusta mucho esta respuesta porque es concisa pero legible. Sin embargo, los números que están fuera de los rangos deben imprimirse como dígitos únicos, no como tuplas (ya que formatearé la salida y tendré diferentes requisitos de formato para números individuales versus rangos de números).

    – mikemaccana

    28 de enero de 2010 a las 13:38


  • La otra respuesta se veía hermosa e inteligente, pero esta es más comprensible para mí y permitió que un principiante como yo la expandiera según mis necesidades.

    – Benny

    3 de abril de 2013 a las 8:35

  • Podría usar una lista de comprensión para imprimir las tuplas que no son de rango como dígitos únicos: print([i if i[0] != i[1] else i[0] for i in group(x)])

    – Nexo

    16 de julio de 2018 a las 23:34


Avatar de usuario de SilentGhost
Fantasma silencioso

Asumiendo que su lista está ordenada:

>>> 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 += 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)]

Avatar de usuario de Andrea Ambu
andrea ambú

Aquí hay algo que debería funcionar, sin necesidad de importar:

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

Avatar de usuario de Alex Riley
Alex Riley

Tenga en cuenta que el código que utiliza groupby no funciona como se indica en Python 3, así que usa esto.

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

Avatar de usuario de Mark Byers
marca byers

Esto no usa una función estándar, solo itera sobre la entrada, pero debería funcionar:

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)

Tenga en cuenta que requiere que la entrada contenga solo números positivos en orden ascendente. Debe validar la entrada, pero este código se omite para mayor claridad.

¿Ha sido útil esta solución?