Permutations vs Combinations: Formula and Examples

You're building a feature flag system with 10 flags. How many possible configurations exist? That's 2^10 combinations. You're generating test cases and need to test all possible orderings of 5 API...

Permutations vs Combinations: Formula and Examples

Key Insights

  • Permutations count arrangements where order matters (P(n,r) = n!/(n-r)!), while combinations count selections where order doesn’t (C(n,r) = n!/(r!(n-r)!))
  • The difference between permutations and combinations is the division by r! — that factor removes duplicate orderings from the count
  • Most real-world software problems need combinations (feature flags, test subsets, team selections), but permutations appear in password generation, ranking systems, and sequence-dependent algorithms

Why This Matters for Developers

You’re building a feature flag system with 10 flags. How many possible configurations exist? That’s 2^10 combinations. You’re generating test cases and need to test all possible orderings of 5 API calls from a set of 8. That’s P(8,5) = 6,720 permutations.

Understanding permutations and combinations isn’t abstract math — it’s essential for algorithm design, complexity analysis, and solving real problems. The core distinction is simple: permutations care about order, combinations don’t. Choosing team members A, B, and C is the same combination whether you pick ABC, BAC, or CBA. But those are three different permutations if you’re assigning them to ranked positions.

This article gives you the formulas, the intuition, and production-ready code to handle both.

Permutations: When Order Matters

A permutation is an arrangement of items where the sequence matters. If you’re selecting 3 books from a shelf of 5 to arrange on your desk, the order you place them creates different permutations.

The formula is:

P(n, r) = n! / (n - r)!

Where n is the total number of items and r is how many you’re selecting.

Intuition: You have n choices for the first position, (n-1) for the second, (n-2) for the third, continuing for r positions. That’s n × (n-1) × (n-2) × … × (n-r+1). This equals n!/(n-r)! because you’re multiplying the first r terms of n!, which is the same as dividing n! by the remaining (n-r) terms.

Example: Arranging 3 books from 5.

  • First position: 5 choices
  • Second position: 4 choices
  • Third position: 3 choices
  • Total: 5 × 4 × 3 = 60 permutations

Here’s a clean implementation:

def factorial(n):
    """Calculate factorial iteratively to avoid recursion limits."""
    if n < 0:
        raise ValueError("Factorial undefined for negative numbers")
    if n == 0 or n == 1:
        return 1
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

def permutations(n, r):
    """Calculate P(n, r) = n! / (n - r)!"""
    if r > n:
        return 0
    if r < 0 or n < 0:
        raise ValueError("n and r must be non-negative")
    return factorial(n) // factorial(n - r)

# Example usage
print(permutations(5, 3))  # 60
print(permutations(8, 2))  # 56

Combinations: When Order Doesn’t Matter

A combination is a selection where order is irrelevant. Selecting 3 team members from 5 candidates — choosing Alice, Bob, and Carol is the same as choosing Carol, Alice, and Bob.

The formula is:

C(n, r) = n! / (r! × (n - r)!)

Intuition: Start with permutations P(n,r), which overcounts because it treats ABC, BAC, CAB, etc. as different. For any r items, there are r! ways to arrange them. Divide by r! to eliminate these duplicate orderings, giving you combinations.

Example: Selecting 3 team members from 5.

  • Permutations: 5 × 4 × 3 = 60
  • Each combination counted 3! = 6 times (ABC, ACB, BAC, BCA, CAB, CBA)
  • Combinations: 60 / 6 = 10
def combinations(n, r):
    """Calculate C(n, r) = n! / (r! × (n - r)!)"""
    if r > n:
        return 0
    if r < 0 or n < 0:
        raise ValueError("n and r must be non-negative")
    # Optimization: C(n, r) = C(n, n-r), use smaller value
    r = min(r, n - r)
    return factorial(n) // (factorial(r) * factorial(n - r))

# Example usage
print(combinations(5, 3))  # 10
print(combinations(8, 2))  # 28

Side-by-Side Comparison

Let’s solve the same problem both ways: you have 5 candidates and need to fill 3 positions.

Scenario 1 (Permutations): President, VP, Secretary — roles are distinct, order matters.

  • Result: P(5,3) = 60

Scenario 2 (Combinations): Three committee members — roles are identical, order doesn’t matter.

  • Result: C(5,3) = 10
Aspect Permutations Combinations
Formula n!/(n-r)! n!/(r!(n-r)!)
Order matters? Yes No
Relationship P(n,r) = C(n,r) × r! C(n,r) = P(n,r) / r!
Example (5,3) 60 10
def compare_perm_comb(n, r):
    """Show permutations vs combinations for the same inputs."""
    p = permutations(n, r)
    c = combinations(n, r)
    print(f"n={n}, r={r}")
    print(f"Permutations (order matters): {p}")
    print(f"Combinations (order doesn't matter): {c}")
    print(f"Ratio P/C: {p/c if c != 0 else 'undefined'} (should equal {factorial(r)}! = {factorial(r)})")
    return p, c

compare_perm_comb(5, 3)
# Output:
# n=5, r=3
# Permutations (order matters): 60
# Combinations (order doesn't matter): 10
# Ratio P/C: 6.0 (should equal 3! = 6)

Practical Applications in Software Engineering

Test Case Selection: You have 20 integration tests but can only run 5 in your CI pipeline. How many ways can you choose them? C(20,5) = 15,504 combinations.

Feature Flag Configurations: With 8 independent feature flags, you have C(8,k) ways to enable exactly k features. Total configurations: sum of C(8,k) for k=0 to 8 = 2^8 = 256.

Algorithm Complexity: Analyzing subset-based algorithms often requires calculating C(n,k) to determine time complexity.

Here’s a practical combination generator:

def generate_combinations(items, r):
    """Generate all combinations of r items from the input list."""
    n = len(items)
    if r > n or r < 0:
        return []
    
    result = []
    
    def backtrack(start, current):
        if len(current) == r:
            result.append(current[:])
            return
        
        for i in range(start, n):
            current.append(items[i])
            backtrack(i + 1, current)
            current.pop()
    
    backtrack(0, [])
    return result

# Example: Select 3 features from 5
features = ['auth', 'cache', 'logging', 'metrics', 'search']
combos = generate_combinations(features, 3)
print(f"Total combinations: {len(combos)}")  # 10
print("First 3:", combos[:3])
# [['auth', 'cache', 'logging'], 
#  ['auth', 'cache', 'metrics'], 
#  ['auth', 'cache', 'search']]

Performance Considerations

For large values, factorial calculations overflow quickly. Use optimization techniques:

1. Cancellation: For C(n,r), cancel common factors before multiplying.

2. Memoization: Cache factorial results for repeated calculations.

3. Use the symmetry property: C(n,r) = C(n, n-r), so always use the smaller value.

from functools import lru_cache

@lru_cache(maxsize=None)
def factorial_cached(n):
    """Cached factorial for performance."""
    if n < 0:
        raise ValueError("Factorial undefined for negative numbers")
    if n == 0 or n == 1:
        return 1
    return n * factorial_cached(n - 1)

def combinations_optimized(n, r):
    """Optimized combination calculation."""
    if r > n or r < 0:
        return 0
    
    # Use symmetry property
    r = min(r, n - r)
    
    # Direct multiplication with cancellation
    result = 1
    for i in range(r):
        result = result * (n - i) // (i + 1)
    return result

# Benchmark
import time

start = time.time()
print(combinations_optimized(100, 50))  # Large calculation
print(f"Time: {time.time() - start:.4f}s")
# Output: 100891344545564193334812497256
# Time: 0.0001s

Common Pitfalls and Edge Cases

Handle edge cases properly to avoid runtime errors:

def safe_permutations(n, r):
    """Permutations with comprehensive error handling."""
    if not isinstance(n, int) or not isinstance(r, int):
        raise TypeError("n and r must be integers")
    
    if n < 0 or r < 0:
        raise ValueError("n and r must be non-negative")
    
    if r > n:
        return 0  # No way to select more items than available
    
    if r == 0:
        return 1  # One way to select nothing
    
    # Check for potential overflow (factorial(21) > 64-bit int)
    if n > 20:
        raise OverflowError(f"n={n} too large, use arbitrary precision library")
    
    return factorial(n) // factorial(n - r)

def safe_combinations(n, r):
    """Combinations with comprehensive error handling."""
    if not isinstance(n, int) or not isinstance(r, int):
        raise TypeError("n and r must be integers")
    
    if n < 0 or r < 0:
        raise ValueError("n and r must be non-negative")
    
    if r > n:
        return 0
    
    if r == 0 or r == n:
        return 1
    
    # Use optimized calculation to avoid overflow
    r = min(r, n - r)
    result = 1
    for i in range(r):
        result = result * (n - i) // (i + 1)
    return result

# Test edge cases
print(safe_combinations(5, 0))   # 1
print(safe_combinations(5, 5))   # 1
print(safe_combinations(5, 6))   # 0
print(safe_permutations(0, 0))   # 1

The key to using permutations and combinations effectively is recognizing which one your problem needs. Ask yourself: if I rearrange the selected items, does it create a different outcome? If yes, use permutations. If no, use combinations. With these implementations and the understanding of when to apply each, you’re equipped to solve counting problems efficiently in production code.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.