Попробуйте решить задачу - дан список, содержащий 100 триллионов элементов. Элементы в списке могут повторяться. Необходимо оценить количество уникальных элементов в этом списке. Методы точного подсчета уникальных элементов не подходят, т.к. они требуют слишком много дополнительной памяти - ее количество должно быть пропорционально количеству уникальных элементов в списке. Вначале рассмотрим основные методы точного подсчета уникальных элементов, чтобы понять, зачем им нужно столько много дополнительной памяти. Затем рассмотрим один из простейших методов оценки количества уникальных элементов, который требует существенно меньше дополнительной памяти.
Основные методы точного подсчета уникальных элементов в списке.
- Метод множеств. Записываем все элементы в структуру данных типа "множество", пропуская дупликаты, после чего подсчитываем количество элементов в множестве. Очевидно, этот метод требует много памяти. Также скорость работы этого метода мгновенно падает в 10000 раз, как только уникальные элементы множества перестают помещаться в оперативную память.
- Метод битовых карт. Этот метод подходит лишь для элементов, чьи значения могут быть представлены целыми числами в ограниченном диапазоне. Если мы знаем максимально возможное значение элемента в списке (Nmax), то мы можем создать битовую карту, содержащую Nmax битов. Затем проходимся по всем элементам списка, устанавливая соответствующий бит в карте для каждого элемента. После этого подсчитываем количество установленных битов. Хотя этот метод требует меньше памяти, чем метод множеств - в лучшем случае требуется всего лишь один бит на каждый уникальный элемент, методу битовых карт может потребоваться слишком много памяти для больших Nmax. Он также начинает тормозить, когда битовая карта перестает помещаться в оперативной памяти.
- Метод сортировки. Сортируем все элементы, после чего проходимся по отсортированному списку, подсчитывая лишь уникальные элементы и пропуская дубликаты. С первого взгляда этот метод кажется тормозным - ведь все знают, что любая сортировка сравнением работает медленнее, чем создание хэш-таблицы или битовой карты (сортировка сравнением для n элементов в лучшем случае требует O(n*ln(n)) операций, в то время как создание множества из тех же элементов на основе хэш-таблиц требует O(n) операций). Но у метода подсчета уникальных элементов с помощью сортировки есть два важных преимущества:
- Существуют эффективные методы сортировки, способные быстро сортировать списки элементов, не умещающиеся в оперативную память. Вот два основных алгоритма быстрой сортировки списков, не умещающихся в оперативную память:
- Разбить список на части, умещающиеся в оперативной памяти; быстро отсортировать эти части, после чего соединить отсортированные части в один отсортированный список, используя n-way merge. Очевидно, что различные части списка могут быть отсортированы параллельно. Это позволяет существенно уменьшить суммарное время, необходимое на сортировку. Основное преимущество этого алгоритма - его вычислительная сложность не зависит от порядка элементов в списке. Этот алгоритм обычно используется программах, ориентированных на работу на одном компьютере. Напирмер, для реализации SELECT COUNT(DISTINCT *) в СУБД.
- Мысленно разделить список на N равных частей, чтобы каждая часть умещалась в оперативную память; выбрать из списка N-1 случайных элементов, отсортировать их и поместить в список A (для выборки случайных элементов из большого списка можно воспользоваться алгоритмом "reservoir sampling"); дополнить список A спереди элементом "минус бесконечность" и сзади элементом "плюс бесконечность"; разделить начальный список на N частей, чтобы каждая часть содержала только элементы со значениями в пределах [A[i] ... A[i+1]); отсортировать каждую часть, помещающуюся в память; если какая-нибудь часть не помещается в память, то рекурсивно применить для нее этот же алгоритм. После того, как все части отсортированы, соединить их в один отсортированный список. Данный алгоритм сортировки на практике работает быстрее первого алгоритма, т.к. в нем почти все операции могут быть выполнены параллельно. Но у него есть один недостаток - его вычислительная сложность зависит от качества выборки случайных элементов. В худшем случае его скорость может упасть до O(n^2). Этот алгоритм обычно используется в программах, ориентированных на параллельную работу на нескольких компьютерах. Например, в MapReduce.
- При подсчете элементы в отсортированном списке посещаются последовательно. Это в 10000 раз быстрее, чем поиск случайного бита в битовой карте или поиск случайного элемента во множестве, если соответствующие структуры данных не помещаются в оперативную память. К тому же подсчет уникальных элементов в отсортированном списке может быть элементарно распараллелен.
- Существуют эффективные методы сортировки, способные быстро сортировать списки элементов, не умещающиеся в оперативную память. Вот два основных алгоритма быстрой сортировки списков, не умещающихся в оперативную память:
Методы оценки количества уникальных элементов в большом списке.
На практике иногда достаточно знать примерное количество уникальных элементов с некоторой погрешностью. Для таких случаев существуют быстрые алгоритмы, которым необходимо намного меньше дополнительной памяти по сравнению с алгоритмами, упомянутыми в предыдущем разделе. Рассмотрим один из таких алгоритмов. Для каждого элемента списка вычисляется значение хэш-функции, удовлетворяющей следующим условиям:- Количество различных значений хэш-функции должно превышать ожидаемое количество уникальных элементов, чтобы минимизировать количество хэш-коллизий, когда различные элементы имеют одинаковое значение хэш-функции. В идеальном случае все элементы должны иметь различные значения хэш-функции. Это условие необходимо для того, чтобы количество уникальных элементов в списке могло быть оценено, как количество уникальных хэш-значений.
- Хэш-значения для элементов из списка должны быть равномерно распределены по области значений хэш-функции. Это условие необходимо, чтобы суммарное количество уникальных хэш-значений для элементов можно было оценить, подсчитав лишь количество уникальных хэш-значений, попавших в заданный интервал. Тогда суммарное количество уникальных хэш-значений может быть вычислено как количество хэш-значений в заданном интервале, переменоженное на количество таких интервалов, помещающихся в области значений хэш-функции.
import hashlib import heapq DIGEST_SPACE_SIZE = 1 << 128 def EstimatedUniqueCount(items, samples_count): """ Estimates the number of unique items in the given list. The function hashes each item in the list, while maintaining samples_count digests with highest values (max_digests). It is assumed that digests are distributed evenly across entire digest space irregardless of input items distribution. If this assumption holds true, then the whole digest space contains K times more items than the region containing max_digests, where K = DIGEST_SPACE_SIZE / (DIGEST_SPACE_SIZE - min(max_digests)) So the number of unique items in the list can be estimated as: estimated_unique_items = K * len(max_digests) The function requires O(samples_count) additional memory. """ # This list will contain up to samples_count digests with highest values. # # The list is actually a min-heap ( # http://en.wikipedia.org/wiki/Heap_%28data_structure%29 ), so max_digests[0] # always contains the minimum digest among maximum digests. max_digests = [] for item in items: m = hashlib.md5() m.update(str(item)) digest = m.digest() if len(max_digests) < samples_count: heapq.heappush(max_digests, digest) else: heapq.heappushpop(max_digests, digest) if not max_digests: return 0 min_max_digest = long(max_digests[0].encode('hex'), 16) return (len(max_digests) * DIGEST_SPACE_SIZE / (DIGEST_SPACE_SIZE - min_max_digest)) def PreciseUniqueCount(items): """ Calculates the number of unique items in the given list. The function requires O(unique_count) additional memory, where unique_count is the number of unique items in the given list. """ return len(frozenset(items)) ################################################################################ # Test code ################################################################################ items = range(1000 * 1000) c_precise = PreciseUniqueCount(items) print 'PreciseUniqueCount(items)=%d' % c_precise for samples_count in range(10): c_estimated = EstimatedUniqueCount(items, samples_count) deviation = float(abs(c_estimated - c_precise)) / c_precise * 100 print 'EstimatedUniqueCount(items, samples_count=%d)=%d, deviation=%.3f%%' % ( samples_count, c_estimated, deviation)Вот результат работы этой программы:
PreciseUniqueCount(items)=1000000 EstimatedUniqueCount(items, samples_count=0)=0, deviation=100.000% EstimatedUniqueCount(items, samples_count=1)=11957528, deviation=1095.753% EstimatedUniqueCount(items, samples_count=2)=6015054, deviation=501.505% EstimatedUniqueCount(items, samples_count=3)=1343696, deviation=34.370% EstimatedUniqueCount(items, samples_count=4)=1002502, deviation=0.250% EstimatedUniqueCount(items, samples_count=5)=1179299, deviation=17.930% EstimatedUniqueCount(items, samples_count=6)=981848, deviation=1.815% EstimatedUniqueCount(items, samples_count=7)=1030321, deviation=3.032% EstimatedUniqueCount(items, samples_count=8)=1001097, deviation=0.110% EstimatedUniqueCount(items, samples_count=9)=1082718, deviation=8.272%Видно, что при использовании интервала, содержащего всего четыре максимальных элемента, погрешность оценки равна 0.25% :) Вышеописанный алгоритм может быть легко распараллелен - разбиваем наш список на произвольное количество частей, находим по samples_count максимальных хэш-значений элементов для каждой части, после чего находим samples_count максимальных хэш-значений среди найденных максимальных хэш-значений для всех частей.
Если вас заинтересовала эта тема, то можете углубиться в нее с помощью вот этой статьи.
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.