Binary Tree in Action (Traversal)
Note
Now all the plugins has supported English. I'm still improving the website...
This article will resolve
LeetCode | Difficulty |
---|---|
114. Flatten Binary Tree to Linked List | 🟠 |
116. Populating Next Right Pointers in Each Node | 🟠 |
226. Invert Binary Tree | 🟢 |
This article follows up on 二叉树心法(纲领篇). Let’s first recap the main points from the previous summary on solving binary tree problems:
Note
There are two main approaches to solving binary tree problems:
1. Can you get the answer by traversing the binary tree once? If so, use a traverse
function along with external variables. This is called the "traversal" approach.
2. Can you define a recursive function that derives the answer to the main problem from the answers to subproblems (subtrees)? If so, write out the recursive function definition and make full use of its return value. This is called the "divide and conquer" approach.
Regardless of which approach you use, you should consider:
What does a single binary tree node need to do? When (preorder/inorder/postorder) does it need to do it? You don't need to worry about other nodes; the recursive function will handle applying the same operation to all nodes.
This article will take a few simple problems as examples to help you practice using these guidelines, and to understand the differences and connections between the "traversal" and "divide and conquer" approaches.
First Problem: Invert a Binary Tree
Let's start with a simple problem, LeetCode problem 226 "Invert Binary Tree". Given the root node root
of a binary tree, your task is to invert the entire tree, making it a mirror image. For example, if the input binary tree is:
4
/ \
2 7
/ \ / \
1 3 6 9
The algorithm should invert the tree in place, so that the tree rooted at root
becomes:
4
/ \
7 2
/ \ / \
9 6 3 1
It's not hard to see that by swapping the left and right child nodes of every node in the binary tree, the final result will be the completely inverted binary tree.
Now, let's go through the general approach for solving binary tree problems in our minds:
1. Can this problem be solved using a "traversal" mindset?
Yes, I can write a traverse
function to visit each node and swap its left and right child nodes.
What does a single node need to do? It needs to swap its left and right child nodes.
When should this be done? It seems that it can be done in pre-order, in-order, or post-order positions.
Based on the above, we can write the following solution code:
class Solution {
// main function
public TreeNode invertTree(TreeNode root) {
// traverse the binary tree, swap the child nodes of each node
traverse(root);
return root;
}
// binary tree traversal function
void traverse(TreeNode root) {
if (root == null) {
return;
}
// *** pre-order position ***
// what each node needs to do is to swap its left and right children
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
// traversal framework, go traverse the nodes of the left and right subtrees
traverse(root.left);
traverse(root.right);
}
}
class Solution {
public:
// main function
TreeNode* invertTree(TreeNode* root) {
// traverse the binary tree, swap the child nodes of each node
if(root != NULL) traverse(root);
return root;
}
// binary tree traversal function
void traverse(TreeNode* root) {
if(root != NULL) {
// *** pre-order position ***
// what each node needs to do is to swap its left and right child nodes
TreeNode* tmp = root->left;
root->left = root->right;
root->right = tmp;
// traversal framework, go and traverse the nodes of the left and right subtrees
traverse(root->left);
traverse(root->right);
}
}
};
class Solution:
# main function
def invertTree(self, root):
# traverse the binary tree, swap the children of each node
self.traverse(root)
return root
# binary tree traversal function
def traverse(self, root):
if not root:
return
# *** pre-order position ***
# what each node needs to do is swap its left and right children
tmp = root.left
root.left = root.right
root.right = tmp
# traversal framework, go traverse the nodes of the left and right subtrees
self.traverse(root.left)
self.traverse(root.right)
// main function
func invertTree(root *TreeNode) *TreeNode {
// traverse the binary tree, swap the child nodes of each node
traverse(root)
return root
}
// binary tree traversal function
func traverse(root *TreeNode) {
if root == nil {
return
}
// *** preorder position ***
// what each node needs to do is swap its left and right children
tmp := root.Left
root.Left = root.Right
root.Right = tmp
// traversal framework, go and traverse the nodes of the left and right subtrees
traverse(root.Left)
traverse(root.Right)
}
// main function
var invertTree = function(root) {
traverse(root);
return root;
};
// binary tree traversal function
function traverse(root) {
if (root == null) {
return;
}
// *** preorder position ***
// what each node needs to do is swap its left and right children
let tmp = root.left;
root.left = root.right;
root.right = tmp;
// traversal framework, go and traverse the nodes of the left and right subtrees
traverse(root.left);
traverse(root.right);
}
You can move the code from the pre-order position to the post-order position, but directly moving it to the in-order position won't work; it needs a slight modification. This should be quite obvious, so I won't elaborate on it.
Technically, we've solved this problem, but for comparison, let's continue thinking.
2. Can this problem be solved using the "divide and conquer" approach?
Let's try to define the invertTree
function:
// definition: invert the binary tree with root, and
// return the root node of the inverted binary tree
TreeNode invertTree(TreeNode root);
// definition: invert the binary tree with root,
// return the root node of the inverted binary tree
TreeNode* invertTree(TreeNode* root);
# Definition: Invert the binary tree rooted at 'root', return the root node of the inverted tree
def invertTree(root: TreeNode)
// Definition: Invert the binary tree rooted at
// 'root', return the root node of the inverted tree
func invertTree(root *TreeNode) *TreeNode
// Definition: Invert the binary tree rooted at
// 'root', return the root node of the inverted tree
var invertTree = function(root) {}
Then think about it, when you execute invertTree(x)
for a certain binary tree node x
, what can you do using the definition of this recursive function?
I can use invertTree(x.left)
to first invert the left subtree of x
, then use invertTree(x.right)
to invert the right subtree of x
. Finally, I swap the left and right subtrees of x
. This exactly completes the inversion of the entire binary tree rooted at x
, thus fulfilling the definition of invertTree(x)
.
Here is the solution code directly:
class Solution {
// Definition: Invert the binary tree rooted at
// 'root', return the root node of the inverted
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
// Utilize the function definition to invert left and right subtrees first
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
// Then swap the left and right child nodes
root.left = right;
root.right = left;
// Consistent with the definition logic: the
// binary tree rooted at 'root' has been inverted,
return root;
}
}
class Solution {
public:
// Definition: Invert the binary tree rooted at
// 'root', return the root node of the inverted
TreeNode* invertTree(TreeNode* root) {
if (root == nullptr) {
return nullptr;
}
// Utilize the function definition to invert the left and right subtrees first
TreeNode* left = invertTree(root->left);
TreeNode* right = invertTree(root->right);
// Then swap the left and right child nodes
root->left = right;
root->right = left;
// Consistent with the definition logic: the
// binary tree rooted at 'root' has been inverted,
return root;
}
};
class Solution:
# Definition: Invert the binary tree rooted at
# 'root', return the root node of the inverted
def invertTree(self, root):
if root is None:
return None
# Utilize the function definition to invert left and right subtrees first
left = self.invertTree(root.left)
right = self.invertTree(root.right)
# Then swap the left and right child nodes
root.left, root.right = right, left
# Consistent with the definition logic: the
# tree rooted at 'root' has been inverted,
return root
// Definition: Invert the binary tree rooted at
// 'root', return the root node of the inverted tree
func invertTree(root *TreeNode) *TreeNode {
if root == nil {
return nil
}
// Use the function definition to invert the left and right subtrees first
left := invertTree(root.Left)
right := invertTree(root.Right)
// Then swap the left and right child nodes
root.Left = right
root.Right = left
// Consistent with the definition logic: the binary
// tree rooted at 'root' has been inverted, return
return root
}
// Definition: Invert the binary tree rooted at
// 'root', and return the root node of the inverted
var invertTree = function(root) {
if (root == null) {
return null;
}
// Utilize the function definition to invert left and right subtrees first
var left = invertTree(root.left);
var right = invertTree(root.right);
// Then swap the left and right child nodes
root.left = right;
root.right = left;
// Consistent with the definition logic: the binary
// tree rooted at 'root' has been inverted, return
return root;
}
The core of this "break down the problem" approach is to give the recursive function a suitable definition and then use that definition to explain your code. If your logic is self-consistent, then your algorithm is correct.
Alright, that's the analysis for this problem. Both the "traversal" and "break down the problem" approaches can solve it. Let's move on to the next problem.
Second Problem: Populating Next Right Pointers in Each Node
This is LeetCode problem #116, "Populating Next Right Pointers in Each Node." Let's look at the problem statement:
116. Populating Next Right Pointers in Each Node | LeetCode |
You are given a perfect binary tree where all leaves are on the same level, and every parent has two children. The binary tree has the following definition:
struct Node { int val; Node *left; Node *right; Node *next; }
Populate each next pointer to point to its next right node. If there is no next right node, the next pointer should be set to NULL
.
Initially, all next pointers are set to NULL
.
Example 1:
Input: root = [1,2,3,4,5,6,7] Output: [1,#,2,3,#,4,5,6,7,#] Explanation: Given the above perfect binary tree (Figure A), your function should populate each next pointer to point to its next right node, just like in Figure B. The serialized output is in level order as connected by the next pointers, with '#' signifying the end of each level.
Example 2:
Input: root = [] Output: []
Constraints:
- The number of nodes in the tree is in the range
[0, 212 - 1]
. -1000 <= Node.val <= 1000
Follow-up:
- You may only use constant extra space.
- The recursive approach is fine. You may assume implicit stack space does not count as extra space for this problem.
// Function signature
Node connect(Node root);
// Function signature
Node* connect(Node* root);
# Function signature
def connect(root: 'Node')
// Function signature
func connect(root *Node) *Node
// Function signature
var connect = function(root) {}
The problem is to connect all nodes at each level of a binary tree using the next
pointer:
Moreover, the problem states that the input is a "perfect binary tree," meaning the entire binary tree forms an equilateral triangle. Except for the rightmost nodes, whose next
pointers will point to null
, all other nodes have adjacent nodes to their right.
How do we solve this problem? Let's mentally go through the binary tree problem-solving approach:
1. Can this problem be solved with a "traversal" mindset?
Certainly, it can.
Each node's task is straightforward: simply set its next
pointer to point to the node on its right.
You might be tempted to mimic the previous problem and write code like this:
// Binary tree traversal function
void traverse(Node root) {
if (root == null || root.left == null) {
return;
}
// Point the next pointer of the left child to the right child
root.left.next = root.right;
traverse(root.left);
traverse(root.right);
}
// Binary tree traversal function
void traverse(TreeNode* root) {
if (root == NULL || root->left == NULL) {
return;
}
// Point the next pointer of the left child to the right child
root->left->next = root->right;
traverse(root->left);
traverse(root->right);
}
# Binary tree traversal function
def traverse(root):
if root is None or root.left is None:
return
# Point the next pointer of the left child to the right child
root.left.next = root.right
traverse(root.left)
traverse(root.right)
// Binary tree traversal function
func traverse(root *Node) {
if root == nil || root.Left == nil {
return
}
// Point the Next pointer of the left child to the right child
root.Left.Next = root.Right
traverse(root.Left)
traverse(root.Right)
}
// Binary tree traversal function
var traverse = function(root) {
if (root == null || root.left == null) {
return;
}
// Point the next pointer of the left child to the right child
root.left.next = root.right;
traverse(root.left);
traverse(root.right);
}
However, this code has significant issues because it can only connect two nodes with the same parent node. Take a look at this diagram:
Node 5 and Node 6 do not share the same parent node, so according to this code's logic, they cannot be linked, which does not meet the intended requirements. So, where does the problem lie?
The traditional traverse
function is designed to visit all nodes of a binary tree, but what we actually want to traverse are the "gaps" between two adjacent nodes.
Therefore, we can abstract the binary tree by considering each box in the diagram as a node:
In this way, a binary tree is abstracted into a ternary tree, where each node on the ternary tree represents two adjacent nodes of the original binary tree.
Now, we just need to implement a traverse
function to traverse this ternary tree. The task for each "ternary tree node" is to connect its two internal binary tree nodes.
class Solution {
// Main function
public Node connect(Node root) {
if (root == null) return null;
// Traverse the "ternary tree" and connect adjacent nodes
traverse(root.left, root.right);
return root;
}
// Ternary tree traversal framework
void traverse(Node node1, Node node2) {
if (node1 == null || node2 == null) {
return;
}
// *** Pre-order position ***
// Connect the two input nodes
node1.next = node2;
// Connect two child nodes with the same parent
traverse(node1.left, node1.right);
traverse(node2.left, node2.right);
// Connect two child nodes across parent nodes
traverse(node1.right, node2.left);
}
}
class Solution {
public:
// Main function
Node* connect(Node* root) {
if (root == nullptr) return nullptr;
// Traverse the "ternary tree" and connect adjacent nodes
traverse(root->left, root->right);
return root;
}
// Ternary tree traversal framework
void traverse(Node* node1, Node* node2) {
if (node1 == nullptr || node2 == nullptr) {
return;
}
// *** Pre-order position ***
// Connect the two input nodes
node1->next = node2;
// Connect two child nodes with the same parent
traverse(node1->left, node1->right);
traverse(node2->left, node2->right);
// Connect two child nodes across different parents
traverse(node1->right, node2->left);
}
};
class Solution:
# Main function
def connect(self, root: 'Node') -> 'Node':
if not root:
return None
# Traverse the "ternary tree" and connect adjacent nodes
self.traverse(root.left, root.right)
return root
# Ternary tree traversal framework
def traverse(self, node1: 'Node', node2: 'Node') -> None:
if not node1 or not node2:
return
# Pre-order position
# Connect the two input nodes
node1.next = node2
# Connect two child nodes with the same parent
self.traverse(node1.left, node1.right)
self.traverse(node2.left, node2.right)
# Connect two child nodes across different parents
self.traverse(node1.right, node2.left)
// Main function
func connect(root *Node) *Node {
if root == nil {
return nil
}
// Traverse the "ternary tree" and connect adjacent nodes
traverse(root.Left, root.Right)
return root
}
// Ternary tree traversal framework
func traverse(node1 *Node, node2 *Node) {
if node1 == nil || node2 == nil {
return
}
// *** Pre-order position ***
// Connect the two input nodes
node1.Next = node2
// Connect two child nodes with the same parent
traverse(node1.Left, node1.Right)
traverse(node2.Left, node2.Right)
// Connect two child nodes across different parents
traverse(node1.Right, node2.Left)
}
var connect = function(root) {
// main function
if (root == null) return null;
// traverse the "ternary tree" and connect adjacent nodes
traverse(root.left, root.right);
return root;
// ternary tree traversal framework
function traverse(node1, node2) {
if (node1 == null || node2 == null) {
return;
}
// *** pre-order position ***
// connect the two input nodes
node1.next = node2;
// connect two child nodes of the same parent
traverse(node1.left, node1.right);
traverse(node2.left, node2.right);
// connect two child nodes across different parents
traverse(node1.right, node2.left);
}
};
This way, the traverse
function traverses the entire "ternary tree," connecting all adjacent binary tree nodes, thus avoiding the issues we encountered earlier and perfectly solving this problem.
2. Can this problem be solved using the "break down the problem" thinking pattern?
Well, it seems there isn't a particularly good approach, so this problem cannot be solved using the "break down the problem" thinking pattern.
Third Problem: Flatten Binary Tree to Linked List
This is LeetCode problem #114 "Flatten Binary Tree to Linked List." Let's look at the problem statement:
114. Flatten Binary Tree to Linked List | LeetCode |
Given the root
of a binary tree, flatten the tree into a "linked list":
- The "linked list" should use the same
TreeNode
class where theright
child pointer points to the next node in the list and theleft
child pointer is alwaysnull
. - The "linked list" should be in the same order as a pre-order traversal of the binary tree.
Example 1:
Input: root = [1,2,5,3,4,null,6] Output: [1,null,2,null,3,null,4,null,5,null,6]
Example 2:
Input: root = [] Output: []
Example 3:
Input: root = [0] Output: [0]
Constraints:
- The number of nodes in the tree is in the range
[0, 2000]
. -100 <= Node.val <= 100
O(1)
extra space)?// The function signature is as follows
void flatten(TreeNode root);
for (int i = n - 2; i >= 0; i--)
r_max[i] = Math.max(height[i], r_max[i + 1]);
for (int i = 1; i < n - 1; i++) {
// hello world
res += Math.min(l_max[i], r_max[i]) - height[i];
}
// The function signature is as follows
void flatten(TreeNode* root);
# The function signature is as follows
def flatten(root: TreeNode)
// The function signature is as follows
func flatten(root *TreeNode)
// The function signature is as follows
var flatten = function(root) {}
1. Can this problem be solved with a "traversal" mindset?
At first glance, it seems possible: perform a pre-order traversal of the entire tree, constructing a "linked list" as you go:
// Virtual head node, dummy.right is the result
TreeNode dummy = new TreeNode(-1);
// Pointer used to build the linked list
TreeNode p = dummy;
void traverse(TreeNode root) {
if (root == null) {
return;
}
// Pre-order position
p.right = new TreeNode(root.val);
p = p.right;
traverse(root.left);
traverse(root.right);
}
// Virtual head node, dummy.right is the result
TreeNode* dummy = new TreeNode(-1);
// Pointer used to build the linked list
TreeNode* p = dummy;
void traverse(TreeNode* root) {
if (root == nullptr) {
return;
}
// Pre-order position
p->right = new TreeNode(root->val);
p = p->right;
traverse(root->left);
traverse(root->right);
}
# Virtual head node, dummy.right is the result
dummy = TreeNode(-1)
# Pointer used to build the linked list
p = dummy
def traverse(root: TreeNode):
if root is None:
return
# Pre-order position
p.right = TreeNode(root.val)
p = p.right
traverse(root.left)
traverse(root.right)
var p *TreeNode
// Virtual head node, dummy.right is the result
var dummy = newTreeNode(-1)
p = dummy
func traverse(root *TreeNode) {
if root == nil {
return
}
// Pre-order position
p.Right = &TreeNode{Val: root.Val}
p = p.Right
traverse(root.Left)
traverse(root.Right)
}
// Virtual head node, dummy.right is the result
var dummy = new TreeNode(-1);
// Pointer used to build the linked list
var p = dummy;
var traverse = function(root) {
if (root == null) {
return;
}
// Pre-order position
p.right = new TreeNode(root.val);
p = p.right;
traverse(root.left);
traverse(root.right);
}
However, pay attention to the signature of the flatten
function, which returns void
. This means the problem expects us to flatten the binary tree into a linked list in place.
With this requirement, we cannot solve the problem through simple binary tree traversal.
2. Can this problem be solved using the "divide and conquer" thinking pattern?
Let's try to define the flatten
function:
// Definition: Input node root, then the binary tree
// rooted at root will be flattened into a linked list
void flatten(TreeNode root);
// Definition: Input node root, then the binary tree
// rooted at root will be flattened into a linked list
void flatten(TreeNode* root);
# Definition: Input node root, then the binary tree with
# root as the root will be flattened into a linked list
def flatten(self, root: TreeNode) -> None:
// Definition: Input node root, then the binary tree with
// root as the root will be flattened into a linked list
func flatten(root *TreeNode) {
}
// Definition: Input node root, then the binary tree with
// root as the root will be flattened into a linked list
var flatten = function(root) {
};
With this function definition, how can we transform a tree into a linked list as required by the problem?
For a node x
, you can follow these steps:
Use
flatten(x.left)
andflatten(x.right)
to flatten the left and right subtrees ofx
.Attach the right subtree of
x
below the left subtree, and then make the entire left subtree the right subtree.
In this way, the entire binary tree rooted at x
is flattened, effectively fulfilling the definition of flatten(x)
.
Let's look directly at the code implementation:
class Solution {
// Definition: flatten the tree with root as the root node into a linked list
public void flatten(TreeNode root) {
// base case
if (root == null) return;
// Utilize the definition to flatten the left and right subtrees
flatten(root.left);
flatten(root.right);
// *** Post-order traversal position ***
// 1. Left and right subtrees have been flattened into linked lists
TreeNode left = root.left;
TreeNode right = root.right;
// 2. Make the left subtree the new right subtree
root.left = null;
root.right = left;
// 3. Attach the original right subtree to the end of the current right subtree
TreeNode p = root;
while (p.right != null) {
p = p.right;
}
p.right = right;
}
}
class Solution {
public:
// Definition: flatten the tree rooted at 'root' into a linked list
void flatten(TreeNode* root) {
// base case
if (root == nullptr) return;
// Use the definition to flatten the left and right subtrees
flatten(root->left);
flatten(root->right);
// *** Post-order traversal position ***
// 1. Left and right subtrees have been flattened into linked lists
TreeNode* left = root->left;
TreeNode* right = root->right;
// 2. Make the left subtree the new right subtree
root->left = nullptr;
root->right = left;
// 3. Attach the original right subtree to the end of the current right subtree
TreeNode* p = root;
while (p->right != nullptr) {
p = p->right;
}
p->right = right;
}
};
class Solution:
# Definition: flatten the tree rooted at 'root' into a linked list
def flatten(self, root) -> None:
# base case
if root is None:
return
# Use the definition to flatten left and right subtrees
self.flatten(root.left)
self.flatten(root.right)
# Post-order traversal position
# 1. Left and right subtrees have been flattened into linked lists
left = root.left
right = root.right
# 2. Make the left subtree the new right subtree
root.left = None
root.right = left
# 3. Attach the original right subtree to the end of the current right subtree
p = root
while p.right is not None:
p = p.right
p.right = right
// Definition: flatten the tree with root as the root into a linked list
func flatten(root *TreeNode) {
// base case
if root == nil {
return
}
// Using the definition, flatten the left and right subtrees
flatten(root.Left)
flatten(root.Right)
// *** Post-order traversal position ***
// 1. Left and right subtrees have been flattened into a linked list
left := root.Left
right := root.Right
// 2. Make the left subtree the right subtree
root.Left = nil
root.Right = left
// 3. Attach the original right subtree to the end of the current right subtree
p := root
for p.Right != nil {
p = p.Right
}
p.Right = right
}
var flatten = function(root) {
// Definition: flatten the tree with root as the root into a linked list
// base case
if (root == null) return;
// Utilize the definition to flatten the left and right subtrees
flatten(root.left);
flatten(root.right);
// *** Post-order traversal position ***
// 1. Left and right subtrees have been flattened into a linked list
var left = root.left;
var right = root.right;
// 2. Make the left subtree the right subtree
root.left = null;
root.right = left;
// 3. Attach the original right subtree to the end of the current right subtree
var p = root;
while (p.right != null) {
p = p.right;
}
p.right = right;
};
See, this is the charm of recursion. You might wonder how the flatten
function flattens the left and right subtrees.
It's not easy to explain, but as long as you understand the definition of flatten
and use it, allowing each node to perform its task, the flatten
function will work according to its definition.
With this, the problem is solved. The recursive approach from our previous discussion on Reverse Nodes in k-Group is somewhat similar to this problem.
Finally, let's revisit the fundamental solution framework for binary tree problems.
There are two main thinking patterns for solving binary tree problems:
1. Can you solve the problem by traversing the binary tree once? If so, use a traverse
function along with external variables. This is known as the "traversal" thinking pattern.
2. Can you define a recursive function that derives the solution to the main problem from the solutions to subproblems (subtrees)? If so, write the definition of this recursive function and make full use of its return value. This is known as the "problem decomposition" thinking pattern.
No matter which thinking pattern you use, consider this:
If you isolate a single binary tree node, what task does it need to perform? At which order position (pre-order/in-order/post-order) should it be performed? You don't need to worry about the other nodes; the recursive function will execute the same operation on all nodes.
I hope you reflect on this approach and apply it to all binary tree problems.
This concludes the article. For more classic binary tree exercises and training in recursive thinking, please refer to the Recursion Practice section in the binary tree chapter.