Быстрый и медленный указатель

В этом уроке ты
  • Решишь задачу с собеседования в BigTech.
  • Освоишь технику “быстрый и медленный указатель” и научишься применять её на практике.
Задача с собеседования

Это одна из самых популярных задач на собеседованиях! Представь, что ты на собеседовании, и подумай, как бы ты решал эту задачу.

Условие

Дан массив nums. Нужно переместить все нули (0) в конец массива, при этом порядок остальных элементов должен сохраниться.

Необходимо изменять исходный массив напрямую, без создания нового массива для хранения результата.

Пример
Ввод: nums = [2,1,0,0,4,0,9]
Вывод: [2,1,4,9,0,0,0]
В чём сложность задачи

Одна из самых первых идей, которая приходит в голову, — создать новый массив, скопировав в него все ненулевые элементы, а затем добавить нужное количество нулей:

using System;
using System.Collections.Generic;

public class Solution
{
    // Время: O(n), где n - размер nums
    // Память: O(n), где n - размер nums
    public static List<int> MoveZeros(int[] nums)
    {
        // Создаем новый массив из не нулевых элементов
        List<int> result = new List<int>();
        foreach (int num in nums)
        {
            if (num != 0)
            {
                result.Add(num);
            }
        }

        // Дополняем массив нулями
        while (result.Count != nums.Length)
        {
            result.Add(0);
        }

        return result;
    }
}

Решение понятное, но не оптимальное с точки зрения памяти! Нужно изменить сам исходный массив, не создавая новый (то есть без дополнительной памяти). Именно решение без выделения дополнительной памяти и ждет интервьюер.

Оптимальное решение

Оптимальное решение достигается с помощью двух указателей:

  • p2 (быстрый указатель) — двигается на каждой итерации и ищет очередной ненулевой элемент.
  • p1 (медленный указатель) — двигается только тогда, когда p2 указывает на ненулевой элемент. Его задача — указывать на позицию в массиве, куда нужно «положить» найденный ненулевой элемент.
using System;
using System.Collections.Generic;

public class Solution
{
    public static List<int> MoveZeros(List<int> nums)
    {
        // Указывает на позицию, куда ставим следующий ненулевой элемент
        int p1 = 0;
        // Указывает на следующий ненулевой элемент
        int p2 = 0; 

        while (p2 < nums.Count)
        {
            if (nums[p2] != 0)
            {
                (nums[p1], nums[p2]) = (nums[p2], nums[p1]);
                p1++;
            }
            p2++;
        }
        return nums;
    }
}

 Рассмотрим как будет работать решение при nums = [0,2,0,1]:

Паттерн “быстрый и медленный указатель”

Этот паттерн чаще всего применяется, когда необходимо перенести какие-то элементы (например, нули) в конец массива или провести замену, не используя дополнительный массив. При этом один указатель (медленный) обычно указывает на позицию для записи нужных элементов, а второй (быстрый) перебирает массив и выполняет основную логику поиска.

Кстати, паттерн “быстрый и медленный указатель” применяется к одному массиву!

Не всегда быстрый и медленный указатель двигаются от начала к концу — иногда применяется направление от конца к началу.
Скорее практиковаться!

Скорее переходи к задачкам и решай их, чтобы закрепить все полученные знания на практике!

Видео не скачано: Unknown video type or protected stream. Оригинальная ссылка