The code searches for the number of possible pathways of actions that reach the goal. I do not want to optimize it, just know the Big-O complexity that it has. The code is the following:
private int countPaths(Node parent, List<Action> usableActions, Node goal)
{
int counter = 0;
foreach(Action act in usableActions)
{
Node node = generateNewNode(parent, act); // Only generate the new node O(1)
if (node.isEquals(goal)) //Check goal
{
counter++;
}
else
{
List<Action> subset = actionSubset(usableActions, act); // return usableAction with act removed
counter += countPaths(node, subset, goal); // usableActions - 1
}
}
return counter;
}
The first loop would give the algorithm a complexity of O(n), but having a recursive call does not know if it is O(n^2), O(n^n) or another option.
As already stated in some of the comments, the time complexity is .
As for memory usage, actionSubset() creates a new list each time it is called (as opposed to having the algorithm operate on the original). But because all lists except the original fall out of scope at the end of each iteration, memory usage will only grow by as a function of the size of usableActions, so .
Related
I ran into the following very difficult interview question:
Consider Queue Data Structure with three operations:
- Add into the front of list (be careful front of list)
- Delete from Tail of the list (end of the list)
- Extract Min (remove)
The best implementation of this data structure has amortized time:
A) three operation at O(1)
B) three operation at O(log n)
C) add and delete in O(1) and Extract-min in O(log n)
D) add and delete in O(log n) and Extract-min in O(n)
After the interview I saw that (C) is the correct answer. Why is this the case?
The first challenge is comparing the options: which option is better than the others and how we can understand the final correct option?
Of the given running times, A is faster than C is faster than B is faster than D.
A is impossible in a comparison-based data structure (the unstated norm here) because it would violate the known Ω(n log n)-time lower bound for comparison sorts by allowing a linear-time sorting algorithm that inserts n elements and then extracts the min n times.
C can be accomplished using an augmented finger tree. Finger trees support queue-like push and pop in amortized constant time, and it's possible to augment each node with the minimum in its sub-tree. To extract the min, we use the augmentations to find the minimum value in the tree, which will be at depth O(log n). Then we extract this minimum by issuing two splits and an append, all of which run in amortized time O(log n).
Another possibility is to represent the sequence as a splay tree whose nodes are augmented by subtree min. Push and pop are O(1) amortized by the dynamic finger theorem.
Fibonacci heaps do not accomplish the same time bound without further examination because deletes cost Θ(log n) amortized regardless of whether the deleted element is the min.
Since C is feasible, there is no need to consider B or D.
Given the limitations on the data structure, we actually don't need the full power of finger trees. The C++ below works by maintaining a list of winner trees, where each tree has size a power of two (ignoring deletion, which we can implement as soft delete without blowing up the amortized running time). The sizes of the trees increase and then decrease, and there are O(log n) of them. This gives the flavor of finger trees with a much lesser implementation headache.
To push on the left, we make a size-1 tree and then merge it until the invariant is restored. The time required is O(1) amortized by the same logic as increasing a binary number by one.
To pop on the right, we split the rightmost winner tree until we find a single element. This may take a while, but we can charge it all to the corresponding push operations.
To extract the max (changed from min for convenience because nullopt is minus infinity, not plus infinity), find the winner tree containing the max (O(log n) since there are O(log n) trees) and then soft delete the max from that winner tree (O(log n) because that's the height of that tree).
#include <stdio.h>
#include <stdlib.h>
#include <list>
#include <optional>
class Node {
public:
using List = std::list<Node *>;
virtual ~Node() = default;
virtual int Rank() const = 0;
virtual std::optional<int> Max() const = 0;
virtual void RemoveMax() = 0;
virtual std::optional<int> PopRight(List &nodes, List::iterator position) = 0;
};
class Leaf : public Node {
public:
explicit Leaf(int value) : value_(value) {}
int Rank() const override { return 0; }
std::optional<int> Max() const override { return value_; }
void RemoveMax() override { value_ = std::nullopt; }
std::optional<int> PopRight(List &nodes, List::iterator position) override {
nodes.erase(position);
return value_;
}
private:
std::optional<int> value_;
};
class Branch : public Node {
public:
Branch(Node *left, Node *right)
: left_(left), right_(right),
rank_(std::max(left->Rank(), right->Rank()) + 1) {
UpdateMax();
}
int Rank() const override { return rank_; }
std::optional<int> Max() const override { return max_; }
void RemoveMax() override {
if (left_->Max() == max_) {
left_->RemoveMax();
} else {
right_->RemoveMax();
}
UpdateMax();
}
std::optional<int> PopRight(List &nodes, List::iterator position) override {
nodes.insert(position, left_);
auto right_position = nodes.insert(position, right_);
nodes.erase(position);
return right_->PopRight(nodes, right_position);
}
private:
void UpdateMax() { max_ = std::max(left_->Max(), right_->Max()); }
Node *left_;
Node *right_;
int rank_;
std::optional<int> max_;
};
class Queue {
public:
void PushLeft(int value) {
Node *first = new Leaf(value);
while (!nodes_.empty() && first->Rank() == nodes_.front()->Rank()) {
first = new Branch(first, nodes_.front());
nodes_.pop_front();
}
nodes_.insert(nodes_.begin(), first);
}
std::optional<int> PopRight() {
while (!nodes_.empty()) {
auto last = --nodes_.end();
if (auto value = (*last)->PopRight(nodes_, last)) {
return value;
}
}
return std::nullopt;
}
std::optional<int> ExtractMax() {
std::optional<int> max = std::nullopt;
for (Node *node : nodes_) {
max = std::max(max, node->Max());
}
for (Node *node : nodes_) {
if (node->Max() == max) {
node->RemoveMax();
break;
}
}
return max;
}
private:
std::list<Node *> nodes_;
};
int main() {
Queue queue;
int choice;
while (scanf("%d", &choice) == 1) {
switch (choice) {
case 1: {
int value;
if (scanf("%d", &value) != 1) {
return EXIT_FAILURE;
}
queue.PushLeft(value);
break;
}
case 2: {
if (auto value = queue.PopRight()) {
printf("%d\n", *value);
} else {
puts("null");
}
break;
}
case 3: {
if (auto value = queue.ExtractMax()) {
printf("%d\n", *value);
} else {
puts("null");
}
break;
}
}
}
}
It sounds like they were probing you for knowing about priority queues implemented by way of fibonacci heaps.
Such implementations has the running times described in answer c.
O(1) time as only one operation has to be performed to locate it, so we can add at the start and delete from the end in a single operation.
O(log n) when we do divide and conquer type of algorithms like binary search, so as we have to extract the minimum instead of doing O(n) and increasing the time complexity we use the O(log n)
You will start by thinking a min-heap for the extract-min operation. This will take O(log n) time, but so will the operations add and delete. You need to think can we have any of these two operations in constant time? Is there any data structure which can do so?
Closest answer is : Fibonacci-heap, used for implementing priority-queues (quite popularly used for implementing Djistra's Algorithm), which has the amortized run-time complexities of O(1) for insert, delete in O(log n) though (but since the operation is always delete from tail, we may be able to achieve this by maintaining a pointer to the last node and doing the decrease-key operation in O(1) time) and O(log n) for delete-min operations.
Internally, fibonacci-heap is a collection of trees, all satisfying the standard min-heap condition (value of parent always lower than its children), where the roots of all these trees are linked using a circular doubly linked list. This section best explains the implementations of each operation alongside its run-time complexity in further detail.
Have a look at this great answer which explains the intuition behind fibonacci-heaps.
Edit: As per your query regarding choosing between B to D, let's discuss them one by one.
(B) would be your first-at-glance answer since it clearly strikes as a min-heap. This eliminates (D) as well since it says extract-min in O(n) time but we clearly know we can do better. Thus leaving (C), with better options for add/delete operations, that is O(1). If you can think of combining multiple min-heaps (roots and children) with a circular doubly linked list, keeping track of a pointer to the root containing the minimum key in a data structure, i.e. a fibonacci-heap, you know that option (C) is possible and since its better than option (B), you have your answer.
Let's explore all the answers.
A is impossible because you can't find the Min in O(1) . Because obviously you should find it before removing it. And you need to do some operations to find it.
B is wrong also because we know that adding is O(1). The delete is also O(1) because we can directly access the last and the 1st elements.
By the same argument D is also wrong.
So we're left with C.
Like you can see here, I have this algorithm and I wanna know if the time complexity here is o(n) or o(n²).
public static boolean isTextPalindrome(String text) {
if (text == null) {
return false;
}
int left = 0;
int right = text.length() - 1;
while (left < right) {
if (text.charAt(left++) != text.charAt(right--)) {
return false;
}
}
return true;
}
In the worst case the complexity of this algorithm is O(n) as it is directly dependend on the input but only linearly. In that case we can ignore the impact of the if condition as it has only constant impact on the complexity.
The best case would be if you input a word that starts with a letter that it does not end with. But as you cannot rely on that we are more interested in this worst case to have an upper limit of the complexity.
I would suggest as a rule of thumb: if you have a loop which depends on the input you have a O(n) complexity. If there is another loop in the first one that is also dependend on the outer one the complexity increases to O(n^2). And so forth for more nested loops.
This code is for finding the loop in a single linked list and i have learned about it from http://blog.ostermiller.org/find-loop-singly-linked-list but could not get my head around why the code has been written the way it has been written.
This solution was devised by Stephen Ostermiller and proven O(n) by Daniel Martin.
function boolean hasLoop(Node startNode){
Node currentNode = startNode;
Node checkNode = null;
int since = 0;
int sinceScale = 2;
do {
if (checkNode == currentNode) return true;
if (since >= sinceScale){
checkNode = currentNode;
since = 0;
sinceScale = 2*sinceScale;
}
since++;
} while (currentNode = currentNode.next());
return false;
}
At last this was mentioned as well:
This solution is O(n) because sinceScale grows linearly with the number of calls to next(). Once sinceScale is greater than the size of the loop, another n calls to next() may be required to detect the loop.
This is Brent's cycle-finding algorithm. https://en.wikipedia.org/wiki/Cycle_detection#Brent%27s_algorithm
I like it better than Floyd's algorithm for most purposes. It does indeed work in O(N) time:
It takes O(N) steps to get currentNode into the looping part of the list.
It will then take O(N) more steps until since == sinceScale, and checkNode is set to currentNode
From that point forward, checkNode and currentNode are both in the loop. As sinceScale gets larger, the frequency at which checkNode is reset decreases. When it's big enough, checkNode will remain constant until currentNode goes all the way around the loop and the cycle is detected. Scaling sinceScale by 2 every time ensures that this happens in O(N) as well.
For finding cycles in a linked list, either Floyd's algorithm or Brent's algorithm work fine, but Brent's algorithm is more convenient in a lot of real-life situations when getting from the current state to the next state is expensive and it would be impractical to move the second "slow" pointer that Floyd's algorithm requires.
This is my solution to the problem, where, given a Binary Tree, you're asked to find, the total sum of all non-directly linked nodes. "Directly linked" refers to parent-child relationship, just to be clear.
My solution
If the current node is visited, you're not allowed to visit the nodes at the next level. If the current node, however, is not visited, you may or may not visit the nodes at the next level.
It passes all tests. However, what is the run time complexity of this Recursive Binary Tree Traversal. I think it's 2^n because, at every node, you have two choices, whether to use it, or not use it, and accordingly, the next level, would have two choices for each of these choices and so on.
Space complexity : Not using any additional space for storage, but since this is a recursive implementation, stack space is used, and the maximum elements in the stack, could be the height of the tree, which is n. so O(n) ?
public int rob(TreeNode root) {
return rob(root, false);
}
public int rob(TreeNode root, boolean previousStateUsed) {
if(root == null)
return 0;
if(root.left == null && root.right == null)
{
if(previousStateUsed == true)
return 0;
return root.val;
}
if(previousStateUsed == true)
{
int leftSumIfCurrentIsNotUsedNotUsed = rob(root.left, false);
int rightSumIfCurrentIsNotUsed = rob(root.right, false);
return leftSumIfCurrentIsNotUsedNotUsed + rightSumIfCurrentIsNotUsed;
}
else
{
int leftSumIfCurrentIsNotUsedNotUsed = rob(root.left, false);
int rightSumIfCurrentIsNotUsed = rob(root.right, false);
int leftSumIsCurrentIsUsed = rob(root.left, true);
int rightSumIfCurrentIsUsed = rob(root.right, true);
return Math.max(leftSumIfCurrentIsNotUsedNotUsed + rightSumIfCurrentIsNotUsed, leftSumIsCurrentIsUsed + rightSumIfCurrentIsUsed + root.val);
}
}
Your current recursive solution would be O(2^n). It's pretty clear to see if we take an example:
Next, let's cross out alternating layers of nodes:
With the remaining nodes we have about n/2 nodes (this will vary, but you can always remove alternating layers to get at least n/2 - 1 nodes worst case). With just these nodes, we can make any combination of them because none of them are conflicting. Therefore we can be certain that this takes at least Omega( 2^(n/2) ) time worst case. You can probably get a tighter bound, but this should make you realize your solution will not scale well.
This problem is a pretty common adaptation of the Max Non-Adajacent Sum Problem.
You should be able to use dynamic programming on this. I would highly recommend it. Imagine we are finding the solution for node i. Let's assume we already have the solution to nodes i.left and i.right and let's also assume we have the solution to their children (i's grandchildren). We now have 2 options for i's max solution:
max-sum(i.left) + max-sum(i.right)
i.val + max-sum(i.left.left) + max-sum(i.left.right) + max-sum(i.right.left) + max-sum(i.right.right)
You take the max of these and that's your solution for i. You can perform this bottom-up DP or use memoization in your current program. Either should work. The best part is, now your solution is O(n)!
Note: This is problem 4.3 from Cracking the Coding Interview 5th Edition
Problem:Given a sorted(increasing order) array, write an algorithm to create a binary search tree with minimal height
Here is my algorithm, written in Java to do this problem
public static IntTreeNode createBST(int[] array) {
return createBST(array, 0, array.length-1);
}
private static IntTreeNode createBST(int[] array, int left, int right) {
if(right >= left) {
int middle = array[(left + right)/2;
IntTreeNode root = new IntTreeNode(middle);
root.left = createBST(array, left, middle - 1);
root.right = createBST(array, middle + 1, right);
return root;
} else {
return null;
}
}
I checked this code against the author's and it's nearly identical. However I am having a hard time with analyzing the time complexity of this algorithm. I know this wouldn't run in O(logn) like Binary Search because you're not doing the same amount of work at each level of recursion. E.G at the first level, 1 unit of work, 2nd level - 2 units of work, 3rd level - 4 units of work, all the way to log2(n) level - n units of work.
So based off that, the number of steps this algorithms takes would be upper bounded by this mathematical expression
which after watching Infinite geometric series, I evaluated to
or 2n which would be in O(n)
Do you guys agree with my work here and that this algorithm would run in O(n) or did I miss something or it actually runs in O(nlogn) or some other function class?
Sometimes you can simplify calculations by calculating the amount of time per item in the result rather than solving recurrence relations. That trick applies here. Start by changing the code to this obviously equivalent form:
private static IntTreeNode createBST(int[] array, int left, int right) {
int middle = array[(left + right)/2;
IntTreeNode root = new IntTreeNode(middle);
if (middle - 1 >= left) {
root.left = createBST(array, left, middle - 1);
}
if (right >= middle + 1) {
root.right = createBST(array, middle + 1, right);
}
return root;
}
Now every call to createBST directly creates 1 node. Since there's n nodes in the final tree, there must be n total calls to createBST and since each call directly performs a constant amount of work, the overall time complexity is O(n).
If and when you get confused in recursion, substitute the recursive call (mentally, of course) as a loop. For example, in your above function, you can imagine the recursive calls to be inside a "while loop". Since, it is now a while loop executed till the time all n nodes are traversed, complexity is O(n).