3

This is based off other question I had on space complexity. Permutation Space Complexity

This is a solution to my problem of enumerating(naming) all sets. (Tested it, it works)

public static void subsets(Set<Integer> s) {
    Queue<Integer> copyToProtectData  = new LinkedList<Integer>();
    for(int member: s) {
        copyToProtectData.add(member);
    }
    generateSubsets(copyToProtectData, new HashSet<Integer>());
}
private static void generateSubsets(Queue<Integer> s, 
         Set<Integer> hashSet) {
    if(s.isEmpty()) {
        System.out.println(hashSet);
    } else {
        int member = s.remove();
        Set<Integer> copy = new HashSet<Integer>();
        for(int i:hashSet) {
            copy.add(i);
        }
        hashSet.add(member);
        Queue<Integer> queueCopy = new LinkedList<Integer>();
        for(int i:s){
            queueCopy.add(i);
        }
        generateSubsets(s, hashSet);
        generateSubsets(queueCopy, copy);           
    }
}

I know that the time complexity of my algorithm of is O(2n) because a solution in discrete math is that a set n has 2n subsets. Is this an acceptable way of evaluating the time complexity of this algorithm(couldn't find a recurrence relation to do this)?

Moving on though, I am still having difficulties of evaluating the space complexity. I am trying to apply what I learned from my last question. In my last question which was on permutations of a String, @ajb said that because of the fact that I store a local string that grows by one on each recursive call, my space complexity is actually O(n2).

I am trying to apply that same here. Lets say my test set is {1,2,3}. To generate the subset {1,2,3}, from my algorithm, when {1,2,3} is finally printed, these "subsets" also exist in memory, - {1}, {}, {1,2},{1],{1,2,3}, {1,2}, meaning its not just a subset that has one less element like in the permutations problem. I also made copies of the leftovers at each round so that one operation in one side won't affect the copy on the other side. This is why I wasn't sure if @ajb's strategy would work here. Would the runtime still be O(n2) or would it be something greater?

Community
  • 1
  • 1
committedandroider
  • 8,711
  • 14
  • 71
  • 126

2 Answers2

1

Typically the way you'd go about analyzing complexity if you want a good bound is through a mix of amortized analysis and other methods - for example, you can try to rewrite the recursion in an iterative form for easier analysis.

To answer your questions more directly: Your runtime is not O(2^n).

These parts of your code is going to increase the complexity to O(n*2^n)

    for(int i:hashSet) {
        copy.add(i);
    }

    for(int i:s){
        queueCopy.add(i);
    }

The reason being that you are going to iterate over not just every subset, but every element of every subset.

With regards to your space complexity question, and assuming garbage collection is on top of things, then yes the space complexity is O(n^2). Even though you are making copies of 2 things instead of just 1, the complexity is still O(n^2) because that only affects the constant factor. If you actually want to save a list of all the subsets, that puts the space complexity all the way up to O(n*2^n) - which you'll still need for your output currently anyway.

  • ummm thanks but how would write this in an iterative form? The way i was taught to do this problem is to use recursion to reduce your set of possibilities til its empty. – committedandroider Mar 28 '15 at 21:56
  • Could you explain that a bit more? Every call allocates space for every iterated element of the two sets. Thus, the amount of allocated space should be proportional to the runtime of a single pass (without the recursive calls). How can the space complexity differ from the time complexity? I'm currently working on solving the recurrence but that seems rather hard. – Nico Schertler Mar 28 '15 at 21:56
  • @NicoSchertler The space complexity can differ from the time complexity if you reuse the space - i.e., if I want to print out the numbers 1 to N, if I make an array to store all of them, and then print them out, that requires O(N) space. If I just have one variable and iterate, printing out as I go, it requires O(1) space. – Alex Anderson Mar 28 '15 at 23:28
  • @committedandroider To write this in iterative form, well, the approach is different. The simplest way I can think of is using a [bitmask](http://en.wikipedia.org/wiki/Mask_%28computing%29) where for a set of size n, you would iterate from 0 to (2^n - 1), and then look at the bits to see what set the mask refers to. For example, for n = 4, we iterate from 0 to 15. Looking at binary: 0000 - empty set 0001 - set containing element 1 0010 - set containing element 2 0011 - set containing elements 1 and 2, etc – Alex Anderson Mar 28 '15 at 23:30
  • Sure, that's what you *could* do (reuse space). But that's not what the code does. Therefore, I don't see why the space complexity should differ. – Nico Schertler Mar 28 '15 at 23:32
  • Since this is Java, and the implementation is recursive, the depth of the recursion will only ever be at most N, and within each call of the function will only ever use ~O(N) space. So if the garbage collection is reliable, then even though the function is called on the order of O(2^n) times, the memory required is continuously allocated and deallocated, only ever requiring O(N^2) space at the same time. – Alex Anderson Mar 28 '15 at 23:33
  • Ok, I agree under that assumption. But let's call it "actively used space". Garbage collection is implementation specific and runs non-deterministically. – Nico Schertler Mar 28 '15 at 23:38
  • Oh i see. Each bit combination represents different elements in the subset. What would the space and time be of this solution? – committedandroider Mar 29 '15 at 00:29
1

You can solve this with a bivariate recurrence relation. The time complexity of a method call depends on the size of s and the size of the hash set (let's call it h). Then:

T(s, h) = O( h               //First loop
             + s - 1)         //Second loop 
           + T(s - 1, h + 1) //First recursive call
           + T(s - 1, h)     //Second recursive call
T(0, h) = O(1)

And we are interested in T(n, 0). The same recurrence relation holds for the space complexity, except that S(0, h) = 0. This assumes that the created sets are never destroyed.

Each instance of T(s, ...) generates two instances of T(s - 1, ...). We start with T(n, ...) and end up with 2^n instances of T(0, ...)=O(1). So

T(n, 0) = 2^n * O(1) + ...

Additionally, we must account for the generated hs and s-1s. Let's start with the s-1s. After the first expansion, there will be 1 n-1. After the second expansions there will be additional two n-2. After the third expansion additional 4 n-3... And n expansions are performed that generate those terms. In total: Sum {i from 1 to n} (2^(i-1)*(n-i)) = 2^n - n - 1. So:

T(n, 0) = 2^n * O(1) + O(2^n - n - 1) + ...

Lastly, the h terms. This is a bit tricky to figure out. The first expansion will just result in a zero term. The next expansion results in one 1 and one 0 term. Then one 2, two 1, one 0. The number is always equal to the binomial coefficient: Sum { e from 0 to n-1 } (Sum { i from 0 to e } ( i * Binom(e, i) )) = 1 - (n-1)*2^n. So finally:

T(n, 0) = 2^n * O(1) + O(2^n - n - 1) + O(1 - (n-1)*2^n)
        = O(n*2^n)

The same complexity applies to the space requirements if all created sets are kept.

Nico Schertler
  • 32,049
  • 4
  • 39
  • 70
  • After the first expansion, shouldn't there be 2 n-1? Say I have subset [1,2,3]. I first take out the 1 so I am left with [2,3]. Then I create a copy of [2,3]. So there be 2 copies of n-1? – committedandroider Mar 29 '15 at 19:45
  • The input is commonly not counted as space requirement. So in the first iteration, only the copy is counted (`s-1`). – Nico Schertler Mar 29 '15 at 19:51
  • How did you come up with this summation formula? Sum {i from 1 to n} (2^(i-1)*(n-i)) = 2^n - n - 1. I was able to come up with this expression 2^(k-1)*(n-k) that can predict how many of n-k there will be after k expansions which is what you got as well. How did you generate that expression? – committedandroider Mar 29 '15 at 21:25
  • Mathematica solved the summation for me ;) There is probably also an analytic way to solve this. – Nico Schertler Mar 30 '15 at 06:14