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 的数字。但对于三的倍数,打印 '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 只存储唯一值。您可以将数组转换为 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() 和展开运算符,或者 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 函数对数组的每个元素执行用户提供的“reducer”回调函数,传入上一个元素计算的返回值。对数组所有元素运行 reducer 的最终结果是一个单一值。处理初始值参数。

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(从头部删除)方法的队列数据结构。

解释: 队列遵循先进先出(FIFO)原则。可以使用数组,push 用于 enqueueshift 用于 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

实现一个简单栈

实现一个具有 push(添加到顶部)和 pop(从顶部删除)方法的栈数据结构。

解释: 栈遵循后进先出(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 计算。可以计算数组元素的实际和。这两个和之间的差值就是缺失的数字。

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 秒后实际运行。

节流函数

实现一个节流函数。节流确保一个函数在指定的时间间隔内最多被调用一次。

解释: 使用一个标志来指示是否允许调用。当被调用时,如果允许,则执行函数,将标志设置为 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

合并两个数组

如何将两个数组合并为一个?

解释: 使用展开语法(...)或 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 指的是什么?

解释: 当函数作为对象的方法被调用时,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)。

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

解释: 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 是单线程的,但它通过事件循环实现并发。调用栈处理同步代码。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

归并排序

实现归并排序算法。

解释: 归并排序是一种分治算法。它将输入数组分成两半,对这两半调用自身,然后合并两个已排序的半部分。合并函数至关重要;它将两个已排序的子数组组合成一个已排序的数组。

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')

实现一个链表

实现一个带有 addprint 方法的单向链表。

解释: 链表是一种线性数据结构,其中元素不存储在连续的内存位置。每个元素(节点)指向下一个元素。您需要一个 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)

实现一个带有 insertfind 方法的二叉搜索树。

解释: 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]

查找两个数组的交集

给定两个数组,编写一个函数来计算它们的交集(两者共有的元素)。

解释: 一个好的方法是将一个数组转换为 Set 以实现平均 O(1) 的查找时间。然后,遍历第二个数组并检查每个元素是否存在于 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 移动到它的末尾,同时保持非零元素的相对顺序。

解释: 使用“双指针”或“雪球”方法。一个指针跟踪下一个非零元素应该放置的位置。遍历数组;如果元素非零,则将其放置在指针位置并递增指针。最后,用零填充其余部分。

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]

最大子数组(Kadane 算法)

给定一个整数数组 nums,找出其中和最大的连续子数组(至少包含一个数字),并返回其和。

解释: Kadane 算法是一种高效的解决方案。遍历数组,跟踪当前位置结束的 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']

最大公约数(GCD)

编写一个函数来查找两个数字的最大公约数(GCD)。

解释: 欧几里得算法是一种高效的方法。如果 b 为 0,则 a 是 GCD。否则,ab 的 GCD 与 ba % ba 除以 b 的余数)的 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)

编写一个函数来查找两个数字的最小公倍数(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 都已解决时解决,或者在任何输入 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]

二叉树的最大深度

给定一个二叉树,找出它的最大深度。

解释: 最大深度是从根节点到最远叶节点的最长路径上的节点数。这可以通过递归找到:最大深度 = 1 + max(最大深度(左子树), 最大深度(右子树))。基本情况是当节点为空时,其深度为 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,除了一个元素外,其他每个元素都出现两次。找出那个只出现一次的元素。

解释: 一种非常高效的解决方案是使用按位异或运算符(^)。数字与自身异或结果为 0。数字与 0 异或结果为数字本身。如果您对数组中的所有数字进行异或操作,则成对的数字将相互抵消(变为 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⌋ 的元素。

解释: Boyer-Moore 投票算法是一种高效的方法。初始化一个 candidate 和一个 count。遍历数组。如果 count 为 0,则将当前元素设置为 candidate。如果当前元素与 candidate 匹配,则 count 加 1;否则,count 减 1。

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'(水)组成的 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 缓存

设计并实现一个最近最少使用(LRU)缓存的数据结构。它应该支持 getput 操作。

解释: LRU 缓存在达到容量时会驱逐最近最少使用的项。常见的实现方法是使用 Map(或哈希映射)进行 O(1) 查找,以及使用双向链表进行 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 的两个端点位于 (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

三数之和

给定一个包含 n 个整数的数组 nums,是否存在元素 a, b, c 使得 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.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/树)

给定一个二叉树,返回其节点值的层序遍历。(即,从左到右,一层一层地)。

解释: 这是一种广度优先搜索(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 关键字被设置为提供的值,并且给定一系列参数,这些参数在调用新函数时提供的任何参数之前。在返回的函数中使用 applycall,处理部分应用(预设参数)。

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 是全局/窗口对象) 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 !== undefined(对于 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),包括 addhasdeletesize 方法。

解释: 您可以使用 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]]

单词搜索

给定一个 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

最小窗口子串

给定两个字符串 st,在 s 中找到一个最小的窗口,它将包含 t 中的所有字符。如果 s 中不存在覆盖 t 中所有字符的窗口,则返回空字符串 ""

解释: 使用滑动窗口方法,带两个指针(leftright)和哈希映射。一个映射存储 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 指针以指向前一个节点。在迭代过程中跟踪 previouscurrentnext 节点。

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 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

什么是 Hoisting?

解释 JavaScript 中的 Hoisting 并提供一个示例。

解释: Hoisting 是 JavaScript 的默认行为,它在代码执行之前将声明移动到当前作用域(全局或函数)的顶部。变量声明(var)会被提升并用 undefined 初始化。函数声明会被完全提升(名称和主体)。letconst 会被提升但不会初始化,导致出现暂时性死区。

console.log(myVar); // undefined (var 被提升并用 undefined 初始化) // console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization (暂时性死区) myFunc(); // 'Hello!' (函数声明被完全提升) var myVar = 'I am a var'; let myLet = 'I am a let'; function myFunc() { console.log('Hello!'); }

解释事件冒泡和捕获

在 DOM 的上下文中解释事件冒泡和捕获。

解释: 这是 HTML DOM 中事件传播的两个阶段。 捕获阶段: 事件从 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) 和基本数据类型。evalnew 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'] }

扁平化深度嵌套数组

编写一个函数来扁平化一个深度嵌套的数组。

解释: 与之前的扁平化(一层)不同,这需要处理任意级别的嵌套。递归是自然而然的。遍历数组。如果元素是数组,则递归调用扁平化并连接结果。否则,添加该元素。

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 数据结构,包含 insertsearchstartsWith 方法。

解释: 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

打乱数组 (Fisher-Yates)

使用 Fisher-Yates (或 Knuth) 算法编写一个函数来原地打乱数组。

解释: Fisher-Yates 算法从最后一个元素到第一个元素遍历数组。在每次迭代中,它将当前元素与数组开头到当前元素(包括当前元素)之间随机选择的元素进行交换。

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]]; // Swap elements } return arr; } console.log(shuffleArray([1, 2, 3, 4, 5])); // e.g., [3, 5, 1, 2, 4]

函数组合 (Compose Functions)

实现一个 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 Functions)

实现一个 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)。

解释: 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; // Found LCA } } return null; // Should not happen in a valid BST with p and q } // Example: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 => LCA is 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(); } // Example: 1 -> [2, 3] -> [null, null, 4, 5] // Serialized: '1,2,#,#,3,4,#,#,5,#,#'

用 `setTimeout` 实现 `setInterval`

实现一个函数 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(); // Schedule the next call }, delay); } run(); return interval; // Return an object to allow clearing } function myClearInterval(interval) { clearTimeout(interval.timerId); } // Example usage: 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); // Add neighbors in reverse to process them in order later (optional) 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; // Transpose 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]]; } } // Reverse each row 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 矩阵,以螺旋顺序返回矩阵中的所有元素。

解释: 使用四个指针定义边界:topbottomleftright。遍历顶部行,然后是右侧列,然后是底部行,然后是左侧列,每次遍历后缩小边界,直到 left 穿过 righttop 穿过 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; } // Example: [[1,1,1],[1,0,1],[1,1,1]] => [[1,0,1],[0,0,0],[1,0,1]]

实现 `Promise.race`

实现一个函数,其行为类似于 Promise.race

解释: Promise.race 接收一个 Promise 数组并返回一个 Promise。这个返回的 Promise 会在任何一个输入 Promise 解决(resolve 或 reject)时立即解决,其值或原因来自那个 Promise。

function myPromiseRace(promises) { return new Promise((resolve, reject) => { if (!promises || promises.length === 0) { return; // Or resolve/reject depending on spec } 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() { // Private methods and variables const privateVar = 'I am private'; function privateMethod() { console.log('Private'); } return { // Public methods and variables 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; // Simplified: No '::' 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 (Simplified) 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; // Peak is to the right } else { right = mid; // Peak is mid or to the left } } return left; // 'left' will be the index of a peak } console.log(findPeakElement([1, 2, 3, 1])); // 2 (index of 3) console.log(findPeakElement([1, 2, 1, 3, 5, 6, 4])); // 5 (index of 6) or 1 (index of 2)

计数比特

给定一个整数 n,返回一个长度为 n + 1 的数组 ans,使得对于每个 i (0 <= i <= n),ans[i]i 的二进制表示中 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); // Or: dp[i] = dp[i & (i - 1)] + 1; (Removes last set bit) } return dp; } console.log(countBits(5)); // [0, 1, 1, 2, 1, 2]

2 的幂

给定一个整数 n,如果它是 2 的幂,则返回 true。否则,返回 false

解释: 二进制表示中 2 的幂恰好有一个“1”位(例如,1=1,2=10,4=100,8=1000)。一个巧妙的位运算技巧是检查 n > 0 并且 (n & (n - 1)) === 0。如果 n 是 2 的幂,则 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]) { // Overlap: merge last[1] = Math.max(last[1], current[1]); } else { // No overlap: add new interval 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 可以被拆分为一个或多个字典单词的空格分隔序列,则返回 true

解释: 使用动态规划。创建一个布尔数组 dp,其中 dp[i] 为 true 表示子字符串 s[0...i-1] 可以被拆分。如果存在 j < i 使得 dp[j] 为 true 且子字符串 s[j...i-1] 在字典中,则 dp[i] 为 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; // Base case: empty string 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'); } } // Default behavior: Set the property 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'; // Throws TypeError // personProxy.age = 200; // Throws 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; // Overlap detected } } 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 接收一个 Promise 数组并返回一个 Promise。这个 Promise 会在任何一个输入 Promise 完成(fulfill)时立即完成。如果所有输入 Promise 都拒绝(reject),它会以一个包含所有拒绝原因的 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) // Resolve as soon as one fulfills .catch(error => { errors[index] = error; rejectionCount++; if (rejectionCount === numPromises) { reject(new AggregateError(errors, 'All promises were rejected')); } }); }); }); } // Example: myPromiseAny([Promise.reject('err1'), Promise.resolve('ok')]) -> 'ok' // Example: myPromiseAny([Promise.reject('err1'), Promise.reject('err2')]) -> AggregateError

观察者模式

在 JavaScript 中实现观察者 (发布/订阅) 设计模式。

解释: 观察者模式定义了对象之间的一对多依赖关系。当一个对象(Subject)改变状态时,其所有依赖项(Observers)都会自动收到通知并更新。Subject 维护一个观察者列表,并提供添加、删除和通知它们的方法。

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,因此保证至少有一个重复项。这可以映射到“查找链表中的环”问题(Floyd 的龟兔赛跑算法)。将数组值视为指针:索引 -> nums[索引]

function findDuplicate(nums) { let tortoise = nums[0]; let hare = nums[0]; // Phase 1: Find the intersection point do { tortoise = nums[tortoise]; hare = nums[nums[hare]]; } while (tortoise !== hare); // Phase 2: Find the entrance to the cycle 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) { // WARNING: This is a very basic and insecure example. // Do NOT use this in production. Use a library like DOMPurify. // Remove script tags let sanitized = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); // Example: Remove onclick attributes (very basic) 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>

编辑距离

给定两个字符串 word1word2,返回将 word1 转换为 word2 所需的最少操作数(插入、删除或替换)。

解释: 这是一个经典的动态规划问题(Levenshtein 距离)。创建一个二维数组 dp,其中 dp[i][j]word1 的前 i 个字符与 word2 的前 j 个字符之间的编辑距离。递推关系涉及考虑插入、删除和替换的成本。

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, // Deletion dp[i][j - 1] + 1, // Insertion dp[i - 1][j - 1] + cost // Substitution/Match ); } } return dp[m][n]; } console.log(minDistance('horse', 'ros')); // 3 console.log(minDistance('intention', 'execution')); // 5

最长递增子序列 (LIS)

给定一个整数数组 nums,返回最长严格递增子序列的长度。

解释: 使用动态规划。设 dp[i] 是以索引 i 结尾的 LIS 的长度。要计算 dp[i],找到所有 j < inums[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,返回一个有效解(或所有解)。

解释: 使用回溯法。逐行放置皇后。对于每一行,尝试将皇后放置在每一列中。在放置之前,检查提议的位置是否安全(不会被前几行的皇后攻击)。如果安全,则放置并递归到下一行。如果递归失败,则回溯(移除皇后)并尝试下一列。

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; // Check col for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) if (board[i][j] === 'Q') return false; // Check diag up-left for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) if (board[i][j] === 'Q') return false; // Check diag up-right 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 } } } 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' isn't a public property // console.log(privateData.get(person)); // Accessible, but not directly via 'person.'

实现 `Promise.allSettled`

实现一个函数,其行为类似于 Promise.allSettled

解释: Promise.allSettled 接收一个 Promise 数组并返回一个 Promise。当所有输入 Promise 都已解决(已完成或已拒绝)时,这个 Promise 完成。完成值为一个对象数组,每个对象描述一个 Promise 的结果({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); } }); }); }); } // Example: myPromiseAllSettled([Promise.resolve(1), Promise.reject('err')]) // -> [{status: 'fulfilled', value: 1}, {status: 'rejected', reason: 'err'}]

查找两个排序数组的中位数

给定两个大小分别为 mn 的排序数组 nums1nums2,返回这两个排序数组的中位数。

解释: 这是一个难题,通常使用对较小数组进行二分查找的方法解决。目标是分割两个数组,使得左侧所有元素都小于或等于右侧所有元素,并且左侧元素的数量等于(或比右侧多一个)。然后可以从边界元素计算中位数。

function findMedianSortedArrays(nums1, nums2) { if (nums1.length > nums2.length) [nums1, nums2] = [nums2, nums1]; // Ensure nums1 is smaller 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('Input arrays are not sorted'); } 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; // Check row if (board[i][col] === numStr) return false; // Check col 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; // Check box } 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] = '.'; // Backtrack } } return false; // No valid number found } } } return true; // Board solved } backtrack(); return board; } // Example requires a 9x9 board with '.' for empty cells.

实现基本中间件模式

实现 Web 框架中常见的简单中间件模式。

解释: 中间件函数在请求到达最终处理程序之前处理请求。每个中间件都可以修改请求/响应或将控制权传递给 next 中间件。创建一个类或对象来管理中间件函数列表并按顺序执行它们。

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)。维护两个集合: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; // Cycle detected 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); // Prevent adding new properties 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; // Fails silently (or throws in strict mode) // delete obj.a; // Fails silently (or throws in strict mode) // obj.a = 10; // Fails silently (or throws in strict mode) 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; // Move 100px in 2 seconds (50px/sec) position = (elapsedTime / 2000) * 100; if (position < 200) { // Keep animating until it reaches 200px box.style.left = position + 'px'; requestAnimationFrame(animate); } else { box.style.left = '200px'; } } // Start the animation // requestAnimationFrame(animate); // Uncomment to run in browser console.log('Web Workers run scripts in background threads.');

实现 `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) { // Handle sparse arrays 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 是一种让 Web 内容在后台线程中运行脚本的方式。主要用例是将长时间运行或计算密集型任务从主线程(处理 UI 更新和用户交互)中卸载,从而防止 UI 无响应或“冻结”。Workers 通过消息传递(postMessageonmessage)与主线程通信。

// main.js /* if (window.Worker) { const myWorker = new Worker('worker.js'); myWorker.postMessage([5, 3]); // Send data to worker myWorker.onmessage = function(e) { console.log('Result from worker:', e.data); } } else { console.log('Your browser does not support Web Workers.'); } */ // worker.js /* self.onmessage = function(e) { console.log('Message received from main script'); const result = e.data[0] * e.data[1]; console.log('Posting result back to main script'); self.postMessage(result); } */ console.log('Web Workers run scripts in background threads.');

解码方法

一条包含字母 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' or '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; // Calculate carry a = a ^ b; // Calculate sum without carry b = carry; // Carry becomes the new '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; // String approach // return String(x) === String(x).split('').reverse().join(''); // Mathematical approach 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)); // [] - Symbols are not included console.log(p[_privateName]); // Bob (Accessible if you have the Symbol)

将 `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('Using Array.from:', args1.reduce((a, b) => a + b, 0)); // 2. Spread Syntax const args2 = [...arguments]; console.log('Using Spread:', args2.reduce((a, b) => a + b, 0)); // 3. Slice const args3 = Array.prototype.slice.call(arguments); console.log('Using Slice:', args3.reduce((a, b) => a + b, 0)); } sumAll(1, 2, 3, 4); // Using Array.from: 10 // Using Spread: 10 // Using Slice: 10