7

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

enter image description here

which after watching Infinite geometric series, I evaluated to enter image description here

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?

Community
  • 1
  • 1
committedandroider
  • 8,711
  • 14
  • 71
  • 126
  • Yes, it's O(n). I'd make that an Answer if I had a clearer idea of what constitutes a good proof of complexity. – Beta May 29 '15 at 05:36
  • @Beta How were you able to tell without a proof? – committedandroider May 29 '15 at 05:38
  • 1
    I saw that the algorithm calls itself twice, each time on n/2 elements, with O(1) extra work and one element removed. I don't think that's a rigorous proof (as in "Let `f(n)` and `g(n)` be functions such that..."), but it was enough that I could see it in my head, like a piece of string cut into pieces and laid out with no overlap. – Beta May 29 '15 at 12:46
  • @Beta And just to make sure, would space complexity here be O(log n), cause the height of the deepest recursive call is log n? – committedandroider May 29 '15 at 17:00
  • As I understand the term "space complexity", it refers to storage space rather than stack depth. The max depth of the call stack is log(n), but the space complexity is O(n), because that's the size of the resultant tree (and the O(log(n)) overhead *adds* to that, it doesn't multiply). – Beta May 29 '15 at 23:06

2 Answers2

5

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).

Paul Hankin
  • 54,811
  • 11
  • 92
  • 118
  • Beat me to it. I just sat down to write the same argument. – Pieter Geerkens May 29 '15 at 05:54
  • Thanks this part of the argument made sense - "since theres n nodes in the final tree, there must be n total calls to createBST". However how did changing that code up show that each call performs a constant amount of work? One call still could make two calls, that are of the same size, but smaller than the original call. Those calls of the same size are getting smaller each time so each call isn't doing a constant amount of work because of the decreasing call size property? – committedandroider May 29 '15 at 06:34
  • @committedandroider I changed the code so that the calls to createBST and the nodes of the result are one-to-one. In the original code, some calls to createBST returned null. The argument can be adapted to the original code, but it's a slight bit messier: for example, you can argue that there's at most N calls to createBST that create nodes and at most 2N calls that return null. – Paul Hankin May 29 '15 at 06:37
  • @PaulHankin Thanks can you clarify the "each call directly performs a constant amount of work" part? How can a call like createBST(array, left, middle - 1) be part of the constant work when left, and middle are different for each call? – committedandroider May 29 '15 at 06:40
  • 1
    By "direct" I meant "excluding work done in recursive calls". If there's N calls (ie: from the top level or recursively) and the amount of direct work in each is K then the total amount of work is NK. That's the core idea of the trick to avoid recurrence relations. – Paul Hankin May 29 '15 at 06:43
  • But could I make an argument that some calls will enter the if block and others won't. (Not all calls enter the if block). Therefore not all calls do the same amount of work? – committedandroider May 29 '15 at 16:55
1

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).

Tejash Desai
  • 466
  • 4
  • 11
  • But this wouldn't work with something with quicksort. You visit all the elements but the runtime would be O(n log n), not O(n). How would you rephrase your method for this situation? I like what you did with the while loop. – committedandroider May 29 '15 at 16:57
  • Yes, I admit its difficult to imagine loops for algorithms like quick sort which increase logarithmically. However, the idea works perfectly for all exponentially increasing functions like O(n^2), O(n^3), etc. We just need to use nested loops. – Tejash Desai May 29 '15 at 19:58