Two pointers and sliding window are two of the most frequently tested techniques in coding interviews. They turn brute force O(n^2) solutions into elegant O(n) solutions. In this article, you will learn converging pointers, same-direction pointers, fast/slow pointers, and both fixed-size and variable-size sliding windows.
We show core examples in Kotlin, Python, and Go.
Two Pointers: Converging (Opposite Direction)
Two pointers start at opposite ends of the array and move toward each other.
Two Sum II (Sorted Array)
Given a sorted array, find two numbers that add up to a target.
Python:
def two_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
current_sum = nums[left] + nums[right]
if current_sum == target:
return [left, right]
elif current_sum < target:
left += 1 # Need a bigger sum
else:
right -= 1 # Need a smaller sum
return []
Kotlin:
fun twoSumSorted(nums: IntArray, target: Int): IntArray {
var left = 0
var right = nums.size - 1
while (left < right) {
val sum = nums[left] + nums[right]
when {
sum == target -> return intArrayOf(left, right)
sum < target -> left++
else -> right--
}
}
return intArrayOf()
}
Go:
func twoSumSorted(nums []int, target int) []int {
left, right := 0, len(nums)-1
for left < right {
sum := nums[left] + nums[right]
if sum == target {
return []int{left, right}
} else if sum < target {
left++
} else {
right--
}
}
return nil
}
Time: O(n). Space: O(1).
Note: LeetCode #167 (Two Sum II) expects 1-based indices in its return value. If you are solving that specific problem, return [left + 1, right + 1] instead. The code above returns 0-based indices, which is the general convention for all other problems.
Container With Most Water
Given heights of lines, find the two lines that hold the most water.
Python:
def max_area(heights):
left, right = 0, len(heights) - 1
result = 0
while left < right:
width = right - left
height = min(heights[left], heights[right])
result = max(result, width * height)
if heights[left] < heights[right]:
left += 1
else:
right -= 1
return result
Time: O(n). Space: O(1).
3Sum
Find all unique triplets that sum to zero. Sort first, then use two pointers for the inner loop.
Python:
def three_sum(nums):
nums.sort()
result = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue # Skip duplicates
left, right = i + 1, len(nums) - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total == 0:
result.append([nums[i], nums[left], nums[right]])
while left < right and nums[left] == nums[left + 1]:
left += 1 # Skip duplicates
while left < right and nums[right] == nums[right - 1]:
right -= 1 # Skip duplicates
left += 1
right -= 1
elif total < 0:
left += 1
else:
right -= 1
return result
Time: O(n^2). Space: O(1) excluding output. Sorting is O(n log n), but the nested loop dominates.
Two Pointers: Same Direction
Both pointers move in the same direction, usually from left to right.
Remove Duplicates from Sorted Array
Remove duplicates in-place and return the new length.
Python:
def remove_duplicates(nums):
if not nums:
return 0
slow = 0
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1
Kotlin:
fun removeDuplicates(nums: IntArray): Int {
if (nums.isEmpty()) return 0
var slow = 0
for (fast in 1 until nums.size) {
if (nums[fast] != nums[slow]) {
slow++
nums[slow] = nums[fast]
}
}
return slow + 1
}
Go:
func removeDuplicates(nums []int) int {
if len(nums) == 0 {
return 0
}
slow := 0
for fast := 1; fast < len(nums); fast++ {
if nums[fast] != nums[slow] {
slow++
nums[slow] = nums[fast]
}
}
return slow + 1
}
Time: O(n). Space: O(1).
Fast and Slow Pointers
One pointer moves one step at a time, the other moves two steps. This technique detects cycles and finds middle elements.
Linked List Cycle Detection (Floyd’s Algorithm)
Python:
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
Find Middle of Linked List
Python:
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # slow is at the middle
Happy Number
A number is happy if repeatedly replacing it with the sum of squares of its digits eventually reaches 1.
Python:
def is_happy(n):
def get_next(num):
total = 0
while num > 0:
digit = num % 10
total += digit * digit
num //= 10
return total
slow = n
fast = get_next(n)
while fast != 1 and slow != fast:
slow = get_next(slow)
fast = get_next(get_next(fast))
return fast == 1
Time: O(d²) where d is the number of digits. The cycle detection (Floyd’s algorithm) guarantees O(1) space.
Sliding Window: Fixed Size
A window of fixed size k slides across the array. Maintain a running calculation as the window moves.
Maximum Sum Subarray of Size K
Python:
def max_sum_subarray(nums, k):
window_sum = sum(nums[:k])
result = window_sum
for i in range(k, len(nums)):
window_sum += nums[i] - nums[i - k] # Add right, remove left
result = max(result, window_sum)
return result
Kotlin:
fun maxSumSubarray(nums: IntArray, k: Int): Int {
var windowSum = nums.take(k).sum()
var result = windowSum
for (i in k until nums.size) {
windowSum += nums[i] - nums[i - k]
result = maxOf(result, windowSum)
}
return result
}
Go:
func maxSumSubarray(nums []int, k int) int {
windowSum := 0
for i := 0; i < k; i++ {
windowSum += nums[i]
}
result := windowSum
for i := k; i < len(nums); i++ {
windowSum += nums[i] - nums[i-k]
if windowSum > result {
result = windowSum
}
}
return result
}
Time: O(n). Space: O(1).
Sliding Window: Variable Size
The window expands and shrinks to find the optimal substring or subarray.
Template for Variable-Size Sliding Window
Python:
def sliding_window_template(s):
window = {} # or counter
left = 0
result = 0
for right in range(len(s)):
# Expand: add s[right] to window
window[s[right]] = window.get(s[right], 0) + 1
# Shrink: while window violates your constraint (implement based on problem)
# Example: while len(window) > k — replace with your actual condition
while window_is_invalid(): # replace window_is_invalid() with your condition
# Remove s[left] from window
window[s[left]] -= 1
if window[s[left]] == 0:
del window[s[left]]
left += 1
# Update result
result = max(result, right - left + 1)
return result
Longest Substring Without Repeating Characters
Python:
def length_of_longest_substring(s):
char_set = set()
left = 0
result = 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])
result = max(result, right - left + 1)
return result
Kotlin:
fun lengthOfLongestSubstring(s: String): Int {
val charSet = mutableSetOf<Char>()
var left = 0
var result = 0
for (right in s.indices) {
while (s[right] in charSet) {
charSet.remove(s[left])
left++
}
charSet.add(s[right])
result = maxOf(result, right - left + 1)
}
return result
}
Go:
func lengthOfLongestSubstring(s string) int {
charSet := make(map[byte]bool)
left, result := 0, 0
for right := 0; right < len(s); right++ {
for charSet[s[right]] {
delete(charSet, s[left])
left++
}
charSet[s[right]] = true
if right-left+1 > result {
result = right - left + 1
}
}
return result
}
Time: O(n). Space: O(min(n, alphabet size)).
Minimum Window Substring
Find the smallest window in s that contains all characters of t.
Python:
from collections import Counter
def min_window(s, t):
if not t or not s:
return ""
need = Counter(t)
have = {}
have_count, need_count = 0, len(need)
result = ""
result_len = float("inf")
left = 0
for right in range(len(s)):
char = s[right]
have[char] = have.get(char, 0) + 1
if char in need and have[char] == need[char]:
have_count += 1
while have_count == need_count:
window_len = right - left + 1
if window_len < result_len:
result_len = window_len
result = s[left:right + 1]
have[s[left]] -= 1
if s[left] in need and have[s[left]] < need[s[left]]:
have_count -= 1
left += 1
return result
Time: O(n + m) where n = len(s), m = len(t). Space: O(m).
How to Recognize These Patterns
| If the problem says… | Try… |
|---|---|
| “Sorted array” + “find pair” | Two pointers (converging) |
| “Remove in-place” | Two pointers (same direction) |
| “Cycle detection” | Fast and slow pointers |
| “Subarray of size k” | Fixed sliding window |
| “Longest/shortest substring with condition” | Variable sliding window |
| “Contiguous subarray” | Sliding window |
| “Palindrome” | Two pointers from both ends |
Common Mistakes
Using two pointers on unsorted data when the algorithm requires sorted input. Converging two pointers for Two Sum only works on sorted arrays.
Forgetting to handle duplicates in 3Sum. Skip duplicate values for all three pointers to avoid duplicate triplets.
Not shrinking the sliding window correctly. Make sure you remove the left element before moving the left pointer.
Using a fixed-size window when the problem needs a variable-size window. “Longest substring” needs variable size. “Sum of k elements” needs fixed size.
Practice Problems
| Problem | Technique | LeetCode # |
|---|---|---|
| Two Sum II | Converging pointers | #167 |
| 3Sum | Sort + converging | #15 |
| Container With Most Water | Converging pointers | #11 |
| Remove Duplicates | Same direction | #26 |
| Linked List Cycle | Fast/slow pointers | #141 |
| Maximum Average Subarray | Fixed window | #643 |
| Longest Substring Without Repeating | Variable window | #3 |
| Minimum Window Substring | Variable window | #76 |
What’s Next?
Two pointers handle many array and string problems, but some problems require exploring all possibilities. The next article covers Recursion and Backtracking — how to systematically try all options and build solutions step by step.
Next: DSA Tutorial #15: Recursion and Backtracking
Full series: DSA from Zero to Interview Ready