Bir listedeki sürekli sayı gruplarını tanımlayın


94

Bir listede sürekli sayı gruplarını tanımlamak istiyorum, böylece:

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

İadeler:

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

Ve bunu yapmanın en iyi yolunun ne olduğunu merak ediyordum (özellikle Python'da yerleşik bir şey varsa).

Düzenleme: Not Başlangıçta, bireysel numaraların aralıklar olarak değil, ayrı sayılar olarak döndürülmesi gerektiğini söylemeyi unuttum.


3
Bu dönüş değeri bir dizge mi?
Mark Byers

İdeal olarak, aralıklar için bağımsız sayılar için ayrı bir tür kullanan bir şeyi tercih edersiniz.
mikemaccana

Yanıtlar:


53

more_itertools.consecutive_groups 4.0 sürümünde eklendi.

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

Kod

Bu aracı uygulayarak ardışık sayı aralıklarını bulan bir üreteç işlevi yaparız.

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]

Kaynak uygulaması taklit klasik tarifi (şekilde @Nadia Alramli gösterdiği).

Not: more_itertoolsaracılığıyla bir üçüncü taraf paketi yüklenebilir olduğunu pip install more_itertools.


121

DÜZENLEME 2: OP'nin yeni gereksinimini yanıtlamak için

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

Çıktı:

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

Xrange'i range veya başka bir özel sınıfla değiştirebilirsiniz.


Python belgelerinin çok düzgün tarifi vardı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)

Çıktı:

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

Tam olarak aynı çıktıyı elde etmek istiyorsanız, bunu yapabilirsiniz:

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

çıktı:

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

DÜZENLE: Örnek belgelerde zaten açıklanmıştır, ancak belki daha fazla açıklamalıyım:

Çözümün anahtarı, ardışık sayıların hepsinin aynı grupta görünmesi için bir aralıkla farklılık göstermektir.

Veri olsaydı: [2, 3, 4, 5, 12, 13, 14, 15, 16, 17] Sonra groupby(enumerate(data), lambda (i,x):i-x)aşağıdakilerden eşdeğerdir:

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
)

Lambda işlevi, öğe dizinini öğe değerinden çıkarır. Yani her bir öğeye lambda uyguladığınızda. Groupby için aşağıdaki anahtarları alacaksınız:

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

groupby öğeleri eşit anahtar değerine göre gruplandırır, bu nedenle ilk 4 öğe birlikte gruplandırılır ve böyle devam eder.

Umarım bu onu daha okunaklı hale getirir.

python 3 sürüm yeni başlayanlar için yararlı olabilir

önce gerekli kitaplıkları içe aktarın

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

4
gerektirmesi dışında neredeyse py3k ile çalışır lambda x:x[0]-x[1].
SilentGhost

Lütfen çok karakterli değişken isimleri kullanabilir misiniz? Map () veya groupby () ile aşina olmayan biri için kg, i ve x'in anlamları net değildir.
mikemaccana

1
Bu, aynı değişken adlarına sahip Python belgelerinden kopyalandı. İsimleri şimdi değiştirdim.
Nadia Alramli

1
Kapsayıcı olmadığı için xrange / range'deki 2. sayıyı artırmanız gerekir. Başka bir deyişle [2,3,4,5] == xrange(2,6), hayır xrange(2,5). Yeni bir kapsayıcı aralık veri türü tanımlamaya değer olabilir.
IceArdor

10
Python 3, ilk örnekte bir sözdizimi hatası atar. İşte python 3 üzerinde çalışacak şekilde güncellenen ilk 2 satır:for key, group in groupby(enumerate(data), lambda i: i[0] - i[1]): group = list(map(itemgetter(1), group))
derek73

16

En azından biraz okunabilir bulduğum "saf" çözüm.

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

Bu yanıtı çok beğendim çünkü kısa ama okunabilir. Bununla birlikte, aralıkların dışında kalan sayılar, tek basamaklı olarak yazdırılmalıdır (çünkü çıktıyı biçimlendireceğim ve sayı aralıklarına karşı bireysel sayılar için farklı biçimlendirme gereksinimlerine sahip
olacağım

4
Diğer cevap güzel ve zekice görünüyordu, ancak bu benim için daha anlaşılır ve benim gibi yeni başlayanların ihtiyaçlarıma göre genişletmesine izin verdi.
Benny

Aralık dışı tupleları tek basamak olarak yazdırmak için bir liste anlama kullanılabilir: print([i if i[0] != i[1] else i[0] for i in group(x)])
Nexus

14

Listenizin sıralandığını varsayarsak:

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

2
[j - i for i, j in enumerate(lst)]zeki :-)
Jochen Ritzel

9

Burada, herhangi bir içe aktarma gerekmeden çalışması gereken bir şey var:

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

6

Lütfen groupbyPython 3'te verilen kodun çalışmadığını unutmayın, bu yüzden bunu kullanın.

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

3

Bu standart bir işlev kullanmaz - yalnızca girdinin üzerinde yinelenir, ancak çalışması gerekir:

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)

Girdinin yalnızca artan sırada pozitif sayılar içermesini gerektirdiğini unutmayın. Girişi doğrulamalısınız, ancak bu kod netlik açısından ihmal edilmiştir.


1

İşte bulduğum cevap. Başkalarının anlayabilmesi için kodu yazıyorum, bu yüzden değişken isimler ve yorumlarla oldukça ayrıntılıyım.

İlk önce hızlı bir yardımcı işlev:

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] 

Ve sonra asıl kod:

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

Örnek çalışma:

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

İadeler:

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

>>> getranges([2, 12, 13])Çıkışlar: [[12, 13]]. Bu kasıtlı mıydı?
SilentGhost

Evet, tek tek numaralar için düzeltmem gerekiyor (sayfadaki yanıtların çoğu için). Şimdi üzerinde çalışıyorum.
mikemaccana

Aslında Nadia'nın cevabını tercih ediyorum, groupby () istediğim standart fonksiyon gibi görünüyor.
mikemaccana

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

Çıktı:

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

1

Kullanılması groupbyve countgelen itertoolsbize kısa çözüm sunuyor. Buradaki fikir, artan bir sırayla, dizin ile değer arasındaki farkın aynı kalacağıdır.

Dizini takip etmek için , kodu aşağıdaki gibi daha temiz hale getiren bir itertools.count kullanabiliriz 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

Bazı örnek çıktılar:

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

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

0

Numpy + anlama listelerini kullanma:
Numpy diff fonksiyonu ile, farklarının bire eşit olmadığı ardışık giriş vektör girişleri belirlenebilir. Giriş vektörünün başlangıcı ve bitişi dikkate alınmalıdır.

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

Çıktı:

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

Not: Bireysel numaraların farklı şekilde ele alınması talebi (aralık olarak değil, ayrı olarak döndürülür) atlandı. Buna, sonuçların sonradan işlenmesiyle ulaşılabilir. Genellikle bu, herhangi bir fayda sağlamadan işleri daha karmaşık hale getirir.


0

Ek ithalat olmadan çalışan kısa bir çözüm. Yinelenebilir, sıralanmamış girdileri sıralar ve yinelenen öğeleri kaldırır:

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

Misal:

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

Bu @ dansalmo en aynıdır solüsyon (bir fonksiyonu olarak verilen değil gibi) biraz zor okuyup uygulamak olsa ben, şaşırtıcı bulduğu.

[start, end)Örneğin return ifadesini değiştirerek "geleneksel" açık aralıkları çıkaracak şekilde kolayca değiştirilebileceğini unutmayın :

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

Bu yanıtı, daha kolay bulunabilir hale getirmek amacıyla bunun bir kopyası olarak işaretlenen başka bir sorudan kopyaladım (şimdi bu konuyu tekrar aradıktan sonra, ilk başta sadece burada soruyu bularak ve cevaplardan memnun kalmadan verilen).


0

Tarafından versiyonları Mark Byers , Andrea Ambu , SilentGhost , Nadia Alramli ve truppo hızlı, basit ve vardır. 'Truppo' sürümü, 1'den farklı adım boyutlarını işlerken aynı çevik davranışı koruyan bir sürüm yazmaya teşvik etti (ve belirli bir adım boyutunda 1 adımı aşmayan tekli öğeler olarak listelendi). Bu verilir burada .

>>> list(ranges([1,2,3,4,3,2,1,3,5,7,11,1,2,3]))
[(1, 4, 1), (3, 1, -1), (3, 7, 2), 11, (1, 3, 1)]
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.