Skip to content

编码题

使用 JS 操作 DOM,向 <body> 中插入一个 <span>

Section titled “使用 JS 操作 DOM,向 <body> 中插入一个 <span>”
// 1. 查询
const body = document.querySelector('body'); // 也可用 getElementsByTagName('body')[0]
// 2. 创建节点
const span = document.createElement('span');
span.textContent = 'Hello DOM'; // 安全写法,自动转义
// 3. 插入
body.appendChild(span); // 追加到最后
// body.prepend(span); // 最前面(IE 不支持 prepend 需 polyfill)

查询 DOM 节点的更多方法,可以参考:搜索:getElement*querySelector*

文档:交叉观察器 API - Web API | MDN

const ExposureSensor = ({ children, onVisible }) => {
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => { // 此处直接解构得到最前面的 index 为 0 的项
if (entry.isIntersecting) {
if (onVisible) {
onVisible(entry);
} }
},
{
threshold: 0.5,
rootMargin: '0px'
}
);
if (ref.current) {
observer.observe(ref.current);
}
return () => {
if (ref.current) {
observer.unobserve(ref.current);
}
};
}, [onVisible]);
return <div ref={ref}>{children}</div>;
};

IntersectionObserver 构造函数的第一个参数是一个回调函数,长这样(注意其第一个参数是个数组):

(entries, observer) => {
entries.forEach(entry => {
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
}

设计一个 React Hook,每次调用它,都返回上次传入的参数

Section titled “设计一个 React Hook,每次调用它,都返回上次传入的参数”
import { useRef } from 'react';
function usePrevious(value) {
const ref = useRef();
const previous = ref.current;
ref.current = value;
return previous;
}

useDebounce 用于防抖一个值

import { useState, useEffect } from "react";
/**
* 一个自定义 Hook,用于防抖一个值。
* @param value 需要进行防抖处理的值 (比如:搜索框的输入)
* @param delay 延迟时间 (毫秒)
* @returns 返回经过防抖处理后的值
*/
function useDebounce(value, delay) {
// 1. 创建一个 state 用于存储防抖后的值
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// 2. 设置一个定时器,在 delay 毫秒后更新 state
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// 3. 清理函数:
// 在下一次 effect 执行前或者组件卸载时,清除上一个定时器
// 这是实现防抖的关键
return () => {
clearTimeout(timer);
};
},
// 只有当 value 或 delay 变化时,才重新执行 effect
[value, delay],
);
return debouncedValue;
}
export default useDebounce;

TIP

useEffect 返回的回调函数会在以下两种情况下被执行:

  • 组件卸载时(unmount)
  • 依赖数组变化导致 effect 重新执行之前(即“下一次 effect 执行前”)

设计一个 React Hook: useDebouncedCallback

Section titled “设计一个 React Hook: useDebouncedCallback”

useDebouncedCallback 用于防抖一个函数

import { useRef, useEffect, useCallback } from "react";
function useDebouncedCallback(fn, delay) {
const fnRef = useRef(fn);
const timerRef = useRef(null);
useEffect(() => {
fnRef.current = fn;
}, [fn]);
const debounced = useCallback(
(...args) => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
// React 组件中几乎不会用到 `this`
// 所以此处直接用的是 `fnRef.current(...args)`
timerRef.current = setTimeout(() => fnRef.current(...args), delay);
},
[delay],
);
useEffect(() => {
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
};
}, []);
return debounced;
}
export default useDebouncedCallback;

或者,如果需要基于已有的 debounce 实现:

import { useRef, useEffect, useMemo } from "react";
function debounce(fn, t) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.call(this, ...args), t);
};
}
export default function useDebouncedCallback(fn, delay) {
const fnRef = useRef(fn);
useEffect(() => {
fnRef.current = fn;
}, [fn]);
const debounced = useMemo(
() => debounce((...args) => fnRef.current(...args), delay),
[delay],
);
return debounced;
}
window.scrollTo({top: 0, behavior: 'smooth'});
target.scrollIntoView({ behavior: 'smooth', block: 'start' });

https://leetcode.cn/problems/event-emitter

type Callback = (...args: any[]) => any;
type Subscription = {
unsubscribe: () => void;
};
class EventEmitter {
private events: Map<string, Set<Callback>> = new Map();
subscribe(eventName: string, callback: Callback): Subscription {
if (!this.events.has(eventName)) {
this.events.set(eventName, new Set());
}
this.events.get(eventName)!.add(callback);
return {
unsubscribe: () => {
const callbacks = this.events.get(eventName);
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
this.events.delete(eventName);
}
}
}
};
}
emit(eventName: string, ...args: any[]): any[] {
const callbacks = this.events.get(eventName);
if (!callbacks) return [];
return Array.from(callbacks).map(cb => {
try {
return cb(...args);
} catch (e) {
console.error('Event callback error:', e);
return undefined;
}
});
}
}
function flatten(arr) {
const result = [];
for (const item of arr) {
if (Array.isArray(item)) {
result.push(...flatten(item)); // push 可接受多个参数
} else {
result.push(item);
}
}
}

如果限制扁平化层数:

https://leetcode.cn/problems/flatten-deeply-nested-array

function flatten(arr, depth = 1) {
const result = [];
for (const item of arr) {
if (Array.isArray(item) && depth > 0) {
result.push(...flatten(item, depth - 1));
} else {
result.push(item);
}
}
}

ES2019 以上版本,可使用 Array.prototype.flat()

const uniqueArray = [...new Set(originalArray)];
// 或
const uniqueArray = Array.from(new Set(originalArray));
const uniqueArray = originalArray.filter((item, index, arr) => arr.indexOf(item) === index);
const uniqueArray = originalArray.reduce((accumulator, currentValue) => {
if (!accumulator.includes(currentValue)) {
accumulator.push(currentValue);
}
return accumulator;
}, []);

https://leetcode.cn/problems/compact-object/description/

精简对象与原始对象相同,只是将包含假值的键移除。该操作适用于对象及其嵌套对象。数组被视为索引作为键的对象。当 Boolean(value) 返回 false 时,值被视为假值。

/**
* 递归地移除对象或数组中所有“假”值(falsy)。
* 假值指 Boolean(value) === false 的值,如:null、undefined、0、""、false、NaN。
*
* 规则:
* 1. 数组:先剔除假元素,再递归压缩剩余元素。
* 2. 普通对象:仅保留真值属性,并递归压缩其值。
* 3. 原始类型:直接返回,由调用者决定取舍。
*
* 注意:typeof null === 'object',因此要先判断 null。
*
* @param {Object|Array} obj - 待压缩的对象或数组
* @return {Object|Array} - 压缩后的新结构(不会修改原结构)
*/
function compactObject(obj) {
/* ---------- 1. 数组分支 ---------- */
if (Array.isArray(obj)) {
// 1.1 过滤掉假元素
const filtered = obj.filter(Boolean); // Boolean 会自动剔除 falsy 值
// 1.2 对剩下的每个元素继续递归压缩
return filtered.map(compactObject);
}
/* ---------- 2. 对象分支(排除 null) ---------- */
if (obj !== null && typeof obj === "object") {
const compacted = {}; // 新对象,存放真值属性
// 遍历自身可枚举属性(不含原型链上的)
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
// 仅当真值时才保留,并递归压缩其内部结构
if (Boolean(value)) {
compacted[key] = compactObject(value);
}
}
}
return compacted;
}
/* ---------- 3. 原始值分支 ---------- */
// 字符串、数字、布尔、undefined、Symbol、BigInt、函数等直接返回
// 由上层调用者通过 Boolean(...) 决定是否丢弃
return obj;
}

(a, b) => a - b: 升序 (a, b) => b - a: 降序

const compareVersions = (a, b) => {
const v1 = a.split('.').map(Number);
const v2 = b.split('.').map(Number);
for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
const num1 = v1[i] || 0;
const num2 = v2[i] || 0;
if (num1 !== num2) {
return num1 - num2;
}
}
return 0;
};
// 使用示例
const versions = ['1.2.3', '2.0.1', '1.10.0', '2.0.0'];
versions.sort(compareVersions);
// 结果:['1.2.3', '1.10.0', '2.0.0', '2.0.1']

实现一个函数,将平铺节点列表转换为一棵树

Section titled “实现一个函数,将平铺节点列表转换为一棵树”
function buildTree(list) {
const map = new Map();
const result = [];
list.forEach((node) => {
map.set(node.id, { ...node });
});
list.forEach((node) => {
const self = map.get(node.id);
if (node.parentId == null) {
result.push(self);
} else {
const parent = map.get(node.parentId);
if (parent) {
if (!parent.children) parent.children = [];
parent.children.push(self);
}
}
});
return result;
}
const data = [
{ id: 1, name: "aaa1" },
{ id: 3, name: "aaa3" },
{ id: 2, name: "aaa2", parentId: 3 },
{ id: 4, name: "aaa4", parentId: 2 },
{ id: 5, name: "aaa5" },
{ id: 6, name: "aaa6", parentId: 4 },
{ id: 7, name: "aaa7", parentId: 4 },
];
console.log(JSON.stringify(buildTree(data), null, 2));
// 为 Array 原型添加 append 方法
Object.defineProperty(Array.prototype, 'append', {
value: function(...items) {
// 保存原始长度
const originalLength = this.length;
// 处理各种类型的参数
for (let i = 0; i < items.length; i++) {
// 模拟 push 的完整行为,包括处理稀疏数组
this[originalLength + i] = items[i];
}
// 更新 length 属性(虽然会自动更新,但为了完整性)
this.length = originalLength + items.length;
// 返回新的长度,与 push 保持一致
return this.length;
},
writable: true,
configurable: true,
enumerable: false // 设置为不可枚举,避免在 for...in 循环中出现
});
// 添加额外的静态方法版本
Array.append = function(array, ...items) {
return array.append(...items);
};
Array.prototype.myMap = function (callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError(`${callback} is not a function`);
}
// 初始累加器为空数组
return this.reduce((acc, cur, idx, arr) => {
// 把当前元素映射后的值“追加”到累加器
return [...acc, callback.call(thisArg, cur, idx, arr)];
}, []);
};

用 ES6 写个 MyPromise:1 秒后 resolve,reject 只能生效一次

Section titled “用 ES6 写个 MyPromise:1 秒后 resolve,reject 只能生效一次”
MyPromise.js
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
constructor(executor) {
this.state = PENDING
this.value = undefined
this.reason = undefined
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED
this.value = value
this.onFulfilledCallbacks.forEach(fn => fn())
}
}
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
const promise2 = new MyPromise((resolve, reject) => {
const handleFulfilled = () => {
setTimeout(() => {
try {
const x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}
const handleRejected = () => {
setTimeout(() => {
try {
const x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}
if (this.state === FULFILLED) handleFulfilled()
else if (this.state === REJECTED) handleRejected()
else {
this.onFulfilledCallbacks.push(handleFulfilled)
this.onRejectedCallbacks.push(handleRejected)
}
})
return promise2
}
catch(onRejected) {
return this.then(null, onRejected)
}
static resolve(val) {
return new MyPromise(resolve => resolve(val))
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason))
}
}
// 简易 Promise 解析过程
function resolvePromise(promise, x, resolve, reject) {
if (promise === x) return reject(new TypeError('Chaining cycle detected'))
if (x && (typeof x === 'object' || typeof x === 'function')) {
let used
try {
const then = x.then
if (typeof then === 'function') {
then.call(x, val => {
if (used) return; used = true
resolvePromise(promise, val, resolve, reject)
}, err => {
if (used) return; used = true
reject(err)
})
} else resolve(x)
} catch (e) {
if (used) return; used = true
reject(e)
}
} else resolve(x)
}
// 测试:1 秒后先 resolve,reject 被忽略
new MyPromise((resolve, reject) => {
console.log('start')
setTimeout(() => {
resolve('ok')
reject('err') // 这句不会生效
}, 1000)
})
.then(v => console.log('then:', v))
.catch(e => console.log('catch:', e))

https://leetcode.cn/problems/allow-one-function-call

const once = (fn) => {
let done = false;
return function (...args) {
if (done) return;
done = true;
return fn.apply(this, args);
};
};

https://leetcode.cn/problems/memoize

function memoize(fn: Fn): Fn {
const memo = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (memo.has(key)) {
return memo.get(key);
}
const value = fn.call(this, ...args);
memo.set(key, value);
return value;
}
}

https://leetcode.cn/problems/merge-intervals

/**
* 合并所有重叠的区间
* @param {number[][]} intervals - 形如 [[start, end], ...]
* @returns {number[][]} - 合并后的区间
*/
const mergeIntervals = (intervals) => {
if (!Array.isArray(intervals) || intervals.length === 0) return [];
// 按起点升序排序
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
// 逐个合并
return sorted.reduce((merged, [s, e]) => {
if (merged.length === 0) return [[s, e]];
const last = merged[merged.length - 1];
// 如果当前区间与 last 重叠,则合并
if (s <= last[1]) {
last[1] = Math.max(last[1], e);
} else {
merged.push([s, e]);
}
return merged;
}, []);
};
/* ====== 测试 ====== */
console.log(
mergeIntervals([
[1, 3],
[2, 6],
[8, 10],
[15, 18],
])
); // [ [1,6], [8,10], [15,18] ]
console.log(mergeIntervals([[1, 4], [4, 5]])); // [ [1,5] ]
/**
* 判断 5 张纯数字牌是否为顺子(不考虑 joker)
* @param {number[]} nums 长度为 5 的数组,元素范围 1-13
* @returns {boolean}
*/
const isStraight = (nums) => {
if (!Array.isArray(nums) || nums.length !== 5) return false;
nums.sort((a, b) => a - b);
return new Set(nums).size === 5 && nums[4] - nums[0] < 5;
};
// --- 测试 ---
console.log(isStraight([3, 1, 4, 2, 5])); // true
console.log(isStraight([1, 3, 5, 7, 9])); // false
console.log(isStraight([1, 2, 3, 4, 6])); // false
console.log(isStraight([10, 11, 12, 13, 9])); // true

判断多叉树里有没有一条从根到叶的路,节点值加起来正好等于给定的数

Section titled “判断多叉树里有没有一条从根到叶的路,节点值加起来正好等于给定的数”

类似:https://leetcode.cn/problems/path-sum

class TreeNode {
constructor(value) {
this.value = value;
this.children = [];
}
addChild(childNode) {
this.children.push(childNode);
}
}
function hasPathSum(root, targetSum) {
if (!root) return false;
function dfs(node, currentSum) {
currentSum += node.value;
if (node.children.length === 0) {
return currentSum === targetSum;
}
for (let child of node.children) {
if (dfs(child, currentSum)) {
return true;
}
}
return false;
}
return dfs(root, 0);
}
// 测试
const root = new TreeNode(1);
const child1 = new TreeNode(2);
const child2 = new TreeNode(3);
const child3 = new TreeNode(4);
const child4 = new TreeNode(5);
const child5 = new TreeNode(6);
const child6 = new TreeNode(7);
root.addChild(child1);
root.addChild(child2);
root.addChild(child3);
child1.addChild(child4);
child1.addChild(child5);
child3.addChild(child6);
console.log(hasPathSum(root, 8)); // true
console.log(hasPathSum(root, 10)); // false
console.log(hasPathSum(root, 12)); // true

实现在目录树中查找目标目录并返回完整路径

Section titled “实现在目录树中查找目标目录并返回完整路径”

给定一棵用数组表示的目录树,每个节点是一个对象,包含

  • name: 字符串,目录或文件名
  • children: 数组,子节点列表(目录才有该字段,文件没有)

请实现一个函数 findPath(tree, targetName),返回从根到目标目录名为 targetName 的一条完整路径(用 ’/’ 拼接)。 如果同名目录出现多次,返回最先找到(深度优先)的那条路径;未找到返回 null

/**
* 在目录树中查找目标目录并返回完整路径
* @param {Array} tree 根节点数组
* @param {string} targetName 要查找的目录名
* @return {string|null} 完整路径或 null
*/
const findPath = (tree, targetName) => {
const dfs = (nodes, path) => {
for (const node of nodes) {
const curPath = path + '/' + node.name;
if (node.name === targetName) return curPath; // 找到即返回
if (node.children) { // 仅目录有 children
const sub = dfs(node.children, curPath);
if (sub) return sub; // 子树找到即返回
}
}
return null;
};
return dfs(tree, '');
};
/* ------------------ 测试 ------------------ */
const tree = [
{
name: 'home',
children: [
{ name: 'user1', children: [{ name: 'docs', children: [] }] },
{ name: 'user2', children: [{ name: 'media', children: [] }] }
]
},
{ name: 'opt', children: [{ name: 'home', children: [] }] }
];
console.log(findPath(tree, 'docs')); // "/home/user1/docs"
console.log(findPath(tree, 'home')); // "/home"(最先出现的)
console.log(findPath(tree, 'xyz')); // null

求最长无重复字符子字符串的长度

Section titled “求最长无重复字符子字符串的长度”

https://leetcode.cn/problems/longest-substring-without-repeating-characters

暴力方法
function lengthOfLongestSubstringBruteForce(s) {
let maxLength = 0;
const n = s.length;
// 外层循环:所有可能的起始位置
for (let i = 0; i < n; i++) {
// 内层循环:所有可能的结束位置(从 i 开始)
for (let j = i; j < n; j++) {
// 检查子字符串 s[i...j] 是否无重复字符
if (isUnique(s, i, j)) {
// 如果无重复,更新最大长度
maxLength = Math.max(maxLength, j - i + 1);
}
}
}
return maxLength;
}
// 辅助函数:检查子字符串是否无重复字符
function isUnique(s, start, end) {
const seen = new Set();
for (let i = start; i <= end; i++) {
const char = s[i];
if (seen.has(char)) {
return false; // 发现重复字符
}
seen.add(char);
}
return true; // 所有字符都是唯一的
}

滑动窗口(时间 O(n),空间 O(min(m, n))):

function lengthOfLongestSubstring(s: string): number {
let maxLength = 0;
let charMap = new Map();
let left = 0;
for (let right = 0; right < s.length; ++right) {
const ch = s.charAt(right);
// 如果出现了已经发现过的字符,
// 并且它在当前窗口内(通过 charMap.get(ch) >= left 判断),
// 那么,就需要更新 left,
// 使窗口的左边界右移
if (
charMap.has(ch) && // 碰到已经发现过的字符
charMap.get(ch) >= left // 确保 left 永远只会向右移动
) {
left = charMap.get(ch) + 1;
}
charMap.set(ch, right);
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
};

Fisher-Yates 算法:

  1. 从数组 末尾 开始遍历
  2. 随机选择 [0, i] 范围内的索引
  3. 交换当前元素与随机元素
  4. 继续向前直到遍历完成

时间复杂度: 空间复杂度:

function shuffle(array) {
const arr = array.slice(); // 复制原数组,避免修改原始数据
for (let i = arr.length - 1; i > 0; i--) {
// 随机选择 0 到 i 之间的索引(包括 i)
const j = Math.floor(Math.random() * (i + 1));
// 交换当前元素与随机选中的元素
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
const original = [1, 2, 3, 4, 5];
const shuffled = shuffle(original);
console.log(shuffled);
function isBracketMatch(str) {
const stack = [];
const bracketMap = {
'(': ')',
'[': ']',
'{': '}'
};
for (let char of str) {
if (bracketMap[char]) {
stack.push(char);
} else if (char === ')' || char === ']' || char === '}') {
if (bracketMap[stack.pop()] !== char) {
return false;
}
}
}
return stack.length === 0;
}

实现一个函数,判断一个数组是否恰好翻转一段连续子数组就能变成升序,原本有序也算 false

Section titled “实现一个函数,判断一个数组是否恰好翻转一段连续子数组就能变成升序,原本有序也算 false”
/**
* 判断给定数组是否可以通过翻转恰好一个连续子数组使其整体有序。
* 如果原数组已经有序,也返回 false(因为不需要翻转)。
*
* @param {number[]} arr
* @returns {boolean}
*/
function canBeSortedByOneFlip(arr) {
const n = arr.length;
if (n <= 1) return false; // 无需翻转
// 1. 找到第一个下降的位置 i
let i = 0;
while (i < n - 1 && arr[i] <= arr[i + 1]) i++;
if (i === n - 1) return false; // 已经有序
// 2. 找到最后一个下降的位置 j
let j = n - 1;
while (j > 0 && arr[j - 1] <= arr[j]) j--;
// 3. 翻转子数组 arr[i..j]
// 手动反转,避免修改原数组
const reversed = [];
for (let k = i; k <= j; k++) reversed.push(arr[j - (k - i)]);
// 构造翻转后的数组
const flipped = arr.slice(0, i).concat(reversed, arr.slice(j + 1));
// 4. 检查翻转后是否整体非降
for (let k = 0; k < n - 1; k++) {
if (flipped[k] > flipped[k + 1]) return false;
}
return true;
}
// ---- 测试用例 ----
console.log(canBeSortedByOneFlip([1, 3, 2, 1, 4, 5])); // true
console.log(canBeSortedByOneFlip([1, 3, 2, 1, 4, 6, 5])); // false
console.log(canBeSortedByOneFlip([1, 2, 3, 4, 5])); // false (已有序)
console.log(canBeSortedByOneFlip([5, 4, 3, 2, 1])); // true
console.log(canBeSortedByOneFlip([1, 2, 4, 3, 5])); // true
console.log(canBeSortedByOneFlip([1, 5, 3, 4, 2, 6])); // false
function addLargeNumbers(numStr1, numStr2) {
// 将字符串拆分为数字数组并反转,便于从低位到高位计算
const arr1 = numStr1.split('').map(Number).reverse();
const arr2 = numStr2.split('').map(Number).reverse();
const maxLength = Math.max(arr1.length, arr2.length);
const result = [];
let carry = 0;
// 逐位相加并处理进位
for (let i = 0; i < maxLength; i++) {
const digit1 = arr1[i] || 0;
const digit2 = arr2[i] || 0;
const sum = digit1 + digit2 + carry;
result.push(sum % 10); // 当前位结果
carry = Math.floor(sum / 10); // 计算进位
}
// 如果最后还有进位,需要额外添加
if (carry > 0) {
result.push(carry);
}
// 反转回正常顺序并拼接成字符串
return result.reverse().join('');
}
// 测试示例:
console.log(addLargeNumbers('123456789', '987654321')); // 输出:"1111111110"
console.log(addLargeNumbers('999', '1')); // 输出:"1000"

https://leetcode.cn/problems/lru-cache

/**
* LRU 缓存(ES6)
* 支持任意类型 key、value
* 所有操作平均时间复杂度 O(1)
*/
class LRUCache {
/**
* @param {number} capacity 最大容量,必须为正整数
*/
constructor(capacity) {
if (!Number.isInteger(capacity) || capacity <= 0) {
throw new TypeError('capacity must be a positive integer');
}
this.capacity = capacity;
this.map = new Map();
}
/**
* 读取 key 对应的值,不存在返回 undefined
* 访问后该 key 变为“最近使用”
* @param {*} key
* @returns {*|undefined}
*/
get(key) {
if (!this.map.has(key)) return undefined;
// 取出值并“移到末尾”
const value = this.map.get(key);
this.map.delete(key); // 先删
this.map.set(key, value); // 再插到末尾
return value;
}
/**
* 写入键值对
* 如果 key 已存在,覆盖并变为“最近使用”
* 如果容量超限,淘汰最久未使用的 key
* @param {*} key
* @param {*} value
* @returns {LRUCache} 支持链式调用
*/
set(key, value) {
if (this.map.has(key)) {
// 已存在,先删除旧位置
this.map.delete(key);
} else if (this.map.size >= this.capacity) { // 注意此处是 `.size` 而非 `.size()`
// 容量满,淘汰第一个(最久未使用)
const firstKey = this.map.keys().next().value;
this.map.delete(firstKey);
}
this.map.set(key, value);
return this; // 允许链式调用
}
/**
* 手动删除某个 key
* @param {*} key
* @returns {boolean} 是否删除成功
*/
delete(key) {
return this.map.delete(key);
}
/**
* 清空缓存
*/
clear() {
this.map.clear();
}
/**
* 当前缓存大小
* @returns {number}
*/
get size() {
return this.map.size;
}
/**
* 按“最近→最久”顺序返回所有键,方便调试
* @returns {Array<*>}
*/
keys() {
return [...this.map.keys()];
}
/**
* 按“最近→最久”顺序返回所有值,方便调试
* @returns {Array<*>}
*/
values() {
return [...this.map.values()];
}
}
/* ---------- 使用示例 ---------- */
const cache = new LRUCache(3);
cache.set('a', 1)
.set('b', 2)
.set('c', 3);
console.log(cache.keys()); // ['a','b','c']
cache.get('a'); // 访问 a
cache.set('d', 4); // 触发淘汰 b
console.log(cache.keys()); // ['c','a','d']
// 节点定义
class ListNode {
constructor(val, next = null) {
this.val = val;
this.next = next;
}
}
/**
* 反转单链表(ES6 迭代版)
* @param {ListNode} head
* @return {ListNode} 新头结点
*/
const reverseList = (head) => {
let prev = null; // 新链表头部
let curr = head; // 当前待处理节点
while (curr) {
const next = curr.next; // 暂存后继
curr.next = prev; // 反向指针
prev = curr; // 新链表头部前移
curr = next; // 处理下一个
}
return prev; // prev 即为反转后的头
};

https://leetcode.cn/problems/reverse-nodes-in-k-group

/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* 反转从 start 开始到 endBefore(不包括 endBefore)的链表部分
* @param {ListNode} start - 要反转的起始节点
* @param {ListNode} endBefore - 反转范围的边界节点(不包含在反转范围内)
* @return {Array} [newHead, newTail] - 反转后的新头节点和新尾节点
*/
function reverseListSegment(start, endBefore) {
let prev = null;
let cur = start;
while (cur !== endBefore) {
const next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return [prev, start]; // 返回反转后的头节点和尾节点
}
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var reverseKGroup = function (head, k) {
if (!head || k <= 1) return head;
// 找到当前组的下一个组的起始节点
let nextGroupStart = head;
for (let i = 0; i < k; ++i) {
if (!nextGroupStart) return head; // 不足 k 个节点,不反转
nextGroupStart = nextGroupStart.next;
}
// 反转当前组的 k 个节点
const [reversedHead, reversedTail] = reverseListSegment(head, nextGroupStart);
// 递归处理剩余部分,并将反转后的尾节点连接到下一组的处理结果
reversedTail.next = reverseKGroup(nextGroupStart, k);
return reversedHead;
};

实现一个函数,返回第 K 个大的数字

Section titled “实现一个函数,返回第 K 个大的数字”

最小堆:

/**
* 返回数组中第 k 大的数字(非全排序)
* @param {number[]} nums
* @param {number} k 从 1 开始计数
* @returns {number|null} 不足 k 个时返回 null
*/
function kthLargest(nums, k) {
if (!Number.isInteger(k) || k <= 0) return null;
const heap = new MinHeap();
for (const v of nums) {
if (heap.size() < k) {
heap.push(v);
} else if (v > heap.peek()) {
heap.pop();
heap.push(v);
}
}
return heap.size() === k ? heap.peek() : null;
}

快速选择(快排半成品):

/**
* 返回数组中第 k 大的数字(QuickSelect,非全排序)
* @param {number[]} nums
* @param {number} k 从 1 开始计数
* @returns {number|null} 不足 k 个时返回 null
*/
function kthLargest(nums, k) {
if (!Number.isInteger(k) || k <= 0 || k > nums.length) return null;
const n = nums.length;
const target = n - k; // 升序下标
let left = 0, right = nums.length - 1;
while (true) {
const pivotIndex = partition(nums, left, right);
if (pivotIndex === target) {
return nums[pivotIndex];
} else if (pivotIndex < target) {
left = pivotIndex + 1;
} else {
right = pivotIndex - 1;
}
}
}
// 单边循环分区
function partition(arr, left, right) {
const pivot = arr[right];
let i = left;
for (let j = left; j < right; j++) {
if (arr[j] < pivot) {
[arr[i], arr[j]] = [arr[j], arr[i]];
i++;
}
}
[arr[i], arr[right]] = [arr[right], arr[i]];
return i;
}

https://leetcode.cn/problems/daily-temperatures

解法:单调栈,栈内元素保持单调递增或递减的顺序。时间复杂度 ,本质上是用空间换时间。

TIP

何时使用单调栈?常见的问题情形是:一维数组中,寻找任一元素的左侧或右侧第一个比自己大或者小的元素,即所谓的“下一个更大元素”、“前一个更小元素”问题。

/**
* @param {number[]} temperatures
* @return {number[]}
*/
var dailyTemperatures = function (temperatures) {
// 1. 初始化结果数组,默认值为 0 (表示没有更高温度)
const answer = new Array(temperatures.length).fill(0);
// 2. 初始化单调栈。栈中存储的是尚未找到“下一个更高温度”的日期的索引
const stack = [];
// 3. 遍历每天的温度
for (let i = 0; i < temperatures.length; i++) {
const currentTemp = temperatures[i];
// 核心逻辑:维护一个单调递减栈
// 只要栈不为空,且当前温度高于栈顶索引对应的温度
// 注意此处是 while
while (
stack.length > 0 &&
currentTemp > temperatures[stack[stack.length - 1]]
) {
// 栈顶索引找到了它的“下一个更高温度”
const prevIndex = stack.pop();
// 计算天数差并更新结果
answer[prevIndex] = i - prevIndex;
}
// 将当前日期的索引入栈,等待未来更高的温度
stack.push(i);
}
return answer;
};