The book is written by Gayle Laakmann, an engineer at Google with a passion to train and mentor other people to become better computer scientists. He has plenty of experience interviewing at Google and other top companies, and in this book he shares his advice for most of the aspects of those sorts of interviews.
Major part of the book consists of interview questions, categorized by topic area, and the possible solutions. Recursion only comes in Chapter 8, but that's where I jumped first nonetheless, as this is my training focus right now.
Coin Change
The first problem I went on to solve involved calculating the number of ways N cents can be represented using quarters, dimes, nickels and pennies. This is my code:
      public static void getChange(int n, int[] coins, int sum){  
           if(sum == n)  
                System.out.println(coins[0] + "q," + coins[1] + "d," +   
                          coins[2] + "n," + coins[3] + "p");  
           else{  
                if(sum + 25 <= n){  
                     coins[0]++;  
                     getChange(n, coins, sum + 25);  
                     coins[0]--;  
                }  
                if(sum + 10 <= n){  
                     coins[1]++;  
                     getChange(n, coins, sum + 10);  
                     coins[1]--;  
                }  
                if(sum + 5 <= n){  
                     coins[2]++;  
                     getChange(n, coins, sum + 5);  
                     coins[2]--;  
                }  
                if(sum + 1 <= n){  
                     coins[3]++;  
                     getChange(n, coins, sum + 1);  
                     coins[3]--;  
                }  
           }  
      }  
Evaluation
One way I could improve the design of this code is by using some OO principles and wrap the Coins structure as an object and have the function return a list of such objects. I avoided that here for simplicity, as this is just proof of recursion concept.
There is a problem with this code in that it will return duplicate combinations of coins. The reason is because there is no differentiation between two pennies, two dimes, etc., so we can reach the same combination by taking different decisions at each recursion depth. For example, to represent 6 we can either select 6 pennies by going depth first on pennies branches, or we can select a nickel and a penny; however, selecting a penny and then a nickel will also be a possibility, which is impossible distinguish until we reach both of those leafs and notice the duplicate.
After reading the provided solution, I realized that I misread the question - it is asking for the number of ways, not the enumeration of all possibilities. The solution in the book also employs bottom-up recursion, as opposed to my top-down method.
It took me a while to understand the solution presented in the book. As we go deeper into the recursion tree, we solve the subproblem of making change for a smaller amount. So, when making change for 25, we look for ways to make change using 0 quarters, 0 dimes, 0 nickels first (i.e. using pennies). Then, we use 1 nickel, 2 nickels, etc., until we hit the sum limit. The base case of recursion is when we hit the lowest denominator - pennies, as we can always make up for remaining amount using pennies.
Balanced Parenthesis
The next problem I went on to solve was enumerating combinations of n-pairs of parenthesis. This one was fairly simple, and the my code closely matched the provided solution in the end:
      public List<String> generateParenthesis(int n){  
           List<String> list = new LinkedList<String>();  
           _generateParenthesis(0, 0, n, new String(), list);  
           return list;  
      }  
        
      private void _generateParenthesis(int open, int closed, int n, String s,   
                List<String> list){  
           if(closed == n)  
                list.add(s);  
           else{  
                if(open < n)  
                     _generateParenthesis(open + 1, closed, n, s + "(", list);  
                if(open > closed)  
                     _generateParenthesis(open, closed + 1, n, s + ")", list);  
           }  
      }  
The only thing I noticed is that the book is missing one possibility for its provided example of 3 pairs: "(()())". The solution in the book also just prints out the solution at the base case, while I like to return the solution list as an object and use an "entry" method to recursion for a more user-friendly code design.
String Permutations
Finally, I worked on the interesting problem of string permutations. The problem definition is simple: given a string, output all possible permutations of its characters. I approached the solution this way:
      private static List<String> permuteString(String s){  
           List<String> list = new LinkedList<String>();  
           _permuteString(s, 0, list, new char[s.length()], new boolean[s.length()]);  
           return list;  
      }  
        
      private static void _permuteString(String s, int i, List<String> list, char[] p,   
                boolean[] used){  
           if(i == s.length()){  
                list.add(String.valueOf(p));  
           }  
           else{  
                for(int j = 0; j < s.length(); j++){  
                     if(used[j])  
                          continue;  
                     else{  
                          p[i] = s.charAt(j);  
                          used[j] = true;  
                          _permuteString(s, i+1, list, p, used);  
                          used[j] = false;  
                     }  
                }  
           }  
      }  
      public static List<String> permuteString(String s){  
           if(s.length() == 0){  
                return new LinkedList<String>(Arrays.asList(""));  
           }  
           else{  
                char first = s.charAt(0);  
                List<String> permutations = new LinkedList<String>();  
                for(String p : permuteString(s.substring(1)))  
                     combine(p, first, permutations);  
                return permutations;  
           }  
      }  
        
      private static void combine(String s, char c, List<String> combinations){  
           if(s.length() == 0)  
                combinations.add(String.valueOf(c));  
           else{  
                combinations.add(s + c);  
                combinations.add(c + s);  
                for(int i = 1; i < s.length(); i++)  
                     combinations.add(s.substring(0, i) + c + s.substring(i, s.length()));  
           }  
      }  
This may seem like an intuitive and clever way to do it, but the top-down approach works better in this case. This is because even though the overall running time is bounded by O(n!), the combination part of top-down approach takes O(1) time, while doing the combination in bottom-up approach takes O(n).
I've ran the method for up to 10 characters, giving the following performance graph:
Note that the y-axis is logarithmic, and for the smaller input size, the top-down method is over an order of magnitude faster than the bottom-up approach. As we approach larger n, the asymptotic n! takes over, and the relative difference between the two method becomes marginal.
 
No comments:
Post a Comment