Finding the top K items in a list efficiently
stevehanov.ca
stevehanov.ca
dataset_1mil = range(1000000)
dataset_1k = range(1000)
dataset_100 = range(100)
[random.shuffle(d) for d in (dataset_1mil, dataset_1k, dataset_100)]
%timeit sorted(dataset_1mil, reverse=True)[:10]
# 1 loops, best of 3: 1.12 s per loop
%timeit heapq.nlargest(10, dataset_1mil)
# 1 loops, best of 3: 330 ms per loop
%timeit heapSearch(dataset_1mil, 10)
# 1 loops, best of 3: 268 ms per loop
%timeit sorted(dataset_1k, reverse=True)[:10]
# 1000 loops, best of 3: 302 us per loop
%timeit heapq.nlargest(10, dataset_1k)
# 1000 loops, best of 3: 248 us per loop
%timeit heapSearch(dataset_1k, 10)
# 1000 loops, best of 3: 346 us per loop
%timeit sorted(dataset_100, reverse=True)[:10]
# 10000 loops, best of 3: 22 us per loop
%timeit heapq.nlargest(10, dataset_100)
# 10000 loops, best of 3: 46.9 us per loop
%timeit heapSearch(dataset_100, 10)
# 10000 loops, best of 3: 128 us per loop
There you have it. Sorted is best for small datasets in the hundreds, break-even is in the thousands, heapSearch wins in the millions.Datastructures are great, but be aware of the strengths and weaknesses of their implementations. Most recently I discovered that checking collisions in many small sets is faster with tuples (creating sets is rather slow). Test things with your data, not just "big data".
Bonus tip: Amortize when you can. If you're sorting your data anyways, then even the cleverest heap won't undo work already done.
The problem in question is 2B:
2. (30 pts.) You are given an array A with n distinct
numbers in it, and another array B of
ranks i1 < i2 < : : : < ik. An element x of A has rank u
if there are exactly u 1 numbers in
A smaller than it. Design an algorithm that outputs the
k elements in A that have the ranks
i1; i2; : : : ; ik.
(B) (20 pts.) Describe a O(n log k) recursive algorithm
for this problem. Prove the bound
on the running time of the algorithm
Source: http://www.cs.illinois.edu/class/sp11/cs473/hw/hw_03.pdfIs that possible?
In a traditional heap implementation, popping is performed by replacing the root with the bottom-right-most element and heapifying down. This is potentially 2 * log(k) comparisons. Pushing is performed by adding the new element to the bottom-right and heapifying up, which is up to log(k) comparisons. We can combine the pop and push into a single operation and reduce the 3 * log(k) comparisons to just the 2 * log(k) comparisons. Instead of replacing the root with the bottom-right-most element, just replace it with the new element and heapify down. This not only reduces the worst-case number of comparison; it improves the expected number of comparisons since the bottom-right element is likely to still belong at the bottom and will be pushed down the entire tree, while the new element is more likely to belong somewhere shallower.
select * from ... order by non-index-column-or-calculated limit k
it would dump all the results into a temp table (possibly on disk) and then sort them, instead of keeping a current top-k buffer.It seems to me that if you know the K'th largest, it's O(N); but otherwise, it's harder.
Finding the kth largest element can be done in O(N) time.
Obviously, as k approaches n, this algorithm is in reality O(n^2)
In fact, this can be done in O(n) time even if k isn't constant: use the selection algorithm to find the k'th largest element (in O(n) time) and then go through the list again and output every number that's k or larger.
It's typically easy to reduce solving such a problem with duplicates allowed into a very similar one without duplicates. Just replace every element x by the ordered pair (x, the position in the array where x came from) and then use a modified comparison function that sorts according to the second element in the pair if two ordered pairs are equal in the first element.
There are N elements in the sequence, and you can arrange for the sorting to be a single-step bubble sort, so that the total cost is O(K) operations per element --> O(NK) overall. Noting that K is constant, it's O(N).
Note that this really assumes K is constant. If K is depends on N, then the array A will expand
The complexity of nth_element is O(N).
#include <algorithm>
#include <vector>
#include <iostream>
#include <iterator>
using namespace std;
int main() {
int K;
cin >> K;
vector<int> numbers((istream_iterator<int>(cin)),
istream_iterator<int>());
nth_element(numbers.begin(), numbers.begin() + K, numbers.end(),
greater<int>());
for (int i = 0; i < K; ++i) {
cout << numbers[i] << ' ';
}
cout << '\n';
}
Input: 5
1 9 1 3 7 8 2 11 2 5 5 9 1 7
Output: 7 9 9 11 8
Note: the output is not sorted.in mine (osx 10.6.7, g++ 4.2.1), it's in
/usr/include/c++/4.2.1/bits/stl_algo.h
and it uses an algorithm very similar to the original article
If you look there, you will see it internally uses a heap.
template<typename _RandomAccessIterator>
inline void
nth_element(_RandomAccessIterator __first, _RandomAccessIterator __nth,
_RandomAccessIterator __last)
{
typedef typename iterator_traits<_RandomAccessIterator>::value_type _ValueType;
// concept requirements
__glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<_RandomAccessIterator>)
__glibcxx_function_requires(_LessThanComparableConcept<_ValueType>)
__glibcxx_requires_valid_range(__first, __nth);
__glibcxx_requires_valid_range(__nth, __last);
if (__first == __last || __nth == __last)
return;
std::__introselect(__first, __nth, __last,
std::__lg(__last - __first) * 2);
} getKSmallest k list = take k (sort list)
or even shorter in point-free notation: getKSmallest k = take k . sort
http://en.wikipedia.org/wiki/Selection_algorithm#Language_su...Some benchmarks might convince me :)
GHCi, version 6.12.1: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ... linking ... done. Loading package integer-gmp ... linking ... done. Loading package base ... linking ... done. Loading package ffi-1.0 ... linking ... done. Prelude> :m Data.List -- for sort function Prelude Data.List> :se +s -- to display timings.
Prelude Data.List> last $ sort [1..2^20] 1048576 (1.34 secs, 544634924 bytes) Prelude Data.List> last $ sort [1..2^21] 2097152 (2.84 secs, 1125744400 bytes) Prelude Data.List> last $ sort [1..2^22] 4194304 (5.51 secs, 2325062504 bytes) Prelude Data.List> 1.34/(2^20)/20 6.389617919921875e-8 (0.02 secs, 1576728 bytes) Prelude Data.List> 2.84/(2^21)/21 6.44865490141369e-8 (0.00 secs, 1578108 bytes) Prelude Data.List> 5.51/(2^22)/22 5.971301685680042e-8 (0.00 secs, 1572220 bytes)
Looks like it really sorts, even strictly ascending sequence (2^22 time problems could be attributed to GC or something).
Let us take first N from sorted list:
Prelude Data.List> sum $ take (2^1) $ sort [1..2^22] 3 (2.25 secs, 760208692 bytes) Prelude Data.List> sum $ take (2^2) $ sort [1..2^22] 10 (2.50 secs, 760292356 bytes) Prelude Data.List> sum $ take (2^3) $ sort [1..2^22] 36 (2.25 secs, 759678188 bytes) Prelude Data.List> sum $ take (2^4) $ sort [1..2^22] 136 (2.51 secs, 760209208 bytes) Prelude Data.List> sum $ take (2^5) $ sort [1..2^22] 528 (2.36 secs, 759678236 bytes) Prelude Data.List> sum $ take (2^6) $ sort [1..2^22] 2080 (2.31 secs, 759682904 bytes)
I don't know why this looks almost constant. But it is!
I expected to be able to show O(nlogk), but it performed even better. ;)
(Algorithms aside, the full sort of 10M numbers, apparently using an underlying C implementation, looks stunishingly slow: Given at most 240M comparisons for merge-sorting the array, taking 10s would mean at throughput only 24M items per sec, bad memory access pattern at play maybe?)