[Daily Coding Problem 223] O(1) space in order traversal of a binary tree
Typically, an implementation of in-order traversal of a binary tree has O(h)
space complexity, where h
is the height of the tree. Write a program to compute the in-order traversal of a binary tree using O(1)
space.
In-order traversal without recursion with O(h) space, where h is the tree's height.
public List<Integer> inOrderTraversal(BinaryTreeNode root) { List<Integer> vals = new ArrayList<>(); if(root == null) { return vals; } Stack<BinaryTreeNode> stack = new Stack<>(); BinaryTreeNode curr = root; while(stack.size() > 0 || curr != null) { while(curr != null) { stack.push(curr); curr = curr.getLeft(); } curr = stack.pop(); vals.add(curr.getVal()); curr = curr.getRight(); } return vals; }
O(1) space solution
The reason that the stack solution requires O(h) space consumption is because we must have some way to traverse back to a current node after traversing its left subtree. To avoid this space consumption, we need to restructure the tree while we traverse it, so that going to the right will always go to the "correct" next node. Namely, right after visiting the rightmost node in a left subtree, we continue to visit this rightmost node's right child that will lead us back to the this left subtree's parent.
Take the binary tree below as an example. Starting from root 8, it has a left subtree. Before traversing its left subtree, we need to get the rightmost descendant of this subtree and set its right child to be the root node 8. Otherwise, we wouldn't have any way of coming back to root. So we set 7's right child to be 8. After this step, traverse the left subtree. For node 3, we do the same, setting 1's right child to be 3. Repeat this until a node has no left subtree. At this point, we know we need to add its value to the result list. In an in-order traversal, we visit a node's right child after visiting the node. So we go back to 3. Now here comes a problem. Since we are constrained with constant space, we do not know that 3's left subtree has been traversed. How do we check that this is the case? We apply the same logic here: trying to the rightmost descendant of 3's left child(1) points to 3. After finding out that 1's right child already points to 3, we know that we have already traversed the left subtree with a root node 1. At this point, we need to revert the changes made to node 1 by setting its right child back to null. Then add 3 to the result list and traverse 3's right subtree.
Algorithm:
1. if the current node has no left subtree, visit it;
2. if it has left subtree, make the rightmost descendant node's right child in its left subtree points to itself;
2(a). If this is already done before, revert the changes, visit the current node, then traverse its right subtree;
2(b). If this has not been done, do the upate, then traverse the current node's left subtree;
3. Repeat steps 1 and 2 until the current node becomes null.
This approach uses O(1) space at the cost of slower runtime as we need to traverse each left subtrees of every node twice.
public List<Integer> inOrderTraversalConstantSpace(BinaryTreeNode root) { List<Integer> vals = new ArrayList<>(); BinaryTreeNode curr = root; while(curr != null) { //add val if there is no left node to go to if(curr.getLeft() == null) { vals.add(curr.getVal()); curr = curr.getRight(); } //make the rightmost descendant of curr's left child points to curr else { BinaryTreeNode rightMostDesc = curr.getLeft(); while(rightMostDesc.getRight() != null && rightMostDesc.getRight() != curr) { rightMostDesc = rightMostDesc.getRight(); } if(rightMostDesc.getRight() == null) { rightMostDesc.setRight(curr); curr = curr.getLeft(); } else { rightMostDesc.setRight(null); vals.add(curr.getVal()); curr = curr.getRight(); } } } return vals; }
This problem basically implements a single threaded binary tree. For more references on this, refer to Threaded Binary Tree: https://www.geeksforgeeks.org/threaded-binary-tree/