I came cross a problem on an online judge as follows:
Check two given binary trees are identical or not. Assuming any number of tweaks are allowed. A tweak is defined as a swap of the children of one node in the tree.
I came up with the following naive algorithm which was accepted.
/**
* #aaram a, b, the root of binary trees.
* #return true if they are tweaked identical, or false.
*/
bool isTweakedIdentical(TreeNode* a, TreeNode* b) {
if (!a || !b)
return !a && !b;
return a->val == b->val &&
((isTweakedIdentical(a->left, b->left) && isTweakedIdentical(a->right, b->right)) ||
(isTweakedIdentical(a->left, b->right) && isTweakedIdentical(a->right, b->left)));
}
However, I can't figure out the time complexity of my solution. Can anyone teach me how to analyze it? I'm not sure when worst case happens.
For starters, let's imagine that you're working with a perfect binary tree with n nodes in it. At each level, you fire off at most four recursive calls, each to a binary tree that has (roughly) n / 2 nodes in it. You're doing O(1) work per call, so we get the recurrence relation
T(n) ≤ 4T(n / 2) + O(1)
Using the Master Theorem, we're in a case where a = 4, b = 2, and d = 0, and since logb a = log2 4 = 2 and d = 0, we see that the runtime is O(n2) in this case.
The problem with this upper bound is that it's not tight. In particular, looking at the structure of the recursive calls that get made, we only get four recursive calls if no short-circuiting exists. That would require the first call in each of the two cases to return true and the second branch to return false. I've spent the last half hour trying to craft a worst-case tree and prove that it's a worst-case tree, but I'm having a heck of a time doing so. I suspect that the answer is somewhere between O(nlog2 3), which is what you get at three calls per node, and O(n2), but I'm not certain about it. Sorry!
This depends on how "binary" you expect the tree to be.
If the trees constructed tend to be more linearly chained, then your algorithm will tend to perform linearly, O(N) (with N non-tail-call recursions which could potentially lead to stack overflow with large trees).
If your trees constructed tend to be more binary, as in both children have roughly equal tree sizes at each node, then your algorithm will perform logarithmically quadratically, O(log2(N)) O(N^2).
Your worst case is when a is linear down the right side, and b is linear down the left side. You could improve your average case by pseudorandomly choosing which paths to check first, but at the cost of a call to rand() for each call to your function. I don't know that it would be worth the cost, but it's just a suggestion.
EDIT
Consider this example for binary trees:
_1_
/ \
_2 3_
/ | | \
4 5 6 7
/| |\ /| |\
8 9 A B C D E F
_1_
/ \
_3 2_
/ | | \
7 6 5 4
/| |\ /| |\
F E D C B A 9 8
These are two examples of heaps. And they match, except they're mirrored. This would take 43 calls to isTweakedIdentical() in order to return true. But this seems to be an O(N) operation, considering it averages 3 calls per node (except for the root). I'll finish editing this later.
Related
Let me explain as best as i can. This is about binary tree using vector.
According to author, the implementation is as follows:
A simple structure for representing a binary tree T is based on a way of numbering
the nodes of T. For every node v of T, let f(v) be the integer defined as follows:
• If v is the root of T, then f(v) = 1
• If v is the left child of node u, then f(v) = 2 f(u)
• If v is the right child of node u, then f(v) = 2 f(u)+ 1
The numbering function f is known as a level numbering of the nodes in a binary
tree T, because it numbers the nodes on each level of T in increasing order from
left to right, although it may skip some numbers (see figures below).
Let n be the number of nodes of T, and let fM be the maximum value of f(v)
over all the nodes of T. The vector S has size N = fM + 1, since the element of S at
index 0 is not associated with any node of T. Also, S will have, in general, a number
of empty elements that do not refer to existing nodes of T. For a tree of height h,
N = O(2^h). In the worst case, this can be as high as 2^n − 1.
Question:
The last statement worst case 2^n-1 does not seem right. Here n=number of nodes. I think he meant 2^h-1 instead of 2^n-1. Using figure a) as an example, this would mean 2^n -1 means 2^15-1 = 32768-1 = 32767. Does not make sense.
Any insight is appreciated.
Thanks.
The worst case is when the tree is degenerated to a chain from the root, where each node has two children, but at least one of which is always a leaf. When this chain has n nodes, then the height of the tree is n/2. The vector must span all the levels and allocate room for full levels, even though there is in this degenerate tree only one node per level. The size S of the vector will still be O(2h), but now that in this degenerate case h is O(n/2) = O(n), this makes it O(2n) in the worst case.
The formula 2n-1 seems to suggest the author does not have a proper binary tree in mind, and then the above reasoning should be done with a degenerate tree that consists of a single chain where every node has at the most one child.
Example of worst case
Here is an example tree (not a proper tree, but the principle for proper trees is similar):
1
/
2
\
5
\
11
So n = 4, and h = 3.
The vector however needs to store all the slots where nodes could have been, so something like this:
_____ 1 _____
/ \
__2__ __ __
/ \ / \
_5_
/ \ / \ / \ / \
11
...so the vector has a size of 1+2+4+8 = 15. (Even 16 when we account for the unused slot 0 in the vector)
This illustrates that the size S of the vector is always O(2h). In this worst case (worst with respect to n, not with respect to h), S is O(2n).
Example n=6
When n=6, we could have this as a best case:
1
/ \
2 3
/ \ \
4 5 7
This tree can be represented by a vector of size 8, where the entries at index 0 and index 6 are filled with nulls (unused).
However, for n=6 we could have a worst case ("worst" for the impact on the vector size) when the tree is very unbalanced:
1
\
2
\
3
\
4
\
5
\
7
Now the tree's height is 5 instead of 2, and the vector needs to put that node 7 in the slot at index 63... S is 64. Remember that the vector spans each complete binary level, which doubles in size at each next level.
So when n is 6, S can be 8, 16, 32, or 64. It depends on the shape of the tree. In each case we have that S=O(2h). But when we express S in terms of n, then there is variation, and the best case is that S=O(n), while the worst case is S=O(2n).
Assume one has a binary search tree B, which is not necessarily balanced, over some domain D with the strict order relation < and n elements.
Given B's extracted pre-order R, post-order T:
Is it possible to compute B's in-order S in O(n) without access to <?
Is it possible to compute B's in-order S using only O(n) comparisons with <?
Furthermore is it possible to compute S in O(n) total?
Note: This is a re-post of a now-deleted unanswered question.
1. In-order without relation <
This is impossible as explained in Why it is impossible to construct Binary Tree with Pre-Order, Post Order and Level Order traversals given?. In short, the input R = cba, T = abc is ambiguous and could stem from both these trees:
a a
/ \
b b
\ /
c c
S = bca S = acb
2. In-order in O(n) comparisons
Using the < relation, one can suddenly differentiate trees like the above although the produce the same pre-order R and post-order T. Given:
R = Ca
with C some arbitrary non-empty range of a's children nodes with C = u...v (i.e. range starts with u and ends with v) one can infer the following:
(1) a < u -> a has 1 direct child (to its right) -> all children are greater than a
(2) v < a -> a has 1 direct child (to its left) -> all children are less than a
(3) otherwise -> a has 2 direct children
Recursion in (1) and (2) is trivial and we have spent O(1) <. In case of (3) we have something of the form:
R = XbYca
T = abXcY
where X and Y are arbitrary sequences. We can split this into the recursion steps:
R = XbYca
T = abXcY
/ \
R = Xb R = Yc
T = bX T = cY
Note that this requires no comparisons with <, but requires to split both ranges. Since X and Y need not be the same length, finding the splitting-point requires us to find b in R, which can be done in O(n) for each level of the recursion tree. So in total we need O(n*d) equality comparisons, where d is the depth of the original binary search tree B (and the recursion tree which mirrors B).
In each recursion step we use at most 2 < comparisons, getting out one element of the range, hence we cannot use more than 2*n < comparisons (which is in O(n)).
3. In-order in O(n) total
In the algorithm given above the problem is that finding the point where to split the range containing all children cannot be done better than in linear time if one cannot afford a lookup table for all elements.
But, if the universe over which B is defined is small enough to be able to create an index table for all entries, one can pre-parse R (e.g. R = xbyca) in O(n) and create a table like this:
a -> 4
b -> 1
c -> 3
d -> N/A
e -> N/A
....
x -> 0
y -> 2
z -> N/A
Only if this is feasible one can achieve overall O(n) with the algorithm described in 2. It consume O(2^D) space.
Is it otherwise possible to produce the in-order S in O(n).
I don't have a proof for this, but do not think it is possible. The rationale is that the problem is too similar to comparison sorting which cannot be better than linearythmic.
In "2." we get away with linear comparisons because we can exploit the structure of the input in conjunction with a lot of equality checks to partially reconstruct the original structure of the binary search tree at least locally. However, I don't see how the size of each sub-tree can be extracted in less than linear time.
I need to suggest an algorithm that takes BST (Binary Search Tree), T1 that has 2^(n + 1) - 1 keys, and build an AVL tree with same keys. The algorithm should be effective in terms of worst and average time complexity (as function of n).
I'm not sure how should I approach this. It is clear that the minimal size of a BST that has 2^(n + 1) - 1 keys is n (and that will be the case if it is full / balanced), but how does it help me?
There is the straight forward method that is to iterate over the tree , each time adding the root of T1 to the AVL tree and then removing it from T1:
Since T1 may not be balanced the delete may cost O(n) in worst case
Insert to the AVL will cost O(log n)
There are 2^(n + 1) - 1
So in total that will cost O(n*logn*2^n) and that is ridiculously expensive.
But why should I remove from T1? I'm paying a lot there and for no good reason.
So I figured why not using tree traversal over T1 , and for each node I'm visiting , add it to the AVL tree:
There are 2^(n + 1) - 1 nodes so traversal will cost O(2^n) (visiting each node once)
Adding the current node each time to the AVL will cost O(logn)
So in total that will cost O(logn * 2^n).
and that is the best time complexity I could think of, the question is, can it be done in a faster way? like in O(2^n) ?
Some way that will make the insert to the AVL tree cost only O(1)?
I hope I was clear and that my question belongs here.
Thank you very much,
Noam
There is an algorithm that balances a BST and runs in linear time called Day Stout Warren Algorithm
Basically all it does is convert the BST into a sorted array or linked list by doing an in-order traversal (O(n)). Then, it recursively takes the middle element of the array, makes it the root, and makes its children the middle elements of the left and right subarrays respectively (O(n)). Here's an example,
UNBALANCED BST
5
/ \
3 8
/ \
7 9
/ \
6 10
SORTED ARRAY
|3|5|6|7|8|9|10|
Now here are the recursive calls and resulting tree,
DSW(initial array)
7
7.left = DSW(left array) //|3|5|6|
7.right = DSW(right array) //|8|9|10|
7
/ \
5 9
5.left = DSW(|3|)
5.right = DSW(|6|)
9.left = DSW(|8|)
9.right = DSW(|10|)
7
/ \
5 9
/ \ / \
3 6 8 10
I am reading the book Artificial Intelligence: A Modern Approach. I came across this sentence describing the time complexity of uniform cost search:
Uniform-cost search is guided by path costs rather than depths, so its
complexity is not easily characterized in terms of b and d. Instead,
let C be the cost of the optimal solution, and assume that every
action costs at least ε. Then the algorithm’s worst-case time and
space complexity is O(b^(1+C/ε)), which can be much greater than b^d.
As to my understanding, C is the cost of the optimal solution, and every action costs at least ε, so that C/ε would be the number of steps taken to the destination. But I don't know how the complexity is derived.
If the branching factor is b, every time you expand out a node, you will encounter k more nodes. Therefore, there are
1 node at level 0,
b nodes at level 1,
b2 nodes at level 2,
b3 nodes at level 3,
...
bk nodes at level k.
So let's suppose that the search stops after you reach level k. When this happens, the total number of nodes you'll have visited will be
1 + b + b2 + ... + bk = (bk+1 - 1) / (b - 1)
That equality follows from the sum of a geometric series. It happens to be the case that bk+1 / (b - 1) = O(bk), so if your goal node is in layer k, then you have to expand out O(bk) total nodes to find the one you want.
If C is your destination cost and each step gets you ε closer to the goal, the number of steps you need to take is given by C / ε + 1. The reason for the +1 is that you start at distance 0 and end at C / ε, so you take steps at distances
0, ε, 2ε, 3ε, ..., (C / ε)ε
And there are 1 + C / ε total steps here. Therefore, there are 1 + C / ε layers, and so the total number of states you need to expand is O(b(1 + C / ε)).
Hope this helps!
templatetypedef's answer is somewhat incorrect. The +1 has nothing to do with the fact that the starting depth is 0. If every step cost is at least ε > 0, and the cost of optimal solution is C, then the maximum depth of the optimal solution occurs at floor(C / ε). But the worst case time/space complexity is in fact O(b(1+floor(C/ε)). The +1 arises because in UCS, we only check whether a node is a goal when we select it for expansion, and not when we generate it (this is to ensure optimality). So in the worst case, we could potentially generate the entire level of nodes that comes after the goal node's residing level (this explains the +1). In comparison, BFS applies the goal test when nodes are generated, so there is no corresponding +1 factor. This is a very important point that he missed.
I am just studying for my class in Algorithms and have been looking over QuickSort. I understand the algorithm and how it works, but not how to get the number of comparisons it does, or what logn actually means, at the end of the day.
I understand the basics, to the extent of :
x=logb(Y) then
b^x = Y
But what does this mean in terms of algorithm performance? It's the number of comparisons you need to do, I understand that...the whole idea just seems so unintelligible though. Like, for QuickSort, each level K invocation involves 2^k invocations each involving sublists of length n/2^K.
So, summing to find the number of comparisons :
log n
Σ 2^k. 2(n/2^k) = 2n(1+logn)
k=0
Why are we summing up to log n ? Where did 2n(1+logn) come from? Sorry for the vagueness of my descriptions, I am just so confused.
If you consider a full, balanced binary tree, then layer by layer you have 1 + 2 + 4 + 8 + ... vertices. If the total number of vertices in the tree is 2^n - 1 then you have 1 + 2 + 4 + 8 + ... + 2^(n-1) vertices, counting layer by layer. Now, let N = 2^n (the size of the tree), then the height of the tree is n, and n = log2(N) (the height of the tree). That's what the log(n) means in these Big O expressions.
below is a sample tree:
1
/ \
2 3
/ \ / \
4 5 6 7
number of nodes in tree is 7 but high of tree is log 7 = 3, log comes when you have divide and conquer methods, in quick sort you divide list into 2 sublist, and continue this until rich small lists, divisions takes logn time (in average case), because the high of division is log n, partitioning in each level takes O(n), because in each level in average you partition N numbers, (may be there are too many list for partitioning, but average number of numbers is N in each level, in fact some of count of lists is N). So for simple observation if you have balanced partition tree you have log n time for partitioning, which means high of tree.
1 forget about b-trees for sec
here' math : log2 N = k is same 2^k=N .. its the definion of log
, it could be natural log(e) N = k aka e^k = n,, or decimal log10 N = k is 10^k = n
2 see perfect , balanced tree
1
1+ 1
1 + 1 + 1+ 1
8 ones
16 ones
etc
how many elements? 1+2+4+8..etc , so for 2 level b-tree there are 2^2-1 elements, for 3 level tree 2^3-1 and etc.. SO HERE'S MAGIC FORMULA: N_TREE_ELEMENTS= number OF levels^ 2 -1 ,or using definition of log : log2 number OF levels= number_of_tree_elements (Can forget about -1 )
3 lets say there's a task to find element in N elements b-tree, w/ K levels (aka height)
where how b-tree is constructed log2 height = number_of_tree elements
MOST IMPORTANT
so by how b-tree is constructed you need no more then 'height' OPERATIONS to find element in all N elements , or less.. so WHAT IS HEIGHT equals : log2 number_of_tree_elements..
so you need log2 N_number_of_tree_elements.. or log(N) for shorter
To understand what O(log(n)) means you might wanna read up on Big O notaion. In shot it means, that if your data set gets 1024 times bigger you runtime will only be 10 times longer (or less)(for base 2).
MergeSort runs in O(n*log(n)), which means it will take 10 240 times longer. Bubble sort runs in O(n^2), which means it will take 1024^2 = 1 048 576 times longer. So there are really some time to safe :)
To understand your sum, you must look at the mergesort algorithm as a tree:
sort(3,1,2,4)
/ \
sort(3,1) sort(2,4)
/ \ / \
sort(3) sort(1) sort(2) sort(4)
The sum iterates over each level of the tree. k=0 it the top, k= log(n) is the buttom. The tree will always be of height log2(n) (as it a balanced binary tree).
To do a little math:
Σ 2^k * 2(n/2^k) =
2 * Σ 2^k * (n/2^k) =
2 * Σ n*2^k/2^k =
2 * Σ n =
2 * n * (1+log(n)) //As there are log(n)+1 steps from 0 to log(n) inclusive
This is of course a lot of work to do, especially if you have more complex algoritms. In those situations you get really happy for the Master Theorem, but for the moment it might just get you more confused. It's very theoretical so don't worry if you don't understand it right away.
For me, to understand issues like this, this is a good way to think about it.