Data Structures and Algorithms Flashcards

1
Q

Given a double ‘x’ and an integer ‘n’. Write a function to calculate ‘x’ raised to the power ‘n’. For example:

power (2, 5) = 32
power (3, 4) = 81
power (1.5, 3) = 3.375
power (2, -2) = 0.25

A

Naïve Approach : Multiply ‘x’ by ‘n’ times, this is O(n) complexity

Improved Solution : Use Divide and Conquer. We divide the exponent by 2 until we reach the base case 1. This will give us logarithmic time & space complexity.

  • *Case 1)** We can express 2 ^ 5 as 2 * 2^(5/2) * 2^(5/2),
  • *Case 2)** if the exponent is even, 3^4 = 3^(4/2) * 3^(4/2);
const powerOfNumRecurse = function(x, n) {
 if (n === 0) {
 return 1;
 }

if (n === 1) {
return x;
}

let temp = powerOfNumRecurse(x, Math.floor(n/2));
 let result;
 //check if n is even or odd
 if (n % 2 === 0) {
 return temp \* temp;
 } else if (n % 2 === 1) {
 return x \* temp \* temp;
 }
};

const powerOfNumMain = function(x, n) {

let isExpNegative = false;
 let result;
if (n \< 0) {
 isExpNegative = true;
 //turn n into positive
 n \*= -1;
 }

let result = powerOfNumRecurse(x, n);

if (isExpNegative) {
return 1/result;
}

return result;
};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

Remove Duplicates from a linked list. The linked list can be sorted, or unsorted. Give two solutions, one with O(1) memory space, and one without memory restrictions

A

Solution #1 : Memory Space O(1). Works for both sorted/unsorted. Use HashSet to store value of all linked lists we’ve seen so far, O(n) space and time complexity. If we see a value we’ve stored already, just skip over it.

Key : Use prev and current again, with prev initialized to null, and current at head. If it’s a duplicate value that we’ve seen before, just skip over it by connecting previous to current’s next. Otherwise, add current value to our hashSet, and just advance prev and current.

Solution #2: If the list is sorted

Since the list is sorted this means duplicates are adjacent to each other. We can do a linear scan starting from head, and only connect to next node if it’s a new value.

Key : Use prev and current again, but with prev at head, and current at head.next.

We’ll traverse through the list, and update prev’s next pointer only if current’s value is new.

Don’t forget to update prev’s next to null. Since it’ll be the tail node in our updated list.

const removeDuplicatesSorted = function(head) {
 //if head is null, or linked list has length 1, return the list as is.
 if (!head || !head.next) {
 return head;
 }
//start at second node as current to prevent null error
 let prev = head;
 let current = head.next;
while (current !== null) {
 //We only do anything, EVER if current's a different value from prev.
 if (current.val !== prev.val) {
 prev.next = current;
 prev = current;
 }
//Otherwise just keep on trucking.
 current = current.next;
 }

prev.next = null;

return head;
}

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

Implement in-order traversal iteratively.

A

Question asked when stuck : how do we examine root’s left and root’s value? Answer : They’re the same. When we pop, the root we’re looking at IS the parent’s left. So all we have to do is examine the value right after we pop from the stack.

Hint : First try with a simple BST using a stack. The key to understanding this is that whenever we pop, we move up one level and we access the parent.

Then we can decide what to do with the parent(root), or the root’s right. Key : We have an inner while loop that goes deep into the left subtrees as long as the current node we’re looking at isn’t null. We need this INSIDE the outer while loop because when we go RIGHT, we have to examine the left sub-trees of that right subtree.

Solution #1: Initialize currentNode to root. We will iterate as long as currentNode exists, and our stack isn’t empty. We need an inner while loop that keeps going deeply left as long as currentNode exists, pushing to the stack each time. If currentNode is null, we simply pop from the stack, examine it. And then go right.(Set currentNode to currentNode.right);

Solution #1: using two cases(current node is null, or not). Key to understanding this is that with DFS, the currentNode we’re looking at will either be null, or not null. If it’s null, that means we can safely move up one level. So we’ll be popping from the stack. Start by initializing a stack and setting current node to root. While loop as long as stack isn’t empty or current node exists.

Case 1) node isn’t null, go left! push to stack and set node to node.left

Case 2) node is null. Move up one level. First pop the node(Moving up one level), and examine it. Then go right! set node to node.right

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

Given a list that represents a permutation of a list, find the next permutation in lexicographical order.

A

Let’s say we have 1, 3, 6, 5, 4
The next permutation in lexicographical order is 1,4,3,5,6. How did I know that?
1. I examined the number right to left looking for the first increasing sequence( 3 -> 6). I know that this is where number swaps will happen to find adjacent permutations.

Go right to left, trying to find A[i-1] <= A[i]. We are finding the first decreasing sequence(right -> left) So A[i] is 6, and A[i-1] is 3.

2. Then, we search right to left, trying to find the number that’s JUST larger than A[i-1]. This is because that’s the number that will come ‘NEXT’ in lexicographical order and replace the number A[i]. This number will exist on the right side of A[i-1].

So step 2 : Go right to left, trying to find A[j] >= A[i-1], as long as j >= i. So in here we’re trying to find the largest element that’s JUST LARGER than A[i-1]. The maximum element to the right of A[i-1]. A[j] is 4.

Step 3. Swap A[i-1] and A[j]. We swap 3 and 4.

Step 4. Since we picked out the maximum to the right of A[i-1], the sequence to the right of A[i-1] will be in decreasing order. We simply REVERSE elements from A[i] to make it increasing order.

const swap = function(list, a, b) {
 let temp = list[a];
 list[a] = list[b];
 list[b] = temp;
}

const nextPermutation = function(nums) {

let i = nums.length-1;
 let j = nums.length-1;
//this loop breaks until we find nums[i-1] \<= nums[i]
 while (i \> 0) {
 if (nums[i-1] \<= nums[i]) {
 break;
 }
 i--;
 }

// if (i <= 0) return false; //If i === 0 this is the LAST permutation since i is the largest number

if (i > 0) {

while (j >= i) {
if (nums[j] >= nums[i-1]) {
break;
}
j–;
}

//swap only if i is greater than 0
 //This means we only swap if it's not the LAST permutation, since for the last permutatio we don't want to do anything until the reversing part.
 swap(nums, j, i-1);
 }

//reset j
j = nums.length-1;
//reverse from i till end
while (i < j) {
swap(nums, i, j);
i++;
j–;
}
};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

Find the Nth fibonacci number using memoization. Do it bottom-up and top-down.

A

TOP-DOWN

const fibonacciRecurse = function(n) {

//D[n] = D[n-1] + D[n-2];
 const memo = {};
const subroutine = function(n) {
 //base case
 if (n \<= 1) return n;

if (memo[n]) {
return memo[n];
}

memo[n] = subroutine(n-1) + subroutine(n-2);
return memo[n];
};

subroutine(n);

return memo[n];
};

BOTTOM-UP

const fibonacciBottomUp = function(n) {
 const memo = {};
 memo[0] = 0;
 memo[1] = 1;
for (let i = 2; i \<= n; i++) {
 memo[i] = memo[i-1] + memo[i-2];
 }

return memo[n];
}

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

Implement a Min Stack that supports getMinimum() in constant time.

A

Two solutions :

#1. Use an extra stack to hold the minimum. We only push to the minStack if it’s a new minimum(If it’s less than or equal to the minimum). This is because we have unique cases with duplicate values! Otherwise we don’t push. On pop(), just check if it’s a minimum we’re popping and then update the minimum as necessary.

#2. Use a tuple to store [element, currentMinimum] as our data.

On push(), update minimum if it’s a new minimum we’re pushing to stack.

Stack.prototype.push = function(elem) {
 let currentMin = this.getMinimum();
 //if it's a new minimum, update currentMin to elem.
 if (currentMin === null || elem \< currentMin) {
 currentMin = elem;
 }
 //store both elem, along with currentMin
 this.storage.push([elem, currentMin]);
}

Stack.prototype.getMinimum = function() {
if (this.storage.length === 0) {
return null;
} else {
return this.storage[this.storage.length-1][1];
}
}

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

Delete a node from a BST, explain how it’ll be done.

A

Basic Approach : We will be using a recursive approach to handle deleting a node from BST.

We can break it down to three general cases :
Case 1) Node to delete is a leaf node. In this case all we have to do is set the node to root, and return it.

  • *Case 2)** Node to delete has one child. In this case all we have to do is set the node to the other existing child, and return it.
  • *Case 3)** Node has two children. This one is a bit more tricky, but we basically find the minimum in the right subtree and copy the value to the node. Then we delete the minimum in the rightsubtree(Use our recursive function).

An important detail in this implementation is to return the root(node) every time so that the properly adjusted nodes are returned up to recursion and we can set the pointers accordingly.

const findMin = function(root) {
 if (!root) return null;

while (root.left) {
root = root.left;
}

return root;
};

const deleteNode = function(root, key) {

if (!root) return root;

if (root.val \< key) {
 root.right = deleteNode(root.right, key);
 } else if (root.val \> key) {
 root.left = deleteNode(root.left, key);
 } else if (root.val === key) {
 //Case 1 : leaf node
 if (!root.left && !root.right) {
 root = null;
 }
else if (!root.left) {
 root = root.right;
 }
else if (!root.right) {
 root = root.left;
 }
else {
 let minInRight = findMin(root.right);
 //copy minInRight's value into root
 root.val = minInRight.val;

root.right = deleteNode(root.right, minInRight.val);
}
}
console.log(‘root :’, root);
return root;
};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

Find an inorder successor in a BST.

A

Naive Solution : Create a list of all the nodes in in-order traversal, and do a linear scan to find the successor. O(n) time, where n is the number of nodes.

Optimized Solution : If you look at finding in-order successor, there’s two cases :

Case 1) Current node has a rightChild. Then our successor is the minimum in the rightChild. Or the leftmost node in the rightChild.

Case 2) Current node has no rightChild. In this case, this is the confusing one - we have to pop back to the parent. There are two cases, if this node is a leftChild or a rightChild of the parent. If it’s a leftChild, we simply return the parent since it’s the successor. If it’s the rightChild, we have to find the parent’s parent(Since the immediate parent would have already been visited). So we have to go to the closest ancestor for which our node would be in its left subtree. Understand this, and the problem is easy.

KEY : We need to implement a findMin() function, the iterative version using a while loop is much cleaner.

Basic Approach : Every time we go LEFT to find our targetNode, just remember the potential successor since it’ll become the parent.

const findMin = function(root) {
 if (!root) return null;

while (root.left) {
root = root.left;
}

return root;
};

const inorderSuccessor = function(root, p, successor) {
 if (!root) return null;

successor = successor || null;

//going left
 if (root.val \> p.val) {
 return inorderSuccessor(root.left, p, root);
 }
 //going right
 else if (root.val \< p.val) {
 return inorderSuccessor(root.right, p, successor);
 }
 //we've found our node
 else if (root.val === p.val) {
 //two cases
 if (root.right) {
 return findMin(root.right);
 } else if (root.right === null) {
 //return successor
 return successor;
 }
 }
};
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

Implement a Queue using stacks. Optimize for push() and for pop() in return.

A

Solution #1., push() is constant, and pop() is O(n).

Use inStack and outStack. Always push to inStack. For pop(), pop from outStack if outStack isn’t empty. If it is empty, we need to pop everything from inStack and push to outStack, and pop the top of outStack.

Since the runtime of outStack depends on how many elements there are on outStack at that given moment(Can be constant if there are items), the more expensive a O(n) dequeue is, the more O(1) it WINS us in the future.

Solution #2. If we optimized for pop() instead.

For pop(), we’ll just pop from stack1.

For push(), we’ll take out everything from stack1, put it into stack 2. Then we push the newest item to stack1. Then we’ll pop everything from stack2 back into stack1. So essentially we’re doing two exchanges between stack1 and stack2, except in the middle we push the newest item in stack1, because this puts the newest item at the back of the queue. In a way we’re using stack2 as a temporary holder, and putting the newest item at the back of the queue to preserve order.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

Given a BST class, implement a delete method to delete a node from a BST.

A

The important thing with deleting a node from BST’s is that we need need access to the parent(Since we have to re-set the parent’s links to its left/right). To achieve this, we will use recursion, and return the current tree(‘root’) at every execution so that when it returns up, the parent has access to the child.

Also, instead of using ‘this’ we will pass in an additional argument, ‘root’ because we won’t be able to re-set the ‘this’ context.

After this, we can break down deletion into three different cases :

First, we have to find our node by going left/right while comparing our root data against the input value. Use recursion here as well to re-adjust the children links.

1) Leaf node(No children)
2) One child
3) Two children

Since we’re returning the root at the end, in each case we simply want to modify the root value. Case 3 is the only tricky one, since we want to first find the minimum in the right-subtree, copy over its data into the root, and then DELETE that duplicate node in the right subtree. Essentially we have a smaller problem of deleting that minimum node in the right subtree.

//Pass the parent as an argument!

*BinarySearchTree.prototype.delete = function(val, root) {
 //we will have to re-set 'this'(which isn't possible), so we will pass in an additional parameter called root that will be initialized to 'this'
 root = root || this;*

if (!root) return null;

*//Go left
 if (root.data \> val) {
 root.leftChild = root.delete(val, root.leftChild);*
*//Go right
 } else if (root.data \< val) {
 root.rightChild = root.delete(val, root.rightChild);
 } else {
 //We've found our node, need to delete it*
*//Caes 1 : Leaf node
 if (root.leftChild === null && root.rightChild === null) {
 root = null;
 } else if (root.leftChild === null) {
 //Case 2 : Only rightChild or Only leftChild
 root = root.rightChild;
 } else if (root.rightChild === null) {
 root = root.leftChild;
 } else {
 //Case 3 : Two children*
*//Find minimum in rightSubtree
 let minInRightSubTree = root.rightChild.findMin();
 console.log("minInRightSubTree :", minInRightSubTree);
 //Copy this data into root!
 root.data = minInRightSubTree.data;*

//now there is a duplicate node in RightSubtree, delete it(We’re calling it FROM the reference of the right subtree.)
root.rightChild = root.delete(minInRightSubTree.data, root.rightChild);
}
}

*//Don't forget to return root
 return root;
};*
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

You’re given N pieces of bread. If the # of profit that you can make on selling i pieces of bread is P[i], find the maximum profit you can make from selling N piece of bread. As input, first you’re given the number of pieces of bread you can sell. The second input is P[i] from P[1] to P[n]. EX : For 4 pieces of bread, P[1] = 1 P[2] = 5 P[3] = 6 P[4] = 7 Since for 4 pieces, you can sell 2 pieces twice, 5 * 2 = 10.

A

We start by defining D[n] as the maximum profit I can make by selling n pieces of bread. Now we can clarify the problem as : Selling x + y + z = n So we sell x pieces of bread to one person, y pieces of bread to another, z pieces to another to total n pieces of bread. Note how many pieces we can sell to this last person. We can sell anywhere between 1 pieces to the whole batch, N pieces. If the last ‘batch’ we sell bread to is one person, D[n] = D[n-1] + P[1] because P[1] is the profit we can make by selling one piece. If it’s two people, D[n] = D[n-2] + P[2] … D[n] = D[0] + P[n] Then, we can express D[n] as D[n] = Max(D[n-i] + P[i]) where i is the # of people we sell bread to. 1 <= i <= n.

const sellFishBread = function(n, profitList) {
 const memo = {};
 memo[0] = 0;
 memo[1] = profitList[0];
//i goes from 1 to n, denoting the total # of fishbread we sell
 for (let i = 1; i \<= n; i++) {
 //j denotes the number of fishbread we sell to the last person
 for (let j = 1; j \<= i; j++) {
 if (!memo[i]) {
 memo[i] = memo[i-j] + profitList[j-1];
 }
 memo[i] = Math.max(memo[i], memo[i-j] + profitList[j-1]);
 }
 }

return memo[n];
};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

Find maximum depth of a tree

A

Key Point : Think of the smallest case. We want to find the maximum depths of the left subtree and the right subtree. However, since we’re going down one level, we add 1 to whatever the maximum of those two values are.

Simple Recursive Solution :

const maxDepth = function(root) {

if (!root) return 0;

return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));

};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

/*
Given a directed graph, design an algorithm to find out whether there is a route between two nodes.
*/

A

BFS/DFS.

With BFS : Can find shortest path bewteen the two nodes.
We use a queue, and a visited set to keep track of nodes visited. At each point if the node we’re examining next is the destination node, we return true.

When implementing DFS, be careful about return values from the recursive functions. We’re returning a boolean, so we can’t return the recursive calls immediately. Spawn off the recursive calls(Don’t return them), if any of them return true, then return true!

const routeDFS = function(sourceNode, destinationNode) {
const visited = new Set();
let current;
let neighbors;

const dfsRecurse = function(node) {
 if (node === destinationNode) {
 return true;
 }

visited.add(node);

neighbors = node.getNeighbors();

for (let nextNeighbor in neighbors) {
 if (!visited.has(nextNeighbor)) {
 //if any of the recursive calls return true, return true immediately.
 if (dfsRecurse(nextNeighbor)) {
 return true;
 }
 }
 }

return false;
};

return dfsRecurse(sourceNode);
};
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

Given two non-empty binary trees s and t, check whether tree t has exactly the same structure and node values with a subtree of s. A subtree of s is a tree consists of a node in s and all of this node’s descendants. The tree s could also be considered as a subtree of itself.

A

Important thing to note here is that in-order traversal won’t work. The reason is, because in a BST, in-order traversal will always print out the nodes in the same order even if the structures are different(Just work through a simple example like [1,0,2] and [null, 0, 1, 2].

So we have to use pre-order traversal, where the order is determined by the traversal itself. We just have to remember to pad the null nodes with ‘null’, and create string representations of each traversal. Afterwards, we check if one is a substring of another.

The runtime of this is O(m * n + m^2 + n^2). This is because we spend linear time with each traversal, and string concatenation takes linear time(Which makes it quadratic), and indexOf takes O(m*n). Space complexity is O(m + n), for each of the strings.

Another approach that’s easier, is simply traverse the first tree until we find a node that’s equal to the second root. Then we recursively call an isEqual helper(Easy to implement). This seems like O(m*n), because each time a node in TreeOne matches TreeTwo, we call an isEqual helper that essentially traverses all nodes in TreeTwo. However, we don’t actually call isEqual on all of TreeTwo, we call it k times, where k is the # of occurrences of TreeTwo’s root in TreeOne.

const isEqual = (treeOne, treeTwo) =\> {
 if (treeOne === null && treeTwo === null) return true;

if (treeOne === null || treeTwo === null) return false;

if (treeOne.val !== treeTwo.val) return false;

//they both have values.
return (
isEqual(treeOne.left, treeTwo.left) && isEqual(treeOne.right, treeTwo.right)
);
};

var isSubtree = function(s, t) {
 if (s === null) return false; //if the larger tree is null and we havent found the subtree return false.
if (isEqual(s, t)) return true;
 return isSubtree(s.left, t) || isSubtree(s.right, t);
};
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

Solve the classic Tower of Hanoi Problem, where you’re given n disks in decreasing size(bottom->top) and you have to move them from Peg A to Peg C, using an auxiliary Peg B in the middle.

A

We start by first considering cases for 1 disk, 2 disks, and 3 disks.

If we only have one disk, we simply move from A -> C using B.

If we have two disks, we first move the smallest disk A -> B. Then move the larger disk from A -> C, then move the smaller disk from B -> C.

Now let’s look at three disks. After going through all the steps, we can see that we can express the three disk problem as :

1). Move 2 disks from A -> B.

2) Move 1 disk(Largest disk) from A -> C.

3) Move 2 disks from B -> C.

No matter the number of disks, every problem can be expressed as such, since we know that the largest disk has to be on the bottom of C. Then we move the rest of the disks to C.

So we can express the recursive algorithm as such :

1). Move n-1 disks from A -> B.

2) Move 1 disk(Largest disk) from A -> C.

3) Move n-1 disks from B -> C.

const towerOfHanoi = function(n, start, mid, end) {
if (n === 1) {
console.log(start, ‘—->’, end);
return;
} else {
towerOfHanoi(n-1, start, end, mid);
towerOfHanoi(1, start, mid, end);
towerOfHanoi(n-1, mid, start, end);
}
};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

Zip a linked list from two ends

1->2->3->4->5->6
Output : 1->6->2->5->3->4

A

Straightforward, Clean Approach :

Split the linked list in the middle, then reverse the second half. Then merge the two lists together, taking one node from each list one at a time.

Recursive approach, slightly more complicated :

Use recursion to access the elements from end to back. This is slightly more complicated because : We need to use a flag to stop the recursion from doing more work after reaching the middle of the list, and also because we have to handle odd # and even # cases.

const zip = function(pList) {
 let front = pList;
 let temp = pList;
 let flag = true;
const recurse = function(back) {
 if (back === null) {
 return;
 }
//go to the tail
 recurse(back.next);
//
 if (front.next === back || front === back && flag) {
 back.next = null;
 flag = false;
 }
if (flag) {
 //save the original next first.
 temp = front.next;
 front.next = back;
 back.next = temp;
 front = temp;
 }

};

recurse(front);
return pList;
};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
17
Q

Implement a stack using queue(s). Implement push, pop, top, isEmpty and discuss their runtimes.

A

Question to ask the interviewer : Do we want to optimize for push() or for pop() ? Since we’re using two queues, we can get constant time for one of them, but linear time for the other.

KEY : Since queues, with enqueue() and dequeue() preserve the original order, we can’t just transfer them back and forth between each other. All we care about is the last item, that’s the only order we have to be able to access. So we’ll use one of the queues to ‘remember’ this last item added. We use one of the empty queues to ‘hold’ the last item added, and dequeue all other elements to that queue. This last item then becomes the first item in the queue. It ‘cheats’ its way back into the first of the line.

Also to avoid taking items out and putting them back into another second time, we SWAP references between the two queues when we’re done.

Solution #1., Optimizing for push() O(1), and pop() O(n).

For push, just push to queue1 always.

For pop(), the interesting one, we dequeue everything from queue1 and put them in queue2, except for the last item. Since we’ve dequeued everything except for the last one, we know this is the last item in the queue. We return this item. We also swap queue1 and queue2’s references.

Solution #2. push() O(n), pop() O(1).

This one is a little bit more tricky, but when we push we’ll be using queue2 to make the last item added cheat its way to the front of the queue to be dequeued first(That way it’s the top item).

For push(), first enqueue to queue1 if it’s empty. If it’s NOT empty, enqueue to queue2, and here’s the key part - dequeue everything from queue1 and enqueue to queue2. Since we’ve enqueued to queue2 the newest item BEFORE the emptying out of queue1, the newest item is in FRONT of the queue.

Then swap queue1 and queue2, so that for pop we’re always dequeuing from queue1.

This is why for pop(), we simply pop from queue1.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
18
Q

Implement post-order traversal iteratively. This is trickier than the other DFS traversals.

A

With post-order we’re looking at

The way we will solve this is by using another variable called visited to keep track of previously visited node. This will prevent us from re-visiting the <ROOT>when we go left, then right, then back to>.

So what we will do is go all the way to the left first.

Then we will peek the top of the stack. We check if it has a right subtree. If it has a right subtree AND the right subtree HASN’T been visited yet, this means we’ve visited so it’s time to visit the node. So we set currentNode to the rightChild of top(top of stack).

Otherwise, this means this is a leftChild so we can safely pop from our stack and visit it. We make sure to set the visited variable to this node.

We then set currentNode to null, so we can move up one level.

So we do solve this the same way we solve the other two, dividing it into two large cases.

Case 1) current node exists We have to keep going left, so push current node to stack, and set node = node.left. Simple.

Case 2) current node is null. This is the tricky part. First we peek the top of the stack’s rightChild(“temp”).

Subcase 1) If temp doesn’t have a rightChild, this means temp is a leaf node <LEFT>. so we’re ready to visit it. Pop temp from stack and examine it.

Then we need to check if temp was a rightChild of current stack’s top. If it WAS a rightChild of top, we pop node from stack and visit the node. Since that means we’re going <RIGHT> ->.

Subcase 2) Right child exists, which means we have to look at the right sub-tree first. Go right by setting current = temp. We’re essentially going up one level(to parent), then going to parent’s right subtree.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
19
Q

Find all permutations of a string. String may include duplicates.

A

If we didn’t have duplicates, we could use the swapping method to fix one character, and swap the rest one by one and using a backtracking solution to solve it. However, since we have to handle duplicates, we will use extra memory, a map to keep track of remaining character count.

Basic Approach : What we’ll do is keep track of remaining count for each character. Then, we will iterate over our list of unique characters, left-right, looking for the next available character(This means remaining count is NOT 0). If we find it, we’ll push the chosen character into the list. We make sure to update the character count at every step. We’ll use index, or level to keep track of the depth of the recursion and reach a base case when our level equals the length of our input.

const permuteUnique = function(nums) {

//generate a map with element : count
 const countMap = {};
 nums.forEach(function(elem) {
 if (!countMap[elem]) {
 countMap[elem] = 1;
 } else {
 countMap[elem] += 1;
 }
 });
const elements = [];
 const count = [];
 let index = 0;

for (let elem in countMap) {
elements.push(elem);
count.push(countMap[elem]);
}

const result = [];

//Recursive algorithm, we go from left to right looking for the next available character(IF remaining count is not 0). Then we push the character into result list. We used this character so we update the characterCount. We use level to keep track of the depth of the recursion.

const generate = function(currentLevel, generated) {
 if (currentLevel === nums.length) {
 result.push(generated.slice());
 return;
 }
 //look for the next available character. We iterate only up to the number of distinct elements' length...THIS IS KEY.
for (let i = 0; i \< elements.length; i++) {
 let currentChar = elements[i];
 if (count[i] === 0) {
 continue;
 }
 //add current character to generated
 generated[currentLevel] = currentChar;
 //decrement characterCount
 count[i] -= 1;
 //Enter next recursive call
 generate(currentLevel+1, generated);
 //Put character Count back
 count[i] += 1;
 }

};
generate(0, []);
return result;
};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
20
Q

Detect if a linked list has a cycle. Then also, find the length of the cycle, and the beginning of the cycle.

A
  • *Solution using a hash table :**
  • As we traverse, we store **something that’s unique about each node**, whether it be data(if data is guaranteed to be unique), or references to the node.
  • These references are **logical**, and not **physical addresses**, because if the nodes are spread acrosss multiple servers, you may think they’re the same even though they’re different.
  • Using hash table to see if “Have I seen this node before?”
  • *Another solution:**
  • “Coloring Technique” : As you go, color nodes you’ve seen before previously
  • Need to extend linked list data structure.
  • *Solution using fastRunner/slowRunner**
  • If the two runners are going the same direction, as long as one runner is faster than the other, they will meet.
  • *Common Errors**
  • checking if fast === slow in the beginning of the while loop, won’t work for linked lists that **only have one length**.
  • *Finding the length of the cycle**
  • Once we reach our anchor, where fast === slow, start a counter from 0, and when we reach the anchor again we’ll have the length of the cycle.

-

Finding the start of cycle.

  • Essentially we’re using the same logic for the problem of finding the **nth node from the end**.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
21
Q

Given an array of integers, rotate the array by N elements.

A

The algorithm for this is straightforward : 1. Reverse the array. 2. Reverse indices from 0 ~ n-1. 3. Reverse indices n ~ length-1 Example : [0,1,2,3,4,5] rotated by 3 is [3,4,5,0,1,2]. When you reverse the original list you get [5,4,3,2,1,0]. Then reversing from index 0 ~ 2 you get [3,4,5, 2,1,0]. Then we reverse from 3 to 5. Which results in [3,4,5,0,1,2]. Simple reversing using index function : const reverseArray = function(arr, start, end) { while (start < end) { //swapping let temp = arr[start]; arr[start] = arr[end]; arr[end] = temp; start++; end–; } };

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
22
Q

Given a string s, partition s such that every substring of the partition is a palindrome.

Return all possible palindrome partitioning of s.

For example, given s = “aab”,
Return

[
[“aa”,”b”],
[“a”,”a”,”b”]
]

A

Approach : While this might seem like a difficult problem, it’s just a backtracking solution. Follow the general pattern for solving backtracking problems.

KEY : The only things we have to worry about are :

  1. Creating substrings
  2. Checking if the substring created is a palindrome(Use helper function).
  3. It’s helpful if the palindromeChecker function takes two indices, start and end.
const isPalindrome = function(string, low, high) {
 while (low \< high) {
 if (string[low++] !== string[high--]) return false;
 }
 return true;
}
var partition = function(s) {
 const result = [];
const recurse = function(startIndex, soFar) {
 if (startIndex === s.length) {
 result.push(soFar.slice());
 }

for (let i = startIndex; i < s.length; i++) {
//only push to list if current substring(startIndex ~ i) a palindrome
if (isPalindrome(s, startIndex, i)) {
let currentSubstr = s.substring(startIndex, i+1);
console.log(‘currentSubstr :’, currentSubstr);
soFar.push(currentSubstr);
recurse(i+1, soFar);
soFar.pop();
}
}
};

recurse(0, []);
return result;
};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
23
Q

Implement pre-order traversal iteratively. Explain how it should be done.

A

Think about the smallest case!!

With pre-order we want to visit the root first, then its left chilren, then its right children.

The important idea here is to use a stack to hold the nodes, as well as the result array to store the results as we traverse pre-order style.

KEY : Since we want to visit root first, we push root values to result AS we traverse down to the left/right. There is no need to push as we pop back up!!! Meaning we only need to push ONCE.

Solution #1 : Start by pushing rootNode into stack. Then all we have to do is pop it, and if the node’s value exists, 1) examine it 2) push to stack node’s right 3) push to stack node’s left ***Note how we push right node first, even though in pre-order it’s . This is because when we pop from the stack, with LIFO leftNode will be popped first.

Solution #2 : Hint : Again, think about the simplest case… Key Point : With pre-order we need to store root’s value AS we traverse down - We use stack to keep track of our root nodes as we traverse down.

As long as node exists, we examine the value first(Since pre-order is root first). We store it in result

Then we push to the stack. We need to push every node we look at to the stack since it’s the only way we’ll be able to move back up(Think recursion).

If node is null, that means we can move up, so we pop from stack(we look at the parent). Then we examine the parent’s right sub-tree.(we’ve already looked at root, so no need to do any pushing here)

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
24
Q

Swap kth node from the beginning, with kth node from the end.

Test cases:

swapNodes(list({11, 4, 6, 7, 1, -99, 0, 2}), 2) ⇒ | 11 | -> | 0 | -> | 6 | -> | 7 | -> | 1 | -> | -99 | -> | 4 | -> | 2 | -> Nil

A

Approach : We can break the problem down into two phases :

  1. First we have to find the kth node from the beginning, and kth node from the end.
  2. We have to swap them.

To swap nodes in a linked list, we need access to the previous elements of the nodes. We will first connect the previous pointers to each other(nodes), then we will connect the nodes’ next pointers to each other’s nexts.

We account for the cases where prevOne or prevTwo might be null, in which we have to reset the heads.

Don’t forget to return the new head in the swapping helper.

const swapper = function(head, prevOne, nodeOne, prevTwo, nodeTwo) {

console.log(‘head :’, head);
console.log(‘prevOne :’, prevOne);
console.log(‘nodeOne :’, nodeOne);
console.log(‘prevTwo :’, prevTwo);
console.log(‘nodeTwo :’, nodeTwo);
if (!nodeOne || !nodeTwo) return;

//if prevOne is null, this means we're swapping the head node.
 if (!prevOne) {
 head = nodeTwo;
 } else {
 prevOne.next = nodeTwo;
 }
// k \> n-k
 if (!prevTwo) {
 head = nodeOne;
 } else {
 prevTwo.next = nodeOne;
 }
//swap!
 let temp = nodeOne.next;
 nodeOne.next = nodeTwo.next;
 nodeTwo.next = temp;

return head;
};

function swapNodes(pList, iK) {

if (!pList || iK <= 0) return;

let prevOne = null;
 let prevTwo = null;
 let temp = pList;
 let nodeOne = pList;
 let nodeTwo = pList;
//find the kth node from beginning first.
 while (iK \> 1 && temp) {
 iK--;
 prevOne = temp;
 temp = temp.next;
 }
//at this point, nodeOne is at kth node from beginning.
 nodeOne = temp;

//now we move nodeTwo(head) and temp together until temp reaches the end

while (temp && temp.next) {
prevTwo = nodeTwo;
nodeTwo = nodeTwo.next;
temp = temp.next;
}

 return swapper(pList, prevOne, nodeOne, prevTwo, nodeTwo);
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
25
Q

Given an integer n, and k, find the kth permutation in

A
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
26
Q

Sort a singly linked list using merge sort, ascending order.

ex) 1->5->6->3->4 // 1->3->4->5->6

A

Merge sort has three steps you need to know.

  1. Dividing the list into two.
  2. Merging the two lists together
  3. Recursive structure(Divide & Conquer). The way this is done is you call the function recursively on the split halves, firstHalf and secondHalf, and return the merged list each time.

Dividing potion : We use slow pointer, fast pointer method to find the middle of the list. But we initialize slow to head, and fast to head.next, so that when fast reaches the end of the list, slow is at the node previous to the midpoint. All we have to do is initialize slow.next to be returned, and then split the list into two by setting slow.next = null.

Merge step : A lot of details to be careful of here. First, handle the edge cases where either / both heads are null.

Then, find the newHead by comparing the values of headA and headB. This is the returned value, since we need to return the head of the newly merged(sorted) list. We will need to initialize another variable, sortedCurrentNode to advance through the sorted list.

Then, we iterate through headA and headB, appending to sortedCurrentNode each time.

Once we’re done, we can check if there are any nodes remaining in either headA, headB, and simply attach it to the end of sortedCurrentNode.

const splitIntoTwo = function(head) {
 //if list is length 0 or 1 just return the head
 if (!head || !head.next) return head;
let slow = head;
 let fast = head.next;

while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}

//at this point, slow will be at the midpoint. save slow’s next(Which is the secondHalf’s head) to a variable, and split the list by setting it to null afterwards

let secondHead = slow.next;
 slow.next = null;

return {firstHalf : head, secondHalf : secondHead};
};

const merge = function(l1, l2) {
 //first if either head is null, return the other
 if (!l1) return l2;
 if (!l2) return l1;
//first find the 'new' head by comparing l1's value and l2's value
 //\*\*\*DONT forget to move l1 and l2 after setting head.
 let newHead = null;
 if (l1.val \<= l2.val) {
 newHead = l1;
 l1 = l1.next;
 } else {
 newHead = l2;
 l2 = l2.next;
 }
//Need to save newHead HERE, to be returned later.
 let sortedNodeCurrent = newHead;
//iterate through l1 and l2, appending the larger node to newHead
 while (l1 && l2) {
 if (l1.val \<= l2.val) {
 sortedNodeCurrent.next = l1;
 l1 = l1.next;
 } else {
 sortedNodeCurrent.next = l2;
 l2 = l2.next;
 }
 sortedNodeCurrent = sortedNodeCurrent.next;
 }
//Edge cases! If either list is left over, just add it to the end of newHead
 if (l1) {
 sortedNodeCurrent.next = l1;
 } else if (l2) {
 sortedNodeCurrent.next = l2;
 }

return newHead;
};

const mergeSortedLinkedList = function(head) {
 if (!head || !head.next) {
 return head;
 }
let splitHeads = splitIntoTwo(head);
 let firstHead = mergeSortedLinkedList(splitHeads.firstHalf);
 let secondHead = mergeSortedLinkedList(splitHeads.secondHalf);
let merged = merge(firstHead, secondHead);
 return merged;
};
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
27
Q

Serialize/Deserialize a BST. What’s the definition of serializing something?

A

Serialize : Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment.

To serialize a BST, we will use an array as a stream - and in the stream we will store all the nodes using DFS, pre-order. We will use a MARKER(*) to denote null nodes. We use the * because using -1 means we can’t use -1 value BST Nodes, and using Number.MAX_VALUE sometimes results in stack overflow.

Serialize Approach : To serialize tree, initialize an array(will be stringified and returned in the end) as our stream, and initialize MARKER(*). Since we’re going pre-order, we handle the base case first, where if current node is null(Then we’ll push MARKER into stream). Otherwise, store current node’s value in stream, and recursively call subroutine on current node’s left, followed by right.

De-serialize Approach : We will first split the string to make it an array. In our subroutine, we wrap everything inside a try-catch block, since reading from our file(input) may result in an error. We shift the first element in our stream, and examine it - if it’s our MARKER, return null. Otherwise, we create a new BSTNode with the value. Then we assign node.left to subroutine(stream), and node.right to subroutine. We don’t forget to return the node after the recursive calls.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
28
Q

Given a sorted, rotated array without duplicates, find out how many times it was rotated

A

Naive solution : Use linear search to find minimum element. Index of minimum gives us the # of rotations(Think about it). We can use a modified version of binary search to cut this down into O(log N) time. Key Point : What unique property does the minimum ‘pivot’ have? Also, what allows us to use binary search? The fact that 1/2 of the array will ALWAYS be sorted at a given time. However, if mid isn’t pivot, we know that if left half of the array is sorted, pivot cannot exist in the left half. ex : [3,4,5,6,0,1,2], mid is 6. Mid isn’t pivot, so we look at left half. It’s sorted, which means pivot can’t exist in the left half. So we have to go right. So this allow us to discard 1/2 of the array every single time. ————————————————————– The algorithm : Case 1 : Array[low] <= Array[high] : This means the array is already sorted, return low(Thats the minimum) Case 2 : Check if array[mid] is a ‘pivot’. If so, return mid. Pivot’s unique property is that its previous and next elements are both greater than it. **When checking next and previous, make sure to use modulo function to wrap around the list. previous = (mid -1 + N) % N; Case 3 : Right half is sorted, so search the left half Case 4 : Left half is sorted, search right half ——————————————————————– Finding prev and next from mid(we have to make sure they wrap around the array) : next = (mid+1) % arr.length prev = (mid-1 + arr.length) % arr.length

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
29
Q

Given an integer N, find the total number of ways to express it in sums of 1, 2, or 3.

Ex) If we are given 4,

1 + 1 + 1 + 1

1 + 1 + 2

1 + 2 + 1

2 + 1 + 1

2 + 2

1 + 3

3 + 1

In total, there are seven different ways to express 4 in sums of 1, 2, or 3.

A

So we start by defining D[n], which is the total # of ways a number can be expressed using 1, 2, or 3. If we express N in sums of different numbers, x + y + …. + z = N This last number z can be 1, 2, or 3. 1) x + y + …. + 1 = N N-1 + 1 = N So the sum of all the numbers before the last number is N-1. Therefore, the # of different ways to generate N-1 becomes D[n-1].

Since we need to consider all three possibilities, D[n] = D[n-1] + D[n-2] + D[n-3].

Just consider 4 as an example.

BOTTOM-UP:

const sumOfOneTwoThree = function(n) {
 const memo = {};
 memo[1] = 1;
 memo[2] = 2;
 memo[3] = 4;
for (let i = 4; i\<= n; i++) {
 memo[i] = memo[i-1] + memo[i-2] + memo[i-3];
 }

return memo[n];
};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
30
Q

Given a singly linked list and the head of a singly linked list, return Nth from last node

A

/*
Approach 1 : Since we don’t want to end up doing two passes to find the length of the list, we set up two pointers, we can think of them like a stick, but with length N. Once the end of the stick reaches the tail(null), the start pointer will be the node we want.

First clarify with the interviewer whether if n===1, it’s the TAIL node(last node), or first node from last(second to last node)

KEY : Need to handle two edge cases :

  1. When n is negative or head doesn’t exist
  2. N is out of bounds, it’s greater than the length of the linked list. For #2, just decrement N by 1 at each point when moving the end pointer. Then check if N is equal to 0. If it’s not, then we know it’s out of bounds!

*/

const getNthFromLast = function(head, n) {
 //if n is less than 1 or head is null, return null
 if (n \< 1 || head === null) return null;
let start = head;
 let end = head;
//move end to the right, as long as
 while (end && n \> 0) {
 end = end.next;
 n-=1;
 }
//at this point, if n is not 0 this means N is greater than
 //the length of the list.
 if (n !== 0) {
 return null;
 }
//stop when end node is the tail node(its next is null)
 while (end !== null) {
 start = start.next;
 end = end.next;
 }

return start;
};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
31
Q

Implement a circular queue that manages array space efficiently.

A

Our interface will look like this :

  • function CircularQueue(capacity) {*
  • this.buffer = new Array(capacity);*
  • this.size = capacity;*
  • this.headIndex = -1;*
  • this.tailIndex = -1;*
  • }*
  • CircularQueue.isFull()*
  • CircularQueue.enqueue(item)*
  • CircularQueue.dequeue()*
  • CircularQueue.peek()*
  • —————————————————————————-*

Using -1 to represent an empty queue is helpful, because if we initialize both at 0, detecting whether or not the queue is empty or full will be indistinguishable.

By that, I mean to detect if the queue is empty, we check if headIndex === tailIndex. To detect if the queue is full, let’s say we’re enqueuing in a way that’s buffer[tail++] = item. Then after filling out the last space, headIndex === tailIndex. So we have no way of distinguishing between empty or full.

To remedy this, what we can do is just leave one space blank in the end, so that to detect if the queue is full, we can do So if (tailIndex + 1) % this.size === headIndex, we know our queue is full. We can check whether tailIndex and headIndex are right next to each other.

Enqueue() :

- First check if queue is full.

  • Then find the new tailIndex by doing this.tailIndex = (this.tailIndex + 1) % this.size, and then add the new element at that index.
  • If this is the first element in the queue, we need to readjust the headIndex to 0.

Dequeue() :

  • First check if queue is empty.
  • Then hold a reference to the item at headIndex to be returned later.
  • We first check if dequing makes the queue empty, by checking if headIndex === tailIndex, in which case we readjust both of them to -1.
  • Otherwise we readjust headIndex, and set element at headIndex to null.

Helpful : http://www.mathcs.emory.edu/~cheung/Courses/171/Syllabus/8-List/array-queue2.html

32
Q

Implement next permutation, which rearranges numbers into the lexicographically next greater permutation of numbers.

If such arrangement is not possible, it must rearrange it as the lowest possible order (ie, sorted in ascending order).

The replacement must be in-place, do not allocate extra memory.

1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

A

Let’s say we have 1, 3, 6, 5, 4

The next permutation in lexicographical order is 1,4,3,5,6. How did I know that?
Well I examined the number right to left looking for an increasing sequence( 3 -> 6). I know that this is where number swaps will happen to find adjacent permutations.

So step 1 : Go right to left, trying to find A[i-1] <= A[i]. We are finding the first decreasing sequence(right -> left). That way we know which point our closest permutations will start from.

Then, we search right to left, trying to find the number that’s JUST larger than A[i-1]. This gives us the next number in the lexicographical sequence. This is because that’s the number that will come ‘NEXT’ in lexicographical order and replace the number A[i]. This number will exist on the right side of A[i-1].

Step 2 : Go right to left, trying to find A[j] <= A[i-1], as long as j >= i. So in here we’re trying to find the largest element that’s SMALLER than A[i-1]. The maximum element to the right of A[i-1].

Step 3. Swap A[i-1] and A[j].

Step 4. Since we picked out the maximum to the right of A[i-1], the sequence to the right of A[i-1] will be in decreasing order. We simply REVERSE elements from A[i] to make it increasing order.

EDGE CASE : If we are encountered with the last permutation(reverse-sorted sequence like 6,5,4,3,2,1), we want the answer to be 1,2,3,4,5,6. So we don’t do anything until the reverse step, so we add a conditional statement to the swaps and checking for j.

——————————————————————————————————————-

*const swap = function(list, a, b) {
 //we take care of -1 by adding list's length, this takes care of the last permutation edge case
 let temp = list[a];
 list[a] = list[b];
 list[b] = temp;*
  • }*
  • const nextPermutation = function(nums) {*
*let i = nums.length-1;
 let j = nums.length-1;*
*//this loop breaks until we find nums[i-1] \<= nums[i]
 while (i \> 0) {
 if (nums[i-1] \<= nums[i]) {
 break;
 }
 i--;
 }*
  • // if (i <= 0) return false; //This is the LAST permutation since i is the largest number*
  • if (i > 0) {*

while (j >= i) {
if (nums[j] >= nums[i-1]) {
break;
}
j–;
}

*//swap only if i is greater than 0
 //This means we only swap if it's not the LAST permutation, since for the last permutatio we don't want to do anything until the reversing part.
 swap(nums, j, i-1);
 }*
  • console.log(‘i : ‘, i);
    console. log(‘j :’, j);*

//reset j
j = nums.length-1;
//reverse from i till end
while (i < j) {
swap(nums, i, j);
i++;
j–;
}
};

*/

33
Q

Given root of a binary tree, delete any subtrees whose nodes sum up to zero.

A

Basic Approach : We will use recursive solution, post-order depth-first traversal. Post-order is perfect since our basic logic is after parsing left subtree, and right subtree, we can then sum them together and return the root value as our recursive case.

What I struggled with : I knew that in the recursive case I’d have to return the value of the root somehow, and in the base case return 0 if the root is null. BUT when writing a recursive case, assume you have a recursive function that does what you’re looking for first. So after writing my base case, I initialize the two key variables, leftTreeSum and rightTreeSum. I know that if leftTreeSum is null, I set the left pointer to null, and vice versa for right pointer(I do this assuming everything else works).

In the end though, I know that I have to return the sum of three values(leftSum + root + rightSum).

And I use a main method to call the recursive function.

const deleteZeroRec = function(root) {
 if (!root) {
 return 0;
 }
let leftTreeSum = deleteZeroRec(root.left);
 let rightTreeSum = deleteZeroRec(root.right);

if (leftTreeSum === 0) {
root.left = null;
}

if (rightTreeSum === 0) {
root.right = null;
}

return root.val + leftTreeSum + rightTreeSum;
};

const deleteZeroSumTrees = function(root) {
 if (root) {
 let sum = deleteZeroRec(root);
 if (sum === 0) {
 root = null;
 }
 }
 return root;
}
34
Q

Check if a tree is symmetric(They’re mirrors of each other). Implement both recursively and iteratively.

A

Hint : When thinking about the recursive solution, recognize that if root isn’t null, all we have to worry about are the left and right children. So our subroutine will therefore take leftTree, and rightTree as its parameter. Then build up the base cases from there.

KEYS for recursive solution : Although the problem only gives you root as input, our subroutine will need to check left and right trees, so use those as arguments.

KEYS for iterative solution : We have to add left and right to queue at the same time, and dequeue both at the same time. When we push, we push them in ‘symmetric order’.

Think about what a symmetric tree actually means. If the root is same, and its left and right are the same, its grandchildren have to be equal as such : (left-right === right-left)

Recursive Solution : base case : If root is null, return true subroutine checkSymmetry takes leftTree, rightTree as parameters.

Case 1 : They’re both null, return true.

Case 2 : One of them is null, return false.

Case 3 : They both exist. Return (leftTree.val === rightTree.val) && checkSymmetry(leftTree.left, rightTree.right) && checkSymmetry(leftTree.right, rightTree.left)

————————————————————————–

Iterative Solution : Use BFS and queue. Initialize queue, push root’s left, and root’s right. Initialize two variables, t1 and t2. Iterate as long as queue isn’t empty - dequeue from queue twice, assigning them to t1 and t2.

Case 1 :If they’re both null, continue.

Case 2 : If one of them is null, return false immediately.

Case 3 : They both exist, if their values are unequal return false.

Case 4 : This is the most important bit. Push t1’s left into the queue, then t2’s right. Then vice versa.(So that we preserve the correct order of trees being pushed in).

const isSymmetric = function(root) {
 if (!root) return true;

const queue = [];

queue.push(root.left);
queue.push(root.right);
let t1, t2;

while (queue.length) {
 t1 = queue.shift();
 t2 = queue.shift();
 //theyre both null, continue
 if (t1 === null && t2 === null) continue;
//one of them is null return false
 if (t1 === null || t2 === null) return false;
//if theyre not null, and value are unequal return false
 if (t1.val !== t2.val) return false;

//push t1’s left, then t2’s right, then vice versa
queue.push(t1.left);
queue.push(t2.right);
queue.push(t1.right);
queue.push(t2.left);
}

return true;
};

35
Q

Implement a function that generates all permutations of a string. EX) permute(‘abc’) // [‘abc’, ‘acb’, ‘bac’, ‘bca’, ‘cab’, ‘cba’]

A

For our recursive function, we’ll have two parameters, USED parameter to denote all letters used so far, and REMAINING to denote all letters left to use.

For ex ) if used = ‘a’, and remaining = ‘bc’ then we know that we still have to use the remaining letters.

So our base case will be if we’ve used up all letters, or there are no remaining letters left, in which case we’ll just push to result.

Otherwise, we need to do three things :

  1. choose a new character from the remaining letters list and use it.
  2. Since we’ve used that character up, take it out from the remaining list.
  3. Call the function recursively with the new used/remaining values.
  • *Key here is that in each recursive function, we need to use a loop to iterate over the remaining letters since we want to use all of them.**
const permuteString = function(string) {
 const result = [];
const generate = function(used, remaining) {
 if (used.length === string.length || remaining.length === 0) {
 result.push(used);
 } else {
 for (let i = 0; i \< remaining.length; i++) {
 let newCharToUse = used + remaining[i];
 let newRemaining = remaining.slice(0, i) + remaining.slice(i+1);
 generate(newCharToUse, newRemaining);
 }
 }
 };
 generate('', string);

return result;
};

36
Q

Implement quicksort, paying careful attention to the partition method

A

In quicksort, partition really is the meat of the problem. The recursive function is simple. Make sure only to do something if low < high(So we know we have the correct range).

You simply call the partition function that returns the pivotIndex, and you then divide & conquer by splitting the list in half according to the pivotIndex.

Now let’s look at the partitioning function since it requires a bit of work :

The partitioning function will take the list, start, and end index as parameters.

We’ll pick the pivot as the rightmost element.
Then, HERE’s the confusing part : We initialize pivotIndex at start. Later on, this will be the index where pivot will be placed.

Then we loop i from start till end-1(Don’t go till end since the last element is our pivot!), and comparing currentElement with pivot.

If currentElement is less than or equal to pivot, we swap element at i(currentIndex) with element at pivotIndex. What this does is push all the elements less than pivot to the left of pivotIndex. We also increment pivotIndex by 1.

When we’re done with this for loop, we’ll have smaller elements on the left, and bigger elements on the right. Simply swap pivotIndex with high index to put the pivot in its correct place and return pivotIndex.

const swap = (list, a, b) =\> {
 const temp = list[a];
 list[a] = list[b];
 list[b] = temp;
 return list;
};
const quickSort = function(list, low, high) {
 low = low || 0;
 high = high || list.length-1;

if (low < high) {
let partitionIndex = partition(list, low, high);
quickSort(list, low, partitionIndex - 1);
quickSort(list, partitionIndex+1, high);
}

return list;
};

const partition = function(list, low, high) {

let pivot = list[high];
 //initialize pivotIndex at low
 let pivotIndex = low;

//loop i from start to high-1(Since high is pivot),
//and if currentElement is LESS than pivot,
//swap it with pivotIndex and increment pivotIndex
//this allows all elements smaller than pivotIndex to go to the left of pivotIndex
//the idea is we want to push all lesser elements than pivot into the LEFT of pivotIndex
for (let i = low; i <= high-1; i++) {
if (list[i] <= pivot) {
swap(list, i, pivotIndex);
pivotIndex++;
}
}
swap(list, pivotIndex, high);
console.log(‘pivotIndex : ‘, pivotIndex);
return pivotIndex;
};

37
Q

Design an algorithm and write code to find the first common ancestor of two nodes in a binary tree(Not necessarily a BST). Avoid storing additional nodes in a data structure.

A

Method 1 : Find path from root => p, path from root => q.
Traverse both paths, find the mismatch point. The node RIGHT BEFORE the mismatch point is the common ancestor.
O(n) time, needs two traversals.

Method 2: Cleaner, one traversal with O(n) using DFS(post-order). Important thing here to note is that we return two values : root, or null. At each point, we simply check if root is either p, or either q, in which case we return the root. We then initialize two additional variables(left, right), which will be assigned to the recursive calls. Those will be ‘bubbled up’ from the bottom, and we know that if they’re both non-null, we’ve found our LCA. Otherwise, we simply return whichever one from either left, right that isn’t null.

How does this change if it’s a BST? It’s even easier.

The LCA is the node where our two nodes will have diverged as children.

So if it’s a BST, that means that the common ancestor of two values will be one that’s in the range between two nodes(inclusive). So at each step, we simply check if the root is less than both nodes(go right), if the root is greater than both nodes(go left). Otherwise, we’ve found our LCA.

Check edge cases, like 28, 30 where 30 is LCA. So one of them is the LCA of both fo them is an edge case.

Binary Tree Solution

const dfs = (root, p, q) =\> {
 if (!root) return null;
//if either node is found, return root.
 if (root === p || root === q) {
 return root;
 }
let leftLCA = dfs(root.left, p, q);
 let rightLCA = dfs(root.right, p, q);
//Case 1: We've found a common ancestor
 if (leftLCA !== null && rightLCA !== null) {
 return root;
 }
//if leftLCA has been found, return it. otherwise, return rightLCA.
 return leftLCA !== null ? leftLCA : rightLCA;
 };
var lowestCommonAncestor = function(root, p, q) {
 if (root.val \< p.val && root.val \< q.val) {
 return lowestCommonAncestor(root.right, p, q);
 } else if (root.val \> p.val && root.val \> q.val) {
 return lowestCommonAncestor(root.right, p, q);
 }
 return root;
};
38
Q

Merge sort a linked list, given a pointer to its head.

A

With merge sort, we only need to do three things :

  1. Divide up the list in half
  2. Recursively call function on each half
  3. Merge the two halves together.

Two problems with merge-sorting a linked list vs. an ordinary array : Since we don’t have indices, we can’t find the middle node easily. So what we’ll do is use the fast/slow pointer trick to find the middle node(Much more elegant than getting length of the linked list).

So we’ll have two helper functions that are a bit tricky to implement :

#1. SplitInHalf :

Before calling splitInHalf, in our main function we’ll initialize an object that contains firstHead, and secondHead(heads for each of the splitted linked lists). splitInHalf will then take the object as a parameter, and assign each new head accordingly.

In splitInHalf, we make sure to cover edge cases where head is null, or there is only one node. Also, after splitting the lists in half, we make sure to terminate the first list’s end by setting slow.next = null.

  1. The merge function : Basic idea is we set up a newHead, the larger node between headA and headB. And then we’ll traverse both listA and listB, appending the larger element to newHead.
    * *Don’t forget to check if there are any nodes leftOver in listA, listB,** and attach the leftover node to merged list. We’ll also return newHead.
const mergeSortLinkedList = function(head) {
 //if list only has single node, no need to sort it
 if (!head || !head.next) {
 return head;
 }

let newHeads = {
firstHead : null,
secondHead : null
};

splitInHalf(head, newHeads);

newHeads.firstHead = mergeSortLinkedList(newHeads.firstHead);
newHeads.secondHead = mergeSortLinkedList(newHeads.secondHead);

return merge(newHeads.firstHead, newHeads.secondHead);
};
const splitInHalf = function(head, newHeads) {
 if (!head) {
 newHeads.firstHead = null;
 newHeads.secondHead = null;
 return;
 }
//only one element in the list
 if (!head.next) {
 newHeads.firstHead = head;
 newHeads.secondHead = null;
 } else {
 //use technique of moving slow and fast
 let slow = head;
 let fast = head.next;
//when we're done, slow will be pointing to the middle element in the list
 while (fast) {
 fast = fast.next;
 if (fast !== null) {
 slow = slow.next;
 fast = fast.next;
 }
 }
 }
newHeads.firstHead = head;
 newHeads.secondHead = slow.next;
 //Terminate the first list by setting the end to null
 slow.next = null;
};
const merge = function(headA, headB) {
 //if one of them is null, return the other
 if (!headA) {
 return headB;
 }

if (!headB) {
return headA;
}

//Set up newHead, and set it to whichever is larger between headA and headB
 let newHead = null;

if (headA.val <= headB.val) {
newHead = headA;
} else {
newHead = headB;
}

//Since we have the head set up, we can now append elements to the list
 let newCurrentNode = newHead;
while (headA && headB) {
 //initialize tempNode that will store the larger of headA/headB
 let tempNode = null;

if (headA.val >= headB.val) {
tempNode = headA;
headA = headA.next;
} else {
tempNode = headB;
headB = headB.next;
}

newCurrentNode.next = tempNode;
newCurrentNode = newCurrentNode.next;
}

//Edge cases where there are nodes leftOver in listA, listB
 if (headA) {
 newCurrentNode.next = headA;
 } else if (headB) {
 newCurrentNode.next = headB;
 }

return newHead;
};

39
Q

Implement a fn to check if a BST is balanced
‘balanced’ = height of two subtrees of any node never differ by more than one

A

Key Point Here : We’ll need to use depth-first-traversal since it’ll hit the leaf nodes first, giving us depths easily. The key point is that we keep track of all the depths, and can short-circuit out false if :

  1. There are more than two depths

2. The difference of two depths are more than 1.

So we perform these checks at every iteration!

Another important thing to remember is that we only store the depths when we reach a leaf node. It might be easier to implement an iterative-version of DFS, since we don’t have to worry about returning from recursive functions.

Recursive Solutions :

const balancedCheckRecurse = function(root, currentDepth, depths) {
 if (!root) return true;
//only add depth for leafs
 if (!root.leftChild && !root.rightChild) {
 if (depths.indexOf(currentDepth) === -1) {
 depths.push(currentDepth);
 }

//Check for balance
if (depths.length > 2 || Math.abs(depths[0] - depths[1]) > 1) {
return false;
}
} else {
return (
balancedCheckRecurse(root.left, currentDepth + 1, depths) &&
balancedCheckRecurse(root.right, currentDepth + 1, depths)
);
}
};

40
Q

Implement a method to print all permutations of a given string.

A

This one is ALWAYS. CONFUSING.

If the input is an array(list), the problem is MUCH trickier - If it’s a string, when we select one character and use it, since strings are immutable we can generate new strings so we don’t have to worry about modifying the original string. But with arrays, we’re accessing the reference of the same list so the approach has to be a bit different.

————————————————————————————-

We will use recursion to generate all permutations - In our recursive function, we will have two parameters, used, and remaining. We will hit the base case when we’ve used up all letters, or there are none remaining, which is when we will store the current string we have.

Otherwise, we need to do three things in our recursive function :

  1. Choose a new character from our remaining list and ‘use’ it
  2. Since we’ve used that character, remove it from the remaining list.
  3. Call the function recursively with the updated used, remaining values.
const permuteString = function(string) {
 const result = [];
const generate = function(used, remaining) {
 if (used.length === string.length || remaining.length === 0) {
 result.push(used);
 } else {
 for (let i = 0; i \< remaining.length; i++) {
 let newString = used + remaining[i];
 let newRemaining = remaining.slice(0, i) + remaining.slice(i+1);
 generate(newString, newRemaining);
 }
 }
 };
 generate('', string);

return result;
};

ALTERNATIVE APPROACH that uses SWAPS.

Let’s say we’re generating permutations of [‘a’, ‘b’, ‘c’]. We would fix a, then we can either generate permutations of ‘b’,’c’, or ‘c’, ‘b’. So we iterate through our remaining letters, swapping characters with the currentIndex of the recursion. We have to put the swaps BACK to their original form afterwards though.

KEY : When reaching base case, we need to store a COPY of our array. I had an error where I was pushing the reference of the same array, and it would just give me [[1,2,3], [1,2,3], [1,2,3],[1,2,3], [1,2,3], [1,2,3]]. We want to make a copy so we can store the swapped states.

var permute = function(A){
 var solutions = [];

var generate = function(currentIndex, input) {

if (currentIndex === A.length) {
solutions.push(input.slice());
return;
}

for (var i = currentIndex; i \< A.length; i++) {
 //swap characters and call recursively
 var temp = input[currentIndex];
 input[currentIndex] = input[i];
 input[i] = temp;

generate(currentIndex+1, input);

input[i] = input[currentIndex];
input[currentIndex] = temp;
}
}

generate(0, A);
return solutions;
}

41
Q

Reverse a linked list, iteratively and recursively.

A

Solution #1. Iteration.

The idea obviously is to traverse the linked list, reversing it one node at a time. So we have to connect the current node back to the previous one, but be able to move to the third ‘next’ node. So we’ll need three pointers, prev and current and next.

We initialize prev and next to null, and current to head.

While current isn’t null, we first save next to current.next(This way we know which node to go the next iteration).

Then we reverse!

current.next = prev;

Then the important part is the order in which we update our variables.

prev = current;

current = next;

Solution #2. Recursion

So the key thing here is to know that the recursive solution uses a stack. We will push each node into one slot into the stack.

Key insight here is that the only thing we have to return is the ‘new head’, or the tail node in the original list. Once we modify the links, we still have access to the previous node in the linked list. So all we have to do is reverse the pointer by adjusting head’s next’s next pointer back to head. Also, we set head’s next to null(Because this will take care of the end of the new reversed list, where we want the tail’s next then becomes null).

const reverseLinkedListRec = function(head) {
 //return the last node, this will be our new head
 if (!head || !head.next) {
 return head;
 }
//new head that will be returned in the end
 var new\_head = reverseList(head.next);
//reverse first
 head.next.next = head;
//set head's next to null.(Fixed later in the next iteration)
 head.next = null;

return new_head;
};

42
Q

Find the “next” node(In-order successor) of a BST.

A

First clarifying question to ask is : Do we have access to the parent?

Either way, we need to look at how in-order traversal works and reverse-engineer it. We know that in-order goes <left> <node> <right><strong>&gt;. </strong>So assuming we've found our node N, if N has a rightchild, the successor is the <strong>leftMost child in the right-subtree.</strong> If N has no rightchild, we have to return its parent. <strong>Except, if N is <em>already</em> on the far right of the tree, then there is no successor. </strong>So we have to return null.</right></node></left>

A simple, recursive way is to ‘remember’ the successor and pass it down to recursive functions. We only re-set the successor when going left(We pass down the parent since it’s the potential successor). Successor is initialized as null.

O(logN) time and space.

const findMin = function(root) {
 if (!root.left) {
 return root;
 } else {
 return findMin(root.left);
 }
};
const inOrderSuccessor = function(root, node, successor) {
 //initialize successor as null.
 successor = successor || null;
//edge case
 if (!root) return null;
if (root.val \< node.val) {
 return inOrderSuccessor(root.right, node, successor);
 } else if (root.val \> node.val) {
 return inOrderSuccessor(root.right, node, root);
 } else if (root.val === node.val) {
 if (root.rightChild) {
 return findMin(root.rightChild);
 } else if (root.right === null) {
 return successor;
 }
 }
};
43
Q

We are given a Binary Search Tree(BST) and a node number n. We have to find the node with nth highest value.

A

Basic Approach : Do in-order, since that will give us BST in order, but reverse. So start by going right, and when we pop from stack, go left.

Hint : When doing reverse in-order traversal, we’ll need a counter to keep track of the count of elements we’ve examined. Only increment count after popping from stack and having examined the current node. Do NOT increment count as we traverse down(Since we’re only adding elements to the stack to be examined later).

const nthLargestBSTNode = function(root, n) {
 if (!root || n \< 1) return null;
const stack = [];
 let currentNode = root;
 let count = 1;

while (stack.length || currentNode) {

if (currentNode) {
stack.push(currentNode);
currentNode = currentNode.right;
continue;
}

currentNode = stack.pop();

//check if count equals currentNode ONLY after popping from stack, since that's when we examine each node.
 if (count === n) {
 return currentNode;
 }

currentNode = currentNode.left;

//increment count since we're looking at new element.
 count++;
 }

return null;
}

44
Q

Given a set of candidate numbers (C) (without duplicates) and a target number (T), find all unique combinations in C where the candidate numbers sums to T.

The same repeated number may be chosen from C unlimited number of times.

Note:
All numbers (including target) will be positive integers.
The solution set must not contain duplicate combinations.
For example, given candidate set [2, 3, 6, 7] and target 7,
A solution set is:
[
[7],
[2, 2, 3]
]

A

We use backtracking to try every single possibility, and since we can use each number unlimited number of time.
Things we need to keep track of in our recursive function :

  1. Remaining(We keep state of the remaining to check if the number we’ve chosen so far add up to target). So in each recursive function we compute remaining - currentElement. We reach our base case when remaining is 0.
  2. Everything else is the same in our general form for solving backtracking solutions, we use a loop to iterate over remaining options, keep track of startIndex.
var combinationSum = function(candidates, target) {
 const result = [];
//recursive routine where each time we choose an element, we subtract it from our target
 //base case is if remaining === 0.
 //iterate through remaining options and choose an element.

const recurse = function(remaining, soFar, startIndex) {
if (remaining < 0) {
return;
} else if (remaining === 0) {
result.push(soFar.slice());
}
for (let i = startIndex; i < candidates.length; i++) {
let currentElementToTry = candidates[i];
soFar.push(currentElementToTry);
//i, NOT i+1 because we can re-try the same element again and again
recurse(remaining - currentElementToTry, soFar, i);
soFar.pop();
}
}

recurse(target, [], 0);

return result;
};

45
Q

Given the root node of a binary tree, print nodes forming the boundary (perimeter).

A

Basic Approach : Divide the problem into three different parts. We first want to print the left perimeter, then the leaf nodes, then the right perimeter. For the right perimeter we print them bottom-up order.

/*
Approach :
1. Print the root node
2. Print left-boundary in top-down order
3. Print leaf nodes in left-right order
4. Print right-boundary in bottom-up order using a stack.
*/

So we’ll have a main function that calls three of our helpers.

printLeftPerimeter : Use simple while loop to traverse down, at every step check if currentNode has a leftChild or a rightChild(Since if leftSubtree doesn’t exist, we have to go Right - that will be the left perimeter of the entire tree). Break if it’s a leaf node.

printLeafNodes : Use recursive function to do a DFS in-order traversal. If we encounter a leaf node, print it.

printRightPerimeter : Use stack to hold all values in the right perimeter. Then pop from stack and print them altogether.

const printLeftPerimeter = function(root) {
 while (root) {
 let current = root.val;
if (root.left) {
 root = root.left;
 } else if (root.right) {
 //if left doesnt exist but right does, go right since that will be the 'left perimeter'
 root = root.right;
 } else {
 //leaf node
 break;
 }

console.log(current + “ “);
}
};

const printLeafNodes = function(root) {

if (root) {
printLeafNodes(root.left);
if (!root.left && !root.right) {
console.log(root.val + “ “);
}
printLeafNodes(root.right);
}
};

const printRightPerimeter = function(root) {
 //use a stack since we want to print bottom-top
 let stack = [];
while (root) {
 let currentVal = root.val;

if (root.right) {
root = root.right;
} else if (root.left) {
root = root.left;
} else {
break;
}
stack.push(currentVal);
}

//pop from stack and print
 while (stack.length) {
 console.log(stack.pop() + " ");
 }
};

const displayTreePerimeter = function(root) {

if (root) {
 //print root's value first
 console.log(root.val + " ");
//print left perimeter
 printLeftPerimeter(root.left);
//if it has children
 if (root.left || root.right) {
 printLeafNodes(root);
 }
//print right perimeter
 printRightPerimeter(root.right);
 }
};
46
Q

Find the total # of ways to fill up a 2*N rectangle with 1x2, 2x1 tiles.

A

Drawing it out will seriously help.

Start by building up from smallest cases, from 0 ~ 3.

If we draw out a rectangle, we can see that we have two cases, where the edge of the rectangle can be filled with ONE 1x2, or with two 2x1 blocks stacked horizontally.

So D[n] can be divided into two cases :

1) D[n-1] where the width is (n-1) and height is 2. D[n-1] = 2 * n-1

2) D[n-1] where the width is (n-2) and height is 2. D[n-2] = 2 * n-2 So we add the 1) and 2) to get D[n]. D[n] = D[n-1] + D[n-2].

47
Q

Given n pairs of parentheses, write a function that generates all of the possible combinations of regular parentheses, sorted in lexicographical order.

Example

For n = 4, the output should be

generateParentheses(n) = [”(((())))”, “((()()))”, “((())())”, “((()))()”, “(()(()))”, “(()()())”, “(()())()”, “(())(())”, “(())()()”, “()((()))”, “()(()())”, “()(())()”, “()()(())”, “()()()()”]

A

KEY : We can only use a closing bracket ’)’ if the # of remaining opening brackets > # of remaining closing brackets. For example, ( ) ) would be false since we used an extra closing before using an extra opening. So it’s more intuitive to start the count of opening/closing brackets from 0 and increment them each time.

const generateParenthesis = function(n) {
 const result = [];

const generate = function(soFar, open, closed, max) {

//we're done, return
 if (soFar.length === max\*2) {
 result.push(soFar);
 return;
 }

if (open < max) {
generate(soFar + ‘(‘, open+1, closed, max);
}

if (closed < open) {
generate(soFar + ‘)’, open, closed+1, max);
}
};

generate(‘’, 0, 0, n);
return result;
};

48
Q

Search a given number in a rotated array that has been rotated by some arbitrary number. Return -1 if the number isn’t found.

A

If we look at the array in example closely, we notice that at least one half of the array is always sorted. We can use this property to our advantage. If the number n lies within the sorted half of the array then our problem is basic binary search. Otherwise discard the sorted half and keep examining the unsorted part.

Example : [3,4,5,6,0,1,2] // target === 0.

Case 1 : Right half is sorted(array[mid] < array[high])

Sub-case : Check if target lies in this range (array[mid] < target <= array[high]) If it does, check right half If it doesn’t, search left half

Case 2 : Left half is sorted(array[start] < array[mid])

Sub-case : Check if target lies in this range (array[low] <= target < array[mid]) If it does, check right half If it doesn’t, search left half

We don’t have to <= array[mid] <= because if it were equal to array[mid], we would’ve returned mid as per first conditional statement of binary search.

49
Q

Given two integers n and k, return all possible combinations of k numbers out of 1 2 3 … n.

Make sure the combinations are sorted.

To elaborate,

  1. Within every entry, elements should be sorted. [1, 4] is a valid entry while [4, 1] is not.
  2. Entries should be sorted within themselves.

Example :
If n = 4 and k = 2, a solution is:

[
[1,2],
[1,3],
[1,4],
[2,3],
[2,4],
[3,4],
]
*/

A

We can use backtracking. Basically, DFS a binary tree where at each node, we can either SELECT the current element, or NOT SELECT it.

const combinationsTwo = function(A, B) {
 const result = [];
 //first generate list
 const list = [];
 for (let i = 1; i \<= A; i++) {
 list.push(i);
 }

const recurse = function(index, soFar) {

if (soFar.length === B) {
result.push(soFar.slice());
return;
}

if (index === list.length) {
return;
}

let currentElem = list[index];

soFar.push(currentElem);
recurse(index+1, soFar);
soFar.pop();
recurse(index+1, soFar);
}

recurse(0, []);

return result;
};

So we have two recursive calls, one where we select the current element, and one where we don’t select it. We make sure to use push() and pop() to have O(1) operations for choosing and un-choosing each option. And once we reach the base case, we make sure to store a copy of the list, not the reference to the list itself.

50
Q

Connect same-level siblings in a BST. Use .next pointers to denote sibling relationships. Connect to null if no sibling exists.

A

Basic Approach : Utilize depth-first traversal, and the idea is to connect the next-level siblings. So if we’re at level x, we connect next level siblings on level x+1 left to right. We return the next-level ‘head’(The leftmost node in the level) to be used in the next iteration.

Important Points : There are two key variables we need to keep track of, nextLevelHead and prevConnected. At every iteration, we first check if nextLevelHead is set - if it isn’t, we set it to the leftMost node in the next level. At every iteration, we also update prevConnected’s next pointer to point to leftMost node in the next level. We also make sure to update prevConnected before we’re done.

We handle two cases - 1) Where node has both left and right children, and 2) Where node only has one child.

When we’re done with one iteration, we simply go to current’s next.

/*Given a binary tree, connect its siblings at each level. Let’s consider the following binary tree as an example.*/

/*
Basic Approach :
1. Traverse nodes at level ‘x’ from left-> right while connecting siblings at level ‘x+1’
2. Now that we’ve finished connecting ‘x+1’, use the ‘head’(left-most) node at ‘x+1’ to apply the same thing to ‘x+2’

*/

const connectSameLevelSiblings = function(root) {
 if (!root) return null;

root.next = null;

while (root) {
root = connectNextLevel(root);
}
}

const connectNextLevel = function(root) {
 let currentNode = root;
 let nextLevelHead = null;
 //use prev to keep track of previously connected nodes
 let prev = null;
while (currentNode) {
 //Case 1 : Both left and rightChild
 if (currentNode.left && currentNode.right) {
 //check if nextLevelHead exists. Set it to current's leftChild
 if (!nextLevelHead) {
 nextLevelHead = currentNodeleft;
 }
//connect left to right
 currentNode.left.next = currentNode.right;
//reset prev's next pointer. If it exists, connect it to current's left
 if (prev) {
 prev.next = currentNode.left;
 }
 //since we're connecting left-\>right, prev will be the right node.
 prev = currentNode.right;
 }
 //Case 2 : Only leftChild
 else if (currentNode.left) {
 if (!nextLevelHead) {
 nextLevelHead = currentNode.left;
 }

if (prev) {
prev.next = currentNode.left;
}

//reset prev to currentNode's right
 prev = currentNode.left;
 }
 else if (currentNode.right) {
 //Case 3 : Only rightChild
 if (!nextLevelHead) {
 nextLevelHead = currentNode.right;
 }

if (prev) {
prev.next = currentNode.right;
}

prev = currentNode.right;
}

currentNode = currentNode.next;
}

//Last node.
 if (prev) {
 prev.next = null;
 }

return nextLevelHead;
};

51
Q

Check if two words are anagrams with each other.

A

Clarify character set(ASCII). Then scan first word, recording count for each character code in the array. Scan second word, recording count for each character but instead decrement count. In the end, check if the character count array sums to 0.

const checkAnagrams = function(word1, word2) {

if (word1.length !== word2.length) return false;

const charSet = new Array(256);
 charSet.fill(0);
for (let i = 0; i \< word1.length; i++) {
 let currentCharCode = word1.charCodeAt(i) - 97;
 charSet[currentCharCode] += 1;
 }
for (let j = 0; j \< word2.length; j++) {
 let currentCharCode = word2.charCodeAt(j) - 97;
 charSet[currentCharCode] -= 1;
 }

for (let count in charSet) {
if (charSet[count] !== 0) {
return false;
}
}

return true;
};

52
Q

Find the length of the longest substring(contiguous) that has matching opening and closing parentheses. We only need length, not the substring itself. You may assume valid input.

console. log(maxLengthMatchingParens(“()()()”)); //6
console. log(maxLengthMatchingParens(“(((())()”)); //6
console. log(maxLengthMatchingParens(“((((((“)); //0
console. log(maxLengthMatchingParens(“(((())((())”)); //4

A

We will use a stack to keep track of longest substring with matching parens.

KEY : We push the index of all opening brackets. If we find a closing bracket, we pop from our stack, and calculate currentIndex - top of stack, which will give us the length of the current matching substring.

Second Key : We initialize stack with -1. This makes it so that for ‘()’, when we reach the ‘)’ that has an index of 1, the length will be 1 - (-1) = 2.

const maxLengthMatchingParens = function(strExpression) {
 const stack = [];
 stack.push(-1);

let maxLength = 0;

for (let i = 0; i \< strExpression.length; i++) {
 let current = strExpression[i];

if (current === ‘(‘) {
stack.push(i);
} else {

//pop previous opening bracket's index
 stack.pop();
//check stack's length
 if (stack.length \> 0) {
 let top = stack[stack.length-1];
 maxLength = Math.max(maxLength, i - top);
 } else {
 //if stack is empty, push current index
 stack.push(i);
 }
 }
 }

return maxLength;
};

Solution without extra space :

Scan left -> right, and then right -> left.

The reason we have to do both scans is if we have an ‘unbalanced’ bracket set like ‘(()’.

We keep count of leftParens and rightParens. If the counts of both are equal, we know that we have a matching set so we update maxLength. We can calculate the new maxLength by multiplying 2 * rightParens, or 2 * leftParens. If we’re going left-> right, we multiply 2 * rightParens.

53
Q

Given a linked list, split it in two such that every other node goes into the new list. For lists with odd number of nodes, first one should be longer. Modify the original list.

EX: {a, b, c, d, e, f, g} => {a, c, e, g} , {b, d, f}

A

Approach :
Initialize two pointers, nodeOne and nodeTwo. nodeOne starts off from original linked list head, nodeTwo starts off from original head.next.

As long as nodeOne and nodeOne.next exists, we can iterate through the linked list, adding the next’s next element to each one.

We don’t forget to set the tail of nodeTwo to null when we’re done.

const splitInTwo = function(head) {
 //if head is null return null
 if (!head) return null;
let nodeOne = head;
 const headTwo = head.next;
 let nodeTwo = headTwo;

while (nodeOne && nodeOne.next) {
nodeOne.next = nodeOne.next.next;
if (nodeTwo.next) {
nodeTwo.next = nodeTwo.next.next;
}
nodeOne = nodeOne.next;
nodeTwo = nodeTwo.next;
}

if (nodeTwo) {
nodeTwo.next = null;
}

return headTwo;
};

54
Q

Delete a node from a linked list given the head, and the target. There might be multiple target elements in the linked list!

A

Solution #1., More Straightforward, O(n) time and O(1) space. In-place solution.

  • Edge Cases :*
    1) Head is equal to target

2) There are multiple items with target.
* *Ex ) 1 -> 1 - > null** with target 1.

Key : Initialize prev and head both to head. If we initialize prev to null it’ll throw an error later when we update prev’s next pointer. Handle the head edge case(Where head’s value is equal to target value) INSIDE our while loop since there might be duplicates.

Solution #2. Create a new linked list using a ‘fake head’. We need to create a fake head because we don’t know what the new head will be just yet(The new head might be equal to target in which case we have to return null).

Key : We create a new ListNode(-1). This we’ll use to append nodes to the end. And we’ll also have a fakeHead, which points to newListNode. In the end we’ll return fakeHead.next(Which will be the ‘new head’).

Don’t forget to set newListNode.next to null.

----------------------------------------------------------------------------
const deleteNodeAlt = function(head, target) {
 if (!head) return null;
let newListNode = new ListNode(-1);
 let fakeHead = newListNode;
while (head !== null) {
 //if it's a new element
 if (head.val !== target) {
 //We have to do two things :
 //1. make a new next pointer on newListNode
 //2. Advance newListNode;
 newListNode.next = head;
 newListNode = newListNode.next;
 }
 head = head.next;
 }
//set tail node to null
 newListNode.next = null;
 return fakeHead.next;
};
55
Q

Find the lowest common ancestor of two nodes in a BST. LCA : “The lowest common ancestor is defined between two nodes v and w as the lowest node in T that has both v and w as descendants (where we allow a node to be a descendant of itself).

A

Naive Solution : Find path from root to node. Then walk the two paths down one by one from left->right to see if they’re equal, and if they are, return the current node. But this requires extra space.

Optimized Solution : Use recursion! We first check if root is either p, or either q, we return the root. Since this will bubble up from the bottom, we know that after going through left-subtrees and right-subtrees, if they’re both existent, we’ve found our LCA. So we return it. Otherwise, we return left or right.

This is a simple but tricky solution. Review Tushar’s video if it’s confusing.

const lowestCommonAncestor = function(root, p, q) {
 if (!root) return null;
//if root is either one return root!
 if (root === p || root === q) {
 return root;
 }
let left = lowestCommonAncestor(root.left, p, q);
 let right = lowestCommonAncestor(root.right, p, q);

if (left && right) {
return root;
}

return left || right;
};

56
Q

Check if two trees, treeA and treeB are identical.

A

Example of a recursive solution, since we’re re-using the same logic we’d use for one tree for its sub-trees.

Recursive Solution : base case : if they’re both null, return true.

Case 1 : One of them is null, or they’re unequal return false;

Case 2: If they both exist :, return (treeA.val === treeB.val) && checkIdenticalTree(treeA.left, treeB.left) && checkIdenticalTree(treeA.right, treeB.right)

57
Q

Find minimum depth of tree.

A

Key Point : Think about the smallest tree, with depth of 1.

Recursive Solution #1 : Think about the simplest cases. To find minimum depth, we only have two cases.

Case 1 : Root has left and right sub-trees. So we have to examine both, find the minimum of left subtree and right subtree. (Don’t forget to add 1 to the computed value since we’re traversing down one level)

Case 2 : Root has only one child. This means we have to go all the way down to the leaf node of this tree, and essentially get its maximum depth. So find the full depth of one of the subtrees that exist.

————————————————————————

const minDepth = function(root) {
 if (!root) return 0;

if (root.left && root.right) return 1+Math.min(minDepth(root.left), minDepth(root.right));

else return 1+Math.max(minDepth(root.left), minDepth(root.right));
};

———————————————————————– Recursive Solution #2: We use DFS to find all depths. Initialize a result array to store all depths. Subroutine getDepths(root, depths)

  • Case 1:* if root is a leaf(no children) push current depth to result.
  • Case 2 :* If root has leftChild, call getDepths recursively with depth+1
  • Case 3* : If root has rightChild, call getDepths recursively with depth+1 Sort result array and return the first element.
58
Q

Write a program that generates all subsets of a set

Ex) [1,2,3] -> [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3] ]

A

Recursive Solution #1 : Using backtracking, we can see that as we scan the array, we can either choose to include the current element, or exclude it. We simply have to store every single possibility. So we can develop a recursive algorithm in which we simply call the recursive function twice, one where we include the current element, and one where we exclude it.

It’s a DFS, where at each step each tree has a subtree of chosen, or not chosen.

Ex) with input [‘a’,’b’,’c’] : sub(0, []) => sub(1, []) => sub(2, []) => sub(3, [])

=> sub(3, [‘c’])

  • sub(2, [‘b’]) => sub(3, [‘b’])*
  • => sub(3, [‘b’, ‘c’])*
  • sub(1, [‘a’]) => …..*
const allSubsetsAlt = function(list) {
 const solution = [];
const recurse = function(index, soFar) {
 if (index === list.length) {
 solution.push(soFar.slice());
 return;
 }

let currentElem = list[index];
soFar.push(currentElem);
recurse(index+1, soFar);

soFar.pop();
recurse(index+1, soFar);
}

recurse(0, []);
return solution;
};

——————————————————————————————————

Recursive Solution #2 : Use DFS. With DFS we simply store the path at every recursive function call. At each function call, we iterate over all possible options, and call DFS with the added element to the current path.

const allSubsetsAltTwo = function(list) {
 const result = [];
const dfs = function(startIndex, path) {
 result.push(path);
for (let i = startIndex; i \< list.length; i++) {
 dfs(i+1, path.concat(list[i]));
 }
 };

dfs(0, []);
return result;
};

How do we handle duplicates?

First sort the input so that duplicate elements are adjacent to each other.

Then, inside our recursive function, the way we check for duplicates is by first checking to see if list[i] === list[i-1]. We also need to check if i > startIndex this is because we only want to skip choosing current element if i === startIndex we’re choosing this element for the first time.

Bit manipulation Solution :

For a number n, we know that there will be n^2 number of subsets. So we generate numbers from 0 to n^2-1, and get the binary number for each of them. For each bit in that number, if the bit is 1, we’ll choose that number from our set.

59
Q

Given a collection of numbers that might contain duplicates, return all possible unique permutations.

For example,
[1,1,2] have the following unique permutations:

[[1,1,2], [1,2,1], [2,1,1] ]

A

We can use the same algorithm for generating permutations, except we need a way to somehow handle duplicates. Using storage systems like map, sets WONT WORK since they can’t handle two completely different objects. So using our algorithm of in-place swaps is overkill here, since the duplicate part is confusing as fuark.

INSTEAD, what we will do is use a map to keep track of each element and its count. We will then go left to right from a list of distinct elements, looking for the next available element. The next available element is one that doesn’t have a count of 0. THE PART THAT GOT ME CONFUSED WAS I was trying to iterate over our original input(That has duplicates) and choosing from duplicates. THIS DOESNT WORK SINCE we have no way of handling duplicate characters. Instead, we only select a character from DISTINCT ELEMENTS that we know we haven’t used yet. So we transform our map to use two additional lists, elements and count. So these arrays are in the order of elements to their counts.

In the recursive algorithm, we do :

  1. keep track of current level. This is used to add to our result list(running total of permutation generated so far).
  2. when we pick a character, we make sure to decrement its count. Once we’re done with a recursive call, we make sure to put it back in its original state.

const permuteUnique = function(nums) {

//generate a map with element : count
 const countMap = {};
 nums.forEach(function(elem) {
 if (!countMap[elem]) {
 countMap[elem] = 1;
 } else {
 countMap[elem] += 1;
 }
 });
const elements = [];
 const count = [];
 let index = 0;

for (let elem in countMap) {
elements.push(Number(elem));
count.push(countMap[elem]);
}

const result = [];

//Recursive algorithm, we go from left to right looking for the next available character(IF remaining count is not 0). Then we push the character into result list. We used this character so we update the characterCount. We use level to keep track of the depth of the recursion.

const generate = function(currentLevel, generated) {
 if (currentLevel === nums.length) {
 result.push(generated.slice());
 return;
 }
 //look for the next available character. We iterate only up to the number of distinct elements' length...THIS IS KEY.
for (let i = 0; i \< elements.length; i++) {
 let currentChar = elements[i];
 if (count[i] === 0) {
 continue;
 }
 //add current character to generated
 generated[currentLevel] = currentChar;
 //decrement characterCount
 count[i] -= 1;
 //Enter next recursive call
 generate(currentLevel+1, generated);
 //Put character Count back
 count[i] += 1;
 }

};
generate(0, []);
return result;
};

60
Q

N-Queens : Given a chess board of size N x N, determine how many ways N queens can be placed on this board so that no two queens attack each other.

[
[“.Q..”, // Solution 1
“…Q”,
“Q…”,
“..Q.”],

[”..Q.”, // Solution 2
“Q…”,
“…Q”,
“.Q..”]
]

A

Every correct solution will be a permutation
[1, 3, 0, 2]
This means queen is in 1st row in 1st column, 3rd row in 2nd column, 0th row in 3rd column, 2nd row in 4th column.

Instead of using the entire board, since we know that there can only be one queen in one column, we change our representation to be a 1-D Array. ‘row for column’. As in, each index represents column #, and each entry represents row # at that column.

Since for this case, if we look at the output, it’s easier to think of the representation as ‘Columns For Every Row’ or ‘Rows for every Column’ because for every row we’re inserting a Q at each column

const swap = function(list, a, b) {
 let temp = list[a];
 list[a] = list[b];
 list[b] = temp;
 return list;
};
const isValid = function(columnsPerRow, index) {
 //we know since we've swapped #'s there can't be duplicate numbers, meaning we've already checked for same row. Since every index is a different column, we've checked for column too. All we have to do is check for ascending/descending diagonals

for (let i = 0; i < index; i++) {

let diagonalOffset = Math.abs(index - i);

if (columnsPerRow[index] === columnsPerRow[i] + diagonalOffset ||
columnsPerRow[index] === columnsPerRow[i] - diagonalOffset) {
return false;
}
}
return true;
}

const generateBoard = function(columnsPerRow) {
// It's easy to go from [1, 3, 0, 2] to this :
// [".Q..",
// "...Q",
// "Q...",
// "..Q."]
 const board = [];
 let emptyRow = new Array(columnsPerRow.length);
 emptyRow.fill(".");
 //first iterate over board length so we can generate N rows
 //then check every columnPerRow to find out which index the Queen is placed at every row
 //we place the Queen at the proper column index, then push the row into board.
for (let currentRow = 0; currentRow \< columnsPerRow.length; currentRow++) {
 let columnIndex = columnsPerRow[currentRow];
 emptyRow[columnIndex] = "Q";
 board.push(emptyRow.join(''));
 emptyRow[columnIndex] = ".";
 }
 return board;
}
//check every possible combination of queens per row,
//and print the output. j represents the current column.
const nQueenSolver = function(columnsPerRow, index, result) {
 if (index === columnsPerRow.length) {
 let board = generateBoard(columnsPerRow.slice());
 result.push(board);
 return;
 }
//iterate over all remaining options, fix one and swap
 for (let i = index; i \< columnsPerRow.length; i++) {
 swap(columnsPerRow, i, index);

if (isValid(columnsPerRow, index)) {
nQueenSolver(columnsPerRow, index+1, result);
}

swap(columnsPerRow, index, i);
}
};

const nQueens = function(n) {
 const columnsPerRow = new Array(n);
 const result = [];
for (let i = 0; i \< n; i++) {
 columnsPerRow[i] = i;
 }

nQueenSolver(columnsPerRow, 0, result);

return result;
};

61
Q

Given a string s, return all the palindromic permutations (without duplicates) of it. Return an empty list if no palindromic permutation could be form.

For example:

Given s = “aabb”, return [“abba”, “baab”].

Given s = “abc”, return [].

A

KEY Insight : We only need to look at half of the input. Because if we know that the input is a permutation of a palindrome, what we can then do is generate all permutations of 1st half, and then simply reverse it and append it as 2nd half to generate the ‘full palindrome’(We will have to handle middle character in case of odd # of characters).

Key issue : Generating the first half of the string. This is tricky because we’ll have to handle setting aside the middle character to be used later to form the full palindrome. Also, we can’t do the popular method of setting character count to 1 if found, and decrementing it by 1 if found again. This fails because this gives us no knowledge of each character count in the end(Since they’ll all be 0 or 1). So for an example “aabbccc”, we want our first half of the string to be something like “abc”, since the middle character in this case will be ‘c’.

So instead we want to keep track of the all the character counts, and detect the # of odd characters. We know that a string is only a palindrome if numOfOdds is either 0 or 1. To generate the first half of the string, what we’ll do is simply iterate through each characterCount(a : 2, b : 1). We will then divide the count by 2 for each character(MAKE SURE TO Math.floor it to prevent adding characters with count of 1, since 1/2 = 1/2 and greater than 0 so this will enter satisfy the loop condition). Once we’ve finished generating firstHalf and middleCharacter, it’s game over.

The rest of the algorithm is identical to generating permutations. Except in the base case we remember to construct the full palindrome.

62
Q

Tower of Hanoi :

The mission is to move all the disks to some another tower without violating the sequence of arrangement. A few rules to be followed for Tower of Hanoi are −

Only one disk can be moved among the towers at any given time.

Only the “top” disk can be removed.

No large disk can sit over a small disk.

A

KEY : Find the recurrence relation.

When we’re solving with 2 discs, let’s say our pegs are labeled A, B, C.

Steps to solve them are as follows :

  1. Move smaller disc from A to B.
  2. Move larger disc from A to C.
  3. Move smaller disc from B to C.

When we’re solving with 3 discs :

  1. Move 2 discs from A to B. (How to move two discs from one tower to another we’ve already solved it).
  2. Move 1 disc from A to C.
  3. Move 2 discs from B to C(We’ve solved this too).

const towerOfHanoi = function(n, start, mid, end) {
if (n === 1) {
console.log(start, ‘—->’, end);
return;
} else {
towerOfHanoi(n-1, start, end, mid);
towerOfHanoi(1, start, mid, end);
towerOfHanoi(n-1, mid, start, end);
}
};

console.log(towerOfHanoi(3, ‘a’, ‘b’, ‘c’));

63
Q

Input : 10?
Output : 101, 100

? Behaves like a wildcard, and can be replaced with either 0 or 1.

Input : 1?0?
Output : 1000, 1001, 1101, 1100

Write a program that takes given strings as input and produces appropriate output

A

The idea is to use recursion, and backtracking more specifically.

Basic Approach : We will use a subroutine to iterate over the string, and once we encounter our wildcard, we’ll consider both cases - One where we place 0, and one where we place 1.

We MUST remember to put the input BACK to the wildcard after the recursive calls.

ALSO, we use currentIndex to check each character, we don’t need a loop to iterate over all the options since we only need to examine one character at a time.

const wildCard = function(input) {
 let array = input.split('');

const result = [];

const subroutine = function(input, index) {
 if (index === array.length) {
 result.push(input.join(''));
 return;
 }

if (input[index] === ‘?’) {
input[index] = ‘0’;
subroutine(input, index+1);

input[index] = ‘1’;
subroutine(input, index+1);

//HAVE to put it back
input[index] = ‘?’;
} else {
subroutine(input, index+1);
}
}

subroutine(array, 0);
return result;
};

Alternative Solution that builds strings up from “”.

const generate = function(currentIndex, generated) {

console. log(‘currentIndex :’, currentIndex);
console. log(‘generated :’, generated);
console. log(‘———————-‘);

if (currentIndex === s.length) {
result.push(generated);
return;
}

//start adding characters
if (s[currentIndex] === ‘?’) {
generate(currentIndex+1, generated.concat(‘0’));
generate(currentIndex+1, generated.concat(‘1’));
} else {
generate(currentIndex+1, generated.concat(s[currentIndex]));
}
};
generate(0, ‘’);
return result;
};

64
Q

Given a set of n elements find their kth permutation.

A

Naive Approach : Generate all permutations in lexicographical order, and using a counter select the kth one. This will require O(N!) Time Complexity.

Instead, we can take advantage of recursion to reduce it to a linear solution

Basic Approach : Let’s say we have an input of [1,2,3]. We know that if there are 2! permutations that start with 1 fixed. And also 2! with 2 fixed, and 2! with 3 fixed. So we can envision blocks of size (n-1)! permutations for each starting letter. What we can do is, we can use k to find which block our permutation lies in, which lets us find out what the next letter in the permutation is. We use a recursive algorithm, and for the next recursive call we simply take out the element we just used from the input list.

KEY : A couple things to watch out for here :

1. Finding the block size at each call. This is easy, just do (n-1)!

2. Selecting the index of the block that the permutation is in. In other words, finding out what the next letter is by finding the right index. This is done by subtracting k-1 and dividing it by the block size. The reason we subtract k by 1 is because, for example, if k = 12, and each block size is 3! = 6, we know we need to select from the second block(index 1). If we simply do k / blockSize = 12 / 6 = 2. We’d end up selecting the second block. You can also find the index by BaekJoon’s method of subtracting k by blockSize every time until we reach a negative number.

3. For the next function call, we reset k by computing k = k - (blockSize * selectedIndex). And we also slice out the element at currentIndex from the input list.

const factorial = function(n) {
 const memo = {};
 memo[0] = 1;
 memo[1] = 1;
for (let i = 2; i \<= n; i++) {
 memo[i] = i \* memo[i-1];
 }
 return memo[n];
};

const findKthPermutation = function(n, k) {

let result = "";
 const list = [];
for (let i = 1; i \<= n; i++) {
 list.push(i);
 }
const generate = function(k, input) {
 if (input.length === 0 || !k) {
 return;
 }

const blockSize = factorial(input.length-1); // 3!

//Get the index of the block that the permutation is in
 let selectedIndex = Math.floor((k-1) / blockSize); //selects block

result+= input[selectedIndex];

//splice out currentIndex element from input
 input.splice(selectedIndex, 1);
//reset k by subtracting it from the # of (blocks \* index)
 k = k - (blockSize \* selectedIndex);

generate(k, input);
};

generate(k, list);
return result;
};

65
Q

Given a Binary Tree, check if it’s a valid Binary Search Tree.

Do it recursively as well as iteratively.

A

First off, a binary search tree that’s valid is one whose left subtree values are all less than the root, and right subtree values are greater.

Simple recursive approach is to utilize lowerBound and upperBound. Initialize both at Infinity, -Infinity and call function recursively on leftChild and rightChild.

  • *Iterative Solution :** Use In-Order Traversal. Keep track of previous element. Since we know inorder has to be sorted, at every step just examine if prev is indeed less than current element. I really like this approach, just make sure you’re also checking if previous element EXISTS when comparing previous element with current element.
//In-order traversal
const validateBST = function(root, minimum, maximum) {
const subroutine = function(root, min, max) {
 if (!root) return true;
//if less than min, or greater than max, return false!
 if (root.val \<= min || root.val \>= max) {

return false;
}

return subroutine(root.left, min, root.val) && subroutine(root.right, root.val, max);
 };
return subroutine(root, -Infinity, Infinity);
};
//Iterative In-order solution
const validateBSTIter = function(root) {
 const stack = [];
 if (!root) return true;
//initialize prev at null
 let prev = null;

while (root || stack.length) {

if (root) {
stack.push(root);
root = root.left;
continue;
}

//root is null, pop up one level & check
 root = stack.pop();

if (prev !== null && root.val <= prev.val) {
return false;
}

prev = root;
root = root.right;
}

return true;
};

66
Q

Reverse a Linked List in k-groups :

Given a linked list, write a function to reverse every k nodes (where k is an input to the function).

Example:

Inputs: 1->2->3->4->5->6->7->8->NULL and k = 3

Output: 3->2->1->6->5->4->8->7->NULL.

Inputs: 1->2->3->4->5->6->7->8->NULL and k = 5

Output: 5->4->3->2->1->8->7->6->NULL.

A

First clarify whether we have to reverse the nodes that are ‘left-out’. This changes the problem.

Recursive Solution : Reverse nodes from head to k, and return the ‘new head’, which is the kth node. And using the same logic, simply set head.next = reverse(next, k). By doing this, we’re setting head(Which will point to the end of the reversed bit from head -> newHead)’s next to the new head of reversing (k+1, …).

const reverse = function(head, k) {
 let count = 0;
 let current = head;
 let next = null;
 let prev = null;
//reversing part
 while (current !== null && count \< k) {
 next = current.next;
 current.next = prev;
 prev = current;
 current = next;
 count++;
 }
//at this point, next is the (k+1)st node. So check if it exists, and
 //connect it to head
 if (next !== null) {
 head.next = reverse(next, k);
 }
//prev is the new head.
 return prev;
};

What if we have to leave the left-out nodes alone, and can’t reverse them? This changes the problem and we can’t reverse as we scan along, we have to first find the k+1st node. We use a helper, reverse that simply reverses a linked list from beginning to end.

After finding the k+1st node, we reverse from head -> k+1st node. Our function will return the new Head. Since we reverse from head -> k+1st first, we know we need to attach head’s pointer to the next half that’s reversed.

So we simply set head.next = reverseKGroup(current, k) since we want to reverse from k+1st —> … —> end.

Remember what our function returns, we return newHead, which is the head of the reversed bit.

const reverse = function(first, last) {
 let prev = null;
 let current = first;
while (current !== last) {
 let next = current.next;
 current.next = prev;
 prev = current;
 current = next;
 }
//make sure to return the new head
 return prev;
}
var reverseKGroup = function(head, k) {
 let current = head;
 let count = 0;
//find the k+1st node
 while (count \< k && current !== null) {
 current = current.next;
 count++;
 }
//count is less than k, just return head since we can leave it alone
 if (count \< k) return head;
//reverse from head --\> k+1st node. Since the reverse function returns the new Head, we can save it to a variable
 let newHead = reverse(head, current);
 head.next = reverseKGroup(current, k);
 return newHead;
};
67
Q

Find the smallest possible operations you can make to make an integer N into 1.

We are given three operations :

  1. If it’s divisible by 3, divide by 3
  2. If it’s divisible by 2, divide by 2
  3. Subtract by 1
A

We start by defining D[n], which is the total # of ways you can turn n into 1. We might think that dividing it by 3 to shrink the number fastest will be a good approach but it isn’t. For 10, subtracting it by 1 then dividing it by 3 three times ==> 4 vs. dividing it by 2, then subtracting 1, then dividing by 2 twice. ===> 5.

We consider the three cases :

We have N —-> *some operations* —-> 1

This total # of operations is D[n].

1) N —–> N/3 ——> 1 First thing we can do is divide n by 3. Then we’re left with n/3. Then we do some number of operations on n/3 to make it 1, which is D[n/3]. We know it takes one operation to divide N by 3. So D[n] = D[n/3] + 1.

2) N ——> N/2 ——> 1 Same deal here, we have D[n] = D[n/2] + 1

3) N ——> N-1 ——–> 1 D[n] = D[n-1] + 1

Since we’re asked to find the smallest possible # of ways, we compute the minimum of 1), 2), 3). Try to do this bottom-up(Iteratively) and top-down(recursively).

const makeOneBottomUp = function(n) {
 const memo = {};
 memo[1] = 0;
 for (let i = 2; i \<= n; i++) {
 //case 1
 memo[i] = memo[i-1] + 1;
if (i % 2 ===0) {
 let temp = memo[i/2] + 1;
 if (memo[i] \> temp) {
 memo[i] = temp;
 }
 }
if (i % 3 ===0) {
 let temp = memo[i/3] + 1;
 if (memo[i] \> temp) {
 memo[i] = temp;
 }
 }
 }

return memo[n];
};

68
Q

Merge two sorted linked lists

A

Simple Problem, we’ll basically be creating a new linked lists appending the smaller node of the two each time.

KEY to remember is that once we’ve exhausted one of the lists, all we have to do is check if one of the list exists, and if it does, simply append it as the next element of the newly sorted lists. No loop is required like the merge function in regular merge sort. This also means we also don’t have to worry about setting the last node’s next to null.

var mergeTwoLists = function(l1, l2) {
 if (!l1) {
 return l2;
 }

if (!l2) {
return l1;
}

let l1Current = l1;
 let l2Current = l2;
 let newHead = null;
//finish setting up new Head;
 if (l1Current.val \<= l2Current.val) {
 newHead = l1Current;
 l1Current = l1Current.next;
 } else {
 newHead = l2Current;
 l2Current = l2Current.next;
 }

let sortedCurrent = newHead;

while (l1Current && l2Current) {
 let tempNode = null;
 if (l1Current.val \<= l2Current.val) {
 tempNode = l1Current;
 l1Current = l1Current.next;
 } else {
 tempNode = l2Current;
 l2Current = l2Current.next;
 }
 sortedCurrent.next = tempNode;
 sortedCurrent = tempNode;
 }

if (l1Current) {
sortedCurrent.next = l1Current;
} else if (l2Current) {
sortedCurrent.next = l2Current;
}

return newHead;
};

69
Q

Mirror/Invert a binary search tree!

A

Obviously we need to use recursion, because we have to swap the left/right child, but need to do that for every node.

Use DFS(You can do it pre-order, or post-order), either way. Just remember to return the root value(null / root) so that the parent can then swap it.

So there’s two parts of the logic :

1 ) Handle base case by returning root

2) Call recursively on left/right subtree
3) Make the swap, and return the root value.

function flipTree(node) {

if (!node) return null;
 //swap left and right nodes
 let temp = node.left;

node. left = node.right;
node. right = temp;

//invoke flipTree for leftChild and rightChild
flipTree(node.left);
flipTree(node.right);

return node;
}

70
Q

Find the LCA of a binary tree/binary search tree. How are the algorithms different?

A

Recursive Approach :

One scan. Using recursion, at every node we’ll check if it’s either n1 or n2, in which case we’ll return the root value.

Then we check recursively on the left-subtree/right-subtree. If they’ve both bubbled up to a value, we’ve FOUND our LCA, so we return it.

Otherwise, we return whichever one of leftLCA / rightLCA isn’t null.

Iterative :
Two scans, get the paths from root ==> node1, root ===> node2. Then compare the two paths -

path1 : [-1, 0, -2, 8]

path2 : [-1, 0]

The node right before the first mismatch is the first ancestor.

What if it’s a BST? How does the approach change? Although the recursive solution can handle both,

71
Q

Implement iterative, non-recursive versions of pre-order, in-order and post-order traversals of a binary tree.

A

First off, if we’re not using recursion we’ll have to use a stack obviously.

Pre-order is rather quite simple. The key to this is to push to the stack the RIGHT child first, because stack is LIFO. With pre-order, just initialize a stack with the root node in it, pop it, examine it and push right, followed by left. That’s it.

In-order is a bit tricker. We’ll use another pointer called currentNode, and first keep going all the way down to the left. Once currentNode is null, we’ll pop, and examine it. And we go to its right child.

const preOrderIter = root =\> {
 const stack = [];
 const result = [];
stack.push(root);
 let current = root;
while (stack.length \> 0) {
 current = stack.pop();
 //process it
 console.log(current);
//put right first
 if (current.right) {
 stack.push(current.right);
 }

if (current.left) {
stack.push(current.left);
}
}
};

const inorderIter = root =\> {
 const stack = [];
 const result = [];
 currentNode = root;
while (stack.length \> 0 || currentNode !== null) {
 //keep pushing left
 while (currentNode !== null) {
 stack.push(currentNode);
 currentNode = currentNode.left;
 }
currentNode = stack.pop();
 //examine it
 result.push(currentNode.val);
 //go right.
 currentNode = currentNode.right;
 }

return result;
};

Post-order is the trickiest one out of all. The most straightforward algorithm for this is to know that post-order is essentially a reverse pre-order traversal, only difference being you visit the right subtree first. So all you have to do is do a pre-order traversal(visiting right first), and then reverse the result in the end.

const postOrderIterative = root =\> {
 const stack = [];
 const resultStack = [];
if (!root) return stack;
 //do pre-order but in reverse order(process right sub tree first, which means we want to push the left subtree first)
 let currentNode = root;
 stack.push(root);

while (stack.length > 0) {
currentNode = stack.pop();

//push it to resultStack
 resultStack.push(currentNode.val);

if (currentNode.left) {
stack.push(currentNode.left);
}

if (currentNode.right) {
stack.push(currentNode.right);
}
}

return resultStack.reverse();
};

72
Q

Given the root of a BST, clone it and return the root of the cloned BST.

A

The most elegant/easiest way is to use recursion using DFS. Use pre-order to process it, then just assign the left/right pointer to each recursive calls.

The important thing here is to not lose reference to the new root. We want to make a recursive function that will return the root each time, so that it’ll get bubbled up and made available to us when all recursive calls are over. Make sure you’re creating a new node with each recursive call(That’s what the processing step will do). Create a new variable, assign it to a new node and return it.

function cloneTree(node) {

 const copyTreeRecurse = (node) =\> {
 let newRoot = null;
 if (node) {
 newRoot = new Node(node.val);
 newRoot.left = copyTreeRecurse(node.left);
 newRoot.right = copyTreeRecurse(node.right);
 }
 return newRoot;
 };
 return copyTreeRecurse(node);
}
73
Q

Find the Kth element in an in-order BST Traversal

A

This problem is essentially the same as finding the kth smallest element in a BST).

The important thing here is to use DFS in-order, but think about how to find the kth element. We can’t just start off the count at 0, and increment it at every recursive call, because we need to count from the leftmost node, going in-order.

So what we need to do is, use a global variable called count(initialized to k). And traverse in-order. Once we reach our leftMost node, we then decrement it at each recursive call. When count hits 0, we know we’ve found our node.

var kthSmallest = function(root, k) {
 let number = 0;
 let count = k;
 const inOrderRecurse = (root) =\> {
 if (root.left) inOrderRecurse(root.left);
//we've reached the leftMost node, start decrementing count
 count--;
 //process, and count is 0(we've found our node)
 if (count === 0) {
 number = root.val;
 return;
 }

if (root.right) inOrderRecurse(root.right);
};

inOrderRecurse(root);
return number;
};

74
Q

Given an array of integers, find the largest multiplication of three numbers.

A

1) Using sorting : The intuition to remember here is that to find the largest multiplication of three numbers, it’s one of two cases : 1) Multiplication of largest 3. 2) Largest positive # * Smallest Two(Two negative numbers whose absolute value is the greatest, and multiply this by the largest positive number). This means that we need to find the three largest items, and the two smallest items. We can simply achieve this by sorting. O(nlogn).
2) A “greedy” approach is to think about what information we need in order to find the largest multiplication of three numbers. We know we need the largestTwo, and smallestTwo, so that we can multiply current number to these two and compare. To find largestTwo and smallestTwo, we need largest and smallest. So we simply initialize these five things(largestThree, largestTwo, smallestTwo, largest, smallest), and start at the third index in the array to find it. IMPORTANT thing is to update it in the right order - largest3, largest2, smallest2, largest, smallest and not vice versa, because doing so will result in multiplying one number against itself.

75
Q

N light bulbs are connected by a wire. Each bulb has a switch associated with it, however due to faulty wiring, a switch also changes the state of all the bulbs to the right of current bulb. Given an initial state of all bulbs, find the minimum number of switches you have to press to turn on all the bulbs. You can press the same switch multiple times.

Input : [0 1 0 1] Return : 4 Explanation : press switch 0 : [1 0 1 0] press switch 1 : [1 1 0 1] press switch 2 : [1 1 1 0] press switch 3 : [1 1 1 1]

A

We know that flipping one switch twice just re-sets the state back to itself, so it’s useless. So at most, we’d use N flips to turn on all the bulbs.

We can take a ‘greedy’ approach, to find the first 0 from the left, because that’s the digit we want to turn on first. Then, since that will switch off all the other bits, we want to find the first 1 from the left. We want to repeat this process, vice-versa.

We can iterate from left ===> right, finding the first 0, then finding the first 1, incrementing the count each time.

bulbs : function(A){
 var flipped = false;
 var switchCount = 0;
 for (var i = 0; i \< A.length; i++) {
 var current = A[i];
 if (!flipped) {
 if (current === 0) {
 switchCount++;
 flipped = true;
 }
 } else if (flipped) {
 if (current === 1) {
 switchCount++;
 flipped = false;
 }
 }
 }
 return switchCount;
 }
76
Q

Assign candy to children :

Each child must have at least one candy.

Children with a higher rating get more candies than their neighbors.

Ratings : [1 2]

The candidate with 1 rating gets 1 candy and candidate with rating cannot get 1 candy as 1 is its neighbor. So rating 2 candidate gets 2 candies. In total, 2+1 = 3 candies need to be given out.

A

Observe this example : 1 4 9 7 4

We need to assign 1 2 3 2 1.

Or look at an example where the first child is greatest : 7 4 2 9 1

We need to assign 3 2 1 2 1.

We can see that trying to do it in one pass involves complex computations, because we never know how many candy the first element needs(We have no reference to compare it with.)

The trick is to take a greedy approach. But the important thing is to go two passes, left=>right and right=>left. We need this, because in the case where we have a mix of increasing / decreasing subsequences, it’s easiest if we handle this using a greedy approach.

So first, go left=>right, only checking a child’s rating with the previous neighbor’s rating. Assign one more candy than the previous neighbor if the rating’s greater. Otherwise, leave it at one. Initialize the first child to 1 candy.

Then, go right=>left, starting with the second to last child and essentially doing the same - Compare current child with the right neighbor, and if the rating’s greater AND the candy count is equal, or LESS, give the current child one more candy.

So we can do this in linear time, with two passes.

candy : function(A){
 var candyCount = 0;
 var candies = [];
 candies[0] = 1;
 for (var i = 1; i \< A.length; i++) {
 //if rating is greater than previous neighbor
 if (A[i] \> A[i-1]) {
 candies[i] = candies[i-1] + 1;
 } else {
 candies[i] = 1;
 }
 }
 for (var j = A.length-2; j \>= 0; j--) {
 if (A[j] \> A[j+1] && candies[j] \<= candies[j+1]) {
 candies[j] = candies[j+1] + 1;
 }
 candyCount += candies[j];
 }

candyCount += candies[candies.length-1];
return candyCount;
}

77
Q

Given an array of integers, find the majority element(An element which occurs more than Math.floor(n/2) times).

A
  1. Naive algorithms : Do a nested loop(n^2) solution to find the count of occurrences, and find if it’s greater than or equal to n/2.

Or, sort the array and scan neighbors to find the count of occurrences(Onlogn).

O(n) Greedy Solution :

  • Majority Vote Algorithm : Initialize a candidate(null), and count to 0. Scan the list, and each time the count is 0, we nominate the candidate to element at current index, and re-set the count to 1. If we find a duplicate element(element equal to candidate), we increment count. If we find something different, we simply decrement the count. We reset the candidate to another element when the count equals 0. This works because if we find two elements x and y in the array that are different, if we REMOVE them, the majority element does not change. We simply repeat this process, and whatever’s left over will be the majority element.