Xác định các nhóm số liên tục trong danh sách


94

Tôi muốn xác định các nhóm số liên tục trong một danh sách, do đó:

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

Lợi nhuận:

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

Và đang tự hỏi cách tốt nhất để làm điều này là gì (đặc biệt là nếu có thứ gì đó được tích hợp sẵn trong Python).

Chỉnh sửa: Lưu ý rằng ban đầu tôi đã quên đề cập rằng các số riêng lẻ phải được trả lại dưới dạng số riêng lẻ, không phải dải ô.


3
Giá trị trả về đó có phải là một chuỗi không?
Đánh dấu Byers

Lý tưởng nhất là thích thứ gì đó sử dụng một loại riêng biệt cho các phạm vi so với các số độc lập.
mikemaccana

Câu trả lời:


52

more_itertools.consecutive_groups đã được thêm vào trong phiên bản 4.0.

Bản giới thiệu

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

Áp dụng công cụ này, chúng tôi tạo một hàm tạo để tìm các dãy số liên tiếp.

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]

Việc triển khai mã nguồn mô phỏng một công thức cổ điển (như được chứng minh bởi @Nadia Alramli).

Lưu ý: more_itertoolslà gói của bên thứ ba có thể cài đặt thông qua pip install more_itertools.


121

CHỈNH SỬA 2: Để trả lời yêu cầu mới của 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])

Đầu ra:

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

Bạn có thể thay thế xrange bằng dải ô hoặc bất kỳ lớp tùy chỉnh nào khác.


Tài liệu Python có một công thức rất gọn gàng cho việc này:

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)

Đầu ra:

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

Nếu bạn muốn nhận được cùng một đầu ra, bạn có thể làm như sau:

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

đầu ra:

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

CHỈNH SỬA: Ví dụ đã được giải thích trong tài liệu nhưng có lẽ tôi nên giải thích thêm:

Chìa khóa của giải pháp là phân biệt với một phạm vi để các số liên tiếp xuất hiện trong cùng một nhóm.

Nếu dữ liệu là: [2, 3, 4, 5, 12, 13, 14, 15, 16, 17] Thì groupby(enumerate(data), lambda (i,x):i-x)tương đương như sau:

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
)

Hàm lambda trừ chỉ số phần tử khỏi giá trị phần tử. Vì vậy, khi bạn áp dụng lambda trên mỗi mục. Bạn sẽ nhận được các khóa sau để chia nhóm:

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

groupby nhóm các phần tử theo giá trị khóa bằng nhau, vì vậy 4 phần tử đầu tiên sẽ được nhóm lại với nhau, v.v.

Tôi hy vọng điều này làm cho nó dễ đọc hơn.

python 3 phiên bản có thể hữu ích cho người mới bắt đầu

nhập các thư viện cần thiết trước

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
hầu như hoạt động trong py3k, ngoại trừ nó yêu cầu lambda x:x[0]-x[1].
SilentGhost

Bạn có thể sử dụng, vui lòng sử dụng tên biến nhiều ký tự? Đối với một người không quen thuộc với map () hoặc groupby (), ý nghĩa của kg, i và x không rõ ràng.
mikemaccana

1
Điều này đã được sao chép từ các tài liệu Python có cùng tên biến. Tôi đã thay đổi tên bây giờ.
Nadia Alramli

1
Bạn sẽ cần tăng số thứ 2 trong xrange / range vì nó không bao gồm. Nói cách khác [2,3,4,5] == xrange(2,6), không phải xrange(2,5). Nó có thể đáng để xác định một kiểu dữ liệu phạm vi bao gồm mới.
IceArdor

10
Python 3 đưa ra một lỗi cú pháp trong ví dụ đầu tiên. Dưới đây là 2 dòng đầu tiên được cập nhật để làm việc trên python 3:for key, group in groupby(enumerate(data), lambda i: i[0] - i[1]): group = list(map(itemgetter(1), group))
derek73

16

Giải pháp "ngây thơ" mà tôi thấy ít đọc được.

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

Tôi thích câu trả lời này rất nhiều vì nó ngắn gọn nhưng có thể đọc được. Tuy nhiên con số nằm ngoài của dãy sẽ được in như một con số, chứ không phải các bộ (như tôi sẽ định dạng đầu ra và có những yêu cầu định dạng khác nhau cho số cá nhân so với các dãy số.
mikemaccana

4
Câu trả lời khác trông đẹp và thông minh, nhưng câu trả lời này dễ hiểu hơn đối với tôi và cho phép một người mới bắt đầu như tôi mở rộng nó theo nhu cầu của mình.
Benny

Có thể sử dụng khả năng hiểu danh sách để in các bộ giá trị không thuộc phạm vi dưới dạng các chữ số duy nhất: print([i if i[0] != i[1] else i[0] for i in group(x)])
Nexus

14

Giả sử danh sách của bạn được sắp xếp:

>>> 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)]thật thông minh :-)
Jochen Ritzel

9

Đây là thứ sẽ hoạt động mà không cần bất kỳ thao tác nhập nào:

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

Xin lưu ý rằng mã sử dụng groupbykhông hoạt động như được cung cấp trong Python 3, vì vậy hãy sử dụng mã này.

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

Điều này không sử dụng một hàm tiêu chuẩn - nó chỉ biểu hiện trên đầu vào, nhưng nó sẽ hoạt động:

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)

Lưu ý rằng nó yêu cầu đầu vào chỉ chứa các số dương theo thứ tự tăng dần. Bạn nên xác thực đầu vào, nhưng mã này được bỏ qua để rõ ràng.


1

Đây là câu trả lời mà tôi đã nghĩ ra. Tôi đang viết mã để người khác hiểu, vì vậy tôi khá dài dòng với các tên và nhận xét biến.

Đầu tiên là một chức năng trợ giúp nhanh:

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] 

Và sau đó là mã thực tế:

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

Chạy ví dụ:

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

trả lại:

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

>>> getranges([2, 12, 13])Đầu ra: [[12, 13]]. Đó là cố ý?
SilentGhost

Đúng, tôi cần sửa các số riêng lẻ (theo hầu hết các câu trả lời trên trang). Làm việc trên nó ngay bây giờ.
mikemaccana

Thực ra tôi thích câu trả lời của Nadia hơn, groupby () có vẻ giống như hàm tiêu chuẩn mà tôi muốn.
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)

Đầu ra:

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

1

Sử dụng groupbycounttừ itertoolscung cấp cho chúng tôi một giải pháp ngắn gọn. Ý tưởng là, trong một trình tự tăng dần, sự khác biệt giữa chỉ số và giá trị sẽ không đổi.

Để theo dõi chỉ mục, chúng ta có thể sử dụng itertools.count , giúp mã sạch hơn khi sử dụng 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

Một số đầu ra mẫu:

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

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

0

Sử dụng danh sách numpy + hiểu:
Với chức năng numpy diff, có thể xác định được các mục nhập vectơ đầu vào mà sự khác biệt của chúng không bằng một. Cần phải xem xét điểm bắt đầu và kết thúc của vector đầu vào.

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

Đầu ra:

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

Lưu ý: Yêu cầu rằng các số riêng lẻ phải được xử lý theo cách khác, (trả về dưới dạng riêng lẻ, không phải phạm vi) đã bị bỏ qua. Điều này có thể đạt được bằng cách xử lý thêm các kết quả. Thông thường điều này sẽ khiến mọi thứ trở nên phức tạp hơn mà không thu được lợi ích gì.


0

Một giải pháp ngắn hạn hoạt động mà không cần nhập khẩu bổ sung. Nó chấp nhận mọi đầu vào có thể lặp lại, sắp xếp các đầu vào không được sắp xếp và loại bỏ các mục trùng lặp:

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

Thí dụ:

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

Điều này giống với giải pháp của @ dansalmo mà tôi thấy tuyệt vời, mặc dù hơi khó đọc và áp dụng (vì nó không được cung cấp dưới dạng một hàm).

Lưu ý rằng nó có thể dễ dàng được sửa đổi để loại bỏ phạm vi mở "truyền thống" [start, end), bằng cách thay đổi câu lệnh trả về:

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

Tôi đã sao chép câu trả lời này từ một câu hỏi khác đã được đánh dấu là bản sao của câu này với mục đích để làm cho nó dễ tìm hơn (sau khi tôi vừa tìm kiếm lại chủ đề này, lúc đầu chỉ tìm thấy câu hỏi ở đây và không hài lòng với câu trả lời được).


0

Các phiên bản của Mark Byers , Andrea Ambu , SilentGhost , Nadia Alramlitruppo rất đơn giản và nhanh chóng. Phiên bản 'truppo' khuyến khích tôi viết một phiên bản vẫn giữ nguyên hành vi nhanh nhẹn trong khi xử lý các kích thước bước khác 1 (và liệt kê dưới dạng các phần tử đơn không kéo dài quá 1 bước với kích thước bước nhất định). Nó được đưa ra ở đây .

>>> 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)]
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.