3

This is problem 9.6 from Cracking the Coding Interview (5th edition)

Implement an algorithm to print all valid combinations of n-pairs of parenthesis
EXAMPLE
Input: 3
Output:"((())), (()()), (())(), ()(()), ()()()"

Here is the algorithm I implemented(in Java)

private static Set<String> getAllComb(int n) {
      Set<String> allPoss = new HashSet<String>();
      if(n>0) {
          if(n==1) {
              allPoss.add("()");
          } else {
              Set<String> before = getAllComb(n-1);
              for(String phrase: before) {
                  int length = phrase.length();
                  for(int start = length - 2; start>=0; start--) {
                      if(phrase.charAt(start) == '(') {
                          String phraseToConsider = phrase.substring(0, start+1) + "()" +
                               phrase.substring(start + 1);
                          if(!allPoss.contains(phraseToConsider)){
                              allPoss.add(phraseToConsider);
                          }
                      }
                  }
                  String phraseToConsider = "()" + phrase.substring(0);
                  if(!allPoss.contains(phraseToConsider)){
                      allPoss.add(phraseToConsider);
                  }
              }
          }
      }
      return allPoss;
}

This produces the correct output. I know that interviewers(at least at Amazon) love to ask you the time and space complexity of your solution. For time complexity, I was able to show that the algorithm runs in O(n) with a recurrence relation. I am having trouble with analyzing the space complexity. I this is a recursive solution so it should be at least O(n) But at each recursive call, I am also generating a set that is bounded by n. Would the total space be O(n) because of the n recursive calls or is it O(n2) because of the set size of bound n for each recursive call n?

Guy Coder
  • 24,501
  • 8
  • 71
  • 136
committedandroider
  • 8,711
  • 14
  • 71
  • 126
  • Some tips on the side that don't directly have to do with your problem: 1) You should be able to use N=0 as your base case; its common in recursive problems to not need to treat the N=1 specially 2) If you use a buffer of length 2n which the recursive calls "fill in" you should be able to bring the space complexity down to O(n). – hugomg Mar 22 '15 at 21:22
  • For n=4, there are 14 possible ways of writing properly matched parantheses. A buffer of length 2n (or kn for any k) is too small to hold the output in general. – chepner Mar 22 '15 at 21:25
  • how would you use a buffer of length 2n? You can't predefine the size of a set. – committedandroider Mar 22 '15 at 22:13
  • @hugomg I just to have another base case of N=1 because it's just one line of code of adding "()". You don't have to go through all the lines of code for iterating over the previous set, all the strings, etc. Is this really bad design though still? – committedandroider Mar 22 '15 at 22:21
  • Its not bad per se but extra lines of code means more places for bugs to hide in :) And usually treating N=1 with the same branch of coda than the rest shouldn't be a big performance hit. If it is then there is somethig fishy going on... – hugomg Mar 22 '15 at 23:08

2 Answers2

3

For time complexity, I was able to show that the algorithm runs in O(n) with a recurrence relation

This is wrong. The number of sequences of balanced parentheses is given by the Catalan numbers: there are exponentially many such sequences. Your algorithm cannot be linear if it is also correctly solving the problem, because just outputting an exponential number of solutions is itself taking exponential time.

As for the memory complexity, you seem to store all solutions for n - 1 at each step n of your recursion, so the memory complexity also looks exponential to me, plus the other strings you create and recursive calls you make at each step, which can only add to the complexity.

You can solve the problem without using exponential memory: think about how you can get rid of storing all previous sequences.

IVlad
  • 43,099
  • 13
  • 111
  • 179
  • Wouldn't you need to store all previous sequences to apply the algorithm? If you don't store them, there's no way to access all the strings and form all the combinations of parentheses. – committedandroider Mar 22 '15 at 22:11
  • @committedandroider - at each step, you have two possibilities: put an open bracket or a closed one. By doing it like this, you keep the memory usage to `O(n)`. This will generate unbalanced sequences as well, but if you figure out how to make it generate only balanced sequences, you're done. Hint: keep track of how many you've opened and how many you've closed. – IVlad Mar 22 '15 at 22:18
  • I don't get quite get what you mean. How I see this problem is finding the possibilities of putting "()" before and after a opening parenthesis. Say I have the first function call f(1) which would be (). In this case, what do you mean by at each step, put an open bracket or a closed bracket? Wouldn't you have to put both an open bracket and a closed bracket? – committedandroider Mar 22 '15 at 22:25
  • @committedandroider Think about how you can construct a solution by putting `(` or `)` from left to right: at each step, you must have more opens than closed. Then you can backtrack. Once you have `n` opens, the rest must be closed. Always put an open if you can. For example, you start with `((()))`. Then you backtrack to `(((`. You can make the last one a closed: `(()`. Then, the next can be an open: `(()(`. Now you can only have closed for the rest: `(()())` etc. – IVlad Mar 22 '15 at 22:51
  • With what you did, I see the process behind going from ((())) to (()()) but how would you go from ((())) to ()()()? What would you first backtrack it to? – committedandroider Mar 22 '15 at 23:07
  • @committedandroider You don't go from `((()))` to `()()()`. You go from `((()))` to `(()())`. Then from `(()())`, you backtrack to `(()(` and change it to `(())` (because this is the first change you can make). Then you complete it with `(())()`. From this, you get to `((` and change it to `()`, from which you get `()(())` etc. Each solution is used to generate the next one by backtracking on it. This only requires keeping at most a single solution in memory at any given time, not all of them. – IVlad Mar 22 '15 at 23:16
  • I still don't quite understand this. Lets say I am going from f(0) to f(1). I start off with "" and then with your algorithm, how would this work in terms of left or right to transform "" to "()"? – committedandroider Mar 23 '15 at 00:19
  • `f(0)` just adds a `(`. So does `f(1)`, so you have `((`. You have to figure out when you can add an open bracket and when you're forced to add a closed one. If you want, ask another question and I'll post more details and pseudocode. – IVlad Mar 23 '15 at 09:00
1

The number of ways to write n pairs of properly matched parentheses is the nth Catalan number, which actually grows exponentially, not quadratical. The space complexity of the output alone is O(2^n); see the wikipedia article for a quick overview of the Catalan numbers.

Notice that you aren't making a single recursive call at each depth, but potentially O(n) recursive calls.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • What do you mean by "notice that you aren't making a single recursive call at each depth"? If I make a call to this function with 4, then that will make a call to 3, which will make a call to 2, then so...... Isn't that a single recursive call at each depth? – committedandroider Mar 22 '15 at 22:15
  • For the call with *n*, you don't necessarily make just a single call with *n-1*, you may make several such calls. This will cause the amount of space used by the algorithm to increase much more quickly, even though there are never more than O(n) *active* calls in existence at one time. – chepner Mar 22 '15 at 23:51
  • Oh so at depth 4, you're actually making in total 4 recursive calls, f(3), f(2), f(1) and f(0)? – committedandroider Mar 23 '15 at 00:21