This is the DOM version of the classic "populate next right pointers" tree problem. Given a target element, find the one immediately to its right at the same depth — the next node on its level, reading left to right. The twist that makes it more than element.nextElementSibling: the neighbor might live under a different parent. The last child of one subtree's right neighbor is the first child of the next subtree at that depth.
Implement nextRightSibling(root, target). Return the element right after target on its level, or null if target is the rightmost at its depth (or is the root, or isn't found).
function nextRightSibling(root, target) {
// returns the element to the right of target at the same depth, or null
}
// <div id=r>
// <div id=p1><i id=a/><i id=b/></div>
// <div id=p2><i id=c/><i id=d/></div>
// depth-2 order: a, b, c, d
nextRightSibling(r, b); // c — crosses from p1's subtree into p2's
nextRightSibling(r, d); // null — rightmost at its depth
nextElementSibling only sees siblings; you need the next node across the whole level.target is the answer.target is the last node on its level, there's nothing to its right.You'll do a level-by-level BFS: process the tree one level at a time, and when you find target in a level, return the next element in that same level (or null if it's last).
"To the right at the same depth" is a level question, and BFS processes a tree exactly one level at a time. If you gather each level as an array and scan it for target, the element right after it in that array is the answer — because the array holds that whole level in left-to-right order, spanning every parent. If target is the last entry in its level's array, there's nothing to its right, so the answer is null. This is why nextElementSibling isn't enough: it stays within one parent, but the right neighbor can be the first child of the next subtree.
Read the tree in rows. Level 0 is [root], level 1 is all of root's children, level 2 is all the grandchildren (in order, across parents), and so on. To build the next row from the current one, concatenate every node's children left to right. Scan each row for target: if it's there at index i, the neighbor is the row's element at i + 1 — or null if i is the last index. Nodes from different parents sit next to each other in the row, which is exactly the cross-parent behavior we want.
The tempting shortcut is nextElementSibling:
function nextRightSiblingNaive(root, target) {
return target.nextElementSibling; // only within the SAME parent
}
It's right sometimes — when the neighbor happens to share a parent — but wrong at every subtree boundary. For b (the last child of p1), nextElementSibling is null, yet the true right neighbor at that depth is c (the first child of p2). The relationship is about level position, not sibling position, so you have to look across the whole level — which BFS gives you.
function nextRightSibling(root, target) {
if (!root || !target) return null;
let level = [root];
while (level.length > 0) {
// Is target on this level? If so, the next entry is its right neighbor.
const i = level.indexOf(target);
if (i !== -1) {
return i + 1 < level.length ? level[i + 1] : null;
}
// Build the next level: every node's children, left to right.
const next = [];
for (const node of level) {
for (const child of node.children) next.push(child);
}
level = next;
}
return null; // target not found
}
module.exports = { nextRightSibling };
level starts as [root]. Each iteration checks whether target is on the current level with indexOf: if it is at index i, the right neighbor is level[i + 1] (or null when i is the last index). Otherwise we build the next level by concatenating every node's children in order — that ordering is what places cousins from different parents adjacently — and repeat. If we exhaust the tree without finding target, it isn't in this subtree, so we return null. It visits each element once: O(n).
nextRightSibling(r, b) on p1 = [a, b], p2 = [c, d] under r:
[r] — indexOf(b) is -1. Build next level from r's children → [p1, p2].[p1, p2] — indexOf(b) is -1. Build next from p1's children [a, b] then p2's [c, d] → [a, b, c, d].[a, b, c, d] — indexOf(b) is 1. 1 + 1 < 4, so return level[2] = c.c is the first child of p2 — the right neighbor of b across the subtree boundary, which nextElementSibling would have missed. And nextRightSibling(r, d) would find d at the last index → null.
nextElementSibling — only sees same-parent siblings; wrong at every subtree edge. Scan the whole level.i + 1 === level.length means target is rightmost → return null, don't read past the array.null); an unfound target falls through to null.treeWalker/nextNode) gives the next element in document order, not the same-depth neighbor; knowing the difference matters.level[i - 1], and "leftmost/rightmost per level" is one more scan of each row.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
This is the DOM version of the classic "populate next right pointers" tree problem. Given a target element, find the one immediately to its right at the same depth — the next node on its level, reading left to right. The twist that makes it more than element.nextElementSibling: the neighbor might live under a different parent. The last child of one subtree's right neighbor is the first child of the next subtree at that depth.
Implement nextRightSibling(root, target). Return the element right after target on its level, or null if target is the rightmost at its depth (or is the root, or isn't found).
function nextRightSibling(root, target) {
// returns the element to the right of target at the same depth, or null
}
// <div id=r>
// <div id=p1><i id=a/><i id=b/></div>
// <div id=p2><i id=c/><i id=d/></div>
// depth-2 order: a, b, c, d
nextRightSibling(r, b); // c — crosses from p1's subtree into p2's
nextRightSibling(r, d); // null — rightmost at its depth
nextElementSibling only sees siblings; you need the next node across the whole level.target is the answer.target is the last node on its level, there's nothing to its right.You'll do a level-by-level BFS: process the tree one level at a time, and when you find target in a level, return the next element in that same level (or null if it's last).
"To the right at the same depth" is a level question, and BFS processes a tree exactly one level at a time. If you gather each level as an array and scan it for target, the element right after it in that array is the answer — because the array holds that whole level in left-to-right order, spanning every parent. If target is the last entry in its level's array, there's nothing to its right, so the answer is null. This is why nextElementSibling isn't enough: it stays within one parent, but the right neighbor can be the first child of the next subtree.
Read the tree in rows. Level 0 is [root], level 1 is all of root's children, level 2 is all the grandchildren (in order, across parents), and so on. To build the next row from the current one, concatenate every node's children left to right. Scan each row for target: if it's there at index i, the neighbor is the row's element at i + 1 — or null if i is the last index. Nodes from different parents sit next to each other in the row, which is exactly the cross-parent behavior we want.
The tempting shortcut is nextElementSibling:
function nextRightSiblingNaive(root, target) {
return target.nextElementSibling; // only within the SAME parent
}
It's right sometimes — when the neighbor happens to share a parent — but wrong at every subtree boundary. For b (the last child of p1), nextElementSibling is null, yet the true right neighbor at that depth is c (the first child of p2). The relationship is about level position, not sibling position, so you have to look across the whole level — which BFS gives you.
function nextRightSibling(root, target) {
if (!root || !target) return null;
let level = [root];
while (level.length > 0) {
// Is target on this level? If so, the next entry is its right neighbor.
const i = level.indexOf(target);
if (i !== -1) {
return i + 1 < level.length ? level[i + 1] : null;
}
// Build the next level: every node's children, left to right.
const next = [];
for (const node of level) {
for (const child of node.children) next.push(child);
}
level = next;
}
return null; // target not found
}
module.exports = { nextRightSibling };
level starts as [root]. Each iteration checks whether target is on the current level with indexOf: if it is at index i, the right neighbor is level[i + 1] (or null when i is the last index). Otherwise we build the next level by concatenating every node's children in order — that ordering is what places cousins from different parents adjacently — and repeat. If we exhaust the tree without finding target, it isn't in this subtree, so we return null. It visits each element once: O(n).
nextRightSibling(r, b) on p1 = [a, b], p2 = [c, d] under r:
[r] — indexOf(b) is -1. Build next level from r's children → [p1, p2].[p1, p2] — indexOf(b) is -1. Build next from p1's children [a, b] then p2's [c, d] → [a, b, c, d].[a, b, c, d] — indexOf(b) is 1. 1 + 1 < 4, so return level[2] = c.c is the first child of p2 — the right neighbor of b across the subtree boundary, which nextElementSibling would have missed. And nextRightSibling(r, d) would find d at the last index → null.
nextElementSibling — only sees same-parent siblings; wrong at every subtree edge. Scan the whole level.i + 1 === level.length means target is rightmost → return null, don't read past the array.null); an unfound target falls through to null.treeWalker/nextNode) gives the next element in document order, not the same-depth neighbor; knowing the difference matters.level[i - 1], and "leftmost/rightmost per level" is one more scan of each row.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.