Определить группы непрерывных чисел в списке

Я хотел бы идентифицировать группы непрерывных чисел в списке, чтобы:

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

Возврат:

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

И было интересно, что лучший способ сделать это был (особенно, если там что-то встроено в Python).

Изменить: Примечание. Я изначально забыл упомянуть, что отдельные числа должны быть возвращены как отдельные номера, а не диапазоны.

Ответ 1

more_itertools.consecutive_groups был добавлен в версии 4.0.

демонстрация

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

Код

Применяя этот инструмент, мы создаем функцию генератора, которая находит диапазоны последовательных чисел.

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]

Источник реализация эмулирует классический рецепт (как это демонстрируется на @Nadia Alramli).

Примечание: more_itertools - это сторонний пакет, устанавливаемый через pip install more_itertools.

Ответ 2

РЕДАКТИРОВАТЬ 2: Чтобы ответить 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])

Выход:

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

Вы можете заменить xrange на range или любой другой пользовательский класс.


Документы Python имеют очень аккуратный рецепт для этого:

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)

Выход:

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

Если вы хотите получить точно такой же вывод, вы можете сделать это:

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

выход:

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

РЕДАКТИРОВАТЬ: Пример уже объяснен в документации, но, возможно, я должен объяснить это больше:

Ключом к решению является различие с диапазоном, так что все последовательные числа появляются в одной группе.

Если данные были: [2, 3, 4, 5, 12, 13, 14, 15, 16, 17] groupby(enumerate(data), lambda (i,x):ix) эквивалентно следующему:

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
)

Лямбда-функция вычитает индекс элемента из значения элемента. Поэтому, когда вы применяете лямбду на каждый предмет. Вы получите следующие ключи для группы:

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

groupby группирует элементы по одинаковому значению ключа, поэтому первые 4 элемента будут сгруппированы вместе и так далее.

Я надеюсь, что это делает его более читабельным.

Версия python 3 может быть полезна для начинающих

импортировать библиотеки, необходимые в первую очередь

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

Ответ 3

"Наивное" решение, которое я считаю немного читаемым, по крайней мере.

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

Ответ 4

Предполагая, что ваш список отсортирован:

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

Ответ 5

Здесь он должен работать, без необходимости импорта:

def myfunc(lst):
    ret = []
    a = b = lst[0]                           # a and b are range 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 start again with a single
    ret.append(a if a==b else (a,b))         # corner case for last single/range
    return ret

Ответ 6

Обратите внимание, что код с использованием groupby не работает, как указано в Python 3, поэтому используйте это.

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

Ответ 7

Это не использует стандартную функцию - она ​​просто просматривает ввод, но он должен работать:

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)

Обратите внимание, что для ввода требуется только положительные числа в порядке возрастания. Вы должны подтвердить ввод, но этот код для ясности опущен.

Ответ 8

Вот ответ, который я придумал. Я пишу код для других людей, чтобы понять, поэтому я довольно многословный с именами переменных и комментариями.

Сначала быстрая вспомогательная функция:

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] 

И тогда фактический код:

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

Пример выполнения:

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

возвращает:

[[2, 5], [12, 17]]

Ответ 9

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)

Вывод:

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

Ответ 10

Использование списков numpy +:
С помощью функции numpy diff могут быть определены последовательные входные векторные записи, что их разность не равна единице. Начало и конец входного вектора необходимо учитывать.

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

Выход:

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

Примечание. Запрос о том, что отдельные числа должны обрабатываться по-разному (возвращаются как отдельные, а не диапазоны), был опущен. Это может быть достигнуто путем дальнейшей обработки результатов. Обычно это усложняет ситуацию, не принося никакой пользы.

Ответ 11

Краткое решение, которое работает без дополнительного импорта. Он принимает любые повторяемые, сортирует несортированные входные данные и удаляет дублирующиеся элементы:

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

Пример:

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

Это то же самое, что и решение @dansalmo, которое мне показалось поразительным, хотя и немного сложным для чтения и применения (поскольку оно не дано как функция).

Обратите внимание, что его можно легко изменить, чтобы он выплевывал "традиционные" открытые диапазоны [start, end), например, путем изменения оператора return:

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

Я скопировал этот ответ из другого вопроса, который был помечен как дубликат этого, чтобы облегчить его поиск (после того, как я только что снова искал эту тему, сначала нашел только вопрос здесь и не был удовлетворен ответами) дано).

Ответ 12

Использование groupby и count от itertools дает нам краткое решение. Идея состоит в том, что в возрастающей последовательности разница между индексом и значением останется неизменной.

Чтобы отслеживать индекс, мы можем использовать itertools.count, который делает код более чистым, используя 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

Некоторые примеры выходных данных:

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

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