Leetcode 572: subtree of another tree run time and space analysis - algorithm

I have question on whether my analysis of runtime and space for algorithm that determine whether a tree S contains a subtree that is exactly the same tree as other tree T My code is as follows:
class Solution {
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if(root == null) return subRoot == null;
return isSameTree(root, subRoot) ||
isSubtree(root.left, subRoot) ||
isSubtree(root.right, subRoot);
}
public boolean isSameTree(TreeNode root, TreeNode subRoot) {
if(root == null) return subRoot == null;
if(subRoot == null) return false;
return root.val == subRoot.val &&
isSameTree(root.left, subRoot.left) && isSameTree(root.right, subRoot.right);
}
}
I think the time is O(S * min(S, T)) where S and T are total number of node of tree s and t respectively, and space is O(max(S, T)). I got the time because we are performing dfs on every node of tree s to determine whether it is the same as tree t, and each dfs takes minimum of node of s and t, and space is because our recursion call stack can at most contain minimum of two tree's node, if smaller tree hits null case while dfsing with larger tree, we will just return

You don't need to check on subRoot being null as it is invalid that null is subtree of null. So, in that case you can return false. Please use this code for your reference and time
complexity O(mn), where m = |nodes| ∈ root and n = |nodes| ∈ subRoot
class Solution {
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if(root == null)
return false;
if (isSameTree(root, subRoot))
return true;
return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}
public boolean isSameTree(TreeNode root, TreeNode subRoot) {
if(root==null || subRoot==null)
return root==subRoot;
return root.val == subRoot.val && isSameTree(root.left, subRoot.left) && isSameTree(root.right, subRoot.right);
}
}

Related

Space complexity of isSubtree function for 2 binary trees

public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if (root == null) return false;
return areSame(root, subRoot) || isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}
public boolean areSame(TreeNode root, TreeNode subRoot) {
if (root == null && subRoot == null) return true;
if (root == null || subRoot == null) return false;
if (subRoot.val != root.val)
return false;
return areSame(root.left, subRoot.left) && areSame(root.right, subRoot.right);
}
Is the space compelxity of my above solution to find if a tree is subtree of another binary tree - O(height(tree1)) (as suggested in most of the discussion comments) or O(height(tree1)+ height(tree2)) where
I think it should be O(height(tree1) + height(tree2)) because isSubtree can go as deep as one branch of tree1, and for each call, the isSame() could go as deep as height(tree2), so the maximum stack memory being used at any instant would be ht1+ht2.
Assuming that the boolean operators && and || are short-circuiting operators (as they are in Java), the recursion depth (and extra stack memory) is upper bounded by O(height(tree1)).
Since isSubtree(root, subRoot) can only call itself (with the first tree's height reduced by 1) or areSame(root, subRoot), and at most one call of 'areSame' directly from 'isSubtree' can be on the stack at a time (because of short-circuiting), the recursion depth is O(height(tree1)) + max-depth-of(areSame(tree1, tree2)).
Now, areSame(root, subRoot) makes no recursive calls if root is null. If root is not null, it may call:
areSame(root.left, subRoot.left) && areSame(root.right, subRoot.right);
Here, it calls areSame only with child nodes of root: the height of the first tree has been reduced by 1, and the first call must complete before the second call starts (since && short-circuits). So there can be at most height(tree1) + 1 calls to areSame on the call-stack at any time, so the total recursion depth/ stack space of isSubtree is O(height(tree1))

Balanced tree with constant-time successor and predecessor given node pointers?

I was asked this question, which I personally find hard:
Create a data structure that can:
Insert elements,
Remove elements,
Search Elements,
In time O(log n)
In addition,
It should have the following two functions which work in time O(1):
next(x):
given a pointer to the node of value x, return a pointer to the node with the smallest bigger value than x.
previous(x)
given a pointer to the node of value x, return a pointer to the node with the biggest smallest value than x.
If each node contains a pointer to its successor and a pointer to its predecessor, or equivalently - if you maintain both a doublely linked list and a tree, where each node in the tree points to its equivalent node in the list and vice versa - you'll get want you want. Updating the list on insert/delete is O(1) (after locating the closest node in the tree). Searching is performed on the tree. Succesor / predecessor are performed on the list.
#RogerLindsjö's idea from the comments is a good one. Essentially, keep a regular, balanced BST, then thread a doubly-linked list through the nodes keeping them in sorted order. That way, given a pointer to a node in the tree, you can find the largest value smaller than it or the smallest value greater than it simply by following the next or previous pointers in each node.
You can maintain this list through insertions and deletions without changing the overall runtime of an insert or delete. For example, here's how you might do an insertion of an element x:
Do a standard BST successor query to find the smallest value larger than x in the tree, and a standard BST predecessor query to find the largest value smaller than x in the tree. Each search takes time O(log n) to complete.
Do a regular BST insertion to insert x. Then, set its next and previous pointers to the two elements you found in the previous step, and update those nodes to point to your new node x. This also takes time O(log n).
The total time for the insertion is then O(log n), matching what a balanced tree can provide.
I'll leave it to you to figure out deletion, which can similarly maintain the linked list pointers without changing the overall cost of the operation.
Like most self-balancing trees, a B+ tree provides Insert, Remove, and Search operations with O(log n) time complexity.
In a B+ tree, a leaf node hosts multiple keys in an array, so the concept of "pointer to node with value x" does not really exist, but we could define it as the tuple (pointer, index), where the pointer is to the node, and index is the slot in which x is stored.
In a B+ tree the nodes at the bottom level contain all the keys, and these nodes are often linked, usually only in forward direction (i.e. to the right), but it is quite possible to also maintain a link in the opposite direction, without increasing the time complexity of the above operations.
With those two remarks in mind, prev-next operations can clearly be executed in O(1) time.
If your elements are integers you can use y-fast trie that supports all mentioned operations in O(log log m). Also, almost any search tree will allow doing these operations in O(log n) time by just going first up and then down (it will require a lot of concentration to not mess up with the order, though)
You can use two pointers in the node of the balanced tree, namely pred - predecessor and succ - successor. While inserting a node into the tree or deleting a node from the tree you just have to do some pointer manipulations, equivalent to those in doubly linked list.
The time complexity will be O(1) in each case.
I have provided my implementation for the insertion and deletion in case of AVL Tree below. The complete implementation is available here.
Structure of node
template<typename T>
struct node {
T key;
int freq;
node<T> *left;
node<T> *right;
node<T> *pred;
node<T> *succ;
int height;
node(T key): key(key), freq(1),
left(nullptr),
right(nullptr),
height(1),
pred(nullptr),
succ(nullptr) {}
};
insert function
node<T> *insert(node<T> *root, T key) {
if(root == nullptr)
return new node<T>(key);
if(!comp(key, root->key) && !comp(root->key, key)) {
++root->freq;
} else if(comp(key, root->key)) {
if(root->left == nullptr) {
node<T> *new_node = new node<T>(key);
/* set the pred and succ ptrs*/
new_node->succ = root;
new_node->pred = root->pred;
if(root->pred != nullptr)
root->pred->succ = new_node;
root->pred = new_node;
root->left = new_node;
} else {
root->left = insert(root->left, key);
}
} else {
if(root->right == nullptr) {
node<T> *new_node = new node<T>(key);
/* set the pred and succ ptrs*/
new_node->pred = root;
new_node->succ = root->succ;
if(root->succ != nullptr)
root->succ->pred = new_node;
root->succ = new_node;
root->right = new_node;
} else {
root->right = insert(root->right, key);
}
}
root->height = max(height(root->left), height(root->right)) + 1;
int bf = balance_factor(root);
node<T> *left = root->left;
node<T> *right = root->right;
if(bf > 1 && left != nullptr && comp(key, left->key)) {
/*
node was inserted at left subtree of left child
fix - right rotate root
*/
root = rotate_right(root);
} else if(bf > 1 && left != nullptr && comp(left->key, key)) {
/*
node was inserted at right subtree of left child
fix - left rotate left child
- right rotate root
*/
root->left = rotate_left(root->left);
root = rotate_right(root);
} else if(bf < -1 && right != nullptr && comp(right->key, key)) {
/*
node was inserted at right subtree of right child
fix - left rotate root
*/
root = rotate_left(root);
} else if(bf < -1 && right != nullptr && comp(key, right->key)) {
/*
node was inserted at left subtree of right child
fix - right rotate right child
- left roatate root
*/
root->right = rotate_right(root->right);
root = rotate_left(root);
}
return root;
}
erase function
node<T> *erase(node<T> *root, T key) {
if(root == nullptr)
return nullptr;
if(comp(key, root->key)) {
root->left = erase(root->left, key);
} else if(comp(root->key, key)) {
root->right = erase(root->right, key);
} else {
if(root->left == nullptr || root->right == nullptr) {
/* update pred and succ ptrs */
if(root->succ != nullptr)
root->succ->pred = root->pred;
if(root->pred != nullptr)
root->pred->succ = root->succ;
if(root->right == nullptr) {
node<T> *temp = root->left;
delete root;
root = temp;
} else {
node<T> *temp = root->right;
delete root;
root = temp;
}
} else {
// node<T> *succ = minVal(root->right);
root->key = root->succ->key;
root->freq = root->succ->freq;
root->right = erase(root->right, root->succ->key);
}
}
if(root != nullptr) {
root->height = max(height(root->left), height(root->right)) + 1;
int bf_root = balance_factor(root);
if(bf_root > 1) {
/*
case R
*/
int bf_left = balance_factor(root->left);
if(bf_left >= 0) {
/*
case R0 and R1
*/
root = rotate_right(root);
} else {
/*
case R -1
*/
root->left = rotate_left(root->left);
root = rotate_right(root);
}
} else if(bf_root < -1) {
/*
Case L
*/
int bf_right = balance_factor(root->right);
if(bf_right <= 0) {
/*
case L0 and L -1
*/
root = rotate_left(root);
} else {
/*
case L1
*/
root->right = rotate_right(root->right);
root = rotate_left(root);
}
}
}
return root;
}

What algorithm is this code using to find the lowest common ancestor of a binary tree?

I found this solution on Leetcode while trying to solve this problem:
https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/
Everyone on Leetcode seems to take for granted how this works. But I don't get it. What is going on here and what algorithm is this using to find the LCA of the binary tree?
public TreeNode lowestCommonAncestorBinaryTree(TreeNode root, TreeNode p, TreeNode q) {
if(root==null) {
return null;
}
if(root==p) {
return p;
}
if(root==q) {
return q;
}
TreeNode left = lowestCommonAncestorBinaryTree(root.left, p, q);
TreeNode right = lowestCommonAncestorBinaryTree(root.right, p, q);
if (left != null && right != null) {
return root;
}
if(left!=null && right==null) {
return left;
}
if(right!=null && left==null) {
return right;
}
return null;
}
Quite simple:
The code looks at the root of the tree. If the root is p or q, then it returns it.
If it's not in the root, it searches in the left and right subtrees of the root, repeating the process until root is actually p or q.
Then comes the 3 last ifs.
if (left != null && right != null) return root;
This means that it found one of the nodes in the left subtree of the root and another in the right subtree of the root, hence root is the LCA.
if(left != null && right == null) return left;
This means that it found a node in the left subtree but no node in the right subtree, then the left node is the parent of the other node, hence the LCA.
if(right != null && left == null) return right;
This means that it found a node in the right subtree but no node in the left subtree, then the right node is the parent of the other node, hence the LCA.
Otherwise, the nodes aren't in the tree and there's no LCA.

number of leaves in a binary tree

I am a beginner to binary trees and have been working my way through the algorithms book. I have learnt about the various traversal methods of BSTs (pre-order, post order etc).
Could someone please explain how one can traverse a BST to count the number of nodes that are leaves (no children) please?
Many thanks!
Use a recursive method:
For a leaf return 1.
For a non-leaf, return the sum of that method applied to its children.
Example in PHP:
class BST {
public $left; // The substree containing the smaller entries
public $right; // The substree containing the larger entries
public $data; // The value that is stored in the node
}
function countLeafs(BST $b) {
// Test whether children exist ...
if ($b->left || $b->right) {
// ... yes, the left or the right child exists. It's not a leaf.
// Return the sum of calling countLeafs() on all children.
return ($b->left ? countLeafs($b->left) : 0)
+ ($b->right ? countLeafs($b->right) : 0);
} else {
// ... no, it's a leaf
return 1;
}
}
The different traversal methods would lead to different algorithms (although for a simple problem like this, all DFS variants are more or less the same).
I assume that you have a BST which consists of objects of type Node. A node has two fields left and right of type Node, which are the children of the node. If a child is not present, the value of that field is null. The whole tree is referenced by a reference to the root, called root. In java:
class Node {
public Node left;
public Node right;
}
Node root;
A DFS is easiest to implement by recursion: define a method
int numberOfLeafs(Node node)
which returns the number of leafs in the subtree rooted by node. Of course, numberOfLeafs(root) should yield the number of leafs of the whole tree.
As said, it is really artificial to distinguish pre-, in-, and post-order traversal here, but I'm gonna do it anyway:
Pre-order DFS: First deal with the current node, then with the children
int numberOfLeafs(Node node) {
int result = 0;
if (node.left == null && node.right == null)
result += 1;
if (node.left != null)
result += numberOfLeafs(node.left)
if (node.right != null)
result += numberOfLeafs(node.right)
return result;
}
In-order DFS: First deal with the left child, then with the current node, then with the right child
int numberOfLeafs(Node node) {
int result = 0;
if (node.left != null)
result += numberOfLeafs(node.left)
if (node.left == null && node.right == null)
result += 1;
if (node.right != null)
result += numberOfLeafs(node.right)
return result;
}
Post-order DFS: First deal with the children, then with the current node
int numberOfLeafs(Node node) {
int result = 0;
if (node.left != null)
result += numberOfLeafs(node.left)
if (node.right != null)
result += numberOfLeafs(node.right)
if (node.left == null && node.right == null)
result += 1;
return result;
}
For a BFS, you typically use a simple loop with a queue in which you add unvisited vertices. I now assume that I have a class Queue to which I can add nodes at the end and take nodes from the front:
Queue queue = new Queue();
queue.add(root);
int numberOfLeafs = 0;
while (!queue.empty) {
// take an unhandled node from the queue
Node node = queue.take();
if (node.left == null && node.right == null)
numberOfLeafs += 1;
if (node.left != null)
queue.add(node.left);
if (node.right != null)
queue.add(node.right);
}
try this
int countLeafNodes(BTNode node) {
if (node == null)
return 0;
if (node.getLeftChild() == null && node.getRightChild() == null
&& node.getParent() != null)//this is a leaf, no left or right child
return 1;
else
return countLeafNodes(node.getLeftChild())
+ countLeafNodes(node.getRightChild());
}
which recursively counts leaf nodes for left and right sub trees and returns the total count

How to find the deepest path from root made up of only 1's in a binary search tree?

We have a binary tree (not a BST) made up of only 0s and 1s. we need to find the deepest 1 with a path from root made up only of 1's
Source : Amazon interview Q
public static int findMaxOnesDepth(Node root){
if(root != null && root.getValue() == 1){
return Math.max(1 + findMaxOnesDepth(root.getLeft()),
1 + findMaxOnesDepth(root.getRight());
}
else {
return 0;
}
}
If the node you are on is a '0', then the depth of '1's is 0. Otherwise, if the node you are on is a '1', then add 1 to the maximum 'one's depth' of both your left and right children - and return the maximum of those.
The above code finds the length, to find the actual nodes along the path, you could use a list to keep track of this
public static ArrayList<Node> findLongestOnesPath(Node root){
ArrayList<Node> currStack = new ArrayList<Node>();
if( root != null && root.getValue() == 1){
currStack.add(root);
ArrayList<Node> leftStack = findLongestOnesPath(root.getLeft());
ArrayList<Node> rightStack = findLongestOnesPath(root.getRight());
if(leftStack.size() > rightStack.size()){
currStack.addAll(leftStack);
}
else{
currStack.addAll(rightStack);
}
}
return currStack;
}

Resources