Skip to main content

Generating Subsets and Permutations

1. Understanding Subset Generation Technique

Approach Description Time Complexity Space Complexity
Recursive Backtracking Build subsets by including/excluding each element recursively. At each step, make two recursive calls: one including the current element and one excluding it. O(2^n) O(n)
Bit Manipulation Use bitmasks to represent subsets. For a set of size n, iterate from 0 to 2^n - 1. Each bit represents whether an element is included. O(n * 2^n) O(1) (excluding output space)
Iterative Start with an empty subset. For each element, generate new subsets by appending the element to all existing subsets. O(2^n) O(2^n)

Example: Recursive Backtracking Implementation

function generateSubsets(nums) {
  const result = [];
  
  function backtrack(start, currentSubset) {
    result.push([...currentSubset]);
    
    for (let i = start; i < nums.length; i++) {
      currentSubset.push(nums[i]);
      backtrack(i + 1, currentSubset);
      currentSubset.pop();
    }
  }
  
  backtrack(0, []);
  return result;
}

// Example usage:
const nums = [1, 2, 3];
console.log(generateSubsets(nums));
// Output: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]

Example: Bit Manipulation Implementation

function generateSubsets(nums) {
  const result = [];
  const n = nums.length;
  
  for (let i = 0; i < (1 << n); i++) {
    const subset = [];
    for (let j = 0; j < n; j++) {
      if (i & (1 << j)) {
        subset.push(nums[j]);
      }
    }
    result.push(subset);
  }
  
  return result;
}

// Example usage:
const nums = [1, 2, 3];
console.log(generateSubsets(nums));
// Output: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
Note: The recursive approach is intuitive and commonly used in interviews. Bit manipulation is efficient for generating all subsets but can be less readable.

2. Generating All Subsets

Method Description Time Complexity Space Complexity
Recursive Recursively build subsets by including or excluding each element O(2^n) O(2^n)
Iterative Start with empty set, for each element add it to all existing subsets O(2^n) O(2^n)
Bit Manipulation Use bitmasks to represent subset inclusion (0 = exclude, 1 = include) O(n * 2^n) O(2^n)

Example: Recursive Approach

function generateSubsets(nums) {
  const result = [];

  function backtrack(start, currentSubset) {
    result.push([...currentSubset]);

    for (let i = start; i < nums.length; i++) {
      currentSubset.push(nums[i]);
      backtrack(i + 1, currentSubset);
      currentSubset.pop();
    }
  }

  backtrack(0, []);
  return result;
}

// Example usage:
const nums = [1, 2, 3];
console.log(generateSubsets(nums));
// Output: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]

Example: Iterative Approach

function generateSubsets(nums) {
  let result = [[]];

  for (const num of nums) {
    const newSubsets = [];
    for (const subset of result) {
      newSubsets.push([...subset, num]);
    }
    result = [...result, ...newSubsets];
  }

  return result;
}

// Example usage:
const nums = [1, 2, 3];
console.log(generateSubsets(nums));
// Output: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]

Example: Bit Manipulation Approach

function generateSubsets(nums) {
  const result = [];
  const n = nums.length;

  for (let i = 0; i < (1 << n); i++) {
    const subset = [];
    for (let j = 0; j < n; j++) {
      if (i & (1 << j)) {
        subset.push(nums[j]);
      }
    }
    result.push(subset);
  }

  return result;
}

// Example usage:
const nums = [1, 2, 3];
console.log(generateSubsets(nums));
// Output: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]

3. Generating Subsets with Duplicates

Method Description Time Complexity Space Complexity
Backtracking with Sorting Sort the input array first to group duplicates together, then use backtracking with a condition to skip duplicates. O(2^n) O(n)
Iterative with Set Build subsets iteratively, using a set to store unique subsets and avoid duplicates. O(2^n) O(2^n)

Example: Backtracking with Sorting

function subsetsWithDup(nums) {
    nums.sort((a, b) => a - b);
    const result = [];
    
    function backtrack(start, path) {
        result.push([...path]);
        
        for (let i = start; i < nums.length; i++) {
            if (i > start && nums[i] === nums[i - 1]) continue;
            path.push(nums[i]);
            backtrack(i + 1, path);
            path.pop();
        }
    }
    
    backtrack(0, []);
    return result;
}

// Example usage:
const input = [1, 2, 2];
console.log(subsetsWithDup(input));
// Output: [[], [1], [1, 2], [1, 2, 2], [2], [2, 2]]

Example: Iterative with Set

function subsetsWithDup(nums) {
    const result = [[]];
    const seen = new Set();
    
    for (const num of nums) {
        const size = result.length;
        for (let i = 0; i < size; i++) {
            const subset = [...result[i], num];
            const key = subset.join(',');
            
            if (!seen.has(key)) {
                seen.add(key);
                result.push(subset);
            }
        }
    }
    
    return result;
}

// Example usage:
const input = [1, 2, 2];
console.log(subsetsWithDup(input));
// Output: [[], [1], [2], [1, 2], [2, 2], [1, 2, 2]]
Note: When generating subsets with duplicates, the key is to avoid generating duplicate subsets. Sorting helps group duplicates together so they can be skipped during backtracking.

4. Generating All Permutations

Method Description Time Complexity Space Complexity
Backtracking Recursive approach that builds permutations by choosing one element at a time and backtracking when all elements are used. O(n! * n) O(n)
Iterative Uses a queue or stack to generate permutations iteratively by inserting elements into all possible positions of existing permutations. O(n! * n) O(n! * n)
Heap's Algorithm Efficient algorithm that generates all permutations by swapping elements in a specific order. O(n!) O(1) (excluding recursion stack)

Example: Backtracking Implementation

function permute(nums) {
    const result = [];
    
    function backtrack(current, remaining) {
        if (remaining.length === 0) {
            result.push([...current]);
            return;
        }
        
        for (let i = 0; i < remaining.length; i++) {
            const num = remaining[i];
            current.push(num);
            backtrack(current, remaining.filter((_, j) => j !== i));
            current.pop();
        }
    }
    
    backtrack([], nums);
    return result;
}

// Example usage:
const input = [1, 2, 3];
console.log(permute(input));
// Output: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

Example: Heap's Algorithm Implementation

function heapsAlgorithm(arr) {
    const result = [];
    
    function generate(k, currentArr) {
        if (k === 1) {
            result.push([...currentArr]);
            return;
        }
        
        generate(k - 1, currentArr);
        
        for (let i = 0; i < k - 1; i++) {
            if (k % 2 === 0) {
                [currentArr[i], currentArr[k - 1]] = [currentArr[k - 1], currentArr[i]];
            } else {
                [currentArr[0], currentArr[k - 1]] = [currentArr[k - 1], currentArr[0]];
            }
            generate(k - 1, currentArr);
        }
    }
    
    generate(arr.length, arr);
    return result;
}

// Example usage:
const input = [1, 2, 3];
console.log(heapsAlgorithm(input));
// Output: [[1,2,3], [2,1,3], [3,1,2], [1,3,2], [2,3,1], [3,2,1]]
Note: Heap's algorithm is more efficient for generating permutations as it avoids creating new arrays at each step, making it optimal in terms of space complexity.

5. Generating Permutations with Duplicates

Approach Description Time Complexity Space Complexity
Backtracking with Frequency Map Use a frequency map to track duplicate elements and avoid generating identical permutations. O(n! * n) O(n)
Sorting + Visited Array Sort the array and use a visited array to skip duplicates during backtracking. O(n! * n) O(n)

Example: Backtracking with Frequency Map

function permuteUnique(nums) {
    const result = [];
    const freq = {};
    for (let num of nums) {
        freq[num] = (freq[num] || 0) + 1;
    }

    function backtrack(current) {
        if (current.length === nums.length) {
            result.push([...current]);
            return;
        }

        for (let num in freq) {
            if (freq[num] > 0) {
                current.push(num);
                freq[num]--;
                backtrack(current);
                current.pop();
                freq[num]++;
            }
        }
    }

    backtrack([]);
    return result;
}

// Example usage:
const nums = [1, 1, 2];
console.log(permuteUnique(nums));
// Output: [[1,1,2], [1,2,1], [2,1,1]]

Example: Sorting + Visited Array

function permuteUnique(nums) {
    const result = [];
    nums.sort(); // Sort to handle duplicates
    const visited = new Array(nums.length).fill(false);

    function backtrack(current) {
        if (current.length === nums.length) {
            result.push([...current]);
            return;
        }

        for (let i = 0; i < nums.length; i++) {
            if (visited[i]) continue;
            // Skip duplicates
            if (i > 0 && nums[i] === nums[i - 1] && !visited[i - 1]) continue;

            visited[i] = true;
            current.push(nums[i]);
            backtrack(current);
            current.pop();
            visited[i] = false;
        }
    }

    backtrack([]);
    return result;
}

// Example usage:
const nums = [1, 1, 2];
console.log(permuteUnique(nums));
// Output: [[1,1,2], [1,2,1], [2,1,1]]

6. Generating Combinations

Method Description Time Complexity Space Complexity
Recursive Backtracking Build combinations by including/excluding each element recursively O(2^n) O(n)
Iterative with Bit Manipulation Use bitmasks to represent subset inclusion O(2^n) O(1)
Built-in Libraries (e.g., Python itertools) Use optimized library functions to generate combinations O(C(n,k)) O(1)

Example: Recursive Backtracking

function generateCombinations(nums, k) {
  const result = [];

  function backtrack(start, current) {
    if (current.length === k) {
      result.push([...current]);
      return;
    }

    for (let i = start; i < nums.length; i++) {
      current.push(nums[i]);
      backtrack(i + 1, current);
      current.pop();
    }
  }

  backtrack(0, []);
  return result;
}

// Example usage:
const nums = [1, 2, 3];
const combinations = generateCombinations(nums, 2);
console.log(combinations); // [[1,2], [1,3], [2,3]]

Example: Iterative with Bitmask

function generateCombinations(nums, k) {
  const result = [];
  const n = nums.length;

  for (let mask = 0; mask < (1 << n); mask++) {
    const combination = [];
    let count = 0;

    for (let i = 0; i < n; i++) {
      if ((mask & (1 << i)) !== 0) {
        combination.push(nums[i]);
        count++;
      }
    }

    if (count === k) {
      result.push(combination);
    }
  }

  return result;
}

// Example usage:
const nums = [1, 2, 3];
const combinations = generateCombinations(nums, 2);
console.log(combinations); // [[1,2], [1,3], [2,3]]
Note: The recursive approach is more intuitive and easier to understand, while the bitmask approach can be faster in some cases due to reduced function call overhead.

7. Generating Letter Case Permutations

Property Description Example
Input String containing letters and digits "a1b2"
Output All possible case permutations of letters in the string ["a1b2", "A1b2", "a1B2", "A1B2"]

Example: Generate letter case permutations

function letterCasePermutation(s) {
  const result = [];
  
  function backtrack(index, current) {
    if (index === s.length) {
      result.push(current);
      return;
    }
    
    const char = s[index];
    
    if (/\d/.test(char)) {
      backtrack(index + 1, current + char);
    } else {
      backtrack(index + 1, current + char.toLowerCase());
      backtrack(index + 1, current + char.toUpperCase());
    }
  }
  
  backtrack(0, "");
  return result;
}

console.log(letterCasePermutation("a1b2"));
// Output: ["a1b2", "A1b2", "a1B2", "A1B2"]
Note: For each letter in the string, we generate two branches: one with lowercase and one with uppercase. Digits are added as-is.

8. Generating Balanced Parentheses

Approach Description Time Complexity Space Complexity
Backtracking Recursively build valid parentheses combinations by ensuring at no point do closing brackets exceed opening brackets. O(4^n / n^(1/2)) O(4^n / n^(1/2))
Dynamic Programming Build valid combinations using previously computed results for smaller numbers of pairs. O(4^n / n^(1/2)) O(4^n / n^(1/2))
Iterative Use a stack or queue to generate valid parentheses combinations iteratively. O(4^n / n^(1/2)) O(4^n / n^(1/2))

Example: Backtracking Implementation

function generateParentheses(n) {
  const result = [];
  
  function backtrack(current, open, close) {
    if (current.length === 2 * n) {
      result.push(current);
      return;
    }
    
    if (open < n) {
      backtrack(current + '(', open + 1, close);
    }
    
    if (close < open) {
      backtrack(current + ')', open, close + 1);
    }
  }
  
  backtrack('', 0, 0);
  return result;
}

// Example usage:
console.log(generateParentheses(3)); 
// Output: ['((()))', '(()())', '(())()', '()(())', '()()()']
Note: The number of valid combinations for n pairs is the nth Catalan number.

9. Generating Unique BSTs

Generating unique Binary Search Trees (BSTs) involves creating all structurally distinct BSTs that can be formed using a given set of values. This is a classic backtracking and dynamic programming problem.

Approach Description Time Complexity Space Complexity
Recursive with Memoization For each root value, recursively generate left and right subtrees from remaining values. O(4^n / n^(3/2)) O(4^n / n^(3/2))
Catalan Number Based Use the nth Catalan number to compute the count and generate trees using a recursive approach. O(4^n / n^(3/2)) O(4^n / n^(3/2))

Example: Generate All Unique BSTs for Values 1 to 3

class TreeNode {
  constructor(val, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

function generateTrees(n) {
  if (n === 0) return [];
  return buildTrees(1, n);
}

function buildTrees(start, end) {
  const trees = [];
  if (start > end) {
    trees.push(null);
    return trees;
  }

  for (let i = start; i <= end; i++) {
    const leftTrees = buildTrees(start, i - 1);
    const rightTrees = buildTrees(i + 1, end);

    for (const left of leftTrees) {
      for (const right of rightTrees) {
        const root = new TreeNode(i);
        root.left = left;
        root.right = right;
        trees.push(root);
      }
    }
  }

  return trees;
}

// Example usage:
const trees = generateTrees(3);
console.log(trees.length); // Output: 5
Note: The number of unique BSTs for n nodes is the nth Catalan number, which can be computed as (2n)! / ((n+1)! * n!).

10. Generating Palindrome Partitions

Method Description Time Complexity Space Complexity
Backtracking Recursively build partitions by checking if each substring is a palindrome. O(2^n) O(n)
Dynamic Programming Precompute palindromic substrings using DP to optimize partition checks. O(n^2) O(n^2)

Example: Backtracking Approach

function partition(s) {
    const result = [];
    backtrack(s, 0, [], result);
    return result;
}

function backtrack(s, start, current, result) {
    if (start === s.length) {
        result.push([...current]);
        return;
    }

    for (let i = start; i < s.length; i++) {
        const substr = s.substring(start, i + 1);
        if (isPalindrome(substr)) {
            current.push(substr);
            backtrack(s, i + 1, current, result);
            current.pop();
        }
    }
}

function isPalindrome(str) {
    let left = 0, right = str.length - 1;
    while (left < right) {
        if (str[left++] !== str[right--]) return false;
    }
    return true;
}

// Example usage:
console.log(partition("aab")); 
// Output: [["a","a","b"], ["aa","b"]]

Example: Dynamic Programming Approach

function partitionWithDP(s) {
    const n = s.length;
    const isPalindrome = Array(n).fill().map(() => Array(n).fill(false));
    
    // Precompute palindromes
    for (let i = 0; i < n; i++) {
        isPalindrome[i][i] = true;
    }

    for (let len = 2; len <= n; len++) {
        for (let i = 0; i <= n - len; i++) {
            const j = i + len - 1;
            if (s[i] === s[j]) {
                if (len === 2 || isPalindrome[i + 1][j - 1]) {
                    isPalindrome[i][j] = true;
                }
            }
        }
    }

    const result = [];
    dfs(s, 0, [], isPalindrome, result);
    return result;
}

function dfs(s, start, current, isPalindrome, result) {
    if (start === s.length) {
        result.push([...current]);
        return;
    }

    for (let i = start; i < s.length; i++) {
        if (isPalindrome[start][i]) {
            current.push(s.substring(start, i + 1));
            dfs(s, i + 1, current, isPalindrome, result);
            current.pop();
        }
    }
}

// Example usage:
console.log(partitionWithDP("aab")); 
// Output: [["a","a","b"], ["aa","b"]]
Note: The backtracking approach explores all possible partitions, while the DP version optimizes palindrome checks using precomputed data.

11. Generating Word Abbreviations

Property Description Example
abbreviationLength Maximum length of the abbreviation 3
includeNumbers Whether to include numbers in abbreviations false

Example: Generate Abbreviations for a Word

function generateAbbreviations(word, maxLength = 3) {
  const abbreviations = [];
  for (let i = 1; i <= Math.min(maxLength, word.length); i++) {
    abbreviations.push(word.substring(0, i));
  }
  return abbreviations;
}

const word = "algorithm";
console.log(generateAbbreviations(word)); // ["a", "al", "alg"]
Note: Abbreviations are commonly used in technical documentation to simplify long terms.

12. Generating N-Queens Solutions

Property Description Notes
n Size of the chessboard (n x n) Must be a positive integer
solutionCount Number of valid N-Queens solutions Calculated using backtracking algorithm

Example: N-Queens Solution for n=4

function solveNQueens(n) {
  const board = Array(n).fill().map(() => Array(n).fill(0));
  const solutions = [];

  function isSafe(row, col) {
    for (let i = 0; i < row; i++) {
      if (board[i][col] === 1) return false;
      if (board[i][col - (row - i)] === 1) return false;
      if (board[i][col + (row - i)] === 1) return false;
    }
    return true;
  }

  function backtrack(row) {
    if (row === n) {
      solutions.push(board.map(row => [...row]));
      return;
    }

    for (let col = 0; col < n; col++) {
      if (isSafe(row, col)) {
        board[row][col] = 1;
        backtrack(row + 1);
        board[row][col] = 0;
      }
    }
  }

  backtrack(0);
  return solutions;
}

const solutions = solveNQueens(4);
console.log(solutions.length); // Output: 2
Note: The N-Queens problem is a classic backtracking algorithm example. For n=4, there are exactly 2 distinct solutions.

Was this article helpful?