Sorting 1 million 8-digit decimal numbers in 1MB of RAM
stackoverflow.com
stackoverflow.com
So imagine you have a bit field where 'one bits' indicate you have seen the number and 'zero bits' indicate you haven't. And you compress it with run-length encoding.
Your initial data structure is '99999999:0' (all zeros, haven't seen any numbers) and then lets say you see the number 3,866,344 so your data structure becomes '3866343:0,1:1,96133654:0' as you can see the numbers will always alternate between number of zero bits and number of '1' bits so you can just assume the odd numbers represent 0 bits and the even numbers 1 bits. This becomes (3866343,1,96133654)
Anyway, as you get numbers you will split and coalesce the bit space until you've either seen all numbers (0, 99999999) or you run out of memory because a hacker has sent you only the even numbers from the space.
Its a clever 'math trick' which explores how you think about numbers (are they a thing or a representation). But I never thought it really gave a good indication of whether or not you would be a good candidate for the company.
So that solution does not cover all possible cases. Lovely of Google to ask that question.
Edit: not only that, the running time of Google's "solution" would be terrible, aren't there other possible data sets not covered or am I missing something?
From my experience interviewing for Google, I beg to differ. Many other accounts seem to confirm this.
Edit: the exact amount of bits necessary is ceil(log_2((1e10+1e6-1) choose 1e6)), which is ~1.756 megabytes.
Edit edit: see udiv's comment below: that 1e10 should be 1e8 of course.
I think the parent's solution works. The typical separation between numbers is about 2^7: that works out to 8 bits per number -- 7 for the length of the '00...0', plus one length-one '1'. 8 bits * 10^6 is 1 MiB exactly. You need a variable-length encoding, so you can fit 2^7 in 7 bits while still allowing larger numbers to be encoded.
An argument that you need context info: if you don't use context info, each bit pattern needs to be rounded up to the nearest number of whole bits. That waste's about N/2 bits. According to my calculations, that would raise the bound from 0.96MiB to 1.02MiB, i.e. too much.
But really, it's even worse. Let's assume you can indeed define such a compression and you manage to use the context sufficiently to avoid wasting too many bits. Further, you manage to do so without using much temporary space at all.
That means you'll need to be updating your sorted list in place. Each update is likely to be a non-trivial task, since you need to find the place to update (with variable-width encoding and no space for luxuries like an index, that's probably a linear scan), and you need to take into account some amount of context - and then, because you've changed some data, recode possibly unbounded amounts (so N) of following context. That makes this sorting algorithm quadratic; and it'll have a high scaling factor too since the operations we're counting here aren't cheap: we're not moving bits around, we're decoding and recoding them all the time in a context-sensitive way and that means doing lots and lots of branches.
Assuming the 1MB limit is reasonably inspired because the chip neads to be tiny, you're probably not dealing with the fastest chip in the world. As an estimate, I'd guess this algorithm (if we could make it) would be about 1000000 times slower than a normal sort (assuming a 13 times higher constant factor), and you'd be running it on something very very slow. On a modern processor, a plain sort takes about 75ms, making this thing 75 ks, or approximately 1 day. On that very limited 1MB proc...
So even you manage to pull it off (very hard), it's almost certainly going to be very slow.
1) It splits a region (it causes a string of zeros to have a one in them) (adds 8 bytes)
2) It coalesces a region (it removes the last remaining zero between two regions) (subtracts 8 bytes)
I'm sure there are other solutions, and as was pointed out it doesn't really deal with duplicates. I suspect a multi-value bit field might cover that.
I simulated it with a Python program and you get about 10^4 coalesces. That doesn't even begin to make a dent in 10^6, and storing this will still take ~7 megabytes.
Right.
and you'll end up with 8 bytes x 10^6 = 7.6 megabytes.
Not if you encode cleverly. The successive differences will be on the scale of 10^8/10^6 = 100, which is a very small number. It takes 7 bits to store, or at least 8 bits in a variable-width encoding.
The reason this was an interview question was because it allowed the interviewer to see if the candidate could see 'past' the requirement of sorting (which pre-loads your mental algorithm cache with all sorts of algorithms) to the somewhat more subtle concept of embedding partial computation into the data structure.
Early in my career at Sun I was tasked with putting mandatory file and record locking into UFS in the kernel, the challenge had always been how do you avoid deadlock? Of course one level deadlock was fairly easy, you need to know if process A is holding a lock and waiting on process B which is holding a lock elsewhere, and you are in the context of process B trying to get this lock then you can see that you will deadlock A. But 'n' level deadlock starts getting harder because the possibilities multiply. I discovered that you could create a doubly linked tree structure which had modest memory growth per-process but any process could walk it one way and look for 'itself' on the list to discover if there was a deadlock possible. Here the 'code' was walk a list and note that you were already on it, so you would deadlock if you got the lock.
The number 'sorting' problem is very much like that because while numbers have different values, they are intrinsic values. Unlike strings which consist of an arbitrary sequence of tokens. So the goal of the question was to identify people who could see the difference between 'sorting numbers' as a problem and 'general sorting.'
Big problem #1: insertions for 1M integers would take ages.
Big problem #2: And some distributions can't be covered this way. For example, 1m integers with distances 0:99 (e.g. +99 each one). Now think the same but with random distance in the range of 0:99. (Note 99999999/1000000 = 99.99, so it's possible input)
Google's approach is nonsense, too.
Or it might have just been some silly way to eliminate candidates for arbitrary reasons. The usual.
Anyway, sorting is a big deal I think. If you can do it faster even by just a little bit than everyone else, that's a competitive advantage. Just my personal opinion.
With all the discussion I decided to code up a quick version in perl. The code is pretty straight forward, start with the tuple (99999999,0) and on each number do one of three actions:
1) Break the number representing a string of zeros into two segments.
2) Extend the length of an existing region of one bits.
3) Collapse two regions of one bits separated by a single zero bit.
Now the two key to the reasons it doesn't work are that one, it takes two 32 bit numbers to identify non-adjacent numbers, and two, for a uniform distribution random number generator the ratio of a million numbers to a space of 100 million means that most of the numbers won't be adjacent. So each 1 bit will cost 8 bytes to store. A million numbers * 8 bytes is 8 million bytes (7.6MB if you use 2^20th as 1MB). Its fun watching the algorithm because it gets slower and slower (its doing a linear search of segments to insert 'bits' and memory goes up and up, until you have about 50M uniques and then it starts getting faster and faster again as it collapses more and more segments.
Storing it on disk with an uncompressed bitmap, runs in O(n) time, and of course you 12,500,000 bytes, just about 12MB (to count multiples you need to multiply that by the number of bits you want to reserve per-number) but doing it in memory only requires a better compression algorithm than simple run-length encoding.
You rock. I wish more people at HN/SO/Google were like you.
There are one million numbers to be sorted, out of a space of one hundred million. That means that no number may be more than 100 away from any other number once fully sorted (7 bits). Therefore, you can simply use deltas to encode your numbers, as 7 bit deltas * one million numbers < 1 MB RAM.
EDIT: should've been clearer: no number may be more than 100 away from any other number on average once fully sorted. Therefore, it's an average of 7 bits per number, maximum. Duplicates are even easier, since it's only one bit to encode the duplicate (a delta of zero).
EDIT 2: As for the encoding to be used, I think a universal code or Golomb code would probably be sufficient. They can get quite close to natural entropy.
Since the #s are stored sorted and bounded in size, they can be encoded as deltas which will be more space efficient than storing absolute values. Now we just need to figure out the worst case encoding and will 1 million values fit?
the best "high level" explanation, i think, is that you are compressing the sorted numbers, which are therefore not random, and so concerns about the incompressibility of random streams are completely irrelevant.
8 decimal digits takes 26.6 bits. An ordered list of 10^6 of these takes 3.17 MiB. The information contained in the ordering is lg(10^6!) ~= 2.20 MiB [0]. So as an unordered multiset, the information content is 0.96 MiB. It's at least theoretically possible to store the input in 1 MiB of working memory. But only just; in fact it's significant that the problem specifies 2^20 bytes, because for an SI megabyte (10^6 bytes), it wouldn't work.
I don't think it's actually possible though. The answers here don't do it. LZMA accomplishes nothing.
[0] Stirling's approximation in base 2: lg(n!) ~= n lg(n) - n/ln(2)
Can you point me to a resource that discusses the information theory behind this claim? I'm interested in learning more, but don't know what to search for.
https://news.ycombinator.com/item?id=4680259
An ordering of a storage scheme doesn't always store information. E.g. if the list is sorted, the ordering is completely determined by the data -- it's redundant.
For storing integers, an idea to is store the pairwise differences between sorted elements. E.g. [30,10,20] -> [10-0,20-10,30-20] = [10,10,10]. If you have N integers of typical size X, the differences will be much smaller, typically on the scale of X/N. With variable-width integers, X/N takes lg(N) fewer bits to encode than X. So you save N lg(N) bits (asymptotically the same as lg(N!)), compared to storing the integers literally.
I saw that comment. I'm not clear how that's not still storing ordered data, or rather how the order there is not storing information.
Obviously my information theory knowledge is weak.
> if the list is sorted, the ordering is completely determined by the data -- it's redundant.
I don't understand this. In what way is the information redundant? If there's 3.17 MB of data in the ordered list, and 2.2 MB of data in the ordering itself, the information stored in the ordering cannot be redundant, because that would mean >4.4MB of information is stored in the ordered list.
Could you please explain this more or point to other links?
This requires no memory other than that for the networking stack. It is, of course, also completely impractical.
I think one way to think about this is from a combinatorics viewpoint: how many possible combinations of sorted number orderings are there? If we give the combination 0,0,0,....,0 the code 0, and 0,0,0,...,1 the code 1, and 99999999, 99999999, ... 99999999 the code N, what is N? In other words, how big is the result space?
Well, one way to think about this is noticing that this is a bijection of the problem of finding the number of monotonic paths in an N x M grid, where N = 1,000,000 and M = 100,000,000. In other words, if you have a grid that is 1,000,000 wide and 100,000,000 tall, how many shortest paths from the bottom left to the top right are there? Shortest paths of course require you only ever either move right or up (if you were to move down or left you would be undoing previously accomplished progress). To see how this is a bijection of our number sorting problem, observe the following:
You can imagine any horizontal leg in our path as a number in our ordering, where the Y location of the leg represents the value (image: http://i.stack.imgur.com/aJp4b.png ). So if the path simply moves to the right all the way to the end, then jumps all the way to the top, that is equivalent to the ordering 0,0,0,...,0. if it instead begins by jumping all the way to the top and then moves to the right 1,000,000 times, that is equivalent to 99999999,99999999,..., 99999999. A path where it moves right once, then up once, then right one, then up once, etc to the very end (then necessarily jumps all the way to the top), is equivalent to 0,1,2,3,...,999999.
Luckily for us this problem has already been solved, such a grid has (N + M) Choose (M) paths:
(1,000,000 + 100,000,000) Choose (100,000,000) ~= 2.27 * 10^2436455
N thus equals 2.27 * 10^2436455, and so the code 0 represents 0,0,0,...,0 and the code 2.27 * 10^2436455 and some change represents 99999999,99999999,..., 99999999.
In order to store all the numbers from 0 to 2.27 * 10^2436455 you need lg2 (2.27 * 10^2436455) = 8.0937 * 10^6 bits.
1 megabyte = 8388608 bits > 8093700 bits
So it appears that we at least actually have enough room to store the result! Now of course the interesting bit is doing the sorting as the numbers stream in. Not sure the best approach to this is given we have 294908 bits remaining. I imagine an interesting technique would be to at each point assume that that is is the entire ordering, finding the code for that ordering, and then as you receive a new number going back and updating the previous code. Hand wave hand wave.
It's probably actually a question of whether the update function could be done in the remaining memory - it certainly couldn't unpack the whole representation.
In your case you translated the problem to a different problem space and solved it there. The other contributors in stack overflow tend to do the same eg: Using network latency, compression etc to solve these problems.
These sort of solutions become very interesting when they become isomorphic to some other real world problems.
Any such path ends at (N,M). Such a path can be represented as a sequence of N+M bits, 0=right, 1=up, where there are exactly M ups. So choosing a path is identical to choosing the positions of the M ups, thus there are (N+M) choose (M) such paths.
EDIT: My first proof was needlessly complicated because it dealt with paths that ended at arbitrary (N,m) but really you can just let the paths go all the way up to (N,M)-- even if the max value in the list is m < M-- and just ignore the last part of the path. It's still a bijection.
Depending on his dataset characteristics a radix sort can have a space requirement as low as a few hundred bytes to sort several million values.
EG: 8-bit values, Simply make a 256byte array. Increase the appropriate count on each value when you see a value. When you've gone through the list, loop through the array outputting count values at that index. It's also quite cache friendly, mind the last time I compared was on a Pentium PRO to quick sort.
For larger datasets, you actually want to compare on the digits (LSD or MSD first), and that'll take more memory.
EDIT: Originally posted that it'd take 256 bytes of memory. That's not true for his dataset.
Got code?
One could implement a simple block allocator, where each block contains a sequential list of deltas.
The trick to fast insertion is to place new blocks at addresses interpolated between 0 and 10^8. If there is a collision, merge blocks. If the input distribution is off, physically reallocate colliding blocks left or right into free space.
So inserting the numbers 10, 20, 1000, 2000, 1M, 2M would give you a heap looking like:
[head->[10,10]->[980]->[1000]->[998000]->[1000000]->tail]
As more numbers are inserted, blocks combine until you end up with one contiguous block.
We're given 1 million integers from 0 to 99,999,999. We only have 1MB of RAM, or an average ~8 bits for each of the million numbers. So we can't store the numbers directly since they take ~27 bits each.
First thought was to use a bitset but that would require 100 million bits, and we only have ~8 million bits RAM, so that's not going to work. Also need to deal with duplicates.
How about this. Something similar to a selection sort algorithm that stores deltas of distances between sorted numbers. As a number is streamed in, we start scanning from the beginning of the list until it's correct position found, where it is inserted and then push down the remaining numbers. This will be O(n^2).
Since the average delta distance between numbers is about 100, we'll use 8-bits to store the delta value. Value 0 means the number is a duplicate of the current number. Values 1-254 mean add this number to the current number for the new number. Value 255 means add 255, then use the next byte as the delta value (repeat until the value != 255).
(Case 1) 1 million ints exactly 100 apart: 0, 100, 200, 300, 400, ..., 99999800, 99999900 Stored as a list of 8-bit delta values: 0, 100, 100, 100, 100, ..., 100, 100 (1 million bytes total)
(Case 2) 1/2 million zeros, then 1/2 million values of 99999999. Stored as: start: 0, 0, 0, 0, ... (1/2 million zero deltas) then: 255, 255, 255, 255, ... (99999999 / 255 = 392,156 times repeated, which gets us to number 99,999,780) then: 219, 0, 0, 0, 0, ... (another 1/2 million zero deltas)
So the total amount of storage for Case 2, which I presume is worse case (but correct me if I'm wrong!) is: 500,000 + 392,156 + 1 + 500,000 = 1,392,157 bytes to store the delta values.
1MB = 1,048,576 bytes, so I'm over by 343,581 bytes... (so close!)
We'll have to modify this scheme so that we reduce the number of 255 values, which should not be hard to do and will get us under the 1MB size limit. Or we could try something fancier like huffman coding to reduce the size of the delta values.
An alternative method is to use one bucket into which all values below a limit (e.g. 20,000,000) are sorted as they arrive, and compress all the rest. When 10^6 values have arrived, transmit the bucket and then reuse the empty bucket repeatedly to sort the compressed values.
I was wondering why you can't use TCP as a form of storage, possibly many ways but latency and buffers would actualy work for you as crude storage. Not that it is need in this case but it is one form of queue that could be abused to store data.
http://stackoverflow.com/questions/12748246/sorting-1-millio...
You can 'store' data in DNS, too by, measuring response time to non-existent domains. The first lookup stores a one; skip it if you want to store a zero, the second one destructively reads it with some probability of data loss.
If you try that encoding scheme with some random data you'll find it won't always fit.
I took 1,000,000 random numbers between 0 and 99,999,999 and sorted them and calculated the deltas. Here's the distribution of the sizes of the deltas:-
diffbits: 1 nos 5009
diffbits: 2 nos 19937
diffbits: 3 nos 38275
diffbits: 4 nos 72319
diffbits: 5 nos 128146
diffbits: 6 nos 202000
diffbits: 7 nos 252698
diffbits: 8 nos 202681
diffbits: 9 nos 72750
diffbits: 10 nos 6148
diffbits: 11 nos 37
totbits = (1*5009)+(2*19937)+(3*38275)... = 6,408,685
That's the total number of bits required to store just the deltas, but doesn't include the length encoding.810241024 - 6408685 = 1979923 spare bits.
Each of the 999,999 deltas will take at least one bit to determine the size of the delta, leaving you with 979,924 bits for extra length encoding and wastage (if you pack multiple lengths under one encoding).
There's no way I can see (after trying lots of permutations) to be able to encode all of those bunches of diffs using those few remaining bits.
What's even more difficult is that you don't have the space to 'calculate statistics for different code types' because you can't fit anywhere near all of the deltas into that 1MB of memory without having them encoded perfectly anyway. Calculating them on partial data is all that you can do, and that's not going to be accurate enough because you've no idea what the unseen data has to hold (it could all be duplicates or 11 bit differences...).
I'm keeping going on looking at this (and trying not to look at any of the posted solutions).
Thank you for your reply!
The ROM is used to store the program itself, so the code doesn't take up any of the limited memory itself.
edit: unless perhaps you make use of haskell's FFI, then maybe haskell could beat it on LOC