DSA sheet · Trees · Binary search tree pattern

Binary Tree to Doubly Linked List

This is the last problem of the pattern. We take a binary tree and, without creating any new nodes, rewire it into a doubly linked list in inorder order. Each node's left pointer becomes "previous" and its right pointer becomes "next". The teacher first solves it the easy way: store the inorder nodes in a list, then link neighbours. Then she removes the list using an idea that keeps coming back in tree problems: decide what to do with each node at the moment you visit it, using a prev pointer.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Doubly linked list (DLL)

A chain where every box has two pointers: prev (the box before it) and next (the box after it). The first box's prev is None and the last box's next is None. The first box is called the head. We return the head, because from it you can reach every other box.

Same node, new meaning for its pointers

tree pointermeaning in the DLL
node.leftprev
node.rightnext
node class (GFG calls it Node and the value is .data; here we use .val)
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left     # will become "prev"
        self.right = right   # will become "next"

Inorder traversal (recap)

Inorder = left subtree, then the node, then the right subtree. The DLL must list the nodes in exactly this order.

Part A · Brute force: store the inorder, then link

GFG "Binary Tree to DLL"

1The question in simple words

Given the root of a binary tree, turn it in place into a doubly linked list:

Example 1
    1
   / \
  2   3
its DLL
None ← 2 ⇄ 1 ⇄ 3 → None

Inorder is 2, 1, 3. So 2's prev is None and its next is 1. 1's prev is 2 and its next is 3. 3's next is None.

Example 2
        10
       /  \
     20    30
    /  \
  40    60
its DLL
None ← 40 ⇄ 20 ⇄ 60 ⇄ 10 ⇄ 30 → None
       head

2What the constraints tell us

3Intuition: get the order first, then tie neighbours

The question literally says "inorder". So the first idea: do an inorder traversal, store the nodes in a list, then connect each node to its neighbours in the list.

nodes4020601030indexes 0 … 4

4Building the conditions

For the node at index i:

The edges need care:

Example: in the tree, 10's left child was 20 and its right child was 30. In the DLL, 10's left must become 60 (the node before it in inorder) and its right stays 30.

Doubt 1: we never touch the first node's left or the last node's right. Could old tree links still be there?
→ No. The first inorder node is the leftmost node, so it has no left child (otherwise that child would come first). The last inorder node is the rightmost node, so it has no right child. Both are already None.
Doubt 2: is it safe to overwrite left and right while looping?
→ Yes. The whole order is already saved in the list, so we no longer need the old tree links to find anything.
Doubt 3: what do we return?
→ The head, which is nodes[0]. If the list were empty, the head would be None. The constraints promise at least 1 node, so nodes[0] is safe. We keep a guard anyway.

5Approach steps

  1. Inorder traversal, appending each node to nodes.
  2. For each i: if i > 0, left = nodes[i-1]. If i < n−1, right = nodes[i+1].
  3. Return nodes[0].

6Code (Python)

BT to DLL, brute force with a list
class Solution:
    def bToDLL(self, root):
        nodes = []
        self.inorder(root, nodes)
        if not nodes:                        # not needed (n >= 1), just safe
            return None
        n = len(nodes)
        for i in range(n):
            if i > 0:
                nodes[i].left = nodes[i - 1]     # prev
            if i < n - 1:
                nodes[i].right = nodes[i + 1]    # next
        return nodes[0]                      # head

    def inorder(self, node, nodes):
        if node is None:
            return
        self.inorder(node.left, nodes)       # left
        nodes.append(node)                   # node
        self.inorder(node.right, nodes)      # right

7Code line by line

linewhat it means
self.inorder(root, nodes)Collect the real node objects in left, node, right order.
for i in range(n):Visit every position in the list.
if i > 0: nodes[i].left = nodes[i - 1]Every node except the first gets a prev. The tree's left pointer now means "previous".
if i < n - 1: nodes[i].right = nodes[i + 1]Every node except the last gets a next.
return nodes[0]The first inorder node is the head.

8Dry run

  1. i = 0 (40): no prev. 40.right = 20.
  2. i = 1 (20): 20.left = 40, 20.right = 60 (it was already 60 in the tree).
  3. i = 2 (60): 60.left = 20, 60.right = 10.
  4. i = 3 (10): 10.left = 60 (it used to be 20), 10.right = 30.
  5. i = 4 (30): 30.left = 10. It's the last node, so no next.
  6. Return 40. Walking right: 40 → 20 → 60 → 10 → 30 ✓. Walking left from 30: 30 → 10 → 60 → 20 → 40 ✓

9Complexity & remember

It works, but the interviewer won't like the extra O(n) list. Next we get rid of it.

Remember brute forceInorder → list of nodes → left = nodes[i-1] (i > 0), right = nodes[i+1] (i < n−1) → return nodes[0].

Part B · Optimal: link while doing the inorder (prev + head)

1The question (same as Part A)

Same input and output. New goal: no extra list.

2What the constraints tell us

3Intuition: remember the last node you visited

If we can't store all the nodes, we must decide at the moment we visit a node. In an inorder traversal, the node visited just before the current one is exactly its previous node in the DLL. So we only need to remember one node: the last one we visited. Call it prev.

When we visit root:

4Building the logic step by step

Where does the work go? In the middle of inorder

Inorder code is: go left, visit, go right. When we stored values, the "visit" line was nodes.append(...). Now we put the linking logic there instead. At that moment we hold the current node (root), and prev holds the one before it.

Finding the head

From 10 we go left to 20, then left to 40, then left to None, which returns. We're back at 40. This is the first node that reaches the "visit" line. At that moment prev is still None (nothing visited yet). So: prev is None ⇒ head = root. Every later visit will find prev not None, so the head is set only once.

Updating prev, always

After 40 is visited, we go up to 20. If we forgot to update prev, 20 would also see "prev is None" and wrongly become the head. So after the if/else, always set prev = root, and do it before going right.

Doubt 1: when we are at 20, why is prev 40 and not 20?
→ root already points at the current node (20). prev means "the one before", which is the node visited last: 40. Then 40.right = 20 and 20.left = 40, and that pair is connected.

Why prev must be shared by all calls (the most important point)

After 60 is visited, prev = 60. Then 60's call ends, 20's call ends, and we're back at 10. Many students expect prev to "go back" to 20 here. It must not. The node just before 10 in inorder is 60. Because prev is one variable shared by all calls (a global or class variable, self.prev in Python), it still holds 60 when we reach 10, which is exactly right. So 60.right = 10 and 10.left = 60.

Doubt 2: what if I pass prev as a function argument instead?
→ Then each call gets its own copy. Returning from 20 to 10 would bring back 10's old copy, not the 60 that was set deep inside. The links would be wrong. Keep it on self (or use nonlocal).
Doubt 3: we set prev.right = root. Could that destroy a right subtree we still need?
→ No. When we visit a node, its old right subtree is either empty, or we have already started walking into it: the call inorder(prev.right) was made before prev's right pointer was overwritten. And changing root.left is safe because root's left subtree is already finished.

What do we return?

The recursion itself returns nothing useful. It just links nodes. The answer is the head we saved at the first visit, so the main function returns self.head.

5Approach steps

  1. prev = None, head = None (shared by all calls).
  2. inorder(root): if root is None → return.
  3. inorder(root.left).
  4. If prev is None → head = root. Else → prev.right = root, root.left = prev.
  5. prev = root.
  6. inorder(root.right).
  7. Return head.

6Code (Python)

BT to DLL, optimal (no extra list)
class Solution:
    def bToDLL(self, root):
        self.prev = None        # last node visited in inorder
        self.head = None        # first node visited in inorder
        self.inorder(root)
        return self.head

    def inorder(self, root):
        if root is None:
            return
        self.inorder(root.left)             # 1. finish the left side
        if self.prev is None:               # 2. first node ever -> head
            self.head = root
        else:                               #    otherwise link prev ⇄ root
            self.prev.right = root
            root.left = self.prev
        self.prev = root                    # 3. root is now the last visited
        self.inorder(root.right)            # 4. then the right side

7Code line by line

linewhat it means
self.prev = None self.head = NoneShared state. Reset on every call to bToDLL, so the same object can be used twice.
if root is None: returnBase case: nothing to visit.
self.inorder(root.left)Everything smaller in inorder order gets linked first.
if self.prev is None: self.head = rootNothing has been visited before, so this is the leftmost node, the head.
self.prev.right = rootThe previous node's "next" is the current node.
root.left = self.prevThe current node's "prev" is the previous node.
self.prev = rootAlways runs (outside the if/else). It moves the "last visited" marker forward.
self.inorder(root.right)Then link the right side. Its first node will see this root as prev.
return self.headThe main function returns the head that was saved, not the root.
Her slip in the editorHer first run failed because she had declared prev and head with the wrong type (TreeNode, while GFG's class is called Node). In Python there are no declared types, but use the field names your platform gives you (GFG uses .data).

8Dry run using the call stack

Tree: 10 (20 (40, 60), 30). io(x) = inorder(x).

  1. io(10) → io(20) → io(40) → io(None) returns.
  2. Visit 40: prev is None → head = 40. prev = 40. io(40.right = None) returns. io(40) ends.
  3. Visit 20: prev = 40 → 40.right = 20, 20.left = 40. prev = 20. Go right: io(60).
  4. io(60) → left is None. Visit 60: prev = 20 → 20.right = 60, 60.left = 20. prev = 60. Right is None. io(60) ends, then io(20) ends.
  5. Visit 10: prev is still 60 (shared) → 60.right = 10, 10.left = 60. prev = 10. Go right: io(30).
  6. io(30) → left None. Visit 30: prev = 10 → 10.right = 30, 30.left = 10. prev = 30. Right None. Everything returns.
  7. Return head = 40. DLL: 40 ⇄ 20 ⇄ 60 ⇄ 10 ⇄ 30 ✓
stack at step 2
io(10)io(20)io(40) · head=40
stack at step 4
io(10)io(20)io(60) · prev=60
stack at step 5
io(10) · prev still 60
visitprev beforeactionprev after
40Nonehead = 4040
204040 ⇄ 2020
602020 ⇄ 6060
106060 ⇄ 1010
301010 ⇄ 3030

9Complexity & remember

The teacher notes that both versions are acceptable, but this one is a little faster (one pass) and lighter on memory, because we rewire the same tree while walking it.

Doubt (Python detail): 10⁶ nodes in a straight line means 10⁶ nested calls.
→ Python's default recursion limit (about 1000) would stop that with RecursionError. For huge skewed inputs in Python, raise the limit with sys.setrecursionlimit, or do the same inorder with an explicit stack. The prev/head logic stays the same.
Remember the optimal versionInorder. At the visit: prev None → head = root, else prev.right = root, root.left = prev. Then prev = root, always. prev is shared. Return head.

Part C · Revision page

Brute force (list)Optimal (prev + head)
orderinorderinorder
links madeafter the traversal, by indexduring the traversal, with prev
headnodes[0]first node that sees prev = None
passes21
timeO(n)O(n)
spaceO(n) list + O(h) stackO(h) stack only
tree pointerDLL meaningfirst nodelast node
leftprevNone (it is the leftmost node)its inorder predecessor
rightnextits inorder successorNone (it is the rightmost node)
If you remember only 5 lines 1. left = prev, right = next, and the order is inorder.
2. Brute force: store the nodes, then link i−1 and i+1, return nodes[0].
3. Optimal: keep a shared prev. The node visited just before is the DLL's previous node.
4. prev None → head. Else prev.right = root, root.left = prev.
5. prev = root always, before going right. Return head, not root.
Mistakes to avoid ✗ forgetting prev = root (prev stays None, so every node overwrites head)
✗ putting prev = root inside the else (the first visit never sets prev, so the same thing happens)
✗ passing prev as a normal argument (it "jumps back" when calls return)
✗ returning root instead of head
✗ in the brute force, setting a prev for i = 0 or a next for the last index (index out of range)
test it yourself (paste under either solution)
root = TreeNode(10, TreeNode(20, TreeNode(40), TreeNode(60)), TreeNode(30))
head = Solution().bToDLL(root)

fwd, node, last = [], head, None
while node:
    fwd.append(node.val)
    last, node = node, node.right
back, node = [], last
while node:
    back.append(node.val)
    node = node.left
print(fwd)    # [40, 20, 60, 10, 30]
print(back)   # [30, 10, 60, 20, 40]

Based on this video: Convert Binary Tree to Doubly Linked List