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.
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)
noxrange(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
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)]
conto_interval = lambda g: (sublst[0], sublst[-1]) if len(sublst := list(g)) > 1 else sublst[0]
– Stef
16 sep 2021 a las 10:48
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
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)]
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
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]))
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.
¿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