Нарязване на списък на n дяла с почти еднаква дължина

Търся бърз, чист, питоничен начин за разделяне на списък на точно n почти равни дяла.

partition([1,2,3,4,5],5)->[[1],[2],[3],[4],[5]]
partition([1,2,3,4,5],2)->[[1,2],[3,4,5]] (or [[1,2,3],[4,5]])
partition([1,2,3,4,5],3)->[[1,2],[3,4],[5]] (there are other ways to slice this one too)

Тук има няколко отговора Итерация върху срезове на списък, които са много близки до това, което искам, освен те са фокусирани върху размера на списъка, а аз се интересувам от броя на списъците (някои от тях също допълват с Няма). Те са тривиално преобразувани, очевидно, но търся най-добра практика.

По същия начин хората са посочили страхотни решения тук Как се разделя списък на парчета с еднакъв размер? за много подобен проблем, но аз се интересувам повече от броя на дяловете, отколкото от конкретния размер, стига да е в рамките на 1. Отново, това е тривиално кабриолет, но търся най-добрата практика.


person Drew    schedule 17.04.2010    source източник


Отговори (5)


def partition(lst, n):
    division = len(lst) / float(n)
    return [ lst[int(round(division * i)): int(round(division * (i + 1)))] for i in xrange(n) ]

>>> partition([1,2,3,4,5],5)
[[1], [2], [3], [4], [5]]
>>> partition([1,2,3,4,5],2)
[[1, 2, 3], [4, 5]]
>>> partition([1,2,3,4,5],3)
[[1, 2], [3, 4], [5]]
>>> partition(range(105), 10)
[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [11, 12, 13, 14, 15, 16, 17, 18, 19, 20], [21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], [32, 33, 34, 35, 36, 37, 38, 39, 40, 41], [42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52], [53, 54, 55, 56, 57, 58, 59, 60, 61, 62], [63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73], [74, 75, 76, 77, 78, 79, 80, 81, 82, 83], [84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94], [95, 96, 97, 98, 99, 100, 101, 102, 103, 104]]

Версия на Python 3:

def partition(lst, n):
    division = len(lst) / n
    return [lst[round(division * i):round(division * (i + 1))] for i in range(n)]
person João Silva    schedule 17.04.2010
comment
Това не работи за нетривиални примери. За partition(range(105), 10) последният подсписък ще има само 6 елемента. - person Daniel Stutzbach; 18.04.2010
comment
Как бихте разделили този списък? - person João Silva; 18.04.2010
comment
@JG: 5 подсписъка с 10 елемента и 5 подсписъка с 11 елемента. - person Daniel Stutzbach; 18.04.2010
comment
@Daniel: Честно, въпреки че това не е много ясно в първоначалния въпрос. Сега е поправено. - person João Silva; 18.04.2010
comment
@JG: Вярвам, че „точно n почти равно“ е трябвало да бъде „точно n с почти еднаква дължина“; „стига да е в рамките на 1“ също е силен намек, дори и неясен и неясен. - person tzot; 18.04.2010

Просто различно разбиране, което работи само ако [[1,3,5],[2,4]] е приемлив дял във вашия пример.

def partition ( lst, n ):
    return [ lst[i::n] for i in xrange(n) ]

Това удовлетворява примера, споменат в примера на @Daniel Stutzbach:

partition(range(105),10)
# [[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
# [1, 11, 21, 31, 41, 51, 61, 71, 81, 91, 101],
# [2, 12, 22, 32, 42, 52, 62, 72, 82, 92, 102],
# [3, 13, 23, 33, 43, 53, 63, 73, 83, 93, 103],
# [4, 14, 24, 34, 44, 54, 64, 74, 84, 94, 104],
# [5, 15, 25, 35, 45, 55, 65, 75, 85, 95],
# [6, 16, 26, 36, 46, 56, 66, 76, 86, 96],
# [7, 17, 27, 37, 47, 57, 67, 77, 87, 97],
# [8, 18, 28, 38, 48, 58, 68, 78, 88, 98],
# [9, 19, 29, 39, 49, 59, 69, 79, 89, 99]]
person Manuel Zubieta    schedule 13.02.2013
comment
Това е чудесно питонично решение - person flatline; 03.05.2013
comment
Чувствам, че трябва да има някакъв хитър начин за получаване на оригиналния, желан вход, но zip( *partition(range(105),10) ) не работи, защото zip съкращава... Все пак, много хубаво. - person Walter Nissen; 15.11.2013
comment
Боже мой, това е прекрасно. - person coneyhelixlake; 09.12.2015
comment
ако сте използвали itertools.izip най-дълго, трябва да работи - person Oscar Smith; 28.06.2016
comment
Наистина много спретнато (ако не се нуждаете от съседни дялове). - person Petr Vepřek; 30.12.2020

Ето версия, която е подобна на тази на Daniel: тя разделя възможно най-равномерно, но поставя всички по-големи дялове в началото:

def partition(lst, n):
    q, r = divmod(len(lst), n)
    indices = [q*i + min(i, r) for i in xrange(n+1)]
    return [lst[indices[i]:indices[i+1]] for i in xrange(n)]

Той също така избягва използването на аритметика с плаваща единица, тъй като това винаги ме кара да се чувствам неудобно. :)

Редактиране: пример, само за да покажа контраста с решението на Daniel Stutzbach

>>> print [len(x) for x in partition(range(105), 10)]
[11, 11, 11, 11, 11, 10, 10, 10, 10, 10]
person Mark Dickinson    schedule 17.04.2010

По-долу е един начин.

def partition(lst, n):
    increment = len(lst) / float(n)
    last = 0
    i = 1
    results = []
    while last < len(lst):
        idx = int(round(increment * i))
        results.append(lst[last:idx])
        last = idx
        i += 1
    return results

Ако len(lst) не може да бъде равномерно разделено на n, тази версия ще разпредели допълнителните елементи на приблизително равни интервали. Например:

>>> print [len(x) for x in partition(range(105), 10)]
[11, 10, 11, 10, 11, 10, 11, 10, 11, 10]

Кодът може да бъде по-прост, ако нямате нищо против всички 11 да са в началото или в края.

person Daniel Stutzbach    schedule 17.04.2010
comment
Използването на плаваща запетая може да доведе до зависими от машината резултати: когато increment*i е (математически) точно по средата между две цели числа, закръгляването може да върви в двете посоки в зависимост от това какви цифрови грешки са въведени. Какво ще кажете вместо това да използвате нещо като idx = len(lst)*i//n? Или може би idx = (len(lst)*i + n//2)//n, за да получите резултати, подобни на текущия код. - person Mark Dickinson; 18.04.2010
comment
За информация това е добре познатият алгоритъм на Bresenham. - person smci; 03.07.2011

Този отговор предоставя функция split(list_, n, max_ratio) за хора, които искат да разделят своя списък на n парчета с най-много max_ratio съотношение в дължината на парчета. То позволява повече вариации от „най-много 1 разлика в дължината на парчето“ на питащия.

Работи чрез вземане на проби от n дължини на парчета в рамките на желания диапазон на съотношението [1, max_ratio), поставяйки ги един след друг, за да образуват „счупена пръчка“ с правилните разстояния между „точки на прекъсване“, но грешна обща дължина. Мащабирането на счупената пръчка до желаната дължина ни дава приблизителните позиции на точките на счупване, които искаме. За да получите целочислени точки на прекъсване, е необходимо последващо закръгляване.

За съжаление, закръглянията могат да направят частите твърде къси и да ви позволят да надвишите max_ratio. Вижте долната част на този отговор за пример.

import random

def splitting_points(length, n, max_ratio):
    """n+1 slice points [0, ..., length] for n random-sized slices.

    max_ratio is the largest allowable ratio between the largest and the
    smallest part.
    """
    ratios = [random.uniform(1, max_ratio) for _ in range(n)]
    normalized_ratios = [r / sum(ratios) for r in ratios]
    cumulative_ratios = [
        sum(normalized_ratios[0:i])
        for i in range(n+1)
    ]
    scaled_distances = [
        int(round(r * length))
        for r in cumulative_ratios
    ]

    return scaled_distances


def split(list_, n, max_ratio):
    """Slice a list into n randomly-sized parts.

    max_ratio is the largest allowable ratio between the largest and the
    smallest part.
    """

    points = splitting_points(len(list_), n, ratio)

    return [
        list_[ points[i] : points[i+1] ]
        for i in range(n)
    ]

Можете да го изпробвате така:

for _ in range(10):
    parts = split('abcdefghijklmnopqrstuvwxyz', 4, 2)
    print([(len(part), part) for part in parts])

Пример за лош резултат:

parts = split('abcdefghijklmnopqrstuvwxyz', 10, 2)

# lengths range from 1 to 4, not 2 to 4
[(3, 'abc'),  (3, 'def'), (1, 'g'),
 (4, 'hijk'), (3, 'lmn'), (2, 'op'),
 (2, 'qr'),  (3, 'stu'),  (2, 'vw'),
 (3, 'xyz')]
person Esteis    schedule 24.08.2015