Live Coding

Kiểm tra xem một đối tượng có rỗng không

Làm thế nào để kiểm tra xem một đối tượng JavaScript có rỗng không?

Giải thích: Một đối tượng được coi là rỗng nếu nó không có bất kỳ thuộc tính nào của riêng nó có thể liệt kê được. Chúng ta có thể sử dụng Object.keys(obj) để trả về một mảng chứa tên các thuộc tính riêng có thể liệt kê được của đối tượng đã cho. Nếu độ dài của mảng này là 0, thì đối tượng đó rỗng.

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

Đảo ngược một chuỗi

Viết một hàm để đảo ngược một chuỗi đã cho.

Giải thích: Cách đơn giản nhất là chuyển chuỗi thành một mảng các ký tự, sử dụng phương thức reverse() có sẵn cho mảng, sau đó nối các ký tự lại thành một chuỗi.

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

Kiểm tra Palindrome

Viết một hàm kiểm tra xem một chuỗi đã cho có phải là chuỗi palindrome không.

Giải thích: Một chuỗi palindrome là một từ hoặc cụm từ đọc xuôi và ngược đều như nhau. Chúng ta có thể kiểm tra điều này bằng cách đảo ngược chuỗi (bỏ qua chữ hoa/thường và các ký tự không phải chữ-số để kiểm tra mạnh mẽ hơn, nhưng ở đây chúng ta thực hiện kiểm tra đơn giản) và so sánh nó với chuỗi gốc.

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

Tìm số lớn nhất trong một mảng

Viết một hàm để tìm số lớn nhất trong một mảng các số.

Giải thích: Bạn có thể lặp qua mảng, theo dõi số lớn nhất tìm được cho đến nay. Hoặc, bạn có thể sử dụng hàm Math.max() cùng với cú pháp spread (...) để truyền các phần tử mảng làm đối số.

function findMaxNumber(arr) { if (arr.length === 0) return undefined; // Hoặc ném lỗi return Math.max(...arr); } console.log(findMaxNumber([1, 5, 2, 9, 3])); // 9

FizzBuzz

Viết một chương trình in các số từ 1 đến n. Nhưng đối với bội số của ba, in 'Fizz' thay vì số và đối với bội số của năm, in 'Buzz'. Đối với các số là bội số của cả ba và năm, in 'FizzBuzz'.

Giải thích: Bài toán cổ điển này kiểm tra logic vòng lặp và điều kiện cơ bản. Bạn cần lặp từ 1 đến n và sử dụng toán tử modulo (%) để kiểm tra khả năng chia hết cho 3 và 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);

Xóa các phần tử trùng lặp khỏi một mảng

Viết một hàm xóa các phần tử trùng lặp khỏi một mảng.

Giải thích: Một cách hiện đại và súc tích để đạt được điều này là sử dụng Set. Set chỉ lưu trữ các giá trị duy nhất. Bạn có thể chuyển đổi mảng thành một Set và sau đó trở lại thành một mảng.

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

Kiểm tra Anagram

Viết một hàm để kiểm tra xem hai chuỗi có phải là anagram của nhau không.

Giải thích: Anagram là những từ được tạo thành bằng cách sắp xếp lại các chữ cái của một từ khác. Để kiểm tra, chúng ta có thể làm sạch, sắp xếp và so sánh các chuỗi. Làm sạch bao gồm loại bỏ các ký tự không phải chữ-số và chuyển đổi thành một trường hợp nhất quán.

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

Tính giai thừa

Viết một hàm để tính giai thừa của một số nguyên không âm.

Giải thích: Giai thừa (n!) của một số là tích của tất cả các số nguyên dương nhỏ hơn hoặc bằng n. Chúng ta có thể tính toán điều này một cách lặp đi lặp lại bằng cách nhân các số từ 1 đến n.

function factorial(n) { if (n < 0) return undefined; // Giai thừa không được định nghĩa cho số âm if (n === 0) return 1; let result = 1; for (let i = 1; i <= n; i++) { result *= i; } return result; } console.log(factorial(5)); // 120

Tính tổng tất cả các số trong một mảng

Viết một hàm trả về tổng của tất cả các số trong một mảng.

Giải thích: Bạn có thể sử dụng phương thức reduce, rất lý tưởng để tích lũy một giá trị duy nhất từ một mảng. Nó nhận một hàm callback và một giá trị khởi tạo (0 để tính tổng).

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

Làm phẳng một mảng lồng nhau

Viết một hàm để làm phẳng một mảng lồng nhau. Để đơn giản, giả sử chỉ có một cấp độ lồng nhau.

Giải thích: Đối với một cấp độ lồng nhau, bạn có thể sử dụng Array.prototype.concat() với toán tử spread hoặc phương thức flat() (ES2019).

function flattenArray(arr) { // Sử dụng flat() (đơn giản hơn nếu ES2019+ ổn) // return arr.flat(); // Sử dụng reduce và concat return arr.reduce((acc, val) => acc.concat(val), []); } console.log(flattenArray([1, [2, 3], 4, [5]])); // [1, 2, 3, 4, 5]

Đếm số nguyên âm trong một chuỗi

Viết một hàm đếm số nguyên âm (a, e, i, o, u) trong một chuỗi đã cho.

Giải thích: Lặp qua chuỗi (không phân biệt chữ hoa/thường) và kiểm tra xem mỗi ký tự có phải là nguyên âm không. Duy trì một bộ đếm.

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

Viết hoa chữ cái đầu câu

Viết một hàm chuyển đổi một câu thành dạng viết hoa chữ cái đầu (chữ cái đầu tiên của mỗi từ được viết hoa).

Giải thích: Chia câu thành các từ, sau đó lặp qua từng từ, viết hoa chữ cái đầu tiên và viết thường phần còn lại. Cuối cùng, nối các từ lại với nhau.

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'

Bài toán hai số có tổng bằng target

Cho một mảng các số nguyên nums và một số nguyên target, trả về chỉ số của hai số sao cho tổng của chúng bằng target.

Giải thích: Một cách tiếp cận phổ biến là sử dụng một hash map (hoặc một đối tượng JavaScript) để lưu trữ các số và chỉ số của chúng khi bạn lặp. Đối với mỗi số, hãy kiểm tra xem target - số_hiện_tại có tồn tại trong map không.

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 []; // Hoặc null, hoặc ném lỗi nếu không có giải pháp } console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]

Thực hiện bộ đếm bằng cách sử dụng Closure

Tạo một hàm trả về một hàm bộ đếm. Mỗi khi hàm được trả về được gọi, nó sẽ tăng lên và trả về một số đếm.

Giải thích: Điều này thể hiện closure. Hàm bên trong có quyền truy cập vào biến count trong phạm vi của hàm bên ngoài, ngay cả sau khi hàm bên ngoài đã hoàn thành thực thi.

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

Tìm ký tự không lặp lại đầu tiên

Viết một hàm tìm ký tự không lặp lại đầu tiên trong một chuỗi.

Giải thích: Bạn có thể sử dụng một hash map để đếm tần suất ký tự. Đầu tiên, lặp qua chuỗi để xây dựng bản đồ tần suất. Sau đó, lặp lại qua chuỗi và trả về ký tự đầu tiên có số lần xuất hiện là 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; // Hoặc một chỉ báo nào đó nếu tất cả đều lặp lại } console.log(firstNonRepeatingChar('aabbcdeeff')); // 'c' console.log(firstNonRepeatingChar('swiss')); // 'w'

Phân đoạn mảng

Viết một hàm chia một mảng thành các nhóm có kích thước đã chỉ định.

Giải thích: Lặp qua mảng, lấy các lát cắt có kích thước đã chỉ định và đẩy chúng vào một mảng mới. Xử lý trường hợp phân đoạn cuối cùng có thể nhỏ hơn.

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

Kiểm tra xem một số có phải là số nguyên tố không

Viết một hàm để xác định xem một số đã cho có phải là số nguyên tố không.

Giải thích: Một số nguyên tố là một số tự nhiên lớn hơn 1 không có ước số dương nào khác ngoài 1 và chính nó. Lặp từ 2 đến căn bậc hai của số, kiểm tra khả năng chia hết. Xử lý các trường hợp đặc biệt như 1 và 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

Tìm từ dài nhất trong một chuỗi

Viết một hàm tìm từ dài nhất trong một câu.

Giải thích: Chia chuỗi thành một mảng các từ. Sau đó, lặp qua mảng, theo dõi từ dài nhất tìm được cho đến nay (hoặc độ dài của nó).

function findLongestWord(str) { const words = str.split(' '); let longestWord = ''; for (const word of words) { // Làm sạch từ nếu cần (ví dụ: loại bỏ dấu câu) if (word.length > longestWord.length) { longestWord = word; } } return longestWord; } console.log(findLongestWord('The quick brown fox jumped over the lazy dog')); // 'jumped'

Thực hiện `Array.prototype.map`

Thực hiện phiên bản của riêng bạn của hàm Array.prototype.map.

Giải thích: Hàm map nhận một callback và tạo một mảng mới bằng cách áp dụng callback cho mỗi phần tử của mảng gốc. Hàm của bạn nên lặp qua mảng và xây dựng mảng mới.

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]

Thực hiện `Array.prototype.filter`

Thực hiện phiên bản của riêng bạn của hàm Array.prototype.filter.

Giải thích: Hàm filter nhận một callback và tạo một mảng mới với tất cả các phần tử vượt qua kiểm tra được thực hiện bởi hàm callback được cung cấp.

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

Thực hiện `Array.prototype.reduce`

Thực hiện phiên bản của riêng bạn của hàm Array.prototype.reduce.

Giải thích: Hàm reduce thực thi một hàm callback 'reducer' do người dùng cung cấp trên mỗi phần tử của mảng, chuyển giá trị trả về từ phép tính trên phần tử trước đó. Kết quả cuối cùng của việc chạy reducer trên tất cả các phần tử của mảng là một giá trị duy nhất. Xử lý đối số giá trị khởi tạo.

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

Memoization - Dãy Fibonacci

Thực hiện một hàm Fibonacci bằng cách sử dụng memoization để tối ưu hóa hiệu suất.

Giải thích: Dãy Fibonacci liên quan đến các phép tính lặp đi lặp lại. Memoization lưu trữ kết quả của các lệnh gọi hàm tốn kém và trả về kết quả đã lưu trong bộ nhớ cache khi cùng một đầu vào xảy ra lần nữa.

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 (nhanh hơn nhiều so với không có memoized)

Kiểm tra dấu ngoặc cân bằng

Viết một hàm để kiểm tra xem một chuỗi chứa các dấu ngoặc {}[]() có cân bằng không.

Giải thích: Sử dụng một stack. Khi bạn gặp dấu ngoặc mở, đẩy nó vào stack. Khi bạn gặp dấu ngoặc đóng, kiểm tra xem stack có rỗng không hoặc nếu phần tử trên cùng là dấu ngoặc mở khớp. Nếu khớp, pop stack. Nếu không, hoặc nếu stack rỗng, nó không cân bằng. Cuối cùng, một stack rỗng có nghĩa là cân bằng.

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

Thực hiện một Queue đơn giản

Thực hiện cấu trúc dữ liệu Queue với các phương thức enqueue (thêm vào cuối) và dequeue (xóa khỏi đầu).

Giải thích: Một hàng đợi tuân theo nguyên tắc Nhập trước, Xuất trước (FIFO). Một mảng có thể được sử dụng, với push cho enqueueshift cho 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

Thực hiện một Stack đơn giản

Thực hiện cấu trúc dữ liệu Stack với các phương thức push (thêm vào trên cùng) và pop (xóa khỏi trên cùng).

Giải thích: Một stack tuân theo nguyên tắc Nhập sau, Xuất trước (LIFO). Một mảng có thể được sử dụng, với các phương thức 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

Tìm số bị thiếu trong dãy

Cho một mảng chứa n số phân biệt lấy từ 0, 1, 2, ..., n, tìm số bị thiếu trong mảng.

Giải thích: Tổng các số từ 0 đến n có thể được tính bằng công thức n*(n+1)/2. Tổng thực tế của các phần tử mảng có thể được tính. Sự khác biệt giữa hai tổng này sẽ là số bị thiếu.

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

Hàm Debounce

Thực hiện một hàm debounce. Debouncing đảm bảo rằng một hàm không được gọi lại cho đến khi một khoảng thời gian nhất định đã trôi qua mà không bị gọi.

Giải thích: Sử dụng setTimeoutclearTimeout. Mỗi khi hàm debounced được gọi, xóa timeout trước đó và đặt một cái mới. Lệnh gọi hàm thực tế chỉ xảy ra khi timeout hoàn thành.

function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(this, args); }, delay); }; } // Ví dụ sử dụng: const sayHello = () => console.log('Hello!'); const debouncedHello = debounce(sayHello, 1000); debouncedHello(); // Được gọi sau 1 giây (nếu không được gọi lại) debouncedHello(); // Đặt lại bộ hẹn giờ debouncedHello(); // Đặt lại bộ hẹn giờ, sẽ thực sự chạy 1 giây sau lệnh gọi này.

Hàm Throttle

Thực hiện một hàm throttle. Throttling đảm bảo rằng một hàm được gọi tối đa một lần trong một khoảng thời gian được chỉ định.

Giải thích: Sử dụng một cờ để cho biết liệu một lệnh gọi có được phép hay không. Khi được gọi, nếu được phép, thực thi hàm, đặt cờ thành false và sử dụng setTimeout để đặt lại cờ sau khoảng thời gian.

function throttle(func, limit) { let inThrottle = false; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // Ví dụ sử dụng: const logScroll = () => console.log('Scrolling...'); const throttledScroll = throttle(logScroll, 1000); // window.addEventListener('scroll', throttledScroll); // Sẽ ghi nhật ký tối đa một lần mỗi giây.

Hàm Currying

Viết một hàm nhận một hàm với hai đối số và trả về phiên bản curried của nó.

Giải thích: Currying biến đổi một hàm với nhiều đối số thành một chuỗi các hàm, mỗi hàm nhận một đối số duy nhất. f(a, b) trở thành 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

Deep Clone một đối tượng

Viết một hàm để thực hiện deep clone một đối tượng JavaScript.

Giải thích: Một shallow clone chỉ sao chép các thuộc tính cấp cao nhất. Một deep clone sao chép đệ quy tất cả các thuộc tính, bao gồm các đối tượng và mảng lồng nhau. Một cách đơn giản (với những hạn chế, ví dụ: không xử lý tốt các hàm, ngày tháng, biểu thức chính quy) là sử dụng JSON.parse(JSON.stringify(obj)). Một cách tiếp cận đệ quy mạnh mẽ hơn.

function deepClone(obj) { // Phiên bản đơn giản (có những hạn chế) try { return JSON.parse(JSON.stringify(obj)); } catch (e) { console.error("Cloning failed:", e); return null; } // Đệ quy mạnh mẽ hơn (ví dụ cơ bản): /* 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 (chứng minh nó là deep clone) console.log(cloned.b.c); // 3

Lấy khóa đối tượng

Làm thế nào để lấy một mảng các khóa từ một đối tượng?

Giải thích: Sử dụng Object.keys(obj).

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

Lấy giá trị đối tượng

Làm thế nào để lấy một mảng các giá trị từ một đối tượng?

Giải thích: Sử dụng Object.values(obj).

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

Kiểm tra xem mảng có chứa giá trị

Làm thế nào để kiểm tra xem một mảng có chứa một giá trị cụ thể không?

Giải thích: Sử dụng Array.prototype.includes(value).

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

Hợp nhất hai mảng

Làm thế nào để hợp nhất hai mảng thành một?

Giải thích: Sử dụng cú pháp spread (...) hoặc Array.prototype.concat().

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

Giải thích 'this' trong Phạm vi Toàn cục

this đề cập đến điều gì trong phạm vi toàn cục trong trình duyệt?

Giải thích: Trong ngữ cảnh thực thi toàn cục (ngoài bất kỳ hàm nào), this đề cập đến đối tượng toàn cục, đó là window trong các trình duyệt web.

console.log(this === window); // true (trong môi trường trình duyệt)

Giải thích 'this' trong một Phương thức đối tượng

this đề cập đến điều gì khi được sử dụng bên trong một phương thức đối tượng?

Giải thích: Khi một hàm được gọi như một phương thức của một đối tượng, this đề cập đến đối tượng mà phương thức đó được gọi trên.

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

Giải thích 'this' với hàm mũi tên

Các hàm mũi tên xử lý this như thế nào?

Giải thích: Các hàm mũi tên không có ngữ cảnh this của riêng chúng. Thay vào đó, chúng kế thừa this từ phạm vi (từ vựng) xung quanh nơi chúng được định nghĩa.

function MyClass() { this.value = 42; setTimeout(() => { console.log(this.value); // Ghi 42 vì 'this' được kế thừa }, 100); } new MyClass();

Sự khác biệt giữa `let`, `const` và `var`

Sự khác biệt chính giữa let, constvar là gì?

Giải thích: var có phạm vi hàm và được hoisted. letconst có phạm vi khối và được hoisted nhưng nằm trong 'vùng chết tạm thời' cho đến khi khai báo. const phải được khởi tạo và không thể gán lại.

function scopeTest() { var a = 1; let b = 2; const c = 3; if (true) { var a = 10; // Khai báo lại và ảnh hưởng đến 'a' bên ngoài let b = 20; // 'b' mới trong khối // const c = 30; // Sẽ là một 'c' mới console.log(a, b, c); // 10, 20, 3 } console.log(a, b, c); // 10, 2, 3 } scopeTest();

Promise là gì?

Giải thích Promise trong JavaScript là gì.

Giải thích: Promise là một đối tượng đại diện cho việc hoàn thành (hoặc thất bại) cuối cùng của một thao tác bất đồng bộ và giá trị kết quả của nó. Nó có thể ở một trong ba trạng thái: đang chờ xử lý, đã hoàn thành hoặc đã từ chối.

const myPromise = new Promise((resolve, reject) => { setTimeout(() => { resolve('Thành công!'); // reject('Lỗi!'); }, 1000); }); myPromise .then(result => console.log(result)) .catch(error => console.error(error));

Sử dụng `async/await`

asyncawait đơn giản hóa việc làm việc với Promise như thế nào?

Giải thích: async/await cung cấp cú pháp đơn giản hóa cho Promise, làm cho mã bất đồng bộ trông và hoạt động giống mã đồng bộ hơn một chút. Một hàm async luôn trả về một Promise và await tạm dừng thực thi cho đến khi một Promise giải quyết.

function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function run() { console.log('Đang bắt đầu...'); await delay(1000); console.log('Đã đợi 1 giây.'); await delay(500); console.log('Đã đợi thêm 0,5 giây.'); return 'Đã hoàn thành!'; } run().then(console.log);

Chuyển đổi chuỗi thành số

Làm thế nào để chuyển đổi một chuỗi thành một số?

Giải thích: Sử dụng parseInt(), parseFloat() hoặc toán tử cộng một ngôi (+).

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

Chuyển đổi số thành chuỗi

Làm thế nào để chuyển đổi một số thành một chuỗi?

Giải thích: Sử dụng String(), number.toString() hoặc nối chuỗi.

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

`JSON.stringify` là gì?

JSON.stringify làm gì?

Giải thích: Nó chuyển đổi một đối tượng JavaScript hoặc giá trị thành một chuỗi JSON.

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

`JSON.parse` là gì?

JSON.parse làm gì?

Giải thích: Nó phân tích một chuỗi JSON, xây dựng giá trị hoặc đối tượng JavaScript được mô tả bởi chuỗi.

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

Toán tử Spread trong Mảng

Toán tử spread được sử dụng với mảng như thế nào?

Giải thích: Nó cho phép một iterable (như một mảng) được mở rộng ở những nơi mong đợi không hoặc nhiều đối số hoặc phần tử. Hữu ích cho việc sao chép, hợp nhất và thêm các phần tử.

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

Toán tử Spread trong Đối tượng

Toán tử spread được sử dụng với đối tượng như thế nào?

Giải thích: Nó cho phép sao chép và hợp nhất các thuộc tính đối tượng.

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 }

Giải cấu trúc Mảng

Giải thích giải cấu trúc mảng bằng một ví dụ.

Giải thích: Đó là một cú pháp cho phép giải nén các giá trị từ mảng thành các biến riêng biệt.

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

Giải cấu trúc Đối tượng

Giải thích giải cấu trúc đối tượng bằng một ví dụ.

Giải thích: Nó cho phép giải nén các thuộc tính từ đối tượng thành các biến riêng biệt.

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'

Thực hiện `call`

Làm thế nào bạn có thể thực hiện một phiên bản cơ bản của Function.prototype.call?

Giải thích: call gọi một hàm với giá trị this được chỉ định và các đối số được cung cấp riêng lẻ. Bạn có thể đính kèm hàm vào ngữ cảnh this, gọi nó, sau đó xóa nó.

Function.prototype.myCall = function(context, ...args) { context = context || window; const uniqueId = Symbol(); // Sử dụng một khóa duy nhất 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!

Thực hiện `apply`

Làm thế nào bạn có thể thực hiện một phiên bản cơ bản của Function.prototype.apply?

Giải thích: apply tương tự như call, nhưng nó chấp nhận các đối số dưới dạng một mảng.

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.

Giải thích Event Loop

Giải thích ngắn gọn Event Loop của JavaScript.

Giải thích: JavaScript là đơn luồng, nhưng nó đạt được tính đồng thời bằng cách sử dụng một vòng lặp sự kiện (event loop). Ngăn xếp cuộc gọi (call stack) xử lý mã đồng bộ. Các Web API xử lý các thao tác bất đồng bộ (như setTimeout, fetch). Khi một thao tác bất đồng bộ hoàn tất, hàm callback của nó sẽ chuyển vào hàng đợi callback (hoặc hàng đợi microtask cho Promise). Vòng lặp sự kiện liên tục kiểm tra xem ngăn xếp cuộc gọi có trống không; nếu có, nó sẽ di chuyển hàm callback tiếp theo từ hàng đợi vào ngăn xếp để thực thi.

console.log('Start'); setTimeout(() => { console.log('Timeout Callback'); // Goes to Callback Queue }, 0); Promise.resolve().then(() => { console.log('Promise Resolved'); // Goes to Microtask Queue }); console.log('End'); // Output Order: Start, End, Promise Resolved, Timeout Callback // (Microtasks run before Macrotasks/Callbacks)

Tìm kiếm nhị phân

Triển khai một hàm tìm kiếm nhị phân cho một mảng đã sắp xếp.

Giải thích: Tìm kiếm nhị phân tìm một mục trong một mảng đã sắp xếp một cách hiệu quả bằng cách liên tục chia đôi khoảng tìm kiếm. Nếu giá trị của khóa tìm kiếm nhỏ hơn mục ở giữa khoảng, thu hẹp khoảng về nửa dưới. Ngược lại, thu hẹp nó về nửa trên. Bạn thực hiện điều này cho đến khi tìm thấy giá trị hoặc khoảng trống.

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; // Found } else if (sortedArray[middle] < key) { start = middle + 1; // Search right half } else { end = middle - 1; // Search left half } } return -1; // Not found } console.log(binarySearch([1, 3, 5, 7, 9, 11], 7)); // 3 console.log(binarySearch([1, 3, 5, 7, 9, 11], 2)); // -1

Merge Sort

Triển khai thuật toán Merge Sort.

Giải thích: Merge Sort là một thuật toán chia để trị. Nó chia mảng đầu vào thành hai nửa, gọi đệ quy cho hai nửa, và sau đó gộp hai nửa đã sắp xếp lại. Hàm gộp (merge) rất quan trọng; nó kết hợp hai mảng con đã sắp xếp thành một mảng đã sắp xếp duy nhất.

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]

Quick Sort

Triển khai thuật toán Quick Sort.

Giải thích: Quick Sort cũng là một thuật toán chia để trị. Nó chọn một phần tử làm trục (pivot) và phân chia mảng đã cho xung quanh trục đã chọn. Các phần tử nhỏ hơn trục sẽ đi sang trái, và các phần tử lớn hơn sẽ đi sang phải. Sau đó nó sắp xếp đệ quy các mảng con.

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]

Bubble Sort

Triển khai thuật toán Bubble Sort.

Giải thích: Bubble Sort là một thuật toán sắp xếp đơn giản lặp đi lặp lại trong danh sách, so sánh các phần tử liền kề và hoán đổi chúng nếu chúng không đúng thứ tự. Việc duyệt qua danh sách được lặp lại cho đến khi danh sách được sắp xếp.

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]]; // Swap swapped = true; } } n--; // Optimization: last element is already in place } while (swapped); return arr; } console.log(bubbleSort([64, 34, 25, 12, 22, 11, 90])); // [11, 12, 22, 25, 34, 64, 90]

Xâu con dài nhất không có ký tự lặp lại

Cho một chuỗi, tìm độ dài của xâu con dài nhất không có ký tự lặp lại.

Giải thích: Sử dụng kỹ thuật cửa sổ trượt (sliding window). Duy trì một cửa sổ (xâu con) và một tập hợp các ký tự trong cửa sổ đó. Mở rộng cửa sổ bằng cách di chuyển con trỏ phải. Nếu tìm thấy một ký tự lặp lại, thu hẹp cửa sổ từ bên trái cho đến khi ký tự lặp lại được loại bỏ.

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

Triển khai danh sách liên kết

Triển khai một Danh sách liên kết đơn (Singly Linked List) với các phương thức add và print.

Giải thích: Một danh sách liên kết là một cấu trúc dữ liệu tuyến tính trong đó các phần tử không được lưu trữ ở các vị trí bộ nhớ liền kề. Mỗi phần tử (nút) trỏ đến phần tử tiếp theo. Bạn cần một lớp Node và một lớp LinkedList để quản lý đầu (head) và thêm các nút mới.

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

Triển khai cây tìm kiếm nhị phân (BST)

Triển khai một Cây tìm kiếm nhị phân (Binary Search Tree) với các phương thức insert và find.

Giải thích: Một BST là một cấu trúc dữ liệu cây nhị phân dựa trên nút có các thuộc tính sau: Cây con bên trái của một nút chỉ chứa các nút có khóa nhỏ hơn khóa của nút đó. Cây con bên phải của một nút chỉ chứa các nút có khóa lớn hơn khóa của nút đó. Cả hai cây con cũng phải là cây tìm kiếm nhị phân.

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

Xoay mảng

Viết một hàm để xoay một mảng sang phải k bước.

Giải thích: Xoay một mảng có nghĩa là di chuyển các phần tử của nó. Đối với xoay phải, phần tử cuối cùng trở thành phần tử đầu tiên. Lặp lại điều này k lần thì được nhưng không hiệu quả. Một cách tốt hơn là sử dụng thao tác mảng hoặc tính toán các vị trí mới.

function rotateArray(nums, k) { k = k % nums.length; // Handle cases where 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]

Tìm giao của hai mảng

Cho hai mảng, viết một hàm để tính giao của chúng (các phần tử chung của cả hai).

Giải thích: Một cách tiếp cận tốt là chuyển đổi một mảng thành một Set để tra cứu thời gian trung bình O(1). Sau đó, lặp qua mảng thứ hai và kiểm tra xem mỗi phần tử có tồn tại trong tập hợp không. Thu thập các kết quả khớp.

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]

Nhóm các từ đảo chữ

Cho một mảng các chuỗi, nhóm các từ đảo chữ lại với nhau.

Giải thích: Điều quan trọng là tìm một chữ ký duy nhất cho mỗi nhóm từ đảo chữ. Một cách phổ biến là sắp xếp mỗi từ theo thứ tự bảng chữ cái. Các từ có cùng chuỗi đã sắp xếp là các từ đảo chữ. Sử dụng một bản đồ băm (hash map) trong đó các khóa là các từ đã sắp xếp và các giá trị là các mảng các từ gốc.

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'])); // Output: [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]

Di chuyển số 0 về cuối

Cho một mảng nums, viết một hàm để di chuyển tất cả các số 0 về cuối trong khi vẫn giữ nguyên thứ tự tương đối của các phần tử khác 0.

Giải thích: Sử dụng phương pháp hai con trỏ hoặc quả cầu tuyết (snowball). Một con trỏ theo dõi vị trí mà phần tử khác 0 tiếp theo sẽ được đặt. Lặp qua mảng; nếu một phần tử khác 0, đặt nó vào vị trí của con trỏ và tăng con trỏ. Cuối cùng, điền phần còn lại bằng số 0.

function moveZeroes(nums) { let nonZeroIndex = 0; // Move all non-zero elements to the front for (let i = 0; i < nums.length; i++) { if (nums[i] !== 0) { nums[nonZeroIndex] = nums[i]; nonZeroIndex++; } } // Fill the rest with zeros 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]

Mảng con lớn nhất (Thuật toán Kadane)

Cho một mảng số nguyên nums, tìm mảng con liên tục (chứa ít nhất một số) có tổng lớn nhất và trả về tổng của nó.

Giải thích: Thuật toán Kadane là một cách hiệu quả để giải quyết vấn đề này. Lặp qua mảng, theo dõi tổng currentMax kết thúc ở vị trí hiện tại và tổng globalMax tìm thấy cho đến nay. Nếu currentMax trở nên âm, đặt lại nó về 0 (hoặc đúng hơn là phần tử hiện tại).

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 (from [4, -1, 2, 1])

Hoán vị của một chuỗi

Viết một hàm để tạo tất cả các hoán vị của một chuỗi đã cho.

Giải thích: Điều này thường được giải quyết bằng cách sử dụng đệ quy và quay lui. Đối với mỗi ký tự, cố định nó và đệ quy tạo hoán vị cho phần còn lại của chuỗi. Trường hợp cơ bản: khi chuỗi chỉ có một ký tự, trả về nó.

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)]; // Use Set to handle duplicate chars if needed } console.log(stringPermutations('abc')); // ['abc', 'acb', 'bac', 'bca', 'cab', 'cba'] console.log(stringPermutations('aab')); // ['aab', 'aba', 'baa']

Ước số chung lớn nhất (GCD)

Viết một hàm để tìm Ước số chung lớn nhất (GCD) của hai số.

Giải thích: Thuật toán Euclid là một phương pháp hiệu quả. Nếu b bằng 0, a là GCD. Ngược lại, GCD của a và b cũng giống như GCD của b và a % b (phần dư của a chia cho b).

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

Bội số chung nhỏ nhất (LCM)

Viết một hàm để tìm Bội số chung nhỏ nhất (LCM) của hai số.

Giải thích: LCM có thể được tính bằng cách sử dụng GCD với công thức: LCM(a, b) = |a * b| / GCD(a, b).

function gcd(a, b) { // Helper from previous problem 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

Triển khai Promise.all

Triển khai một hàm hoạt động giống như Promise.all.

Giải thích: Promise.all nhận một mảng các promise và trả về một promise duy nhất sẽ được giải quyết khi tất cả các promise đầu vào đã được giải quyết, hoặc bị từ chối nếu bất kỳ promise đầu vào nào bị từ chối. Giá trị được giải quyết là một mảng các giá trị được giải quyết.

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); // Reject immediately on any error }); }); } // Example usage: 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']

Đảo ngược cây nhị phân

Viết một hàm để đảo ngược một cây nhị phân.

Giải thích: Để đảo ngược một cây nhị phân, bạn hoán đổi các con trái và phải cho mỗi nút. Điều này có thể được thực hiện đệ quy hoặc lặp (sử dụng hàng đợi hoặc ngăn xếp).

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; } // Swap the children [root.left, root.right] = [root.right, root.left]; // Recur for left and right children invertTree(root.left); invertTree(root.right); return root; } // Example: 4 -> [2, 7] -> [1, 3, 6, 9] // Becomes: 4 -> [7, 2] -> [9, 6, 3, 1]

Độ sâu tối đa của cây nhị phân

Cho một cây nhị phân, tìm độ sâu tối đa của nó.

Giải thích: Độ sâu tối đa là số nút dọc theo đường dẫn dài nhất từ nút gốc đến nút lá xa nhất. Điều này có thể được tìm thấy đệ quy: MaxDepth = 1 + max(MaxDepth(trái), MaxDepth(phải)). Trường hợp cơ bản là khi một nút rỗng, độ sâu của nó là 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; } // Example: 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

Thời điểm tốt nhất để mua/bán cổ phiếu

Bạn được cung cấp một mảng prices trong đó prices[i] là giá của một cổ phiếu nhất định vào ngày thứ i. Bạn muốn tối đa hóa lợi nhuận của mình bằng cách chọn một ngày để mua một cổ phiếu và chọn một ngày khác trong tương lai để bán cổ phiếu đó. Trả về lợi nhuận tối đa.

Giải thích: Lặp qua các giá, theo dõi giá tối thiểu đã gặp cho đến nay (minPrice) và lợi nhuận tối đa đã tìm thấy cho đến nay (maxProfit). Đối với mỗi ngày, tính toán lợi nhuận tiềm năng nếu bạn bán hôm nay (giá hiện tại - minPrice) và cập nhật maxProfit nếu nó cao hơn.

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 (Buy at 1, Sell at 6) console.log(maxProfit([7, 6, 4, 3, 1])); // 0 (No profit possible)

Số duy nhất

Cho một mảng số nguyên không rỗng nums, mọi phần tử xuất hiện hai lần ngoại trừ một. Tìm số duy nhất đó.

Giải thích: Một cách rất hiệu quả để giải quyết vấn đề này là sử dụng toán tử bitwise XOR (^). XOR một số với chính nó sẽ cho kết quả là 0. XOR một số với 0 sẽ cho kết quả là chính số đó. Nếu bạn XOR tất cả các số trong mảng, các cặp sẽ triệt tiêu nhau (trở thành 0), chỉ còn lại số duy nhất.

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

Phần tử chiếm đa số

Cho một mảng nums có kích thước n, trả về phần tử chiếm đa số. Phần tử chiếm đa số là phần tử xuất hiện nhiều hơn ⌊n / 2⌋ lần.

Giải thích: Thuật toán bỏ phiếu Boyer-Moore là một cách hiệu quả. Khởi tạo một candidate và một count. Lặp qua mảng. Nếu count bằng 0, đặt phần tử hiện tại làm candidate. Nếu phần tử hiện tại khớp với candidate, tăng count; ngược lại, giảm 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

Leo cầu thang

Bạn đang leo cầu thang. Cần n bước để lên đến đỉnh. Mỗi lần bạn có thể leo 1 hoặc 2 bước. Hỏi có bao nhiêu cách khác nhau để bạn có thể leo lên đến đỉnh?

Giải thích: Đây là một bài toán quy hoạch động kinh điển, rất giống với dãy Fibonacci. Số cách để đạt được bước n là tổng số cách để đạt được bước n-1 (bằng cách đi 1 bước) và số cách để đạt được bước n-2 (bằng cách đi 2 bước). 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

Tích của mảng ngoại trừ chính nó

Cho một mảng số nguyên nums, trả về một mảng answer sao cho answer[i] bằng tích của tất cả các phần tử của nums ngoại trừ nums[i]. Không sử dụng toán tử chia.

Giải thích: Bạn có thể giải quyết vấn đề này bằng cách tính toán tích tiền tố và tích hậu tố. Kết quả tại chỉ số i là tích của tất cả các phần tử trước i nhân với tích của tất cả các phần tử sau i.

function productExceptSelf(nums) { const n = nums.length; const answer = new Array(n).fill(1); // Calculate prefix products let prefix = 1; for (let i = 0; i < n; i++) { answer[i] = prefix; prefix *= nums[i]; } // Calculate suffix products and multiply with prefix 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]

Số lượng đảo

Cho một bản đồ lưới 2D gồm '1's (đất liền) và '0's (nước), đếm số lượng đảo. Một hòn đảo được bao quanh bởi nước và được hình thành bằng cách nối các vùng đất liền kề theo chiều ngang hoặc chiều dọc.

Giải thích: Lặp qua từng ô của lưới. Nếu bạn tìm thấy một '1' (đất liền), tăng số lượng đảo và bắt đầu tìm kiếm theo chiều sâu (DFS) hoặc tìm kiếm theo chiều rộng (BFS) từ ô đó. Trong quá trình tìm kiếm, đánh dấu tất cả các '1' được kết nối là '0' (hoặc đã truy cập) để tránh đếm lại chúng.

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'; // Mark as visited 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 (Need to run on a copy or restore grid)

Bộ nhớ đệm LRU

Thiết kế và triển khai một cấu trúc dữ liệu cho Bộ nhớ đệm được sử dụng gần đây nhất (LRU). Nó phải hỗ trợ các hoạt động get và put.

Giải thích: Bộ nhớ đệm LRU loại bỏ mục được sử dụng ít nhất gần đây khi đạt đến dung lượng. Một triển khai phổ biến sử dụng Map (hoặc một bản đồ băm) để tra cứu O(1) trung bình và một Danh sách liên kết kép (Doubly Linked List) để cập nhật trạng thái 'được sử dụng gần đây' O(1). Map của JavaScript duy trì thứ tự chèn, điều này có thể đơn giản hóa một triển khai cơ bản.

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); // Remove to re-insert at the 'end' (most recent) 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); // Evicts 2 console.log(cache.get(2)); // -1

Tạo dấu ngoặc đơn

Cho n cặp dấu ngoặc đơn, viết một hàm để tạo tất cả các tổ hợp dấu ngoặc đơn đúng định dạng.

Giải thích: Đây là một bài toán quay lui kinh điển. Duy trì số lượng dấu ngoặc mở và đóng. Bạn có thể thêm một dấu ngoặc mở nếu open < n. Bạn có thể thêm một dấu ngoặc đóng nếu close < open. Trường hợp cơ bản là khi độ dài chuỗi đạt 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)); // ['((()))', '(()())', '(())()', '()(())', '()()()']

Bể chứa nước lớn nhất

Cho n số nguyên không âm a1, a2, ..., an, trong đó mỗi số đại diện cho một điểm tại tọa độ (i, ai). n đường thẳng đứng được vẽ sao cho hai điểm cuối của đường thẳng i nằm tại (i, ai) và (i, 0). Tìm hai đường thẳng, cùng với trục x tạo thành một thùng chứa, sao cho thùng chứa đó chứa được nhiều nước nhất.

Giải thích: Sử dụng phương pháp hai con trỏ. Bắt đầu với một con trỏ ở đầu và một con trỏ ở cuối. Tính toán diện tích. Diện tích bị giới hạn bởi đường ngắn hơn. Để có thể tìm thấy một diện tích lớn hơn, di chuyển con trỏ trỏ vào đường ngắn hơn vào bên trong.

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

Cho một mảng nums gồm n số nguyên, có các phần tử a, b, c trong nums sao cho a + b + c = 0? Tìm tất cả các bộ ba duy nhất trong mảng cho tổng bằng 0.

Giải thích: Đầu tiên, sắp xếp mảng. Sau đó, lặp qua mảng với một con trỏ i. Đối với mỗi i, sử dụng thêm hai con trỏ, left (bắt đầu từ i+1) và right (bắt đầu từ cuối), để tìm hai số có tổng bằng -nums[i]. Xử lý các bản sao để đảm bảo các bộ ba duy nhất.

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; // Skip duplicates for 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++; // Skip duplicates while (left < right && nums[right] === nums[right - 1]) right--; // Skip duplicates 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]]

Vị trí chèn tìm kiếm

Cho một mảng đã sắp xếp các số nguyên phân biệt và một giá trị mục tiêu, trả về chỉ mục nếu mục tiêu được tìm thấy. Nếu không, trả về chỉ mục mà nó sẽ được chèn vào theo thứ tự.

Giải thích: Đây là một biến thể của tìm kiếm nhị phân. Thực hiện tìm kiếm nhị phân, nhưng nếu không tìm thấy phần tử, con trỏ start sẽ kết thúc ở vị trí mà phần tử đó nên được chèn vào.

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; // If not found, start is the insertion point } 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

Gộp hai danh sách đã sắp xếp (Danh sách liên kết)

Gộp hai danh sách liên kết đã sắp xếp và trả về nó dưới dạng một danh sách mới đã sắp xếp. Danh sách mới nên được tạo bằng cách nối các nút của hai danh sách đầu tiên lại với nhau.

Giải thích: Sử dụng một nút đầu giả để đơn giản hóa quá trình. Sử dụng một con trỏ để xây dựng danh sách mới. So sánh các nút hiện tại của cả hai danh sách đầu vào và nối nút nhỏ hơn vào danh sách mới, di chuyển con trỏ của danh sách đó. Lặp lại cho đến khi một danh sách cạn kiệt, sau đó nối phần còn lại của danh sách kia.

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; } // Example: 1->2->4 and 1->3->4 => 1->1->2->3->4->4

Cây đối xứng

Cho một cây nhị phân, kiểm tra xem nó có phải là hình ảnh phản chiếu của chính nó không (tức là đối xứng quanh tâm của nó).

Giải thích: Điều này có thể được giải quyết đệ quy. Một cây là đối xứng nếu cây con bên trái là hình ảnh phản chiếu của cây con bên phải. Tạo một hàm trợ giúp isMirror(t1, t2) kiểm tra xem hai cây có phải là hình ảnh phản chiếu không. Nó nên xác minh: 1. t1.val === t2.val. 2. t1.left là hình ảnh phản chiếu của t2.right. 3. t1.right là hình ảnh phản chiếu của t2.left.

class Node { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } } function isSymmetric(root) { if (!root) return true; function isMirror(t1, t2) { if (!t1 && !t2) return true; if (!t1 || !t2 || t1.val !== t2.val) return false; return isMirror(t1.left, t2.right) && isMirror(t1.right, t2.left); } return isMirror(root.left, root.right); } // Example: 1 -> [2, 2] -> [3, 4, 4, 3] is symmetric.

Duyệt theo cấp độ (BST/Tree)

Cho một cây nhị phân, trả về cách duyệt theo cấp độ các giá trị nút của nó. (tức là, từ trái sang phải, từng cấp độ một).

Giải thích: Đây là Tìm kiếm theo chiều rộng (BFS). Sử dụng một hàng đợi. Bắt đầu bằng cách thêm gốc vào hàng đợi. Trong khi hàng đợi không trống, xử lý tất cả các nút ở cấp độ hiện tại. Đối với mỗi nút đã xử lý, thêm các con của nó (nếu có) vào hàng đợi cho cấp độ tiếp theo.

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; } // Example: 3 -> [9, 20] -> [null, null, 15, 7] // Output: [[3], [9, 20], [15, 7]]

Chuyển đổi mảng đã sắp xếp thành BST cân bằng chiều cao

Cho một mảng trong đó các phần tử được sắp xếp theo thứ tự tăng dần, chuyển đổi nó thành một cây tìm kiếm nhị phân (BST) cân bằng chiều cao.

Giải thích: Để tạo một BST cân bằng chiều cao, bạn nên chọn phần tử giữa của mảng làm gốc. Sau đó, đệ quy xây dựng cây con bên trái từ nửa bên trái của mảng và cây con bên phải từ nửa bên phải.

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); } // Example: [-10, -3, 0, 5, 9] // Output: A tree like [0, -3, 9, -10, null, 5, null]

Triển khai bind

Triển khai phiên bản Function.prototype.bind của riêng bạn.

Giải thích: bind tạo một hàm mới, khi được gọi, có từ khóa this của nó được đặt thành giá trị được cung cấp, với một chuỗi các đối số đã cho đứng trước bất kỳ đối số nào được cung cấp khi hàm mới được gọi. Sử dụng apply hoặc call trong một hàm được trả về, xử lý ứng dụng một phần (các đối số được đặt trước).

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

Tìm phần tử lớn thứ K

Tìm phần tử lớn thứ k trong một mảng chưa sắp xếp. Lưu ý rằng đó là phần tử lớn thứ k trong thứ tự đã sắp xếp, không phải phần tử phân biệt thứ k.

Giải thích: Một cách tiếp cận đơn giản là sắp xếp mảng và sau đó chọn phần tử tại chỉ số n - k. Một cách tiếp cận hiệu quả hơn cho các mảng lớn liên quan đến việc sử dụng một min-heap hoặc một thuật toán chọn như Quickselect.

function findKthLargest(nums, k) { // Simple approach: Sort nums.sort((a, b) => b - a); // Sort in descending order return nums[k - 1]; // Note: Quickselect would be more efficient in an interview // but sorting is easier to implement quickly. } 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

Kiểm tra xem đối tượng có thuộc tính không

Làm thế nào bạn có thể kiểm tra xem một đối tượng có một thuộc tính cụ thể không?

Giải thích: Bạn có thể sử dụng toán tử in (kiểm tra thuộc tính riêng và kế thừa), Object.prototype.hasOwnProperty.call(obj, prop) (chỉ kiểm tra thuộc tính riêng), hoặc đơn giản là obj.prop !== undefined (có thể khó khăn với các giá trị undefined).

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 is inherited)

Số nguyên thành số La Mã

Viết một hàm để chuyển đổi một số nguyên thành biểu diễn số La Mã của nó.

Giải thích: Tạo một ánh xạ các số La Mã và các giá trị tương ứng của chúng, được sắp xếp từ lớn nhất đến nhỏ nhất. Lặp qua ánh xạ này. Đối với mỗi giá trị, trừ nó khỏi số đầu vào càng nhiều lần càng tốt, nối thêm số La Mã mỗi lần.

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

Số La Mã thành số nguyên

Viết một hàm để chuyển đổi một số La Mã thành một số nguyên.

Giải thích: Tạo một bản đồ các ký hiệu La Mã thành các giá trị. Lặp qua chuỗi. Nếu giá trị của ký hiệu hiện tại nhỏ hơn giá trị của ký hiệu tiếp theo (như IV hoặc IX), trừ giá trị hiện tại; ngược lại, thêm nó.

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

Triển khai một Set

Triển khai một cấu trúc dữ liệu Set cơ bản (không sử dụng Set tích hợp sẵn) với các phương thức add, has, delete và size.

Giải thích: Bạn có thể sử dụng một đối tượng JavaScript (bản đồ băm) làm bộ nhớ bên dưới. Các khóa sẽ là các phần tử tập hợp (bạn có thể cần xử lý cách lưu trữ các loại khác nhau và đảm bảo tính duy nhất làm khóa).

class MySet { constructor() { this.items = {}; this._size = 0; } add(element) { if (!this.has(element)) { this.items[element] = element; // Store the 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

Tam giác Pascal

Cho một số nguyên numRows, tạo ra numRows đầu tiên của tam giác Pascal.

Giải thích: Trong tam giác Pascal, mỗi số là tổng của hai số trực tiếp phía trên nó. Bắt đầu với [[1]]. Đối với mỗi hàng tiếp theo, bắt đầu và kết thúc bằng 1. Mỗi phần tử ở giữa là tổng của phần tử ở cùng chỉ số và chỉ số trước đó từ hàng trên.

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

Tìm kiếm từ

Cho một bảng 2D và một từ, tìm xem từ đó có tồn tại trong lưới không. Từ có thể được tạo thành từ các chữ cái của các ô liền kề tuần tự, trong đó các ô 'liền kề' là hàng xóm theo chiều ngang hoặc chiều dọc.

Giải thích: Điều này yêu cầu một tìm kiếm theo chiều sâu (DFS) với quay lui. Lặp qua từng ô. Nếu một ô khớp với chữ cái đầu tiên của từ, hãy bắt đầu DFS. Hàm DFS kiểm tra các ô lân cận, đảm bảo chúng khớp với chữ cái tiếp theo và chưa được truy cập trong đường dẫn hiện tại. Nếu một đường dẫn thất bại, quay lui bằng cách bỏ đánh dấu ô.

function exist(board, word) { const rows = board.length; const cols = board[0].length; function dfs(r, c, index) { if (index === word.length) return true; // Word found if (r < 0 || c < 0 || r >= rows || c >= cols || board[r][c] !== word[index]) { return false; } const temp = board[r][c]; board[r][c] = '#'; // Mark as visited 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; // Backtrack 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

Xâu con cửa sổ tối thiểu

Cho hai chuỗi s và t, tìm cửa sổ tối thiểu trong s sẽ chứa tất cả các ký tự trong t. Nếu không có cửa sổ nào như vậy trong s bao phủ tất cả các ký tự trong t, trả về chuỗi trống.

Giải thích: Sử dụng phương pháp cửa sổ trượt với hai con trỏ (left và right) và các bản đồ băm. Một bản đồ lưu trữ số lượng ký tự cần thiết từ t. Một bản đồ khác lưu trữ số lượng trong cửa sổ hiện tại. Mở rộng cửa sổ với right. Khi cửa sổ chứa tất cả các ký tự cần thiết, cố gắng thu hẹp nó với left để tìm cửa sổ tối thiểu có thể.

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'

Đảo ngược danh sách liên kết

Cho head, đầu của một danh sách liên kết đơn, đảo ngược danh sách và trả về danh sách đã đảo ngược.

Giải thích: Bạn cần lặp qua danh sách, thay đổi con trỏ next của mỗi nút để trỏ đến nút trước đó. Theo dõi các nút previous, current và next trong quá trình lặp.

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; // Store next node current.next = prev; // Reverse current node's pointer prev = current; // Move prev one step forward current = next; // Move current one step forward } return prev; // New head is the last 'prev' } // Example: 1->2->3->4->5 becomes 5->4->3->2->1

Phát hiện chu trình trong danh sách liên kết

Cho head, đầu của một danh sách liên kết, xác định xem danh sách liên kết có chu trình trong đó không.

Giải thích: Sử dụng thuật toán Rùa và Thỏ của Floyd. Có hai con trỏ, một con di chuyển một bước một lần (slow) và một con di chuyển hai bước một lần (fast). Nếu có một chu trình, con trỏ fast cuối cùng sẽ bắt kịp con trỏ 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; // Reached end, no cycle slow = slow.next; fast = fast.next.next; } return true; // Pointers met, cycle exists } // Example: 3->2->0->-4 with -4 pointing back to 2. hasCycle returns true.

Triển khai Object.create

Triển khai một hàm mô phỏng hành vi của Object.create(proto).

Giải thích: Object.create tạo một đối tượng mới, sử dụng một đối tượng hiện có làm nguyên mẫu của đối tượng mới được tạo. Bạn có thể đạt được điều này bằng cách tạo một hàm tạo tạm thời, đặt nguyên mẫu của nó thành proto đầu vào, và sau đó trả về một phiên bản mới của nó.

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 là gì?

Giải thích hoisting trong JavaScript và cung cấp một ví dụ.

Giải thích: Hoisting là hành vi mặc định của JavaScript là di chuyển các khai báo lên đầu phạm vi hiện tại (toàn cục hoặc hàm) trước khi thực thi mã. Các khai báo biến (var) được hoisted và khởi tạo với undefined. Các khai báo hàm được hoisted hoàn toàn (cả tên và thân hàm). let và const được hoisted nhưng không được khởi tạo, dẫn đến Vùng chết tạm thời (Temporal Dead Zone).

console.log(myVar); // undefined (var is hoisted and initialized with undefined) // console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization (TDZ) myFunc(); // 'Hello!' (Function declaration is fully hoisted) var myVar = 'I am a var'; let myLet = 'I am a let'; function myFunc() { console.log('Hello!'); }

Giải thích Nổi bọt sự kiện và Bắt giữ sự kiện

Giải thích Nổi bọt sự kiện (Event Bubbling) và Bắt giữ sự kiện (Capturing) trong ngữ cảnh của DOM.

Giải thích: Đây là hai giai đoạn của sự lan truyền sự kiện trong HTML DOM. Giai đoạn Bắt giữ: Sự kiện đi xuống từ cửa sổ đến phần tử mục tiêu. Giai đoạn Nổi bọt: Sự kiện đi lên từ phần tử mục tiêu trở lại cửa sổ. Theo mặc định, hầu hết các trình xử lý sự kiện được đăng ký trong giai đoạn nổi bọt. Bạn có thể sử dụng addEventListener(type, listener, useCapture) với useCapture = true để xử lý các sự kiện trong giai đoạn bắt giữ.

// In an HTML structure: <div><p><span>Click Me</span></p></div> // If you click the <span>: // Capturing: window -> document -> html -> body -> div -> p -> span // Bubbling: span -> p -> div -> body -> html -> document -> window /* JS Example div.addEventListener('click', () => console.log('Div clicked'), true); // Capturing p.addEventListener('click', () => console.log('P clicked'), false); // Bubbling span.addEventListener('click', () => console.log('Span clicked'), false); // Bubbling // Output would be: Div clicked, Span clicked, P clicked */

Triển khai JSON.parse thủ công (cơ bản)

Thử triển khai một phiên bản rất cơ bản của JSON.parse (xử lý các đối tượng đơn giản, mảng, chuỗi, số).

Giải thích: Đây là một nhiệm vụ rất phức tạp khi thực hiện đầy đủ, nhưng đối với một thiết lập mã hóa trực tiếp, bạn có thể được yêu cầu xử lý một tập hợp con rất đơn giản. Bạn sẽ cần phân tích cú pháp chuỗi, xác định ranh giới đối tượng {} và mảng [], cặp khóa-giá trị ('key': value), và các kiểu dữ liệu cơ bản. eval hoặc new Function có thể gian lận điều này nhưng nguy hiểm. Một trình phân tích cú pháp thực sự sẽ sử dụng bộ phân tích từ vựng/bộ mã hóa và trình phân tích cú pháp.

function basicJsonParse(jsonString) { // WARNING: Using new Function is insecure like eval. // This is a simplified, INSECURE example for demonstration only. // A real implementation requires a proper parser. 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'] }

Làm phẳng một mảng lồng nhau sâu

Viết một hàm để làm phẳng một mảng lồng nhau sâu.

Giải thích: Không giống như làm phẳng trước đó (một cấp), điều này cần xử lý bất kỳ cấp độ lồng nhau nào. Đệ quy là một sự phù hợp tự nhiên. Lặp qua mảng. Nếu một phần tử là một mảng, gọi đệ quy làm phẳng nó và nối kết quả. Ngược lại, thêm phần tử.

function deepFlatten(arr) { let flattened = []; arr.forEach(item => { if (Array.isArray(item)) { flattened = flattened.concat(deepFlatten(item)); } else { flattened.push(item); } }); return flattened; // ES2019+ provides a much simpler way: // return arr.flat(Infinity); } console.log(deepFlatten([1, [2, [3, 4], 5], 6])); // [1, 2, 3, 4, 5, 6]

Triển khai một Trie (Cây tiền tố)

Triển khai cấu trúc dữ liệu Trie với các phương thức insert, search và startsWith.

Giải thích: Trie là một cấu trúc dữ liệu dạng cây được sử dụng để lưu trữ và truy xuất hiệu quả các khóa trong một tập dữ liệu chuỗi. Mỗi nút đại diện cho một ký tự, và các đường dẫn từ gốc đại diện cho các từ hoặc tiền tố.

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

Xáo trộn một mảng (Fisher-Yates)

Viết một hàm để xáo trộn một mảng tại chỗ bằng cách sử dụng thuật toán Fisher-Yates (hoặc Knuth).

Giải thích: Thuật toán Fisher-Yates lặp lại một mảng từ phần tử cuối cùng đến phần tử đầu tiên. Trong mỗi lần lặp, nó hoán đổi phần tử hiện tại với một phần tử được chọn ngẫu nhiên từ đầu mảng cho đến phần tử hiện tại (bao gồm).

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]

Kết hợp hàm

Triển khai một hàm compose nhận nhiều hàm và trả về một hàm mới áp dụng chúng từ phải sang trái.

Giải thích: Phép kết hợp hàm (f ∘ g)(x) = f(g(x)) áp dụng một hàm vào kết quả của hàm khác. Một compose(f, g, h) tổng quát sẽ có nghĩa là f(g(h(x))). Sử dụng reduceRight để triển khai thanh lịch.

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

Chuyển đổi hàm

Triển khai một hàm pipe nhận nhiều hàm và trả về một hàm mới áp dụng chúng từ trái sang phải.

Giải thích: Tương tự như compose, nhưng thứ tự áp dụng bị đảo ngược: pipe(f, g, h) có nghĩa là h(g(f(x))). Sử dụng reduce để triển khai.

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

Bài toán đổi tiền xu

Cho một mảng các mệnh giá tiền xu và một số tiền, tìm số lượng tiền xu tối thiểu để tạo thành số tiền đó. Giả sử có vô số mỗi loại tiền xu.

Giải thích: Đây là một bài toán quy hoạch động kinh điển. Tạo một mảng dp trong đó dp[i] lưu trữ số lượng tiền xu tối thiểu cần thiết cho số tiền i. dp[i] = min(dp[i - coin]) + 1 cho tất cả các loại tiền xu.

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

Tổ tiên chung thấp nhất (BST)

Cho một cây tìm kiếm nhị phân (BST), tìm tổ tiên chung thấp nhất (LCA) của hai nút đã cho trong BST.

Giải thích: LCA là nút sâu nhất có cả hai nút đã cho làm hậu duệ. Trong một BST, bạn có thể tìm thấy nó bằng cách duyệt từ gốc. Nếu cả hai nút đều nhỏ hơn nút hiện tại, hãy đi sang trái. Nếu cả hai đều lớn hơn, hãy đi sang phải. Nếu một nút nhỏ hơn và một nút lớn hơn (hoặc một nút là nút hiện tại), thì nút hiện tại là 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

Mã hóa và giải mã cây nhị phân

Thiết kế một thuật toán để mã hóa và giải mã một cây nhị phân.

Giải thích: Mã hóa chuyển đổi một cây thành một chuỗi hoặc mảng. Giải mã tái tạo cây. Một cách phổ biến là Duyệt tiền thứ tự (DFS). Sử dụng một ký tự đặc biệt (ví dụ: '#') cho các nút rỗng để duy trì cấu trúc.

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

Triển khai setInterval bằng setTimeout

Triển khai một hàm mySetInterval mô phỏng setInterval nhưng sử dụng setTimeout đệ quy.

Giải thích: setInterval chạy một hàm lặp đi lặp lại sau mỗi N mili giây. Bạn có thể đạt được điều này bằng cách cho một hàm tự gọi chính nó với setTimeout sau mỗi lần thực thi. Bạn cũng cần một cách để xóa nó (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);

Duyệt đồ thị (BFS & DFS)

Triển khai Tìm kiếm theo chiều rộng (BFS) và Tìm kiếm theo chiều sâu (DFS) cho một đồ thị đã cho (biểu diễn danh sách kề).

Giải thích: BFS khám phá các hàng xóm trước (sử dụng Hàng đợi), trong khi DFS khám phá càng xa càng tốt dọc theo mỗi nhánh (sử dụng Ngăn xếp hoặc đệ quy). Theo dõi các nút đã truy cập để tránh chu trình và công việc thừa.

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

Xoay ảnh (Ma trận)

Bạn được cung cấp một ma trận 2D n x n biểu diễn một hình ảnh. Xoay hình ảnh 90 độ (theo chiều kim đồng hồ) tại chỗ.

Giải thích: Một cách phổ biến để đạt được điều này là trước tiên chuyển vị ma trận (hoán đổi matrix[i][j] với matrix[j][i]) và sau đó đảo ngược mỗi hàng.

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

Duyệt Ma Trận Xoắn Ốc

Cho một ma trận m x n, trả về tất cả các phần tử của ma trận theo thứ tự xoắn ốc.

Giải thích: Sử dụng bốn con trỏ để xác định các ranh giới: top, bottom, left, right. Duyệt hàng trên cùng, sau đó cột phải, sau đó hàng dưới cùng, sau đó cột trái, thu hẹp các ranh giới sau mỗi lần duyệt, cho đến khi left vượt qua right hoặc top vượt qua 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]

Thiết Lập Ma Trận Về 0

Cho một ma trận m x n, nếu một phần tử là 0, hãy đặt toàn bộ hàng và cột của nó về 0. Thực hiện tại chỗ.

Giải thích: Một cách tiếp cận đơn giản đòi hỏi không gian phụ trợ O(m*n). Để thực hiện nó trong không gian O(1) (hoặc O(m+n)), bạn có thể sử dụng hàng đầu tiên và cột đầu tiên để lưu trữ thông tin về những hàng/cột nào cần được đặt về 0. Bạn cần các cờ riêng biệt cho việc liệu hàng/cột đầu tiên có cần được đặt về 0 hay không.

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; } // Ví dụ: [[1,1,1],[1,0,1],[1,1,1]] => [[1,0,1],[0,0,0],[1,0,1]]

Triển Khai Promise.race

Triển khai một hàm hoạt động giống như Promise.race.

Giải thích: Promise.race nhận một mảng các promise và trả về một promise duy nhất. Promise được trả về này sẽ giải quyết (resolve hoặc reject) ngay khi bất kỳ promise đầu vào nào giải quyết, với giá trị hoặc lý do từ promise đó.

function myPromiseRace(promises) { return new Promise((resolve, reject) => { if (!promises || promises.length === 0) { return; // Hoặc resolve/reject tùy thuộc vào quy định } 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'

Mẫu Singleton

Triển khai mẫu thiết kế Singleton trong JavaScript.

Giải thích: Mẫu Singleton đảm bảo rằng một lớp chỉ có một thể hiện duy nhất và cung cấp một điểm truy cập toàn cục đến nó. Điều này có thể đạt được bằng cách sử dụng một closure để giữ thể hiện.

const Singleton = (function() { let instance; function createInstance() { // Các phương thức và biến riêng tư const privateVar = 'Tôi là riêng tư'; function privateMethod() { console.log('Riêng tư'); } return { // Các phương thức và biến công khai publicVar: 'Tôi là công khai', publicMethod: function() { console.log('Công khai'); }, 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()); // 'Tôi là riêng tư'

Xác Thực Địa Chỉ IP

Viết một hàm để kiểm tra xem một chuỗi đã cho có phải là địa chỉ IPv4 hoặc IPv6 hợp lệ hay không.

Giải thích: Đối với IPv4, kiểm tra 4 số (0-255) được phân tách bằng dấu chấm, không có số 0 đứng đầu (ngoại trừ '0'). Đối với IPv6, kiểm tra 8 nhóm 1-4 chữ số thập lục phân được phân tách bằng dấu hai chấm (xử lý các biến thể như '::'). Regex có thể được sử dụng, nhưng phân tích cú pháp thủ công thường rõ ràng hơn cho các cuộc phỏng vấn.

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; // Đơn giản hóa: Không có '::' 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 (Đơn giản hóa) console.log(validIPAddress('256.256.256.256')); // Không phải cả hai

Tìm Phần Tử Đỉnh

Một phần tử đỉnh là một phần tử lớn hơn hẳn các phần tử lân cận của nó. Cho một mảng đầu vào nums, tìm một phần tử đỉnh và trả về chỉ số của nó.

Giải thích: Vì bất kỳ đỉnh nào cũng được, và nums[-1] và nums[n] được coi là -Infinity, bạn có thể sử dụng tìm kiếm nhị phân đã sửa đổi. Nếu nums[mid] nhỏ hơn nums[mid+1], một đỉnh phải tồn tại ở bên phải. Ngược lại, một đỉnh phải tồn tại ở bên trái (hoặc mid chính nó là một đỉnh).

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; // Đỉnh ở bên phải } else { right = mid; // Đỉnh là mid hoặc ở bên trái } } return left; // 'left' sẽ là chỉ số của một đỉnh } console.log(findPeakElement([1, 2, 3, 1])); // 2 (chỉ số của 3) console.log(findPeakElement([1, 2, 1, 3, 5, 6, 4])); // 5 (chỉ số của 6) hoặc 1 (chỉ số của 2)

Đếm Bit

Cho một số nguyên n, trả về một mảng ans có độ dài n + 1 sao cho với mỗi i (0 <= i <= n), ans[i] là số lượng bit 1 trong biểu diễn nhị phân của i.

Giải thích: Bạn có thể giải quyết vấn đề này bằng cách sử dụng quy hoạch động. Chú ý mối quan hệ: dp[i] = dp[i >> 1] + (i & 1). Số lượng bit 1 trong i là số lượng bit 1 trong i dịch phải (tức là i/2), cộng thêm 1 nếu i là số lẻ.

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); // Hoặc: dp[i] = dp[i & (i - 1)] + 1; (Xóa bit 1 cuối cùng) } return dp; } console.log(countBits(5)); // [0, 1, 1, 2, 1, 2]

Lũy Thừa Của Hai

Cho một số nguyên n, trả về true nếu nó là lũy thừa của hai. Ngược lại, trả về false.

Giải thích: Một lũy thừa của hai trong biểu diễn nhị phân chỉ có đúng một bit '1' (ví dụ: 1=1, 2=10, 4=100, 8=1000). Một mẹo bitwise thông minh là kiểm tra xem n > 0 và (n & (n - 1)) === 0. Nếu n là lũy thừa của hai, n-1 sẽ có tất cả các bit dưới bit '1' đó được đặt thành '1'. Phép AND chúng lại sẽ cho kết quả là 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

Ghép Khoảng

Cho một mảng các khoảng thời gian intervals trong đó intervals[i] = [starti, endi], ghép tất cả các khoảng thời gian chồng chéo, và trả về một mảng các khoảng thời gian không chồng chéo.

Giải thích: Đầu tiên, sắp xếp các khoảng thời gian dựa trên thời gian bắt đầu của chúng. Sau đó, lặp qua các khoảng thời gian đã sắp xếp. Nếu khoảng thời gian hiện tại chồng chéo với khoảng thời gian cuối cùng trong danh sách kết quả, hãy ghép chúng bằng cách cập nhật thời gian kết thúc của khoảng thời gian cuối cùng. Ngược lại, thêm khoảng thời gian hiện tại vào danh sách kết quả.

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]) { // Chồng chéo: ghép last[1] = Math.max(last[1], current[1]); } else { // Không chồng chéo: thêm khoảng thời gian mới merged.push(current); } } return merged; } console.log(mergeIntervals([[1, 3], [2, 6], [8, 10], [15, 18]])); // [[1, 6], [8, 10], [15, 18]]

Ngắt Từ

Cho một chuỗi s và một từ điển wordDict, trả về true nếu s có thể được phân đoạn thành một chuỗi các từ từ điển được phân tách bằng dấu cách.

Giải thích: Sử dụng quy hoạch động. Tạo một mảng boolean dp trong đó dp[i] là true nếu chuỗi con s[0...i-1] có thể được phân đoạn. dp[i] là true nếu tồn tại một j < i sao cho dp[j] là true và chuỗi con s[j...i-1] nằm trong từ điển.

function wordBreak(s, wordDict) { const wordSet = new Set(wordDict); const n = s.length; const dp = new Array(n + 1).fill(false); dp[0] = true; // Trường hợp cơ sở: chuỗi rỗng 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

Triển Khai Array.prototype.flat

Triển khai phiên bản của riêng bạn của Array.prototype.flat, tạo một mảng mới với tất cả các phần tử mảng con được nối vào đó một cách đệ quy đến một độ sâu được chỉ định.

Giải thích: Sử dụng đệ quy. Lặp qua mảng. Nếu một phần tử là một mảng và độ sâu hiện tại nhỏ hơn độ sâu được chỉ định, hãy gọi đệ quy flatten trên nó. Ngược lại, đẩy phần tử đó. Xử lý độ sâu mặc định là 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]

Đảo Ngược Các Từ Trong Một Chuỗi

Cho một chuỗi đầu vào s, đảo ngược thứ tự các từ.

Giải thích: Một 'từ' được định nghĩa là một chuỗi các ký tự không phải khoảng trắng. Cắt khoảng trắng ở đầu và cuối chuỗi, tách chuỗi bằng một hoặc nhiều khoảng trắng, lọc ra bất kỳ chuỗi rỗng nào do nhiều khoảng trắng gây ra, đảo ngược mảng và nối lại với một khoảng trắng duy nhất.

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'

Trình Phân Tích Chuỗi Truy Vấn

Viết một hàm để phân tích một chuỗi truy vấn URL thành một đối tượng.

Giải thích: Tách chuỗi bằng &. Đối với mỗi phần, tách bằng = để lấy khóa và giá trị. Giải mã các thành phần URI cho cả khóa và giá trị. Xử lý các trường hợp mà một khóa có thể xuất hiện nhiều lần (tạo một mảng) hoặc không có giá trị.

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 }

Sử Dụng Proxy Để Xác Thực

Minh họa cách sử dụng đối tượng Proxy để xác thực thuộc tính đối tượng.

Giải thích: Một Proxy cho phép bạn chặn và tùy chỉnh các hoạt động được thực hiện trên một đối tượng. Sử dụng bẫy set để xác thực giá trị trước khi gán chúng cho một thuộc tính. Ném lỗi nếu xác thực thất bại.

const validator = { set: function(obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { throw new TypeError('Tuổi không phải là số nguyên'); } if (value < 0 || value > 150) { throw new RangeError('Tuổi không hợp lệ'); } } // Hành vi mặc định: Đặt thuộc tính 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'; // Ném TypeError // personProxy.age = 200; // Ném RangeError

Phòng Họp

Cho một mảng các khoảng thời gian họp [[start1, end1], [start2, end2], ...], xác định xem một người có thể tham dự tất cả các cuộc họp hay không.

Giải thích: Nếu một người có thể tham dự tất cả các cuộc họp, điều đó có nghĩa là không có hai cuộc họp nào chồng chéo. Để kiểm tra điều này, hãy sắp xếp các khoảng thời gian theo thời gian bắt đầu của chúng. Sau đó, lặp qua các khoảng thời gian đã sắp xếp và kiểm tra xem thời gian bắt đầu của cuộc họp hiện tại có trước thời gian kết thúc của cuộc họp trước đó hay không. Nếu có, có một sự chồng chéo, và người đó không thể tham dự tất cả.

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; // Phát hiện chồng chéo } } return true; } console.log(canAttendMeetings([[0, 30], [5, 10], [15, 20]])); // false (5<30) console.log(canAttendMeetings([[7, 10], [2, 4]])); // true

Triển Khai Promise.any

Triển khai một hàm hoạt động giống như Promise.any.

Giải thích: Promise.any nhận một mảng các promise và trả về một promise duy nhất. Promise này sẽ được fulfilled ngay khi bất kỳ promise đầu vào nào được fulfilled. Nếu tất cả các promise đầu vào đều bị rejected, nó sẽ bị rejected với một AggregateError chứa tất cả các lý do rejection.

function myPromiseAny(promises) { return new Promise((resolve, reject) => { const numPromises = promises.length; if (numPromises === 0) { reject(new AggregateError([], 'Tất cả các promise đã bị từ chối')); return; } let rejectionCount = 0; const errors = []; promises.forEach((promise, index) => { Promise.resolve(promise) .then(resolve) // Resolve ngay khi một cái được fulfilled .catch(error => { errors[index] = error; rejectionCount++; if (rejectionCount === numPromises) { reject(new AggregateError(errors, 'Tất cả các promise đã bị từ chối')); } }); }); }); } // Ví dụ: myPromiseAny([Promise.reject('err1'), Promise.resolve('ok')]) -> 'ok' // Ví dụ: myPromiseAny([Promise.reject('err1'), Promise.reject('err2')]) -> AggregateError

Mẫu Observer

Triển khai mẫu thiết kế Observer (Pub/Sub) trong JavaScript.

Giải thích: Mẫu Observer định nghĩa một mối quan hệ phụ thuộc một-nhiều giữa các đối tượng. Khi một đối tượng (Chủ thể) thay đổi trạng thái, tất cả các đối tượng phụ thuộc của nó (Quan sát viên) được thông báo và cập nhật tự động. Chủ thể duy trì một danh sách các quan sát viên và cung cấp các phương thức để thêm, xóa và thông báo cho chúng.

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} đã nhận: ${data}`); } } const subject = new Subject(); const obs1 = new Observer('Quan sát viên 1'); const obs2 = new Observer('Quan sát viên 2'); subject.subscribe(obs1); subject.subscribe(obs2); subject.notify('Có gì đó đã xảy ra!'); // Quan sát viên 1 đã nhận: Có gì đó đã xảy ra! // Quan sát viên 2 đã nhận: Có gì đó đã xảy ra!

Tìm Số Trùng Lặp

Cho một mảng các số nguyên nums chứa n + 1 số nguyên trong đó mỗi số nguyên nằm trong phạm vi [1, n] bao gồm, tìm số bị lặp lại.

Giải thích: Vì các số nằm trong [1, n] và kích thước mảng là n+1, điều này đảm bảo có ít nhất một số trùng lặp. Điều này có thể được ánh xạ thành một vấn đề 'Tìm Chu Kỳ Trong Danh Sách Liên Kết' (Floyd's Tortoise and Hare). Coi các giá trị mảng là con trỏ: index -> nums[index].

function findDuplicate(nums) { let tortoise = nums[0]; let hare = nums[0]; // Giai đoạn 1: Tìm điểm giao nhau do { tortoise = nums[tortoise]; hare = nums[nums[hare]]; } while (tortoise !== hare); // Giai đoạn 2: Tìm lối vào chu kỳ 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

Bộ Lọc HTML Cơ Bản

Viết một hàm cơ bản để lọc một chuỗi HTML, loại bỏ các thẻ có khả năng gây hại (như <script>) trong khi giữ lại các thẻ an toàn (như <p>, <b>).

Giải thích: Đây là một chủ đề phức tạp, và các bộ lọc trong thế giới thực rất tinh vi. Đối với một cuộc phỏng vấn, một cách tiếp cận cơ bản có thể liên quan đến việc sử dụng Regex để loại bỏ các thẻ cụ thể hoặc chỉ cho phép một danh sách trắng các thẻ. Điều này không an toàn cho sản xuất.

function basicSanitize(html) { // CẢNH BÁO: Đây là một ví dụ rất cơ bản và không an toàn. // KHÔNG sử dụng cái này trong sản xuất. Hãy sử dụng một thư viện như DOMPurify. // Xóa các thẻ script let sanitized = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); // Ví dụ: Xóa các thuộc tính onclick (rất cơ bản) sanitized = sanitized.replace(/\s(on\w+)=['"]?[^'"]*['"]?/gi, ''); return sanitized; } console.log(basicSanitize('<p>Xin chào <script>alert("XSS")</script> <b onclick="danger()">Thế giới</b></p>')); // <p>Xin chào <b >Thế giới</b></p>

Khoảng Cách Chỉnh Sửa

Cho hai chuỗi word1 và word2, trả về số lượng phép toán tối thiểu (chèn, xóa hoặc thay thế) cần thiết để chuyển đổi word1 thành word2.

Giải thích: Đây là một bài toán quy hoạch động kinh điển (khoảng cách Levenshtein). Tạo một mảng 2D dp trong đó dp[i][j] là khoảng cách chỉnh sửa giữa i ký tự đầu tiên của word1 và j ký tự đầu tiên của word2. Quan hệ đệ quy bao gồm việc xem xét chi phí của việc chèn, xóa và thay thế.

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, // Xóa dp[i][j - 1] + 1, // Chèn dp[i - 1][j - 1] + cost // Thay thế/Khớp ); } } return dp[m][n]; } console.log(minDistance('horse', 'ros')); // 3 console.log(minDistance('intention', 'execution')); // 5

Dãy Con Tăng Dài Nhất (LIS)

Cho một mảng số nguyên nums, trả về độ dài của dãy con tăng chặt dài nhất.

Giải thích: Sử dụng quy hoạch động. Gọi dp[i] là độ dài của LIS kết thúc tại chỉ số i. Để tính dp[i], tìm tất cả j < i sao cho nums[j] < nums[i], và lấy dp[i] = 1 + max(dp[j]). Câu trả lời cuối cùng là giá trị lớn nhất trong mảng 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)

Bài Toán N-Queens

Bài toán N-Queens là bài toán đặt N quân hậu trên bàn cờ N×N sao cho không có hai quân hậu nào đe dọa nhau. Cho N, trả về một lời giải hợp lệ (hoặc tất cả).

Giải thích: Sử dụng thuật toán quay lui. Đặt quân hậu theo từng hàng. Đối với mỗi hàng, thử đặt quân hậu vào mỗi cột. Trước khi đặt, kiểm tra xem vị trí đề xuất có an toàn không (không bị tấn công bởi các quân hậu ở các hàng trước). Nếu an toàn, đặt nó và đệ quy cho hàng tiếp theo. Nếu đệ quy thất bại, quay lui (loại bỏ quân hậu) và thử cột tiếp theo.

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; // Kiểm tra cột for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) if (board[i][j] === 'Q') return false; // Kiểm tra đường chéo lên-trái for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) if (board[i][j] === 'Q') return false; // Kiểm tra đường chéo lên-phải 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] = '.'; // Quay lui } } } backtrack(0); return results; } console.log(solveNQueens(4)); // [ [ '.Q..', '...Q', 'Q...', '..Q.' ], [ '..Q.', 'Q...', '...Q', '.Q..' ] ]

Sử Dụng WeakMap Cho Dữ Liệu Riêng Tư

Minh họa cách sử dụng WeakMap để lưu trữ dữ liệu riêng tư cho các thể hiện lớp.

Giải thích: WeakMap cho phép bạn liên kết dữ liệu với một đối tượng theo cách không ngăn chặn việc thu gom rác nếu đối tượng không còn được tham chiếu. Điều này hữu ích để tạo các thành viên 'riêng tư' trong các lớp JavaScript trước khi các trường riêng tư gốc được phổ biến rộng rãi hoặc cho các trường hợp sử dụng cụ thể.

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' không phải là một thuộc tính công khai // console.log(privateData.get(person)); // Có thể truy cập, nhưng không trực tiếp qua 'person.'

Triển Khai Promise.allSettled

Triển khai một hàm hoạt động giống như Promise.allSettled.

Giải thích: Promise.allSettled nhận một mảng các promise và trả về một promise duy nhất. Promise này được fulfilled khi tất cả các promise đầu vào đã được settled (được fulfilled hoặc rejected). Giá trị fulfilled là một mảng các đối tượng, mỗi đối tượng mô tả kết quả của một promise ({status: 'fulfilled', value: ...} hoặc {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); } }); }); }); } // Ví dụ: myPromiseAllSettled([Promise.resolve(1), Promise.reject('err')]) // -> [{status: 'fulfilled', value: 1}, {status: 'rejected', reason: 'err'}]

Tìm Trung Vị Của Hai Mảng Đã Sắp Xếp

Cho hai mảng đã sắp xếp nums1 và nums2 có kích thước lần lượt là m và n, trả về trung vị của hai mảng đã sắp xếp.

Giải thích: Đây là một bài toán khó thường được giải bằng cách tiếp cận tìm kiếm nhị phân trên mảng nhỏ hơn. Mục tiêu là phân vùng cả hai mảng sao cho tất cả các phần tử ở phía bên trái nhỏ hơn hoặc bằng tất cả các phần tử ở phía bên phải, và số lượng phần tử ở bên trái bằng (hoặc hơn một) bên phải. Trung vị sau đó có thể được tính toán từ các phần tử biên.

function findMedianSortedArrays(nums1, nums2) { if (nums1.length > nums2.length) [nums1, nums2] = [nums2, nums1]; // Đảm bảo nums1 nhỏ hơn 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('Các mảng đầu vào không được sắp xếp'); } console.log(findMedianSortedArrays([1, 3], [2])); // 2.0 console.log(findMedianSortedArrays([1, 2], [3, 4])); // 2.5

Giải Sudoku

Viết một chương trình để giải câu đố Sudoku bằng cách điền vào các ô trống.

Giải thích: Sử dụng thuật toán quay lui. Tìm một ô trống. Thử điền nó với các số từ 1 đến 9. Đối với mỗi số, kiểm tra xem nó có hợp lệ không (không vi phạm các quy tắc Sudoku). Nếu hợp lệ, gọi đệ quy bộ giải. Nếu đệ quy trả về true, một giải pháp đã được tìm thấy. Nếu không, quay lui (đặt lại ô) và thử số tiếp theo.

function solveSudoku(board) { function isValid(row, col, numStr) { for (let i = 0; i < 9; i++) { if (board[row][i] === numStr) return false; // Kiểm tra hàng if (board[i][col] === numStr) return false; // Kiểm tra cột 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; // Kiểm tra ô vuông 3x3 } 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] = '.'; // Quay lui } } return false; // Không tìm thấy số hợp lệ } } } return true; // Bảng đã được giải } backtrack(); return board; } // Ví dụ yêu cầu một bảng 9x9 với '.' cho các ô trống.

Triển Khai Một Mẫu Middleware Cơ Bản

Triển khai một mẫu middleware đơn giản thường thấy trong các framework web.

Giải thích: Các hàm middleware xử lý một yêu cầu trước khi nó đến trình xử lý cuối cùng. Mỗi middleware có thể sửa đổi yêu cầu/phản hồi hoặc chuyển quyền kiểm soát cho middleware tiếp theo. Tạo một lớp hoặc đối tượng quản lý danh sách các hàm middleware và thực thi chúng theo trình tự.

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: Đang ghi nhật ký...'); req.logged = true; next(); }); app.use((req, next) => { console.log('2: Đang xác thực...'); req.authed = true; next(); }); app.use((req, next) => { console.log('3: Trình xử lý cuối cùng:', req); }); app.handle({ data: 'một số yêu cầu' }); // 1: Đang ghi nhật ký... // 2: Đang xác thực... // 3: Trình xử lý cuối cùng: { data: 'một số yêu cầu', logged: true, authed: true }

Phát Hiện Chu Trình Trong Đồ Thị Có Hướng

Cho một đồ thị có hướng, viết một hàm để xác định xem nó có chứa chu trình hay không.

Giải thích: Sử dụng Tìm kiếm theo chiều sâu (DFS). Duy trì hai tập hợp: visiting (các nút hiện đang trong stack đệ quy) và visited (các nút đã được khám phá hoàn toàn). Nếu bạn gặp một nút trong tập hợp visiting trong quá trình DFS, bạn đã tìm thấy một cạnh ngược, có nghĩa là có một chu trình.

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; // Phát hiện chu trình 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

Triển Khai Hành Vi Object.freeze

Giải thích Object.freeze và triển khai một phiên bản (nông).

Giải thích: Object.freeze ngăn chặn việc thêm các thuộc tính mới, xóa các thuộc tính hiện có, và thay đổi các thuộc tính hiện có hoặc khả năng liệt kê, cấu hình, hoặc ghi của chúng. Nó làm cho một đối tượng trở thành bất biến (nông). Bạn có thể triển khai nó bằng cách sử dụng Object.preventExtensions và Object.defineProperty để làm cho các thuộc tính hiện có không thể ghi và không thể cấu hình.

function myFreeze(obj) { Object.preventExtensions(obj); // Ngăn chặn việc thêm các thuộc tính mới 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; // Thất bại âm thầm (hoặc ném lỗi trong chế độ nghiêm ngặt) // delete obj.a; // Thất bại âm thầm (hoặc ném lỗi trong chế độ nghiêm ngặt) // obj.a = 10; // Thất bại âm thầm (hoặc ném lỗi trong chế độ nghiêm ngặt) console.log(obj); // { a: 1, b: 2 }

Sử Dụng requestAnimationFrame Để Tạo Hoạt Ảnh Mượt Mà

Giải thích requestAnimationFrame và chỉ ra cách sử dụng nó cho một vòng lặp hoạt ảnh đơn giản.

Giải thích: requestAnimationFrame (rAF) cho trình duyệt biết bạn muốn thực hiện một hoạt ảnh và yêu cầu trình duyệt gọi một hàm được chỉ định để cập nhật hoạt ảnh trước lần vẽ lại tiếp theo. Nó hiệu quả và mượt mà hơn so với việc sử dụng setTimeout hoặc setInterval cho các hoạt ảnh, vì nó đồng bộ hóa với tốc độ làm mới của trình duyệt và tạm dừng khi tab không hiển thị.

// 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; // Di chuyển 100px trong 2 giây (50px/giây) position = (elapsedTime / 2000) * 100; if (position < 200) { // Tiếp tục hoạt ảnh cho đến khi đạt 200px box.style.left = position + 'px'; requestAnimationFrame(animate); } else { box.style.left = '200px'; } } // Bắt đầu hoạt ảnh // requestAnimationFrame(animate); // Bỏ comment để chạy trong trình duyệt console.log('Sử dụng requestAnimationFrame cho hoạt ảnh trình duyệt.');

Triển Khai Array.prototype.some

Triển khai phiên bản của riêng bạn của Array.prototype.some.

Giải thích: some kiểm tra xem ít nhất một phần tử trong mảng có vượt qua bài kiểm tra được triển khai bởi hàm được cung cấp hay không. Nó trả về true nếu nó tìm thấy một phần tử mà hàm callback trả về true; ngược lại, nó trả về false.

function mySome(arr, callback, thisArg) { for (let i = 0; i < arr.length; i++) { if (i in arr) { // Xử lý các mảng thưa 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

Triển Khai Array.prototype.every

Triển khai phiên bản của riêng bạn của Array.prototype.every.

Giải thích: every kiểm tra xem tất cả các phần tử trong mảng có vượt qua bài kiểm tra được triển khai bởi hàm được cung cấp hay không. Nó trả về true nếu tất cả các phần tử đều vượt qua (hoặc nếu mảng trống), và false nếu bất kỳ phần tử nào không vượt qua.

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

Triển Khai Array.prototype.findIndex

Triển khai phiên bản của riêng bạn của Array.prototype.findIndex.

Giải thích: findIndex trả về chỉ số của phần tử đầu tiên trong mảng thỏa mãn hàm kiểm tra được cung cấp. Ngược lại, nó trả về -1, cho biết không có phần tử nào vượt qua bài kiểm tra.

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 Là Gì?

Giải thích Web Workers là gì và trường hợp sử dụng chính của chúng.

Giải thích: Web Workers là một phương tiện để nội dung web chạy các script trong các luồng nền. Trường hợp sử dụng chính là để giảm tải các tác vụ chạy lâu hoặc tốn nhiều tài nguyên tính toán khỏi luồng chính (xử lý cập nhật UI và tương tác người dùng), do đó ngăn chặn UI trở nên không phản hồi hoặc bị đóng băng. Workers giao tiếp với luồng chính thông qua truyền thông báo (postMessage và onmessage).

// main.js /* if (window.Worker) { const myWorker = new Worker('worker.js'); myWorker.postMessage([5, 3]); // Gửi dữ liệu đến worker myWorker.onmessage = function(e) { console.log('Kết quả từ worker:', e.data); } } else { console.log('Trình duyệt của bạn không hỗ trợ Web Workers.'); } */ // worker.js /* self.onmessage = function(e) { console.log('Tin nhắn nhận được từ script chính'); const result = e.data[0] * e.data[1]; console.log('Đăng kết quả trở lại script chính'); self.postMessage(result); } */ console.log('Web Workers chạy script trong các luồng nền.');

Giải Mã Các Cách

Một tin nhắn chứa các chữ cái từ A-Z có thể được mã hóa thành các số bằng cách sử dụng ánh xạ 'A' -> 1, 'B' -> 2, ..., 'Z' -> 26. Cho một chuỗi s chỉ chứa các chữ số, trả về số cách để giải mã nó.

Giải thích: Sử dụng quy hoạch động. Gọi dp[i] là số cách để giải mã s[0...i-1]. dp[i] phụ thuộc vào dp[i-1] (nếu s[i-1] là một mã 1 chữ số hợp lệ) và dp[i-2] (nếu s[i-2...i-1] là một mã 2 chữ số hợp lệ).

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' hoặc 'L') console.log(numDecodings('226')); // 3 ('BBF', 'BZ', 'VF') console.log(numDecodings('06')); // 0

Phép Cộng Bitwise (không dùng +)

Viết một hàm để cộng hai số nguyên mà không sử dụng toán tử +.

Giải thích: Sử dụng các toán tử bitwise. Tổng có thể được tính bằng a ^ b (tổng không có bit nhớ), và bit nhớ có thể được tính bằng (a & b) << 1. Lặp lại quá trình này cho đến khi bit nhớ trở thành 0.

function getSum(a, b) { while (b !== 0) { const carry = (a & b) << 1; // Tính bit nhớ a = a ^ b; // Tính tổng không có bit nhớ b = carry; // Bit nhớ trở thành 'b' mới } return a; } console.log(getSum(3, 5)); // 8 console.log(getSum(-2, 3)); // 1

Kiểm Tra Số Đối Xứng (Số)

Cho một số nguyên x, trả về true nếu x là số đối xứng, và false nếu không.

Giải thích: Một số âm không phải là số đối xứng. Chuyển đổi số thành chuỗi và kiểm tra xem chuỗi có phải là số đối xứng không (đọc giống nhau từ trái sang phải và ngược lại). Hoặc, đảo ngược số bằng toán học và so sánh.

function isPalindromeNumber(x) { if (x < 0) return false; // Cách tiếp cận chuỗi // return String(x) === String(x).split('').reverse().join(''); // Cách tiếp cận toán học 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

Mẫu Factory

Triển khai mẫu thiết kế Factory trong JavaScript.

Giải thích: Mẫu Factory cung cấp một giao diện để tạo đối tượng trong một lớp cha, nhưng cho phép các lớp con thay đổi loại đối tượng sẽ được tạo. Nó hữu ích để tạo đối tượng mà không cần chỉ định lớp chính xác.

class Car { constructor(options) { this.type = 'Ô tô'; this.doors = options.doors || 4; } } class Truck { constructor(options) { this.type = 'Xe tải'; this.bedSize = options.bedSize || 'dài'; } } 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('Loại phương tiện không xác định'); } } } const factory = new VehicleFactory(); const car = factory.createVehicle({ vehicleType: 'car', doors: 2 }); const truck = factory.createVehicle({ vehicleType: 'truck', bedSize: 'ngắn' }); console.log(car); // Car { type: 'Ô tô', doors: 2 } console.log(truck); // Truck { type: 'Xe tải', bedSize: 'ngắn' }

Giải Thích Symbol Và Trường Hợp Sử Dụng

Giải thích Symbol là gì trong JavaScript và cung cấp một trường hợp sử dụng, chẳng hạn như tạo các thuộc tính 'riêng tư' hoặc các khóa đối tượng duy nhất.

Giải thích: Symbol là một kiểu dữ liệu nguyên thủy được giới thiệu trong ES6. Các thể hiện của nó là duy nhất và bất biến. Chúng thường được sử dụng làm khóa cho các thuộc tính đối tượng để tránh xung đột tên giữa các thư viện khác nhau hoặc để tạo các thuộc tính giả riêng tư (mặc dù không thực sự riêng tư, chúng không thể truy cập được thông qua lặp thông thường hoặc Object.keys).

const _privateName = Symbol('name'); const _privateMethod = Symbol('greet'); class Person { constructor(name) { this[_privateName] = name; } [_privateMethod]() { console.log(`Xin chào, tên tôi là ${this[_privateName]}`); } introduce() { this[_privateMethod](); } } const p = new Person('Bob'); p.introduce(); // Xin chào, tên tôi là Bob console.log(Object.keys(p)); // [] - Các Symbol không được bao gồm console.log(p[_privateName]); // Bob (Có thể truy cập nếu bạn có Symbol)

Chuyển Đổi Arguments Thành Một Mảng Thực Sự

Làm thế nào bạn có thể chuyển đổi đối tượng arguments (có sẵn trong các hàm không phải arrow) thành một mảng JavaScript thực sự?

Giải thích: Đối tượng arguments trông giống như một mảng nhưng không phải là một mảng; nó thiếu các phương thức như map, filter, v.v. Bạn có thể chuyển đổi nó bằng cách sử dụng:

  1. Array.from(arguments) (ES6)
  2. [...arguments] (Cú pháp Spread ES6)
  3. Array.prototype.slice.call(arguments) (Cách cũ)
function sumAll() { // 1. Array.from const args1 = Array.from(arguments); console.log('Sử dụng Array.from:', args1.reduce((a, b) => a + b, 0)); // 2. Spread Syntax const args2 = [...arguments]; console.log('Sử dụng Spread:', args2.reduce((a, b) => a + b, 0)); // 3. Slice const args3 = Array.prototype.slice.call(arguments); console.log('Sử dụng Slice:', args3.reduce((a, b) => a + b, 0)); } sumAll(1, 2, 3, 4); // Sử dụng Array.from: 10 // Sử dụng Spread: 10 // Sử dụng Slice: 10