Live Coding

Проверка объекта на пустоту

Как проверить, является ли объект JavaScript пустым?

Объяснение: Объект пуст, если у него нет собственных перечисляемых свойств. Мы можем использовать Object.keys(obj), который возвращает массив собственных перечисляемых имен свойств данного объекта. Если длина этого массива равна 0, объект пуст.

function isEmpty(obj) { return Object.keys(obj).length === 0; } console.log(isEmpty({})); // true console.log(isEmpty({ a: 1 })); // false

Развернуть строку

Напишите функцию для разворачивания заданной строки.

Объяснение: Простейший способ — преобразовать строку в массив символов, использовать встроенный метод reverse() для массивов, а затем объединить символы обратно в строку.

function reverseString(str) { return str.split('').reverse().join(''); } console.log(reverseString('hello')); // 'olleh'

Проверка на палиндром

Напишите функцию, которая проверяет, является ли заданная строка палиндромом.

Объяснение: Палиндром — это слово или фраза, которые читаются одинаково как в прямом, так и в обратном направлении. Мы можем проверить это, развернув строку (игнорируя регистр и неалфавитно-цифровые символы для более надежной проверки, но здесь мы делаем простую проверку) и сравнив ее с оригиналом.

function isPalindrome(str) { const reversed = str.split('').reverse().join(''); return str === reversed; } console.log(isPalindrome('racecar')); // true console.log(isPalindrome('hello')); // false

Поиск максимального числа в массиве

Напишите функцию для поиска наибольшего числа в массиве чисел.

Объяснение: Вы можете перебирать массив, отслеживая наибольшее найденное число. Альтернативно, вы можете использовать функцию Math.max() вместе с оператором spread (...), чтобы передать элементы массива в качестве аргументов.

function findMaxNumber(arr) { if (arr.length === 0) return undefined; // Или выбросить ошибку return Math.max(...arr); } console.log(findMaxNumber([1, 5, 2, 9, 3])); // 9

FizzBuzz

Напишите программу, которая выводит числа от 1 до n. Но для чисел, кратных трем, выводит 'Fizz' вместо числа, а для чисел, кратных пяти, выводит 'Buzz'. Для чисел, которые кратны как трем, так и пяти, выводит 'FizzBuzz'.

Объяснение: Эта классическая задача проверяет базовую логику циклов и условий. Вам нужно перебирать числа от 1 до n и использовать оператор по модулю (%) для проверки делимости на 3 и 5.

function fizzBuzz(n) { for (let i = 1; i <= n; i++) { if (i % 3 === 0 && i % 5 === 0) { console.log('FizzBuzz'); } else if (i % 3 === 0) { console.log('Fizz'); } else if (i % 5 === 0) { console.log('Buzz'); } else { console.log(i); } } } fizzBuzz(15);

Удаление дубликатов из массива

Напишите функцию, которая удаляет повторяющиеся элементы из массива.

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

function removeDuplicates(arr) { return [...new Set(arr)]; } console.log(removeDuplicates([1, 2, 2, 3, 4, 4, 5])); // [1, 2, 3, 4, 5]

Проверка на анаграмму

Напишите функцию для проверки, являются ли две строки анаграммами друг друга.

Объяснение: Анаграммы — это слова, образованные перестановкой букв другого слова. Для проверки мы можем очистить, отсортировать и сравнить строки. Очистка включает удаление неалфавитно-цифровых символов и преобразование в единый регистр.

function isAnagram(str1, str2) { const clean = (str) => str.replace(/[^a-z0-9]/gi, '').toLowerCase(); const sorted = (str) => clean(str).split('').sort().join(''); return sorted(str1) === sorted(str2); } console.log(isAnagram('listen', 'silent')); // true console.log(isAnagram('hello', 'world')); // false

Вычисление факториала

Напишите функцию для вычисления факториала неотрицательного целого числа.

Объяснение: Факториал (n!) числа — это произведение всех положительных целых чисел, меньших или равных n. Мы можем вычислить это итеративно, умножая числа от 1 до n.

function factorial(n) { if (n < 0) return undefined; // Факториал не определен для отрицательных чисел if (n === 0) return 1; let result = 1; for (let i = 1; i <= n; i++) { result *= i; } return result; } console.log(factorial(5)); // 120

Сумма всех чисел в массиве

Напишите функцию, которая возвращает сумму всех чисел в массиве.

Объяснение: Вы можете использовать метод reduce, который идеально подходит для накопления одного значения из массива. Он принимает функцию обратного вызова и начальное значение (0 для суммирования).

function sumArray(arr) { return arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0); } console.log(sumArray([1, 2, 3, 4])); // 10

Развернуть вложенный массив

Напишите функцию для разворачивания вложенного массива. Для простоты предположим только один уровень вложенности.

Объяснение: Для одного уровня вложенности вы можете использовать Array.prototype.concat() с оператором spread или метод flat() (ES2019).

function flattenArray(arr) { // Использование flat() (проще, если ES2019+ подходит) // return arr.flat(); // Использование reduce и concat return arr.reduce((acc, val) => acc.concat(val), []); } console.log(flattenArray([1, [2, 3], 4, [5]])); // [1, 2, 3, 4, 5]

Подсчет гласных в строке

Напишите функцию, которая подсчитывает количество гласных (a, e, i, o, u) в заданной строке.

Объяснение: Переберите строку (без учета регистра) и проверьте, является ли каждый символ гласной. Поддерживайте счетчик.

function countVowels(str) { const vowels = 'aeiou'; let count = 0; for (let char of str.toLowerCase()) { if (vowels.includes(char)) { count++; } } return count; } console.log(countVowels('Hello World')); // 3

Преобразование предложения в регистр заголовка

Напишите функцию, которая преобразует предложение в регистр заголовка (первая буква каждого слова заглавная).

Объяснение: Разделите предложение на слова, затем переберите каждое слово, сделав первую букву заглавной, а остальные — строчными. Наконец, соедините слова обратно.

function titleCase(str) { return str.toLowerCase().split(' ').map(word => { return word.charAt(0).toUpperCase() + word.slice(1); }).join(' '); } console.log(titleCase('i am a little tea pot')); // 'I Am A Little Tea Pot'

Задача двух сумм

Учитывая массив целых чисел nums и целое число target, верните индексы двух чисел, сумма которых равна target.

Объяснение: Распространенный подход — использовать хеш-карту (или объект JavaScript) для хранения чисел и их индексов по мере итерации. Для каждого числа проверяйте, существует ли target - current_number в карте.

function twoSum(nums, target) { const map = {}; for (let i = 0; i < nums.length; i++) { const complement = target - nums[i]; if (map[complement] !== undefined) { return [map[complement], i]; } map[nums[i]] = i; } return []; // Или null, или выбросить ошибку, если нет решения } console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]

Реализация счетчика с использованием замыканий

Создайте функцию, которая возвращает функцию счетчика. Каждый раз, когда вызывается возвращаемая функция, она должна увеличивать и возвращать счетчик.

Объяснение: Это демонстрирует замыкания. Внутренняя функция имеет доступ к переменной count из области видимости внешней функции, даже после того, как внешняя функция завершила выполнение.

function createCounter() { let count = 0; return function() { count++; return count; }; } const counter1 = createCounter(); console.log(counter1()); // 1 console.log(counter1()); // 2 const counter2 = createCounter(); console.log(counter2()); // 1

Поиск первого неповторяющегося символа

Напишите функцию, которая находит первый неповторяющийся символ в строке.

Объяснение: Вы можете использовать хеш-карту для подсчета частоты символов. Сначала переберите строку, чтобы построить карту частот. Затем снова переберите строку и верните первый символ с числом 1.

function firstNonRepeatingChar(str) { const charCount = {}; for (const char of str) { charCount[char] = (charCount[char] || 0) + 1; } for (const char of str) { if (charCount[char] === 1) { return char; } } return null; // Или какой-либо индикатор, если все повторяются } console.log(firstNonRepeatingChar('aabbcdeeff')); // 'c' console.log(firstNonRepeatingChar('swiss')); // 'w'

Разбиение массива на чанки

Напишите функцию, которая разбивает массив на группы заданного размера.

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

function chunkArray(arr, size) { const chunked = []; let index = 0; while (index < arr.length) { chunked.push(arr.slice(index, index + size)); index += size; } return chunked; } console.log(chunkArray([1, 2, 3, 4, 5, 6, 7], 3)); // [[1, 2, 3], [4, 5, 6], [7]]

Проверка, является ли число простым

Напишите функцию, которая определяет, является ли заданное число простым.

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

function isPrime(num) { if (num <= 1) return false; if (num <= 3) return true; if (num % 2 === 0 || num % 3 === 0) return false; for (let i = 5; i * i <= num; i = i + 6) { if (num % i === 0 || num % (i + 2) === 0) return false; } return true; } console.log(isPrime(7)); // true console.log(isPrime(10)); // false

Поиск самого длинного слова в строке

Напишите функцию, которая находит самое длинное слово в предложении.

Объяснение: Разделите строку на массив слов. Затем переберите массив, отслеживая самое длинное найденное слово (или его длину).

function findLongestWord(str) { const words = str.split(' '); let longestWord = ''; for (const word of words) { // Очистить слова, если необходимо (например, удалить пунктуацию) if (word.length > longestWord.length) { longestWord = word; } } return longestWord; } console.log(findLongestWord('The quick brown fox jumped over the lazy dog')); // 'jumped'

Реализация `Array.prototype.map`

Реализуйте свою собственную версию функции Array.prototype.map.

Объяснение: Функция map принимает обратный вызов и создает новый массив, применяя обратный вызов к каждому элементу исходного массива. Ваша функция должна перебирать массив и строить новый массив.

function myMap(arr, callback) { const mappedArray = []; for (let i = 0; i < arr.length; i++) { mappedArray.push(callback(arr[i], i, arr)); } return mappedArray; } const numbers = [1, 4, 9]; const roots = myMap(numbers, Math.sqrt); console.log(roots); // [1, 2, 3]

Реализация `Array.prototype.filter`

Реализуйте свою собственную версию функции Array.prototype.filter.

Объяснение: Функция filter принимает обратный вызов и создает новый массив со всеми элементами, которые проходят тест, реализованный предоставленной функцией обратного вызова.

function myFilter(arr, callback) { const filteredArray = []; for (let i = 0; i < arr.length; i++) { if (callback(arr[i], i, arr)) { filteredArray.push(arr[i]); } } return filteredArray; } const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present']; const result = myFilter(words, word => word.length > 6); console.log(result); // ['exuberant', 'destruction', 'present']

Реализация `Array.prototype.reduce`

Реализуйте свою собственную версию функции Array.prototype.reduce.

Объяснение: Функция reduce выполняет предоставленную пользователем функцию обратного вызова-«редуктора» для каждого элемента массива, передавая возвращаемое значение из вычисления предыдущего элемента. Конечным результатом выполнения редуктора по всем элементам массива является одно значение. Обработайте аргумент начального значения.

function myReduce(arr, callback, initialValue) { let accumulator = initialValue; let startIndex = 0; if (initialValue === undefined) { if (arr.length === 0) throw new TypeError('Reduce of empty array with no initial value'); accumulator = arr[0]; startIndex = 1; } for (let i = startIndex; i < arr.length; i++) { accumulator = callback(accumulator, arr[i], i, arr); } return accumulator; } const array1 = [1, 2, 3, 4]; const sum = myReduce(array1, (acc, curr) => acc + curr, 0); console.log(sum); // 10

Мемоизация - Последовательность Фибоначчи

Реализуйте функцию Фибоначчи с использованием мемоизации для оптимизации производительности.

Объяснение: Последовательность Фибоначчи включает повторяющиеся вычисления. Мемоизация сохраняет результаты дорогостоящих вызовов функций и возвращает кэшированный результат, когда те же входные данные встречаются снова.

function memoizedFib() { const cache = {}; function fib(n) { if (n in cache) { return cache[n]; } if (n <= 1) { return n; } const result = fib(n - 1) + fib(n - 2); cache[n] = result; return result; } return fib; } const fibonacci = memoizedFib(); console.log(fibonacci(10)); // 55 console.log(fibonacci(40)); // 102334155 (гораздо быстрее, чем без мемоизации)

Проверка на сбалансированные скобки

Напишите функцию для проверки, является ли строка, содержащая скобки {}[](), сбалансированной.

Объяснение: Используйте стек. Когда вы встречаете открывающую скобку, поместите ее в стек. Когда вы встречаете закрывающую скобку, проверьте, пуст ли стек или является ли верхний элемент соответствующей открывающей скобкой. Если совпадает, извлеките из стека. В противном случае, или если стек пуст, он несбалансирован. В конце пустой стек означает сбалансированный.

function isBalanced(str) { const stack = []; const map = { '(': ')', '{': '}', '[': ']' }; for (let char of str) { if (map[char]) { stack.push(char); } else if (Object.values(map).includes(char)) { if (stack.length === 0) return false; const lastOpen = stack.pop(); if (map[lastOpen] !== char) return false; } } return stack.length === 0; } console.log(isBalanced('({[]})')); // true console.log(isBalanced('([)]')); // false

Реализация простой очереди

Реализуйте структуру данных Queue с методами enqueue (добавить в конец) и dequeue (удалить из начала).

Объяснение: Очередь следует принципу «первым пришел — первым ушел» (FIFO). Может быть использован массив с методами push для enqueue и shift для dequeue.

class Queue { constructor() { this.items = []; } enqueue(element) { this.items.push(element); } dequeue() { if (this.isEmpty()) return 'Underflow'; return this.items.shift(); } front() { if (this.isEmpty()) return 'No elements in Queue'; return this.items[0]; } isEmpty() { return this.items.length === 0; } } const q = new Queue(); q.enqueue(10); q.enqueue(20); console.log(q.dequeue()); // 10 console.log(q.front()); // 20

Реализация простого стека

Реализуйте структуру данных Stack с методами push (добавить наверх) и pop (удалить сверху).

Объяснение: Стек следует принципу «последним пришел — первым ушел» (LIFO). Может быть использован массив с методами push и pop.

class Stack { constructor() { this.items = []; } push(element) { this.items.push(element); } pop() { if (this.isEmpty()) return 'Underflow'; return this.items.pop(); } peek() { return this.items[this.items.length - 1]; } isEmpty() { return this.items.length === 0; } } const s = new Stack(); s.push(10); s.push(20); console.log(s.pop()); // 20 console.log(s.peek()); // 10

Поиск пропущенного числа в последовательности

Дан массив, содержащий n различных чисел от 0, 1, 2, ..., n. Найдите то, которое отсутствует в массиве.

Объяснение: Сумма чисел от 0 до n может быть вычислена по формуле n*(n+1)/2. Фактическая сумма элементов массива может быть вычислена. Разница между этими двумя суммами будет пропущенным числом.

function findMissingNumber(nums) { const n = nums.length; const expectedSum = n * (n + 1) / 2; const actualSum = nums.reduce((sum, num) => sum + num, 0); return expectedSum - actualSum; } console.log(findMissingNumber([3, 0, 1])); // 2 console.log(findMissingNumber([9, 6, 4, 2, 3, 5, 7, 0, 1])); // 8

Функция Debounce

Реализуйте функцию debounce. Debouncing гарантирует, что функция не будет вызвана снова, пока не пройдет определенное количество времени без ее вызова.

Объяснение: Используйте setTimeout и clearTimeout. Каждый раз, когда вызывается функция debounce, очистите предыдущий тайм-аут и установите новый. Фактический вызов функции происходит только тогда, когда тайм-аут завершается.

function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(this, args); }, delay); }; } // Пример использования: const sayHello = () => console.log('Hello!'); const debouncedHello = debounce(sayHello, 1000); debouncedHello(); // Вызов через 1 сек (если не будет вызван снова) debouncedHello(); // Сбрасывает таймер debouncedHello(); // Сбрасывает таймер, фактически выполнится через 1 сек после этого вызова.

Функция Throttle

Реализуйте функцию throttle. Throttling гарантирует, что функция вызывается не чаще одного раза в указанный интервал времени.

Объяснение: Используйте флаг, чтобы указать, разрешен ли вызов. При вызове, если разрешено, выполните функцию, установите флаг в false и используйте setTimeout для сброса флага после интервала.

function throttle(func, limit) { let inThrottle = false; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // Пример использования: const logScroll = () => console.log('Scrolling...'); const throttledScroll = throttle(logScroll, 1000); // window.addEventListener('scroll', throttledScroll); // Будет логировать не чаще одного раза в секунду.

Каррирование функции

Напишите функцию, которая принимает функцию с двумя аргументами и возвращает ее каррированную версию.

Объяснение: Каррирование преобразует функцию с несколькими аргументами в последовательность функций, каждая из которых принимает один аргумент. f(a, b) становится f(a)(b).

function curry(fn) { return function(a) { return function(b) { return fn(a, b); }; }; } function add(a, b) { return a + b; } const curriedAdd = curry(add); const add5 = curriedAdd(5); console.log(add5(3)); // 8 console.log(curriedAdd(10)(20)); // 30

Глубокое клонирование объекта

Напишите функцию для выполнения глубокого клонирования объекта JavaScript.

Объяснение: Поверхностное клонирование копирует только свойства верхнего уровня. Глубокое клонирование рекурсивно копирует все свойства, включая вложенные объекты и массивы. Простой способ (с ограничениями, например, плохо обрабатывает функции, даты, регулярные выражения) — использование JSON.parse(JSON.stringify(obj)). Рекурсивный подход более надежен.

function deepClone(obj) { // Простая версия (с ограничениями) try { return JSON.parse(JSON.stringify(obj)); } catch (e) { console.error("Cloning failed:", e); return null; } // Более надежная рекурсивная (базовый пример): /* if (obj === null || typeof obj !== 'object') { return obj; } let clone = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { clone[key] = deepClone(obj[key]); } } return clone; */ } const original = { a: 1, b: { c: 2 } }; const cloned = deepClone(original); cloned.b.c = 3; console.log(original.b.c); // 2 (доказывает, что это глубокий клон) console.log(cloned.b.c); // 3

Получение ключей объекта

Как получить массив ключей из объекта?

Объяснение: Используйте Object.keys(obj).

function getKeys(obj) { return Object.keys(obj); } console.log(getKeys({a: 1, b: 2})); // ['a', 'b']

Получение значений объекта

Как получить массив значений из объекта?

Объяснение: Используйте Object.values(obj).

function getValues(obj) { return Object.values(obj); } console.log(getValues({a: 1, b: 2})); // [1, 2]

Проверка, содержит ли массив значение

Как проверить, содержит ли массив определенное значение?

Объяснение: Используйте Array.prototype.includes(value).

function checkIncludes(arr, val) { return arr.includes(val); } console.log(checkIncludes([1, 2, 3], 2)); // true

Объединение двух массивов

Как объединить два массива в один?

Объяснение: Используйте оператор spread (...) или Array.prototype.concat().

function mergeArrays(arr1, arr2) { return [...arr1, ...arr2]; } console.log(mergeArrays([1, 2], [3, 4])); // [1, 2, 3, 4]

Объяснение 'this' в глобальной области видимости

На что ссылается this в глобальной области видимости в браузере?

Объяснение: В глобальном контексте выполнения (вне любой функции) this ссылается на глобальный объект, который в веб-браузерах является window.

console.log(this === window); // true (в среде браузера)

Объяснение 'this' в методе объекта

На что ссылается this при использовании внутри метода объекта?

Объяснение: Когда функция вызывается как метод объекта, this ссылается на объект, для которого вызывается метод.

const myObject = { prop: 'Hello', greet() { return this.prop; } }; console.log(myObject.greet()); // 'Hello'

Объяснение 'this' со стрелочными функциями

Как стрелочные функции обрабатывают this?

Объяснение: Стрелочные функции не имеют собственного контекста this. Вместо этого они наследуют this из окружающей (лексической) области видимости, где они определены.

function MyClass() { this.value = 42; setTimeout(() => { console.log(this.value); // Выводит 42, потому что 'this' наследуется }, 100); } new MyClass();

Различия между `let`, `const` и `var`

Каковы основные различия между let, const и var?

Объяснение: var имеет функциональную область видимости и поднимается. let и const имеют блочную область видимости и поднимаются, но находятся в «временной мертвой зоне» до объявления. const должен быть инициализирован и не может быть переназначен.

function scopeTest() { var a = 1; let b = 2; const c = 3; if (true) { var a = 10; // Переопределяет и влияет на внешний 'a' let b = 20; // Новый 'b' внутри блока // const c = 30; // Был бы новым 'c' console.log(a, b, c); // 10, 20, 3 } console.log(a, b, c); // 10, 2, 3 } scopeTest();

Что такое Promise?

Объясните, что такое Promise в JavaScript.

Объяснение: Promise — это объект, представляющий окончательное завершение (или отказ) асинхронной операции и ее результирующее значение. Он может находиться в одном из трех состояний: ожидание, выполнено или отклонено.

const myPromise = new Promise((resolve, reject) => { setTimeout(() => { resolve('Success!'); // reject('Error!'); }, 1000); }); myPromise .then(result => console.log(result)) .catch(error => console.error(error));

Использование `async/await`

Как async и await упрощают работу с Promises?

Объяснение: async/await предоставляет синтаксический сахар над Promises, делая асинхронный код выглядящим и ведущим себя немного больше как синхронный код. Асинхронная функция всегда возвращает Promise, а await приостанавливает выполнение до тех пор, пока Promise не разрешится.

function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function run() { console.log('Starting...'); await delay(1000); console.log('Waited 1 second.'); await delay(500); console.log('Waited another 0.5 seconds.'); return 'Finished!'; } run().then(console.log);

Преобразование строки в число

Как преобразовать строку в число?

Объяснение: Используйте parseInt(), parseFloat() или унарный оператор плюс (+).

const str = '123.45'; console.log(parseInt(str)); // 123 console.log(parseFloat(str)); // 123.45 console.log(+str); // 123.45

Преобразование числа в строку

Как преобразовать число в строку?

Объяснение: Используйте String(), number.toString() или конкатенацию строк.

const num = 123; console.log(String(num)); // '123' console.log(num.toString()); // '123' console.log('' + num); // '123'

Что такое `JSON.stringify`?

Что делает JSON.stringify?

Объяснение: Он преобразует объект или значение JavaScript в строку JSON.

const obj = { name: 'Alice', age: 30 }; const jsonString = JSON.stringify(obj); console.log(jsonString); // '{"name":"Alice","age":30}'

Что такое `JSON.parse`?

Что делает JSON.parse?

Объяснение: Он разбирает строку JSON, конструируя значение или объект JavaScript, описанный строкой.

const jsonString = '{"name":"Alice","age":30}'; const obj = JSON.parse(jsonString); console.log(obj.name); // 'Alice'

Оператор spread в массивах

Как используется оператор spread с массивами?

Объяснение: Он позволяет расширять итерируемый объект (например, массив) в местах, где ожидается ноль или более аргументов или элементов. Полезно для копирования, слияния и добавления элементов.

const arr1 = [1, 2]; const arr2 = [3, 4]; const combined = [...arr1, ...arr2]; // [1, 2, 3, 4] const copy = [...arr1]; // [1, 2]

Оператор spread в объектах

Как используется оператор spread с объектами?

Объяснение: Он позволяет копировать и объединять свойства объекта.

const obj1 = { a: 1, b: 2 }; const obj2 = { b: 3, c: 4 }; const merged = { ...obj1, ...obj2 }; // { a: 1, b: 3, c: 4 } const copy = { ...obj1 }; // { a: 1, b: 2 }

Деструктуризация массивов

Объясните деструктуризацию массивов на примере.

Объяснение: Это синтаксис, который позволяет распаковывать значения из массивов в отдельные переменные.

const [a, b] = [10, 20]; console.log(a); // 10 console.log(b); // 20 const [x, , z] = [1, 2, 3]; console.log(z); // 3

Деструктуризация объектов

Объясните деструктуризацию объектов на примере.

Объяснение: Это позволяет распаковывать свойства из объектов в отдельные переменные.

const person = { name: 'Bob', age: 25 }; const { name, age } = person; console.log(name); // 'Bob' console.log(age); // 25 const { name: personName } = person; console.log(personName); // 'Bob'

Реализация `call`

Как можно реализовать базовую версию Function.prototype.call?

Объяснение: call вызывает функцию с указанным значением this и аргументами, предоставленными по отдельности. Вы можете прикрепить функцию к контексту this, вызвать ее, а затем удалить.

Function.prototype.myCall = function(context, ...args) { context = context || window; const uniqueId = Symbol(); // Использовать уникальный ключ context[uniqueId] = this; const result = context[uniqueId](...args); delete context[uniqueId]; return result; } function greet(greeting, punctuation) { console.log(`${greeting}, ${this.name}${punctuation}`); } greet.myCall({ name: 'Charlie' }, 'Hi', '!'); // Hi, Charlie!

Реализация `apply`

Как можно реализовать базовую версию Function.prototype.apply?

Объяснение: apply аналогичен call, но принимает аргументы в виде массива.

Function.prototype.myApply = function(context, argsArray) { context = context || window; const uniqueId = Symbol(); context[uniqueId] = this; const result = context[uniqueId](...(argsArray || [])); delete context[uniqueId]; return result; } function greet(greeting, punctuation) { console.log(`${greeting}, ${this.name}${punctuation}`); } greet.myApply({ name: 'David' }, ['Hello', '.']); // Hello, David.

Объяснение цикла событий

Кратко объясните цикл событий JavaScript.

Объяснение: JavaScript является однопоточным, но достигает параллелизма с помощью цикла событий. Стек вызовов обрабатывает синхронный код. Веб-API обрабатывают асинхронные операции (например, setTimeout, fetch). Когда асинхронная операция завершается, ее обратный вызов переходит в очередь обратного вызова (или очередь микрозадач для Promises). Цикл событий постоянно проверяет, пуст ли стек вызовов; если это так, он перемещает следующий обратный вызов из очереди в стек для выполнения.

console.log('Start'); setTimeout(() => { console.log('Timeout Callback'); // Переходит в очередь обратного вызова }, 0); Promise.resolve().then(() => { console.log('Promise Resolved'); // Переходит в очередь микрозадач }); console.log('End'); // Порядок вывода: Start, End, Promise Resolved, Timeout Callback // (Микрозадачи выполняются до макрозадач/обратных вызовов)

Бинарный поиск

Реализуйте функцию бинарного поиска для отсортированного массива.

Объяснение: Бинарный поиск эффективно находит элемент в отсортированном массиве путем многократного деления интервала поиска пополам. Если значение ключа поиска меньше элемента в середине интервала, сузьте интервал до нижней половины. В противном случае, сузьте его до верхней половины. Вы делаете это до тех пор, пока значение не будет найдено или интервал не станет пустым.

function binarySearch(sortedArray, key) { let start = 0; let end = sortedArray.length - 1; while (start <= end) { let middle = Math.floor((start + end) / 2); if (sortedArray[middle] === key) { return middle; // Найдено } else if (sortedArray[middle] < key) { start = middle + 1; // Искать в правой половине } else { end = middle - 1; // Искать в левой половине } } return -1; // Не найдено } console.log(binarySearch([1, 3, 5, 7, 9, 11], 7)); // 3 console.log(binarySearch([1, 3, 5, 7, 9, 11], 2)); // -1

Сортировка слиянием

Реализуйте алгоритм сортировки слиянием.

Объяснение: Сортировка слиянием — это алгоритм «разделяй и властвуй». Он делит входной массив на две половины, вызывает себя для двух половин, а затем объединяет две отсортированные половины. Функция слияния имеет решающее значение; она объединяет два отсортированных подмассива в один отсортированный массив.

function mergeSort(arr) { if (arr.length <= 1) return arr; const mid = Math.floor(arr.length / 2); const left = mergeSort(arr.slice(0, mid)); const right = mergeSort(arr.slice(mid)); return merge(left, right); } function merge(left, right) { let result = []; let leftIndex = 0; let rightIndex = 0; while (leftIndex < left.length && rightIndex < right.length) { if (left[leftIndex] < right[rightIndex]) { result.push(left[leftIndex]); leftIndex++; } else { result.push(right[rightIndex]); rightIndex++; } } return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex)); } console.log(mergeSort([38, 27, 43, 3, 9, 82, 10])); // [3, 9, 10, 27, 38, 43, 82]

Быстрая сортировка

Реализуйте алгоритм быстрой сортировки.

Объяснение: Быстрая сортировка также является алгоритмом «разделяй и властвуй». Он выбирает элемент в качестве опорного и разделяет заданный массив вокруг выбранного опорного элемента. Элементы, меньшие опорного, идут влево, а элементы, большие, идут вправо. Затем он рекурсивно сортирует подмассивы.

function quickSort(arr) { if (arr.length <= 1) return arr; const pivot = arr[arr.length - 1]; const left = []; const right = []; for (let i = 0; i < arr.length - 1; i++) { if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return [...quickSort(left), pivot, ...quickSort(right)]; } console.log(quickSort([10, 8, 2, 1, 6, 3, 9, 4, 7, 5])); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Пузырьковая сортировка

Реализуйте алгоритм пузырьковой сортировки.

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

function bubbleSort(arr) { let n = arr.length; let swapped; do { swapped = false; for (let i = 0; i < n - 1; i++) { if (arr[i] > arr[i + 1]) { [arr[i], arr[i + 1]] = [arr[i + 1], arr[i]]; // Обмен swapped = true; } } n--; // Оптимизация: последний элемент уже на месте } while (swapped); return arr; } console.log(bubbleSort([64, 34, 25, 12, 22, 11, 90])); // [11, 12, 22, 25, 34, 64, 90]

Самая длинная подстрока без повторяющихся символов

Дана строка, найдите длину самой длинной подстроки без повторяющихся символов.

Объяснение: Используйте технику «скользящего окна». Поддерживайте окно (подстроку) и набор символов в этом окне. Расширяйте окно, перемещая правый указатель. Если найден повторяющийся символ, сузьте окно слева до тех пор, пока повторяющийся символ не будет удален.

function lengthOfLongestSubstring(s) { let maxLength = 0; let start = 0; const charMap = {}; for (let end = 0; end < s.length; end++) { const char = s[end]; if (charMap[char] >= start) { start = charMap[char] + 1; } charMap[char] = end; maxLength = Math.max(maxLength, end - start + 1); } return maxLength; } console.log(lengthOfLongestSubstring('abcabcbb')); // 3 ('abc') console.log(lengthOfLongestSubstring('bbbbb')); // 1 ('b') console.log(lengthOfLongestSubstring('pwwkew')); // 3 ('wke')

Реализация связного списка

Реализуйте односвязный список с методами add и print.

Объяснение: Связный список — это линейная структура данных, где элементы не хранятся в смежных ячейках памяти. Каждый элемент (узел) указывает на следующий. Вам нужен класс Node и класс LinkedList для управления головой и добавления новых узлов.

class Node { constructor(data, next = null) { this.data = data; this.next = next; } } class LinkedList { constructor() { this.head = null; } add(data) { const newNode = new Node(data); if (!this.head) { this.head = newNode; } else { let current = this.head; while (current.next) { current = current.next; } current.next = newNode; } } print() { let current = this.head; let list = ''; while (current) { list += current.data + ' -> '; current = current.next; } console.log(list + 'null'); } } const list = new LinkedList(); list.add(10); list.add(20); list.add(30); list.print(); // 10 -> 20 -> 30 -> null

Реализация двоичного дерева поиска (BST)

Реализуйте двоичное дерево поиска с методами insert и find.

Объяснение: BST — это древовидная структура данных, основанная на узлах, которая обладает следующими свойствами: левое поддерево узла содержит только узлы с ключами меньшими, чем ключ узла. Правое поддерево узла содержит только узлы с ключами большими, чем ключ узла. Оба поддерева также должны быть двоичными деревьями поиска.

class Node { constructor(data) { this.data = data; this.left = null; this.right = null; } } class BST { constructor() { this.root = null; } insert(data) { const newNode = new Node(data); if (!this.root) { this.root = newNode; return; } this._insertNode(this.root, newNode); } _insertNode(node, newNode) { if (newNode.data < node.data) { if (!node.left) node.left = newNode; else this._insertNode(node.left, newNode); } else { if (!node.right) node.right = newNode; else this._insertNode(node.right, newNode); } } find(data) { return this._findNode(this.root, data); } _findNode(node, data) { if (!node) return null; if (data < node.data) return this._findNode(node.left, data); else if (data > node.data) return this._findNode(node.right, data); else return node; } } const bst = new BST(); bst.insert(10); bst.insert(5); bst.insert(15); bst.insert(7); console.log(bst.find(7)); // Node { data: 7, left: null, right: null } console.log(bst.find(12)); // null

Поворот массива

Напишите функцию для поворота массива вправо на k шагов.

Объяснение: Поворот массива означает перемещение его элементов. При повороте вправо последний элемент становится первым. Повторение этого k раз работает, но неэффективно. Лучший способ — использовать манипуляции с массивом или вычислять новые позиции.

function rotateArray(nums, k) { k = k % nums.length; // Обработка случаев, когда k > длины if (k === 0) return nums; const partToMove = nums.splice(nums.length - k); nums.unshift(...partToMove); return nums; } console.log(rotateArray([1, 2, 3, 4, 5, 6, 7], 3)); // [5, 6, 7, 1, 2, 3, 4]

Поиск пересечения двух массивов

Даны два массива, напишите функцию для вычисления их пересечения (элементы, общие для обоих).

Объяснение: Хороший подход — преобразовать один массив в Set для поиска со средней сложностью O(1). Затем переберите второй массив и проверьте, существует ли каждый элемент в наборе. Соберите совпадения.

function intersection(nums1, nums2) { const set1 = new Set(nums1); const resultSet = new Set(); for (const num of nums2) { if (set1.has(num)) { resultSet.add(num); } } return [...resultSet]; } console.log(intersection([1, 2, 2, 1], [2, 2])); // [2] console.log(intersection([4, 9, 5], [9, 4, 9, 8, 4])); // [9, 4]

Группировка анаграмм

Дан массив строк, сгруппируйте анаграммы вместе.

Объяснение: Главное — найти уникальную сигнатуру для каждой группы анаграмм. Распространенный способ — отсортировать каждое слово в алфавитном порядке. Слова, которые приводят к одной и той же отсортированной строке, являются анаграммами. Используйте хеш-карту, где ключами являются отсортированные слова, а значениями — массивы исходных слов.

function groupAnagrams(strs) { const map = {}; for (const str of strs) { const sortedStr = str.split('').sort().join(''); if (!map[sortedStr]) { map[sortedStr] = []; } map[sortedStr].push(str); } return Object.values(map); } console.log(groupAnagrams(['eat', 'tea', 'tan', 'ate', 'nat', 'bat'])); // Вывод: [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]

Перемещение нулей в конец

Дан массив nums, напишите функцию для перемещения всех 0 в конец, сохраняя относительный порядок ненулевых элементов.

Объяснение: Используйте подход «два указателя» или «снежный ком». Один указатель отслеживает позицию, куда должен быть помещен следующий ненулевой элемент. Переберите массив; если элемент ненулевой, поместите его на позицию указателя и увеличьте указатель. Наконец, заполните остаток нулями.

function moveZeroes(nums) { let nonZeroIndex = 0; // Переместите все ненулевые элементы в начало for (let i = 0; i < nums.length; i++) { if (nums[i] !== 0) { nums[nonZeroIndex] = nums[i]; nonZeroIndex++; } } // Заполните остаток нулями for (let i = nonZeroIndex; i < nums.length; i++) { nums[i] = 0; } return nums; } console.log(moveZeroes([0, 1, 0, 3, 12])); // [1, 3, 12, 0, 0]

Максимальный подмассив (алгоритм Кадане)

Дан целочисленный массив nums, найдите непрерывный подмассив (содержащий хотя бы одно число), который имеет наибольшую сумму, и верните его сумму.

Объяснение: Алгоритм Кадане — это эффективный способ решения этой задачи. Переберите массив, отслеживая currentMax сумму, заканчивающуюся на текущей позиции, и globalMax сумму, найденную до сих пор. Если currentMax становится отрицательным, сбросьте его до 0 (или, скорее, до текущего элемента).

function maxSubArray(nums) { let globalMax = -Infinity; let currentMax = 0; for (let i = 0; i < nums.length; i++) { currentMax = Math.max(nums[i], currentMax + nums[i]); if (currentMax > globalMax) { globalMax = currentMax; } } return globalMax; } console.log(maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // 6 (из [4, -1, 2, 1])

Перестановки строки

Напишите функцию для генерации всех перестановок заданной строки.

Объяснение: Обычно это решается с использованием рекурсии и обратного отслеживания. Для каждого символа зафиксируйте его и рекурсивно генерируйте перестановки для остальной части строки. Базовый случай: когда строка состоит только из одного символа, верните ее.

function stringPermutations(str) { if (str.length === 0) return ['']; if (str.length === 1) return [str]; const results = []; for (let i = 0; i < str.length; i++) { const char = str[i]; const remainingChars = str.slice(0, i) + str.slice(i + 1); const perms = stringPermutations(remainingChars); for (const perm of perms) { results.push(char + perm); } } return [...new Set(results)]; // Используйте Set для обработки повторяющихся символов, если необходимо } console.log(stringPermutations('abc')); // ['abc', 'acb', 'bac', 'bca', 'cab', 'cba'] console.log(stringPermutations('aab')); // ['aab', 'aba', 'baa']

Наибольший общий делитель (НОД)

Напишите функцию для нахождения наибольшего общего делителя (НОД) двух чисел.

Объяснение: Алгоритм Евклида — это эффективный метод. Если b равно 0, a — это НОД. В противном случае НОД a и b совпадает с НОД b и a % b (остаток от деления a на b).

function gcd(a, b) { while (b !== 0) { let temp = b; b = a % b; a = temp; } return a; } console.log(gcd(48, 18)); // 6 console.log(gcd(101, 103)); // 1

Наименьшее общее кратное (НОК)

Напишите функцию для нахождения наименьшего общего кратного (НОК) двух чисел.

Объяснение: НОК может быть вычислено с использованием НОД по формуле: НОК(a, b) = |a * b| / НОД(a, b).

function gcd(a, b) { // Вспомогательная функция из предыдущей задачи while (b !== 0) { let temp = b; b = a % b; a = temp; } return a; } function lcm(a, b) { if (a === 0 || b === 0) return 0; return Math.abs(a * b) / gcd(a, b); } console.log(lcm(15, 20)); // 60 console.log(lcm(7, 5)); // 35

Реализация Promise.all

Реализуйте функцию, которая ведет себя как Promise.all.

Объяснение: Promise.all принимает массив промисов и возвращает один промис, который разрешается, когда все входные промисы разрешены, или отклоняется, если какой-либо входной промис отклоняется. Разрешенное значение — это массив разрешенных значений.

function myPromiseAll(promises) { return new Promise((resolve, reject) => { const results = []; let completedCount = 0; const numPromises = promises.length; if (numPromises === 0) { resolve([]); return; } promises.forEach((promise, index) => { Promise.resolve(promise) .then(value => { results[index] = value; completedCount++; if (completedCount === numPromises) { resolve(results); } }) .catch(reject); // Немедленно отклонить при любой ошибке }); }); } // Пример использования: const p1 = Promise.resolve(3); const p2 = 42; const p3 = new Promise((resolve) => setTimeout(resolve, 100, 'foo')); myPromiseAll([p1, p2, p3]).then(values => console.log(values)); // [3, 42, 'foo']

Инвертирование двоичного дерева

Напишите функцию для инвертирования двоичного дерева.

Объяснение: Чтобы инвертировать двоичное дерево, вы меняете местами левого и правого дочерних элементов для каждого узла. Это можно сделать рекурсивно или итеративно (используя очередь или стек).

class Node { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } } function invertTree(root) { if (root === null) { return null; } // Поменять местами дочерние элементы [root.left, root.right] = [root.right, root.left]; // Рекурсивно для левого и правого дочерних элементов invertTree(root.left); invertTree(root.right); return root; } // Пример: 4 -> [2, 7] -> [1, 3, 6, 9] // Становится: 4 -> [7, 2] -> [9, 6, 3, 1]

Максимальная глубина двоичного дерева

Дано двоичное дерево, найдите его максимальную глубину.

Объяснение: Максимальная глубина — это количество узлов по самому длинному пути от корневого узла до самого дальнего листового узла. Это можно найти рекурсивно: MaxDepth = 1 + max(MaxDepth(left), MaxDepth(right)). Базовый случай — когда узел равен null, его глубина равна 0.

class Node { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } } function maxDepth(root) { if (root === null) { return 0; } const leftDepth = maxDepth(root.left); const rightDepth = maxDepth(root.right); return Math.max(leftDepth, rightDepth) + 1; } // Пример: 3 -> [9, 20] -> [null, null, 15, 7] const tree = new Node(3, new Node(9), new Node(20, new Node(15), new Node(7))); console.log(maxDepth(tree)); // 3

Лучшее время для покупки/продажи акций

Вам дан массив prices, где prices[i] — это цена данной акции на i-й день. Вы хотите максимизировать свою прибыль, выбрав один день для покупки одной акции и выбрав другой день в будущем для продажи этой акции. Верните максимальную прибыль.

Объяснение: Переберите цены, отслеживая минимальную цену, встреченную до сих пор (minPrice), и максимальную прибыль, найденную до сих пор (maxProfit). Для каждого дня вычисляйте потенциальную прибыль, если бы вы продали сегодня (текущая цена - minPrice), и обновляйте maxProfit, если она выше.

function maxProfit(prices) { let minPrice = Infinity; let maxProfit = 0; for (let i = 0; i < prices.length; i++) { if (prices[i] < minPrice) { minPrice = prices[i]; } else if (prices[i] - minPrice > maxProfit) { maxProfit = prices[i] - minPrice; } } return maxProfit; } console.log(maxProfit([7, 1, 5, 3, 6, 4])); // 5 (Купить по 1, продать по 6) console.log(maxProfit([7, 6, 4, 3, 1])); // 0 (Прибыль невозможна)

Единственное число

Дан непустой массив целых чисел nums, каждый элемент встречается дважды, кроме одного. Найдите это единственное число.

Объяснение: Очень эффективный способ решения этой задачи — использование побитового оператора XOR (^). Операция XOR числа с самим собой приводит к 0. Операция XOR числа с 0 приводит к самому числу. Если вы примените XOR ко всем числам в массиве, пары взаимно уничтожатся (станут 0), оставив только единственное число.

function singleNumber(nums) { let result = 0; for (const num of nums) { result ^= num; } return result; } console.log(singleNumber([2, 2, 1])); // 1 console.log(singleNumber([4, 1, 2, 1, 2])); // 4

Мажоритарный элемент

Дан массив nums размера n, верните мажоритарный элемент. Мажоритарный элемент — это элемент, который появляется более ⌊n / 2⌋ раз.

Объяснение: Алгоритм голосования Бойера-Мура является эффективным способом. Инициализируйте candidate (кандидата) и count (счетчик). Переберите массив. Если count равен 0, установите текущий элемент в качестве candidate. Если текущий элемент совпадает с candidate, увеличьте count; в противном случае уменьшите count.

function majorityElement(nums) { let candidate = null; let count = 0; for (const num of nums) { if (count === 0) { candidate = num; } count += (num === candidate) ? 1 : -1; } return candidate; } console.log(majorityElement([3, 2, 3])); // 3 console.log(majorityElement([2, 2, 1, 1, 1, 2, 2])); // 2

Подъем по лестнице

Вы поднимаетесь по лестнице. Для достижения вершины требуется n шагов. Каждый раз вы можете подняться на 1 или 2 ступеньки. Сколькими различными способами вы можете подняться на вершину?

Объяснение: Это классическая задача динамического программирования, очень похожая на последовательность Фибоначчи. Количество способов достичь ступени n — это сумма способов достичь ступени n-1 (сделав 1 шаг) и способов достичь ступени n-2 (сделав 2 шага). dp[n] = dp[n-1] + dp[n-2].

function climbStairs(n) { if (n <= 2) return n; let oneStepBefore = 2; let twoStepsBefore = 1; let allWays = 0; for (let i = 3; i <= n; i++) { allWays = oneStepBefore + twoStepsBefore; twoStepsBefore = oneStepBefore; oneStepBefore = allWays; } return allWays; } console.log(climbStairs(2)); // 2 (1+1, 2) console.log(climbStairs(3)); // 3 (1+1+1, 1+2, 2+1) console.log(climbStairs(4)); // 5

Произведение массива, кроме себя

Дан целочисленный массив nums, верните массив answer такой, что answer[i] равен произведению всех элементов nums, кроме nums[i]. Не используйте оператор деления.

Объяснение: Вы можете решить это, вычисляя префиксные произведения и суффиксные произведения. Результат по индексу i — это произведение всех элементов до i, умноженное на произведение всех элементов после i.

function productExceptSelf(nums) { const n = nums.length; const answer = new Array(n).fill(1); // Вычислить префиксные произведения let prefix = 1; for (let i = 0; i < n; i++) { answer[i] = prefix; prefix *= nums[i]; } // Вычислить суффиксные произведения и умножить на префикс let suffix = 1; for (let i = n - 1; i >= 0; i--) { answer[i] *= suffix; suffix *= nums[i]; } return answer; } console.log(productExceptSelf([1, 2, 3, 4])); // [24, 12, 8, 6]

Количество островов

Дана двумерная сетка, состоящая из '1' (суша) и '0' (вода), подсчитайте количество островов. Остров окружен водой и образован соединением соседних участков суши по горизонтали или вертикали.

Объяснение: Переберите каждую ячейку сетки. Если вы нашли '1' (суша), увеличьте счетчик островов и начните поиск в глубину (DFS) или поиск в ширину (BFS) из этой ячейки. Во время поиска отметьте все соединенные '1' как '0' (или посещенные), чтобы избежать их повторного подсчета.

function numIslands(grid) { if (!grid || grid.length === 0) return 0; let count = 0; const rows = grid.length; const cols = grid[0].length; function dfs(r, c) { if (r < 0 || c < 0 || r >= rows || c >= cols || grid[r][c] === '0') { return; } grid[r][c] = '0'; // Отметить как посещенное dfs(r + 1, c); dfs(r - 1, c); dfs(r, c + 1); dfs(r, c - 1); } for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (grid[r][c] === '1') { count++; dfs(r, c); } } } return count; } const grid1 = [ ['1', '1', '1', '1', '0'], ['1', '1', '0', '1', '0'], ['1', '1', '0', '0', '0'], ['0', '0', '0', '0', '0'] ]; // console.log(numIslands(grid1)); // 1 (Нужно запустить на копии или восстановить сетку)

Кэш LRU

Разработайте и реализуйте структуру данных для кэша Least Recently Used (LRU). Она должна поддерживать операции get и put.

Объяснение: Кэш LRU вытесняет наименее недавно использованный элемент, когда достигается емкость. Распространенная реализация использует Map (или хеш-карту) для поиска за O(1) в среднем и двусвязный список для обновления статуса «недавно использованного» за O(1). Map в JavaScript поддерживает порядок вставки, что может упростить базовую реализацию.

class LRUCache { constructor(capacity) { this.capacity = capacity; this.cache = new Map(); } get(key) { if (!this.cache.has(key)) return -1; const value = this.cache.get(key); this.cache.delete(key); // Удалить, чтобы вставить снова в «конец» (самый недавний) this.cache.set(key, value); return value; } put(key, value) { if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.capacity) { const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); } this.cache.set(key, value); } } const cache = new LRUCache(2); cache.put(1, 1); cache.put(2, 2); console.log(cache.get(1)); // 1 cache.put(3, 3); // Вытесняет 2 console.log(cache.get(2)); // -1

Генерация скобок

Дано n пар скобок, напишите функцию для генерации всех комбинаций правильно сформированных скобок.

Объяснение: Это классическая задача с обратным отслеживанием. Поддерживайте счетчики открытых и закрытых скобок. Вы можете добавить открытую скобку, если open < n. Вы можете добавить закрытую скобку, если close < open. Базовый случай — когда длина строки достигает 2 * n.

function generateParenthesis(n) { const results = []; function backtrack(currentString, openCount, closeCount) { if (currentString.length === n * 2) { results.push(currentString); return; } if (openCount < n) { backtrack(currentString + '(', openCount + 1, closeCount); } if (closeCount < openCount) { backtrack(currentString + ')', openCount, closeCount + 1); } } backtrack('', 0, 0); return results; } console.log(generateParenthesis(3)); // ['((()))', '(()())', '(())()', '()(())', '()()()']

Контейнер с наибольшим количеством воды

Даны n неотрицательных целых чисел a1, a2, ..., an, где каждое представляет точку с координатой (i, ai). Нарисовано n вертикальных линий так, что две конечные точки линии i находятся в (i, ai) и (i, 0). Найдите две линии, которые вместе с осью x образуют контейнер, так чтобы контейнер содержал наибольшее количество воды.

Объяснение: Используйте подход с двумя указателями. Начните с одного указателя в начале и одного в конце. Вычислите площадь. Площадь ограничена более короткой линией. Чтобы потенциально найти большую площадь, переместите указатель, указывающий на более короткую линию, внутрь.

function maxArea(height) { let max = 0; let left = 0; let right = height.length - 1; while (left < right) { const h = Math.min(height[left], height[right]); const w = right - left; max = Math.max(max, h * w); if (height[left] < height[right]) { left++; } else { right--; } } return max; } console.log(maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7])); // 49

3Sum

Дан массив nums из n целых чисел, существуют ли элементы a, b, c в nums такие, что a + b + c = 0? Найдите все уникальные тройки в массиве, которые дают сумму, равную нулю.

Объяснение: Сначала отсортируйте массив. Затем переберите массив с помощью одного указателя i. Для каждого i используйте еще два указателя: left (начиная с i+1) и right (начиная с конца), чтобы найти два числа, сумма которых равна -nums[i]. Обработайте дубликаты, чтобы обеспечить уникальные тройки.

function threeSum(nums) { nums.sort((a, b) => a - b); const results = []; for (let i = 0; i < nums.length - 2; i++) { if (i > 0 && nums[i] === nums[i - 1]) continue; // Пропустить дубликаты для i let left = i + 1; let right = nums.length - 1; let target = -nums[i]; while (left < right) { let sum = nums[left] + nums[right]; if (sum === target) { results.push([nums[i], nums[left], nums[right]]); while (left < right && nums[left] === nums[left + 1]) left++; // Пропустить дубликаты while (left < right && nums[right] === nums[right - 1]) right--; // Пропустить дубликаты left++; right--; } else if (sum < target) { left++; } else { right--; } } } return results; } console.log(threeSum([-1, 0, 1, 2, -1, -4])); // [[-1, -1, 2], [-1, 0, 1]]

Позиция вставки поиска

Дан отсортированный массив различных целых чисел и целевое значение, верните индекс, если цель найдена. Если нет, верните индекс, куда она была бы вставлена по порядку.

Объяснение: Это вариация бинарного поиска. Выполните бинарный поиск, но если элемент не найден, указатель start окажется в позиции, куда должен быть вставлен элемент.

function searchInsert(nums, target) { let start = 0; let end = nums.length - 1; while (start <= end) { let mid = Math.floor((start + end) / 2); if (nums[mid] === target) { return mid; } else if (nums[mid] < target) { start = mid + 1; } else { end = mid - 1; } } return start; // Если не найдено, start — это точка вставки } console.log(searchInsert([1, 3, 5, 6], 5)); // 2 console.log(searchInsert([1, 3, 5, 6], 2)); // 1 console.log(searchInsert([1, 3, 5, 6], 7)); // 4

Объединение двух отсортированных списков (связные списки)

Объедините два отсортированных связных списка и верните их как новый отсортированный список. Новый список должен быть создан путем объединения узлов первых двух списков.

Объяснение: Используйте фиктивный головной узел, чтобы упростить процесс. Используйте указатель для построения нового списка. Сравните текущие узлы обоих входных списков и добавьте меньший в новый список, продвигая указатель этого списка. Повторяйте, пока один список не будет исчерпан, затем добавьте остаток другого списка.

class ListNode { constructor(val = 0, next = null) { this.val = val; this.next = next; } } function mergeTwoLists(l1, l2) { let dummyHead = new ListNode(); let current = dummyHead; while (l1 !== null && l2 !== null) { if (l1.val < l2.val) { current.next = l1; l1 = l1.next; } else { current.next = l2; l2 = l2.next; } current = current.next; } current.next = l1 !== null ? l1 : l2; return dummyHead.next; } // Пример: 1->2->4 и 1->3->4 => 1->1->2->3->4->4

Симметричное дерево

Дано двоичное дерево, проверьте, является ли оно зеркальным отражением самого себя (т.е. симметричным относительно своего центра).

Объяснение: Это можно решить рекурсивно. Дерево симметрично, если левое поддерево является зеркальным отображением правого поддерева. Создайте вспомогательную функцию isMirror(t1, t2), которая проверяет, являются ли два дерева зеркалами. Она должна проверять: 1. t1.val === t2.val. 2. t1.left является зеркалом t2.right. 3. t1.right является зеркалом t2.left.

class Node { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } } function isSymmetric(root) { if (!root) return true; function isMirror(t1, t2) { if (!t1 && !t2) return true; if (!t1 || !t2 || t1.val !== t2.val) return false; return isMirror(t1.left, t2.right) && isMirror(t1.right, t2.left); } return isMirror(root.left, root.right); } // Пример: 1 -> [2, 2] -> [3, 4, 4, 3] симметрично.

Обход по уровням (BST/Дерево)

Дано двоичное дерево, верните обход по уровням его узлов (т.е. слева направо, уровень за уровнем).

Объяснение: Это обход в ширину (BFS). Используйте очередь. Начните с добавления корня в очередь. Пока очередь не пуста, обработайте все узлы на текущем уровне. Для каждого обработанного узла добавьте его дочерние элементы (если они существуют) в очередь для следующего уровня.

class Node { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } } function levelOrder(root) { if (!root) return []; const result = []; const queue = [root]; while (queue.length > 0) { const levelSize = queue.length; const currentLevel = []; for (let i = 0; i < levelSize; i++) { const node = queue.shift(); currentLevel.push(node.val); if (node.left) queue.push(node.left); if (node.right) queue.push(node.right); } result.push(currentLevel); } return result; } // Пример: 3 -> [9, 20] -> [null, null, 15, 7] // Вывод: [[3], [9, 20], [15, 7]]

Преобразование отсортированного массива в сбалансированное по высоте BST

Дан массив, элементы которого отсортированы по возрастанию, преобразуйте его в сбалансированное по высоте двоичное дерево поиска (BST).

Объяснение: Чтобы создать сбалансированное по высоте BST, вы должны выбрать средний элемент массива в качестве корня. Затем рекурсивно построить левое поддерево из левой половины массива и правое поддерево из правой половины.

class TreeNode { constructor(val = 0, left = null, right = null) { this.val = val; this.left = left; this.right = right; } } function sortedArrayToBST(nums) { if (!nums || nums.length === 0) return null; function buildBST(start, end) { if (start > end) return null; const mid = Math.floor((start + end) / 2); const node = new TreeNode(nums[mid]); node.left = buildBST(start, mid - 1); node.right = buildBST(mid + 1, end); return node; } return buildBST(0, nums.length - 1); } // Пример: [-10, -3, 0, 5, 9] // Вывод: Дерево типа [0, -3, 9, -10, null, 5, null]

Реализация `bind`

Реализуйте свою собственную версию Function.prototype.bind.

Объяснение: bind создает новую функцию, которая при вызове имеет ключевое слово this, установленное на предоставленное значение, с заданной последовательностью аргументов, предшествующих любым аргументам, предоставленным при вызове новой функции. Используйте apply или call внутри возвращаемой функции, обрабатывая частичное применение (предварительно установленные аргументы).

Function.prototype.myBind = function(context, ...bindArgs) { const originalFunc = this; return function(...callArgs) { return originalFunc.apply(context, [...bindArgs, ...callArgs]); }; } const module = { x: 42, getX: function() { return this.x; } }; const unboundGetX = module.getX; console.log(unboundGetX()); // undefined (this - это глобальный объект/window) const boundGetX = unboundGetX.myBind(module); console.log(boundGetX()); // 42

Нахождение K-го наибольшего элемента

Найдите k-й по величине элемент в несортированном массиве. Обратите внимание, что это k-й по величине элемент в отсортированном порядке, а не k-й уникальный элемент.

Объяснение: Простой подход — отсортировать массив, а затем выбрать элемент по индексу n - k. Более эффективный подход для больших массивов включает использование min-кучи или алгоритма выбора, такого как Quickselect.

function findKthLargest(nums, k) { // Простой подход: Сортировка nums.sort((a, b) => b - a); // Сортировка по убыванию return nums[k - 1]; // Примечание: Quickselect был бы более эффективным на собеседовании, // но сортировку проще реализовать быстро. } console.log(findKthLargest([3, 2, 1, 5, 6, 4], 2)); // 5 console.log(findKthLargest([3, 2, 3, 1, 2, 4, 5, 5, 6], 4)); // 4

Проверка наличия свойства у объекта

Как проверить, имеет ли объект определенное свойство?

Объяснение: Вы можете использовать оператор in (проверяет собственные и унаследованные свойства), Object.prototype.hasOwnProperty.call(obj, prop) (проверяет только собственные свойства) или просто obj.prop !== undefined (может быть сложно с неопределенными значениями).

const person = { name: 'Eve', age: 28 }; function hasProp(obj, prop) { console.log(`Использование 'in': ${prop in obj}`); console.log(`Использование 'hasOwnProperty': ${Object.prototype.hasOwnProperty.call(obj, prop)}`); } hasProp(person, 'name'); // true, true hasProp(person, 'toString'); // true, false (toString наследуется)

Преобразование целого числа в римское

Напишите функцию для преобразования целого числа в его римское представление.

Объяснение: Создайте соответствие римских цифр и их значений, упорядоченных от наибольшего к наименьшему. Переберите эту карту. Для каждого значения вычитайте его из входного числа столько раз, сколько возможно, добавляя римскую цифру каждый раз.

function intToRoman(num) { const map = { M: 1000, CM: 900, D: 500, CD: 400, C: 100, XC: 90, L: 50, XL: 40, X: 10, IX: 9, V: 5, IV: 4, I: 1 }; let result = ''; for (let key in map) { while (num >= map[key]) { result += key; num -= map[key]; } } return result; } console.log(intToRoman(3)); // III console.log(intToRoman(58)); // LVIII console.log(intToRoman(1994)); // MCMXCIV

Преобразование римской цифры в целое число

Напишите функцию для преобразования римской цифры в целое число.

Объяснение: Создайте карту римских символов в значения. Переберите строку. Если значение текущего символа меньше значения следующего символа (например, 'IV' или 'IX'), вычтите текущее значение; в противном случае добавьте его.

function romanToInt(s) { const map = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 }; let result = 0; for (let i = 0; i < s.length; i++) { const currentVal = map[s[i]]; const nextVal = map[s[i + 1]]; if (nextVal > currentVal) { result -= currentVal; } else { result += currentVal; } } return result; } console.log(romanToInt('III')); // 3 console.log(romanToInt('LVIII')); // 58 console.log(romanToInt('MCMXCIV')); // 1994

Реализация `Set`

Реализуйте базовую структуру данных Set (без использования встроенного Set) с методами add, has, delete и size.

Объяснение: Вы можете использовать объект JavaScript (хеш-карту) в качестве базового хранилища. Ключами будут элементы набора (возможно, вам потребуется подумать о том, как хранить различные типы и обеспечивать уникальность в качестве ключей).

class MySet { constructor() { this.items = {}; this._size = 0; } add(element) { if (!this.has(element)) { this.items[element] = element; // Храним элемент this._size++; return true; } return false; } delete(element) { if (this.has(element)) { delete this.items[element]; this._size--; return true; } return false; } has(element) { return Object.prototype.hasOwnProperty.call(this.items, element); } get size() { return this._size; } } const set = new MySet(); set.add(1); set.add(2); console.log(set.has(1)); // true console.log(set.size); // 2 set.delete(1); console.log(set.has(1)); // false

Треугольник Паскаля

Дано целое число numRows, сгенерируйте первые numRows строк треугольника Паскаля.

Объяснение: В треугольнике Паскаля каждое число является суммой двух чисел непосредственно над ним. Начните с [[1]]. Для каждой последующей строки начинайте и заканчивайте 1. Каждый средний элемент — это сумма элемента с тем же индексом и предыдущего индекса из строки выше.

function generatePascalsTriangle(numRows) { if (numRows === 0) return []; const triangle = [[1]]; for (let i = 1; i < numRows; i++) { const prevRow = triangle[i - 1]; const newRow = [1]; for (let j = 1; j < i; j++) { newRow.push(prevRow[j - 1] + prevRow[j]); } newRow.push(1); triangle.push(newRow); } return triangle; } console.log(generatePascalsTriangle(5)); // [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]

Поиск слова

Даны двумерная доска и слово, найдите, существует ли это слово в сетке. Слово может быть составлено из букв последовательно смежных ячеек, где «смежные» ячейки являются соседними по горизонтали или вертикали.

Объяснение: Это требует поиска в глубину (DFS) с обратным отслеживанием. Переберите каждую ячейку. Если ячейка совпадает с первой буквой слова, начните DFS. Функция DFS проверяет соседей, убеждаясь, что они соответствуют следующей букве и не были посещены в текущем пути. Если путь не удался, выполните откат, сняв отметку с ячейки.

function exist(board, word) { const rows = board.length; const cols = board[0].length; function dfs(r, c, index) { if (index === word.length) return true; // Слово найдено if (r < 0 || c < 0 || r >= rows || c >= cols || board[r][c] !== word[index]) { return false; } const temp = board[r][c]; board[r][c] = '#'; // Отметить как посещенное const found = dfs(r + 1, c, index + 1) || dfs(r - 1, c, index + 1) || dfs(r, c + 1, index + 1) || dfs(r, c - 1, index + 1); board[r][c] = temp; // Откат return found; } for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (board[r][c] === word[0] && dfs(r, c, 0)) { return true; } } } return false; } const board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']]; console.log(exist(board, 'ABCCED')); // true console.log(exist(board, 'SEE')); // true console.log(exist(board, 'ABCB')); // false

Минимальная подстрока-окно

Даны две строки s и t, найдите минимальное окно в s, которое будет содержать все символы в t. Если такого окна в s, которое охватывает все символы в t, нет, верните пустую строку "".

Объяснение: Используйте подход скользящего окна с двумя указателями (left и right) и хеш-картами. Одна карта хранит количество необходимых символов из t. Другая карта хранит количество символов в текущем окне. Расширяйте окно с помощью right. Как только окно содержит все необходимые символы, попробуйте сузить его с помощью left, чтобы найти минимально возможное окно.

function minWindow(s, t) { if (!t || !s || s.length < t.length) return ""; const tMap = {}; for (const char of t) tMap[char] = (tMap[char] || 0) + 1; let required = Object.keys(tMap).length; let formed = 0; const windowMap = {}; let left = 0; let minLen = Infinity; let result = ""; for (let right = 0; right < s.length; right++) { const char = s[right]; windowMap[char] = (windowMap[char] || 0) + 1; if (tMap[char] && windowMap[char] === tMap[char]) { formed++; } while (left <= right && formed === required) { if (right - left + 1 < minLen) { minLen = right - left + 1; result = s.substring(left, right + 1); } const leftChar = s[left]; windowMap[leftChar]--; if (tMap[leftChar] && windowMap[leftChar] < tMap[leftChar]) { formed--; } left++; } } return result; } console.log(minWindow('ADOBECODEBANC', 'ABC')); // 'BANC'

Разворот связного списка

Дана head, голова односвязного списка, разверните список и верните развернутый список.

Объяснение: Вам нужно пройти по списку, изменяя указатель next каждого узла, чтобы он указывал на предыдущий узел. Отслеживайте previous, current и next узлы во время итерации.

class ListNode { constructor(val = 0, next = null) { this.val = val; this.next = next; } } function reverseList(head) { let prev = null; let current = head; let next = null; while (current !== null) { next = current.next; // Сохранить следующий узел current.next = prev; // Развернуть указатель текущего узла prev = current; // Переместить prev на один шаг вперед current = next; // Переместить current на один шаг вперед } return prev; // Новая голова - это последний 'prev' } // Пример: 1->2->3->4->5 становится 5->4->3->2->1

Обнаружение цикла в связном списке

Дана head, голова связного списка, определите, есть ли в связном списке цикл.

Объяснение: Используйте алгоритм «Черепаха и Заяц» Флойда. Используйте два указателя: один движется по одному шагу за раз (slow), а другой — по два шага за раз (fast). Если есть цикл, указатель fast в конечном итоге догонит указатель slow.

class ListNode { constructor(val = 0, next = null) { this.val = val; this.next = next; } } function hasCycle(head) { if (!head || !head.next) return false; let slow = head; let fast = head.next; while (slow !== fast) { if (!fast || !fast.next) return false; // Достигнут конец, цикла нет slow = slow.next; fast = fast.next.next; } return true; // Указатели встретились, цикл существует } // Пример: 3->2->0->-4, где -4 указывает обратно на 2. hasCycle возвращает true.

Реализация `Object.create`

Реализуйте функцию, которая имитирует поведение Object.create(proto).

Объяснение: Object.create создает новый объект, используя существующий объект в качестве прототипа вновь созданного объекта. Это можно сделать, создав временную функцию-конструктор, установив ее прототип в входной proto и затем вернув новый экземпляр этого объекта.

function myObjectCreate(proto) { if (typeof proto !== 'object' && typeof proto !== 'function') { if (proto !== null) { throw new TypeError('Прототип объекта может быть только Object или null: ' + proto); } } function F() {} F.prototype = proto; return new F(); } const person = { isHuman: false, printIntroduction: function() { console.log(`Меня зовут ${this.name}. Я человек? ${this.isHuman}`); } }; const me = myObjectCreate(person); me.name = 'Мэтью'; me.isHuman = true; me.printIntroduction(); // Меня зовут Мэтью. Я человек? true console.log(Object.getPrototypeOf(me) === person); // true

Что такое Hoisting?

Объясните hoisting в JavaScript и приведите пример.

Объяснение: Hoisting — это поведение JavaScript по умолчанию, при котором объявления перемещаются в верхнюю часть текущей области видимости (глобальной или функции) до выполнения кода. Объявления переменных (var) поднимаются и инициализируются значением undefined. Объявления функций полностью поднимаются (как имя, так и тело). let и const поднимаются, но не инициализируются, что приводит к временной мертвой зоне (Temporal Dead Zone).

console.log(myVar); // undefined (var поднимается и инициализируется undefined) // console.log(myLet); // ReferenceError: Невозможно получить доступ к 'myLet' до инициализации (TDZ) myFunc(); // 'Привет!' (Объявление функции полностью поднимается) var myVar = 'Я var'; let myLet = 'Я let'; function myFunc() { console.log('Привет!'); }

Объяснение всплытия и перехвата событий

Объясните всплытие и перехват событий в контексте DOM.

Объяснение: Это две фазы распространения событий в HTML DOM. Фаза перехвата (Capturing Phase): Событие распространяется сверху вниз от window до целевого элемента. Фаза всплытия (Bubbling Phase): Событие распространяется снизу вверх от целевого элемента обратно к window. По умолчанию большинство обработчиков событий регистрируются на фазе всплытия. Вы можете использовать addEventListener(type, listener, useCapture) с useCapture = true для обработки событий на фазе перехвата.

// В HTML-структуре: <div><p><span>Нажми меня</span></p></div> // Если вы нажмете на <span>: // Перехват: window -> document -> html -> body -> div -> p -> span // Всплытие: span -> p -> div -> body -> html -> document -> window /* Пример JS div.addEventListener('click', () => console.log('Нажат div'), true); // Перехват p.addEventListener('click', () => console.log('Нажат p'), false); // Всплытие span.addEventListener('click', () => console.log('Нажат span'), false); // Всплытие // Вывод будет: Нажат div, Нажат span, Нажат p */

Реализация `JSON.parse` вручную (базовая)

Попробуйте реализовать очень базовую версию JSON.parse (обработка простых объектов, массивов, строк, чисел).

Объяснение: Это очень сложная задача в полном объеме, но для живого кодирования вас могут попросить обработать очень упрощенное подмножество. Вам нужно будет проанализировать строку, идентифицируя границы объектов {} и массивов [], пары ключ-значение ("key": value) и базовые типы данных. eval или new Function могут схитрить, но они опасны. Настоящий парсер использовал бы лексер/токенизатор и парсер.

function basicJsonParse(jsonString) { // ВНИМАНИЕ: Использование new Function небезопасно, как eval. // Это упрощенный, НЕБЕЗОПАСНЫЙ пример только для демонстрации. // Реальная реализация требует правильного парсера. try { return (new Function('return ' + jsonString))(); } catch (e) { throw new SyntaxError('Неверный JSON: ' + e.message); } } console.log(basicJsonParse('{"a": 1, "b": ["x", "y"]}')); // { a: 1, b: ['x', 'y'] }

Развертывание глубоко вложенного массива

Напишите функцию для развертывания глубоко вложенного массива.

Объяснение: В отличие от предыдущего развертывания (один уровень), это должно обрабатывать любой уровень вложенности. Рекурсия естественным образом подходит. Переберите массив. Если элемент является массивом, рекурсивно вызовите для него функцию развертывания и объедините результат. В противном случае добавьте элемент.

function deepFlatten(arr) { let flattened = []; arr.forEach(item => { if (Array.isArray(item)) { flattened = flattened.concat(deepFlatten(item)); } else { flattened.push(item); } }); return flattened; // ES2019+ предлагает гораздо более простой способ: // return arr.flat(Infinity); } console.log(deepFlatten([1, [2, [3, 4], 5], 6])); // [1, 2, 3, 4, 5, 6]

Реализовать префиксное дерево (Trie)

Реализуйте структуру данных Trie с методами insert, search и startsWith.

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

class TrieNode { constructor() { this.children = {}; this.isEndOfWord = false; } } class Trie { constructor() { this.root = new TrieNode(); } insert(word) { let node = this.root; for (const char of word) { if (!node.children[char]) { node.children[char] = new TrieNode(); } node = node.children[char]; } node.isEndOfWord = true; } search(word) { let node = this.root; for (const char of word) { if (!node.children[char]) return false; node = node.children[char]; } return node.isEndOfWord; } startsWith(prefix) { let node = this.root; for (const char of prefix) { if (!node.children[char]) return false; node = node.children[char]; } return true; } } const trie = new Trie(); trie.insert('apple'); console.log(trie.search('apple')); // true console.log(trie.search('app')); // false console.log(trie.startsWith('app')); // true

Перемешать массив (Фишер-Йетс)

Напишите функцию для перемешивания массива на месте с использованием алгоритма Фишера-Йетса (или Кнута).

Объяснение: Алгоритм Фишера-Йетса итерирует массив от последнего элемента к первому. В каждой итерации он меняет местами текущий элемент со случайно выбранным элементом из начала массива до текущего элемента (включительно).

function shuffleArray(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; // Поменять элементы местами } return arr; } console.log(shuffleArray([1, 2, 3, 4, 5])); // например, [3, 5, 1, 2, 4]

Композиция функций

Реализуйте функцию compose, которая принимает несколько функций и возвращает новую функцию, применяющую их справа налево.

Объяснение: Композиция функций (f ∘ g)(x) = f(g(x)) применяет одну функцию к результату другой. Общая compose(f, g, h) будет означать f(g(h(x))). Используйте reduceRight для элегантной реализации.

function compose(...funcs) { return function(initialArg) { return funcs.reduceRight((acc, func) => func(acc), initialArg); }; } const add5 = x => x + 5; const multiply3 = x => x * 3; const subtract2 = x => x - 2; const composedFunc = compose(subtract2, multiply3, add5); console.log(composedFunc(10)); // (10 + 5) * 3 - 2 = 15 * 3 - 2 = 45 - 2 = 43

Пайп функций

Реализуйте функцию pipe, которая принимает несколько функций и возвращает новую функцию, применяющую их слева направо.

Объяснение: Аналогично compose, но порядок применения обратный: pipe(f, g, h) означает h(g(f(x))). Используйте reduce для реализации.

function pipe(...funcs) { return function(initialArg) { return funcs.reduce((acc, func) => func(acc), initialArg); }; } const add5 = x => x + 5; const multiply3 = x => x * 3; const subtract2 = x => x - 2; const pipedFunc = pipe(add5, multiply3, subtract2); console.log(pipedFunc(10)); // (10 + 5) * 3 - 2 = 15 * 3 - 2 = 45 - 2 = 43

Задача о размене монет

Учитывая массив номиналов монет и сумму, найдите минимальное количество монет, чтобы составить эту сумму. Предполагается бесконечный запас каждой монеты.

Объяснение: Это классическая задача динамического программирования. Создайте массив dp, где dp[i] хранит минимальное количество монет, необходимое для суммы i. dp[i] = min(dp[i - coin]) + 1 для всех монет.

function coinChange(coins, amount) { const dp = new Array(amount + 1).fill(Infinity); dp[0] = 0; for (let i = 1; i <= amount; i++) { for (const coin of coins) { if (i - coin >= 0) { dp[i] = Math.min(dp[i], dp[i - coin] + 1); } } } return dp[amount] === Infinity ? -1 : dp[amount]; } console.log(coinChange([1, 2, 5], 11)); // 3 (5 + 5 + 1) console.log(coinChange([2], 3)); // -1

Наименьший общий предок (BST)

Учитывая двоичное дерево поиска (BST), найдите наименьшего общего предка (LCA) двух заданных узлов в BST.

Объяснение: LCA — это самый глубокий узел, у которого оба заданных узла являются потомками. В BST вы можете найти его, обходя дерево от корня. Если оба узла меньше текущего узла, идите влево. Если оба больше, идите вправо. Если один меньше, а один больше (или один является текущим узлом), то текущий узел является LCA.

class Node { constructor(val) { this.val = val; this.left = this.right = null; } } function lowestCommonAncestor(root, p, q) { let current = root; while (current) { if (p.val > current.val && q.val > current.val) { current = current.right; } else if (p.val < current.val && q.val < current.val) { current = current.left; } else { return current; // Найден LCA } } return null; // Не должно произойти в действительном BST с p и q } // Пример: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 => LCA это 6

Сериализация и десериализация бинарного дерева

Разработайте алгоритм для сериализации и десериализации бинарного дерева.

Объяснение: Сериализация преобразует дерево в строку или массив. Десериализация восстанавливает дерево. Распространенный способ — прямой обход (DFS). Используйте специальный маркер (например, '#') для пустых узлов, чтобы сохранить структуру.

class Node { constructor(val) { this.val = val; this.left = this.right = null; } } function serialize(root) { const values = []; function dfs(node) { if (!node) { values.push('#'); return; } values.push(node.val); dfs(node.left); dfs(node.right); } dfs(root); return values.join(','); } function deserialize(data) { const values = data.split(','); let index = 0; function buildTree() { if (values[index] === '#') { index++; return null; } const node = new Node(parseInt(values[index])); index++; node.left = buildTree(); node.right = buildTree(); return node; } return buildTree(); } // Пример: 1 -> [2, 3] -> [null, null, 4, 5] // Сериализованный: '1,2,#,#,3,4,#,#,5,#,#'

Реализовать `setInterval` с помощью `setTimeout`

Реализуйте функцию mySetInterval, которая имитирует setInterval, но использует setTimeout рекурсивно.

Объяснение: setInterval запускает функцию многократно каждые N миллисекунд. Это можно достичь, заставив функцию вызывать саму себя с setTimeout после каждого выполнения. Вам также понадобится способ ее очистки (myClearInterval).

function mySetInterval(callback, delay, ...args) { const interval = { timerId: null }; function run() { interval.timerId = setTimeout(() => { callback.apply(this, args); run(); // Запланировать следующий вызов }, delay); } run(); return interval; // Вернуть объект, чтобы разрешить очистку } function myClearInterval(interval) { clearTimeout(interval.timerId); } // Пример использования: let count = 0; const intervalId = mySetInterval(() => { console.log(`Hello: ${++count}`); if (count === 3) myClearInterval(intervalId); }, 500);

Обход графа (BFS & DFS)

Реализуйте поиск в ширину (BFS) и поиск в глубину (DFS) для заданного графа (представление списком смежности).

Объяснение: BFS сначала исследует соседей (используя очередь), в то время как DFS исследует как можно дальше по каждой ветви (используя стек или рекурсию). Отслеживайте посещенные узлы, чтобы избежать циклов и избыточной работы.

const graph = { A: ['B', 'C'], B: ['D'], C: ['E'], D: [], E: ['F'], F: [] }; function bfs(graph, startNode) { const queue = [startNode]; const visited = new Set(); const result = []; visited.add(startNode); while (queue.length > 0) { const node = queue.shift(); result.push(node); for (const neighbor of graph[node]) { if (!visited.has(neighbor)) { visited.add(neighbor); queue.push(neighbor); } } } return result; } function dfs(graph, startNode) { const stack = [startNode]; const visited = new Set(); const result = []; while (stack.length > 0) { const node = stack.pop(); if (!visited.has(node)) { visited.add(node); result.push(node); // Добавить соседей в обратном порядке, чтобы обработать их позже (необязательно) for (let i = graph[node].length - 1; i >= 0; i--) { stack.push(graph[node][i]); } } } return result; } console.log('BFS:', bfs(graph, 'A')); // ['A', 'B', 'C', 'D', 'E', 'F'] console.log('DFS:', dfs(graph, 'A')); // ['A', 'B', 'D', 'C', 'E', 'F']

Поворот изображения (матрица)

Вам дана двумерная матрица n x n, представляющая изображение. Поверните изображение на 90 градусов (по часовой стрелке) на месте.

Объяснение: Распространенный способ достижения этого — сначала транспонировать матрицу (поменять местами matrix[i][j] и matrix[j][i]), а затем перевернуть каждую строку.

function rotate(matrix) { const n = matrix.length; // Транспонирование for (let i = 0; i < n; i++) { for (let j = i; j < n; j++) { [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]]; } } // Перевернуть каждую строку for (let i = 0; i < n; i++) { matrix[i].reverse(); } return matrix; } const matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; console.log(rotate(matrix)); // [[7, 4, 1], [8, 5, 2], [9, 6, 3]]

Спиральный обход матрицы

Учитывая матрицу m x n, верните все элементы матрицы в спиральном порядке.

Объяснение: Используйте четыре указателя для определения границ: top, bottom, left, right. Обходите верхнюю строку, затем правый столбец, затем нижнюю строку, затем левый столбец, сужая границы после каждого обхода, пока left не пересечет right или top не пересечет bottom.

function spiralOrder(matrix) { if (!matrix || matrix.length === 0) return []; const rows = matrix.length, cols = matrix[0].length; const result = []; let top = 0, bottom = rows - 1, left = 0, right = cols - 1; while (top <= bottom && left <= right) { for (let i = left; i <= right; i++) result.push(matrix[top][i]); top++; for (let i = top; i <= bottom; i++) result.push(matrix[i][right]); right--; if (top <= bottom) { for (let i = right; i >= left; i--) result.push(matrix[bottom][i]); bottom--; } if (left <= right) { for (let i = bottom; i >= top; i--) result.push(matrix[i][left]); left++; } } return result; } const matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; console.log(spiralOrder(matrix)); // [1, 2, 3, 6, 9, 8, 7, 4, 5]

Установка нулей в матрице

Учитывая матрицу m x n, если элемент равен 0, установите всю его строку и столбец в 0. Сделайте это на месте.

Объяснение: Наивный подход требует O(m*n) дополнительного пространства. Чтобы сделать это в пространстве O(1) (или O(m+n)), вы можете использовать первую строку и первый столбец для хранения информации о том, какие строки/столбцы нужно обнулить. Вам нужны отдельные флаги для того, нужно ли обнулять саму первую строку/столбец.

function setZeroes(matrix) { const rows = matrix.length, cols = matrix[0].length; let firstColZero = false; for (let r = 0; r < rows; r++) { if (matrix[r][0] === 0) firstColZero = true; for (let c = 1; c < cols; c++) { if (matrix[r][c] === 0) { matrix[r][0] = 0; matrix[0][c] = 0; } } } for (let r = rows - 1; r >= 0; r--) { for (let c = cols - 1; c >= 1; c--) { if (matrix[r][0] === 0 || matrix[0][c] === 0) { matrix[r][c] = 0; } } if (firstColZero) matrix[r][0] = 0; } return matrix; } // Пример: [[1,1,1],[1,0,1],[1,1,1]] => [[1,0,1],[0,0,0],[1,0,1]]

Реализовать `Promise.race`

Реализуйте функцию, которая ведет себя как Promise.race.

Объяснение: Promise.race принимает массив промисов и возвращает один промис. Этот возвращаемый промис устанавливается (разрешается или отклоняется) как только любой из входных промисов устанавливается, с его значением или причиной.

function myPromiseRace(promises) { return new Promise((resolve, reject) => { if (!promises || promises.length === 0) { return; // Или разрешить/отклонить в зависимости от спецификации } promises.forEach(promise => { Promise.resolve(promise).then(resolve, reject); }); }); } const p1 = new Promise((resolve) => setTimeout(resolve, 500, 'one')); const p2 = new Promise((resolve) => setTimeout(resolve, 100, 'two')); myPromiseRace([p1, p2]).then(value => console.log(value)); // 'two'

Паттерн «Одиночка»

Реализуйте паттерн проектирования «Одиночка» (Singleton) в JavaScript.

Объяснение: Паттерн «Одиночка» гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. Этого можно добиться с помощью замыкания для хранения экземпляра.

const Singleton = (function() { let instance; function createInstance() { // Приватные методы и переменные const privateVar = 'I am private'; function privateMethod() { console.log('Private'); } return { // Публичные методы и переменные publicVar: 'I am public', publicMethod: function() { console.log('Public'); }, getPrivate: function() { return privateVar; } }; } return { getInstance: function() { if (!instance) { instance = createInstance(); } return instance; } }; })(); const instance1 = Singleton.getInstance(); const instance2 = Singleton.getInstance(); console.log(instance1 === instance2); // true console.log(instance1.getPrivate()); // 'I am private'

Проверка IP-адреса

Напишите функцию для проверки, является ли заданная строка допустимым IPv4 или IPv6-адресом.

Объяснение: Для IPv4 проверьте 4 числа (0-255), разделенные точками, без ведущих нулей (кроме самого '0'). Для IPv6 проверьте 8 групп из 1-4 шестнадцатеричных цифр, разделенных двоеточиями (обработайте варианты, такие как '::'). Можно использовать регулярные выражения, но ручной парсинг часто более понятен для интервью.

function validIPAddress(IP) { function isIPv4(s) { const parts = s.split('.'); if (parts.length !== 4) return false; for (const part of parts) { if (!/^[0-9]+$/.test(part)) return false; if (part.length > 1 && part.startsWith('0')) return false; const num = parseInt(part); if (num < 0 || num > 255) return false; } return true; } function isIPv6(s) { const parts = s.split(':'); if (parts.length !== 8) return false; // Упрощено: без '::' for (const part of parts) { if (part.length < 1 || part.length > 4) return false; if (!/^[0-9a-fA-F]+$/.test(part)) return false; } return true; } if (IP.includes('.')) return isIPv4(IP) ? 'IPv4' : 'Neither'; if (IP.includes(':')) return isIPv6(IP) ? 'IPv6' : 'Neither'; return 'Neither'; } console.log(validIPAddress('172.16.254.1')); // IPv4 console.log(validIPAddress('2001:0db8:85a3:0000:0000:8a2e:0370:7334')); // IPv6 (Упрощено) console.log(validIPAddress('256.256.256.256')); // Ни то, ни другое

Найти пиковый элемент

Пиковый элемент — это элемент, который строго больше своих соседей. Учитывая входной массив nums, найдите пиковый элемент и верните его индекс.

Объяснение: Поскольку подойдет любой пик, а nums[-1] и nums[n] считаются -Infinity, вы можете использовать модифицированный двоичный поиск. Если nums[mid] меньше nums[mid+1], пик должен существовать справа. В противном случае пик должен существовать слева (или сам mid является пиком).

function findPeakElement(nums) { let left = 0; let right = nums.length - 1; while (left < right) { let mid = Math.floor((left + right) / 2); if (nums[mid] < nums[mid + 1]) { left = mid + 1; // Пик справа } else { right = mid; // Пик это mid или слева } } return left; // 'left' будет индексом пика } console.log(findPeakElement([1, 2, 3, 1])); // 2 (индекс 3) console.log(findPeakElement([1, 2, 1, 3, 5, 6, 4])); // 5 (индекс 6) или 1 (индекс 2)

Подсчет битов

Учитывая целое число n, верните массив ans длиной n + 1 такой, что для каждого i (0 <= i <= n), ans[i] — это количество единиц в двоичном представлении i.

Объяснение: Вы можете решить это с помощью динамического программирования. Обратите внимание на отношение: dp[i] = dp[i >> 1] + (i & 1). Количество единиц в i — это количество единиц в i, сдвинутом вправо (т.е. i/2), плюс 1, если i нечетное.

function countBits(n) { const dp = new Array(n + 1).fill(0); for (let i = 1; i <= n; i++) { dp[i] = dp[i >> 1] + (i & 1); // Или: dp[i] = dp[i & (i - 1)] + 1; (Удаляет последний установленный бит) } return dp; } console.log(countBits(5)); // [0, 1, 1, 2, 1, 2]

Степень двойки

Учитывая целое число n, верните true, если оно является степенью двойки. В противном случае верните false.

Объяснение: Степень двойки в двоичном представлении имеет ровно один бит '1' (например, 1=1, 2=10, 4=100, 8=1000). Хитрый побитовый трюк состоит в том, чтобы проверить, является ли n > 0 и (n & (n - 1)) === 0. Если n является степенью двойки, n-1 будет иметь все биты ниже этой '1' установленными в '1'. Побитовое И дает 0.

function isPowerOfTwo(n) { return n > 0 && (n & (n - 1)) === 0; } console.log(isPowerOfTwo(16)); // true console.log(isPowerOfTwo(1)); // true console.log(isPowerOfTwo(18)); // false

Объединение интервалов

Учитывая массив intervals, где intervals[i] = [starti, endi], объедините все перекрывающиеся интервалы и верните массив неперекрывающихся интервалов.

Объяснение: Сначала отсортируйте интервалы по времени начала. Затем проитерируйте по отсортированным интервалам. Если текущий интервал перекрывается с последним интервалом в списке результатов, объедините их, обновив время окончания последнего интервала. В противном случае добавьте текущий интервал в список результатов.

function mergeIntervals(intervals) { if (intervals.length === 0) return []; intervals.sort((a, b) => a[0] - b[0]); const merged = [intervals[0]]; for (let i = 1; i < intervals.length; i++) { const last = merged[merged.length - 1]; const current = intervals[i]; if (current[0] <= last[1]) { // Перекрытие: объединить last[1] = Math.max(last[1], current[1]); } else { // Нет перекрытия: добавить новый интервал merged.push(current); } } return merged; } console.log(mergeIntervals([[1, 3], [2, 6], [8, 10], [15, 18]])); // [[1, 6], [8, 10], [15, 18]]

Разбиение слова

Учитывая строку s и словарь строк wordDict, верните true, если s может быть сегментирована в последовательность из одного или нескольких слов из словаря, разделенных пробелами.

Объяснение: Используйте динамическое программирование. Создайте логический массив dp, где dp[i] истинно, если подстрока s[0...i-1] может быть сегментирована. dp[i] истинно, если существует j < i такой, что dp[j] истинно и подстрока s[j...i-1] находится в словаре.

function wordBreak(s, wordDict) { const wordSet = new Set(wordDict); const n = s.length; const dp = new Array(n + 1).fill(false); dp[0] = true; // Базовый случай: пустая строка for (let i = 1; i <= n; i++) { for (let j = 0; j < i; j++) { if (dp[j] && wordSet.has(s.substring(j, i))) { dp[i] = true; break; } } } return dp[n]; } console.log(wordBreak('leetcode', ['leet', 'code'])); // true console.log(wordBreak('applepenapple', ['apple', 'pen'])); // true console.log(wordBreak('catsandog', ['cats', 'dog', 'sand', 'and', 'cat'])); // false

Реализовать `Array.prototype.flat`

Реализуйте свою версию Array.prototype.flat, которая создает новый массив со всеми элементами подмассива, рекурсивно объединенными в него до указанной глубины.

Объяснение: Используйте рекурсию. Проитерируйте по массиву. Если элемент является массивом и текущая глубина меньше указанной глубины, рекурсивно вызовите flatten для него. В противном случае добавьте элемент. Обработайте глубину по умолчанию равную 1.

function myFlat(arr, depth = 1) { let flattened = []; arr.forEach(item => { if (Array.isArray(item) && depth > 0) { flattened.push(...myFlat(item, depth - 1)); } else { flattened.push(item); } }); return flattened; } console.log(myFlat([1, [2, [3, 4], 5], 6])); // [2, [3, 4], 5] console.log(myFlat([1, [2, [3, 4], 5], 6], 2)); // [1, 2, 3, 4, 5, 6]

Развернуть слова в строке

Учитывая входную строку s, разверните порядок слов.

Объяснение: «Слово» определяется как последовательность непробельных символов. Обрежьте строку, разделите ее одним или несколькими пробелами, отфильтруйте все пустые строки, образовавшиеся из-за нескольких пробелов, переверните массив и снова объедините его одинарными пробелами.

function reverseWords(s) { return s.trim().split(/\s+/).reverse().join(' '); } console.log(reverseWords('the sky is blue')); // 'blue is sky the' console.log(reverseWords(' hello world ')); // 'world hello' console.log(reverseWords('a good example')); // 'example good a'

Парсер строки запроса

Напишите функцию для парсинга строки запроса URL в объект.

Объяснение: Разделите строку по &. Для каждой части разделите по = для получения ключа и значения. Декодируйте компоненты URI как для ключа, так и для значения. Обработайте случаи, когда ключ может встречаться несколько раз (создайте массив) или не имеет значения.

function parseQueryString(query) { if (query.startsWith('?')) { query = query.substring(1); } const result = {}; if (!query) return result; query.split('&').forEach(pair => { const [key, value] = pair.split('='); const decodedKey = decodeURIComponent(key); const decodedValue = value !== undefined ? decodeURIComponent(value) : true; if (result.hasOwnProperty(decodedKey)) { if (Array.isArray(result[decodedKey])) { result[decodedKey].push(decodedValue); } else { result[decodedKey] = [result[decodedKey], decodedValue]; } } else { result[decodedKey] = decodedValue; } }); return result; } console.log(parseQueryString('?foo=bar&baz=qux&baz=quux&corge')); // { foo: 'bar', baz: ['qux', 'quux'], corge: true }

Использование `Proxy` для валидации

Продемонстрируйте, как использовать объект Proxy для валидации свойств объекта.

Объяснение: Proxy позволяет перехватывать и настраивать операции, выполняемые над объектом. Используйте ловушку set для валидации значений перед их присвоением свойству. Выбросьте ошибку, если валидация не удалась.

const validator = { set: function(obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { throw new TypeError('The age is not an integer'); } if (value < 0 || value > 150) { throw new RangeError('The age seems invalid'); } } // Поведение по умолчанию: установить свойство obj[prop] = value; return true; } }; const person = {}; const personProxy = new Proxy(person, validator); personProxy.age = 30; // OK console.log(personProxy.age); // 30 // personProxy.age = 'thirty'; // Выбрасывает TypeError // personProxy.age = 200; // Выбрасывает RangeError

Переговорные комнаты

Учитывая массив интервалов встреч [[start1, end1], [start2, end2], ...], определите, сможет ли человек посетить все встречи.

Объяснение: Если человек может посетить все встречи, это означает, что никакие две встречи не перекрываются. Чтобы проверить это, отсортируйте интервалы по времени их начала. Затем проитерируйте по отсортированным интервалам и проверьте, начинается ли текущая встреча до окончания предыдущей. Если да, то есть перекрытие, и человек не сможет посетить все встречи.

function canAttendMeetings(intervals) { if (intervals.length < 2) return true; intervals.sort((a, b) => a[0] - b[0]); for (let i = 1; i < intervals.length; i++) { if (intervals[i][0] < intervals[i - 1][1]) { return false; // Обнаружено перекрытие } } return true; } console.log(canAttendMeetings([[0, 30], [5, 10], [15, 20]])); // false (5<30) console.log(canAttendMeetings([[7, 10], [2, 4]])); // true

Реализовать `Promise.any`

Реализуйте функцию, которая ведет себя как Promise.any.

Объяснение: Promise.any принимает массив промисов и возвращает один промис. Этот промис выполняется, как только любой из входных промисов выполняется. Если все входные промисы отклоняются, он отклоняется с AggregateError, содержащим все причины отклонения.

function myPromiseAny(promises) { return new Promise((resolve, reject) => { const numPromises = promises.length; if (numPromises === 0) { reject(new AggregateError([], 'All promises were rejected')); return; } let rejectionCount = 0; const errors = []; promises.forEach((promise, index) => { Promise.resolve(promise) .then(resolve) // Разрешить, как только один выполнится .catch(error => { errors[index] = error; rejectionCount++; if (rejectionCount === numPromises) { reject(new AggregateError(errors, 'All promises were rejected')); } }); }); }); } // Пример: myPromiseAny([Promise.reject('err1'), Promise.resolve('ok')]) -> 'ok' // Пример: myPromiseAny([Promise.reject('err1'), Promise.reject('err2')]) -> AggregateError

Паттерн «Наблюдатель»

Реализуйте паттерн проектирования «Наблюдатель» (Pub/Sub) в JavaScript.

Объяснение: Паттерн «Наблюдатель» определяет зависимость «один ко многим» между объектами. Когда один объект (Субъект) изменяет состояние, все его зависимые объекты (Наблюдатели) автоматически уведомляются и обновляются. Субъект поддерживает список наблюдателей и предоставляет методы для их добавления, удаления и уведомления.

class Subject { constructor() { this.observers = []; } subscribe(observer) { this.observers.push(observer); } unsubscribe(observer) { this.observers = this.observers.filter(obs => obs !== observer); } notify(data) { this.observers.forEach(observer => observer.update(data)); } } class Observer { constructor(name) { this.name = name; } update(data) { console.log(`${this.name} received: ${data}`); } } const subject = new Subject(); const obs1 = new Observer('Наблюдатель 1'); const obs2 = new Observer('Наблюдатель 2'); subject.subscribe(obs1); subject.subscribe(obs2); subject.notify('Что-то произошло!'); // Наблюдатель 1 получил: Что-то произошло! // Наблюдатель 2 получил: Что-то произошло!

Найти повторяющееся число

Учитывая массив целых чисел nums, содержащий n + 1 целых чисел, где каждое целое число находится в диапазоне [1, n] включительно, найдите одно повторяющееся число.

Объяснение: Поскольку числа находятся в [1, n], а размер массива n+1, это гарантирует хотя бы один дубликат. Это можно сопоставить с задачей «Найти цикл в связном списке» (Черепаха и Заяц Флойда). Обработайте значения массива как указатели: индекс -> nums[индекс].

function findDuplicate(nums) { let tortoise = nums[0]; let hare = nums[0]; // Фаза 1: Найти точку пересечения do { tortoise = nums[tortoise]; hare = nums[nums[hare]]; } while (tortoise !== hare); // Фаза 2: Найти вход в цикл let ptr1 = nums[0]; let ptr2 = tortoise; while (ptr1 !== ptr2) { ptr1 = nums[ptr1]; ptr2 = nums[ptr2]; } return ptr1; } console.log(findDuplicate([1, 3, 4, 2, 2])); // 2 console.log(findDuplicate([3, 1, 3, 4, 2])); // 3

Базовый HTML-санитайзер

Напишите базовую функцию для очистки HTML-строки, удаляя потенциально вредоносные теги (например, <script>) и сохраняя безопасные теги (например, <p>, <b>).

Объяснение: Это сложная тема, и реальные санитайзеры очень сложны. Для интервью базовый подход может включать использование регулярных выражений для удаления определенных тегов или разрешение только белого списка тегов. Это небезопасно для использования в продакшене.

function basicSanitize(html) { // ВНИМАНИЕ: Это очень простой и небезопасный пример. // НЕ ИСПОЛЬЗУЙТЕ это в продакшене. Используйте библиотеку, такую как DOMPurify. // Удалить теги script let sanitized = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); // Пример: Удалить атрибуты onclick (очень базовый) sanitized = sanitized.replace(/\s(on\w+)=['"]?[^'"]*['"]?/gi, ''); return sanitized; } console.log(basicSanitize('<p>Hello <script>alert("XSS")</script> <b onclick="danger()">World</b></p>')); // <p>Hello <b >World</b></p>

Расстояние редактирования

Учитывая две строки word1 и word2, верните минимальное количество операций (вставка, удаление или замена), необходимых для преобразования word1 в word2.

Объяснение: Это классическая задача динамического программирования (расстояние Левенштейна). Создайте двумерный массив dp, где dp[i][j] — это расстояние редактирования между первыми i символами word1 и первыми j символами word2. Рекуррентное соотношение включает рассмотрение стоимости вставки, удаления и замены.

function minDistance(word1, word2) { const m = word1.length, n = word2.length; const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0)); for (let i = 0; i <= m; i++) dp[i][0] = i; for (let j = 0; j <= n; j++) dp[0][j] = j; for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { const cost = word1[i - 1] === word2[j - 1] ? 0 : 1; dp[i][j] = Math.min( dp[i - 1][j] + 1, // Удаление dp[i][j - 1] + 1, // Вставка dp[i - 1][j - 1] + cost // Замена/Совпадение ); } } return dp[m][n]; } console.log(minDistance('horse', 'ros')); // 3 console.log(minDistance('intention', 'execution')); // 5

Наибольшая возрастающая подпоследовательность (LIS)

Учитывая целочисленный массив nums, верните длину самой длинной строго возрастающей подпоследовательности.

Объяснение: Используйте динамическое программирование. Пусть dp[i] будет длиной LIS, заканчивающейся на индексе i. Чтобы вычислить dp[i], найдите все j < i такие, что nums[j] < nums[i], и возьмите dp[i] = 1 + max(dp[j]). Конечный ответ — это максимальное значение в массиве dp.

function lengthOfLIS(nums) { if (nums.length === 0) return 0; const n = nums.length; const dp = new Array(n).fill(1); let maxLength = 1; for (let i = 1; i < n; i++) { for (let j = 0; j < i; j++) { if (nums[i] > nums[j]) { dp[i] = Math.max(dp[i], dp[j] + 1); } } maxLength = Math.max(maxLength, dp[i]); } return maxLength; } console.log(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18])); // 4 (2, 3, 7, 101)

Задача N ферзей

Головоломка N ферзей — это задача размещения N шахматных ферзей на доске N×N таким образом, чтобы ни один из двух ферзей не угрожал друг другу. Учитывая N, верните одно допустимое решение (или все).

Объяснение: Используйте метод проб и ошибок (backtracking). Размещайте ферзей построчно. Для каждой строки попробуйте разместить ферзя в каждом столбце. Перед размещением проверьте, является ли предложенная позиция безопасной (не атакована ферзями из предыдущих строк). Если безопасно, разместите его и рекурсивно вызовите для следующей строки. Если рекурсия не удалась, вернитесь назад (удалите ферзя) и попробуйте следующий столбец.

function solveNQueens(n) { const results = []; const board = Array(n).fill('.').map(() => Array(n).fill('.')); function isSafe(row, col) { for (let i = 0; i < row; i++) if (board[i][col] === 'Q') return false; // Проверить столбец for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) if (board[i][j] === 'Q') return false; // Проверить диагональ вверх-влево for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) if (board[i][j] === 'Q') return false; // Проверить диагональ вверх-вправо return true; } function backtrack(row) { if (row === n) { results.push(board.map(r => r.join(''))); return; } for (let col = 0; col < n; col++) { if (isSafe(row, col)) { board[row][col] = 'Q'; backtrack(row + 1); board[row][col] = '.'; // Откат } } } backtrack(0); return results; } console.log(solveNQueens(4)); // [ [ '.Q..', '...Q', 'Q...', '..Q.' ], [ '..Q.', 'Q...', '...Q', '.Q..' ] ]

Использование `WeakMap` для приватных данных

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

Объяснение: WeakMap позволяет связывать данные с объектом таким образом, чтобы это не препятствовало сборке мусора, если на объект больше нет ссылок. Это полезно для создания «приватных» членов в классах JavaScript до того, как собственные приватные поля стали широко доступны, или для конкретных случаев использования.

const privateData = new WeakMap(); class Person { constructor(name, age) { privateData.set(this, { name: name, age: age }); } getName() { return privateData.get(this).name; } getAge() { return privateData.get(this).age; } celebrateBirthday() { privateData.get(this).age++; } } const person = new Person('Alice', 30); console.log(person.getName()); // Alice person.celebrateBirthday(); console.log(person.getAge()); // 31 // console.log(person.name); // undefined - 'name' не является публичным свойством // console.log(privateData.get(person)); // Доступно, но не напрямую через 'person.'

Реализовать `Promise.allSettled`

Реализуйте функцию, которая ведет себя как Promise.allSettled.

Объяснение: Promise.allSettled принимает массив промисов и возвращает один промис. Этот промис выполняется, когда все входные промисы были завершены (либо выполнены, либо отклонены). Значением выполнения является массив объектов, каждый из которых описывает результат одного промиса ({status: 'fulfilled', value: ...} или {status: 'rejected', reason: ...}).

function myPromiseAllSettled(promises) { return new Promise((resolve) => { const numPromises = promises.length; const results = []; let settledCount = 0; if (numPromises === 0) { resolve([]); return; } promises.forEach((promise, index) => { Promise.resolve(promise) .then(value => { results[index] = { status: 'fulfilled', value: value }; }) .catch(reason => { results[index] = { status: 'rejected', reason: reason }; }) .finally(() => { settledCount++; if (settledCount === numPromises) { resolve(results); } }); }); }); } // Пример: myPromiseAllSettled([Promise.resolve(1), Promise.reject('err')]) // -> [{status: 'fulfilled', value: 1}, {status: 'rejected', reason: 'err'}]

Найти медиану двух отсортированных массивов

Учитывая два отсортированных массива nums1 и nums2 размеров m и n соответственно, верните медиану двух отсортированных массивов.

Объяснение: Это сложная задача, часто решаемая с помощью двоичного поиска по меньшему массиву. Цель состоит в том, чтобы разделить оба массива так, чтобы все элементы с левой стороны были меньше или равны всем элементам с правой стороны, а количество элементов слева равнялось (или было на один больше) количеству элементов справа. Медиану затем можно вычислить из граничных элементов.

function findMedianSortedArrays(nums1, nums2) { if (nums1.length > nums2.length) [nums1, nums2] = [nums2, nums1]; // Убедиться, что nums1 меньше const m = nums1.length, n = nums2.length; let low = 0, high = m; while (low <= high) { const partitionX = Math.floor((low + high) / 2); const partitionY = Math.floor((m + n + 1) / 2) - partitionX; const maxX = (partitionX === 0) ? -Infinity : nums1[partitionX - 1]; const minX = (partitionX === m) ? Infinity : nums1[partitionX]; const maxY = (partitionY === 0) ? -Infinity : nums2[partitionY - 1]; const minY = (partitionY === n) ? Infinity : nums2[partitionY]; if (maxX <= minY && maxY <= minX) { if ((m + n) % 2 === 0) { return (Math.max(maxX, maxY) + Math.min(minX, minY)) / 2; } else { return Math.max(maxX, maxY); } } else if (maxX > minY) { high = partitionX - 1; } else { low = partitionX + 1; } } throw new Error('Входные массивы не отсортированы'); } console.log(findMedianSortedArrays([1, 3], [2])); // 2.0 console.log(findMedianSortedArrays([1, 2], [3, 4])); // 2.5

Решатель судоку

Напишите программу для решения головоломки Судоку, заполняя пустые ячейки.

Объяснение: Используйте метод проб и ошибок (backtracking). Найдите пустую ячейку. Попробуйте заполнить ее числами от 1 до 9. Для каждого числа проверьте, является ли оно допустимым (не нарушает ли правила Судоку). Если допустимо, рекурсивно вызовите решатель. Если рекурсия возвращает true, решение найдено. В противном случае вернитесь назад (сбросьте ячейку) и попробуйте следующее число.

function solveSudoku(board) { function isValid(row, col, numStr) { for (let i = 0; i < 9; i++) { if (board[row][i] === numStr) return false; // Проверить строку if (board[i][col] === numStr) return false; // Проверить столбец const boxRow = 3 * Math.floor(row / 3) + Math.floor(i / 3); const boxCol = 3 * Math.floor(col / 3) + i % 3; if (board[boxRow][boxCol] === numStr) return false; // Проверить блок } return true; } function backtrack() { for (let r = 0; r < 9; r++) { for (let c = 0; c < 9; c++) { if (board[r][c] === '.') { for (let n = 1; n <= 9; n++) { const numStr = String(n); if (isValid(r, c, numStr)) { board[r][c] = numStr; if (backtrack()) return true; board[r][c] = '.'; // Откат } } return false; // Допустимое число не найдено } } } return true; // Доска решена } backtrack(); return board; } // Пример требует доски 9x9 с '.' для пустых ячеек.

Реализовать базовый паттерн Middleware

Реализуйте простой паттерн Middleware, часто встречающийся в веб-фреймворках.

Объяснение: Функции Middleware обрабатывают запрос до того, как он достигнет конечного обработчика. Каждое middleware может изменить запрос/ответ или передать управление следующему next middleware. Создайте класс или объект, который управляет списком функций middleware и выполняет их последовательно.

class App { constructor() { this.middlewares = []; } use(fn) { this.middlewares.push(fn); } handle(request) { let index = 0; const next = () => { if (index < this.middlewares.length) { const middleware = this.middlewares[index++]; middleware(request, next); } }; next(); } } const app = new App(); app.use((req, next) => { console.log('1: Логирование...'); req.logged = true; next(); }); app.use((req, next) => { console.log('2: Аутентификация...'); req.authed = true; next(); }); app.use((req, next) => { console.log('3: Конечный обработчик:', req); }); app.handle({ data: 'некоторый запрос' }); // 1: Логирование... // 2: Аутентификация... // 3: Конечный обработчик: { data: 'некоторый запрос', logged: true, authed: true }

Обнаружение цикла в ориентированном графе

Учитывая ориентированный граф, напишите функцию для определения, содержит ли он цикл.

Объяснение: Используйте поиск в глубину (DFS). Поддерживайте два набора: visiting (узлы, находящиеся в текущем стеке рекурсии) и visited (полностью исследованные узлы). Если вы встретите узел из набора visiting во время DFS, вы обнаружили обратное ребро, что означает наличие цикла.

function hasCycleDirected(graph) { const visiting = new Set(); const visited = new Set(); const nodes = Object.keys(graph); function dfs(node) { visiting.add(node); for (const neighbor of graph[node]) { if (visiting.has(neighbor)) return true; // Цикл обнаружен if (!visited.has(neighbor)) { if (dfs(neighbor)) return true; } } visiting.delete(node); visited.add(node); return false; } for (const node of nodes) { if (!visited.has(node)) { if (dfs(node)) return true; } } return false; } const cyclicGraph = { A: ['B'], B: ['C'], C: ['A'] }; const acyclicGraph = { A: ['B', 'C'], B: ['D'], C: ['E'], D: [], E: [] }; console.log(hasCycleDirected(cyclicGraph)); // true console.log(hasCycleDirected(acyclicGraph)); // false

Реализовать поведение `Object.freeze`

Объясните Object.freeze и реализуйте его (поверхностную) версию.

Объяснение: Object.freeze предотвращает добавление новых свойств, удаление существующих свойств и изменение существующих свойств или их перечисляемости, настраиваемости или записываемости. Он делает объект неизменяемым (поверхностно). Вы можете реализовать его, используя Object.preventExtensions и Object.defineProperty, чтобы сделать существующие свойства незаписываемыми и ненастраиваемыми.

function myFreeze(obj) { Object.preventExtensions(obj); // Запретить добавление новых свойств Object.keys(obj).forEach(key => { Object.defineProperty(obj, key, { writable: false, configurable: false }); }); return obj; } const obj = { a: 1, b: 2 }; myFreeze(obj); // obj.c = 3; // Терпит неудачу беззвучно (или выбрасывает ошибку в строгом режиме) // delete obj.a; // Терпит неудачу беззвучно (или выбрасывает ошибку в строгом режиме) // obj.a = 10; // Терпит неудачу беззвучно (или выбрасывает ошибку в строгом режиме) console.log(obj); // { a: 1, b: 2 }

Использование `requestAnimationFrame` для плавной анимации

Объясните requestAnimationFrame и покажите, как использовать его для простой анимации.

Объяснение: requestAnimationFrame (rAF) сообщает браузеру, что вы хотите выполнить анимацию, и запрашивает, чтобы браузер вызвал указанную функцию для обновления анимации перед следующей перерисовкой. Это более эффективно и плавно, чем использование setTimeout или setInterval для анимации, так как он синхронизируется с частотой обновления браузера и приостанавливается, когда вкладка не видна.

// HTML: <div id='box' style='width: 50px; height: 50px; background: red; position: absolute; left: 0px;'></div> const box = document.getElementById('box'); let position = 0; let startTime = null; function animate(currentTime) { if (!startTime) startTime = currentTime; const elapsedTime = currentTime - startTime; // Переместить на 100px за 2 секунды (50px/сек) position = (elapsedTime / 2000) * 100; if (position < 200) { // Продолжать анимацию, пока не достигнет 200px box.style.left = position + 'px'; requestAnimationFrame(animate); } else { box.style.left = '200px'; } } // Запустить анимацию // requestAnimationFrame(animate); // Раскомментировать, чтобы запустить в браузере console.log('Используйте requestAnimationFrame для анимации в браузере.');

Реализовать `Array.prototype.some`

Реализуйте свою версию Array.prototype.some.

Объяснение: some проверяет, проходит ли хотя бы один элемент в массиве тест, реализованный предоставленной функцией. Он возвращает true, если находит элемент, для которого обратный вызов возвращает true; в противном случае он возвращает false.

function mySome(arr, callback, thisArg) { for (let i = 0; i < arr.length; i++) { if (i in arr) { // Обработка разреженных массивов if (callback.call(thisArg, arr[i], i, arr)) { return true; } } } return false; } const arr = [1, 2, 3, 4, 5]; const hasEven = mySome(arr, x => x % 2 === 0); console.log(hasEven); // true

Реализовать `Array.prototype.every`

Реализуйте свою версию Array.prototype.every.

Объяснение: every проверяет, проходят ли все элементы в массиве тест, реализованный предоставленной функцией. Он возвращает true, если все элементы проходят (или если массив пуст), и false, если какой-либо элемент не проходит.

function myEvery(arr, callback, thisArg) { for (let i = 0; i < arr.length; i++) { if (i in arr) { if (!callback.call(thisArg, arr[i], i, arr)) { return false; } } } return true; } const arr1 = [1, 30, 39, 29, 10, 13]; const isBelow40 = myEvery(arr1, x => x < 40); console.log(isBelow40); // true const arr2 = [1, 30, 39, 29, 10, 45]; const isBelow40_2 = myEvery(arr2, x => x < 40); console.log(isBelow40_2); // false

Реализовать `Array.prototype.findIndex`

Реализуйте свою версию Array.prototype.findIndex.

Объяснение: findIndex возвращает индекс первого элемента в массиве, который удовлетворяет предоставленной тестовой функции. В противном случае он возвращает -1, указывая, что ни один элемент не прошел тест.

function myFindIndex(arr, callback, thisArg) { for (let i = 0; i < arr.length; i++) { if (i in arr) { if (callback.call(thisArg, arr[i], i, arr)) { return i; } } } return -1; } const arr = [5, 12, 8, 130, 44]; const isLargeNumber = (element) => element > 13; console.log(myFindIndex(arr, isLargeNumber)); // 3

Что такое Web Workers?

Объясните, что такое Web Workers и их основное назначение.

Объяснение: Web Workers — это способ для веб-контента запускать скрипты в фоновых потоках. Основное назначение — разгрузить длительные или вычислительно интенсивные задачи из основного потока (который обрабатывает обновления пользовательского интерфейса и взаимодействия с пользователем), тем самым предотвращая зависание или «замораживание» пользовательского интерфейса. Воркеры обмениваются данными с основным потоком посредством передачи сообщений (postMessage и onmessage).

// main.js /* if (window.Worker) { const myWorker = new Worker('worker.js'); myWorker.postMessage([5, 3]); // Отправить данные воркеру myWorker.onmessage = function(e) { console.log('Результат от воркера:', e.data); } } else { console.log('Ваш браузер не поддерживает Web Workers.'); } */ // worker.js /* self.onmessage = function(e) { console.log('Сообщение получено от основного скрипта'); const result = e.data[0] * e.data[1]; console.log('Отправка результата обратно в основной скрипт'); self.postMessage(result); } */ console.log('Web Workers запускают скрипты в фоновых потоках.');

Способы декодирования

Сообщение, содержащее буквы от A до Z, может быть закодировано в числа с использованием отображения 'A' -> 1, 'B' -> 2, ..., 'Z' -> 26. Учитывая строку s, содержащую только цифры, верните количество способов ее декодирования.

Объяснение: Используйте динамическое программирование. Пусть dp[i] будет количеством способов декодирования s[0...i-1]. dp[i] зависит от dp[i-1] (если s[i-1] является допустимым 1-значным кодом) и dp[i-2] (если s[i-2...i-1] является допустимым 2-значным кодом).

function numDecodings(s) { if (!s || s[0] === '0') return 0; const n = s.length; const dp = new Array(n + 1).fill(0); dp[0] = 1; dp[1] = 1; for (let i = 2; i <= n; i++) { const oneDigit = parseInt(s.substring(i - 1, i)); const twoDigits = parseInt(s.substring(i - 2, i)); if (oneDigit >= 1) { dp[i] += dp[i - 1]; } if (twoDigits >= 10 && twoDigits <= 26) { dp[i] += dp[i - 2]; } } return dp[n]; } console.log(numDecodings('12')); // 2 ('AB' или 'L') console.log(numDecodings('226')); // 3 ('BBF', 'BZ', 'VF') console.log(numDecodings('06')); // 0

Побитовое сложение (без `+`)

Напишите функцию для сложения двух целых чисел без использования оператора +.

Объяснение: Используйте побитовые операторы. Сумма может быть вычислена как a ^ b (сумма без переноса), а перенос может быть вычислен как (a & b) << 1. Повторяйте этот процесс, пока перенос не станет равным 0.

function getSum(a, b) { while (b !== 0) { const carry = (a & b) << 1; // Вычислить перенос a = a ^ b; // Вычислить сумму без переноса b = carry; // Перенос становится новым 'b' } return a; } console.log(getSum(3, 5)); // 8 console.log(getSum(-2, 3)); // 1

Проверка на палиндром (число)

Учитывая целое число x, верните true, если x является палиндромом, и false в противном случае.

Объяснение: Отрицательное число не является палиндромом. Преобразуйте число в строку и проверьте, является ли строка палиндромом (читается одинаково вперед и назад). Альтернативно, переверните число математически и сравните.

function isPalindromeNumber(x) { if (x < 0) return false; // Строковый подход // return String(x) === String(x).split('').reverse().join(''); // Математический подход let original = x; let reversed = 0; while (x > 0) { reversed = reversed * 10 + (x % 10); x = Math.floor(x / 10); } return original === reversed; } console.log(isPalindromeNumber(121)); // true console.log(isPalindromeNumber(-121)); // false console.log(isPalindromeNumber(10)); // false

Паттерн «Фабрика»

Реализуйте паттерн проектирования «Фабрика» в JavaScript.

Объяснение: Паттерн «Фабрика» предоставляет интерфейс для создания объектов в суперклассе, но позволяет подклассам изменять тип создаваемых объектов. Он полезен для создания объектов без указания точного класса.

class Car { constructor(options) { this.type = 'Car'; this.doors = options.doors || 4; } } class Truck { constructor(options) { this.type = 'Truck'; this.bedSize = options.bedSize || 'long'; } } class VehicleFactory { createVehicle(options) { if (options.vehicleType === 'car') { return new Car(options); } else if (options.vehicleType === 'truck') { return new Truck(options); } else { throw new Error('Неизвестный тип транспортного средства'); } } } const factory = new VehicleFactory(); const car = factory.createVehicle({ vehicleType: 'car', doors: 2 }); const truck = factory.createVehicle({ vehicleType: 'truck', bedSize: 'short' }); console.log(car); // Car { type: 'Car', doors: 2 } console.log(truck); // Truck { type: 'Truck', bedSize: 'short' }

Объяснить `Symbol` и вариант использования

Объясните, что такое Symbol в JavaScript, и приведите пример использования, например, для создания «приватных» свойств или уникальных ключей объекта.

Объяснение: Symbol — это примитивный тип данных, введенный в ES6. Его экземпляры уникальны и неизменяемы. Они часто используются в качестве ключей для свойств объекта, чтобы избежать конфликтов имен между различными библиотеками или для создания псевдоприватных свойств (хотя и не совсем приватных, они недоступны через обычную итерацию или Object.keys).

const _privateName = Symbol('name'); const _privateMethod = Symbol('greet'); class Person { constructor(name) { this[_privateName] = name; } [_privateMethod]() { console.log(`Привет, меня зовут ${this[_privateName]}`); } introduce() { this[_privateMethod](); } } const p = new Person('Боб'); p.introduce(); // Привет, меня зовут Боб console.log(Object.keys(p)); // [] - Символы не включаются console.log(p[_privateName]); // Боб (Доступно, если у вас есть Символ)

Преобразование `arguments` в реальный массив

Как можно преобразовать объект arguments (доступный в не-стрелочных функциях) в реальный массив JavaScript?

Объяснение: Объект arguments выглядит как массив, но таковым не является; ему не хватает таких методов, как map, filter и т.д. Вы можете преобразовать его с помощью:

  1. Array.from(arguments) (ES6)
  2. [...arguments] (синтаксис расширения ES6)
  3. Array.prototype.slice.call(arguments) (старый способ)
function sumAll() { // 1. Array.from const args1 = Array.from(arguments); console.log('Использование Array.from:', args1.reduce((a, b) => a + b, 0)); // 2. Spread Syntax const args2 = [...arguments]; console.log('Использование Spread:', args2.reduce((a, b) => a + b, 0)); // 3. Slice const args3 = Array.prototype.slice.call(arguments); console.log('Использование Slice:', args3.reduce((a, b) => a + b, 0)); } sumAll(1, 2, 3, 4); // Использование Array.from: 10 // Использование Spread: 10 // Использование Slice: 10