2025大厂面试常见手撕题

12 阅读6分钟

不包含代码阅读题,常见的代码阅读题在系列的面经里面有。 简单高频,面试官放过你系列的手撕题

前端手撕

1.防抖

  • 定义:每次触发都会重新计时,隔n秒后执行 (进化:react的防抖hook、搜索框带防抖)

      //普通函数
      function debounce(fn,delay){
          let timer = null;
          return function(...args){
              clearTimeout(timer);
              timer = setTimeout(()=>{
                      fn.apply(this,args);
              },delay);
          }
      }  
      
      //React hook version
      const useDebounce(fn,delay){
          const timer = useRef(null);
          const fnRef = useRef(fn);
          
          useEffect(()=>{
              fnRef.current = fn;
          },[fn]); // 为什么需要[fn]
          
          const debounce = useMemo(()=>{
              return function(..args){
                  clearTimeout(timer.current);
                  timer.current = setTimeout(()=>{
                      fnRef.current(...args);
                  },delay);
              };
          },[delay]);
          
          useEffect(()=>{
              return ()=>clearTimeout(timer.current);
          },[]);
          return debounce;
      }
      
      
      
    

2.数组flat

(不使用flat)

function flatten(arr){
    let res = [];
    for(let item of arr){
        if(Array.isArray(item)){
            const res1 = flatten(item);
            res.push(...res1);
        }else{
            res.push(item);
        }
     }
     return res;
}

function flatten(arr){
    return arr.reduce((acc,cur)=>{
        if(Array.isArray(cur)){
            acc.push(...flatten(cur))
        }else{
            acc.push(cur)
        }
        return acc;
    },[]);
}

3.Promise系列

手写PromiseAll

function myPromiseAll(Promises){
    return new Promise((resolve,reject) =>{
        const res =[];
        let count = 0;

        if(Promises.length===0){
            resolve([]);
            return; //结束
        }
        Promises.forEach((p,i) =>{ //把每个promise的then/catch注册好(同步),但是还没有执行完成,所以count在forEach执行完成后仍然是0)
            Promise.resolve(p).then((value)=>{
                count+=1;
                res[i] = value;
                  if(count === Promises.length){
                        resolve(res);
                    }
            }).catch((err)=>{
                reject(err);
            })
        }


    }
}

并发池 (限制并发请求的数量,同时最多3个请求);

 function pool(tasks,limit){
     let curFinished = 0;
     let curRuns = 0;
     let idx = 0;
     const res = [];
     return new Promise((resolve,reject)=>{
          if(tasks.length === 0 ){
             resolve(res);
             return;
         }
        const runs = ()=>{
             while(curRuns <limit && idx<tasks.length){
                 const cur = idx ++;
                 curRuns ++;
                 Promise.resolve(tasks[cur]())
                 .then(v=>{res[cur] = v;})
                 .catch(reject)
                 .finally(()=>{
                    curRuns--;
                    curFinished ++;
                    
                    if(curFinished === tasks.length){
                        resolve(res);
                    } else runs();
             }
         
      
        }
         runs();
    }
 }

固定数量的图片上传

        function chunk(arr,batchSize){
            const out = [];
            for(let i = 0;i<arr.length;i+=batchSize){
                out.push(arr.slice(i,i+batchSize);
            }
            return out;
        }

实现Scheduler

class Scheduler {
  constructor(limit = 2) {
    this.limit = limit;
    this.running = 0;
    this.queue = [];
  }

  add(taskFn) {
    // taskFn: () => Promise<any>
    return new Promise((resolve, reject) => {
      const run = () => {
        this.running++;

        Promise.resolve()
          .then(taskFn)
          .then(resolve, reject)
          .finally(() => {
            this.running--;
            this._next();
          });
      };

      this.queue.push(run);
      this._next();
    });
  }

  _next() {
    while (this.running < this.limit && this.queue.length > 0) {
      const job = this.queue.shift();
      job();
    }
  }
}

4. CSS手写元素居中

水平垂直居中

//flex
    .parent{
        display:flex;
        justify-content:center;
        align-items:center;
        
      .child{
          position:absolute;
          top:50%;
          left:50%;
          transform:translate(-50%,50%);
      }
 //绝对定位 + transform
 .parent { position: relative; }

.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

//
 .parent {
  display: grid;
  place-items: center; /* 等价于 align-items + justify-items */
}
 

5. CSS手写两栏布局

//flex 常见 等高列
.container { display: flex; }
.left { width: 200px; }
.right { flex: 1; min-width: 0; }


//float + margin 兼容性好,需要清除浮动
.left {
  float: left;
  width: 200px;
}
.right {
  margin-left: 200px;
}
.container::after {
  content: "";
  display: block;
  clear: both;
}

//grid
.container {
  display: grid;
  grid-template-columns: 200px 1fr;
}

6. CSS三列布局

//flex 
.wrap {
  display: flex;
}
.left { width: 200px; }
.right { width: 240px; }
.mid { flex: 1; min-width: 0; }

//grid
.wrap {
display: grid;
  grid-template-columns: 200px 1fr 240px;
}
.left { grid-column: 1; }
.mid  { grid-column: 2; }
.right{ grid-column: 3; }

//圣杯 Float+负margin
<div class="wrap holy">
  <div class="mid">Middle auto</div>
  <div class="left">Left 200</div>
  <div class="right">Right 240</div>
</div>
.holy{
  padding: 0 240px 0 200px; /* 给左右留出位置 */
}
.holy .mid{
  float:left;
  width:100%;
}
.holy .left{
  float:left;
  width:200px;
  margin-left:-100%;
  position:relative;
  left:-200px;
}
.holy .right{
  float:left;
  width:240px;
  margin-left:-240px;
  position:relative;
  right:-240px;
}
.holy::after{
  content:"";
  display:block;
  clear:both;
}

7.loadsh.get:如何读取深层嵌套的对象的属性

function get(obj, path, defaultValue) {
  if (obj == null) return defaultValue;

  const keys = Array.isArray(path) ? path : parsePath(path);

  let cur = obj;
  for (const key of keys) {
    if (cur == null) return defaultValue;
    cur = cur[key];
  }
  return cur === undefined ? defaultValue : cur;
}

function parsePath(path) {
  if (typeof path !== "string" || path.trim() === "") return [];
  // 把 a[0] / a["b"] / a['b'] 转成 a.0 / a.b / a.b
  const normalized = path
    .replace(/\[(\d+)\]/g, ".$1")
    .replace(/\["([^"]+)"\]/g, ".$1")
    .replace(/\['([^']+)'\]/g, ".$1")
    .replace(/^\./, "");

  return normalized.split(".").filter(Boolean);
}

// quick test
// const o = { a: [{ b: { c: 1 } }], x: { "x-y": 2 } };
// console.log(get(o, "a[0].b.c", 0));     // 1
// console.log(get(o, 'x["x-y"]', 0));     // 2
// console.log(get(o, "a[1].b.c", "NA"));  // "NA"

8.观察者模式

on(event,fn) 注册
once(event, fn) 只执行一
off(evemt,fn)
trigger(event,...args) 触发

class Emitter {
  constructor() {
    this._events = new Map(); // event -> Set<listener>
  }

  on(event, fn) {
    if (!this._events.has(event)) this._events.set(event, new Set());
    this._events.get(event).add(fn);
    // 返回取消订阅函数(实用加分)
    return () => this.off(event, fn);
  }

  once(event, fn) {
    const wrapper = (...args) => {
      this.off(event, wrapper);
      fn(...args);
    };
    wrapper._origin = fn; // 便于 off 传原 fn 也能移除 once 包装
    return this.on(event, wrapper);
  }

  off(event, fn) {
    // off() 清空全部
    if (event === undefined) {
      this._events.clear();
      return;
    }

    const set = this._events.get(event);
    if (!set) return;

    // off(event) 清空该事件
    if (fn === undefined) {
      set.clear();
      this._events.delete(event);
      return;
    }

    // off(event, fn) 移除指定回调(含 once 包装)
    for (const listener of set) {
      if (listener === fn || listener._origin === fn) {
        set.delete(listener);
      }
    }
    if (set.size === 0) this._events.delete(event);
  }

  trigger(event, ...args) {
    const set = this._events.get(event);
    if (!set) return;

    // 拷贝一份,避免回调里 off/once 修改集合影响遍历
    [...set].forEach((fn) => fn(...args));
  }
}

// quick test
// const bus = new Emitter();
// const off = bus.on("a", (x) => console.log("on", x));
// bus.once("a", (x) => console.log("once", x));
// bus.trigger("a", 1); // on 1, once 1
// bus.trigger("a", 2); // on 2
// off();
// bus.trigger("a", 3); // no output

算法手撕

1.快排

function quickSort(arr){
    if(arr.length <=1) return arr;
    const p = arr[0];
    const left = [];
    const right = [];
    
    for(let i =0;i<arr.length;i++){
        if(arr[i]<p) left.push(arr[i]);
        else right.push(arr[i]);
    }
    
    return quickSort(left).concat(p,quickSort(right));

冒泡、插入、归并都是稳定的; 快排平均nlogn;最坏是n^2;

2.括号匹配

function isValid(s){
    const stack = [];
    const map = {
        ')','(',
        ']','[',
        ')','('
    }
    
    for(let ch of s){
        if(map[ch]){
            if(stack.pop()!==map[ch]) return false;
         }else{
             stack.push(ch);
         }
     }
     return stack.length === 0;
}

3.链表反转

function reverse(head){
    let prev = null;
    let cur = head;
    
    while(cur){
        const next = cur.next;
        cur.next = prev;
        prev = cur;
        cur = next;
    }
    return prev;
}

4.翻转数组

function reverse(arr) {
  let l = 0, r = arr.length - 1
  while (l < r) {
    [arr[l], arr[r]] = [arr[r], arr[l]]
    l++
    r--
  }
  return arr
}

5.数组转树

input:

const list = [
  { id: 1, pid: 0, name: "A" },
  { id: 2, pid: 1, name: "B" },
  { id: 3, pid: 1, name: "C" },
  { id: 4, pid: 2, name: "D" },
];

Code:

function arrayToTree(list, { idKey="id", pidKey="pid", childrenKey="children", rootPid=0 } = {}) {
  const map = new Map();
  const roots = [];

  // 1) 先把所有节点放进 map,并初始化 children
  for (const item of list) {
    const node = { ...item, [childrenKey]: [] };
    map.set(node[idKey], node);
  }

  // 2) 再做父子挂载
  for (const item of list) {
    const id = item[idKey];
    const pid = item[pidKey];
    const node = map.get(id);

    if (pid === rootPid || pid == null) {
      roots.push(node);
    } else {
      const parent = map.get(pid);
      if (parent) parent[childrenKey].push(node);
      else roots.push(node); // 父节点缺失:兜底当根
    }
  }

  return roots;
}

6.树转数组

DFS先序:

function treeToArray(roots, { idKey="id", pidKey="pid", childrenKey="children" } = {}) {
  const res = [];

  function dfs(node, parentId) {
    const { [childrenKey]: children, ...rest } = node;
    // 还原/补充 pid
    res.push({ ...rest, [pidKey]: parentId });

    if (Array.isArray(children)) {
      for (const child of children) {
        dfs(child, node[idKey]);
      }
    }
  }

  for (const root of roots) dfs(root, 0);
  return res;
}

BFS层序

function treeToArrayBFS(roots, { idKey="id", pidKey="pid", childrenKey="children" } = {}) {
  const res = [];
  const queue = roots.map(r => ({ node: r, parentId: 0 }));

  while (queue.length) {
    const { node, parentId } = queue.shift();
    const { [childrenKey]: children, ...rest } = node;
    res.push({ ...rest, [pidKey]: parentId });

    if (Array.isArray(children)) {
      for (const child of children) {
        queue.push({ node: child, parentId: node[idKey] });
      }
    }
  }
  return res;
}

7.链表有环

function hasCycle(head){
    let slow = head;
    let fast = head;
    while(fast && fast.next){
        slow = slow.next;
        fast = fast.next.next;
        if(slow === fast){
            let p = head;
            while(p!==slow){
                p = p.next;
                slow = slow.next;
            }
            return p;
        }
    }
    
    return false;
}

8.和为k的子数组个数

function sub(nums,k){
    const cnt =new Map();
    cnt.set(0,1);
    let pre = 0;
    let ans = 0;
    
    for(const x of num){
        pre+=x;
        ans+=(cnt.get(pre-k) || 0);
        cnt.set(pre,(cnt.get(pre) || 0) +1);
    }
    return ans;
}

9.695-最大岛屿

BFS

 function maxAreaOfIsland(grid) {
  const m = grid.length;
  if (m === 0) return 0;
  const n = grid[0].length;

  let ans = 0;
  const dirs = [[1,0],[-1,0],[0,1],[0,-1]];

  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (grid[i][j] !== 1) continue;

      let area = 0;
      const q = [[i, j]];
      let head = 0;
      grid[i][j] = 0;

      while (head < q.length) {
        const [r, c] = q[head++];
        area++;

        for (const [dr, dc] of dirs) {
          const nr = r + dr, nc = c + dc;
          if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] === 1) {
            grid[nr][nc] = 0;
            q.push([nr, nc]);
          }
        }
      }

      ans = Math.max(ans, area);
    }
  }
  return ans;
}

分割等和子集的最大数量(最大k),分割成k个不相交的子集和,每个子集和相等,求最大的k

function maxEqualSumSubsets(nums) {
  const n = nums.length;
  const sum = nums.reduce((a, b) => a + b, 0);

  // 排序:大数先放,剪枝更强
  nums = [...nums].sort((a, b) => b - a);

  // 尝试从最大 k 开始
  for (let k = n; k >= 1; k--) {
    if (sum % k !== 0) continue;
    const target = sum / k;
    if (nums[0] > target) continue;

    if (canPartitionK(nums, k, target)) return k;
  }
  return 1;

  function canPartitionK(nums, k, target) {
    const buckets = new Array(k).fill(0);

    function dfs(idx) {
      if (idx === nums.length) return true;

      const num = nums[idx];
      const seen = new Set(); // 剪枝:同层相同 bucket 和不重复尝试

      for (let i = 0; i < k; i++) {
        if (buckets[i] + num > target) continue;
        if (seen.has(buckets[i])) continue;
        seen.add(buckets[i]);

        buckets[i] += num;
        if (dfs(idx + 1)) return true;
        buckets[i] -= num;

        // 剪枝:如果当前 num 放进空桶都失败,后面空桶也一样失败
        if (buckets[i] === 0) break;
      }
      return false;
    }

    return dfs(0);
  }
}

// quick test
// console.log(maxEqualSumSubsets([1,1,1,1])); // 4
// console.log(maxEqualSumSubsets([4,3,2,3,5,2,1])); // 4 (经典例子:每桶和=5)