5

I came across an interesting problem:

Given a binary tree print it in inward spiral order i.e first print level 1, then level n, then level 2, then n-1 and so on.

For Ex:
1
2 3
4 5 6 7
8 9 10 11 12 13 14 15

Should Output:
1 15 14 13 12 11 10 9 8 2 3 7 6 5 4

I have thought of a solution:

  • Store each level elements in a list
    lists[0] = [1]
    lists[1] = [2, 3]
    lists[2] = [4, 5, 6, 7]
    lists[3] = [8, 9, 10, 11, 12, 13, 14, 15]

  • Loop through the list of lists in the order required (0, n-1, 1, n-2, ...) and print. Here n is the number of levels which is 4 in the above case.

Its space complexity would be O(n). I am sure there might be a better solution to it with a better space complexity, but I cannot think if it. Anyone has any pointers?

Atri
  • 5,511
  • 5
  • 30
  • 40
  • If you are going to go about storing items, you might as well use a hashMap which will give you O(1) lookup performance. – Michael Queue Dec 14 '15 at 02:10
  • @MichaelQueue HashMap doesn't make a difference in this case. Even array of list will give me O(1) lookup for each list. – Atri Dec 14 '15 at 02:13
  • Hi Atri, if you have the time, can you please explain how the accepted answer is space O(1). Does the "print node" not imply that O(n) space is required to fit the solution into the print output? Thank you. – Ian Mc Jan 21 '16 at 13:57

5 Answers5

3

You don't need to store nodes level by level, you can solve the task by storing all nodes in a deque structure, and also maintaining node's level in an array.

Deque <Integer> deque = new LinkedList();
Queue <Integer> q = new LinkedList();
int[]level = new int[n];//we need to store the level of each node, n is number of node
q.add(0);//Adding the root node
while(!q.isEmpty()){
    int node = q.deque();
    for(int child : getNodeChildren(node)){
         level[child] = level[node] + 1;//Keep track the level of child node
         q.add(child);
         deque.add(child);//Store the child node into the queue.
    }
}
// Print output
boolean printHead = true;
while(!deque.isEmpty()){
   if(printHead){
      int node = deque.pollFirst();
      print node;
      //Check if we already printed every node in this current level, 
      //if yes, we need to switch to print from the end of the queue
      if(!deque.isEmpty() && level[deque.getFirst()] != level[node]){
          printHead = false;
      }
   }else{
      int node = deque.pollLast();
      print node;
      if(!deque.isEmpty() && level[deque.getLast()] != level[node]){
          printHead = true;
      }
   } 
}

The logic of the code is:

We keep printing from the beginning of the deque, if the next node is not in the same level as the last printed node, which also means that we just printed all elements of the current level, we need to switch to print from the end of the deque, vice versa. We continue this process until all nodes are printed.

Space complexity is O(n), time complexity is O(n).

Pham Trung
  • 11,204
  • 2
  • 24
  • 43
  • Good implementation. Thanks for the input, though I am looking for an algorithm (if it exists) where space complexity can be less than O(n). I was thinking on the lines of storing only the 1st level and last level somehow, and then repeating recursively for 2nd and n-1 level etc. I found an algorithm but its time complexity is more than O(n). – Atri Dec 14 '15 at 05:57
  • @ashutosh yes, there is always decision need to be made between time and space, however, I don't think you can do better than O(n), as we always need O(n) space complexity to store the tree :). And talking about language wise, in Java, to keep a list of array list to store nodes, IMO, it will take a lot of overhead more than just using a deque. – Pham Trung Dec 14 '15 at 06:05
  • 2
    Space complexity doesn't include the input. Its about all the extra space(other than input) that you need while computing. Otherwise there could never be O(1) space complexity solutions. Ex: Searching a number in an array. – Atri Dec 14 '15 at 06:58
  • @Atri I don't think so, for your example, searching a number in an array, so, you don't need to store all data, but only need to fetch then remove data one by one, which achieve O(1) space. Space complexity here is the number of RAM memory has been used for the program. – Pham Trung Jan 18 '16 at 03:43
  • 1
    But as per your logic => *we always need O(n) space complexity to store the tree*, the same way for searching a number in an array you will need to store the array of numbers and would never be space O(1). – Atri Jan 18 '16 at 04:59
2

Here's a simple solution with O(1) space and O(n * h) time, where n is the number of nodes and h is the tree height. Keep two variables pointing to the current levels to print, then print each node after traversing the tree according to a binary string who's length corresponds to the appropriate tree depth; go left for "0", right for "1" (for example, the left-most node in your example is reached by "000", its neighbor is at "001"). Since to print one node, we must make at most h iterations, time complexity is O(n * h).

Here is the algorithm in slightly more detailed words:

down = 0
up = tree height

while:
  if down > up:
    break
  else:
    set exp to down then up (or only one if up == down):
      for i = 0 to 2^exp - 1:
        s = binary string i, padded to exp digits
        traverse tree according to s
        if a node exists by the end of s:
          print node
    down = down + 1
    up = up - 1

I'm not sure if it would be an order of magnitude faster, but you could use the same principle walking within the tree, rather than starting from the root for each node: go back up until a "0" is encountered, flip it to "1", then keep left all the way down to the target level and repeat recursively until the string is all "1"s. For example, target level 4:

0000 => (000)1 => (00)10 => (001)1 => (0)100...etc.
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
1

I would like to start by thanking the poster for his question, and presenting a practical example of how time complexity and space complexity oppose one another. Given a large amount of space, your algorithm can be extremely fast (because you can create 'state'). Conversely, if you are very restricted in space, you will find it necessary to perform extra iterations to compensate for lack of state.

It turns out that this problem is easy to solve in space complexity O(n). If you are permitted to create space for the result (O(n)), there are several algorithms that can create a solution in time complexity O(n). One solution is posted (which I like), and I also provide a different, and perhaps elegant approach to also solve in O(n) and in time complexity O(n); refer to method solveOrderN().

The challenge is how to find a solution in space complexity less than O(n). To do this, you must not be permitted to create a result space, and you are forced as a result to operate within the tree space given in the question. You are forced to swap elements around.

The solution I have provided – solveSubOrderN() - does not create any result space; the answer is returned in the same memory space as the question.

I was very optimistic I could solve this problem in O(log base 2(n)) and even in time complexity close to O(n). But after much analysis, I can not get this aggressive.

When you begin to swap elements, you will eventually hit a 'halting condition', when you circle back to a non-processable element. If this halt did not exist, you could achieve O(log base 2 n). But you can't avoid it. So to compensate for this, I was forced to create some space (a boolean array) which represents the state of an element being processed.

I would like to discuss how this boolean state is much different than creating space for a result/solution. When you create for a solution (of n elements, of size s), you are creating space=n*s. In this question, s is an Integer. In general s can be very large, which makes it an 'expensive'' process (and the reason the poster created this problem). The space for an array of boolean is considerably less, and even arguably negligible as s grows in size (i.e. n/s approaches 0 as s grows).

It also turns out that you can not achieve time complexity O(n) when space complexity is less than O(n). You are forced to iterate again when you hit a halting condition. But it turns out that for a binary tree, the amount of extra iterations are small (and each iteration is to simply find the next start point).

So in summary, please find below two solution s; One has space complexity O(n) and time complexity O(n), but more importantly one which has a space complexity less than O(n), with a time complexity very close to O(n).

public class BinaryTree {

public static void main(String[] args) {

    int treeN2[] = {1, 2, 3};
    int treeN3[] = {1, 2, 3, 4, 5, 6, 7};
    int treeN4[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
    int treeN5[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};

    int answer[] = solveOrderN(treeN5);
    System.out.println(Arrays.toString(answer));
    solveSubOrderN(treeN5);
    System.out.println(Arrays.toString(treeN5));

}

/**
 * Given a binary tree, Perform inward spiral tree traversal.<br>
 * With this approach, there is no space created for the result.
 * All manipulation is done within the original tree<br>
 * Some space is created (boolean[n]) to add necessary state of processing.
 * Space Complexity:  Less than O(n), greater than log(base2)(n)
 * Time Complexity:  Slightly greater than O(n)
 * @param tree Input tree
 */
public static void solveSubOrderN(int tree[]) {

    boolean complete[] = new boolean[tree.length];
    Arrays.fill(complete, false);

    System.out.println("Solving Sub O(n); tree size="+tree.length);
    int n = log2Round(tree.length+1);

    if (n == 1)
        return;

    int o[] = getLevelOffsets(n);
    System.out.println("Number of levels="+n);

    int pos = 0;
    complete[0] = true;
    int currentValue = 0;
    int moves=1;

    while (moves < tree.length) {
        pos = getStartingPos(complete);
        currentValue = tree[pos];
        tree[pos] = 0;
        while (true) {
            int nextPos = getTargetPosition(pos, o, n);
            int nextValue = tree[nextPos];
            tree[nextPos] = currentValue;
            complete[nextPos] = true;
            currentValue = nextValue;
            pos = nextPos;
            moves++;
            if (currentValue == 0)
                break;
        }
    }
}

/**
 * Given a binary tree, Perform inward spiral tree traversal.
 * Space Complexity: O(n)
 * Time Complexity: O(n)
 * @param tree Input tree
 * @return The solution
 */
public static int[] solveOrderN(int tree[]) {
    int answer[] = new int[tree.length];
    int n = log2Round(tree.length+1);
    int o[] = getLevelOffsets(n);
    System.out.println("Solving O(n); tree size="+tree.length);
    System.out.println("Number of levels="+n);

    for (int i = 0; i < tree.length; i++) {
        answer[getTargetPosition(i, o, n)] = tree[i];
    }
    return answer;
}

/**
 * Search for the first unprocessed element
 * @param complete An array of boolean (true = processed)
 * @return
 */
public static int getStartingPos(boolean[] complete) {
    for (int i=0; i<complete.length; i++) {
        if (!complete[i])
            return i;
    }
    return 1;
}

public static int getTargetPosition(int pos, int o[], int n) {
    int row = getRow(pos);
    int rowOrder = getRowOrder(row, n);
    boolean isReversed = isBottom(row, n);
    int posInRow = getPosInRow(pos, n);
    int rowOffset = getRowOffset(rowSize(row), posInRow, isReversed);
    return o[rowOrder]+rowOffset;
}

public static int getRowOffset(int rowSize, int posInRow, boolean isReversed) {
    if (!isReversed)
        return posInRow;
    else
        return rowSize-posInRow-1;
}

public static int rowSize(int row) {
    return exp(row, 2);
}

public static int getPosInRow(int pos, int n) {
    int row = getRow(pos);
    return pos-(exp(row,2)-1);
}

/**
 * The top n/2 rows print forward, the bottom n/2 rows print reversed
 * @param row Zero based row [0 to n-1]
 * @param n Number of levels to the tree
 * @return true if line should be printed forward, false if reversed
 */
public static boolean isBottom(int row, int n) {
    int halfRounded = n/2;
    return (row <= n-halfRounded-1) ? false : true;
}

public static int exp(int n, int pow) {
    return (int)Math.pow(pow, n);
}

public static double log2(int n) {
    return (Math.log(n) / Math.log(2));
}

public static int log2Round(int n) {
    return (int)log2(n);
}

/**
 * For a given position [0 to N-1], find the level on the binary tree [0 to n-1]
 * @param pos Zero based position in the tree (0 to N-1)
 * @return Zero based level (0 to n-1)
 */
public static int getRow(int pos) {
    return log2Round(pos+1);
}

/**
 * For a given row [0 to n-1], find the order in which that line would be processed [1 to log base 2 n]
 * @param row The row in the tree [0 to n-1]
 * @return The order that row would be processed [0 to n-1]
 */
public static int getRowOrder(int row, int n) {
    return isBottom(row, n) ? (n-row-1)*2+1 : row*2;
}

public static int getRowForOffset(int row, int n) {
    boolean isOdd = row%2 == 1;
    return isOdd ? n-(row-1)/2 - 1 : row/2;
}

/**
 * Compute the offset for a given ordered row
 * @param n The number of levels in the tree
 * @return Generated offsets for each row (to keep time complexity at O(n))
 */
public static int[] getLevelOffsets(int n) {
    int o[] = new int[n];
    Arrays.fill(o, 0);
    o[0] = 0;
    int offset = 0;
    for (int i=0; i<n; i++) {
        int nextRow = getRowForOffset(i, n);
        o[i] = offset;
        offset += exp(nextRow, 2);
    }
    return o;
}

}
Ian Mc
  • 5,656
  • 4
  • 18
  • 25
1

You can resolve it without a significative extra space, but it'll need O(n). The time complexity depend on the representation of binary tree. If you have an array, time complexity will be O(n), example:

public static void main(String[] args) {
    int[] binaryTree = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
    int nodes = binaryTree.length;
    int levels = (int) Math.ceil(Math.log(nodes) / Math.log(2));
    int[] increment = {1, -1};
    int index = 0;
    for (int i = 0; i < levels; i++) {
        int level = (index) * levels + increment[index] * (i / 2) - index;
        int begin = (int) Math.round(Math.pow(2, level));
        int[] range = {begin, 2 * begin - 1};
        for (int j = range[index]; j != range[1 - index]; j += increment[index]) {
            System.out.print(" " + binaryTree[j-1]);
        }
        System.out.print(" " + binaryTree[range[1 - index]-1]);
        index = 1 - index;
    }
    System.out.println();
}

If you have a binary tree with nodes defined as:

publi class Node {
    Node[] children;
    int value;
}

Time complexity (without extra space) will be O(n*log(n)). Example:

public class BinaryNode {
    BinaryNode[] children = {null, null};
    int value;

    public BinaryNode(int value) {
        this.value = value;
    }

    private int getRecursive(int reference) {
        int result = value;
        if (reference > 1) {
            result = children[reference%2].getRecursive(reference/2);
        }
        return result;
    }

    public int get(int p) {
        int reference = 1;
        int p2 = p;
        while (p2 > 1){
            reference = (reference << 1) + (p2 & 1);
            p2 >>= 1;
        }
        return getRecursive(reference);
    }

    private void generateLevels(int levels){
        if (levels > 0){
            children[0] = new BinaryNode(value*2);
            children[0].generateLevel(levels -1);
            children[1] = new BinaryNode(value*2+1);
            children[1].generateLevel(levels -1);
        }
    }

    public static BinaryNode generate(int levels){
        BinaryNode result = null;
        if (levels > 0){
            result = new BinaryNode(1);
            result.generateLevels(levels - 1);
        }
        return result;
    }

    public static void main(String[] args) {

        int levels = 5;
        BinaryNode btreeRoot = BinaryNode.generate(levels);
        int[] increment = {1, -1};
        int index = 0;
        for (int i = 0; i < levels; i++) {
            int level = (index) * levels + increment[index] * (i / 2) - index;
            int begin = (int) Math.round(Math.pow(2, level));
            int[] range = {begin, 2 * begin - 1};
            for (int j = range[index]; j != range[1 - index]; j += increment[index]) {
                System.out.print(" " + btreeRoot.get(j));
            }
            System.out.print(" " + btreeRoot.get(range[1 - index]));
            index = 1 - index;
        }
        System.out.println();
    }
}

EDIT

How do you compute h with n? Easy:

h = log2(n)

Therefore, time complexity: O(n * log n) == O(n * h).

The algorithm obtain an element in O(log(n)), and you need to obtain all (n elements) so, the result time complexity is O(n * log(n)). And the algorithm do it without extra space.

BtreeRoot is the original binary tree, the algorithm obtain all nodes in the correct order, computes the next level in level. level goes to values (1, levels -1, 2, levels - 2) in O(1) (log2(n) times) In every level, the algorithm obtain its m nodes: from 2^level to 2^(level +1) - 1. the sum of all levels is n (number of nodes), Depends on level (odd or pair), it calculates the current node position from beginning to end or reverse. Then you know the position of current node to print, It need to take the value, in a perfect binary tree, it cost Log(n).

David Maust
  • 8,080
  • 3
  • 32
  • 36
David Pérez Cabrera
  • 4,960
  • 2
  • 23
  • 37
1

By saying your space complexity is < O(n), it seems like you are making an assumption about the way in which the tree is represented, and then you cannot augment that representation with any additional data and/or structure in your solution (that is O(n)).

For my answer, I will make the assumption that the tree is complete (or at least balanced), where balanced means that the height of the tree is O(logN).

Under this assumption we can store the tree in a heap data structure. In this structure, the node data is stored in an array, where, the levels are arranged:

 [0][1][1][2][2][2][2][3][3][3][3][3][3][3][3]...

A given location in the array would have a pointer to the node data or a pointer to NULL if that node did not exist in the tree.

The amount of space in this case is O(2^L), where L is the number of levels in the tree. In a balanced tree, L = O(log(N)), so the size of this representation is O(N).

Also assume we know the size of this array, then the number of levels of the tree is ceiling(log_2(size)).

Assuming Levels go from 0...K-1, the starting location of any level in the array is: 2^L - 1.

Here is the pseudocode of the algorithm.

PrintLevel( Array<Node> heap, level) {
    int levelStart = power( 2, level ) - 1;
    int nextLevelStart = (levelStart + 1) * 2 - 1; // same as power( 2, level+1 ) - 1
    for( int i = levelStart; i < nextLevelStart; i++ ) {
        if( heap[i] != NULL ) {
            print( heap[i] );
        }
    }
}

SpiralPrint( Array<Node> heap ) {
      int lowLevel = Ceil(Log(heap.size())) - 1
      int highLevel = 0
      while( lowLevel >= highLevel ){
           PrintLevel( heap, highLevel );
           if (lowLevel > highLevel) 
                PrintLevel( heap, lowLevel );
           highLevel += 1;
           lowLevel -= 1;
      }
  }
Ben Jackson
  • 311
  • 3
  • 3