(Эффективно для памяти) реализовать `sorted` как генератор - PullRequest
1 голос
/ 22 апреля 2020

Я полагаю, что это не особенно новая топи c, и я думаю, что есть лучшие реализации, чем моя: я ищу (а) тип / тип алгоритма, с которым я имею дело - его фактическое имя или подобное - и (b) потенциально лучшая реализация.

Общая проблема: Представьте себе список a, который очень длинный - слишком длинный для размещения в памяти более одного раза. Список содержит «случайную» последовательность вещей, которые можно сортировать (работают <, > и ==). Я хочу перебрать все записи в списке, включая дубликаты, в порядке возрастания, но без копирования списка или создания чего-либо такого же «экстремального» размера. Я также хочу сохранить исходный порядок записей в a, то есть исключен sort на месте. Поэтому я хочу минимизировать объем памяти, необходимый для сортировки, не изменяя исходный источник данных. Сортировка

Python не затрагивает исходные данные, но создает новый список, имеющий тот же размер / длину, что и оригинал. Поэтому моя основная идея c состоит в том, чтобы повторно реализовать sorted в качестве генератора:

def sorted_nocopy_generator(data_list):
    state_max = max(data_list)
    state = min(data_list)
    state_count = data_list.count(state)
    for _ in range(state_count):
        yield state
    index = state_count
    while index < len(data_list):
        new_state = state_max
        for entry in data_list:
            if state < entry < new_state:
                new_state = entry
        state = new_state
        state_count = data_list.count(state)
        for _ in range(state_count):
            yield state
        index += state_count

Это можно проверить следующим образом:

import random

a_min = 0
a_max = 10000
a = list(range(a_min, a_max)) # test data
a.extend((random.randint(a_min, a_max - 1) for _ in range(len(a) // 10))) # some double entries
random.shuffle(a) # "random" order
a_control = a.copy() # for verification that a is not altered

a_test_sorted_nocopy_generator = list(sorted_nocopy_generator(a))
assert a == a_control
a_test_sorted = sorted(a)
assert a == a_control
assert a_test_sorted == a_test_sorted_nocopy_generator

Он масштабируется O (N ^ 2), например, как сортировка пузырьков. Какой алгоритм я ищу? Как можно оптимизировать эту вещь (возможно, торгуя некоторым памятью)?

Ответы [ 2 ]

4 голосов
/ 22 апреля 2020

Набросок здесь. Где N = len(data_list) и S = sqrt(N), использует O(S) дополнительную память и занимает наихудший случай O(N*log(N)) время:

  • Для каждого последовательного фрагмента длины S в оригинале данные, скопируйте этот фрагмент во временный список, используйте .sort(), чтобы отсортировать его на месте, затем запишите результат в уникальный временный файл. Всего будет около S временных файлов.

  • Введите эти временные файлы в heapq.merge(). Это генератор, отслеживающий только S наименьших на данный момент значений на входах S, поэтому эта часть также имеет O(S) нагрузку на память.

  • Удаление временных файлов .

Чем больше памяти вы можете использовать для этого, тем меньше временных файлов потребуется, и тем быстрее это будет go.

Сокращение постоянного коэффициента

Как отмечается в комментариях, надежда для алгоритма времени суб-квадратичного c невелика. Тем не менее, вы можете сделать многое, чтобы сократить постоянный коэффициент в вашем исходном алгоритме, сократив количество проходов по данным. Вот один из способов получения следующих K записей при каждом проходе по данным. Тем не менее, оно остается в квадрате c -время общего времени.

def sorted_nocopy_generator(data_list, K=100):
    import itertools
    from bisect import insort

    assert K >= 1
    total = 0
    too_small = None

    while total < len(data_list):
        active = [] # hold the next K entries
        entry2count = {}
        for entry in data_list:
            if entry in entry2count:
                entry2count[entry] += 1
            elif ((too_small is None or too_small < entry) and
                  (len(active) < K or entry < active[-1])):
                insort(active, entry)
                entry2count[entry] = 1
                if len(active) > K: # forget the largest
                    del entry2count[active.pop()]
        for entry in active:
            count = entry2count[entry]
            yield from itertools.repeat(entry, count)
            total += count
        too_small = active[-1]

И устранение наихудших случаев

Как и в ответе @ btilly, наихудшие случаи в приведенном выше коде можно обойти, используя максимальная куча вместо. Затем добавление новой записи в active имеет наихудшее время O(log(K)) вместо O(K).

К счастью, модуль heapq уже предоставляет что-то пригодное для этой цели. Но работа с дубликатами становится чем-то вроде головной боли - максимальная куча «под прикрытием», используемая heapq, не раскрывается.

Так что в следующих особых случаях самый большой из самых маленьких K записей интерес, используя .count() (как в вашей исходной программе), чтобы сделать полный проход, чтобы подсчитать, сколько их.

Но вместо того, чтобы делать это для каждого уникального элемента, он требуется только один раз для K элементов.

Дополнительное использование памяти пропорционально K.

def sorted_nocopy_generator(data_list, K=100):
    import itertools
    from heapq import nsmallest

    assert K >= 1
    too_small = None
    ntodo = len(data_list)

    while ntodo:
        if too_small is None:
            active = nsmallest(K, data_list)
        else:
            active = nsmallest(K, (x for x in data_list
                                     if x > too_small))
        too_small = active[-1]
        for x in active:
            if x == too_small:
                break
            yield x
            ntodo -= 1
        count = data_list.count(too_small)
        yield from itertools.repeat(too_small, count)
        ntodo -= count
        assert ntodo >= 0
3 голосов
/ 22 апреля 2020

Предположим, что у вас есть память для k вещей в отдельном списке. Тогда следующее будет масштабироваться как O((n + n^2/k) log(k)). В крайних случаях константы k это квадратичные c, а если k - фиксированная дробь n, то это O(n log(n)).

Идея состоит в том, чтобы создать буфер k мельчайшие вещи, которые вы еще не вернули. Преврати его в мини-кучу, а затем используй операции кучи, чтобы вернуть вещи из него во время O(log(k)) на возвращаемый элемент. Когда буфер пуст, снова заполните буфер и продолжайте, как прежде, снова. (Буфер начинается пустым.)

Чтобы создать буфер, вы сканируете массив, вставляя в него вещи, которые больше последнего возвращенного, пока вы не достигните k вещей в буфере. Затем превратите его в максимальную кучу и замените вещи в куче, когда они больше, чем возвращено, и меньше, чем то, что уже есть. Когда он будет пополнен, реорганизуйте его в мини-кучу.

Вам потребуется повторно заполнить буфер n/k раз. Каждый раз O(n log(k)) операция наихудший случай. (Если k << n и массив находится в случайном порядке, то среднее время выполнения равно O(n), потому что почти все сравнивается с максимумом в куче, а затем отбрасывается.)

Важный случай края: Если у вас есть дубликаты, вам необходимо учитывать случай, когда в ваш буфер включены только некоторые копии самой большой вещи. Один из подходов состоит в том, чтобы отслеживать, сколько копий самой большой вещи должно быть в куче, но на самом деле там не хранилось. Таким образом, после того, как вы очистите кучу, вы также можете вернуть все отсутствующие дубликаты, а затем приступить к поиску более крупных элементов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...