Отже, ти вже побував на кількох технічних співбесідах і зрозумів одне: всі люблять запитувати про масиви та рядки. Якщо твій досвід схожий на мій, ти, напевно, помітив, що приблизно 7 із 10 співбесід зосереджені на масивах. Решта 3? Теж масиви, але, може, ще якась задача на рядки або щось екзотичніше — графи чи динамічне програмування. Задачі на бази даних? Рідкість.
Давайте розберемо найпоширеніші алгоритми, потрібні для успішного проходження цих співбесід — обіцяю, все буде просто і без зайвої нудьги. До кожної задачі додано точний хід думок, який варто озвучувати під час live coding, готовий код на TypeScript, Python, Java, PHP та Go, а також пряме посилання на LeetCode, щоб ти міг практикуватись одразу після прочитання.
Перед початком: правильне мислення під час live coding
Перш ніж переходити до алгоритмів, засвой одне: інтерв'юери оцінюють те, як ти думаєш, а не те, чи пам'ятаєш відповідь. Шаблон, що спрацьовує майже в кожному live coding раунді, виглядає так:
- Перефразуй задачу своїми словами та уточни обмеження (розмір вхідних даних, крайні випадки, чи може масив бути порожнім, чи є дублікати, чи відсортований, тощо).
- Пройдись по маленькому прикладу вручну — на папері або набравши у редакторі.
- Спочатку запропонуй рішення грубої сили, назви його складність, потім запитай: "Варто оптимізувати, чи це прийнятно?"
- Оптимізуй, розпізнавши патерн (хеш-мапа, два вказівники, sliding window, DP).
- Пиши код уважно, коментуючи дії по ходу.
- Тестуй на крайніх випадках: порожній вхід, один елемент, самі дублікати, від'ємні числа, максимальний розмір.
Офер отримують не ті, хто мовчки пише ідеальне рішення, — а ті, хто дає інтерв'юеру побачити хід своїх думок. А тепер перейдемо до алгоритмів.
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 лякає людей, але це просто рекурсія + кешування. Рецепт завжди однаковий:
- Визнач стан — що означає
dp[i](абоdp[i][j])? - Запиши рекурентне співвідношення — як
dp[i]залежить від менших підзадач? - Визнач базовий випадок.
- Обери напрямок — зверху вниз (мемоізація) або знизу вгору (табуляція).
- Оптимізуй пам'ять, якщо потрібні лише кілька останніх станів.
Задача 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 (за порядком)
- Two Sum — #1
- Best Time to Buy and Sell Stock — #121
- Contains Duplicate — #217
- Valid Anagram — #242
- Maximum Subarray — #53
- Longest Substring Without Repeating Characters — #3
- Group Anagrams — #49
- Climbing Stairs — #70
- House Robber — #198
- Number of Islands — #200
- Longest Palindromic Substring — #5
- Longest Common Subsequence — #1143
- Course Schedule — #207
- Subarray Sum Equals K — #560
- Longest Repeating Character Replacement — #424






