Подборка из базы собесов · Сбер, Райффайзенбанк · сложность по возрастанию
У каждой задачи есть блок «Пример использования» — конкретные вызовы и то, что функция должна вернуть, включая граничные случаи. Все выводы в примерах получены прогоном в node, не на глаз. Код копируется кнопкой.
1
Уникальные значения массива без Set
🟢 juniorСбер27.09.2024📄 листинг
алгоритмыjavascript
Написать функцию, возвращающую только уникальные значения массива, без нативного Set.
Пример использования
// Что нужно написать: функцию uniqueValues(array) -> новый массив
// без повторов, порядок = порядок первого появления. Set использовать НЕЛЬЗЯ.
uniqueValues([1, 1, 2, 3, 5, 2, 8, 9, 1, 0]);
// -> [1, 2, 3, 5, 8, 9, 0]
uniqueValues([1, '1', 1, '1']);
// -> [1, '1'] число и строка — РАЗНЫЕ значения, не схлопывать
uniqueValues([0, 0, false, '']);
// -> [0, false, ''] falsy-значения не теряются
uniqueValues(['b', 'a', 'b', 'c', 'a']);
// -> ['b', 'a', 'c'] порядок первого появления
uniqueValues([NaN, NaN, undefined, null, undefined]);
// -> [NaN, undefined, null]
uniqueValues([]);
// -> []
Все выводы получены прогоном в node. Главный кейс — второй: если строить ключ без типа, 1 и '1' склеятся в один и вернётся [1]. Последний кейс с NaN — то, чем добивают: сравнение через indexOf его не ловит.
Задача не про алгоритм, а про то, чем является ключ объекта. Ключи объекта — только строки и символы, любое другое значение приводится к строке. Отсюда весь набор грабель. Сложность O(n) — за один проход, потому что поиск по ключу объекта константный; вложенный indexOf дал бы O(n²).
Разбор по шагам
Идём по массиву один раз, слева направо.
Для каждого значения строим ключ, однозначно кодирующий тип + значение.
Если такого ключа ещё не было — значение уникально, кладём в результат и помечаем ключ.
Порядок результата = порядок первого появления.
А если… (чем добивают)
«А теперь без объекта и без Set» → filter((value, index, array) => array.indexOf(value) === index). Красиво, но O(n²) — на это и смотрят.
«А если в массиве объекты?» → строковый ключ ломается: {a:1} и {a:2} оба дадут [object Object]. Нужен Map/WeakSet со сравнением по ссылке.
«А NaN?» → String(NaN) даёт 'NaN', ключи совпадут, дубль поймается. А вот indexOf провалится: [NaN].indexOf(NaN) === -1. Отличная добивка.
Частые ошибки
if (!seen[key]) вместо in — концептуально неверно (полагается на truthy флага).
Забыть префикс типа → [1, '1'] вернёт один элемент. Прогнал: наивная версия даёт [1].
Обычный {} вместо Object.create(null) — редко, но стреляет на ключах из прототипа.
Сказать «сложность O(n)» для варианта с indexOf внутри filter.
Как объяснить вслух
«Беру словарь встреченных значений и прохожу массив один раз — это O(n). Главная тонкость: ключи объекта всегда строки, поэтому число 1 и строка '1' схлопнулись бы в один ключ. Чтобы этого не было, кладу в ключ ещё и тип.»
Решение
// Без Set: объект-словарь уже встреченных значений.
// Object.create(null) — чистый словарь без прототипа: иначе ключи
// вроде "constructor" или "toString" уже «существуют» и сломают проверку.
function uniqueValues(sourceArray) {
const alreadySeen = Object.create(null);
const result = [];
for (const value of sourceArray) {
// Ключ с префиксом типа: ключи объекта — всегда строки,
// поэтому число 1 и строка '1' дали бы один и тот же ключ "1".
const key = typeof value + ':' + String(value);
// Проверяем через `in`, а не через if (alreadySeen[key]):
// значение-флаг может быть falsy, а нам важен сам факт наличия ключа.
if (!(key in alreadySeen)) {
alreadySeen[key] = true;
result.push(value); // порядок первого появления сохраняется
}
}
return result;
}
2
Есть ли дубли среди аргументов
🟢 juniorСбер30.09.2024📄 листинг
алгоритмыjavascript
Функция возвращает true, если среди аргументов есть дубли. Что выведет каждый вызов?
// Функция УЖЕ написана. Задача: сказать, что выведет каждый вызов, и объяснить почему.
hasDuplicates(1, 2, 3); // -> false
hasDuplicates(1, 2, 2); // -> true
hasDuplicates('a', 'b', 'a'); // -> true
hasDuplicates('1', 1); // -> false строка и число не равны
hasDuplicates(NaN, NaN); // -> true ХОТЯ NaN === NaN это false!
hasDuplicates(0, -0); // -> true ХОТЯ Object.is(0,-0) это false
hasDuplicates(); // -> false пустой вызов
hasDuplicates({}, {}); // -> false разные объекты = разные ссылки
const sameObject = {};
hasDuplicates(sameObject, sameObject); // -> true одна и та же ссылка
Два вывода ломают интуицию: NaN, NaN → true и 0, -0 → true. Внутри Set работает SameValueZero — он не равен ни ===, ни Object.is. Это и есть суть задачи.
Вся задача — про алгоритм сравнения внутри Set. Он называется SameValueZero и отличается от === ровно в одном месте: NaN равен самому себе. И отличается от Object.is тоже ровно в одном: 0 и -0 считаются равными.
Разбор по шагам
new Set(args) выкидывает повторы по SameValueZero.
Если размер Set меньше числа аргументов — значит что-то схлопнулось, то есть дубль был.
('1', 1) → строка и число остаются разными элементами, size = 2 = длина → false.
А если… (чем добивают)
«Хочу, чтобы '1' и 1 считались дублями» → нормализовать перед Set: args.map(String). Но тогда true и 'true' тоже станут дублями — это уже продуктовое решение, стоит сказать вслух.
«А если аргументы — объекты с одинаковыми полями?» → Set бессилен, нужно сравнение по содержимому: ключ через JSON.stringify (с поправкой на порядок ключей) или ручной deep-equal.
«Сложность?» → O(n), один проход + константная вставка в хеш. Наивное двойное for — O(n²).
Частые ошибки
Ответить true на ('1', 1) — самая частая ошибка, кажется, что Set приводит типы.
Уверенно сказать, что Set сравнивает через ===: тогда NaN, NaN «должны» дать false, а реально true.
Сказать, что Object.is — то же самое, что внутри Set. Разница в -0.
Ждать, что ({},{}) даст true.
Как объяснить вслух
«Set убирает повторы, поэтому сравниваю его размер с числом аргументов. Важно, что Set не приводит типы — строка '1' и число 1 остаются разными, поэтому последний вызов false. И ещё Set сравнивает не через строгое равенство, а через SameValueZero: NaN там равен самому себе, в отличие от ===.»
Решение
// Решение в условии уже верное — ценность задачи в разборе поведения Set.
// Проверка фактов прогоном:
new Set([1, 2, 3]).size; // 3 === 3 аргумента -> дублей нет -> false
new Set([1, 2, 2]).size; // 2 !== 3 -> true
new Set(['a','b','a']).size; // 2 !== 3 -> true
new Set(['1', 1]).size; // 2 === 2 -> false: Set НЕ приводит типы
// Set сравнивает по SameValueZero — это НЕ ===:
NaN === NaN; // false
new Set([NaN, NaN]).size; // 1 <- дубль пойман, хотя === говорит "разные"
new Set([0, -0]).size; // 1 <- считает одинаковыми
Object.is(0, -0); // false <- а Object.is различает
// Объекты сравниваются по ссылке, а не по содержимому:
new Set([{}, {}]).size; // 2 -> дубль НЕ найден
const shared = {};
new Set([shared, shared]).size; // 1
3
Вычесть N рабочих дней из даты
🟡 middleСбер23.12.2024📄 листинг
алгоритмыдаты
Написать функцию, которая вычитает N рабочих дней из даты, пропуская субботы и воскресенья.
Пример использования
// Что нужно написать: subtractBusinessDays(date, n) -> НОВЫЙ Date,
// отступив назад на n РАБОЧИХ дней (сб и вс не считаются).
// Входную дату мутировать нельзя.
const wednesday = new Date(2026, 8, 30); // ср 30.09.2026
subtractBusinessDays(wednesday, 3);
// -> пт 25.09.2026 (вт 29, пн 28, пт 25 — выходные пропущены)
subtractBusinessDays(new Date(2026, 8, 28), 1); // пн 28.09
// -> пт 25.09.2026 перепрыгнули через выходные
subtractBusinessDays(new Date(2026, 9, 3), 1); // сб 03.10
// -> пт 02.10.2026 старт в выходной
subtractBusinessDays(wednesday, 0);
// -> ср 30.09.2026 ноль = та же дата
subtractBusinessDays(wednesday, 10);
// -> ср 16.09.2026 ровно две недели назад
console.log(wednesday); // ср 30.09.2026 — исходная дата НЕ изменилась
Проверено прогоном, включая немутабельность: Date изменяемый, и setDate на аргументе испортил бы дату у вызывающего кода. Кейс «10 рабочих дней = ровно 2 недели» удобен для самопроверки на созвоне.
✅ Проверено прогоном: ср 30.09 − 3 → пт 25.09 (сошлось с условием). Плюс: старт в сб/вс, n=0, немутабельность входа.
✅ Ответ и разбор
Почему так
Ключевая мысль: нельзя вычесть сразу n дней и потом «поправить» на выходные — поправка сама может попасть на выходной. Нужно шагать по одному дню назад и уменьшать счётчик только когда попали на будний день. Второй важный момент — Date мутабельный, поэтому работаем с копией.
Разбор по шагам
Копируем дату: new Date(startDate.getTime()).
Пока остаток рабочих дней больше нуля — сдвигаем дату на день назад.
«Добавь праздники» → передавать Set строк 'YYYY-MM-DD' и проверять вместе с выходными.
«А если n огромное, 10000?» → цикл станет медленным. Оптимизация: целых недель Math.floor(n / 5) = столько же недель назад, остаток дошагать вручную. Хороший ответ на «ускорь».
«А время?» → setDate сохраняет часы/минуты. Если нужна чистая дата — обнулить через setHours(0,0,0,0).
«Переход на летнее время?» → setDate безопаснее, чем вычитание 24*60*60*1000 мс: второе ломается на переводе часов.
Частые ошибки
Мутировать входной Date — самая частая. Функция «работает», но портит дату у вызывающего кода.
Вычесть n календарных дней и один раз откатиться с выходных — неверно при n больше 5.
Вычитать миллисекунды date - n*86400000 — ломается на переводе часов.
Перепутать индексы дней: 0 это воскресенье, а не понедельник.
Уменьшать счётчик до проверки на выходной.
Как объяснить вслух
«Иду по одному дню назад от копии исходной даты. Счётчик уменьшаю только если день будний — так выходные проскакиваются сами, и не нужно ничего досчитывать отдельно. Копию делаю потому, что Date мутабельный и setDate изменил бы дату вызывающего кода.»
Решение
// Возвращаем НОВЫЙ Date: исходный мутировать нельзя,
// иначе функция незаметно испортит дату у вызывающего кода.
function subtractBusinessDays(startDate, businessDaysToSubtract) {
const result = new Date(startDate.getTime());
let remainingBusinessDays = businessDaysToSubtract;
// Шагаем ровно по одному дню назад.
// Нельзя вычесть сразу n дней и «поправиться» на выходные:
// поправка сама может попасть на субботу или воскресенье.
while (remainingBusinessDays > 0) {
result.setDate(result.getDate() - 1);
// getDay(): 0 — воскресенье, 6 — суббота.
const dayOfWeek = result.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
// Счётчик уменьшаем ТОЛЬКО за будни —
// так выходные проскакиваются бесплатно.
if (!isWeekend) {
remainingBusinessDays--;
}
}
return result;
}
4
PaginationHelper — класс постраничного вывода
🟡 middleРайффайзенбанк17.08.2025📄 листинг
алгоритмыооп
Реализовать класс для постраничного вывода коллекции. Индексация страниц и элементов — с нуля; если запрошенной страницы или элемента не существует, метод возвращает -1.
Пример использования
// Что нужно написать: класс с 4 методами. Индексация с НУЛЯ.
// Нет такой страницы / элемента -> вернуть -1 (не 0, не undefined, не бросать).
const helper = new PaginationHelper(Array.from({ length: 24 }, (_, i) => i), 10);
helper.itemCount(); // -> 24 всего элементов
helper.pageCount(); // -> 3 страниц: 10 + 10 + 4
helper.pageItemCount(0); // -> 10 полная страница
helper.pageItemCount(2); // -> 4 последняя, неполная
helper.pageItemCount(3); // -> -1 такой страницы нет
helper.pageIndex(23); // -> 2 последний элемент на 3-й странице
helper.pageIndex(0); // -> 0
helper.pageIndex(24); // -> -1 элемента нет
helper.pageIndex(-1); // -> -1
// ГРАНИЧНЫЙ СЛУЧАЙ, на котором валится большинство:
const full = new PaginationHelper(Array.from({ length: 20 }, (_, i) => i), 10);
full.pageCount(); // -> 2
full.pageItemCount(1); // -> 10, а НЕ 0 (остаток от деления = 0, но страница полная)
// Пустая коллекция:
const empty = new PaginationHelper([], 10);
empty.pageCount(); // -> 0
empty.pageItemCount(0); // -> -1
Главная ловушка — 20 элементов по 10. Наивное itemCount % itemsPerPage для последней страницы даёт 0, хотя страница полная. Этот кейс в условии не написан, но спрашивают его почти всегда.
✅ Проверено прогоном: все эталоны из условия сошлись. Сверх того: 20 по 10 → pageItemCount(1) = 10, пустая коллекция → pageCount 0, pageItemCount(0) = -1.
✅ Ответ и разбор
Почему так
Задача не про алгоритм, а про аккуратность с границами. Вся суть в трёх формулах: количество страниц — Math.ceil, страница элемента — Math.floor, размер последней страницы — остаток от деления с обработкой нуля. Плюс единый контракт «нет данных → -1».
Разбор по шагам
pageCount = Math.ceil(длина / размерСтраницы) — неполный хвост тоже занимает страницу.
pageIndex(i) = Math.floor(i / размерСтраницы), но сначала проверка, что элемент существует.
pageItemCount(p): если страница не последняя — полный размер; если последняя — остаток, а при остатке 0 — полный размер.
Любой выход за границы — -1, единообразно.
А если… (чем добивают)
«20 элементов по 10 — сколько на последней?» → главный добивающий вопрос. Ответ 10, а наивная формула даёт 0.
«Пустая коллекция?» → pageCount = 0, любой pageItemCount = -1.
«itemsPerPage = 0 или отрицательный?» → деление на ноль даст Infinity/NaN. Стоит проговорить валидацию в конструкторе.
Вернуть 0 вместо 10 для полной последней страницы — ошибка номер один.
Забыть проверку границ и вернуть undefined/NaN вместо -1.
Пропустить отрицательные индексы: pageIndex(-1) без проверки даст -1 «случайно», а pageIndex(-15) уже -2.
Использовать Math.round вместо ceil/floor.
Считать pageCount заново в каждом методе, когда коллекция может меняться, — или наоборот, закешировать и разойтись с реальностью.
Как объяснить вслух
«Страниц — потолок от деления, потому что неполный хвост тоже страница. Для размера страницы проверяю, последняя ли она: если нет, это полный размер; если да — остаток от деления, но с оговоркой, что остаток ноль означает полную страницу, а не пустую. Все выходы за границы возвращают минус один, чтобы контракт был единым.»
Решение
class PaginationHelper {
constructor(collection, itemsPerPage) {
this.collection = collection;
this.itemsPerPage = itemsPerPage;
}
itemCount() {
return this.collection.length;
}
pageCount() {
// Именно ceil: 24 элемента по 10 — это 3 страницы,
// последняя неполная, но она всё равно существует.
return Math.ceil(this.collection.length / this.itemsPerPage);
}
pageItemCount(pageIndex) {
// Единый контракт: любой выход за границы — -1.
if (pageIndex < 0 || pageIndex >= this.pageCount()) {
return -1;
}
const isLastPage = pageIndex === this.pageCount() - 1;
if (!isLastPage) {
return this.itemsPerPage; // все страницы кроме последней всегда полные
}
// САМОЕ ВАЖНОЕ МЕСТО.
// Остаток 0 означает, что последняя страница ПОЛНАЯ (20 по 10 -> 10),
// а не пустая. Без этой проверки метод вернёт 0 — классический баг.
const remainder = this.itemCount() % this.itemsPerPage;
return remainder === 0 ? this.itemsPerPage : remainder;
}
pageIndex(itemIndex) {
// Проверяем существование элемента ДО вычислений:
// иначе floor молча вернёт правдоподобное число для несуществующего индекса.
if (itemIndex < 0 || itemIndex >= this.itemCount()) {
return -1;
}
return Math.floor(itemIndex / this.itemsPerPage);
}
}
5
fetchDataWithRetry — запрос с повторами
🟡 middleСбер14.05.2025📄 листинг
промисыasyncfetch
Написать функцию, которая асинхронно ходит GET-запросом по url и возвращает JSON. Если произошла ошибка — повторить до 5 раз. Если так и не удалось — вернуть ошибку «Заданный url недоступен».
Пример использования
// Что нужно написать: fetchDataWithRetry(url, maxRetries = 5)
// -> Promise с распарсенным JSON.
// Ошибка (сеть ИЛИ статус не 2xx) -> повторить, всего до 5 попыток.
// Все 5 неудачны -> бросить Error('Заданный url недоступен').
const data = await fetchDataWithRetry('https://api.example.com/data');
// успех с первого раза -> {...}, всего запросов: 1
// два раза 500, потом 200 -> {...}, всего запросов: 3
// 404, потом 200 -> {...}, всего запросов: 2 (404 — ТОЖЕ повод для retry)
// сеть упала, потом 200 -> {...}, всего запросов: 2
try {
await fetchDataWithRetry('https://api.example.com/down');
} catch (error) {
console.log(error.message); // -> 'Заданный url недоступен'
// всего запросов: ровно 5, не 6
}
Ключевой момент, который валит решение целиком: fetchне реджектится на 404 и 500 — он спокойно резолвится с ok: false. Без ручной проверки response.ok retry не сработает ни разу. Счётчики запросов сняты прогоном с замоканным fetch.
✅ Проверено прогоном (fetch замокан, считались вызовы): 2 падения → 3 запроса и успех · вечное падение → ровно 5 запросов и «Заданный url недоступен» · 404 → retry сработал.
✅ Ответ и разбор
Почему так
Главное знание задачи: fetch реджектится только при сетевой ошибке — нет соединения, DNS, CORS. HTTP-статусы 404 и 500 для него — нормально завершённый запрос, промис резолвится с ok: false. Поэтому try/catch вокруг fetch без проверки response.ok не поймает серверные ошибки, и retry будет мёртвым кодом.
Разбор по шагам
Цикл попыток от 1 до maxRetries.
await fetch(url) — ловим сетевые падения через catch.
Проверяем response.ok вручную и бросаем ошибку сами, если статус не 2xx.
Успех — сразу возвращаем await response.json() и выходим из цикла.
Если это была последняя попытка — бросаем ошибку с нужным текстом.
А если… (чем добивают)
«Добавь задержку между попытками» → экспоненциальный backoff: await new Promise(r => setTimeout(r, 2 ** attempt * 100)). Ждут именно этого слова.
«А 404 стоит повторять?» → по-хорошему нет: это не временная ошибка, повтор бесполезен. Ретраить 5xx и сетевые, а 4xx отдавать сразу. Скажешь это — большой плюс.
«Как отменить запрос?» → AbortController + таймаут на каждую попытку.
«А если тело не JSON?» → response.json() бросит, и это уйдёт в тот же catch — то есть будет ретрай на битом ответе. Стоит проговорить, нужно ли так.
Частые ошибки
Не проверить response.ok — фатально: на 500 функция вернёт мусор вместо повтора.
Сделать 6 запросов вместо 5 (ошибка на границе цикла).
Рекурсия без базового случая или с потерей счётчика.
Проглотить исходную ошибку и не отдать ничего — вызывающий код не поймёт, что случилось.
Ретраить мгновенно, без паузы — на упавшем сервере это DDoS самому себе.
Как объяснить вслух
«Оборачиваю fetch в цикл попыток. Важная тонкость: fetch не реджектится на 404 и 500 — промис успешно резолвится с ok: false. Поэтому я явно проверяю response.ok и бросаю ошибку сам, иначе повторы просто не сработают. После последней неудачной попытки бросаю ошибку с текстом из условия. На проде добавил бы экспоненциальную задержку и не ретраил 4xx.»
Решение
async function fetchDataWithRetry(url, maxRetries = 5) {
// Цикл, а не рекурсия: проще контролировать точное число попыток.
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
// КЛЮЧЕВАЯ СТРОКА.
// fetch реджектится ТОЛЬКО на сетевых ошибках.
// На 404 и 500 он спокойно резолвится с ok: false,
// поэтому статус проверяем руками и бросаем ошибку сами —
// иначе повторы никогда не запустятся.
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// Успех — сразу выходим, оставшиеся попытки не тратим.
return await response.json();
} catch (error) {
// Ошибка на ПОСЛЕДНЕЙ попытке — дальше пробовать нечего.
if (attempt === maxRetries) {
throw new Error('Заданный url недоступен');
}
// На проде здесь была бы пауза с экспоненциальным ростом,
// чтобы не добивать уже упавший сервер:
// await new Promise(resolve => setTimeout(resolve, 2 ** attempt * 100));
}
}
}
Порядок на созвоне — как пронумеровано: 1 и 2 разогрев, 3 и 4 основная часть, 5 финал с разговором про надёжность.