10 Subsets
10 Subsets
Subsets (easy)
https://leetcode.com/problems/subsets/
     Given a set with distinct elements, find all of its distinct subsets.
To generate all subsets of the given set, we can use the Breadth First Search
(BFS) approach. We can start with an empty set, iterate through all numbers
one-by-one, and add them to existing sets to create new subsets.
Let’s take the example-2 mentioned above to go through each step of our algo-
rithm:
Given set: [1, 5, 3]
  1. Start with an empty set: [[]]
  2. Add the first number 1 to all the existing subsets to create new subsets:
     [[],[1]];
  3. Add the second number 5 to all the existing subsets: [[], [1], [5],
     [1,5]];
  4. Add the third number 3 to all the existing subsets: [[], [1], [5],
     [1,5], [3], [1,3], [5,3], [1,5,3]].
Since the input set has distinct elements, the above steps will ensure that we
will not have any duplicate subsets.
function findSubsets(nums) {
  const subsets = [];
    //we will take all existing subsets and insert the current
    //number in them to create new subsets
    const n = subsets.length
                                        1
          //clone the permutation
          // const set1 = subsets[j].slice(0)
          // set1.push(currentNumber)
          subsets.push([...subsets[j], nums[i]])
      }
  }
  return subsets;
};
findSubsets([1, 3])
findSubsets([1, 5, 3])
  • Since, in each step, the number of subsets doubles as we add each element
    to all the existing subsets, therefore, we will have a total of O(2�) subsets,
    where N is the total number of elements in the input set. And since we
    construct a new subset from an existing set, therefore, the time complexity
    of the above algorithm will be O(N*2�).
  • All the additional space used by our algorithm is for the output list. Since
    we will have a total of O(2�) subsets, and each subset can take up to O(N)
    space, therefore, the space complexity of our algorithm will be O(N*2�).
                                       2
Sorted set: [1, 3, 3, 5]
  1. Start with an empty set: [[]]
  2. Add the first number 1 to all the existing subsets to create new subsets:
     [[], [1]];
  3. Add the second number 3 to all the existing subsets: [[], [1], [3],
     [1,3]].
  4. The next number 3 is a duplicate. If we add it to all existing subsets we
     will get:
     [[], [1], [3], [1,3], [3], [1,3], [3,3], [1,3,3]]
We got two duplicate subsets: [3], [1,3]
Whereas we only needed the new subsets: [3,3], [1,3,3]
To handle this instead of adding 3 to all the existing subsets, we only add it to
the new subsets which were created in the previous 3rd step:
     [[], [1], [3], [1,3], [3,3], [1,3,3]]
  5. Finally, add the forth number 5 to all the existing subsets: [[],
     [1], [3], [1,3], [3,3], [1,3,3], [5], [1,5], [3,5], [1,3,5],
     [3,3,5], [1,3,3,5]]
function subsetsWithDupe(nums) {
  //sort the numbers to handle duplicates
  nums.sort((a,b) => a-b)
subsets.push([])
  let start = 0
  let end = 0
end = subsets.length - 1
                                       3
           subsets.push([...subsets[j], nums[i]])
       }
  }
  return subsets;
};
subsetsWithDupe([1, 3, 3])
subsetsWithDupe([1, 5, 3, 3])
  • Since, in each step, the number of subsets doubles (if not duplicate) as we
    add each element to all the existing subsets, therefore, we will have a total
    of O(2�) subsets, where N is the total number of elements in the input set.
    And since we construct a new subset from an existing set, therefore, the
    time complexity of the above algorithm will be O(N*2�).
  • All the additional space used by our algorithm is for the output list. Since,
    at most, we will have a total of O(2�) subsets, and each subset can take
    up to O(N) space, therefore, the space complexity of our algorithm will be
    O(N*2�).
Permutations (medium)
https://leetcode.com/problems/permutations/
       Given a set of distinct numbers, find all of its permutations.
Permutation is defined as the re-arranging of the elements of the set. For exam-
ple, {1, 2, 3} has the following six permutations:
  1.   {1,   2,   3}
  2.   {1,   3,   2}
  3.   {2,   1,   3}
  4.   {2,   3,   1}
  5.   {3,   1,   2}
  6.   {3,   2,   1}
If a set has n distinct elements it will have n! permutations.
This problem follows the Subsets pattern and we can follow a similar Breadth
First Search (BFS) approach. However, unlike Subsets, every permutation must
contain all the numbers.
Let’s take the example mentioned below to generate all the permutations. Fol-
lowing a BFS approach, we will consider one number at a time:
  1. If the given set is empty then we have only an empty permutation set: []
  2. Let’s add the first element 1, the permutations will be: [1]
  3. Let’s add the second element 3, the permutations will be: [3,1], [1,3]
                                         4
  4. Let’s add the third element 5, the permutations will be: [5,3,1],
     [3,5,1], [3,1,5], [5,1,3], [1,5,3], [1,3,5]
Let’s analyze the permutations in the 3rd and 4th step. How can we generate
permutations in the 4th step from the permutations of the 3rd step?
If we look closely, we will realize that when we add a new number 5, we take each
permutation of the previous step and insert the new number in every position to
generate the new permutations. For example, inserting 5 in different positions
of [3,1] will give us the following permutations:
  1. Inserting 5 before 3: [5,3,1]
  2. Inserting 5 between 3 and 1: [3,5,1]
  3. Inserting 5 after 1: [3,1,5]
function findPermutations(nums) {
  const result = [];
  let permutations = [[]]
  let numsLength = nums.length
                                       5
  return result;
};
findPermutations([1, 3, 5])
  • We know that there are a total of N! permutations of a set with N numbers.
    In the algorithm above, we are iterating through all of these permutations
    with the help of the two ‘for’ loops. In each iteration, we go through all the
    current permutations to insert a new number in them. To insert a number
    into a permutation of size ‘N will take O(N), which makes the overall time
    complexity of our algorithm O(N*N!).
  • All the additional space used by our algorithm is for the result list and
    the queue to store the intermediate permutations. If you see closely, at
    any time, we don’t have more than N! permutations between the result
    list and the queue. Therefore the overall space complexity to store N!
    permutations each containing N elements will be O(N*N!).
Recursive Solution
function permute(nums) {
  //recursion
  let subsets = []
  return subsets
};
                                       6
     Given a string, find all of its permutations preserving the character
     sequence but changing case.
This problem follows the Subsets pattern and can be mapped to Permutations.
Let’s take Example-2 mentioned above to generate all the permutations. Follow-
ing a BFS approach, we will consider one character at a time. Since we need to
preserve the character sequence, we can start with the actual string and process
each character (i.e., make it upper-case or lower-case) one by one:
  1. Starting with the actual string: "ab7c"
  2. Processing the first character a, we will get two permutations: "ab7c",
     "Ab7c"
  3. Processing the second character b, we will get four permutations: "ab7c",
     "Ab7c", "aB7c", "AB7c"
  4. Since the third character is a digit, we can skip it.
  5. Processing the fourth character c, we will get a total of eight permuta-
     tions: "ab7c", "Ab7c", "aB7c", "AB7c", "ab7C", "Ab7C", "aB7C",
     "AB7C"
Let’s analyze the permutations in the 3rd and the 5th step. How can we generate
the permutations in the 5th step from the permutations in the 3rd step?
If we look closely, we will realize that in the 5th step, when we processed the
new character c, we took all the permutations of the previous step (3rd) and
changed the case of the letter c in them to create four new permutations.
function findLetterCaseStringPermutations(str) {
  const permutations = [];
  permutations.push(str)
                                       7
           permutations.push(chs.join(''))
       }
    }
  }
  return permutations;
};
findLetterCaseStringPermutations("ad52")
findLetterCaseStringPermutations("ab7c")
  • Since we can have2� permutations at the most and while processing each
    permutation we convert it into a character array, the overall time com-
    plexity of the algorithm will be O(N*2�).
  • All the additional space used by our algorithm is for the output list. Since
    we can have a total of O(2�) permutations, the space complexity of our
    algorithm is O(N*2�).
                                        8
       than N. We can also add ) as we do have an equivalent open parenthesis,
       so our list of combinations will be: ((, ()
  5.   In the next iteration, for the first combination ((, we can add another (
       to it as the count of open parenthesis will be less than N, we can also add
       ) as we do have an equivalent open parenthesis. This gives us two new
       combinations: ((( and ((). For the second combination (), we can add
       another ( to it since the count of open parenthesis will be less than N. We
       can’t add ) as we don’t have an equivalent open parenthesis, so our list of
       combinations will be: (((, ((), ()(
  6.   Following the same approach, next we will get the following list of combi-
       nations: “((()”, “(()(”, “(())”, “()((”, “()()”
  7.   Next we will get: “((())”, “(()()”, “(())(”, “()(()”, “()()(”
  8.   Finally, we will have the following combinations of balanced parentheses:
       “((()))”, “(()())”, “(())()”, “()(())”, “()()()”
  9.   We can’t add more parentheses to any of the combinations, so we stop
       here.
class ParenthesesString {
  constructor(str, openCount, closeCount) {
    this.str = str;
    this.openCount = openCount;
    this.closeCount = closeCount;
  }
}
function generateValidParentheses(num) {
  let result = [];
  let queue = []
  while(queue.length > 0) {
    const ps = queue.shift()
                                        9
          queue.push(new ParenthesesString(`${ps.str})`, ps.openCount, ps.closeCount + 1))
      }
    }
  }
  return result;
};
generateValidParentheses(2)
generateValidParentheses(3)
  • Let’s try to estimate how many combinations we can have for N pairs of
    balanced parentheses. If we don’t care for the ordering - that ) can only
    come after ( - then we have two options for every position, i.e., either
    put open parentheses or close parentheses. This means we can have a
    maximum of 2� combinations. Because of the ordering, the actual number
    will be less than 2�
  • If you see the visual representation of Example-2 closely you will realize
    that, in the worst case, it is equivalent to a binary tree, where each node
    will have two children. This means that we will have 2� leaf nodes and
    2�-1 intermediate nodes. So the total number of elements pushed to the
    queue will be 2�−1, which is asymptotically equivalent to O(2�). While
    processing each element, we do need to concatenate the current string
    with ( or ). This operation will take O(N), so the overall time complexity
    of our algorithm will be O(N*2�). This is not completely accurate but
    reasonable enough to be presented in the interview.
  • All the additional space used by our algorithm is for the output list. Since
    we can’t have more than O(2�) combinations, the space complexity of our
    algorithm is O(N*2�).
Recursive Solution
function generateValidParentheses(num) {
  const result = [];
  const parenthesesString = Array(2 * num);
  generateValidParenthesesRecursive(num, 0, 0, parenthesesString, 0, result);
  return result;
}
                                      10
        if (openCount < num) { // if we can add an open parentheses, add it
          parenthesesString[index] = '(';
         generateValidParenthesesRecursive(num, openCount + 1, closeCount, parenthesesString, in
        }
        if (openCount > closeCount) { // if we can add a close parentheses, add it
          parenthesesString[index] = ')';
          generateValidParenthesesRecursive(num, openCount, closeCount + 1, parenthesesString, i
        }
    }
}
generateValidParentheses(2)
generateValidParentheses(3)
                                         11
     combination B gives us B_ and BA.
  5. The next iteration will give us: _ _ _, 2T, 1A_, 1AT, B _ _, B1T,
     BA_, BAT
  6. The final iteration will give us:3, 2T, 1A1, 1AT, B2, B1T, BA1, BAT
class AbbreviatedWord {
  constructor(str, start, count) {
    this.str = str;
    this.start = start;
    this.count = count;
  }
}
function generateGeneralizedAbbreviation(word) {
  let wLength = word.length
  const result = [];
  const queue = [];
  queue.push(new AbbreviatedWord('', 0, 0))
  while(queue.length > 0) {
    const abWord = queue.shift()
    if(abWord.start === wLength){
      if(abWord.count !== 0) {
        abWord.str += abWord.count
      }
      result.push(abWord.str)
    } else {
      //continue abbreviating by incrementing the current abbreviation count
      queue.push(new AbbreviatedWord(abWord.str, abWord.start + 1, abWord.count + 1))
      //restart abbreviating, append the count and the current character to the string
      if(abWord.count !== 0){
        abWord.str += abWord.count
      }
      let newWord = abWord.str + word[abWord.start]
      queue.push(new AbbreviatedWord(newWord, abWord.start + 1, 0))
    }
  }
  return result;
};
                                     12
      you will realize that it is equivalent to a binary tree, where each node
      has two children. This means that we will have 2� leaf nodes and 2�-1
      intermediate nodes, so the total number of elements pushed to the queue
      will be 2� + 2�-1, which is asymptotically equivalent to O(2�). While
      processing each element, we do need to concatenate the current string with
      a character. This operation will take O(N), so the overall time complexity
      of our algorithm will be O(N*2�).
    • All the additional space used by our algorithm is for the output list. Since
      we can’t have more than O(2�) combinations, the space complexity of our
      algorithm is O(N*2�).
Recursive Solution
function generate_generalized_abbreviation(word) {
  const result = [];
  generate_abbreviation_recursive(word, '', 0, 0, result);
  return result;
}
        // restart abbreviating, append the count and the current character to the string
        if (count !== 0) {
          abWord += count;
        }
        const newWord = abWord + word[start];
        generate_abbreviation_recursive(word, newWord, start + 1, 0, result);
    }
}
                                        13
      Given an expression containing digits and operations (+, -, *),
      find all possible ways in which the expression can be evaluated by
      grouping the numbers and operators using parentheses.
This problem follows the Subsets pattern and can be mapped to Balanced Paren-
theses. We can follow a similar BFS approach.
Let’s take the first example to generate different ways to evaluate the expression.
  1. We can iterate through the expression character-by-character.
  2. we can break the expression into two halves whenever we get an operator
     (+, -, *).
  3. The two parts can be calculated by recursively calling the function.
  4. Once we have the evaluation results from the left and right halves, we can
     combine them to produce all results.
function diffWaysToEvaluateExpression(input) {
  const result = [];
  // base case: if the input string is a number, parse and add it to output.
  if(!(input.includes('+')) && !(input.includes('-')) && !(input.includes('*'))) {
     result.push(parseInt(input))
     } else {
       for(let i = 0; i < input.length; i++){
         const char = input[i];
         if(isNaN(parseInt(char, 10))){
        // if not a digit
        // break the equation here into two parts and make recursively calls
           const leftParts = diffWaysToEvaluateExpression(input.substring(0, i))
           const rightParts = diffWaysToEvaluateExpression(input.substring(i + 1))
                                        14
  return result;
};
Memoized Solution
The problem has overlapping subproblems, as our recursive calls can be evalu-
ating the same sub-expression multiple times. To resolve this, we can use mem-
oization and store the intermediate results in a HashMap. In each function call,
we can check our map to see if we have already evaluated this sub-expression
before
function diffWaysToEvaluateExpression(input) {
  diffWaysToEvaluateExpressionRecursive({}, input)
}
  if(input in map) {
    //issue with the hashmap here
    return map[input]
    // console.log(map[input])
  }
  const result = [];
                                      15
                     const rightParts = diffWaysToEvaluateExpression(input.substring(i + 1))
                  // console.log(leftParts.length)
                     for (let l = 0; l < leftParts.length; l++) {
                   for (let r = 0; r < rightParts.length; r++) {
                      let part1 = leftParts[l],
                        part2 = rightParts[r];
                      if (char === '+') {
                        result.push(part1 + part2);
                      } else if (char === '-') {
                        result.push(part1 - part2);
                      } else if (char === '*') {
                        result.push(part1 * part2);
                      }
                   }
              }
          }
      }
  }
map[input] = result
  return result;
};
                                         16
}
function findUniqueTrees(n) {
    if (n <= 0) {
     return [];
  }
  return findUniqueTreesRecursive(1, n);
}
findUniqueTrees(2)//List containing root nodes of all structurally unique BSTs, Here are the
findUniqueTrees(3)//List containing root nodes of all structurally unique BSTs, Here are the
    • The time complexity of this algorithm will be exponential and will be simi-
      lar to Balanced Parentheses. Estimated time complexity will be O(n*2^n)
      but the actual time complexity ( O(4^n/\sqrt{n}) is bounded by the
                                       17
    Catalan number and is beyond the scope of a coding interview.
  • The space complexity of this algorithm will be exponential too, estimated
    at O(2^n), but the actual will be ( O(4^n/\sqrt{n}).
function countTrees(n) {
if (n <= 1) {
    return 1;
  }
  let count = 0;
  for (let i = 1; i < n + 1; i++) {
    // making 'i' the root of the tree
    const countOfLeftSubtrees = countTrees(i - 1);
    const countOfRightSubtrees = countTrees(n - i);
    count += (countOfLeftSubtrees * countOfRightSubtrees);
  }
  return count;
};
countTrees(2)//2, As we saw in the previous problem, there are 2 unique BSTs storing numbers
countTrees(3)//5, There will be 5 unique BSTs that can store numbers from 1 to 3.
  • The time complexity of this algorithm will be exponential and will be simi-
    lar to Balanced Parentheses. Estimated time complexity will be O(n*2^n)
                                       18
    but the actual time complexity ( O(4^n/\sqrt{n}) is bounded by the
    Catalan number and is beyond the scope of a coding interview.
  • The space complexity of this algorithm will be exponential too, estimated
    O(2^n) but the actual will be ( O(4^n/\sqrt{n}).
Memoized version
Our algorithm has overlapping subproblems as our recursive call will be evalu-
ating the same sub-expression multiple times. To resolve this, we can use mem-
oization and store the intermediate results in a HashMap. In each function call,
we can check our map to see if we have already evaluated this sub-expression
before.
class TreeNode {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
};
function countTrees(n) {
  return countTreesRec({}, n);
}
function countTrees(map, n) {
if (n <= 1) {
    return 1;
  }
  let count = 0;
  for (let i = 1; i < n + 1; i++) {
    // making 'i' the root of the tree
    const countOfLeftSubtrees = countTrees(i - 1);
    const countOfRightSubtrees = countTrees(n - i);
    count += (countOfLeftSubtrees * countOfRightSubtrees);
  }
map[n] = count;
                                      19
  return count;
};
countTrees(2)//2, As we saw in the previous problem, there are 2 unique BSTs storing numbers
countTrees(3)//5, There will be 5 unique BSTs that can store numbers from 1 to 3.
  • The time complexity of the memoized algorithm will be O(n^2), since we
    are iterating from 1 to n and ensuring that each sub-problem is evaluated
    only once.
  • The space complexity will be O(n) for the memoization map.
20