leetcode-236. Nearest common ancestor of binary tree

Posted by BLINDGUY on Sat, 19 Feb 2022 13:48:41 +0100

leetcode-236. Nearest common ancestor of binary tree

deque method

class Solution {
public:
	deque<TreeNode*> dqp;
	deque<TreeNode*> dqq;
	bool findNode(TreeNode* root, deque<TreeNode*>& dq, TreeNode* target) {
		dq.push_back(root);
		
		if(root == target) {
			return true;
		}
		
		bool ret = false;
		if(root->left) {
			ret = findNode(root->left, dq, target);
		}
		
		if (ret) {
			return true;
		}

		if(root->right) {
			ret = findNode(root->right, dq, target);
		}

		if (ret) {
			return true;
		}		
		
		dq.pop_back();
		return false;
	}

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
		// Nearest common ancestor
		findNode(root, dqp, p);
		findNode(root, dqq, q);
		
		TreeNode* ret = NULL;
		while(!dqp.empty() && !dqq.empty()) {
			if (dqp.front() == dqq.front()) {
				ret = dqp.front();
				dqp.pop_front();
				dqq.pop_front();
			} else {
				break;
			}
		}
		
		return ret;
    }
};

One time recursion

class Solution {
public:
	TreeNode* ans;
	bool dfs(TreeNode* root, TreeNode* p, TreeNode* q) {
		bool lresult = false;
		bool rresult = false;

		if (root->left) {
			lresult = dfs(root->left, p, q);
		}
		
		if (root->right) {
			rresult = dfs(root->right, p, q);
		}
		
		if (lresult && rresult) {
			// On the left and right subtrees of the current node, there is a collateral relationship
			ans = root;
		}  else if (lresult || rresult) {
			if (root == p || root == q) {
				// On the subtree of the current node, it is a direct relationship
				ans = root;
			}
		}
		
		return lresult || rresult || (root == p) || (root == q);
	}

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        dfs(root, p, q);
        return ans;
    }
};

(lresult && rresult)
Both the left subtree and the right subtree contain p nodes or q nodes,
If the left subtree contains p nodes, the right subtree can only contain q nodes, and vice versa,
Because p node and q node are different and unique nodes,
Therefore, if this judgment condition is met, it means that root is the nearest public ancestor to be found.

(lresult || rresult) && (root == p || root == q)
Considering that root is just a p-node or q-node, and one of its left or right subtrees contains another node,
Therefore, if this judgment condition is met, it can also indicate that root is the nearest public ancestor to find.

Time complexity: O(N), where N is the number of nodes of the binary tree.
All nodes of the binary tree have and will only be accessed once, so the time complexity is O(N).
Spatial complexity: O(N), where N is the number of nodes of the binary tree.
The stack depth of recursive calls depends on the height of the binary tree. In the worst case, the binary tree is a chain, and the height is N.

Hash

class Solution {
public:
	unordered_set<TreeNode*> us;
	unordered_map<TreeNode*, TreeNode*> um;

	void dfs(TreeNode* root) {
		if (root->left) {
			um[root->left] = root;
			dfs(root->left);
		}

		if (root->right) {
			um[root->right] = root;
			dfs(root->right);
		}	
	}

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
		TreeNode* ans;
        dfs(root); // Root has no root node
		
		unordered_map<TreeNode*, TreeNode*>::iterator umit = um.find(p);
		while(umit != um.end()) {
			us.insert(umit->first);
			umit = um.find(umit->second);
		}
		
		umit = um.find(q);
		while(umit != um.end()) {
			if (us.count(umit->first))
				return umit->first;
			umit = um.find(umit->second);
		}		
		
        return root;
    }
};

class Solution {
public:
	unordered_set<TreeNode*> us;
	unordered_map<TreeNode*, TreeNode*> um;

	void dfs(TreeNode* root) {
		if (root->left) {
			um[root->left] = root;
			dfs(root->left);
		}

		if (root->right) {
			um[root->right] = root;
			dfs(root->right);
		}	
	}

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
		TreeNode* ans;
		um[root] = NULL;
        dfs(root);
		
		unordered_map<TreeNode*, TreeNode*>::iterator umit = um.find(p);
		while(umit != um.end()) {
			us.insert(umit->first);
			umit = um.find(umit->second);
		}
		
		umit = um.find(q);
		while(umit != um.end()) {
			if (us.count(umit->first))
				return umit->first;
			umit = um.find(umit->second);
		}

        return NULL;	
    }
};

Take [3,5,1,6,2,0,8, null, null, 7,4] P = 5, q = 3 as an example
Construct unordered_map
5 3
1 3
6 5
2 5
0 1
0 8
7 2
4 2

Fill unordered with p_ set
5

Access unordered with q_ set
Visit 4
Visit 2
Visit 5 ok

Then the nearest ancestor is 5

thinking
The hash table is used to store the parent nodes of all nodes, and then the parent node information of the node can be used to jump up from the p node,
And record the visited nodes, and then jump up from the q node. If you encounter the visited nodes,
Then this node is the nearest public ancestor to find.

algorithm
Traverse the whole binary tree from the root node, and record the parent node pointer of each node with a hash table.
Start from the p-node and continue to move to its ancestors, and use the data structure to record the visited ancestors.
Similarly, start from the q node and continue to move to its ancestors. If any ancestors have been visited,
This means that this is the deepest common ancestor of p and q, that is, LCA node.

Hash optimization

Only the required paths are recorded

class Solution {
public:
	unordered_set<TreeNode*> us;
	unordered_map<TreeNode*, TreeNode*> um;
	TreeNode* gp;
    TreeNode* gq;
	
	bool dfs(TreeNode* root) {
		bool lchild = false;
		bool rchild = false;

		if (root->left) {
			lchild = dfs(root->left);
		}

		if (root->right) {
			rchild = dfs(root->right);
		}
		
		if (lchild && rchild) {
			um[root->left] = root;
			um[root->right] = root;
			return true;
		} else if (lchild) {
			um[root->left] = root;
			return true;
		} else if (rchild) {
			um[root->right] = root;
			return true;
		} else if (root == gp || root == gq) {
			return true;
		}

		return false;
	}

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
		TreeNode* ans;
		gp = p;
		gq = q;
		um[root] = NULL;
        dfs(root);
		
		unordered_map<TreeNode*, TreeNode*>::iterator umit = um.find(p);
		while(umit != um.end()) {
			us.insert(umit->first);
			umit = um.find(umit->second);
		}
		
		umit = um.find(q);
		while(umit != um.end()) {
			if (us.count(umit->first))
				return umit->first;
			umit = um.find(umit->second);
		}
		
		return NULL;
    }
};

Recursive optimization

Think again, if two nodes are in the same subtree, the node with the largest generation will be returned directly;
If two nodes are in different subtrees, the current node will be returned directly

class Solution {
public:
	TreeNode* gp;
	TreeNode* gq;
	TreeNode* ans;
	
	bool dfs(TreeNode* root) {
		bool lresult = false;
		bool rresult = false;
		if (root->left) {
			lresult = dfs(root->left);
		}
		
		if (root->right) {
			rresult = dfs(root->right);
		}
		
		if (lresult && rresult) {
			// p and q are scattered on the left and right sides of root
			ans = root;
			return true;
        } else if (root == gp || root == gq) {
			// When it is distributed on one side, it is dominated by those with large generations
			// Recursion can be realized. The elements with small generations will be changed first, and the elements with large generations will be changed later
			// It can ensure that the last modification of ans is to back up large elements
			// This else if should be in front of the next else if
			// Otherwise, the ans will not be modified after the younger generation returns
			ans = root;
			return true;  
        } else if (lresult || rresult) {
			return true;
		} else {
            return false;
        }
	}
	

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
		gp = p;
		gq = q;
		dfs(root);
		return ans;
    }
};

appendix

us handling of NULL

#include <iostream>
#include <unordered_map>
#include <unordered_set>

using namespace std;

int main()
{
	int a = 3;
	unordered_set<int*> us;
	unordered_set<int*>::iterator usit;
	cout << "===> before" << endl;
	cout << "us's size() is " << us.size() << endl;
	cout << "us's count(NULL) is " << us.count(NULL) << endl;
	usit = us.find(NULL);
	if (usit != us.end()) {
		cout << "usit can find NULL element" << endl;
	} else {
		cout << "usit can't find NULL element" << endl;
	}

	us.insert(NULL);

	cout << "===> after" << endl;
	cout << "us's size() is " << us.size() << endl;
	cout << "us's count(NULL) is " << us.count(NULL) << endl;
	
	usit = us.find(NULL);
	if (usit != us.end()) {
		cout << "usit can find NULL element" << endl;
	} else {
		cout << "usit can't find NULL element" << endl;
	}

	return 0;
}

===> before
us's size() is 0
us's count(NULL) is 0
usit can't find NULL element
===> after
us's size() is 1
us's count(NULL) is 1
usit can find NULL element

um handling of NULL

#include <iostream>
#include <unordered_map>
#include <unordered_set>

using namespace std;

int main()
{
	int a = 3;
	unordered_map<int*, int*> um;
	unordered_map<int*, int*>::iterator umit;
	cout << "===> before" << endl;
	cout << "um's size() is " << um.size() << endl;
	cout << "um's count(NULL) is " << um.count(NULL) << endl;
	umit = um.find(NULL);
	if (umit != um.end()) {
		cout << "umit can find NULL element" << endl;
	} else {
		cout << "umit can't find NULL element" << endl;
	}

	um[NULL] = NULL;

	cout << "===> after" << endl;
	cout << "um's size() is " << um.size() << endl;
	cout << "um's count(NULL) is " << um.count(NULL) << endl;
	umit = um.find(NULL);
	if (umit != um.end()) {
		cout << "umit can find NULL element" << endl;
	} else {
		cout << "umit can't find NULL element" << endl;
	}
	return 0;
}

===> before
um's size() is 0
um's count(NULL) is 0
umit can't find NULL element
===> after
um's size() is 1
um's count(NULL) is 1
umit can find NULL element

Topics: Algorithm leetcode