Trees & Graphs
- Resolve a loosely written path string to a node before you walk it, and pick a sentinel for a path that does not exist
- Walk a shallow, million-wide directory level by level with a queue, then stream paths in delete order when the result will not fit in memory
- Rebuild a span tree from flat parent pointers and score every root-to-leaf chain
- Find the fastest route through a one-way service map and return the hops along with the time
Once your directory walk runs, Datadog interviewers usually change the tree. First, a path might arrive as a loose string from a config file that has to resolve to a node before you walk it. Then the directory turns shallow and extremely wide, and soon the list of paths you were building no longer fits in memory. The basic walk is in Recursion & Pattern Matching, and these problems pick up where it ends.
Two of the problems leave the filesystem behind but use the same skill. An APM trace reaches the backend as flat span records that point at their parents, so you rebuild the tree before you can score anything. A service map is a weighted graph. To find the quickest route between two hosts, you run Dijkstra and save a parent pointer on every update. In all of them, the decision that matters most comes early, and it's about where you keep the work you haven't done yet.
Why hierarchies keep coming back
Datadog's reported coding problems use hierarchies a lot because the product is full of them. A host's directory tree, a trace's spans and a service map's call graph are all structures the Agent or the backend reads every day. The base version is easy on purpose. The difference between candidates is how the code holds up once the input gets harder, such as a path that may not exist or a queue with millions of entries in it.
/var/log on hosts where one folder holds years of rotated files. The trace view rebuilds span trees from parent ids before it can draw a flame graph, and the service map is a latency-weighted graph of which service calls which. All of that code runs on a customer's host or inside a busy backend, where a walk that holds every path at once is the failure that matters. So the follow-ups ask about memory and depth long before anyone wants a cleverer algorithm.Where the pending work lives
Every problem below is a traversal, and the choice that matters is the container holding the work you have not done yet. The call stack is fine for ten levels and falls over at ten thousand. An explicit stack survives any depth. A queue drains one level before it starts the next, which suits a shallow tree with a huge fan-out. A generator keeps one root-to-leaf path in memory and hands results back as it finds them, and the min-heap in Dijkstra is the same idea ordered by distance.
deep tree, small fan-out -> explicit stack of (node, path)shallow tree, huge fan-out -> queue, drained one level at a timeresult too big to return -> generator, one root-to-leaf path heldweighted graph, fastest path -> min-heap keyed by time so farName the container before you write the loop. Once you have said "a queue of (node, path) pairs" or "a min-heap keyed by latency", each follow-up turns into a small swap on code you already have, and the walk itself never gets rewritten.
Asked at Datadog
Five problems, ordered by how often each shape comes up. The path-string size walk leads, followed by the wide directory and the streaming delete order. The span tree and the service map close the set. Work each one in the editor before you open the walkthrough.
Directory Size by Path: Parse It, Then Walk It
Problem
du -sh /var/log would print. The path comes out of a config file, so expect it written loosely.A node is either a file,
{"type": "file", "size": } , or a directory, {"type": "dir", "children": {: , ...}} . file_system is the root directory.Write
total_size(file_system, path). Split path on / and resolve the names from the root. Empty pieces from a leading, trailing or doubled slash are ignored, so "/var/log/", "var/log" and "//var//log" name the same directory and "/" names the root itself. Names are case-sensitive.When the path resolves to a directory, return the total size of every file at any depth below it, with
0 for an empty directory. A path that resolves to a file returns that file's size. A missing name, or a step into a file partway along, means the path does not exist. Return -1.The missing leading slash and the trailing slash add no names, so the path resolves to var, and its two files sum to 1783894 + 924818 = 2708712.
wifi.log is a file, so nothing named old can sit beneath it and the path does not exist.
Two phases. Turn the path into a list of names by splitting on the slash and dropping the empty pieces, then walk down from the root one name at a time, looking each one up in the current directory's children. Sum every file under the node you land on with the same walk you would use for a plain directory size. A file is its own total.
Directory Size by Path: Parse It, Then Walk It
Click "Run" to execute your code
du -sh /var/log it has to turn that string into a node. The size walk is the directory problem Datadog asks most, and the path parameter is its most reported follow-up. One candidate ran out of screen time on the parse, which makes it the half to rehearse. Before writing code, say out loud how you treat a leading slash, a trailing slash, a path that names a file, and a path that does not exist. Pick the sentinel for that last case early. A return of 0 would look identical to an empty directory.Million-Wide Directory: Walk It Without Recursion
Problem
{"type": "file", "size": } and a directory is {"type": "dir", "children": {: , ...}} .Write
level_order_paths(node, path) without recursion. path is the path of node itself, and a child's path is its parent's path, a /, then the child's name.Return the paths at or below
node grouped by level, as a list of lists. The first list is [path], the second holds every path one level down, the third every path two levels down, and so on until the deepest level. Inside a level, keep paths grouped by parent in the order those parents appear one level up, and list a directory's children in ascending name order by plain string comparison, so syslog.10 comes before syslog.2 and README comes before aux.The three entries directly under /var/log form level one in name order, and the files inside app and nginx form level two.
A lone file has nothing below it, so the only level holds its own path.
Keep a first-in, first-out queue of (node, path) pairs, seeded with the starting node. At the top of each round, the queue holds exactly one level, so read its length, pop that many entries into a fresh list, and enqueue each directory's children in sorted name order as you go. When the round ends, the list you built is a finished level and the queue holds the next one.
Million-Wide Directory: Walk It Without Recursion
Click "Run" to execute your code
Streaming Walk: Yield Paths Instead of Collecting Them
Problem
The archive is regular, so it comes as a layout instead of a nested dictionary.
layout[d] lists the entries of every directory at depth d, where root sits at depth 0 and any directory deeper than the layout is empty. An entry ending in / is a subdirectory, named without the slash; any other entry is a file. A child's path is its parent's path, a /, then the child's name.Write
first_k_deletes(layout, root, k). Walk in delete order: inside a directory, take entries in listed order, emit a file's path when you reach it, finish a subdirectory completely before moving on, and emit the directory's own path only after everything inside it. Some layouts describe over a hundred million paths, so generate them lazily and stop as soon as you have k.Return the first
k paths of that stream, in order, or all of them if the tree has fewer.The subdirectory sub holds x.log, so x.log comes out before sub itself, and /d is last because it only empties once b.log is gone.
The walk goes three levels down to the only file, emits it, then emits the now-empty c, and k = 2 stops it there.
A post-order depth-first walk written as a generator. Each directory yields its files and the full stream of each subdirectory in listed order, then yields its own path. The caller pulls from the generator and stops after k paths, so the work tracks k plus the depth of the tree and never the size of the archive.
Streaming Walk: Yield Paths Instead of Collecting Them
Click "Run" to execute your code
yield, so the walk becomes an iterator that owns a stack of (path, depth, next entry) frames, or a Go callback that returns false to stop. Then comes "one directory has millions of files", where a single listing no longer fits in a frame; a paged listing lets each frame hold a cursor. Spilling the stack to disk stops mattering once the stack holds one root-to-leaf path.Max Root-to-Leaf Sum: Score Every Path Down the Tree
Problem
[span_id, parent_id, delta], where delta is the change in that span's self time, in milliseconds, against the previous deploy. A negative delta means the span got faster. The root span has a parent_id of -1. Records arrive in any order, and ids are arbitrary integers.Write
max_root_to_leaf(spans). A call chain runs from the root down to a leaf, meaning a span with no children. Its score is the sum of delta over every span on the chain, root and leaf included. There is at least one span and exactly one root.Return the highest score over all root-to-leaf chains.
The three chains score 3 + 2 + 5 = 10, 3 + 2 - 1 = 4 and 3 + 4 + 1 = 8, so the best is 10.
The root has one child and every chain must end at a leaf, so the only chain scores 4 - 3 = 1.
Rebuild the tree first. One pass over the records fills a map from each parent id to its child ids and spots the root by its parent of -1. Then walk down from the root carrying a running sum. At a leaf the running sum is a finished chain score, and the answer is the largest of those.
Max Root-to-Leaf Sum: Score Every Path Down the Tree
Click "Run" to execute your code
Service Graph: Fastest Path Between Two Hosts
Problem
Write
fastest_path(servers, times, start_ip, end_ip). servers is a list of IP address strings. times is a square matrix of the same size where times[i][j] is the time in milliseconds to send data from servers[i] directly to servers[j], or -1 when there is no direct link.Links are one-way, so
times[i][j] and times[j][i] can differ. The diagonal is 0 and every real link takes a positive time. Both start_ip and end_ip appear in servers, and the inputs guarantee that the fastest route, when one exists, is unique.Return the fastest route as the list of IP addresses it passes through, from
start_ip to end_ip with both ends included. Return [start_ip] when the two are the same server, and an empty list when end_ip cannot be reached.The direct link costs 10 ms and the detour through .2 alone costs 2 + 7 = 9, but going .1 to .2 to .3 to .4 costs 2 + 3 + 1 = 6.
Every link points toward .4 and none leaves it, so .1 cannot be reached in this direction.
Weighted edges mean breadth-first search no longer works, because the fewest hops can be the slowest route. Run Dijkstra: keep the best known time to every server and repeatedly settle the cheapest unsettled one, relaxing its outgoing links. Each time a link improves a server, record which server you came from. Walking those parent pointers back from the end and reversing gives the route.
Service Graph: Fastest Path Between Two Hosts
Click "Run" to execute your code
Where candidates lose it
Do
- Resolve the path before you walk. Split on slashes, drop the empty pieces, and return -1 for a missing name so it never looks like an empty directory.
- Say why ten levels needs no explicit stack. Depth is cheap on a shallow tree. The memory goes to the frontier and the result list, and the queue is the answer to that.
- Snapshot the queue length per level. Pop exactly that many pairs before the next level starts, and sort each directory's children by plain string comparison.
- Yield a directory after its contents. Post-order is what makes every streamed path safe to delete the moment it arrives.
- Record a parent on every relax. The route comes from walking those pointers back from the end server, then reversing the list.
Don't
- Don't return 0 for a path that does not exist. That answer collides with an empty directory, and the caller cannot tell the two apart.
- Don't collect the whole stream and slice off k. Stop pulling once you hold k paths, or the generator buys you nothing.
- Don't assume spans arrive parent first. Build the children map from every record, then find the root by its parent id of -1.
- Don't clamp a negative leaf to zero. A chain has to end at a leaf here, so a negative delta at the bottom still counts toward its score.
- Don't treat -1 as a weight. It marks a missing link, and every link in the service map runs one way.
Key Takeaways
- ✓The path-string size walk is the most reported follow-up to the directory-size problem, and most of its risk sits in the parse: loose slashes, a file target, and a sentinel for a missing path.
- ✓A shallow, million-wide tree wants a queue that drains one level at a time. When the output itself is too big, a post-order generator keeps memory to a single path.
- ✓Trace and service-map problems reuse the same traversal after a setup step: rebuild the span tree from parent ids, or keep parent pointers through Dijkstra so the route comes back with its time.
- ✓Choose the container for pending work before you code. Stack, queue, generator and heap are each a small swap once the walk is written.
Citing this page?How we research these guides
Datadog interview guideKnow what Datadog asks before you walk in.
24 coding · 4 system design · 30 behavioral questions
Get their questions first.
InterviewLegend is independent and not affiliated with or endorsed by any company it covers.