Решение
Порядок вывода
1
7
3
5
2
4
6
Объяснение
Event Loop в Node.js работает в несколько фаз, и критически важно различать:
Порядок выполнения в каждом цикле:
Пошаговый разбор кода
Этап 1: Синхронный код (Call Stack)
Решение: Array.prototype.myReduce
Полная реализация
Решение
Базовая реализация
function promiseAll<T>(promises: Promise<T>[]): Promise<T[]> {
return new Promise((resolve, reject) => {
// Если массив пуст, резолвим пустой массив
if (promises.length === 0) {
resolve([]);
return;
}
const results: T[] = [];
let completedCount = 0;
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then((value) => {
results[index] = value;
completedCount++;
// Если все промисы выполнены, резолвим результат
if (completedCount === promises.length) {
resolve(results);
}
})
.catch((error) => {
// При первой ошибке сразу реджектим весь результат
reject(error);
});
});
});
}
Как это работает
Ключевые моменты:
Решение
Числа Фибоначчи: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55... Каждое число = сумма двух предыдущих. F(n) = F(n-1) + F(n-2)
Подход 1: Простая рекурсия (неоптимальная)
function fibonacci(n: number): number {
if (n < 0) throw new Error("n must be non-negative");
if (n === 0) return 0;
if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Примеры:
console.log(fibonacci(0)); // 0
console.log(fibonacci(1)); // 1
console.log(fibonacci(10)); // 55
console.log(fibonacci(20)); // 6765
Проблема: Экспоненциальная сложность O(2^n)
fibonacci(5) вызывает:
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) ...
/ \
...
Много дублирующихся вычислений! fib(3) вычисляется 2 раза, fib(2) - 3 раза
Подход 2: Мемоизация (O(n) время)
Решение: Архитектура сервиса записи к врачу с напоминаниями
1. Архитектура системы
┌─────────────┐
│ API │ (Express/Fastify)
│ Endpoints │
└──────┬──────┘
│
├──► AppointmentService (бизнес-логика)
│
├──► ReminderQueue (Bull/Redis)
│
├──► Database (PostgreSQL)
│
└──► NotificationService (SMS/Push/Email)
2. Структура базы данных
-- Таблица пациентов
CREATE TABLE patients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
phone VARCHAR(20) NOT NULL UNIQUE,
email VARCHAR(255),
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Решение
Полная реализация
Array.prototype.myFilter = function<T>(callback: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T[] {
const result: T[] = [];
for (let i = 0; i < this.length; i++) {
// Проверяем, что элемент существует в массиве (важно для разреженных массивов)
if (i in this) {
// Вызываем callback с контекстом thisArg, если передан
const shouldInclude = callback.call(thisArg, this[i], i, this);
// Если callback вернул truthy значение, добавляем элемент в результат
if (shouldInclude) {
result.push(this[i]);
}
}
}
return result;
};
Ключевые моменты реализации
1. Итерация через элементы
for цикл для перебора всех индексовi in this необходима для корректной работы с разреженными массивами (sparse arrays), где есть пропускиРешение: Бинарный поиск
Итеративная реализация
function binarySearch(arr: number[], target: number): number {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = left + Math.floor((right - left) / 2);
const midValue = arr[mid];
if (midValue === target) {
return mid;
} else if (midValue < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
const arr = [1, 3, 5, 7, 9, 11, 13, 15];
console.log(binarySearch(arr, 7)); // 3
console.log(binarySearch(arr, 1)); // 0
console.log(binarySearch(arr, 15)); // 7
console.log(binarySearch(arr, 6)); // -1
Рекурсивная реализация
function binarySearchRecursive(arr: number[], target: number): number {
return helper(arr, target, 0, arr.length - 1);
}
Решение
Базовая реализация
import { Request, Response, NextFunction } from 'express';
function requestLogger() {
return (req: Request, res: Response, next: NextFunction) => {
const startTime = Date.now();
// Отслеживаем момент отправки ответа
res.on('finish', () => {
const duration = Date.now() - startTime;
console.log(`${req.method} ${req.url} - ${duration}ms`);
});
next();
};
}
Как это работает
Принцип работы middleware:
startTime = Date.now()duration = Date.now() - startTimenext() вызывает следующий middlewareПорядок выполнения:
Решение: Сумма чисел в диапазоне
1. Наивное решение O(n)
function sumRangeNaive(start: number, end: number): number {
let sum = 0;
const min = Math.min(start, end);
const max = Math.max(start, end);
for (let i = min; i <= max; i++) {
sum += i;
}
return sum;
}
console.log(sumRangeNaive(1, 5)); // 15
console.log(sumRangeNaive(5, 1)); // 15
console.log(sumRangeNaive(-3, 3)); // 0
Минусы: На диапазоне 1-1,000,000 требует 1M итераций
2. Оптимальное решение O(1) - Формула арифметической прогрессии
function sumRange(start: number, end: number): number {
// Гарантируем, что start <= end
const min = Math.min(start, end);
const max = Math.max(start, end);
// Формула: sum = n * (first + last) / 2
// где n = количество элементов
const count = max - min + 1;
const sum = (count * (min + max)) / 2;
return sum;
}
Решение
Базовая реализация
Array.prototype.myMap = function<T, U>(
callback: (value: T, index: number, array: T[]) => U
): U[] {
const result: U[] = [];
for (let i = 0; i < this.length; i++) {
// Проверяем, что индекс существует (для sparse arrays)
if (i in this) {
result[i] = callback(this[i], i, this);
}
}
return result;
};
Версия с поддержкой thisArg
Array.prototype.myMap = function<T, U>(
callback: (this: any, value: T, index: number, array: T[]) => U,
thisArg?: any
): U[] {
const result: U[] = [];
for (let i = 0; i < this.length; i++) {
if (i in this) {
// Вызываем callback с заданным контекстом this
result[i] = callback.call(thisArg, this[i], i, this);
}
}
return result;
};
Как это работает
Ключевые моменты:
Решение: Deep Clone объекта
Базовая реализация
function deepClone<T>(obj: T): T {
// Примитивные типы и null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Date
if (obj instanceof Date) {
return new Date(obj.getTime()) as unknown as T;
}
// RegExp
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags) as unknown as T;
}
// Массивы
if (Array.isArray(obj)) {
const clonedArray: any[] = [];
for (let i = 0; i < obj.length; i++) {
clonedArray[i] = deepClone(obj[i]);
}
return clonedArray as T;
}
// Объекты
if (obj instanceof Object) {
const clonedObject: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObject[key] = deepClone((obj as any)[key]);
}
}
return clonedObject as T;
}
return obj;
}
С обработкой циклических ссылок (WeakMap)
Решение
Загрузка 4 ГБ файла требует архитектурного подхода. Нельзя загружать всё в память.
Почему не в память?
Node.js может использовать максимум ~1.4 ГБ памяти (V8 heap). Загрузка 4 ГБ файла приведёт к OutOfMemory error и падению процесса. Весь сервер зависнет.
Правильный подход: Streams + S3 Multipart Upload
1. Используем AWS SDK с lib-storage
import { Upload } from '@aws-sdk/lib-storage';
import { S3Client } from '@aws-sdk/client-s3';
const s3Client = new S3Client({ region: 'us-east-1' });
app.post('/upload', async (req, res) => {
const bucketName = process.env.AWS_BUCKET;
const key = `uploads/${Date.now()}`;
const totalSize = parseInt(req.headers['content-length'] || '0', 10);
try {
const upload = new Upload({
client: s3Client,
params: {
Bucket: bucketName,
Key: key,
Body: req, // Поток напрямую
ContentType: req.headers['content-type'],
},
partSize: 5 * 1024 * 1024,
});
Решение
Базовая реализация
function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
let timerId: NodeJS.Timeout | null = null;
return function (...args: Parameters<T>) {
// Отменяем предыдущий таймер
if (timerId !== null) {
clearTimeout(timerId);
}
// Устанавливаем новый таймер
timerId = setTimeout(() => {
fn(...args);
timerId = null;
}, delay);
};
}
Как это работает
Ключевой механизм — замыкание (Closure):
timerId сохраняется в замыкании возвращаемой функцииtimerIdПорядок выполнения:
debouncedLog(1) → timerId = setTimeout(..., 300)debouncedLog(2) → clearTimeout(timerId) → новый таймер на 300мсdebouncedLog(3) → clearTimeout(timerId) → новый таймер на 300мсfn(3) → вывод 3Решение
Базовая реализация
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Как это работает
Принцип работы:
Пошаговый процесс:
Старт: sleep(2000)
├─ Создан новый Promise
├─ setTimeout установлен на 2000мс
├─ Возвращаем промис
└─ При await управление передаётся дальше
Через 2 сек:
├─ setTimeout срабатывает
├─ resolve() выполняется
├─ Промис резолвится
└─ await разблокируется, код продолжается
Пример использования
async function demo() {
console.log("Start");
await sleep(2000);
console.log("After 2 seconds");
await sleep(1000);
console.log("After 1 more second");
}
demo();
Решение
Вариант 1: Использование let (Рекомендуется)
const arr = [10, 20, 30, 40, 50];
for (let i = 0; i < arr.length; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
// Вывод:
// 0 (через 1 сек)
// 1 (через 2 сек)
// 2 (через 3 сек)
// 3 (через 4 сек)
// 4 (через 5 сек)
Почему это работает:
let создаёт новое замыкание для каждой итерацииВариант 2: Использование IIFE (Старый подход с var)
const arr = [10, 20, 30, 40, 50];
for (var i = 0; i < arr.length; i++) {
(function(index) {
setTimeout(() => {
console.log(index);
}, index * 1000);
})(i);
}
Решение: Удаление дубликатов из массива
1. С помощью Set (оптимально)
function removeDuplicatesSet<T>(arr: T[]): T[] {
return Array.from(new Set(arr));
// или
// return [...new Set(arr)];
}
const arr = [1, 2, 2, 3, 4, 4, 5, 1];
console.log(removeDuplicatesSet(arr)); // [1, 2, 3, 4, 5]
Плюсы: O(n) время, O(n) память, сохраняет порядок, очень быстро
Минусы: Сравнивает по ===, проблема с объектами и NaN
2. С помощью filter и indexOf
function removeDuplicatesFilter<T>(arr: T[]): T[] {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
const arr = [1, 2, 2, 3, 4, 4, 5, 1];
console.log(removeDuplicatesFilter(arr)); // [1, 2, 3, 4, 5]
Плюсы: Простой, читаемый код, работает с любыми типами
Минусы: O(n²) время, медленнее на больших массивах
3. С помощью reduce
Решение
Факториал n! = 1 × 2 × 3 × ... × n, где 0! = 1 по определению. Разберу несколько подходов.
Подход 1: Рекурсивный (классический)
function factorialRecursive(n: number): number {
// Граничные случаи
if (n < 0) throw new Error("Factorial undefined for negative numbers");
if (n === 0 || n === 1) return 1;
// Рекурсивный случай: n! = n × (n-1)!
return n * factorialRecursive(n - 1);
}
// Примеры:
console.log(factorialRecursive(5)); // 120
console.log(factorialRecursive(0)); // 1
console.log(factorialRecursive(10)); // 3628800
Как работает:
factorialRecursive(5)
= 5 * factorialRecursive(4)
= 5 * (4 * factorialRecursive(3))
= 5 * (4 * (3 * factorialRecursive(2)))
= 5 * (4 * (3 * (2 * factorialRecursive(1))))
= 5 * (4 * (3 * (2 * 1)))
= 5 * (4 * (3 * 2))
= 5 * (4 * 6)
= 5 * 24
= 120
Решение
Flattening — преобразование вложенного массива в одномерный. Это классическая задача на рекурсию, которая хорошо показывает понимание рекурсивных алгоритмов.
Подход 1: Классическая рекурсия
function flatten(arr: any[]): any[] {
const result: any[] = [];
for (const item of arr) {
if (Array.isArray(item)) {
// Рекурсивно вызываем flatten и добавляем элементы
result.push(...flatten(item));
} else {
// Если не массив, добавляем элемент напрямую
result.push(item);
}
}
return result;
}
// Примеры:
console.log(flatten([1, [2, [3, [4]], 5]]));
// [1, 2, 3, 4, 5]
console.log(flatten([[1, 2], [3, [4, [5, [6]]]]]]));
// [1, 2, 3, 4, 5, 6]
Как работает:
Решение
Палиндром — это строка, которая читается одинаково в обе стороны. Решу эту задачу пошагово, рассмотрев несколько подходов.
Основной подход с регулярными выражениями
function isPalindrome(str: string): boolean {
// Очищаем строку: удаляем всё кроме букв и цифр, переводим в нижний регистр
const cleaned = str.replace(/[^a-z0-9]/gi, "").toLowerCase();
// Проверяем, равна ли строка её зеркальному отражению
return cleaned === cleaned.split("").reverse().join("");
}
Объяснение:
replace(/[^a-z0-9]/gi, "") — регулярное выражение удаляет всё кроме букв (a-z) и цифр (0-9), флаг i для игнорирования регистра, g для глобального поиска.toLowerCase() — приводим к нижнему регистру.split("") — разбиваем на массив символов.reverse() — разворачиваем массив.join("") — объединяем обратно в строкуОптимизированный подход с двумя указателями
Решение
Базовая реализация
function curry<T extends (...args: any[]) => any>(
fn: T
): (...args: any[]) => any {
const arity = fn.length; // Количество параметров функции
return function curried(...args: any[]): any {
// Если собрали достаточно аргументов, вызываем функцию
if (args.length >= arity) {
return fn(...args);
}
// Иначе возвращаем функцию, которая соберёт оставшиеся аргументы
return (...nextArgs: any[]) => curried(...args, ...nextArgs);
};
}
Как это работает
Принцип каррирования:
Пошаговый процесс для curriedSum(1)(2)(3):
Шаг 1: curriedSum(1)
- args = [1]
- arity = 3 (функция sum ожидает 3 параметра)
- 1 < 3, возвращаем новую функцию
Решение
Порядок вывода
setTimeout2 // 100 мс
setTimeout1 // 1000 мс
setTimeout4 // 1000 мс
Promise1 // 1000 мс (срабатывает после setTimeout1/4)
Promise3 // 1000 мс (срабатывает после setTimeout1/4)
setTimeout3 // 2000 мс
Promise2 // 2000 мс (срабатывает после setTimeout3)
Объяснение по временной шкале
t = 0 мс: Регистрация всех таймеров
const myPromise = (delay) => new Promise((res) => setTimeout(res, delay));
Решение
Базовая реализация
function flattenObject(
obj: Record<string, any>,
prefix: string = ""
): Record<string, any> {
const result: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
const newKey = prefix ? `${prefix}.${key}` : key;
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
// Рекурсивно обрабатываем вложенные объекты
Object.assign(result, flattenObject(value, newKey));
} else {
// Добавляем примитивные значения
result[newKey] = value;
}
}
}
return result;
}
Как это работает
Принцип работы:
Решение
Базовая реализация
function throttle<T extends (...args: any[]) => any>(
fn: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean = false;
return function (...args: Parameters<T>) {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
Как это работает
Принцип работы:
inThrottle блокирует повторные вызовы на время limitlimit миллисекунд флаг сбрасываетсяВызовы функции при частых обращениях:
Время: 0ms 100ms 200ms 300ms 400ms 500ms 600ms 700ms 800ms 900ms 1000ms
Вызовы: X X X X X - - - - - X
Выпол: ✓ ✗ ✗ ✗ ✗ - - - - - ✓
Решение
Группировка элементов — одна из самых частых операций в обработке данных. Нужно создать объект, где ключ — город, значение — массив людей из этого города. Временная сложность O(n) означает один проход по массиву.
Подход 1: Простой цикл (самый понятный)
interface Person {
name: string;
city: string;
}
type GroupedByCity = Record<string, Person[]>;
function groupByCity(people: Person[]): GroupedByCity {
const result: GroupedByCity = {};
for (const person of people) {
// Если города нет в результате, создаём пустой массив
if (!result[person.city]) {
result[person.city] = [];
}
// Добавляем человека к его городу
result[person.city].push(person);
}
return result;
}
Анализ:
Решение
Эта задача требует создания вложенной структуры объектов, где каждая точка в строке представляет уровень вложенности. Разберу несколько подходов.
Подход 1: С использованием reduce (элегантный)
function stringToNestedObject(str: string, value: any): Record<string, any> {
// Разбиваем строку по точкам
const keys = str.split(".");
// Идём с конца и создаём объекты
return keys.reduceRight((acc, key) => {
return { [key]: acc };
}, value);
}
// Примеры:
console.log(stringToNestedObject("a.b.c", 42));
// { a: { b: { c: 42 } } }
console.log(stringToNestedObject("user.name.first", "John"));
// { user: { name: { first: "John" } } }
Как работает:
reduceRight идёт справа налево (от конца)value (42)Решение
FizzBuzz — классическая задача для проверки базовой логики программирования. Разберу несколько подходов от простого к более элегантному.
Базовый подход с if-else
function fizzBuzz(n: number): void {
for (let i = 1; i <= n; i++) {
if (i % 15 === 0) {
console.log("FizzBuzz");
} else if (i % 3 === 0) {
console.log("Fizz");
} else if (i % 5 === 0) {
console.log("Buzz");
} else {
console.log(i);
}
}
}
Ключевой момент: проверяем сначала на 15 (делимое и на 3, и на 5), потом на 3, затем на 5. Порядок важен!
Более элегантный подход с конструированием строки
function fizzBuzz(n: number): void {
for (let i = 1; i <= n; i++) {
let output = "";
if (i % 3 === 0) output += "Fizz";
if (i % 5 === 0) output += "Buzz";
console.log(output || i);
}
}
Решение
Простое число — это натуральное число больше 1, которое делится только на 1 и на себя. Разберу несколько подходов от базового к оптимизированному.
Подход 1: Базовый (O(n))
function isPrime(n: number): boolean {
// Граничные случаи
if (n <= 1) return false;
if (n === 2) return true;
if (n % 2 === 0) return false;
// Проверяем делители от 3 до n
for (let i = 3; i < n; i += 2) {
if (n % i === 0) return false;
}
return true;
}
Анализ:
Подход 2: Оптимизированный (O(√n)) — РЕКОМЕНДУЕМЫЙ
Решение
Анаграмма — это слово или фраза, составленная из букв другого слова/фразы в другом порядке. Разберу несколько подходов.
Подход 1: Сортировка символов
function isAnagram(str1: string, str2: string): boolean {
// Очищаем строки, переводим в нижний регистр
const normalize = (str: string) =>
str.toLowerCase().replace(/[^a-z0-9]/g, "");
const clean1 = normalize(str1);
const clean2 = normalize(str2);
// Сортируем символы и сравниваем
return clean1.split("").sort().join("") === clean2.split("").sort().join("");
}
Как работает:
.split("").sort().join("")Временная сложность: O(n log n) из-за сортировки Пространственная сложность: O(n)
Подход 2: Подсчёт символов (оптимально)
Решение
Поиск минимального и максимального элемента — базовая задача, которая показывает понимание алгоритмов и обработки граничных случаев. Хотя Math.max/min существуют, запрет на них заставляет написать собственную реализацию.
Подход 1: Простой цикл (классический)
interface MinMax {
min: number;
max: number;
}
function findMinMax(arr: number[]): MinMax {
// Граничный случай: пустой массив
if (arr.length === 0) {
throw new Error("Array must not be empty");
}
let min = arr[0];
let max = arr[0];
// Начинаем с индекса 1, так как 0 уже обработана
for (let i = 1; i < arr.length; i++) {
if (arr[i] < min) {
min = arr[i];
}
if (arr[i] > max) {
max = arr[i];
}
}
return { min, max };
}
// Примеры:
console.log(findMinMax([3, 1, 4, 1, 5, 9, 2, 6]));
// { min: 1, max: 9 }
console.log(findMinMax([-5, 0, 5, 10, -10]));
// { min: -10, max: 10 }
Решение
Подход 1: С использованием регулярного выражения
function countVowels(str: string): number {
const matches = str.match(/[aeiou]/gi);
return matches ? matches.length : 0;
}
Как работает:
/[aeiou]/gi — ищет любую гласнуюg флаг — все совпаденияi флаг — регистронезависимыйmatch() возвращает массив или nullПодход 2: С использованием простого цикла
function countVowels(str: string): number {
const vowels = 'aeiouAEIOU';
let count = 0;
for (const char of str) {
if (vowels.includes(char)) {
count++;
}
}
return count;
}
Подход 3: С использованием filter
function countVowels(str: string): number {
return [...str].filter(char => /[aeiou]/i.test(char)).length;
}
Подход 4: С использованием Set
Решение
Порядок вывода
1
6
3
5
2
4
7
Объяснение
Это типичная задача на понимание Event Loop в JavaScript. Разберу пошагово.
Основные концепции
Call Stack — стек синхронного кода
Microtask Queue — очередь микрозадач (Promises, queueMicrotask, MutationObserver)
Macrotask Queue — очередь макрозадач (setTimeout, setInterval, setImmediate, I/O)
Порядок исполнения
console.log(1); // Выводит: 1
setTimeout(...); // Добавляет в Macrotask Queue
Promise.reject(3).. // Добавляет в Microtask Queue
new Promise(...); // setTimeout создаёт новый Promise
Promise.resolve(5).. // Добавляет в Microtask Queue
console.log(6); // Выводит: 6
setTimeout(...); // Добавляет в Macrotask Queue
Вывод: 1, 6Решение
Базовая реализация
function mergeIntervals(intervals: number[][]): number[][] {
// Если интервалов меньше 2, возвращаем как есть
if (intervals.length <= 1) {
return intervals;
}
// Сортируем интервалы по началу
intervals.sort((a, b) => a[0] - b[0]);
const result: number[][] = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const current = intervals[i];
const last = result[result.length - 1];
// Если текущий интервал пересекается с последним, объединяем
if (current[0] <= last[1]) {
last[1] = Math.max(last[1], current[1]);
} else {
// Иначе добавляем как новый интервал
result.push(current);
}
}
return result;
}
Как это работает
Принцип работы: