- Alex0:02
Hi, I'm Alex. Thanks for making the time. This is the coding round, about forty-five minutes, and we'll try to get through two problems: roughly twenty minutes each, then a few minutes at the end for your questions. There's an editor on the right, and you can run your code against the examples as often as you like. Any standard library is fine. If you get stuck, say what you're thinking; silence is harder for me to work with than a wrong idea. I'll mostly stay quiet while you code. Before we start, what are you working on these days?
- You0:39
Hi, thanks. Um, so I'm in the last year of my computer science degree. Most of my time right now goes into my thesis project, which is a booking system for shared lab equipment, like microscopes and a couple of really expensive centrifuges. So, funnily enough, a lot of time slots and overlapping reservations. It's a Python backend with Postgres and a small web front end. And in the evenings I do practice problems, mostly to get used to talking while I code, which I'm, uh, still working on. I tend to go quiet when I'm concentrating, so feel free to poke me.
- Alex1:26
Okay. Lab equipment, so people fight over the same slots. What's been the hardest part of that project so far?
- You1:34
Honestly, two people booking the same slot at the same moment. The first version checked for a conflict and then inserted, and of course two requests could both pass the check. I ended up using an exclusion constraint in Postgres on the time range, so the database rejects the second one. It took me a while to even reproduce the bug, I had to write a little script that fires requests in parallel.
- Alex2:10
How do you know the constraint actually fixed it, and not just made it rarer?
- You2:15
Uh, fair question. I ran the same parallel script a few thousand times before and after. Before, it produced a double booking every few hundred runs, after, never, and the second request gets a clean error that the API turns into a conflict response. That's not a proof, but the database enforcing it is the part I trust, rather than my code checking first.
- Alex2:47
Okay. Let's start. First problem. Given a list of intervals with start and end times, merge all overlapping intervals and return the result. Input is on standard input: the first number is n, then n pairs of start and end. Print the merged intervals one per line, start and end separated by a space. For example, 1 3, 2 6, 8 10 and 15 18 become 1 6, 8 10 and 15 18, because the first two overlap. The other examples are on the right.
- You3:19
Okay. A couple of questions first. Are the intervals sorted when they come in? And if one ends at 3 and the next one starts at 3, do they count as overlapping? Because in my booking system they don't, a slot ending at three and one starting at three is fine, but here it might be different.
- Alex3:47
Here they do. Touching intervals merge: 1 to 3 and 3 to 5 become 1 to 5. The input is not sorted, and the list may be empty, in which case you print nothing. A start is never bigger than its end, and both ends are integers.
- You4:05
Okay, good to know. And how big can n get? Just so I know if something quadratic is even on the table. And the output, sorted by start, I assume, like in the examples?
- Alex4:22
Assume up to a hundred thousand intervals, and values that fit in a normal integer. Output sorted by start, yes. Before you write anything, I'd like to hear the approach and roughly what it costs.
- You4:36
Okay. So, um, my first thought is the naive one, just to have something. Compare every pair, and whenever two overlap, merge them into one, remove the two originals, and start over from the beginning, until a full pass finds nothing to merge. That definitely works, it's correct by construction because it only stops when nothing overlaps, it's just slow.
- Alex5:09
How slow, roughly?
- You5:10
One pass over all pairs is n squared. And every merge can trigger another full pass, and there can be up to n minus one merges, so worst case it's, um, n cubed. With a hundred thousand intervals that's ten to the fifteen, so, no. Even n squared is ten to the ten, which in Python is way too slow.
- Alex5:42
Okay. Can you do better? Think about what forces you to compare every pair.
- You5:47
Hmm. The expensive part is that an interval could overlap anything anywhere in the list. If I put them in some order first, then maybe only neighbours matter. Sort by end time? Then, um, no, wait. If I sort by end, an interval with a very early start could still come late and swallow several earlier ones, like 1 to 10 would come after 2 to 3 and 5 to 6. Sorting by start is better: once they're sorted by start, the next interval can only overlap the last block I've merged, because everything before that block already ended earlier, or got absorbed into it.
- Alex6:49
Why can't the next interval overlap something before the last block? Convince me.
- You6:54
Because the merged list is disjoint and sorted. Say the new interval reached back into an earlier block. Its start is at least the start of the last block, since we're going in order of start. So if it overlapped an earlier block it would have to start before that block ends, which is before the last block starts. Contradiction. So checking only the last one is enough. Then it's one pass: extend the last block, or start a new one. Sorting is n log n, the pass is linear.
- Alex7:39
Okay. Walk me through the first example with that rule, before you code.
- You7:44
Sorted by start it's 1 3, 2 6, 8 10, 15 18, already in order. Start the merged list with 1 3. Then 2 6: 2 is less than or equal to 3, so they overlap, the block becomes 1 6. Then 8 10: 8 is bigger than 6, so a new block. 15 18, bigger than 10, new block. So 1 6, 8 10, 15 18, which matches the expected output.
- Alex8:18
Okay. Go ahead and code it. Talk me through it as you go; I'll mostly stay quiet until you run it.
- You8:26
Sure. I'll read the input in main and keep merge as its own function, so I can test it separately. I'll read all of standard input at once and split, so I don't care how the pairs are laid out on the lines.
- You9:22
So, merge. Sort by start, with a key on the first element. Then the merged list. For each start and end: if merged is non-empty and the start is less than or equal to the last block's end, I extend it, otherwise I append a new pair. Less than or equal, not strictly less, because touching intervals merge. And I store lists, not tuples, so I can update the end in place. Then main: n, then two n numbers, pair them up, print each block.
- You11:02
Sent to the workspace, python
1import sys2 3 4def merge(intervals):5 intervals.sort(key=lambda iv: iv[0])6 merged = []7 for start, end in intervals:8 if merged and start <= merged[-1][1]:9 merged[-1][1] = end10 else:11 merged.append([start, end])12 return merged13 14 15def main():16 data = sys.stdin.read().split()17 if not data:18 return19 n = int(data[0])20 nums = list(map(int, data[1:1 + 2 * n]))21 intervals = [[nums[i], nums[i + 1]] for i in range(0, 2 * n, 2)]22 for start, end in merge(intervals):23 print(start, end)24 25 26main()27
- You11:03
Okay, I think that's it. Let me run the first example.
- You11:17
The first example passes. Let me run all of them.
- You11:41
Three pass, the third one fails. That's the unsorted one where one interval contains the others. I print 1 3 and then 5 6, and it expects just 1 10. Hmm.
- Alex11:57
Before you change anything: what's happening on that input? Walk me through it.
- You12:02
Um, let me trace it. After sorting it's 1 10, then 2 3, then 5 6. Merged starts as 1 10. Then 2 3 starts inside 1 10, so I merge, but I set- I set the end to 3. I overwrite the end and, uh, lose the 10. Then, um, 5 6 doesn't overlap 1 3 anymore, so it becomes its own block. Um, so when I merge I need the max of the two ends, not just the new end. I, uh, wrote it as if the new interval always ends later, which is, I mean, exactly the contained case I mentioned five minutes ago.
- You13:25
Sent to the workspace, python
1import sys2 3 4def merge(intervals):5 intervals.sort(key=lambda iv: iv[0])6 merged = []7 for start, end in intervals:8 if merged and start <= merged[-1][1]:9 merged[-1][1] = max(merged[-1][1], end)10 else:11 merged.append([start, end])12 return merged13 14 15def main():16 data = sys.stdin.read().split()17 if not data:18 return19 n = int(data[0])20 nums = list(map(int, data[1:1 + 2 * n]))21 intervals = [[nums[i], nums[i + 1]] for i in range(0, 2 * n, 2)]22 for start, end in merge(intervals):23 print(start, end)24 25 26main()27
- You13:33
Now all four pass, including the touching one and the single point, seven seven.
- Alex13:46
Mm-hm. The four examples are the ones I gave you. What else would you test?
- You13:51
The empty input, n is zero, so it should print nothing. I also want to check that n zero doesn't crash in the parsing, since I slice the data. And completely empty standard input, which I handle with the early return. Let me run the zero one.
- You14:26
Yes, it prints nothing and exits cleanly. Other cases I'd want: all intervals identical, they should collapse to one. One interval that contains everything, which is the case that just failed. A chain where each one touches the next, like 1 2, 2 3, 3 4, which should become 1 4. And negative times would work too, since I only compare numbers.
- Alex14:57
Think about the upper end of the input size. Anything in the parsing or the output that hurts at a hundred thousand intervals?
- You15:06
Not really. Reading everything at once is two hundred thousand numbers, a few megabytes at most. The one thing I'd change for really big inputs is printing: a print per line is slow in Python, so I'd join the lines and write once. At a hundred thousand it's fine.
- Alex15:31
Okay. Time and space complexity of the final version?
- You15:35
Sorting is O(n log n) and the sweep is O(n), so O(n log n) overall, the sort dominates. Space is O(n) for the merged list in the worst case, when nothing overlaps. And Python's sort, which is Timsort, can use up to n over two extra on top of that.
- Alex15:59
Is n log n the best possible here?
- You16:02
Hmm. In general, I think so, but I'm not sure how to prove it. Intuitively, if the intervals are tiny and don't overlap, the output is just the input in sorted order, so merging is at least as hard as sorting, and comparison sorting is n log n. If the coordinates were small integers you could do better with counting, but I wouldn't bet on that being allowed.
- Alex16:43
Here's a follow-up, closer to your booking system. What if intervals arrive one at a time and you must keep the merged set up to date? Say a few hundred per second, and someone can ask for the merged set at any time.
- You16:59
Um, so I'd keep the merged blocks sorted by start, all the time. For a new interval I binary search where its start goes, with bisect, and then I have to merge it with the blocks it overlaps, which is the one just before it, maybe, and then however many after it, I walk right while the next block starts before my end, and the—
- Alex17:37
Sorry, before you go on. Why only the one just before it?
- You17:42
Because the blocks before that one end before the previous block starts, they're disjoint. Same argument as before. So at most one block to the left can be touched. To the right, it can swallow several. Then the cost: finding the spot is log n, but inserting into or deleting from a Python list is O(n), because it shifts elements. A balanced tree or a sorted container would make it log n per block touched.
- Alex18:20
One insertion can swallow many blocks. What's the cost amortized over many insertions?
- You18:25
Hmm. Each block can be swallowed at most once, after that it's gone. So over m insertions the total swallowing work is at most m. So amortized it's O(log n) per insert with the tree. With the plain list it's still the shifting that hurts, that stays linear per insert.
- Alex18:54
Python has no balanced tree built in. What would you actually use?
- You18:59
There's the sortedcontainers package, which is a third-party library, but it's very widely used. If I couldn't add a dependency, honestly, I'd keep the plain list. At a hundred thousand blocks the shift is a memmove of under a megabyte, that's microseconds, and a few hundred inserts a second is nothing. I'd measure before writing a tree by hand.
- Alex19:29
Now suppose bookings can also be cancelled, so an interval can be removed again after it was merged. What changes?
- You19:37
Oh, that's much harder. Once two intervals are merged into one block I've lost the information about where each of them was, so I can't undo it. I'd have to keep the original intervals, not just the blocks. Um, then a removal could split a block into several pieces, and to find them I'd basically re-merge the originals that were inside that block. That's linear in the size of the block. For something better I think you'd want an interval tree or a segment tree with counts over the coordinates, but I haven't implemented one of those in a long time.
- Alex20:33
Last one on this problem. How would you find the free gaps between merged intervals?
- You20:39
That's easy once they're merged: walk the merged list and every pair of neighbours gives a gap from the previous end to the next start. If the ends are inclusive, uh, then for integers the gap is previous end plus one to next start minus one, and if they're touching there's no gap at all. Linear after the merge. That's basically the free-slot view in my booking system.
- Alex21:11
Okay. Let's leave that one there. We're a bit over twenty minutes in, so let's move on.
- Alex21:19
Second problem. Given an array of integers and an integer k, return the k most frequent elements. Input: n, then the n numbers, then k. Print the k values on one line. For example, 1 1 1 2 2 3 with k equal to 2 gives 1 and 2.
- You21:38
Can there be ties at the boundary? Like if the k-th and the k-plus-first value have the same count, which one do I return?
- Alex21:52
The answer is guaranteed to be unique, so you won't get a tie at the boundary. k is between 1 and the number of distinct values, so it's always possible to return k values.
- You22:05
Okay. And the output order? And can the numbers be negative?
- Alex22:12
Order doesn't matter for the answer, but the examples print them ascending. Yes, negatives are possible.
- You22:18
Okay. So the first step is clearly counting: a dictionary from value to frequency, one pass, O(n). Negatives don't matter for a dictionary. Then the simple version is to sort the distinct values by count, descending, and take the first k. If m is the number of distinct values, that's O(n plus m log m). I'll write that first, so I have something that works, and then see if I can do better.
- Alex22:54
Okay. Go ahead and write the simple version.
- You23:16
Same reading as before. Counter from collections for the counting. Then sorted over the keys, with the count as the key, reversed, and slice the first k.
- You24:09
Sent to the workspace, python
1import sys2from collections import Counter3 4 5def top_k(nums, k):6 counts = Counter(nums)7 ordered = sorted(counts, key=lambda v: counts[v], reverse=True)8 return ordered[:k]9 10 11def main():12 data = sys.stdin.read().split()13 n = int(data[0])14 nums = list(map(int, data[1:1 + n]))15 k = int(data[1 + n])16 print(*top_k(nums, k))17 18 19main()20
- You24:22
All four pass, including the negative one.
- Alex24:28
What's the complexity of that, with n numbers and m distinct values?
- You24:33
Counting is O(n), sorting the distinct values is O(m log m), so O(n plus m log m), and m can be as big as n, so O(n log n) worst case. Space O(m) for the counts.
- Alex24:52
Think about what you're sorting. You only need the top k, not a full ordering. What structure exploits that?
- You24:59
A heap. A min-heap of size k, keyed on frequency. I push every value with its count, and when the heap gets bigger than k I pop the smallest, so what's left at the end is the k biggest. Each push and pop is log of the heap size, so it's O(n plus m log n)... no, sorry, m log k, the heap never has more than k plus one items.
- Alex25:38
When does the difference between log m and log k actually matter?
- You25:43
When k is much smaller than m. Like the top ten out of a million distinct values: log ten instead of log a million, and the heap is tiny in memory. If k is close to m it's basically the same as sorting. Python also has heapq dot nlargest, which does exactly this, but I'll write it out.
- Alex26:11
Walk me through the heap on the first example before you write it.
- You26:17
Counts are 1 three times, 2 twice, 3 once, k is 2. Push three-comma-one, heap has one item. Push two-comma-two, two items, fine. Push one-comma-three, three items, more than k, so pop the smallest, which is the count one, value 3. Left with value 2 and value 1. Which is the answer.
- You27:26
Sent to the workspace, python
1import heapq2import sys3from collections import Counter4 5 6def top_k(nums, k):7 counts = Counter(nums)8 heap = []9 for value, freq in counts.items():10 heapq.heappush(heap, (freq, value))11 if len(heap) > k:12 heapq.heappop(heap)13 return [value for freq, value in heap]14 15 16def main():17 data = sys.stdin.read().split()18 n = int(data[0])19 nums = list(map(int, data[1:1 + n]))20 k = int(data[1 + n])21 print(*top_k(nums, k))22 23 24main()25
- You27:43
Huh. Two fail now, the first one and the last one. Example 1 prints 2 1 instead of 1 2. And the last one prints 9 8 instead of 8 9.
- Alex27:59
The set of values looks the same. And the first version passed all four. Why?
- You28:05
Because, um, the first version printed in order of frequency, highest first, and in these examples that happens to be ascending by value too. That was, uh, luck, I never actually sorted by value. The heap gives them back in heap order, smallest frequency first. The set of values is right, it's just the order. I'll sort the output, that's k log k, which doesn't change the bound. I should have sorted in the first version too.
- You29:10
Sent to the workspace, python
1import heapq2import sys3from collections import Counter4 5 6def top_k(nums, k):7 counts = Counter(nums)8 heap = []9 for value, freq in counts.items():10 heapq.heappush(heap, (freq, value))11 if len(heap) > k:12 heapq.heappop(heap)13 return [value for freq, value in heap]14 15 16def main():17 data = sys.stdin.read().split()18 n = int(data[0])19 nums = list(map(int, data[1:1 + n]))20 k = int(data[1 + n])21 print(*sorted(top_k(nums, k)))22 23 24main()25
- You29:17
All four pass again.
- Alex29:25
Okay. Can you get it linear in n, without the log factor?
- You29:30
Linear... Quickselect on the counts would be linear on average: partition the distinct values around the k-th largest frequency, like the partition step in quicksort, and recurse into one side only. But worst case it's quadratic, unless I do the median-of-medians thing, and it's fiddly to write in an interview. Um, is there something simpler...
- Alex30:09
Think about the values you're ordering by. What's the largest a frequency can be?
- You30:14
At most n. Oh, so the frequencies are small integers, I can bucket by frequency: an array of n plus one lists, put each value in the bucket of its count, then walk from the top bucket down until I have k values. That's O(n) time and O(n) space, no comparisons at all. Let me write that.
- You31:22
Sent to the workspace, python
1import sys2from collections import Counter3 4 5def top_k(nums, k):6 counts = Counter(nums)7 buckets = [[] for _ in range(len(nums) + 1)]8 for value, freq in counts.items():9 buckets[freq].append(value)10 out = []11 for freq in range(len(nums), 0, -1):12 for value in buckets[freq]:13 out.append(value)14 if len(out) == k:15 return out16 return out17 18 19def main():20 data = sys.stdin.read().split()21 n = int(data[0])22 nums = list(map(int, data[1:1 + n]))23 k = int(data[1 + n])24 print(*sorted(top_k(nums, k)))25 26 27main()28
- You31:33
All pass. The final sort is only over k values, so it's still linear apart from that. In practice I'd probably still pick the heap, though. The buckets array is size n even when there are only a few distinct values, so it's mostly empty lists, and the heap version is easier to read.
- Alex32:01
Different setting now. Say the numbers are product IDs from a click stream, billions a day, and you have one machine with a fixed amount of memory. What if the data is a huge stream and memory is limited?
- You32:16
So I can't keep a count for every value. Honestly, I don't know the standard answer here. I think there's a probabilistic structure, a sketch of some kind, that keeps approximate counts in fixed memory, with hashing, but I couldn't explain how it works or what error it gives. What I can reason about is that you'd have to give up exact answers.
- Alex32:56
Okay. Say you can keep only a fixed number of counters, a few thousand. A value with no counter arrives and every counter is taken. What do you do?
- You33:07
Then, um, when a value comes that has a counter, increment it. If it doesn't and there's a free counter, take it. If they're all full... maybe decrement all of them, and drop the ones that hit zero? Then something really frequent can't be pushed out for long, because it gets incremented more often than everything gets decremented. So the heavy values survive. But the counts are only lower bounds, and I'm not sure about the exact guarantee, like how frequent something has to be to be safe.
- Alex33:59
Mm-hm. You said frequent values survive. Try to work out how frequent a value must be to be safe.
- You34:07
Okay. Say c counters and n items in the stream. Every time I decrement, I decrement c counters plus the new item I'm throwing away, so c plus one units of count disappear together. That can happen at most n over c plus one times in total. So a value's counter is at most n over c plus one below its true count. So anything that appears more than n over c plus one times must still have a counter at the end. I think that's right, but I'd want to check it on a small example.
- Alex35:06
Okay. One more variation. What if frequencies change over time and you need the current top k? Say the top products in the last hour, refreshed every few seconds.
- You35:17
Then I'd count over a window, like the last hour, with a count per value per minute, and drop the oldest minute as time moves. The totals change by small steps. Recomputing the heap on every query is m log k, so if queries are frequent I'd keep a sorted structure keyed on count that I update whenever a count changes, and read the top k off the end.
- Alex35:53
Before we finish, back to the first problem. You said the sort can use extra memory. Would you change the merge if memory were tight?
- You36:03
I could write the merged blocks back into the same list with a write index, and truncate at the end. The sort is already in place, intervals dot sort. Then it's O(1) extra apart from what the sort itself needs. It does modify the caller's list, though, so I'd say so in the docstring or name it something like merge in place.
- Alex36:36
Okay. That's both problems. We have a few minutes left. Do you have any questions for me?
- You36:43
Yes, um. What does a good first few months look like for someone joining as a new grad? Like, what would you want to see from them?
- Alex36:57
It depends on the team. Usually a small, well-scoped project first, so you learn the codebase and the deploy process end to end. Asking questions early matters more than shipping fast, and there's a lot of code review in both directions. The hiring team can give you the specifics for this role, including who you'd work with and what the first project would likely be.
- You37:22
Okay. And is this kind of problem close to the day-to-day work, or more of a filter?
- Alex37:32
Some of both. Nobody merges intervals all day, but reading a problem carefully, testing your own code and reasoning about cost come up constantly. The reasoning and the testing matter more than the exact problem.
- You37:45
Okay. And what happens after this round? How soon would I hear back?
- Alex37:53
The report from this round is ready in a few minutes. The hiring team handles the next steps and the timing; they'll be in touch about that.
- You38:03
Okay. That's all from me, thank you.
- Alex38:10
Thanks, that's time. The report covers both problems and will be ready in a few minutes.