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()関数とスプレッド構文 (...) を使用して、配列要素を引数として渡すこともできます。

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までの数値を表示するプログラムを作成してください。ただし、3の倍数の場合は数値の代わりに「Fizz」と表示し、5の倍数の場合は「Buzz」と表示します。3と5の両方の倍数である数値の場合は、「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は一意の値をのみ格納します。配列をSetに変換し、その後配列に戻すことができます。

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

アナグラムチェック

2つの文字列が互いにアナグラムであるかどうかをチェックする関数を作成してください。

説明: アナグラムとは、別の単語の文字を並べ替えて作られた単語のことです。チェックするには、文字列をクリーンアップし、ソートし、比較します。クリーンアップには、非英数字の削除と一貫した大文字小文字への変換が含まれます。

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

ネストされた配列をフラット化する

ネストされた配列をフラット化する関数を作成してください。簡単にするために、ネストは1レベルのみとします。

説明: 1レベルのネストの場合、スプレッド演算子またはflat()メソッド(ES2019)と一緒にArray.prototype.concat()を使用できます。

function flattenArray(arr) { // flat() を使用(ES2019+ がOKならよりシンプル) // 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'

Two Sum 問題

整数の配列numsと整数targetが与えられた場合、合計がtargetになる2つの数値のインデックスを返します。

説明: 一般的なアプローチは、ハッシュマップ(または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

シンプルなキューの実装

enqueue(最後に追加)とdequeue(先頭から削除)メソッドを持つキューデータ構造を実装してください。

説明: キューはFirst-In, First-Out(FIFO)の原則に従います。配列は、enqueueにはpushdequeueにはshiftを使用して使用できます。

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

シンプルなスタックの実装

push(上に追加)とpop(上から削除)メソッドを持つスタックデータ構造を実装してください。

説明: スタックはLast-In, First-Out(LIFO)の原則に従います。配列は、pushpopメソッドを使用して使用できます。

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

シーケンス内の欠落している数値を見つける

0, 1, 2, ..., nから取得されたn個の異なる数値を含む配列が与えられた場合、配列から欠落している数値を見つけます。

説明: 0からnまでの数値の合計は、n*(n+1)/2の式を使用して計算できます。配列要素の実際の合計を計算できます。これら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

デバウンス関数

デバウンス関数を実装してください。デバウンスは、関数が呼び出されずに一定の時間が経過するまで、関数が再度呼び出されないようにします。

説明: setTimeoutclearTimeoutを使用します。デバウンス関数が呼び出されるたびに、以前のタイムアウトをクリアし、新しいタイムアウトを設定します。実際の関数呼び出しは、タイムアウトが完了したときにのみ行われます。

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秒後に実際に実行される。

スロットル関数

スロットル関数を実装してください。スロットルは、関数が指定された時間間隔内で最大1回だけ呼び出されるようにします。

説明: 呼び出しが許可されているかどうかを示すフラグを使用します。呼び出されたとき、許可されていれば関数を実行し、フラグを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); // 1秒に1回までしかログが出力されない。

カリー化関数

2つの引数を持つ関数を受け取り、そのカリー化されたバージョンを返す関数を作成してください。

説明: カリー化は、複数の引数を持つ関数を、それぞれが単一の引数を受け取る関数のシーケンスに変換します。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

2つの配列を結合する

2つの配列を1つに結合するにはどうすればよいですか?

説明: スプレッド構文 (...) またはArray.prototype.concat()を使用します。

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

グローバルスコープにおける「this」を説明する

ブラウザのグローバルスコープでは、thisは何を指しますか?

説明: グローバル実行コンテキスト(関数の外側)では、thisはグローバルオブジェクトを指し、Webブラウザでは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); // 'this'が継承されているため、42を出力 }, 100); } new MyClass();

`let`、`const`、`var`の違い

letconstvarの主な違いは何ですか?

説明: varは関数スコープで巻き上げられます。letconstはブロックスコープで巻き上げられますが、宣言されるまで「一時的なデッドゾーン」にあります。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とは何か?

JavaScriptにおけるPromiseとは何かを説明してください。

説明: Promiseは、非同期操作の最終的な完了(または失敗)とその結果の値を表すオブジェクトです。pending、fulfilled、rejectedの3つの状態のいずれかになります。

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`の使用

asyncawaitはPromiseとの連携をどのように簡素化しますか?

説明: async/awaitはPromiseのシンタックスシュガーを提供し、非同期コードを同期コードのように見せて動作させます。async関数は常に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'

配列におけるスプレッド演算子

スプレッド演算子は配列でどのように使用されますか?

説明: イテラブル(配列など)が、ゼロ個以上の引数または要素が期待される場所で展開されることを可能にします。コピー、結合、要素の追加に役立ちます。

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

オブジェクトにおけるスプレッド演算子

スプレッド演算子はオブジェクトでどのように使用されますか?

説明: オブジェクトのプロパティのコピーと結合を可能にします。

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の基本的なバージョンをどのように実装できますか?

説明: applycallと似ていますが、引数を配列として受け取ります。

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はシングルスレッドですが、イベントループを使用して同時実行性を実現しています。コールスタックは同期コードを処理します。Web APIは非同期操作(setTimeout、fetchなど)を処理します。非同期操作が完了すると、そのコールバックはコールバックキュー(またはPromiseの場合はマイクロタスクキュー)に移動します。イベントループは、コールスタックが空であるかどうかを常にチェックし、空であれば、キューから次のコールバックをスタックに移動して実行します。

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

マージソート

マージソートアルゴリズムを実装してください。

説明: マージソートは分割統治アルゴリズムです。入力配列を2つの半分に分割し、両方の半分に対して自身を呼び出し、その後、2つのソートされた半分をマージします。マージ関数が重要です。これは、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 > length の場合を処理 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]

2つの配列の共通部分を見つける

2つの配列が与えられた場合、それらの共通部分(両方に共通する要素)を計算する関数を作成してください。

説明: 効率的なアプローチは、一方の配列をSetに変換して平均O(1)の時間で検索できるようにすることです。次に、2番目の配列を反復処理し、各要素がSetに存在するかどうかをチェックします。一致するものを収集します。

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を最後に移動する関数を作成してください。

説明: 「2つのポインター」または「スノーボール」アプローチを使用します。1つのポインターは、次の非ゼロ要素が配置される位置を追跡します。配列を反復処理します。要素が非ゼロの場合、それをポインターの位置に配置し、ポインターをインクリメントします。最後に、残りをゼロで埋めます。

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が与えられた場合、最大の合計を持つ連続する部分配列(少なくとも1つの数値を含む)を見つけて、その合計を返します。

説明: カデインのアルゴリズムは、これを解決するための効率的な方法です。配列を反復処理し、現在の位置で終了する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]から)

文字列の順列

与えられた文字列のすべての順列を生成する関数を作成してください。

説明: これは通常、再帰とバックトラックを使用して解決されます。各文字について、それを固定し、文字列の残りの部分の順列を再帰的に生成します。基本ケース:文字列が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']

最大公約数(GCD)

2つの数値の最大公約数(GCD)を見つける関数を作成してください。

説明: ユークリッドの互除法は効率的な方法です。bが0の場合、aがGCDです。そうでない場合、abのGCDは、ba % babで割った余り)のGCDと同じです。

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

最小公倍数(LCM)

2つの数値の最小公倍数(LCM)を見つける関数を作成してください。

説明: LCMは、GCDを使用して次の式で計算できます: LCM(a, b) = |a * b| / GCD(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はPromiseの配列を受け取り、すべての入力Promiseが解決されたときに解決する単一のPromiseを返します。または、いずれかの入力Promiseが拒否された場合に拒否されます。解決された値は、解決された値の配列です。

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日目の所定の株の価格です。1つの株を買う日と、将来の異なる日にその株を売る日を選択することで、利益を最大化したいと考えています。最大の利益を返してください。

説明: 価格を反復処理し、これまでに遭遇した最小価格(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が与えられた場合、すべての要素は2回出現しますが、1つだけ異なります。その単一の要素を見つけてください。

説明: これを解決する非常に効率的な方法は、XORビット演算子 (^) を使用することです。数値とそれ自身をXORすると0になります。数値と0をXORすると、その数値自体になります。配列内のすべての数値を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

多数決要素

サイズnの配列numsが与えられた場合、多数決要素を返します。多数決要素は、⌊n / 2⌋回より多く出現する要素です。

説明: ボイヤー・ムーア投票アルゴリズムは効率的な方法です。candidatecountを初期化します。配列を反復処理します。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[i]nums[i]を除くnumsのすべての要素の積に等しい配列answerを返します。除算演算子を使用しないでください。

説明: これは、プレフィックス積とサフィックス積を計算することで解決できます。インデックス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'(水)の2Dグリッドマップが与えられた場合、島の数を数えます。島は水に囲まれ、水平または垂直に隣接する陸地が連結されて形成されます。

説明: グリッドの各セルを反復処理します。'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) キャッシュのデータ構造を設計し、実装してください。getput操作をサポートする必要があります。

説明: LRUキャッシュは、容量に達したときに最も最近使用されていない項目を削除します。一般的な実装では、O(1)の検索にはMap(またはハッシュマップ)を、O(1)の「最近使用された」ステータスの更新には二重連結リストを使用します。JavaScriptのMapは挿入順序を維持するため、基本的な実装を簡素化できます。

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の2つの端点は(i, ai)(i, 0)にあります。コンテナが最も多くの水を含むように、x軸とともにコンテナを形成する2つの線を見つけてください。

説明: 2つのポインターのアプローチを使用します。一方のポインターを先頭に、もう一方のポインターを末尾に置きます。面積を計算します。面積は短い線によって制限されます。より大きな面積を見つける可能性がある場合、短い線を指しているポインターを内側に移動します。

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

n個の整数を含む配列numsが与えられた場合、a + b + c = 0となるnums内の要素a, b, cはありますか?合計がゼロになる配列内のすべての一意の3つ組を見つけてください。

説明: まず、配列をソートします。次に、1つのポインターiで配列を反復処理します。各iについて、さらに2つのポインター、lefti+1から開始)とright(末尾から開始)を使用して、合計が-nums[i]になる2つの数値を見つけます。一意の3つ組を確保するために重複を処理します。

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

2つのソート済みリストをマージする(連結リスト)

2つのソート済み連結リストをマージし、新しいソート済みリストとして返します。新しいリストは、最初の2つのリストのノードを結合して作成する必要があります。

説明: ダミーヘッドノードを使用してプロセスを簡素化します。ポインターを使用して新しいリストを構築します。両方の入力リストの現在のノードを比較し、小さい方を新しいリストに追加し、そのリストのポインターを進めます。一方のリストがなくなるまでこれを繰り返し、その後、もう一方のリストの残りを追加します。

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

対称ツリー

2分木がそれ自身の鏡像である(すなわち、中心に関して対称である)かどうかをチェックしてください。

説明: これは再帰的に解決できます。ツリーは、左サブツリーが右サブツリーの鏡像である場合に対称です。2つのツリーが鏡像であるかどうかをチェックするヘルパー関数 isMirror(t1, t2) を作成します。これは、1. t1.val === t2.val。 2. t1.leftt2.right の鏡像であること。 3. t1.rightt2.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/ツリー)

2分木が与えられた場合、そのノードの値のレベル順走査を返します。(つまり、左から右へ、レベルごとに)。

説明: これは幅優先探索(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 is global/window) const boundGetX = unboundGetX.myBind(module); console.log(boundGetX()); // 42

K番目に大きい要素を見つける

ソートされていない配列の中からK番目に大きい要素を見つけてください。これはソートされた順序でのK番目に大きい要素であり、K番目に異なる要素ではないことに注意してください。

説明: 簡単なアプローチは、配列をソートし、n - k番目の要素を選択することです。大規模な配列の場合のより効率的なアプローチには、最小ヒープまたは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 !== undefinedundefined値の場合に注意が必要)を使用できます。

const person = { name: 'Eve', age: 28 }; function hasProp(obj, prop) { console.log(`Using 'in': ${prop in obj}`); console.log(`Using '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`を実装する

addhasdeletesizeメソッドを持つ基本的なSetデータ構造を(組み込みのSetを使用せずに)実装してください。

説明: 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を生成してください。

説明: パスカルの三角形では、各数値はそれの真上にある2つの数値の合計です。[[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]]

単語検索

2Dボードと単語が与えられた場合、単語がグリッド内に存在するかどうかを見つけてください。単語は、連続して隣接するセル(「隣接」するセルとは水平方向または垂直方向に隣接するセル)の文字から構築できます。

説明: これはバックトラッキングを伴う深さ優先探索(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

最小ウィンドウ部分文字列

2つの文字列stが与えられた場合、t内のすべての文字を含むs内の最小ウィンドウを見つけてください。s内にt内のすべての文字をカバーするそのようなウィンドウがない場合、空の文字列""を返します。

説明: 2つのポインタ(leftright)とハッシュマップを使用したスライディングウィンドウアプローチを使用します。1つのマップは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ポインタを前のノードを指すように変更する必要があります。イテレーション中にpreviouscurrent、および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を1ステップ進める current = next; // currentを1ステップ進める } return prev; // 新しいヘッドは最後の'prev' } // 例: 1->2->3->4->5 は 5->4->3->2->1 になります。

連結リスト内のサイクルを検出

連結リストの先頭であるheadが与えられた場合、連結リストにサイクルがあるかどうかを判断してください。

説明: 「フロイドの亀とウサギのアルゴリズム」を使用します。1ステップずつ進むポインタ(slow)と2ステップずつ進むポインタ(fast)を2つ持ちます。サイクルがある場合、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; // ポインタが合流、サイクルが存在 } // 例: -4が2を指す3->2->0->-4。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 prototype may only be an Object or null: ' + proto); } } function F() {} F.prototype = proto; return new F(); } const person = { isHuman: false, printIntroduction: function() { console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`); } }; const me = myObjectCreate(person); me.name = 'Matthew'; me.isHuman = true; me.printIntroduction(); // My name is Matthew. Am I human? true console.log(Object.getPrototypeOf(me) === person); // true

巻き上げとは?

JavaScriptにおける巻き上げを説明し、例を挙げてください。

説明: 巻き上げとは、JavaScriptがコード実行前に宣言(var)を現在のスコープ(グローバルまたは関数)の先頭に移動させるデフォルトの動作です。変数宣言(var)は巻き上げられ、undefinedで初期化されます。関数宣言は完全に巻き上げられます(名前と本体の両方)。letconstは巻き上げられますが、初期化されず、一時的デッドゾーンにつながります。

console.log(myVar); // undefined (varは巻き上げられ、undefinedで初期化されます) // console.log(myLet); // ReferenceError: 'myLet' にアクセスする前に初期化できません (TDZ) myFunc(); // 'Hello!' (関数宣言は完全に巻き上げられます) var myVar = 'I am a var'; let myLet = 'I am a let'; function myFunc() { console.log('Hello!'); }

イベントバブリングとキャプチャリングを説明する

DOMの文脈でイベントバブリングとキャプチャリングを説明してください。

説明: これらはHTML DOMにおけるイベント伝播の2つのフェーズです。 キャプチャリングフェーズ: イベントはwindowからターゲット要素まで下へ伝播します。 バブリングフェーズ: イベントはターゲット要素からwindowまで上へ伝播します。デフォルトでは、ほとんどのイベントハンドラーはバブリングフェーズ中に登録されます。addEventListener(type, listener, useCapture)useCapture = trueとともに使用することで、キャプチャリングフェーズ中にイベントを処理できます。

// HTML構造: <div><p><span>Click Me</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 clicked'), true); // キャプチャリング p.addEventListener('click', () => console.log('P clicked'), false); // バブリング span.addEventListener('click', () => console.log('Span clicked'), false); // バブリング // 出力は: Div clicked, Span clicked, P clicked となります。 */

`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('Invalid JSON: ' + e.message); } } console.log(basicJsonParse('{"a": 1, "b": ["x", "y"]}')); // { a: 1, b: ['x', 'y'] }

深くネストされた配列を平坦化する

深くネストされた配列を平坦化する関数を書いてください。

説明: 以前の平坦化(1レベル)とは異なり、これは任意のレベルのネストを処理する必要があります。再帰が自然に適合します。配列をイテレートします。要素が配列の場合、その要素に対して再帰的に平坦化を呼び出し、結果を連結します。それ以外の場合は、要素を追加します。

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]

トライ(プレフィックスツリー)を実装する

insertsearchstartsWithメソッドを持つトライデータ構造を実装してください。

説明: トライは、文字列のデータセット内のキーを効率的に格納および取得するために使用されるツリーのようなデータ構造です。各ノードは文字を表し、ルートからのパスは単語またはプレフィックスを表します。

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[i]が金額iに必要な最小コイン数を格納する配列dpを作成します。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)が与えられた場合、BST内の2つの与えられたノードの最小共通祖先(LCA)を見つけてください。

説明: 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; // pとqを持つ有効なBSTでは発生しないはずです } // 例: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 => LCA は 6

二分木のシリアライズとデシリアライズ

二分木をシリアライズおよびデシリアライズするアルゴリズムを設計してください。

説明: シリアライズはツリーを文字列または配列に変換します。デシリアライズはツリーを再構築します。一般的な方法は先行順走査(DFS)です。構造を保持するために、nullノードに特別なマーカー(例: '#')を使用します。

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,#,#'

`setTimeout`で`setInterval`を実装する

setIntervalを模倣し、setTimeoutを再帰的に使用する関数mySetIntervalを実装してください。

説明: 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の2D行列で表される画像が与えられます。画像を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行列が与えられた場合、行列のすべての要素を螺旋順序で返します。

説明: 4つのポインタを使用して境界を定義します: topbottomleftright。上部の行を走査し、次に右側の列を走査し、次に下部の行を走査し、次に左側の列を走査し、各走査後に境界を縮小します。これはleftrightを横切るか、topbottomを横切るまで続きます。

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'

シングルトンパターン

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の場合、コロンで区切られた1-4桁の16進数の8つのグループをチェックします('::'のようなバリエーションも処理します)。正規表現を使用することもできますが、面接では手動解析の方が明確な場合が多いです。

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')); // Neither

ピーク要素の検索

ピーク要素は、隣接する要素よりも厳密に大きい要素です。入力配列 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 が与えられた場合、長さ n + 1 の配列 ans を返します。ans[i] は、i(0 <= i <= n)の二進数表現における1の数です。

解説: 動的計画法を使用してこれを解決できます。関係に注目してください: dp[i] = dp[i >> 1] + (i & 1)i の1の数は、i を右にシフトした数(つまり、i/2)の1の数に、i が奇数の場合は1を加えたものです。

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]

2のべき乗

整数 n が与えられた場合、それが2のべき乗であれば true を返します。そうでなければ false を返します。

解説: 2のべき乗の二進数表現は、ちょうど1つの '1' ビットを持ちます(例: 1=1, 2=10, 4=100, 8=1000)。巧妙なビット演算トリックは、n > 0 かつ (n & (n - 1)) === 0 をチェックすることです。n が2のべき乗の場合、n-1 はその '1' より下のすべてのビットが '1' に設定されます。それらをAND演算すると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[i] = [starti, endi] である intervals の配列が与えられた場合、重複するすべての区間をマージし、重複しない区間の配列を返します。

解説: まず、開始時間に基づいて区間をソートします。次に、ソートされた区間を反復処理します。現在の区間が結果リストの最後の区間と重複している場合、最後の区間の終了時間を更新してマージします。そうでない場合は、現在の区間を結果リストに追加します。

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 が与えられた場合、s が1つ以上の辞書単語の空白区切りのシーケンスに分割できる場合に true を返します。

解説: 動的計画法を使用します。ブール配列 dp を作成し、dp[i] は部分文字列 s[0...i-1] が分割可能である場合にtrueとします。dp[i] は、j < i が存在し、dp[j] がtrueであり、かつ部分文字列 s[j...i-1] が辞書にある場合にtrueとなります。

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 の独自のバージョンを実装してください。

解説: 再帰を使用します。配列を反復処理します。要素が配列であり、現在の深さが指定された深さよりも小さい場合、その要素に対して再帰的に平坦化を呼び出します。そうでなければ、要素をプッシュします。デフォルトの深さ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 が与えられた場合、単語の順序を反転してください。

解説: 「単語」は非空白文字のシーケンスとして定義されます。文字列をトリミングし、1つ以上のスペースで分割し、複数のスペースから生じる空の文字列を除外し、配列を反転し、単一のスペースで結合して戻します。

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], ...] が与えられた場合、その人がすべての会議に出席できるかどうかを判断してください。

解説: その人がすべての会議に出席できる場合、2つの会議が重複しないことを意味します。これをチェックするには、会議をその開始時間でソートします。次に、ソートされた会議を反復処理し、現在の会議の開始時間が前の会議の終了時間より前であるかどうかをチェックします。もしそうなら、重複があり、その人はすべての会議に出席できません。

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

オブザーバーパターン

JavaScriptでオブザーバー(Pub/Sub)デザインパターンを実装してください。

解説: オブザーバーパターンは、オブジェクト間に1対多の依存関係を定義します。あるオブジェクト(サブジェクト)が状態を変更すると、そのすべての依存オブジェクト(オブザーバー)に自動的に通知され、更新されます。サブジェクトはオブザーバーのリストを保持し、それらを追加、削除、通知するメソッドを提供します。

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('Observer 1'); const obs2 = new Observer('Observer 2'); subject.subscribe(obs1); subject.subscribe(obs2); subject.notify('Something happened!'); // Observer 1 received: Something happened! // Observer 2 received: Something happened!

重複する数値の検索

n + 1 個の整数を含む配列 nums が与えられた場合、各整数が [1, n] の範囲内に含まれる場合、重複する数値を見つけてください。

解説: 数値が [1, n] の範囲にあり、配列のサイズが n+1 であるため、少なくとも1つの重複が保証されます。これは「連結リストのサイクル検出」問題(フロイドのトーとヘアのアルゴリズム)にマッピングできます。配列の値をポインタとして扱います: index -> nums[index]

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のようなライブラリを使用してください。 // スクリプトタグを削除 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>

編集距離

2つの文字列 word1word2 が与えられた場合、word1word2 に変換するのに必要な最小操作回数(挿入、削除、置換)を返します。

解説: これは古典的な動的計画法問題(レーベンシュタイン距離)です。dp[i][j]word1 の最初の i 文字と word2 の最初の j 文字間の編集距離を表す2D配列 dp を作成します。漸化式は、挿入、削除、置換のコストを考慮します。

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] をインデックス i で終わるLISの長さとします。dp[i] を計算するには、nums[j] < nums[i] となるすべての j < 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のチェス盤に配置して、どの2つの女王も互いに脅かさないようにする問題です。Nが与えられた場合、有効な解を1つ(またはすべて)返します。

解説: バックトラッキングを使用します。女王を1行ずつ配置します。各行について、各列に女王を配置してみます。配置する前に、提案された位置が安全であるか(前の行の女王によって攻撃されないか)をチェックします。安全であれば、配置して次の行を再帰的に呼び出します。再帰が失敗した場合、バックトラック(女王を削除)して次の列を試します。

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 はプロミスの配列を受け取り、単一のプロミスを返します。このプロミスは、すべての入力プロミスが解決(解決または拒否)されたときに解決されます。解決値はオブジェクトの配列であり、各オブジェクトは1つのプロミスの結果を記述します({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'}]

2つのソート済み配列の中央値を検索

サイズがそれぞれ mn の2つのソート済み配列 nums1nums2 が与えられた場合、2つのソート済み配列の中央値を返します。

解説: これは、小さい方の配列に対して二分探索アプローチで解決されることが多い難しい問題です。目標は、両方の配列を分割して、左側のすべての要素が右側のすべての要素以下になり、左側の要素の数が右側の要素の数と等しい(または1つ多い)ようにすることです。中央値は、境界要素から計算できます。

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

数独ソルバー

空のセルを埋めることで数独パズルを解くプログラムを記述してください。

解説: バックトラッキングを使用します。空のセルを見つけます。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のボードが必要です。

基本的なミドルウェアパターンの実装

Webフレームワークでよく見られるシンプルなミドルウェアパターンを実装してください。

解説: ミドルウェア関数は、リクエストが最終的なハンドラに到達する前にリクエストを処理します。各ミドルウェアは、リクエスト/レスポンスを変更したり、コントロールを次のミドルウェアに渡したりできます。ミドルウェア関数のリストを管理し、それらを順番に実行するクラスまたはオブジェクトを作成します。

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: Logging...'); req.logged = true; next(); }); app.use((req, next) => { console.log('2: Authenticating...'); req.authed = true; next(); }); app.use((req, next) => { console.log('3: Final Handler:', req); }); app.handle({ data: 'some request' }); // 1: Logging... // 2: Authenticating... // 3: Final Handler: { data: 'some request', logged: true, authed: true }

有向グラフのサイクルの検出

有向グラフが与えられた場合、それがサイクルを含むかどうかを判断する関数を記述してください。

解説: 深さ優先探索(DFS)を使用します。2つのセットを保持します: visiting(現在再帰スタックにあるノード)と visited(完全に探索されたノード)。DFS中に visiting セット内のノードに遭遇した場合、後方エッジが見つかったことになり、それはサイクルがあることを意味します。

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.preventExtensionsObject.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) は、アニメーションを実行したいことをブラウザに伝え、次の再描画の前に指定された関数を呼び出してアニメーションを更新するようにブラウザに要求します。ブラウザのリフレッシュレートと同期し、タブが表示されていないときに一時停止するため、アニメーションに setTimeoutsetInterval を使用するよりも効率的でスムーズです。

// 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; // 2秒で100px移動(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('Webブラウザのアニメーションには requestAnimationFrame を使用してください。');

`Array.prototype.some` の実装

Array.prototype.some の独自のバージョンを実装してください。

解説: some は、配列内の少なくとも1つの要素が、指定された関数によって実装されたテストに合格するかどうかをテストします。コールバックが 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は、ウェブコンテンツがバックグラウンドスレッドでスクリプトを実行するための手段です。主なユースケースは、UIの更新やユーザーインタラクションを処理するメインスレッドから、長時間実行されるタスクや計算量の多いタスクをオフロードし、UIが応答しなくなったり「フリーズ」したりするのを防ぐことです。Workerは、メッセージパッシング(postMessageonmessage)を介してメインスレッドと通信します。

// main.js /* if (window.Worker) { const myWorker = new Worker('worker.js'); myWorker.postMessage([5, 3]); // Workerにデータを送信 myWorker.onmessage = function(e) { console.log('Workerからの結果:', 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

ビットごとの加算(`+` なし)

+ 演算子を使用せずに2つの整数を加算する関数を記述してください。

解説: ビット演算子を使用します。合計は 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 が与えられた場合、x が回文であれば true を返し、そうでなければ 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('Unknown vehicle type'); } } } 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` とユースケースの説明

JavaScriptの Symbol とは何かを説明し、例えば「プライベート」プロパティの作成やユニークなオブジェクトキーなどのユースケースを提供してください。

解説: Symbol はES6で導入されたプリミティブなデータ型です。そのインスタンスはユニークで不変です。これらは、異なるライブラリ間の名前の衝突を避けるため、または(真にプライベートではないが、通常の反復や Object.keys を介してアクセスできない)擬似プライベートプロパティを作成するためのオブジェクトプロパティのキーとしてよく使用されます。

const _privateName = Symbol('name'); const _privateMethod = Symbol('greet'); class Person { constructor(name) { this[_privateName] = name; } [_privateMethod]() { console.log(`Hello, my name is ${this[_privateName]}`); } introduce() { this[_privateMethod](); } } const p = new Person('Bob'); p.introduce(); // Hello, my name is Bob console.log(Object.keys(p)); // [] - シンボルは含まれません console.log(p[_privateName]); // Bob (シンボルがあればアクセス可能)

`arguments` を実際の配列に変換する

(アロー関数ではない)関数で利用可能な arguments オブジェクトを、実際のJavaScript配列に変換するにはどうすればよいですか?

解説: arguments オブジェクトは配列のように見えますが、実際には配列ではありません。mapfilter などのメソッドがありません。これは、次の方法で変換できます。

  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. スプレッド構文 const args2 = [...arguments]; console.log('スプレッドを使用:', 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 // スプレッドを使用: 10 // Slice を使用: 10