Coding Interview Patterns Module 12 – Tree patterns: breadth-first
Side views and zigzag level order
Return the left and right side views of a binary tree, its zigzag level order and the level with the largest sum, all from one level-order pass.
What you will learn
- Find the left and right side views of a binary tree from the first and last node of each level
- Generate zigzag level order by writing every other level backwards, straight into place
- Find the level with the largest sum, keeping the smallest level on a tie and handling sums below zero
- Compare the level-order pass with a depth-first search that visits the right child first
Before you start
What you’ll learn · Before you start
What you will learn
- Find the left and right side views of a binary tree from the first and last node of each level
- Generate zigzag level order by writing every other level backwards, straight into place
- Find the level with the largest sum, keeping the smallest level on a tie and handling sums below zero
- Compare the level-order pass with a depth-first search that visits the right child first
Before you start
On this page
The side views of a binary tree keep one node per level: the right side view the last node of each level, as a camera far to the right would record it, and the left side view the first. Zigzag level order reads the first level from left to right, the next from right to left, and so on. Both come from the level-order template, which hands over each level complete and in left-to-right order, and the same pass can find the level with the largest sum. Below are an original problem with all three tasks, the obvious method priced at 100,000 nodes, and the one-pass idea.
The problem: a drone show filmed from both sides
The problem
A drone show flies a formation stored as a binary tree. The lead drone is row 0; a drone’s left follower flies one row lower and to its left, its right follower one row lower and to its right. Each drone reports its battery reserve, a whole number from −1,000 to 1,000 that is below zero when it holds less than planned. Write three functions:
side_views(root): cameras far to the left and far to the right record, on each row, only the drone nearest to them. Return both lists of reserves, from row 0 down.zigzag(root): the commentator names the drones row by row, turning around at the end of each row: row 0 from left to right, row 1 from right to left, and so on. Return the rows in that order.best_level(root): the row whose reserves add up to the most, the smallest such row on a tie, or −1 for an empty formation.
A formation has up to 100,000 drones.
In the diagram’s formation, [4, -2, 7, 3, -8, None, 5, 6] in level order, the left camera records 4, −2, 3 and 6
and the right camera 4, 7, 5 and 6: no drone on the right reaches row 3. The zigzag rows are 4; 7, −2; 3, −8, 5; and
6, and row 3 is best, with 6. In [-3, -1, -2] both rows total −3, so row 0 wins.
The example formation, one row at a time
Text description of the diagram
The formation has seven drones, drawn from the top down with arrows from each drone to its followers. The lead drone, 4, is alone on row 0. Its left follower is −2 and its right follower is 7, on row 1. On row 2, −2 leads 3 and −8, and 7 has only a right follower, 5. On row 3, 3 has a left follower, 6, which is the only drone on that row.
The text at the left of each row gives the direction in which the zigzag order reads the row, and the row's total:
- Row 0 is read left to right; its total is 4.
- Row 1 is read right to left, so it comes out as 7, then −2; its total is 5.
- Row 2 is read left to right: 3, −8, 5; its total is 0.
- Row 3 is read right to left; its only drone is 6, so its total is 6.
A double border marks the drone that a camera far to the right records on each row: 4, 7, 5 and 6, the right view. A dashed border marks the drone that a camera far to the left records: 4, −2, 3 and 6, the left view. The right view ends with 6 although 6 is a follower on the left side, because no drone on the right reaches row 3. The largest total is 6, on row 3, so the best row is 3.
The brute force: rescan the formation for every row
Without a queue, the direct method finds each row on its own: for row r, walk the formation from the lead drone, no deeper than row r, and collect that row’s drones from left to right. Each walk visits every drone on rows 0 to r. That is cheap on a balanced formation, but a line of drones has a row for every drone.
views/brute_force.py
"""Row by row, rescanning: for each row r, walk the whole formation again, no deeper than r, to collect that row.
All three answers come from the rows: the first and last value of each row are the side views, every other row read
backwards gives the zigzag order, and the row totals give the best row. The counter shows what that costs.
"""
from tree import build, formation, line
visits = 0
def row(root, r):
"""The values of row r, from left to right, found by a depth-first walk that stops below row r."""
global visits
values = []
stack = [(root, 0)] if root is not None else []
while stack:
node, depth = stack.pop()
visits += 1
if depth == r:
values.append(node.val)
continue
# Push the right child first, so that the left one is taken first: rows come out from left to right.
if node.right is not None:
stack.append((node.right, depth + 1))
if node.left is not None:
stack.append((node.left, depth + 1))
return values
def rows_by_rescanning(root):
rows = []
while True:
values = row(root, len(rows))
if not values: # an empty row: the formation has no more rows
return rows
rows.append(values)
def answers(root):
rows = rows_by_rescanning(root)
left_view = [r[0] for r in rows]
right_view = [r[-1] for r in rows]
zigzag = [r if d % 2 == 0 else r[::-1] for d, r in enumerate(rows)]
totals = [sum(r) for r in rows]
best = totals.index(max(totals)) if totals else -1 # index() finds the first, so the smallest row wins ties
return left_view, right_view, zigzag, best
print(answers(build([4, -2, 7, 3, -8, None, 5, 6])))
print(answers(build([-3, -1, -2])))
print(answers(None))
print()
for n in (500, 1_000, 2_000):
visits = 0
rows_by_rescanning(line(n))
print(f"a line of {n:,} drones: {visits:,} visits; n(n + 1)/2 + n = {n * (n + 1) // 2 + n:,}")
visits = 0
rows_by_rescanning(formation(4_095))
print(f"a complete formation of 4,095 drones in 12 rows: {visits:,} visits")
n = 100_000
print(f"a line of {n:,} drones would need {n * (n + 1) // 2 + n:,} visits") Output
([4, -2, 3, 6], [4, 7, 5, 6], [[4], [7, -2], [3, -8, 5], [6]], 3) ([-3, -1], [-3, -2], [[-3], [-2, -1]], 0) ([], [], [], -1) a line of 500 drones: 125,750 visits; n(n + 1)/2 + n = 125,750 a line of 1,000 drones: 501,500 visits; n(n + 1)/2 + n = 501,500 a line of 2,000 drones: 2,003,000 visits; n(n + 1)/2 + n = 2,003,000 a complete formation of 4,095 drones in 12 rows: 12,273 visits a line of 100,000 drones would need 5,000,150,000 visits
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py
views/tree.py
"""A binary tree node, a builder for a tree written as a list in level order, and two large test formations."""
from collections import deque
class Node:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build(values):
"""The tree whose level-order list is `values`, with None where a child is missing."""
if not values or values[0] is None:
return None
root = Node(values[0])
queue = deque([root])
i = 1
while queue and i < len(values):
node = queue.popleft()
if values[i] is not None:
node.left = Node(values[i])
queue.append(node.left)
i += 1
if i < len(values) and values[i] is not None:
node.right = Node(values[i])
queue.append(node.right)
i += 1
return root
def score(i):
"""The reserve of drone i of a test formation: a fixed scramble of -1,000 to 1,000, the same in every language."""
return i * 7919 % 2001 - 1000
def formation(n):
"""A complete formation of n drones, filled row by row from the left; drone i is the i-th in level order."""
nodes = [Node(score(i)) for i in range(n)]
for i, node in enumerate(nodes):
if 2 * i + 1 < n:
node.left = nodes[2 * i + 1]
if 2 * i + 2 < n:
node.right = nodes[2 * i + 2]
return nodes[0] if nodes else None
def line(n):
"""n drones in one line, each the right follower of the one before."""
root = None
for i in range(n - 1, -1, -1):
root = Node(score(i), None, root)
return root
def release(root):
"""Takes a tree apart one link at a time.
Freeing a tree frees each node from inside the step that frees its parent. CPython copes with a line of 100,000
nodes, but Pyodide, the Python behind the Run button, stopped when a line of 3,000 nodes was freed at once, so the
examples take their long test trees apart with this loop when they are done with them.
"""
stack = [root] if root is not None else []
while stack:
node = stack.pop()
stack.extend(child for child in (node.left, node.right) if child is not None)
node.left = node.right = None Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The counts match n(n + 1)/2 + n, the last term being the walk that finds no row below the deepest. A line of 100,000 drones needs about 5 × 10⁹ visits, some 8 minutes of CPython at the 10⁷ simple steps a second of the lesson on constraints; balanced test trees never show it.
What to notice
Every answer is about one row as a whole: its first drone, its last drone, its order or its total. The level-order template delivers each row once, complete and from left to right, in O(n) for the whole tree, so one pass can answer all three.
One pass of the level-order template
Run the template of the module’s first lesson: when a row starts, read the queue’s length, take that many drones and add their followers at the back. Inside that loop:
- Side views: the drone taken first is the left view’s, and the one taken last, at position size − 1, the right view’s.
- Zigzag level order: on odd rows the drone taken at position i goes to position size − 1 − i, so the row comes out backwards with no second pass. Reversing the finished row, a deque filled from its left end and two stacks give the same order.
- Best row: add up the row and keep the best total with its row, starting from the first row’s total, never 0, and replacing it only when a row is strictly larger, so a tie keeps the earlier row.
A depth-first search that visits the right child first also finds the right view: it keeps the first node met at each new depth. On the example formation, the pass goes like this:
| Row | Queue as the row starts | Left | Right | Zigzag row | Total | Best so far |
|---|---|---|---|---|---|---|
| 0 | 4 | 4 | 4 | 4 | 4 | row 0 |
| 1 | −2, 7 | −2 | 7 | 7, −2 | 5 | row 1 |
| 2 | 3, −8, 5 | 3 | 5 | 3, −8, 5 | 0 | row 1 |
| 3 | 6 | 6 | 6 | 6 | 6 | row 3 |
Practise: the best row
The exercise asks for best_level(root) in one pass. Its sample tests include rows that all add up to less than
zero, a tie and a line of 1,500 drones.
Exercise · Easy · Python
The row of a formation with the largest total
Write best_level(root): the number of the row whose values add up to the most, counting the root's row as 0. If two or more rows share the largest total, return the smallest of their numbers. An empty formation (root is None) has no rows: return -1. Each node has val, left and right, and a missing child is None.
# 4 on row 0; -2 and 7 on row 1; 3, -8 and 5 on row 2; 6 on row 3 (the example formation of the lesson)
best_level(root) # 3: the row totals are 4, 5, 0 and 6
Use one level-order pass with collections.deque. The sample tests include rows whose totals are all below zero, two rows that tie, and a line of 1,500 drones, deeper than a recursive solution can go by default.
Starter code · best.py
from collections import deque
def best_level(root):
"""The row with the largest total, the smallest row on ties; -1 for an empty formation."""
# Replace this line with your code.
return -1 The sample tests · test_best.py
from collections import deque
from best import best_level
class Node:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build(values):
"""A tree from its values in level order, with None where a child is missing."""
if not values or values[0] is None:
return None
root = Node(values[0])
queue = deque([root])
i = 1
while queue and i < len(values):
node = queue.popleft()
if values[i] is not None:
node.left = Node(values[i])
queue.append(node.left)
i += 1
if i < len(values) and values[i] is not None:
node.right = Node(values[i])
queue.append(node.right)
i += 1
return root
def test_example_formation():
"""the row totals are 4, 5, 0 and 6"""
assert best_level(build([4, -2, 7, 3, -8, None, 5, 6])) == 3
def test_empty_formation():
"""no rows at all"""
assert best_level(None) == -1
def test_single_drone_below_zero():
"""one row, whose total is below zero, is still the best row"""
assert best_level(Node(-7)) == 0
def test_every_total_below_zero():
"""the totals are -5, -3 and -4, so row 1 is the best even though no total reaches 0"""
assert best_level(build([-5, -1, -2, -1, -1, -1, -1])) == 1
def test_tie_keeps_the_earlier_row():
"""rows 0 and 1 both add up to -3"""
assert best_level(build([-3, -1, -2])) == 0
def test_tie_after_a_better_row():
"""totals 1, 9, 2 and 9: row 1 wins, and row 3 only ties with it"""
assert best_level(build([1, 4, 5, 1, None, None, 1, 9])) == 1
def test_rows_kept_apart():
"""a large value on row 2 belongs to row 2, not to row 1"""
assert best_level(build([1, 2, 3, 40, -50, 6, 7])) == 1
def test_zero_totals():
"""totals 0, 0 and 1"""
assert best_level(build([0, 5, -5, None, 1])) == 2
def test_wide_formation():
"""a complete formation of 4,095 drones whose values are their positions in level order: the last row wins"""
nodes = [Node(i) for i in range(4095)]
for i, node in enumerate(nodes):
if 2 * i + 1 < len(nodes):
node.left = nodes[2 * i + 1]
if 2 * i + 2 < len(nodes):
node.right = nodes[2 * i + 2]
assert best_level(nodes[0]) == 11
def test_deep_line():
"""1,500 drones in one line, one per row: deeper than a recursive solution can go by default"""
root = node = Node(-1)
for i in range(1, 1500):
child = Node(5 if i == 1200 else -1)
if i % 2:
node.right = child
else:
node.left = child
node = child
assert best_level(root) == 1200 A hint
Keep two numbers while the rows go by: the best total so far and the row it belongs to. What should the best total be before the first row, so that a formation whose rows all add up to less than zero still gets an answer, and which comparison lets the earlier row keep a tie?
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
The one pass in five languages, with its invariant Pro: opens with a Pro, Premium or Ultimate pass
The three functions share one loop, and what changes between languages is how each spells the queue and the end of a row. Here they are in five languages with their recorded runs, the invariant that makes them right, and a stress test that catches four plausible slips.
In this part:
- The code in Python, JavaScript, Java, C++ and Go
- The invariant, and why the last drone taken is the rightmost
- Four slips, caught by a stress test
3 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Zigzag order five ways, and depth-first views, measured Pro: opens with a Pro, Premium or Ultimate pass
There are several correct ways to write zigzag order and one tempting way that is quadratic, and the right view has a depth-first form with its own memory profile. This part times five zigzag methods on 262,143 drones and measures what the depth-first view keeps on its stack.
In this part:
- Five ways to write zigzag order, timed on 262,143 drones
- The right view by depth-first search, and Python’s recursion limit
- Queue, recursion or a stack of your own: a trade-off table
2 runnable examples (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Edge cases, variants and follow-up questions Pro: opens with a Pro, Premium or Ultimate pass
Ties, totals below zero and lopsided formations are where an online judge’s unseen tests catch these functions. Here are the edge cases to test, four variants solved in a recorded run, the follow-up questions that interviews add, and a practice set with sample answers.
In this part:
- Edge cases to test before you submit
- Variants, with answers
- Follow-up questions, answered
- A practice set with sample answers
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher · Warm-up
Why is the right side view of a binary tree the last node of each level, rather than the nodes reached by following right children from the root?
A camera on the right sees, on every level, the node with nothing to its right, and that node sits in the left subtree whenever the right subtree is shorter. If the root's right child is a leaf while its left child has children, the deeper levels hold no node on the right at all, so the path of right children stops early and misses them. Level order produces each level complete and from left to right, so the last node taken on a level is the rightmost one on it, whichever subtree it belongs to. By the same argument the first node of each level is the left side view. A depth-first search gets the same answer when it visits the right child first and records the first node it meets at each new depth.
-
Mid to senior
A service receives trees only as level-order lists with null for missing children, such as [4, -2, 7, 3, -8, null, 5, 6], and must return the right side view of millions of them. Can you answer without building node objects? What does it cost in time and memory?
-
Mid to senior
Drones join a formation one at a time, each as a new leaf under a drone already flying, and after every join the right-hand camera's view must be reported. Can you do better than a full level-order pass after each join? What do you store, and what does one join cost?
Key takeaways
- The side views are the first and the last node of each level, not the path of right children.
- Zigzag level order writes every other row backwards, straight into place, and the best row starts from the first row’s total and changes only on a strictly larger one.
- Rescanning the tree for every row costs n(n + 1)/2 + n visits on a line: about 5 × 10⁹ at 100,000 drones.
References
- collections: deque objects (Python Software Foundation)
- Time complexity of operations on built-in types (Python Software Foundation)
- sys: getrecursionlimit and setrecursionlimit (Python Software Foundation)
- Class ArrayDeque (Java SE 25 API) (Oracle)
- JEP 330: Launch Single-File Source-Code Programs (OpenJDK)
- Working Draft, Programming Languages: C++ (N4950), 24.6.6 Class template queue (ISO/IEC JTC1/SC22/WG21)
- AddressSanitizer (Clang documentation) (LLVM Project)
- The Go Programming Language Specification: numeric types (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress