Отже, ти вже побував на кількох технічних співбесідах і зрозумів одне: всі люблять запитувати про масиви та рядки. Якщо твій досвід схожий на мій, ти, напевно, помітив, що приблизно 7 із 10 співбесід зосереджені на масивах. Решта 3? Теж масиви, але, може, ще якась задача на рядки або щось екзотичніше — графи чи динамічне програмування. Задачі на бази даних? Рідкість.

Давайте розберемо найпоширеніші алгоритми, потрібні для успішного проходження цих співбесід — обіцяю, все буде просто і без зайвої нудьги. До кожної задачі додано точний хід думок, який варто озвучувати під час live coding, готовий код на TypeScript, Python, Java, PHP та Go, а також пряме посилання на LeetCode, щоб ти міг практикуватись одразу після прочитання.


Перед початком: правильне мислення під час live coding

Перш ніж переходити до алгоритмів, засвой одне: інтерв'юери оцінюють те, як ти думаєш, а не те, чи пам'ятаєш відповідь. Шаблон, що спрацьовує майже в кожному live coding раунді, виглядає так:

  1. Перефразуй задачу своїми словами та уточни обмеження (розмір вхідних даних, крайні випадки, чи може масив бути порожнім, чи є дублікати, чи відсортований, тощо).
  2. Пройдись по маленькому прикладу вручну — на папері або набравши у редакторі.
  3. Спочатку запропонуй рішення грубої сили, назви його складність, потім запитай: "Варто оптимізувати, чи це прийнятно?"
  4. Оптимізуй, розпізнавши патерн (хеш-мапа, два вказівники, sliding window, DP).
  5. Пиши код уважно, коментуючи дії по ходу.
  6. Тестуй на крайніх випадках: порожній вхід, один елемент, самі дублікати, від'ємні числа, максимальний розмір.

Офер отримують не ті, хто мовчки пише ідеальне рішення, — а ті, хто дає інтерв'юеру побачити хід своїх думок. А тепер перейдемо до алгоритмів.


1. Масиви: головний герой співбесід

Якби у співбесід був маскот, ним був би звичайний масив. Приблизно 7 із 10 задач містять масиви в тій чи іншій формі. Опануй ці патерни — і ти вирішиш більшу частину підготовки до співбесід.

Чому масиви такі популярні?

Масиви дають доступ O(1) за індексом, їх легко аналізувати, і вони дозволяють інтерв'юерам перевіряти навички оптимізації. Хитрість рідко полягає у тому, щоб "знайти відповідь" — треба знайти найкращу відповідь.


Задача 1.1 — Two Sum

LeetCode #1 — leetcode.com/problems/two-sum

Дано масив цілих чисел nums та ціле число target. Поверніть індекси двох чисел, сума яких дорівнює target.

Хід думок, який варто озвучити:

"Груба сила — два вкладені цикли, O(n²). Можна краще. Для кожного числа x мені потрібне target − x. Якщо я зберігатиму в хеш-мапі вже переглянуті значення, я зможу перевірити це доповнення за O(1). Один прохід, O(n) за часом, O(n) за пам'яттю."

function twoSum(nums: number[], target: number): number[] {
  const seen = new Map<number, number>(); // value -> index

  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];
    if (seen.has(complement)) {
      return [seen.get(complement)!, i];
    }
    seen.set(nums[i], i);
  }

  return []; // problem guarantees a solution, but be defensive
}
def two_sum(nums: list[int], target: int) -> list[int]:
    seen: dict[int, int] = {}  # value -> index

    for i, num in enumerate(nums):
        complement = target - num
        if complement in seen:
            return [seen[complement], i]
        seen[num] = i

    return []
public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> seen = new HashMap<>(); // value -> index

    for (int i = 0; i < nums.length; i++) {
        int complement = target - nums[i];
        if (seen.containsKey(complement)) {
            return new int[] { seen.get(complement), i };
        }
        seen.put(nums[i], i);
    }

    return new int[0];
}
function twoSum(array $nums, int $target): array
{
    $seen = []; // value => index

    foreach ($nums as $i => $num) {
        $complement = $target - $num;
        if (isset($seen[$complement])) {
            return [$seen[$complement], $i];
        }
        $seen[$num] = $i;
    }

    return [];
}
func twoSum(nums []int, target int) []int {
    seen := make(map[int]int) // value -> index

    for i, num := range nums {
        complement := target - num
        if j, ok := seen[complement]; ok {
            return []int{j, i}
        }
        seen[num] = i
    }

    return []int{}
}

Складність: Час O(n), Пам'ять O(n).

Зверни увагу на: Дублікати (nums = [3, 3], target = 6 — хеш-мапа має зберігати індекс перед перевіркою, а не після). Від'ємні числа. Один елемент, використаний двічі — задача це забороняє, тому ми перевіряємо мапу до вставки поточного значення.


Задача 1.2 — Best Time to Buy and Sell Stock

LeetCode #121 — leetcode.com/problems/best-time-to-buy-and-sell-stock

Дано масив prices, де prices[i] — ціна акції в день i. Максимізуйте прибуток від однієї пари купівля-продаж (купуємо перед продажем).

Хід думок:

"Відстежуємо мінімальну ціну, яку бачили до цього. Для кожного дня найкращий прибуток якщо продам сьогодні — це ціна сьогодні − мінімум до цього моменту. Зберігаємо поточний максимум цієї різниці."

function maxProfit(prices: number[]): number {
  let minPrice = Infinity;
  let maxProfit = 0;

  for (const price of prices) {
    if (price < minPrice) {
      minPrice = price;
    } else if (price - minPrice > maxProfit) {
      maxProfit = price - minPrice;
    }
  }

  return maxProfit;
}
def max_profit(prices: list[int]) -> int:
    min_price = float("inf")
    max_profit = 0

    for price in prices:
        if price < min_price:
            min_price = price
        elif price - min_price > max_profit:
            max_profit = price - min_price

    return max_profit
public int maxProfit(int[] prices) {
    int minPrice = Integer.MAX_VALUE;
    int maxProfit = 0;

    for (int price : prices) {
        if (price < minPrice) {
            minPrice = price;
        } else if (price - minPrice > maxProfit) {
            maxProfit = price - minPrice;
        }
    }

    return maxProfit;
}
function maxProfit(array $prices): int
{
    $minPrice = PHP_INT_MAX;
    $maxProfit = 0;

    foreach ($prices as $price) {
        if ($price < $minPrice) {
            $minPrice = $price;
        } elseif ($price - $minPrice > $maxProfit) {
            $maxProfit = $price - $minPrice;
        }
    }

    return $maxProfit;
}
func maxProfit(prices []int) int {
    minPrice := math.MaxInt
    maxProfit := 0

    for _, price := range prices {
        if price < minPrice {
            minPrice = price
        } else if price-minPrice > maxProfit {
            maxProfit = price - minPrice
        }
    }

    return maxProfit
}

Складність: Час O(n), Пам'ять O(1).

Типове уточнення: "А що, якщо можна купувати і продавати кілька разів?" — Це LeetCode #122, і відповідь — підсумувати всі додатні різниці між сусідніми днями.


Задача 1.3 — Maximum Subarray (алгоритм Кадане)

LeetCode #53 — leetcode.com/problems/maximum-subarray

Знайдіть суміжний підмасив із найбільшою сумою.

Хід думок:

"На кожному індексі у мене є вибір: продовжити попередній підмасив або почати новий звідси. Продовжую, якщо поточна сума залишається додатною — інакше починаю заново. Це і є алгоритм Кадане."

function maxSubArray(nums: number[]): number {
  let currentSum = nums[0];
  let maxSum = nums[0];

  for (let i = 1; i < nums.length; i++) {
    currentSum = Math.max(nums[i], currentSum + nums[i]);
    maxSum = Math.max(maxSum, currentSum);
  }

  return maxSum;
}
def max_sub_array(nums: list[int]) -> int:
    current_sum = max_sum = nums[0]

    for num in nums[1:]:
        current_sum = max(num, current_sum + num)
        max_sum = max(max_sum, current_sum)

    return max_sum
public int maxSubArray(int[] nums) {
    int currentSum = nums[0];
    int maxSum = nums[0];

    for (int i = 1; i < nums.length; i++) {
        currentSum = Math.max(nums[i], currentSum + nums[i]);
        maxSum = Math.max(maxSum, currentSum);
    }

    return maxSum;
}
function maxSubArray(array $nums): int
{
    $currentSum = $nums[0];
    $maxSum = $nums[0];

    for ($i = 1; $i < count($nums); $i++) {
        $currentSum = max($nums[$i], $currentSum + $nums[$i]);
        $maxSum = max($maxSum, $currentSum);
    }

    return $maxSum;
}
func maxSubArray(nums []int) int {
    currentSum, maxSum := nums[0], nums[0]

    for i := 1; i < len(nums); i++ {
        currentSum = max(nums[i], currentSum+nums[i])
        maxSum = max(maxSum, currentSum)
    }

    return maxSum
}
// Go 1.21+ has built-in max(); earlier versions need a tiny helper.

Складність: Час O(n), Пам'ять O(1).

Крайній випадок, який варто згадати: Всі від'ємні числа — відповідь є найбільший окремий елемент. Алгоритм обробляє це коректно, оскільки max(nums[i], currentSum + nums[i]) вибере nums[i], коли продовження погіршує результат.


Поради щодо масивів

  • Завжди запитуй, чи відсортований масив. Відсортований масив відкриває два вказівники та бінарний пошук.
  • Патерн двох вказівників вирішує задачі на кшталт "знайти пару з сумою X у відсортованому масиві" за O(n).
  • Префіксні суми (prefix[i] = nums[0] + … + nums[i]) перетворюють багато задач на підмасиви на O(1) range queries — дивіться LeetCode #560 — Subarray Sum Equals K.

2. Рядки: вишуканий родич масиву

Рядки — це масиви символів із кількома додатковими нюансами: незмінність у деяких мовах (Java, Python, JavaScript), кодування (ASCII проти Unicode) та пошук за шаблоном. Більшість задач на рядки зводяться до задач на масиви, щойно ти це засвоїш.


Задача 2.1 — Reverse String (на місці)

LeetCode #344 — leetcode.com/problems/reverse-string

Розгорни s на місці. Потрібно використати O(1) додаткової пам'яті.

Хід думок:

"Два вказівники: один на початку, інший наприкінці — обмінюємо і рухаємось назустріч, доки не зустрінуться."

function reverseString(s: string[]): void {
  let left = 0;
  let right = s.length - 1;

  while (left < right) {
    [s[left], s[right]] = [s[right], s[left]];
    left++;
    right--;
  }
}
def reverse_string(s: list[str]) -> None:
    left, right = 0, len(s) - 1
    while left < right:
        s[left], s[right] = s[right], s[left]
        left += 1
        right -= 1
public void reverseString(char[] s) {
    int left = 0;
    int right = s.length - 1;

    while (left < right) {
        char tmp = s[left];
        s[left] = s[right];
        s[right] = tmp;
        left++;
        right--;
    }
}
function reverseString(array &$s): void
{
    $left = 0;
    $right = count($s) - 1;

    while ($left < $right) {
        [$s[$left], $s[$right]] = [$s[$right], $s[$left]];
        $left++;
        $right--;
    }
}
func reverseString(s []byte) {
    left, right := 0, len(s)-1

    for left < right {
        s[left], s[right] = s[right], s[left]
        left++
        right--
    }
}

Складність: Час O(n), Пам'ять O(1).

Підводний камінь: У реальному коді s[::-1] у Python або [...s].reverse().join("") у JS зробить це в один рядок — але інтерв'юер хоче побачити, що ти розумієш алгоритм, а не просто синтаксис мови. Згадай скорочення, щоб показати обізнаність, а потім реалізуй вручну.


Задача 2.2 — Valid Anagram

LeetCode #242 — leetcode.com/problems/valid-anagram

Поверніть true, якщо t є анаграмою s.

Два підходи — покажи, що знаєш обидва:

Підхід A — Сортування: O(n log n) за часом, O(1) додаткової пам'яті при сортуванні на місці. Простий, працює.

Підхід B — Лічильник частот: O(n) за часом, O(1) за пам'яттю (алфавіт обмежений — 26 малих літер або 128 ASCII).

function isAnagram(s: string, t: string): boolean {
  if (s.length !== t.length) return false;

  const counts = new Map<string, number>();
  for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);

  for (const ch of t) {
    const remaining = counts.get(ch);
    if (!remaining) return false;
    counts.set(ch, remaining - 1);
  }

  return true;
}
from collections import Counter

def is_anagram(s: str, t: str) -> bool:
    return Counter(s) == Counter(t)
public boolean isAnagram(String s, String t) {
    if (s.length() != t.length()) return false;

    int[] counts = new int[26];
    for (int i = 0; i < s.length(); i++) {
        counts[s.charAt(i) - 'a']++;
        counts[t.charAt(i) - 'a']--;
    }

    for (int c : counts) if (c != 0) return false;
    return true;
}
function isAnagram(string $s, string $t): bool
{
    if (strlen($s) !== strlen($t)) return false;

    return count_chars($s, 1) === count_chars($t, 1);
}
func isAnagram(s, t string) bool {
    if len(s) != len(t) {
        return false
    }

    var counts [26]int
    for i := 0; i < len(s); i++ {
        counts[s[i]-'a']++
        counts[t[i]-'a']--
    }

    for _, c := range counts {
        if c != 0 {
            return false
        }
    }
    return true
}

Типове уточнення: "А що, якщо рядок містить символи Unicode?" — Використовуй хеш-мапу; не припускай масив із 26 елементів. Трюк із обмеженим масивом у Java/Go розрахований на малі ASCII-літери; версії на TS/Python/PHP з Map/Counter підтримують Unicode без змін.


Задача 2.3 — Longest Palindromic Substring

LeetCode #5 — leetcode.com/problems/longest-palindromic-substring

Поверніть найдовший паліндромний підрядок у s.

Хід думок — "розширення від центру":

"Паліндром є симетричним відносно свого центру. Є 2n−1 можливих центрів — n одиночних символів (непарна довжина) і n−1 між символами (парна довжина). Для кожного центру розширюємось назовні, доки символи збігаються. Відстежуємо найдовший. O(n²) за часом, O(1) за пам'яттю."

function longestPalindrome(s: string): string {
  if (s.length < 2) return s;

  let start = 0;
  let maxLen = 1;

  const expand = (left: number, right: number): void => {
    while (left >= 0 && right < s.length && s[left] === s[right]) {
      if (right - left + 1 > maxLen) {
        start = left;
        maxLen = right - left + 1;
      }
      left--;
      right++;
    }
  };

  for (let i = 0; i < s.length; i++) {
    expand(i, i);     // odd-length palindrome
    expand(i, i + 1); // even-length palindrome
  }

  return s.substring(start, start + maxLen);
}
def longest_palindrome(s: str) -> str:
    if len(s) < 2:
        return s

    start, max_len = 0, 1

    def expand(left: int, right: int) -> None:
        nonlocal start, max_len
        while left >= 0 and right < len(s) and s[left] == s[right]:
            if right - left + 1 > max_len:
                start = left
                max_len = right - left + 1
            left -= 1
            right += 1

    for i in range(len(s)):
        expand(i, i)      # odd
        expand(i, i + 1)  # even

    return s[start:start + max_len]
public String longestPalindrome(String s) {
    if (s.length() < 2) return s;

    int[] best = {0, 1}; // {start, maxLen}
    for (int i = 0; i < s.length(); i++) {
        expand(s, i, i, best);
        expand(s, i, i + 1, best);
    }
    return s.substring(best[0], best[0] + best[1]);
}

private void expand(String s, int left, int right, int[] best) {
    while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
        if (right - left + 1 > best[1]) {
            best[0] = left;
            best[1] = right - left + 1;
        }
        left--;
        right++;
    }
}
function longestPalindrome(string $s): string
{
    if (strlen($s) < 2) return $s;

    $start = 0;
    $maxLen = 1;

    $expand = function (int $left, int $right) use ($s, &$start, &$maxLen): void {
        while ($left >= 0 && $right < strlen($s) && $s[$left] === $s[$right]) {
            if ($right - $left + 1 > $maxLen) {
                $start = $left;
                $maxLen = $right - $left + 1;
            }
            $left--;
            $right++;
        }
    };

    for ($i = 0; $i < strlen($s); $i++) {
        $expand($i, $i);
        $expand($i, $i + 1);
    }

    return substr($s, $start, $maxLen);
}
func longestPalindrome(s string) string {
    if len(s) < 2 {
        return s
    }

    start, maxLen := 0, 1
    expand := func(left, right int) {
        for left >= 0 && right < len(s) && s[left] == s[right] {
            if right-left+1 > maxLen {
                start = left
                maxLen = right - left + 1
            }
            left--
            right++
        }
    }

    for i := 0; i < len(s); i++ {
        expand(i, i)
        expand(i, i+1)
    }

    return s[start : start+maxLen]
}

Складність: Час O(n²), Пам'ять O(1).

Варто згадати: Існує алгоритм O(n) під назвою Manacher's, але жоден інтерв'юер не очікує його знання. Якщо запитають — скажи, що знаєш про його існування, але розширення від центру є реалістичною відповіддю на співбесіді.


3. Хеш-мапи: незаспіваний герой

Хеш-мапи забезпечують O(1) у середньому для пошуку, вставки та видалення. Щоразу, коли в задачі є слова "знайти", "порахувати", "дублікат" або "частота" — хеш-мапа — це твій перший інстинкт.


Задача 3.1 — Contains Duplicate

LeetCode #217 — leetcode.com/problems/contains-duplicate

Поверніть true, якщо будь-яке значення зустрічається хоча б двічі.

function containsDuplicate(nums: number[]): boolean {
  const seen = new Set<number>();
  for (const num of nums) {
    if (seen.has(num)) return true;
    seen.add(num);
  }
  return false;
}

// One-liner alternative:
const containsDuplicateShort = (nums: number[]) =>
  new Set(nums).size !== nums.length;
def contains_duplicate(nums: list[int]) -> bool:
    return len(set(nums)) != len(nums)
public boolean containsDuplicate(int[] nums) {
    Set<Integer> seen = new HashSet<>();
    for (int num : nums) {
        if (!seen.add(num)) return true; // add() returns false if already present
    }
    return false;
}
function containsDuplicate(array $nums): bool
{
    return count(array_unique($nums)) !== count($nums);
}
func containsDuplicate(nums []int) bool {
    seen := make(map[int]struct{})
    for _, num := range nums {
        if _, ok := seen[num]; ok {
            return true
        }
        seen[num] = struct{}{}
    }
    return false
}

Складність: Час O(n), Пам'ять O(n).


Задача 3.2 — Group Anagrams

LeetCode #49 — leetcode.com/problems/group-anagrams

Згрупуйте рядки, що є анаграмами один одного.

Хід думок:

"Два рядки є анаграмами тоді і тільки тоді, коли їхні відсортовані форми рівні. Використовуємо відсортовану форму як ключ хеш-мапи, групуємо рядки під нею."

function groupAnagrams(strs: string[]): string[][] {
  const groups = new Map<string, string[]>();

  for (const str of strs) {
    const key = str.split("").sort().join("");
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key)!.push(str);
  }

  return Array.from(groups.values());
}
from collections import defaultdict

def group_anagrams(strs: list[str]) -> list[list[str]]:
    groups: dict[str, list[str]] = defaultdict(list)
    for s in strs:
        key = "".join(sorted(s))
        groups[key].append(s)
    return list(groups.values())
public List<List<String>> groupAnagrams(String[] strs) {
    Map<String, List<String>> groups = new HashMap<>();

    for (String str : strs) {
        char[] chars = str.toCharArray();
        Arrays.sort(chars);
        String key = new String(chars);
        groups.computeIfAbsent(key, k -> new ArrayList<>()).add(str);
    }

    return new ArrayList<>(groups.values());
}
function groupAnagrams(array $strs): array
{
    $groups = [];

    foreach ($strs as $str) {
        $chars = str_split($str);
        sort($chars);
        $key = implode("", $chars);
        $groups[$key][] = $str;
    }

    return array_values($groups);
}
func groupAnagrams(strs []string) [][]string {
    groups := make(map[string][]string)

    for _, str := range strs {
        chars := []byte(str)
        sort.Slice(chars, func(i, j int) bool { return chars[i] < chars[j] })
        key := string(chars)
        groups[key] = append(groups[key], str)
    }

    result := make([][]string, 0, len(groups))
    for _, g := range groups {
        result = append(result, g)
    }
    return result
}

Складність: Час O(n · k log k), де k — середня довжина рядка. Пам'ять O(n · k).

Оптимізація для згадки: Замість сортування використовуй кортеж із 26 лічильників символів як ключ. Це знижує час до O(n · k).


4. Sliding Window: техніка ніндзя

Патерн sliding window відмінно підходить для задач, що стосуються суміжних підмасивів або підрядків — знайти найдовший, із найбільшою сумою, порахувати кількість тощо. Два варіанти:

  • Вікно фіксованого розміру — ти знаєш k наперед.
  • Вікно змінного розміру — ти розширюєш і звужуєш вікно залежно від умови.

Задача 4.1 — Maximum Average Subarray I (фіксоване вікно)

LeetCode #643 — leetcode.com/problems/maximum-average-subarray-i

Знайдіть максимальне середнє значення будь-якого суміжного підмасиву довжиною k.

Хід думок:

"Обчислюємо суму першого вікна, потім зсуваємо: віднімаємо елемент, що виходить зліва, і додаємо новий елемент справа. O(n)."

function findMaxAverage(nums: number[], k: number): number {
  let windowSum = 0;
  for (let i = 0; i < k; i++) windowSum += nums[i];

  let maxSum = windowSum;
  for (let i = k; i < nums.length; i++) {
    windowSum += nums[i] - nums[i - k];
    maxSum = Math.max(maxSum, windowSum);
  }

  return maxSum / k;
}
def find_max_average(nums: list[int], k: int) -> float:
    window_sum = sum(nums[:k])
    max_sum = window_sum

    for i in range(k, len(nums)):
        window_sum += nums[i] - nums[i - k]
        max_sum = max(max_sum, window_sum)

    return max_sum / k
public double findMaxAverage(int[] nums, int k) {
    int windowSum = 0;
    for (int i = 0; i < k; i++) windowSum += nums[i];

    int maxSum = windowSum;
    for (int i = k; i < nums.length; i++) {
        windowSum += nums[i] - nums[i - k];
        maxSum = Math.max(maxSum, windowSum);
    }

    return (double) maxSum / k;
}
function findMaxAverage(array $nums, int $k): float
{
    $windowSum = array_sum(array_slice($nums, 0, $k));
    $maxSum = $windowSum;

    for ($i = $k; $i < count($nums); $i++) {
        $windowSum += $nums[$i] - $nums[$i - $k];
        $maxSum = max($maxSum, $windowSum);
    }

    return $maxSum / $k;
}
func findMaxAverage(nums []int, k int) float64 {
    windowSum := 0
    for i := 0; i < k; i++ {
        windowSum += nums[i]
    }

    maxSum := windowSum
    for i := k; i < len(nums); i++ {
        windowSum += nums[i] - nums[i-k]
        if windowSum > maxSum {
            maxSum = windowSum
        }
    }

    return float64(maxSum) / float64(k)
}

Складність: Час O(n), Пам'ять O(1).


Задача 4.2 — Longest Substring Without Repeating Characters (змінне вікно)

LeetCode #3 — leetcode.com/problems/longest-substring-without-repeating-characters

Знайдіть довжину найдовшого підрядка без символів, що повторюються.

Хід думок — озвучуйте уважно:

"Використовую sliding window із двома вказівниками, left і right. Розширюю right і додаю символи до множини. Коли натрапляю на дублікат, рухаю left вперед — видаляючи символи з множини — доки дублікат не зникне. Вікно завжди без дублікатів, тому відстежую його максимальний розмір."

function lengthOfLongestSubstring(s: string): number {
  const seen = new Set<string>();
  let left = 0;
  let maxLen = 0;

  for (let right = 0; right < s.length; right++) {
    while (seen.has(s[right])) {
      seen.delete(s[left]);
      left++;
    }
    seen.add(s[right]);
    maxLen = Math.max(maxLen, right - left + 1);
  }

  return maxLen;
}
def length_of_longest_substring(s: str) -> int:
    seen: set[str] = set()
    left = 0
    max_len = 0

    for right, ch in enumerate(s):
        while ch in seen:
            seen.remove(s[left])
            left += 1
        seen.add(ch)
        max_len = max(max_len, right - left + 1)

    return max_len
public int lengthOfLongestSubstring(String s) {
    Set<Character> seen = new HashSet<>();
    int left = 0;
    int maxLen = 0;

    for (int right = 0; right < s.length(); right++) {
        while (seen.contains(s.charAt(right))) {
            seen.remove(s.charAt(left));
            left++;
        }
        seen.add(s.charAt(right));
        maxLen = Math.max(maxLen, right - left + 1);
    }

    return maxLen;
}
function lengthOfLongestSubstring(string $s): int
{
    $seen = [];
    $left = 0;
    $maxLen = 0;

    for ($right = 0; $right < strlen($s); $right++) {
        while (isset($seen[$s[$right]])) {
            unset($seen[$s[$left]]);
            $left++;
        }
        $seen[$s[$right]] = true;
        $maxLen = max($maxLen, $right - $left + 1);
    }

    return $maxLen;
}
func lengthOfLongestSubstring(s string) int {
    seen := make(map[byte]bool)
    left, maxLen := 0, 0

    for right := 0; right < len(s); right++ {
        for seen[s[right]] {
            delete(seen, s[left])
            left++
        }
        seen[s[right]] = true
        if right-left+1 > maxLen {
            maxLen = right - left + 1
        }
    }

    return maxLen
}

Складність: Час O(n) — кожен символ входить у вікно і виходить із нього не більше одного разу. Пам'ять O(min(n, розмір алфавіту)).

Поширений варіант: LeetCode #424 — Longest Repeating Character Replacement. Той самий патерн, інший інваріант — вікно є дійсним, поки розмір_вікна − найчастіший_символ ≤ k.


5. Динамічне програмування: коли все серйозно

DP лякає людей, але це просто рекурсія + кешування. Рецепт завжди однаковий:

  1. Визнач стан — що означає dp[i] (або dp[i][j])?
  2. Запиши рекурентне співвідношення — як dp[i] залежить від менших підзадач?
  3. Визнач базовий випадок.
  4. Обери напрямок — зверху вниз (мемоізація) або знизу вгору (табуляція).
  5. Оптимізуй пам'ять, якщо потрібні лише кілька останніх станів.

Задача 5.1 — Climbing Stairs

LeetCode #70 — leetcode.com/problems/climbing-stairs

Ви можете піднятися на 1 або 2 сходинки за раз. Скільки різних способів дістатися до сходинки n?

Хід думок:

"Щоб дістатися до сходинки n, мій останній крок — або 1 сходинка з n−1, або 2 сходинки з n−2. Отже, ways(n) = ways(n−1) + ways(n−2) — це числа Фібоначчі. Базові випадки: ways(1) = 1, ways(2) = 2. Потрібні лише два останні значення, тому O(1) пам'яті."

function climbStairs(n: number): number {
  if (n <= 2) return n;

  let prev2 = 1;
  let prev1 = 2;

  for (let i = 3; i <= n; i++) {
    const curr = prev1 + prev2;
    prev2 = prev1;
    prev1 = curr;
  }

  return prev1;
}
def climb_stairs(n: int) -> int:
    if n <= 2:
        return n

    prev2, prev1 = 1, 2
    for _ in range(3, n + 1):
        prev2, prev1 = prev1, prev1 + prev2

    return prev1
public int climbStairs(int n) {
    if (n <= 2) return n;

    int prev2 = 1;
    int prev1 = 2;

    for (int i = 3; i <= n; i++) {
        int curr = prev1 + prev2;
        prev2 = prev1;
        prev1 = curr;
    }

    return prev1;
}
function climbStairs(int $n): int
{
    if ($n <= 2) return $n;

    $prev2 = 1;
    $prev1 = 2;

    for ($i = 3; $i <= $n; $i++) {
        $curr = $prev1 + $prev2;
        $prev2 = $prev1;
        $prev1 = $curr;
    }

    return $prev1;
}
func climbStairs(n int) int {
    if n <= 2 {
        return n
    }

    prev2, prev1 := 1, 2
    for i := 3; i <= n; i++ {
        prev2, prev1 = prev1, prev1+prev2
    }

    return prev1
}

Складність: Час O(n), Пам'ять O(1).

Чому це важливо: Це найпростіша задача на DP. Якщо ти можеш чітко сформулювати рекурентне співвідношення та оптимізацію пам'яті тут — ти продемонстрував інтерв'юеру весь інструментарій DP.


Задача 5.2 — House Robber

LeetCode #198 — leetcode.com/problems/house-robber

На вулиці стоять будинки, у кожному є гроші. Не можна грабувати два сусідніх будинки. Максимізуйте загальну суму.

Хід думок:

"На будинку i у мене два варіанти: пограбувати його (додати nums[i] до найкращого результату до i−2) або пропустити (перенести найкращий результат до i−1). Тобто dp[i] = max(dp[i−1], dp[i−2] + nums[i])."

function rob(nums: number[]): number {
  let prev2 = 0; // max loot up to i-2
  let prev1 = 0; // max loot up to i-1

  for (const num of nums) {
    const curr = Math.max(prev1, prev2 + num);
    prev2 = prev1;
    prev1 = curr;
  }

  return prev1;
}
def rob(nums: list[int]) -> int:
    prev2 = prev1 = 0
    for num in nums:
        prev2, prev1 = prev1, max(prev1, prev2 + num)
    return prev1
public int rob(int[] nums) {
    int prev2 = 0;
    int prev1 = 0;

    for (int num : nums) {
        int curr = Math.max(prev1, prev2 + num);
        prev2 = prev1;
        prev1 = curr;
    }

    return prev1;
}
function rob(array $nums): int
{
    $prev2 = 0;
    $prev1 = 0;

    foreach ($nums as $num) {
        $curr = max($prev1, $prev2 + $num);
        $prev2 = $prev1;
        $prev1 = $curr;
    }

    return $prev1;
}
func rob(nums []int) int {
    prev2, prev1 := 0, 0

    for _, num := range nums {
        curr := max(prev1, prev2+num)
        prev2 = prev1
        prev1 = curr
    }

    return prev1
}

Складність: Час O(n), Пам'ять O(1).

Типове уточнення: LeetCode #213 — House Robber II (будинки у колі). Трюк: запустіть лінійне рішення двічі — один раз без першого будинку, один раз без останнього — і візьміть максимум.


Задача 5.3 — Longest Common Subsequence

LeetCode #1143 — leetcode.com/problems/longest-common-subsequence

Дано два рядки. Знайдіть довжину їхньої найдовшої спільної підпослідовності (не обов'язково суміжної).

Хід думок — двовимірне DP:

"Визначаємо dp[i][j] = довжина LCS для text1[0..i] і text2[0..j]. Якщо символи збігаються, dp[i][j] = dp[i−1][j−1] + 1. Інакше, dp[i][j] = max(dp[i−1][j], dp[i][j−1]) — відкидаємо один символ із будь-якого рядка і беремо найкраще."

function longestCommonSubsequence(text1: string, text2: string): number {
  const m = text1.length;
  const n = text2.length;
  const dp: number[][] = Array.from({ length: m + 1 }, () =>
    new Array(n + 1).fill(0),
  );

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (text1[i - 1] === text2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }

  return dp[m][n];
}
def longest_common_subsequence(text1: str, text2: str) -> int:
    m, n = len(text1), len(text2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i - 1] == text2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

    return dp[m][n]
public int longestCommonSubsequence(String text1, String text2) {
    int m = text1.length();
    int n = text2.length();
    int[][] dp = new int[m + 1][n + 1];

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }

    return dp[m][n];
}
function longestCommonSubsequence(string $text1, string $text2): int
{
    $m = strlen($text1);
    $n = strlen($text2);
    $dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));

    for ($i = 1; $i <= $m; $i++) {
        for ($j = 1; $j <= $n; $j++) {
            if ($text1[$i - 1] === $text2[$j - 1]) {
                $dp[$i][$j] = $dp[$i - 1][$j - 1] + 1;
            } else {
                $dp[$i][$j] = max($dp[$i - 1][$j], $dp[$i][$j - 1]);
            }
        }
    }

    return $dp[$m][$n];
}
func longestCommonSubsequence(text1, text2 string) int {
    m, n := len(text1), len(text2)
    dp := make([][]int, m+1)
    for i := range dp {
        dp[i] = make([]int, n+1)
    }

    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            if text1[i-1] == text2[j-1] {
                dp[i][j] = dp[i-1][j-1] + 1
            } else {
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
            }
        }
    }

    return dp[m][n]
}

Складність: Час O(m·n), Пам'ять O(m·n) — можна оптимізувати до O(min(m, n)), зберігаючи лише два рядки.


6. Графи: завдання для розуму

Задачі на графи перевіряють твою здатність мислити зв'язками. Два основних обходи — BFS (черга, знаходить найкоротші шляхи у невважених графах) та DFS (стек/рекурсія, чудово підходить для зв'язних компонентів і виявлення циклів) — вирішують переважну більшість питань на графи.


Задача 6.1 — Number of Islands

LeetCode #200 — leetcode.com/problems/number-of-islands

Дано сітку m × n із '1' (суша) та '0' (вода). Порахуйте кількість островів.

Хід думок:

"Проходимо по сітці. Коли знаходжу невідвіданий шматок суші — це новий острів. Запускаю BFS звідти, щоб обійти весь зв'язний компонент, позначаючи відвіданим. Кожного разу, коли запускаю flood-fill, збільшую лічильник."

function numIslands(grid: string[][]): number {
  if (!grid.length) return 0;
  const rows = grid.length;
  const cols = grid[0].length;
  const dirs: [number, number][] = [[1, 0], [-1, 0], [0, 1], [0, -1]];
  let count = 0;

  const bfs = (sr: number, sc: number): void => {
    const queue: [number, number][] = [[sr, sc]];
    let head = 0; // index pointer instead of shift() — O(1) dequeue
    grid[sr][sc] = "0";

    while (head < queue.length) {
      const [r, c] = queue[head++];
      for (const [dr, dc] of dirs) {
        const nr = r + dr;
        const nc = c + dc;
        if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === "1") {
          grid[nr][nc] = "0";
          queue.push([nr, nc]);
        }
      }
    }
  };

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === "1") {
        count++;
        bfs(r, c);
      }
    }
  }

  return count;
}
from collections import deque

def num_islands(grid: list[list[str]]) -> int:
    if not grid:
        return 0

    rows, cols = len(grid), len(grid[0])
    count = 0

    def bfs(r: int, c: int) -> None:
        queue = deque([(r, c)])
        grid[r][c] = "0"
        while queue:
            row, col = queue.popleft()
            for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                nr, nc = row + dr, col + dc
                if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == "1":
                    grid[nr][nc] = "0"
                    queue.append((nr, nc))

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == "1":
                count += 1
                bfs(r, c)

    return count
public int numIslands(char[][] grid) {
    if (grid.length == 0) return 0;
    int rows = grid.length;
    int cols = grid[0].length;
    int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    int count = 0;

    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            if (grid[r][c] != '1') continue;
            count++;
            Deque<int[]> queue = new ArrayDeque<>();
            queue.offer(new int[]{r, c});
            grid[r][c] = '0';
            while (!queue.isEmpty()) {
                int[] cell = queue.poll();
                for (int[] d : dirs) {
                    int nr = cell[0] + d[0];
                    int nc = cell[1] + d[1];
                    if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == '1') {
                        grid[nr][nc] = '0';
                        queue.offer(new int[]{nr, nc});
                    }
                }
            }
        }
    }

    return count;
}
function numIslands(array $grid): int
{
    if (empty($grid)) return 0;
    $rows = count($grid);
    $cols = count($grid[0]);
    $dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
    $count = 0;

    for ($r = 0; $r < $rows; $r++) {
        for ($c = 0; $c < $cols; $c++) {
            if ($grid[$r][$c] !== '1') continue;
            $count++;
            $queue = [[$r, $c]];
            $head = 0;
            $grid[$r][$c] = '0';
            while ($head < count($queue)) {
                [$row, $col] = $queue[$head++];
                foreach ($dirs as [$dr, $dc]) {
                    $nr = $row + $dr;
                    $nc = $col + $dc;
                    if ($nr >= 0 && $nr < $rows && $nc >= 0 && $nc < $cols && $grid[$nr][$nc] === '1') {
                        $grid[$nr][$nc] = '0';
                        $queue[] = [$nr, $nc];
                    }
                }
            }
        }
    }

    return $count;
}
func numIslands(grid [][]byte) int {
    if len(grid) == 0 {
        return 0
    }
    rows, cols := len(grid), len(grid[0])
    dirs := [4][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
    count := 0

    for r := 0; r < rows; r++ {
        for c := 0; c < cols; c++ {
            if grid[r][c] != '1' {
                continue
            }
            count++
            queue := [][2]int{{r, c}}
            grid[r][c] = '0'
            for len(queue) > 0 {
                cell := queue[0]
                queue = queue[1:]
                for _, d := range dirs {
                    nr, nc := cell[0]+d[0], cell[1]+d[1]
                    if nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == '1' {
                        grid[nr][nc] = '0'
                        queue = append(queue, [2]int{nr, nc})
                    }
                }
            }
        }
    }

    return count
}

Складність: Час O(m·n), Пам'ять O(m·n) у найгіршому випадку (черга містить найбільший острів у пікові).


Задача 6.2 — Course Schedule (виявлення циклів / топологічне сортування)

LeetCode #207 — leetcode.com/problems/course-schedule

Дано numCourses та пари передумов [a, b] (потрібно прослухати b перед a). Чи можна пройти всі курси?

Хід думок:

"Будуємо орієнтований граф із передумов. Задача зводиться до: чи є в цьому графі цикл? Якщо так — неможливо. Використаю алгоритм Кана — обчислю вхідні ступені вершин, починаю з вершин із вхідним ступенем 0, видаляю їх і зменшую ступені сусідів. Якщо обробив усі numCourses вершин — циклу немає."

function canFinish(numCourses: number, prerequisites: number[][]): boolean {
  const graph: number[][] = Array.from({ length: numCourses }, () => []);
  const inDegree: number[] = new Array(numCourses).fill(0);

  for (const [course, prereq] of prerequisites) {
    graph[prereq].push(course);
    inDegree[course]++;
  }

  const queue: number[] = [];
  for (let i = 0; i < numCourses; i++) {
    if (inDegree[i] === 0) queue.push(i);
  }

  let processed = 0;
  let head = 0;
  while (head < queue.length) {
    const course = queue[head++];
    processed++;
    for (const next of graph[course]) {
      if (--inDegree[next] === 0) queue.push(next);
    }
  }

  return processed === numCourses;
}
from collections import defaultdict, deque

def can_finish(num_courses: int, prerequisites: list[list[int]]) -> bool:
    graph = defaultdict(list)
    in_degree = [0] * num_courses

    for course, prereq in prerequisites:
        graph[prereq].append(course)
        in_degree[course] += 1

    queue = deque(i for i in range(num_courses) if in_degree[i] == 0)
    processed = 0

    while queue:
        course = queue.popleft()
        processed += 1
        for nxt in graph[course]:
            in_degree[nxt] -= 1
            if in_degree[nxt] == 0:
                queue.append(nxt)

    return processed == num_courses
public boolean canFinish(int numCourses, int[][] prerequisites) {
    List<List<Integer>> graph = new ArrayList<>();
    int[] inDegree = new int[numCourses];
    for (int i = 0; i < numCourses; i++) graph.add(new ArrayList<>());

    for (int[] p : prerequisites) {
        graph.get(p[1]).add(p[0]);
        inDegree[p[0]]++;
    }

    Deque<Integer> queue = new ArrayDeque<>();
    for (int i = 0; i < numCourses; i++) {
        if (inDegree[i] == 0) queue.offer(i);
    }

    int processed = 0;
    while (!queue.isEmpty()) {
        int course = queue.poll();
        processed++;
        for (int next : graph.get(course)) {
            if (--inDegree[next] == 0) queue.offer(next);
        }
    }

    return processed == numCourses;
}
function canFinish(int $numCourses, array $prerequisites): bool
{
    $graph = array_fill(0, $numCourses, []);
    $inDegree = array_fill(0, $numCourses, 0);

    foreach ($prerequisites as [$course, $prereq]) {
        $graph[$prereq][] = $course;
        $inDegree[$course]++;
    }

    $queue = [];
    for ($i = 0; $i < $numCourses; $i++) {
        if ($inDegree[$i] === 0) $queue[] = $i;
    }

    $processed = 0;
    $head = 0;
    while ($head < count($queue)) {
        $course = $queue[$head++];
        $processed++;
        foreach ($graph[$course] as $next) {
            if (--$inDegree[$next] === 0) $queue[] = $next;
        }
    }

    return $processed === $numCourses;
}
func canFinish(numCourses int, prerequisites [][]int) bool {
    graph := make([][]int, numCourses)
    inDegree := make([]int, numCourses)

    for _, p := range prerequisites {
        graph[p[1]] = append(graph[p[1]], p[0])
        inDegree[p[0]]++
    }

    queue := []int{}
    for i := 0; i < numCourses; i++ {
        if inDegree[i] == 0 {
            queue = append(queue, i)
        }
    }

    processed := 0
    for len(queue) > 0 {
        course := queue[0]
        queue = queue[1:]
        processed++
        for _, next := range graph[course] {
            inDegree[next]--
            if inDegree[next] == 0 {
                queue = append(queue, next)
            }
        }
    }

    return processed == numCourses
}

Складність: Час O(V + E), Пам'ять O(V + E).

Типове уточнення: LeetCode #210 — Course Schedule II запитує про конкретний порядок — той самий алгоритм, просто збираємо оброблені курси у список.


Шпаргалка: розпізнавання патернів

Сигнал у задачі Ймовірний патерн
"Знайти пару / трійку з сумою X" Хеш-мапа (невідсортований) або два вказівники (відсортований)
"Суміжний підмасив / підрядок із властивістю X" Sliding window
"Найдовший / найкоротший / кількість X" + підзадачі, що перетинаються Динамічне програмування
"Кількість способів зробити…" DP (часто) або комбінаторика
"Зв'язні компоненти в сітці" DFS/BFS flood fill
"Порядок завдань із залежностями" Топологічне сортування
"Найкоротший шлях, невважений" BFS
"Найкоротший шлях, вважений, невід'ємний" Dijkstra's
"Знайти k-й найбільший / найменший" Купа (черга з пріоритетом)
"Вже відсортований вхід" Два вказівники або бінарний пошук

Довідник складності

Операція Масив Хеш-мапа Купа BST (збалансований)
Доступ за індексом O(1) O(log n)
Пошук O(n) O(1) сер. O(n) O(log n)
Вставка O(n) (у середині) O(1) сер. O(log n) O(log n)
Видалення O(n) (у середині) O(1) сер. O(log n) O(log n)
Мін / макс O(n) O(n) O(1) O(log n)

Мовні підводні камені, які варто знати

Кілька особливостей мов, які ловлять кандидатів частіше, ніж самі алгоритми. Згадай відповідний пункт на співбесіді — і заробиш додатковий плюс.

TypeScript / JavaScript

  • Array.prototype.shift() — це O(n), не O(1). Для BFS на великих вхідних даних використовуй індекс-вказівник (head++) по масиву — або Deque із бібліотеки.
  • arr.sort() сортує лексикографічно ([10, 2, 1] стає [1, 10, 2]). Завжди передавай компаратор: arr.sort((a, b) => a - b).
  • Set.has() — це O(1); array.includes() — O(n). Не плутай їх.
  • Math.max(...arr) кидає RangeError для масивів більше ~100k елементів. Використовуй цикл reduce.
  • Рядки незмінні: s[i] = "x" тихо нічого не робить. Спочатку перетвори на масив.

Python

  • Аргументи зі змінними значеннями за замовчуванням обчислюються один раз: def f(x=[]) ділить той самий список між усіма викликами. Використовуй def f(x=None) і ініціалізуй всередині.
  • list.pop(0) — це O(n). Використовуй collections.deque для черг.
  • Counter — найчистіший спосіб рахувати що завгодно ітерабельне — не вдавайся до словника з ручним інкрементом.
  • dict зберігає порядок вставки з версії 3.7; set — ні.
  • Для числових діапазонів range(n) є лінивим і займає O(1) пам'яті; list(range(n)) матеріалізується і займає O(n).

Java

  • int[] не має методу Arrays.contains(...) як у JS. Використовуй Set<Integer> для швидкого пошуку.
  • Arrays.asList(arr) на int[] дає тобі List<int[]> (один елемент!), не List<Integer>. Використовуй Arrays.stream(arr).boxed().toList().
  • Конкатенація String у циклі — це O(n²) через незмінність. Використовуй StringBuilder на гарячих шляхах.
  • HashMap.computeIfAbsent — найчистіший спосіб побудувати мапи зі списками — без танців із null-перевіркою та put.
  • Автоупаковка має вартість: надавай перевагу int[] перед List<Integer> у щільних циклах.

PHP

  • Оператор === строгий; == виконує нестрогі порівняння типів і є пасткою. Завжди === у коді для співбесіди.
  • Масиви в PHP є одночасно числовими й асоціативними — array_unique працює для обох, але count рахує всі записи.
  • str_split($s) повертає масив рядків по одному символу. Для Unicode використовуй mb_str_split.
  • usort($arr, fn ($a, $b) => $a - $b) — чистий компаратор для сортування; уникай знакового переповнення для великих цілих (використовуй оператор <=>).
  • array_shift і array_unshift — це O(n): вони переіндексовують весь масив. Використовуй індексні вказівники в BFS-подібному коді.

Go

  • len() — це O(1) для зрізів, рядків і мап — викликай вільно в циклах.
  • slice[1:] для видалення з черги — це O(1), але не звільняє базовий масив; для довготривалих процесів — копіюй хвіст.
  • Мапи не мають методу "contains" — ідіоматична форма із двома значеннями value, ok := m[key].
  • Рядки внутрішньо є байтовими зрізами; ітерація з range дає руни (кодові точки Unicode). len(s) повертає байти, не символи.
  • Сортування зрізу: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }). Немає вбудованого int.compare як у Java.

На завершення

Коли йдеться про live coding співбесіди, масиви та рядки — це твоя основа. Хеш-мапи щодня перетворюють рішення грубої сили O(n²) на O(n). Sliding window і два вказівники — це патерни оптимізації, від яких інтерв'юери схвально кивають. Динамічне програмування лякає, доки не усвідомиш, що кожна DP-задача підкоряється одному й тому самому рецепту з п'яти кроків. Графи — це мислення зв'язками, а BFS разом із DFS покривають більшість того, що ти зустрінеш.

Але мета-навичка, що виграє співбесіди, — це комунікація: перефразуй задачу, пройдись по маленькому прикладу, запропонуй грубу силу, оптимізуй, коментуй код по ходу, тестуй крайні випадки. Офер отримують не ті, хто мовчки пише ідеальне рішення, — а ті, хто дає інтерв'юеру побачити хід своїх думок.

Вибери п'ять задач із LeetCode, наведених нижче. Вирішуй кожну двічі — один раз на папері, один раз у своєму IDE. Засікай час. Через тиждень ти почнеш розпізнавати патерни, ще не дочитавши умову задачі.

Удачі — і вперед за оффером. 🚀


Рекомендований список задач LeetCode (за порядком)

  1. Two Sum — #1
  2. Best Time to Buy and Sell Stock — #121
  3. Contains Duplicate — #217
  4. Valid Anagram — #242
  5. Maximum Subarray — #53
  6. Longest Substring Without Repeating Characters — #3
  7. Group Anagrams — #49
  8. Climbing Stairs — #70
  9. House Robber — #198
  10. Number of Islands — #200
  11. Longest Palindromic Substring — #5
  12. Longest Common Subsequence — #1143
  13. Course Schedule — #207
  14. Subarray Sum Equals K — #560
  15. Longest Repeating Character Replacement — #424