web-dev-qa-db-fra.com

Identifier des groupes de nombres continus dans une liste

J'aimerais identifier des groupes de nombres continus dans une liste, de sorte que:

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

Résultats:

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

Et je me demandais quelle était la meilleure façon de procéder (en particulier si quelque chose était intégré à Python).

Edit: Note: Au départ, j'avais oublié de mentionner que les numéros individuels devaient être renvoyés sous forme de nombres individuels et non de plages.

70
mikemaccana

more_itertools.consecutive_groups a été ajouté à la version 4.0.

Démo

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

En appliquant cet outil, nous créons une fonction génératrice qui recherche des plages de nombres consécutifs.

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'implémentation source émule une recette classique } (comme l'a démontré @Nadia Alramli).

Remarque: more_itertools est un package tiers installable via pip install more_itertools .

19
pylang

EDIT 2: Répondre à la nouvelle exigence du PO

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

Sortie:

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

Vous pouvez remplacer xrange par range ou n’importe quelle autre classe personnalisée.


Les documents Python ont une recette très nette pour cela:

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)

Sortie:

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

Si vous voulez obtenir exactement le même résultat, procédez comme suit:

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

sortie:

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

EDIT: L'exemple est déjà expliqué dans la documentation mais je devrais peut-être l'expliquer davantage:

La clé de la solution est différenciation avec une plage telle que les numéros consécutifs apparaissent tous dans le même groupe.

Si les données étaient: [2, 3, 4, 5, 12, 13, 14, 15, 16, 17] Alors groupby(enumerate(data), lambda (i,x):i-x) est équivalent à ce qui suit:

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 fonction lambda soustrait l'index de l'élément de la valeur de l'élément. Donc, lorsque vous appliquez le lambda sur chaque élément. Vous obtiendrez les clés suivantes pour groupby:

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

groupby regroupe les éléments par valeur de clé égale, ainsi les 4 premiers éléments seront regroupés, et ainsi de suite.

J'espère que cela le rend plus lisible. 

La version python 3 peut être utile pour les débutants

importer les bibliothèques nécessaires en premier

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

La solution "naïve" que je trouve au moins lisible.

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

En supposant que votre liste soit triée:

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

Voici quelque chose qui devrait fonctionner, sans aucune importation nécessaire:

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

Veuillez noter que le code utilisant groupby ne fonctionne pas comme indiqué dans Python 3, utilisez donc ceci.

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

Cela n’utilise pas de fonction standard, c’est juste l’initialisation de la saisie, mais ça devrait marcher:

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)

Notez que cela nécessite que l'entrée ne contienne que des nombres positifs par ordre croissant. Vous devez valider la saisie, mais ce code est omis pour plus de clarté.

3
Mark Byers

Voici la réponse que je suis venu avec. J'écris le code pour que les autres comprennent, alors je suis assez bavard avec des noms de variables et des commentaires.

D'abord une fonction d'aide rapide:

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] 

Et puis le code actuel: 

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

Exemple d'exécution:

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

résultats:

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

Sortie:

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

Une solution courte qui fonctionne sans importations supplémentaires. Il accepte toutes les itérations, trie les entrées non triées et supprime les éléments en double:

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

Exemple:

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

C’est la même chose que la solution de @ dansalmo que j’ai trouvée incroyable, mais un peu difficile à lire et à appliquer (car elle n’est pas donnée en tant que fonction).

Notez qu'il pourrait facilement être modifié pour cracher des plages ouvertes "traditionnelles" [start, end), par exemple. modifier l'instruction de retour:

    return [(s, e+1) for s, e in Zip(edges, edges)]

J'ai copié cette réponse de une autre question qui était marquée comme une copie de celle-ci dans le but de la rendre plus facile à trouver (après que je venais de chercher à nouveau ce sujet, ne trouvant que la question ici au début satisfait des réponses données).

0
coldfix

Utilisation de numpy + listes de compréhension:
Avec la fonction numpy diff, il est possible d’identifier les entrées de vecteur d’entrée consécutives pour lesquelles leur différence n’est pas égale à un. Le début et la fin du vecteur d’entrée doivent être pris en compte. 

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

Sortie:

 [[ 2  5]   
  [12 17]   
  [20 20]]

Remarque: La demande selon laquelle les numéros individuels doivent être traités différemment ((renvoyée en tant que valeur individuelle, pas de plage) a été omise. Ceci peut être atteint en post-traitant davantage les résultats. Habituellement, cela rend les choses plus complexes sans gagner aucun avantage. 

0
Nir

Utiliser groupby et count à partir de itertools nous donne une solution courte. L'idée est que, dans un ordre croissant, la différence entre l'index et la valeur reste la même.

Afin de garder une trace de l'index, nous pouvons utiliser un itertools.count , ce qui rend le code plus propre en utilisant 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

Quelques exemples de sortie:

print(intervals([0, 1, 3, 4, 6]))
# [[0, 1], [3, 4], [6, 6]]

print(intervals([2, 3, 4, 5]))
# [[2, 5]]
0
Thierry Lathuille