简体   繁体   中英

Space complexity of dfs

I am trying to analyze the space complexity of the following algorithm:

 /**
 * // This is the interface that allows for creating nested lists.
 * // You should not implement it, or speculate about its implementation
 * public interface NestedInteger {
 *     // Constructor initializes an empty nested list.
 *     public NestedInteger();
 *
 *     // Constructor initializes a single integer.
 *     public NestedInteger(int value);
 *
 *     // @return true if this NestedInteger holds a single integer, rather than a nested list.
 *     public boolean isInteger();
 *
 *     // @return the single integer that this NestedInteger holds, if it holds a single integer
 *     // Return null if this NestedInteger holds a nested list
 *     public Integer getInteger();
 *
 *     // Set this NestedInteger to hold a single integer.
 *     public void setInteger(int value);
 *
 *     // Set this NestedInteger to hold a nested list and adds a nested integer to it.
 *     public void add(NestedInteger ni);
 *
 *     // @return the nested list that this NestedInteger holds, if it holds a nested list
 *     // Return null if this NestedInteger holds a single integer
 *     public List<NestedInteger> getList();
 * }
 */
class Solution {
     public int depthSum(List<NestedInteger> nestedList) {
        return helper(nestedList, 1);

    }
    public int helper(List<NestedInteger> nestedList, int depth){
        if(nestedList == null || nestedList.size() == 0){
            return 0;
        }
         int sum = 0;
         for(NestedInteger list : nestedList){
             if(list.isInteger()){
                  sum = sum + depth * list.getInteger();
            }
             if(list.getList() != null){
                 sum = sum + helper(list.getList(), depth + 1);
            }
        }
        return sum;

    }
}

The space complexity should be O(D) where $D$ is the maximal level of nesting in the input. Why is that true?

My analysis: According to Google, space complexity refers to how much memory, in the worst case, is needed at any point in the algorithm. Because Java is passed by value, then each time we call the helper function we need extra space for the input, so the most memory we use will be for when we first call the helper where it takes up a space equal to the space used to hold the input which does not seem to be O(D).

Since you're determining space complexity by the depth of the call stack plus any additional data structures the routine allocates, then I agree that DFS on a nested list is O(d) where d is the maximum depth of the tree. Let's look at a DFS on a typical tree:

    a
   / \
  b   e
 / \
c   d 

The DFS will call the recursive function with a as the root:

    a

Its first child will be visited:

    a
   /
  b
    a
   /
  b
 /
c

At this point, the DFS hits a leaf and begins to backtrack. We've hit the maximum memory that the DFS will consume.

    a
   /
  b

The next leaf matches but does not exceed the max depth of the tree:

    a
   /
  b
   \
    d
    a
   /
  b

Now visit the right subtree of the root:

    a
     \
      e
    a

And we're done. Remember that a call stack frame is created for every node and then popped after the node is visited.

Now that we agree that no more than d stack frames are live at any moment in time, the next step is to determine how large a single stack frame is. If we convince ourselves that it's O(1) , that is, the size of the input has no impact on the stack frame size, then our overall complexity is O(d) .

The answer to this question is that although Java is pass-by-value, the "value" is not a copy of the list data structure that's in memory. Rather, it's just a reference to the data structure. References are constant-sized, so there's a fixed overhead per frame.

.-------------- heap memory ---------------.
| ... [the actual list data in memory] ... |
`--------------------^---------------------`
                     |
    +----------------+
    |                |
.---|- frame 0 ----. |
|  nestedList sum  | |
`------------------` |
                     |
    +----------------+ 
    |                |
.---|- frame 1 ----. |
|  nestedList sum  | |
`------------------` |
                     |
    +----------------+ 
    |
.---|- frame 2 ----.
|  nestedList sum  |
`------------------`

The above graphic is oversimplified; these list references aren't all pointing to the outer list, but rather sub-lists of the outer list, so the actual situation is more like:

.------------------ heap memory ------------------.
| ... [list] ... [list] ... [list] ... [list] ... |
`-------^------------^--------^-------------------`
        |            |        |
    +---+            |        |
    |                |        |
.---|- frame 0 ----. |        |
|  nestedList sum  | |        |
`------------------` |        |
                     |        |
    +----------------+        |
    |                         |
.---|- frame 1 ----.          |
|  nestedList sum  |          |
`------------------`          |
                              |
    +-------------------------+ 
    |
.---|- frame 2 ----.
|  nestedList sum  |
`------------------`

However, all of the heap memory was allocated before the function ran; there's no new anywhere in the function (and no functions are called that themselves call new ). It's a pure traversal/search routine with a couple of tiny variables pointing to memory.

If you're still not convinced, you can use a memory profiler to run this code with different data structures and plot the result. This should give a linear graph proportional to the depth of the data structure.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM