4.1 Route Between Nodes: Given a directed graph, design an algorithm to find out whether there is a
route between two nodes
Approach 1: Brute force approach:
Look a look at each nodes pointers values. If it doesn't exist, then look at those pointers pointers values. Accumulate this process over time. Search the pointers accumulated values over time, searching through this array everytime until your search is exhausted. If one or the other original nodes intersection in there values, then the result is true.
Approach 2: Breadth first search
Iterate through the graph, marking nodes as visited to avoid repetition.
Notice how queue's are really nice in exploring and checking all adjacent nodes. Its a nice algorithm to know in general.
// great for lightweight labelingenumState { Unvisited, Visiting, Visited };boolisConnected(Graph g,Node*one,Node*two) { // same node or no linksif (one == two ||one.getAdjacent() ==nullptr||two.getAdjacent() ==nullptr)returntrue; // mark each nodefor (Node i :g.getNodes()) {i.state =State.Unvisited; } // base caseone.state =State.Visiting; queue<Node*> explore;explore.push(one); Node *temp =newNode(); // explore adjacent node starting from arbitrary nodewhile (!explore.isEmpty()) { // explore one node at a time, and its adjacent pairs temp =explore.pop();if (temp !=nullptr) {for (Node i :temp.getAdjacent()) {if (i.state ==State.Unvisted) {if (i == two) returntrue;i.state ==State.Visiting;explore.push(i); } } }temp.state =State.Visited; }returnfalse;}
4.2 Minimal Tree: Given a sorted (increasing order) array with unique integer elements, write an algorithm to create a binary search tree with minimal height.
This algorithm can be solved by continuously taking the midpoint of the array and inserting it into the Binary search tree using level order insertion.
Or even more simply, this algorithm follows a pre-order traversal pattern: ->access, ->left, -> right. We can apply this recursive procedure using our midpoint rule.
I've referenced below an image that describes the way I went about this process.
I've noticed that procedure of belowing the tree comes out to be a level order tranversal (or BFS). Each node represents an low and high that can calculate the index of nums to insert into the tree next. In addition every subsequential node after that is based on its previous.
boolisValidIndex(int low,int high,int size) {return (low <= high && low >=0&& low <= size -1&& high >=0&& high <= size -1);}voidbuild_min_tree(vector<int> &nums) {if (nums.empty()) return; queue<pair<int,int>> low_high;int low =0;int high =nums.size() -1;int middle;low_high.push(make_pair(low, high));while (!low_high.empty()) { pair<int,int> add_this =low_high.front();low_high.pop(); middle =add_this.first + ((add_this.second -add_this.first) /2); cout <<"index:"<< middle <<" ("<<nums.at(middle) <<" ) "; // BST::insert(nums.at(middle)); // add the next two descendents depending on the previous low =add_this.first; high = middle -1;if (isValidIndex(low, high,nums.size())) {low_high.push(make_pair(low, high)); } low = middle +1; high =add_this.second;if (isValidIndex(low, high,nums.size())) {low_high.push(make_pair(low, high)); } }}intmain(){ vector<int> nums = {0,1,2,3,4,5,6,7,8,9};build_min_tree(nums);pause(); nums = { 0,1,2,3,4,5,6,7,8,9,10,11,12,123,200,300,2010 };build_min_tree(nums);pause();}
This follows the BST route, albeit it has been modified to print out actually level by level.
All that is required next of this is to simplify insert the elements printed out in the for loop.
voidLL_BFS(Node*root) { vector<Node*> current;current.push_back(root);while (!current.empty()) { // print all the current entiresfor (auto&i : current) { cout <<i->value <<" "; } cout << endl; // set up the next levelint prev_size =current.size();for (int i =0; i < prev_size; i++) { // erase current node, and replace with its children Node *temp =current.at(i);if (temp->left)current.push_back(temp->left);if (temp->right)current.push_back(temp->right); } // erase all old elementscurrent.erase(current.begin(),current.begin() + prev_size); } }
4.4 Check Balanced: Implement a function to check if a binary tree is balanced. For the purposes of this question, a balanced tree is defined to be a tree such that the heights of the two subtrees of any node never differ by more than one.
Brainstorm:
Reminded me of a post order traversal, where we recursively access individual subtrees, starting with the left.
4.7 Build Order: You are given a list of projects and a list of dependencies (which is a list of pairs of projects, where the second project is dependent on the first project). All of a project's dependencies must be built before the project is. Find a build order that will allow the projects to be built. If there is no valid build order, return an error.
The idea with this problem is to build a graph that represents the dependencies provided. However, the direction we chose to do so is important. In the example, (a d) implies that a is dependent on d, or we can write that as (a->d) in the graph. However, because we can trying to prioritize the things the depend on nothing first, then I chose to write it the other way (d->a). This, in the beginning, I will have dependencies f and e empty.
Faults in this implementation: In my linked list implementation, I am lazy and havent bothered to handle memory leaks. Also, there is a more elegent solution to removal all element of val.
EXAMPLE
Input:
projects: a, b, c, d, e, f
dependencies: (a, d), (f, b), (b, d), (f, a), (d, c)
Output: f, e, a, b, d, c
4.8 First Common Ancestor: Design an algorithm and write code to find the first common ancestor of two nodes in a binary tree. Avoid storing additional nodes in a data structure. NOTE: This is not necessarily a binary search tree.
This problem is similar to finding the intersection of two linked lists.
The first common ancestor of two nodes is also the first intersections of two nodes in a binary tree.
I am going to assume that each node has accessibility to its parent.
As a reminder, depth measures the number of edges a particular node has to its root, while the height of a tree measure the number of edges a particular node has to its deepest leaf. We use depth to measure distance because we are going to be traversing UP the tree, rather than down.
Node*firstCommonAncestor(Node*one,Node* two) {if (!one ||!two) returnnullptr;int distance =depth(one) -depth(two); // if (-), then two is larger, so increment it first, and vise-versaif (distance <0) {for (int i =0; i <abs(distance); i++) { two =two->parent; } // now increment at the same timewhile (one != two) { one =one->parent; two =two->parent; } }else {for (int i =0; i <abs(distance); i++) { one =one->parent; } // now increment at the same timewhile (one != two) { one =one->parent; two =two->parent; } }return one; // or two}intdepth(Node*node) {int depth =0;while (node->parent) { depth++; node =node->parent; }return depth;}
Now I am going to assume that the the parent of each node is not in the Object of Node.
Now I am going to assume that the the parent of each node is not in the Object of Node.
The idea goes as follows:
First we check the base condition: we will assume that a node cannot be a descendent of itself. So that if both nodes are shared across the same linage, then will return null. This is what the first condition checks for. Either the root is null to begin with or q contains p or p contains q.
Now we can step into the recursive condition: but we first have to understand the following:
One, the root is always the highest common ancestor of all nodes, since all nodes can trace their linage all the way up to the root.
Two, the first instance starting from the root that q and p a are on different sides of the tree, we know the root is the lowest common ancestor. This is because all the previous ancestoral nodes before are were some level of the highest common ancestor.
So the question now lyes: what part of the tree do we descend down? Well, if p_contained_in_left != q_contained_in_right, it could either be one of the two: !p_contained_in_left == q_contained_in_right or !q_contained_in_right == p_contained_in_left. The first one says that both p and q are contained in the right child of the subtree. The second one says both p and q are contained in the left child of the subtree. So we always continue to descend down the tree that contains both p and q.
4.9 BST Sequences: A binary search tree was created by traversing through an array from left to right and inserting each element. Given a binary search tree with distinct elements, print all possible arrays that could have led to this tree.
Output: {2, 1, 3}, {2, 3, 1}
Brain storm:
I first noticed that a pre order traversal nicely prints out a possible combination of the array. I can use this transversal once, and use the output array as the basis of the remainder of my combinations depending on a few properties.
I will have a level order traversal, which will check to see if has another sibling (same parent), by using the property of B-trees that children with siblings are expected to grow by 2^n in pairs of two per level. In a binary configuration, this will determine whether the number is flippable in the combination.
num unique flips = log_2(num flippables)
Each unique flip can be expressed as a combination on the number of states
num combination states = 2^(unique flip), an even number
In my pre-order tranversal array, each unique flip occurs with the next proceeding flip, and this will produce another combination based on the pre order combination.
Then, using each uniquely flipped array, we can write as a combination of the others, discluding itself and the preorder combination.
4.10 Check Subtree: Tl and T2 are two very large binary trees, with Tl much bigger than T2. Create an algorithm to determine if T2 is a subtree of Tl. A tree T2 is a subtree of T1 if there exists a node n in Tl such that the subtree of n is identical to T2 . That is, if you cut off the tree at node n, the two trees would be identical.
Method 1:
With this method, I am using the idea that within a preorder traversal, a subtree exists if within said tranversal exists the consecutive sequence of values of the subtree.
However, we must indicate null elements in the tree because in doing so we preverse the entire uniqueness of a tree. Then we compare two preorder traversals with the null elements included, we ensure a subtree exists when they both preserve the same values and null elements.
O(N) to traverse, O(N) extra memory, and then O(N^2) to check subarray.
Lets try and get rid of that N^2!
Method 2:
This method checks to see whether or two trees are the same on the basis that their addresses are the same. So while it doesnt work with two independent trees, it will work with the same tree.
O(N) time and space.
I've modified the subtree comparison by simply finding first element of the preorder tranversal of the second tree, and then confirming the remainder of the sequence. This will work with duplicate elements, since the addresses are unique.
template<typenameiter>boolisEqual(iter main_start,vector<Node*> &nums) {for (int i =0; i <nums.size(); i++) {if (&(*(main_start + i)) !=&nums.at(i)) {returnfalse; } }returntrue;}intfind(vector<Node*> &nums,Node* findthis) {for (int i =0; i <nums.size(); i++) {if (&nums.at(i) ==&findthis) {return i; } }return INT_MAX;}// testing if nums2 is subarray of nums1boolisSubArray(vector<Node*> &main,vector<Node*> &nums) {int index =find(main,nums.at(0));if (index == INT_MAX) returnfalse;else { // confirm the rest of the sequencereturnisEqual(main.begin() + index, nums); }}voidpreOrderTrav(Node*root,vector<Node*> &ar) {if (!root) {ar.push_back(nullptr);return; }ar.push_back(root);preOrderTrav(root->left, ar);preOrderTrav(root->right, ar);}boolisSubtree(Node*root1,Node*root2) { vector<Node*> tree1, tree2;preOrderTrav(root1, tree1);preOrderTrav(root2, tree2);print(tree1);print(tree2);returnisSubArray(tree1, tree2);}
To make this work with two independent subtrees, I've modified it to take in the values instead.
This may not work with nodes in a tree that contain duplicate elements!
template<typenameiter>boolisEqual(iter main_start,vector<int> &nums) {for (int i =0; i <nums.size(); i++) {if (*(main_start + i) !=nums.at(i)) {returnfalse; } }returntrue;}intfind(vector<int> &nums,int findthis) {for (int i =0; i <nums.size(); i++) {if (nums.at(i) == findthis) {return i; } }return INT_MIN;}// testing if nums2 is subarray of nums1boolisSubArray(vector<int> &main,vector<int> &nums) {int index =find(main,nums.at(0));if (index == INT_MIN) returnfalse;else { // confirm the rest of the sequencereturnisEqual(main.begin() + index, nums); }}voidpreOrderTrav(Node*root,vector<int> &ar) {if (!root) {ar.push_back(INT_MAX);return; }ar.push_back(root->value);preOrderTrav(root->left, ar);preOrderTrav(root->right, ar);}boolisSubtree(Node*root1,Node*root2) { vector<int> tree1, tree2;preOrderTrav(root1, tree1);preOrderTrav(root2, tree2);print(tree1);print(tree2);returnisSubArray(tree1, tree2);}
4.11 Random Node: You are implementing a binary search tree class from scratch, which, in addition to insert, find, and delete, has a method getRandomNode() which returns a random node from the tree. All nodes should be equally likely to be chosen. Design and implement an algorithm for getRandomNode, and explain how you would implement the rest of the methods.
My getRandNode assumes that our bst has kept track of the max level. I use this variable to indicate to the tree how far we should go down as a random index.
Then, whilst tranversing the tree recursively, I keep track of this count, and simply join a random left or right.
Node*tranverseRand(Node*root,int level,int count) {if (level <= count) return root; cout <<root->value << endl;pause();srand(time(nullptr));bool direction =rand() %1;if (root->right && direction) tranverseRand(root->right, level,++count);elseif(root->left &&!direction) tranverseRand(root->left, level,++count); // otherwise just take the path that existselseif (root->right)tranverseRand(root->right, level,++count);elsetranverseRand(root->left, level,++count);}Node*getRandomNode(Node*root,int maxLevel) {if (!root) returnnullptr;srand(time(nullptr));int level =rand() % maxLevel; // [0 - maxLevel]returntranverseRand(root, level,0);}
However, while this approach is fast, it doesnt represent the nodes equally. It would if the left and right sides were the same size, but it is likely not the case. We cannot have left and right be chosen on a 50/50 likelyhood. If the left side contains more nodes, it would be represented as more likely to go left.
To fix this, what we can do is maintain the size of the left and right subtree PER node. At this point, we would then be able to rightfully balance the probabilities of the left and right subtrees.
Node*tranverseRand(Node*root,int level,int count) {if (level <= count) return root;bool direction =rand() %root->size;if (root->right && direction >root->leftsize) tranverseRand(root->right, level,++count);elseif(root->left && direction <=root->leftsize)tranverseRand(root->left, level,++count); // otherwise just take the path that existselseif (root->right)tranverseRand(root->right, level,++count);elsetranverseRand(root->left, level,++count);}Node*getRandomNode(Node*root,int maxLevel) {if (!root) returnnullptr;srand(time(nullptr));int level =rand() % maxLevel; // [0 - maxLevel]returntranverseRand(root, level,0);}Node*tranverseRand(Node*root,int level,int count) {if (level <= count) return root;bool direction =rand() %root->size;if (root->right && direction >root->leftsize) tranverseRand(root->right, level,++count);elseif(root->left && direction <=root->leftsize)tranverseRand(root->left, level,++count); // otherwise just take the path that existselseif (root->right)tranverseRand(root->right, level,++count);elsetranverseRand(root->left, level,++count);}Node*getRandomNode(Node*root,int maxLevel) {if (!root) returnnullptr;srand(time(nullptr));int level =rand() % maxLevel; // [0 - maxLevel]returntranverseRand(root, level,0);}
An a lot simpler approach would be to just tranverse (any traversal) the tree until we reach a random index n. Where n is 0 % size_tree.