Find the longest common prefix string amongst an array of strings. If there is no common prefix, return an empty string "".
Core Idea: Vertical Scan (Character by Character Comparison)
The most intuitive and efficient approach is to compare the strings character by character, from left to right. We assume the first string is our initial candidate for the common prefix, and then we verify this prefix against all other strings.
/**
* @param {string[]} strs
* @return {string}
*/
function longestCommonPrefix(strs) {
if (strs.length === 0) {
return "";
}
if (strs.length === 1) {
return strs[0];
}
for (let charIndex = 0; charIndex < strs[0].length; charIndex++) {
const currentChar = strs[0][charIndex];
for (let strIndex = 1; strIndex < strs.length; strIndex++) {
if (charIndex === strs[strIndex].length || strs[strIndex][charIndex] !== currentChar) {
return strs[0].substring(0, charIndex);
}
}
}
return strs[0];
}
Given an input string `s`, reverse the order of the words. Words are separated by at least one space. Return a string of words in reverse order concatenated by a single space, with no extra spaces.
Core Idea: Split, Filter, Reverse, Join
The most straightforward way is to split the string into words, filter out empty strings, reverse the array of words, and then join them back.
/**
* @param {string} s
* @return {string}
*/
function reverseWords(s) {
const words = s.split(/\s+/);
words.reverse();
return words.join(' ');
}
Alternative Core Idea: Two-Pointer Approach (More "In-Place")
This method involves cleaning spaces, reversing the entire character array, then reversing each word individually.
/**
* Helper function to clean up spaces:
* - Removes leading/trailing spaces.
* - Reduces multiple spaces between words to a single space.
* @param {string} s - The input string.
* @returns {character[]} - A character array with cleaned spaces.
*/
function cleanSpaces(s) {
let left = 0;
let right = s.length - 1;
while (left <= right && s[left] === ' ') {
left++;
}
while (left <= right && s[right] === ' ') {
right--;
}
const charArray = [];
for (let i = left; i <= right; i++) {
if (s[i] !== ' ') {
charArray.push(s[i]);
} else if (charArray.length > 0 && charArray[charArray.length - 1] !== ' ') {
charArray.push(' ');
}
}
return charArray;
}
/**
* Helper function to reverse characters within a specified range in an array in-place.
* @param {character[]} arr - The character array.
* @param {number} start - The starting index of the range (inclusive).
* @param {number} end - The ending index of the range (inclusive).
*/
function reverseCharRange(arr, start, end) {
while (start < end) {
[arr[start], arr[end]] = [arr[end], arr[start]];
start++;
end--;
}
}
/**
* @param {string} s
* @return {string}
*/
function reverseWordsTwoPointer(s) {
const charArray = cleanSpaces(s);
if (charArray.length === 0) {
return "";
}
reverseCharRange(charArray, 0, charArray.length - 1);
let wordStart = 0;
while (wordStart < charArray.length) {
let wordEnd = wordStart;
while (wordEnd < charArray.length && charArray[wordEnd] !== ' ') {
wordEnd++;
}
reverseCharRange(charArray, wordStart, wordEnd - 1);
wordStart = wordEnd + 1;
}
return charArray.join('');
}
Given two strings `needle` and `haystack`, return the index of the first occurrence of `needle` in `haystack`, or `-1` if `needle` is not part of `haystack`.
Core Idea: Brute-Force (Window Sliding and Comparison)
Slide a window of `needle`'s length across `haystack` and compare the substring within that window to `needle`.
/**
* @param {string} haystack
* @param {string} needle
* @return {number}
*/
function strStr(haystack, needle) {
if (needle.length === 0) {
return 0;
}
if (haystack.length < needle.length) {
return -1;
}
for (let i = 0; i <= haystack.length - needle.length; i++) {
let match = true;
for (let j = 0; j < needle.length; j++) {
if (haystack[i + j] !== needle[j]) {
match = false;
break;
}
}
if (match) {
return i;
}
}
return -1;
}
Given two strings `ransomNote` and `magazine`, return `true` if `ransomNote` can be constructed by using the letters from `magazine` and `false` otherwise. Each letter in `magazine` can only be used once in `ransomNote`.
Core Idea: Character Frequency Counting
Count character frequencies in `magazine`, then "consume" them while iterating through `ransomNote`. Order does not matter.
/**
* @param {string} ransomNote
* @param {string} magazine
* @return {boolean}
*/
function canConstruct(ransomNote, magazine) {
if (ransomNote.length > magazine.length) { // Optimization
return false;
}
const magazineFreqMap = new Map();
for (const char of magazine) {
magazineFreqMap.set(char, (magazineFreqMap.get(char) || 0) + 1);
}
for (const charOfNote of ransomNote) {
if (!magazineFreqMap.has(charOfNote) || magazineFreqMap.get(charOfNote) === 0) {
return false;
}
magazineFreqMap.set(charOfNote, magazineFreqMap.get(charOfNote) - 1);
}
return true;
}
Given two strings `s` and `t`. String `t` is generated by random shuffling string `s` and then add one more letter at a random position. Return the letter that was added to `t`.
Core Idea: Bitwise XOR
XORing all character codes in `s` and `t` will cancel out common characters, leaving only the added one.
/**
* @param {string} s
* @param {string} t
* @return {character}
*/
function findTheDifference(s, t) {
let charCodeSum = 0;
for (let i = 0; i < s.length; i++) {
charCodeSum ^= s.charCodeAt(i);
}
for (let i = 0; i < t.length; i++) {
charCodeSum ^= t.charCodeAt(i);
}
return String.fromCharCode(charCodeSum);
}
Alternative Core Idea: Frequency Map
Count frequencies in `s`, then decrement using `t`. The character that causes a count to go to zero or negative is the added one.
/**
* @param {string} s
* @param {string} t
* @return {character}
*/
function findTheDifferenceMap(s, t) {
const charCounts = new Map();
for (const char of s) {
charCounts.set(char, (charCounts.get(char) || 0) + 1);
}
for (const char of t) {
if (!charCounts.has(char) || charCounts.get(char) === 0) {
return char;
}
charCounts.set(char, charCounts.get(char) - 1);
}
return ''; // Should not be reached
}
Given an array of strings `words`, return the words that can be typed using letters of the alphabet on only one row of American keyboard. Case-insensitive.
Core Idea: Pre-map Characters to Rows and Validate Each Word
Create a map from characters to their keyboard row. Then, for each word, check if all its characters belong to the same row.
/**
* @param {string[]} words
* @return {string[]}
*/
function findWords(words) {
const resultWords = [];
const charToRowMap = new Map();
"qwertyuiop".split('').forEach(char => charToRowMap.set(char, 1));
"asdfghjkl".split('').forEach(char => charToRowMap.set(char, 2));
"zxcvbnm".split('').forEach(char => charToRowMap.set(char, 3));
for (const word of words) {
if (word.length === 0) continue;
const lowerCaseWord = word.toLowerCase();
const expectedRow = charToRowMap.get(lowerCaseWord[0]);
if (expectedRow === undefined) continue; // Should not happen with letter inputs
let isValidWord = true;
for (let i = 1; i < lowerCaseWord.length; i++) {
const char = lowerCaseWord[i];
const actualRow = charToRowMap.get(char);
if (actualRow !== expectedRow) {
isValidWord = false;
break;
}
}
if (isValidWord) {
resultWords.push(word);
}
}
return resultWords;
}
Given two strings `s` and `t`, determine if they are isomorphic. Isomorphic means characters in `s` can be replaced to get `t`, preserving order, with consistent and unique mappings.
Core Idea: Using Two Hash Maps for Bi-directional Mapping
Use two maps: `sToTMap` (s char to t char) and `tToSMap` (t char to s char) to ensure both consistency and uniqueness of mappings.
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
function isIsomorphic(s, t) {
if (s.length !== t.length) {
return false;
}
const sToTMap = new Map();
const tToSMap = new Map();
for (let i = 0; i < s.length; i++) {
const sChar = s[i];
const tChar = t[i];
if (sToTMap.has(sChar)) {
if (sToTMap.get(sChar) !== tChar) {
return false;
}
} else {
if (tToSMap.has(tChar)) {
return false;
}
sToTMap.set(sChar, tChar);
tToSMap.set(tChar, sChar);
}
}
return true;
}
Given a `pattern` and a string `s`, find if `s` follows the same pattern. A bijection must exist between a letter in `pattern` and a non-empty word in `s`.
Core Idea: Using Two Hash Maps for Bi-directional Mapping (Char to Word)
Similar to Isomorphic Strings, but mapping characters to words. Use `charToWord` and `wordToChar` maps.
/**
* @param {string} pattern
* @param {string} s
* @return {boolean}
*/
function wordPattern(pattern, s) {
const words = s.split(' ');
if (pattern.length !== words.length) {
return false;
}
const charToWord = new Map();
const wordToChar = new Map();
for (let i = 0; i < pattern.length; i++) {
const pChar = pattern[i];
const currentWord = words[i];
if (charToWord.has(pChar)) {
if (charToWord.get(pChar) !== currentWord) {
return false;
}
} else {
if (wordToChar.has(currentWord)) {
return false;
}
charToWord.set(pChar, currentWord);
wordToChar.set(currentWord, pChar);
}
}
return true;
}
Given an array of positive integers `nums` and a positive integer `target`, return the minimal length of a subarray whose sum is greater than or equal to `target`. If no such subarray, return `0`.
Core Idea: Sliding Window
Use two pointers (`left`, `right`) to define a window. Expand the window by moving `right`. When sum meets target, shrink from `left` to find minimum length, updating `minLength` along the way.
/**
* @param {number} target
* @param {number[]} nums
* @return {number}
*/
function minSubArrayLen(target, nums) {
let left = 0;
let currentSum = 0;
let minLength = Infinity;
for (let right = 0; right < nums.length; right++) {
currentSum += nums[right];
while (currentSum >= target) {
minLength = Math.min(minLength, right - left + 1);
currentSum -= nums[left];
left++;
}
}
return minLength === Infinity ? 0 : minLength;
}
Given `heights` (current order) and `expected` (sorted order), return the number of indices where `heights[i] != expected[i]`.
Core Idea (Optimal): Counting Sort Principles
Leverage the small, fixed range of heights (1-100) to count frequencies and implicitly reconstruct the sorted order without a full sort.
/**
* @param {number[]} heights
* @return {number}
*/
function heightCheckerOptimal(heights) {
let mismatches = 0;
const heightCounts = new Array(101).fill(0);
for (const height of heights) {
heightCounts[height]++;
}
let currentHeightsIndex = 0;
for (let expectedHeight = 1; expectedHeight <= 100; expectedHeight++) {
while (heightCounts[expectedHeight] > 0) {
if (heights[currentHeightsIndex] !== expectedHeight) {
mismatches++;
}
heightCounts[expectedHeight]--;
currentHeightsIndex++;
}
}
return mismatches;
}
Given two integer arrays `nums1` and `nums2`, return an array of their intersection. Each element in the result must be unique.
Core Idea: Using a Hash Set
Convert one array into a hash set for $O(1)$ average time lookups, then iterate through the second array, adding common elements to a result set.
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
function intersection(nums1, nums2) {
const nums1Set = new Set(nums1);
const intersectionSet = new Set();
for (const num of nums2) {
if (nums1Set.has(num)) {
intersectionSet.add(num);
}
}
return Array.from(intersectionSet);
}
Given a string `s` containing just `'(', ')', '{', '}', '['` and `']'`, determine if the input string is valid (brackets closed by same type, in correct order, every close has open).
Core Idea: Using a Stack
A stack's LIFO property perfectly matches parenthesis matching: the most recently opened bracket must be the first one closed.
/**
* @param {string} s
* @return {boolean}
*/
function isValid(s) {
const stack = [];
const bracketMap = {
'(': ')',
'{': '}',
'[': ']'
};
for (let i = 0; i < s.length; i++) {
const char = s[i];
if (bracketMap.hasOwnProperty(char)) {
stack.push(char);
} else {
if (stack.length === 0) {
return false;
}
const lastOpenBracket = stack.pop();
if (bracketMap[lastOpenBracket] !== char) {
return false;
}
}
}
return stack.length === 0;
}
Implement `MyHashMap` with `put(key, value)`, `get(key)`, `remove(key)` without using built-in hash table libraries.
Core Idea: Separate Chaining with Array of Lists
Use an underlying array of "buckets". Each bucket stores a list of `[key, value]` pairs to handle collisions. A simple modulo hash function is used.
class MyHashMap {
constructor() {
this.BUCKET_SIZE = 1000;
this.buckets = new Array(this.BUCKET_SIZE);
for (let i = 0; i < this.BUCKET_SIZE; i++) {
this.buckets[i] = [];
}
}
_hash(key) {
return key % this.BUCKET_SIZE;
}
put(key, value) {
const index = this._hash(key);
const bucket = this.buckets[index];
for (let i = 0; i < bucket.length; i++) {
const pair = bucket[i];
if (pair[0] === key) {
pair[1] = value;
return;
}
}
bucket.push([key, value]);
}
get(key) {
const index = this._hash(key);
const bucket = this.buckets[index];
for (let i = 0; i < bucket.length; i++) {
const pair = bucket[i];
if (pair[0] === key) {
return pair[1];
}
}
return -1;
}
remove(key) {
const index = this._hash(key);
const bucket = this.buckets[index];
for (let i = 0; i < bucket.length; i++) {
const pair = bucket[i];
if (pair[0] === key) {
bucket.splice(i, 1);
return;
}
}
}
}
Given a `dictionary` of roots and a `sentence`, replace all derivatives in the sentence with the shortest root forming it. Return the sentence after replacement.
Core Idea: Trie (Prefix Tree) for Efficient Root Lookup
A Trie allows efficient prefix matching and naturally helps find the shortest root by stopping at the first `isEndOfWord` node encountered during traversal.
class TrieNode {
constructor() {
this.children = {};
this.isEndOfWord = false;
}
}
/**
* @param {string[]} dictionary
* @param {string} sentence
* @return {string}
*/
function replaceWords(dictionary, sentence) {
const root = new TrieNode();
for (const rootWord of dictionary) {
let currentNode = root;
for (const char of rootWord) {
if (!currentNode.children[char]) {
currentNode.children[char] = new TrieNode();
}
currentNode = currentNode.children[char];
}
currentNode.isEndOfWord = true;
}
const wordsInSentence = sentence.split(' ');
const resultWords = [];
for (const word of wordsInSentence) {
let currentNode = root;
let foundRoot = null;
for (let charIndex = 0; charIndex < word.length; charIndex++) {
const char = word[charIndex];
if (!currentNode.children[char]) {
break;
}
currentNode = currentNode.children[char];
if (currentNode.isEndOfWord) {
foundRoot = word.substring(0, charIndex + 1);
break;
}
}
if (foundRoot !== null) {
resultWords.push(foundRoot);
} else {
resultWords.push(word);
}
}
return resultWords.join(' ');
}
Given two non-empty linked lists representing two non-negative integers (digits in reverse order), add them and return the sum as a linked list.
Core Idea: Iterative Addition with Carry
Simulate manual addition by iterating through both lists simultaneously, adding digits and managing a carry-over.
// Definition for singly-linked list.
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val);
this.next = (next === undefined ? null : next);
}
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
function addTwoNumbers(l1, l2) {
const dummyHead = new ListNode(0);
let current = dummyHead;
let p1 = l1;
let p2 = l2;
let carry = 0;
while (p1 !== null || p2 !== null || carry !== 0) {
const digit1 = p1 ? p1.val : 0;
const digit2 = p2 ? p2.val : 0;
const sum = digit1 + digit2 + carry;
const newDigit = sum % 10;
carry = Math.floor(sum / 10);
current.next = new ListNode(newDigit);
current = current.next;
if (p1) {
p1 = p1.next;
}
if (p2) {
p2 = p2.next;
}
}
return dummyHead.next;
}
Determine if a number `n` is happy. A happy number eventually reaches 1 when repeatedly replaced by the sum of the squares of its digits. Non-happy numbers loop endlessly in a cycle not including 1.
Core Idea: Detecting Cycles (Hash Set)
Use a hash set to keep track of numbers encountered. If a number repeats, it's in a cycle and not happy.
/**
* @param {number} n
* @return {boolean}
*/
function isHappy(n) {
const getNext = (num) => {
let totalSum = 0;
while (num > 0) {
let digit = num % 10;
totalSum += digit * digit;
num = Math.floor(num / 10);
}
return totalSum;
};
const seenNumbers = new Set();
while (n !== 1 && !seenNumbers.has(n)) {
seenNumbers.add(n);
n = getNext(n);
}
return n === 1;
}
Given a Roman numeral string, convert it to an integer. Handle subtractive forms (IV, IX, XL, XC, CD, CM).
Core Idea: Iterate from Left to Right, Checking for Subtractive Cases
Use a value map for symbols. Iterate, if current value is less than next value, subtract; otherwise, add.
/**
* @param {string} s
* @return {number}
*/
function romanToInt(s) {
const romanValues = new Map([
['I', 1], ['V', 5], ['X', 10], ['L', 50],
['C', 100], ['D', 500], ['M', 1000]
]);
let total = 0;
for (let i = 0; i < s.length; i++) {
const currentValue = romanValues.get(s[i]);
const nextValue = (i + 1 < s.length) ? romanValues.get(s[i + 1]) : 0;
if (currentValue < nextValue) {
total -= currentValue;
} else {
total += currentValue;
}
}
return total;
}
Given an integer, convert it to a Roman numeral. Follow rules for additive and subtractive forms.
Core Idea: Greedy Approach with Predefined Mappings
Use an ordered list of Roman numeral values and symbols (including subtractive forms) from largest to smallest. Greedily subtract the largest possible value from the number.
/**
* @param {number} num
* @return {string}
*/
function intToRoman(num) {
const romanMap = [
{ value: 1000, symbol: "M" }, { value: 900, symbol: "CM" },
{ value: 500, symbol: "D" }, { value: 400, symbol: "CD" },
{ value: 100, symbol: "C" }, { value: 90, symbol: "XC" },
{ value: 50, symbol: "L" }, { value: 40, symbol: "XL" },
{ value: 10, symbol: "X" }, { value: 9, symbol: "IX" },
{ value: 5, symbol: "V" }, { value: 4, symbol: "IV" },
{ value: 1, symbol: "I" }
];
let romanNumeral = "";
for (const pair of romanMap) {
while (num >= pair.value) {
romanNumeral += pair.symbol;
num -= pair.value;
}
}
return romanNumeral;
}
Given an `m x n` matrix, return all elements of the matrix in spiral order.
Core Idea: Boundary Tracking with Four Pointers
Use `top`, `bottom`, `left`, `right` pointers to define boundaries. "Peel off" layers of the matrix (right, down, left, up) and shrink boundaries until all elements are visited.
/**
* @param {number[][]} matrix
* @return {number[]}
*/
function spiralOrder(matrix) {
const result = [];
const m = matrix.length;
if (m === 0) return result;
const n = matrix[0].length;
if (n === 0) return result;
let top = 0;
let bottom = m - 1;
let left = 0;
let right = n - 1;
while (top <= bottom && left <= right) {
for (let c = left; c <= right; c++) {
result.push(matrix[top][c]);
}
top++;
if (top > bottom) break;
for (let r = top; r <= bottom; r++) {
result.push(matrix[r][right]);
}
right--;
if (left > right) break;
for (let c = right; c >= left; c--) {
result.push(matrix[bottom][c]);
}
bottom--;
if (top > bottom) break;
for (let r = bottom; r >= top; r--) {
result.push(matrix[r][left]);
}
left++;
}
return result;
}
Given an `m x n` grid `board` and a string `word`, return `true` if `word` exists in the grid. Word constructed from adjacent cells (horizontally/vertically). Same cell not used more than once.
Core Idea: Depth-First Search (DFS) with Backtracking
Explore all possible paths from every cell matching `word[0]`. Recursively try to match subsequent characters by moving to adjacent, unvisited cells. Mark visited, then unmark (backtrack).
/**
* @param {character[][]} board
* @param {string} word
* @return {boolean}
*/
function exist(board, word) {
const m = board.length;
if (m === 0) return false;
const n = board[0].length;
if (n === 0) return false;
if (word.length === 0) return true;
const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]];
const dfs = (row, col, wordIndex) => {
if (wordIndex === word.length) {
return true;
}
if (row < 0 || row >= m || col < 0 || col >= n || board[row][col] !== word[wordIndex]) {
return false;
}
const originalChar = board[row][col];
board[row][col] = '#'; // Mark as visited
let found = false;
for (const [dr, dc] of directions) {
const newRow = row + dr;
const newCol = col + dc;
if (dfs(newRow, newCol, wordIndex + 1)) {
found = true;
break;
}
}
board[row][col] = originalChar; // Backtrack: restore original char
return found;
};
for (let r = 0; r < m; r++) {
for (let c = 0; c < n; c++) {
if (board[r][c] === word[0]) {
if (dfs(r, c, 0)) {
return true;
}
}
}
}
return false;
}
Given `paths` of directory info, return all duplicate files in the file system (same content). Output is a list of groups of file paths.
Core Idea: Hash Map to Group by Content
Use a hash map where keys are file contents and values are lists of file paths. Parse each input string to extract content and path, then populate the map.
/**
* @param {string[]} paths
* @return {string[][]}
*/
function findDuplicate(paths) {
const contentMap = new Map();
for (const pathString of paths) {
const parts = pathString.split(' ');
const directoryPath = parts[0];
for (let i = 1; i < parts.length; i++) {
const fileInfo = parts[i];
const openParenIndex = fileInfo.indexOf('(');
const fileName = fileInfo.substring(0, openParenIndex);
const fileContent = fileInfo.substring(openParenIndex + 1, fileInfo.length - 1);
const fullFilePath = `${directoryPath}/${fileName}`;
if (!contentMap.has(fileContent)) {
contentMap.set(fileContent, []);
}
contentMap.get(fileContent).push(fullFilePath);
}
}
const result = [];
for (const filePathsArray of contentMap.values()) {
if (filePathsArray.length > 1) {
result.push(filePathsArray);
}
}
return result;
}