DSA Advanced Techniques

Beyond data structures and basic algorithms, there are several powerful problem-solving patterns that appear repeatedly in coding challenges and real-world optimization. This topic covers three of the most important: Two Pointers, Sliding Window, and Backtracking. Mastering these patterns enables solving a wide variety of problems elegantly and efficiently.

Two Pointers Technique

The Two Pointers technique uses two index variables that move through a data structure — typically from opposite ends or at different speeds. This eliminates the need for nested loops, reducing O(n²) solutions to O(n).

Pattern 1: Opposite Direction Pointers

One pointer starts at the left, one at the right. They move toward each other.


# Check if a string is a palindrome
def is_palindrome(s):
    left = 0
    right = len(s) - 1
    while left < right:
        if s[left] != s[right]:
            return False
        left += 1
        right -= 1
    return True

print(is_palindrome("racecar"))   # Output: True
print(is_palindrome("hello"))     # Output: False

Pattern 2: Two Sum in a Sorted Array


def two_sum_sorted(arr, target):
    left = 0
    right = len(arr) - 1
    while left < right:
        current_sum = arr[left] + arr[right]
        if current_sum == target:
            return [left, right]
        elif current_sum < target:
            left += 1      # Need larger sum — move left pointer right
        else:
            right -= 1     # Need smaller sum — move right pointer left
    return []

print(two_sum_sorted([1, 3, 6, 8, 11], 9))   # Output: [1, 3] — 3 + 6 = 9 (Wait: indices 1,2)
# Corrected:
print(two_sum_sorted([1, 3, 6, 8, 11], 14))  # Output: [2, 3] — 6 + 8 = 14

Pattern 3: Removing Duplicates from a Sorted Array


def remove_duplicates(arr):
    if not arr:
        return 0
    slow = 0
    for fast in range(1, len(arr)):
        if arr[fast] != arr[slow]:
            slow += 1
            arr[slow] = arr[fast]
    return slow + 1   # Number of unique elements

nums = [1, 1, 2, 2, 3, 4, 4, 5]
k = remove_duplicates(nums)
print("Unique count:", k)
print("Array:", nums[:k])   # Output: [1, 2, 3, 4, 5]

Sliding Window Technique

The Sliding Window technique maintains a subset (window) of elements from a sequence and slides it across the data. It avoids recomputing the entire window each time by incrementally adding the new element and removing the old one. This reduces many O(n²) brute-force solutions to O(n).

Fixed-Size Sliding Window


# Maximum sum of any subarray of size k
def max_sum_subarray(arr, k):
    n = len(arr)
    if n < k:
        return None

    # Compute sum of first window
    window_sum = sum(arr[:k])
    max_sum = window_sum

    # Slide the window: add next element, remove first element
    for i in range(k, n):
        window_sum += arr[i] - arr[i - k]
        max_sum = max(max_sum, window_sum)

    return max_sum

print(max_sum_subarray([2, 1, 5, 1, 3, 2], 3))   # Output: 9  (5+1+3)

Variable-Size Sliding Window


# Smallest subarray with sum >= target
def min_subarray_length(arr, target):
    left = 0
    current_sum = 0
    min_len = float('inf')

    for right in range(len(arr)):
        current_sum += arr[right]

        while current_sum >= target:
            min_len = min(min_len, right - left + 1)
            current_sum -= arr[left]
            left += 1

    return min_len if min_len != float('inf') else 0

print(min_subarray_length([2, 3, 1, 2, 4, 3], 7))   # Output: 2  (4+3)

Longest Substring Without Repeating Characters


def longest_unique_substring(s):
    char_set = set()
    left = 0
    max_len = 0

    for right in range(len(s)):
        while s[right] in char_set:
            char_set.remove(s[left])
            left += 1
        char_set.add(s[right])
        max_len = max(max_len, right - left + 1)

    return max_len

print(longest_unique_substring("abcabcbb"))   # Output: 3  (abc)
print(longest_unique_substring("pwwkew"))     # Output: 3  (wke)

Backtracking

Backtracking is a problem-solving technique that builds solutions incrementally and abandons (backtracks) a partial solution as soon as it determines that it cannot possibly lead to a valid complete solution. It is like navigating a maze — if a path leads to a dead end, step back and try a different direction.

Backtracking is used for problems involving permutations, combinations, subsets, and constraint satisfaction.

Generating All Subsets


def subsets(nums):
    result = []

    def backtrack(start, current):
        result.append(current[:])   # Add a copy of current subset
        for i in range(start, len(nums)):
            current.append(nums[i])       # Include nums[i]
            backtrack(i + 1, current)     # Recurse
            current.pop()                 # Backtrack — remove nums[i]

    backtrack(0, [])
    return result

print(subsets([1, 2, 3]))
# Output: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]

Generating All Permutations


def permutations(nums):
    result = []

    def backtrack(current):
        if len(current) == len(nums):
            result.append(current[:])
            return
        for num in nums:
            if num not in current:
                current.append(num)
                backtrack(current)
                current.pop()   # Backtrack

    backtrack([])
    return result

print(permutations([1, 2, 3]))
# Output: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

N-Queens Problem

Place N queens on an N×N chessboard so no two queens threaten each other.


def n_queens(n):
    results = []
    cols = set()
    pos_diag = set()   # row + col
    neg_diag = set()   # row - col

    board = [["." for _ in range(n)] for _ in range(n)]

    def backtrack(row):
        if row == n:
            results.append(["".join(r) for r in board])
            return
        for col in range(n):
            if col in cols or (row + col) in pos_diag or (row - col) in neg_diag:
                continue   # Invalid position — skip
            # Place queen
            board[row][col] = "Q"
            cols.add(col)
            pos_diag.add(row + col)
            neg_diag.add(row - col)

            backtrack(row + 1)

            # Remove queen (backtrack)
            board[row][col] = "."
            cols.remove(col)
            pos_diag.remove(row + col)
            neg_diag.remove(row - col)

    backtrack(0)
    return len(results)   # Number of valid arrangements

print(n_queens(4))   # Output: 2
print(n_queens(8))   # Output: 92

Comparing the Three Techniques

  • Two Pointers: Works on sorted arrays or linked lists. Reduces O(n²) pair problems to O(n).
  • Sliding Window: Works on contiguous subarrays or substrings. Reduces O(n²) window problems to O(n).
  • Backtracking: Explores all valid solutions via DFS and abandons invalid paths early. Exponential time in the worst case but pruning makes it practical.

Key Points

  • Two Pointers uses two index variables to scan from ends inward or at different speeds — ideal for sorted arrays and pair-sum problems.
  • Sliding Window maintains a moving subset of data, updating incrementally — ideal for subarray or substring optimization problems.
  • Backtracking builds solutions step-by-step, using recursion to explore paths and abandoning dead ends early.
  • These three techniques appear in a large percentage of coding interview problems across all difficulty levels.
  • Recognizing the right pattern is often the key to solving a problem efficiently.
  • Backtracking problems can often be identified by keywords like "all possible," "all combinations," or "generate all."

Leave a Comment