Two Pointers (два указателя) — техника, при которой мы используем два индекса для обхода массива. Это часто позволяет свести решение с O(n²) до O(n).
Два основных варианта:
Навстречу друг другу — left идёт вправо, right идёт влево
В одном направлении — slow и fast двигаются вправо с разной скоростью
Вариант 1: Навстречу
[1, 2, 3, 4, 5, 6, 7]
L-> <-R
Вариант 2: В одном направлении
[1, 2, 3, 4, 5, 6, 7]
S->
F->
Когда использовать
Массив отсортирован (или можно отсортировать)
Нужно найти пару/тройку с определённым свойством
Нужно разделить массив на части
Работа с палиндромами
Нужно удалить/перемещать элементы in-place
Задача 1: Two Sum (отсортированный массив)
Условие: дан отсортированный массив, найти два числа с заданной суммой.
function twoSumSorted(array $arr, int $target): array
{
$left = 0;
$right = count($arr) - 1;
while ($left < $right) {
$currentSum = $arr[$left] + $arr[$right];
if ($currentSum === $target) {
return [$left, $right];
} elseif ($currentSum < $target) {
$left++; // Need more — move left
} else {
$right--; // Need less — move right
}
}
return [];
}
// Example: arr=[1,2,3,4,6], target=6
// left=0, right=4: 1+6=7 > 6 -> right=3
// left=0, right=3: 1+4=5 < 6 -> left=1
// left=1, right=3: 2+4=6 == 6 -> [1, 3]
func twoSumSorted(arr []int, target int) [2]int {
left, right := 0, len(arr)-1
for left < right {
currentSum := arr[left] + arr[right]
if currentSum == target {
return [2]int{left, right}
} else if currentSum < target {
left++ // Need more — move left
} else {
right-- // Need less — move right
}
}
return [2]int{-1, -1}
}
// Example: arr=[1,2,3,4,6], target=6
// left=0, right=4: 1+6=7 > 6 -> right=3
// left=0, right=3: 1+4=5 < 6 -> left=1
// left=1, right=3: 2+4=6 == 6 -> [1, 3]
// Nullable tuple communicates "not found" without a sentinel pair
public static (int Left, int Right)? TwoSumSorted(IReadOnlyList<int> arr, int target)
{
int left = 0;
int right = arr.Count - 1;
while (left < right)
{
int currentSum = arr[left] + arr[right];
if (currentSum == target)
{
return (left, right);
}
if (currentSum < target)
{
left++; // Need more — move left
}
else
{
right--; // Need less — move right
}
}
return null;
}
// Example: arr=[1,2,3,4,6], target=6
// left=0, right=4: 1+6=7 > 6 -> right=3
// left=0, right=3: 1+4=5 < 6 -> left=1
// left=1, right=3: 2+4=6 == 6 -> (1, 3)
def two_sum_sorted(arr: list[int], target: int) -> tuple[int, int] | None:
left, right = 0, len(arr) - 1
while left < right:
current_sum = arr[left] + arr[right]
if current_sum == target:
return left, right
if current_sum < target:
left += 1 # Need more — move left
else:
right -= 1 # Need less — move right
return None
# Example: arr=[1,2,3,4,6], target=6
# left=0, right=4: 1+6=7 > 6 -> right=3
# left=0, right=3: 1+4=5 < 6 -> left=1
# left=1, right=3: 2+4=6 == 6 -> (1, 3)
**Сложность:** O(n) времени, O(1) памяти.
Сравни с наивным подходом O(n²):
// O(n²) — brute force all pairs
function twoSumBrute(array $arr, int $target): array
{
$n = count($arr);
for ($i = 0; $i < $n; $i++) {
for ($j = $i + 1; $j < $n; $j++) {
if ($arr[$i] + $arr[$j] === $target) {
return [$i, $j];
}
}
}
return [];
}
// O(n²) — brute force all pairs
func twoSumBrute(arr []int, target int) [2]int {
n := len(arr)
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
if arr[i]+arr[j] == target {
return [2]int{i, j}
}
}
}
return [2]int{-1, -1}
}
// O(n²) — brute force all pairs
public static (int Left, int Right)? TwoSumBrute(IReadOnlyList<int> arr, int target)
{
int n = arr.Count;
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
if (arr[i] + arr[j] == target)
{
return (i, j);
}
}
}
return null;
}
# O(n²) — brute force all pairs
def two_sum_brute(arr: list[int], target: int) -> tuple[int, int] | None:
n = len(arr)
for i in range(n):
for j in range(i + 1, n):
if arr[i] + arr[j] == target:
return i, j
return None
func threeSum(nums []int) [][]int {
sort.Ints(nums) // O(n log n)
var result [][]int
n := len(nums)
for i := 0; i < n-2; i++ {
// Skip duplicates for i
if i > 0 && nums[i] == nums[i-1] {
continue
}
left, right := i+1, n-1
for left < right {
total := nums[i] + nums[left] + nums[right]
if total == 0 {
result = append(result, []int{nums[i], nums[left], nums[right]})
// Skip duplicates
for left < right && nums[left] == nums[left+1] {
left++
}
for left < right && nums[right] == nums[right-1] {
right--
}
left++
right--
} else if total < 0 {
left++
} else {
right--
}
}
}
return result
}
// Example: [-1, 0, 1, 2, -1, -4]
// Sorted: [-4, -1, -1, 0, 1, 2]
// Result: [[-1, -1, 2], [-1, 0, 1]]
public static List<int[]> ThreeSum(int[] nums)
{
Array.Sort(nums); // O(n log n)
var result = new List<int[]>();
int n = nums.Length;
for (int i = 0; i < n - 2; i++)
{
// Skip duplicates for i
if (i > 0 && nums[i] == nums[i - 1])
{
continue;
}
int left = i + 1;
int right = n - 1;
while (left < right)
{
int total = nums[i] + nums[left] + nums[right];
if (total == 0)
{
result.Add([nums[i], nums[left], nums[right]]);
// Skip duplicates
while (left < right && nums[left] == nums[left + 1])
{
left++;
}
while (left < right && nums[right] == nums[right - 1])
{
right--;
}
left++;
right--;
}
else if (total < 0)
{
left++;
}
else
{
right--;
}
}
}
return result;
}
// Example: [-1, 0, 1, 2, -1, -4]
// Sorted: [-4, -1, -1, 0, 1, 2]
// Result: [[-1, -1, 2], [-1, 0, 1]]
def three_sum(nums: list[int]) -> list[list[int]]:
nums.sort() # O(n log n)
result: list[list[int]] = []
n = len(nums)
for i in range(n - 2):
# Skip duplicates for i
if i > 0 and nums[i] == nums[i - 1]:
continue
left, right = i + 1, n - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total == 0:
result.append([nums[i], nums[left], nums[right]])
# Skip duplicates
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
elif total < 0:
left += 1
else:
right -= 1
return result
# Example: [-1, 0, 1, 2, -1, -4]
# Sorted: [-4, -1, -1, 0, 1, 2]
# Result: [[-1, -1, 2], [-1, 0, 1]]
**Сложность:** O(n²) времени (сортировка + цикл с two pointers), O(1) доп. памяти.
Задача 3: Container With Most Water
Условие: массив высот, найти максимальную площадь контейнера.
function maxArea(array $heights): int
{
$left = 0;
$right = count($heights) - 1;
$maxWater = 0;
while ($left < $right) {
$width = $right - $left;
$height = min($heights[$left], $heights[$right]);
$area = $width * $height;
$maxWater = max($maxWater, $area);
// Move the pointer with smaller height
if ($heights[$left] < $heights[$right]) {
$left++;
} else {
$right--;
}
}
return $maxWater;
}
// Why move the smaller one?
// If we move the taller one — width decreases and height won't increase
// (limited by the shorter one). Area will definitely decrease.
// Moving the shorter one — there's a chance to find a taller wall.
func maxArea(heights []int) int {
left, right := 0, len(heights)-1
maxWater := 0
for left < right {
width := right - left
height := min(heights[left], heights[right])
area := width * height
maxWater = max(maxWater, area)
// Move the pointer with smaller height
if heights[left] < heights[right] {
left++
} else {
right--
}
}
return maxWater
}
// Why move the smaller one?
// If we move the taller one — width decreases and height won't increase
// (limited by the shorter one). Area will definitely decrease.
// Moving the shorter one — there's a chance to find a taller wall.
public static int MaxArea(IReadOnlyList<int> heights)
{
int left = 0;
int right = heights.Count - 1;
int maxWater = 0;
while (left < right)
{
int width = right - left;
int height = Math.Min(heights[left], heights[right]);
int area = width * height;
maxWater = Math.Max(maxWater, area);
// Move the pointer with smaller height
if (heights[left] < heights[right])
{
left++;
}
else
{
right--;
}
}
return maxWater;
}
// Why move the smaller one?
// If we move the taller one — width decreases and height won't increase
// (limited by the shorter one). Area will definitely decrease.
// Moving the shorter one — there's a chance to find a taller wall.
def max_area(heights: list[int]) -> int:
left, right = 0, len(heights) - 1
max_water = 0
while left < right:
width = right - left
height = min(heights[left], heights[right])
max_water = max(max_water, width * height)
# Move the pointer with smaller height
if heights[left] < heights[right]:
left += 1
else:
right -= 1
return max_water
# Why move the smaller one?
# If we move the taller one — width decreases and height won't increase
# (limited by the shorter one). Area will definitely decrease.
# Moving the shorter one — there's a chance to find a taller wall.
**Сложность:** O(n) времени, O(1) памяти.
Задача 4: Проверка палиндрома
function isPalindrome(string $s): bool
{
// Keep only letters and digits, convert to lowercase
$s = preg_replace('/[^a-zA-Z0-9]/', '', strtolower($s));
$left = 0;
$right = strlen($s) - 1;
while ($left < $right) {
if ($s[$left] !== $s[$right]) {
return false;
}
$left++;
$right--;
}
return true;
}
// "A man, a plan, a canal: Panama" -> true
// "racecar" -> true
// "hello" -> false
func isPalindrome(s string) bool {
// Keep only letters and digits, convert to lowercase
var cleaned []byte
for _, ch := range strings.ToLower(s) {
if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') {
cleaned = append(cleaned, byte(ch))
}
}
left, right := 0, len(cleaned)-1
for left < right {
if cleaned[left] != cleaned[right] {
return false
}
left++
right--
}
return true
}
// "A man, a plan, a canal: Panama" -> true
// "racecar" -> true
// "hello" -> false
public static bool IsPalindrome(string s)
{
// Keep only letters and digits, convert to lowercase
char[] cleaned = s.ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray();
int left = 0;
int right = cleaned.Length - 1;
while (left < right)
{
if (cleaned[left] != cleaned[right])
{
return false;
}
left++;
right--;
}
return true;
}
// "A man, a plan, a canal: Panama" -> true
// "racecar" -> true
// "hello" -> false
def is_palindrome(s: str) -> bool:
# Keep only letters and digits, convert to lowercase
cleaned = [ch for ch in s.lower() if ch.isalnum()]
left, right = 0, len(cleaned) - 1
while left < right:
if cleaned[left] != cleaned[right]:
return False
left += 1
right -= 1
return True
# "A man, a plan, a canal: Panama" -> True
# "racecar" -> True
# "hello" -> False
## Задача 5: Удаление элемента in-place (slow/fast)
// Remove all occurrences of $val from array
function removeElement(array &$arr, int $val): int
{
$slow = 0;
for ($fast = 0; $fast < count($arr); $fast++) {
if ($arr[$fast] !== $val) {
$arr[$slow] = $arr[$fast];
$slow++;
}
}
return $slow; // New length
}
// arr = [3, 2, 2, 3], val = 3
// fast=0: arr[0]=3, skip
// fast=1: arr[1]=2, arr[0]=2, slow=1
// fast=2: arr[2]=2, arr[1]=2, slow=2
// fast=3: arr[3]=3, skip
// Result: [2, 2, ...], length = 2
// Remove all occurrences of val from slice
func removeElement(arr []int, val int) int {
slow := 0
for fast := 0; fast < len(arr); fast++ {
if arr[fast] != val {
arr[slow] = arr[fast]
slow++
}
}
return slow // New length
}
// arr = [3, 2, 2, 3], val = 3
// fast=0: arr[0]=3, skip
// fast=1: arr[1]=2, arr[0]=2, slow=1
// fast=2: arr[2]=2, arr[1]=2, slow=2
// fast=3: arr[3]=3, skip
// Result: [2, 2, ...], length = 2
// Remove all occurrences of val from array
public static int RemoveElement(int[] arr, int val)
{
int slow = 0;
for (int fast = 0; fast < arr.Length; fast++)
{
if (arr[fast] != val)
{
arr[slow] = arr[fast];
slow++;
}
}
return slow; // New length
}
// arr = [3, 2, 2, 3], val = 3
// fast=0: arr[0]=3, skip
// fast=1: arr[1]=2, arr[0]=2, slow=1
// fast=2: arr[2]=2, arr[1]=2, slow=2
// fast=3: arr[3]=3, skip
// Result: [2, 2, ...], length = 2
# Remove all occurrences of val from list, in place
def remove_element(arr: list[int], val: int) -> int:
slow = 0
for value in arr:
if value != val:
arr[slow] = value
slow += 1
return slow # New length
# arr = [3, 2, 2, 3], val = 3
# fast=0: arr[0]=3, skip
# fast=1: arr[1]=2, arr[0]=2, slow=1
# fast=2: arr[2]=2, arr[1]=2, slow=2
# fast=3: arr[3]=3, skip
# Result: [2, 2, ...], length = 2
Визуализация slow/fast:
Начало: [3, 2, 2, 3], val=3
S
F
Шаг 1: [3, 2, 2, 3] arr[F]=3, пропускаем
S
F
Шаг 2: [2, 2, 2, 3] arr[F]=2 != 3, копируем
S
F
Шаг 3: [2, 2, 2, 3] arr[F]=2 != 3, копируем
S
F
Шаг 4: [2, 2, 2, 3] arr[F]=3, пропускаем
S
Результат: [2, 2, ...] slow = 2
Задача 6: Сортировка цветов (Dutch National Flag)
// Array of 0, 1, 2. Sort in-place in a single pass.
function sortColors(array &$nums): void
{
$low = 0; // Boundary for 0
$mid = 0; // Current element
$high = count($nums) - 1; // Boundary for 2
while ($mid <= $high) {
if ($nums[$mid] === 0) {
[$nums[$low], $nums[$mid]] = [$nums[$mid], $nums[$low]];
$low++;
$mid++;
} elseif ($nums[$mid] === 1) {
$mid++;
} else { // nums[mid] === 2
[$nums[$mid], $nums[$high]] = [$nums[$high], $nums[$mid]];
$high--;
// Don't increment mid — need to check the new element
}
}
}
// [2, 0, 2, 1, 1, 0] -> [0, 0, 1, 1, 2, 2]
// Slice of 0, 1, 2. Sort in-place in a single pass.
func sortColors(nums []int) {
low, mid, high := 0, 0, len(nums)-1 // Boundaries
for mid <= high {
switch nums[mid] {
case 0:
nums[low], nums[mid] = nums[mid], nums[low]
low++
mid++
case 1:
mid++
case 2:
nums[mid], nums[high] = nums[high], nums[mid]
high--
// Don't increment mid — need to check the new element
}
}
}
// [2, 0, 2, 1, 1, 0] -> [0, 0, 1, 1, 2, 2]
// Array of 0, 1, 2. Sort in-place in a single pass.
public static void SortColors(int[] nums)
{
int low = 0; // Boundary for 0
int mid = 0; // Current element
int high = nums.Length - 1; // Boundary for 2
while (mid <= high)
{
switch (nums[mid])
{
case 0:
(nums[low], nums[mid]) = (nums[mid], nums[low]);
low++;
mid++;
break;
case 1:
mid++;
break;
case 2:
(nums[mid], nums[high]) = (nums[high], nums[mid]);
high--;
// Don't increment mid — need to check the new element
break;
default:
throw new ArgumentOutOfRangeException(nameof(nums), "Only values 0, 1, 2 are allowed");
}
}
}
// [2, 0, 2, 1, 1, 0] -> [0, 0, 1, 1, 2, 2]
# List of 0, 1, 2. Sort in-place in a single pass.
def sort_colors(nums: list[int]) -> None:
low = 0 # Boundary for 0
mid = 0 # Current element
high = len(nums) - 1 # Boundary for 2
while mid <= high:
match nums[mid]:
case 0:
nums[low], nums[mid] = nums[mid], nums[low]
low += 1
mid += 1
case 1:
mid += 1
case 2:
nums[mid], nums[high] = nums[high], nums[mid]
high -= 1
# Don't increment mid — need to check the new element
case _:
raise ValueError("Only values 0, 1, 2 are allowed")
# [2, 0, 2, 1, 1, 0] -> [0, 0, 1, 1, 2, 2]
## Задача 7: Сжатие массива (remove duplicates)
// Remove duplicates from sorted array
function removeDuplicates(array &$nums): int
{
if (empty($nums)) {
return 0;
}
$slow = 0;
for ($fast = 1; $fast < count($nums); $fast++) {
if ($nums[$fast] !== $nums[$slow]) {
$slow++;
$nums[$slow] = $nums[$fast];
}
}
return $slow + 1;
}
// [1, 1, 2, 2, 3] -> [1, 2, 3, ...], returns 3
// Remove duplicates from sorted slice
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
}
// [1, 1, 2, 2, 3] -> [1, 2, 3, ...], returns 3
// Remove duplicates from sorted array
public static int RemoveDuplicates(int[] nums)
{
if (nums.Length == 0)
{
return 0;
}
int slow = 0;
for (int fast = 1; fast < nums.Length; fast++)
{
if (nums[fast] != nums[slow])
{
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1;
}
// [1, 1, 2, 2, 3] -> [1, 2, 3, ...], returns 3
# Remove duplicates from sorted list
def remove_duplicates(nums: list[int]) -> int:
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
# [1, 1, 2, 2, 3] -> [1, 2, 3, ...], returns 3
## Шаблон для решения задач с Two Pointers
// Template 1: Moving towards each other
function twoPointersOpposite(array $arr): mixed
{
$left = 0;
$right = count($arr) - 1;
while ($left < $right) {
// Compute something with $arr[$left] and $arr[$right]
if ($conditionToMoveLeft) {
$left++;
} elseif ($conditionToMoveRight) {
$right--;
} else {
// Found answer or process
$left++;
$right--;
}
}
}
// Template 2: Same direction (slow/fast)
function twoPointersSameDirection(array &$arr): int
{
$slow = 0;
for ($fast = 0; $fast < count($arr); $fast++) {
if (someCondition($arr[$fast])) {
$arr[$slow] = $arr[$fast];
$slow++;
}
}
return $slow; // Boundary of processed part
}
// Template 1: Moving towards each other
func twoPointersOpposite(arr []int) {
left, right := 0, len(arr)-1
for left < right {
// Compute something with arr[left] and arr[right]
if conditionToMoveLeft {
left++
} else if conditionToMoveRight {
right--
} else {
// Found answer or process
left++
right--
}
}
}
// Template 2: Same direction (slow/fast)
func twoPointersSameDirection(arr []int) int {
slow := 0
for fast := 0; fast < len(arr); fast++ {
if someCondition(arr[fast]) {
arr[slow] = arr[fast]
slow++
}
}
return slow // Boundary of processed part
}
// Template 1: Moving towards each other
public static void TwoPointersOpposite(IReadOnlyList<int> arr)
{
int left = 0;
int right = arr.Count - 1;
while (left < right)
{
// Compute something with arr[left] and arr[right]
if (conditionToMoveLeft)
{
left++;
}
else if (conditionToMoveRight)
{
right--;
}
else
{
// Found answer or process
left++;
right--;
}
}
}
// Template 2: Same direction (slow/fast)
public static int TwoPointersSameDirection(int[] arr)
{
int slow = 0;
for (int fast = 0; fast < arr.Length; fast++)
{
if (SomeCondition(arr[fast]))
{
arr[slow] = arr[fast];
slow++;
}
}
return slow; // Boundary of processed part
}
# Template 1: Moving towards each other
def two_pointers_opposite(arr: list[int]) -> None:
left, right = 0, len(arr) - 1
while left < right:
# Compute something with arr[left] and arr[right]
if condition_to_move_left:
left += 1
elif condition_to_move_right:
right -= 1
else:
# Found answer or process
left += 1
right -= 1
# Template 2: Same direction (slow/fast)
def two_pointers_same_direction(arr: list[int]) -> int:
slow = 0
for value in arr:
if some_condition(value):
arr[slow] = value
slow += 1
return slow # Boundary of processed part
> **Запомни:** Two Pointers работает когда: 1) Массив отсортирован — используй встречные указатели. 2) Нужно фильтрация/перемещение in-place — используй slow/fast. 3) Ключевой инсайт — на каждом шаге один из указателей двигается, значит максимум O(n) шагов.
Итоги
Задача
Подход
Сложность
Two Sum (sorted)
Навстречу
O(n)
Three Sum
Фикс + навстречу
O(n²)
Container With Water
Навстречу
O(n)
Палиндром
Навстречу
O(n)
Remove Element
Slow/Fast
O(n)
Sort Colors
3 указателя
O(n)
Remove Duplicates
Slow/Fast
O(n)
Проверь себя
В задаче Two Sum на отсортированном массиве [1, 2, 4, 6, 10], target=8. Какие шаги выполнят указатели?
Какова временная сложность алгоритма Three Sum?
В задаче Sort Colors (Dutch National Flag) почему при swap с high мы НЕ увеличиваем mid?
```php
if ($nums[$mid] === 2) {
swap($nums[$mid], $nums[$high]);
$high--;
// mid НЕ увеличивается!
}
```
Какой вариант Two Pointers используется для удаления элементов in-place?
Code Challenges
Переворот строки на месте
Переверните строку на месте, используя технику двух указателей. Верните перевёрнутую строку.