letcode 高频算法题JS解法集锦

1,735 阅读14分钟

为什么要学算法?

当我准备面试字节阿里的时候,我就知道,算法这一关,我大抵是逃不过去了。但是我的内心一直是有点排斥的,总觉得回溯、动态、贪心之类的算法在前端编码中根本用不到,前端er绞尽脑汁去刷题完全是浪费生命啊。但是我都明白的道理大厂为什么不明白呢?于是我细细思量,斗胆揣测一下原因:

  • 有一部分溜须拍马平常不写代码的人看到算法题很容易知难而退,一下子就把他们筛选出来了
  • 对于某些特殊的业务场景,算法解题思路确实会给我带来神奇的效果
  • 算法不只是解题,还有数据结构、时间复杂度、空间复杂度分析、优化,养成好的代码分析优化习惯后对编码能力的提升是潜移默化的
  • 当你觉得一件事情没意义,而公司要求你去执行,你会不会服从公司的安排呢?算法也有一定的服从性考验作用
  • 优秀的人才面对挑战是会迎难而上的,算法对于优秀的人才来说不过是锻炼思维的媒介而已,并不存在难的说法

想清楚了这些之后,先不要急着刷算法,推荐先去学习一下《数据结构与算法之美》 我觉得是很有好处的。这本书让我收获了很多,也推荐你们去看看。当然也可以看看《漫画算法》等等其他的数据结构和算法入门书籍。下面是我刷过的一些热题,字节面了3个部门也遇到了不少原题,都在下面了。希望你们也能有这样的好运。

下面都是letcode题号+js解法,题目懒得复制了,感兴趣的自己去看看哦~

动态规划

509 斐波那契数列

function fib(n){
  const res=[];
  for(let i=0;i<=n;i++){
    if(i<2){res[i]=i}
    else{
      res[i]=res[i-1]+res[i-2]
    }
  }
  return res[n-1]
}
console.log(fib(10))

70.爬楼梯

function fn(n) {
  const dp = [0, 1, 2];
  for (let i = 3; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }
  return dp[n];
}
console.log(fn(4));

55. 跳跃游戏

var canJump = function(nums) {
  const len = nums.length;
  const dp = Array(len);
  dp[0] = true;
  for (let i = 0; i < len; i++) {
    if (dp[i]) {
      for (let j = 0; j < nums[i]; j++) {
        if (i + j + 1 === len) {
          return true;
        } else {
          dp[i + j + 1] = true;
        }
      }
    }
  }
  return !!dp.pop();
};
console.log(canJump([3, 2, 1, 0, 4]));

62.不同路径

// 2维数组实现
function fn(n, m) {
  const dp = [];
  for (let row = 0; row < n; row++) {
    dp[row] = [1];
  }
  for (let col = 0; col < m; col++) {
    dp[0][col] = 1;
  }
  for (let row = 1; row < n; row++) {
    for (let col = 1; col < m; col++) {
      dp[row][col] = dp[row - 1][col] + dp[row][col - 1];
    }
  }
  return dp[n - 1][m - 1];
}

console.log(fn(7, 3));
// 1维数组实现
function fn1(n, m) {
  const dp = new Array(m).fill(1);
  for (let row = 1; row < n; row++) {
    for (let col = 1; col < m; col++) {
      dp[col] = dp[col - 1] + dp[col];
    }
  }
  return dp[m - 1];
}
console.log(fn1(7, 3));

63.不同路径2

var uniquePathsWithObstacles = function(obstacleGrid) {
    const m = obstacleGrid.length
    const n = obstacleGrid[0].length
    const dp = Array(m).fill().map(item => Array(n).fill(0))
    
    for (let i = 0; i < m && obstacleGrid[i][0] === 0; ++i) {
        dp[i][0] = 1
    }
    
    for (let i = 0; i < n && obstacleGrid[0][i] === 0; ++i) {
        dp[0][i] = 1
    }
    
    for (let i = 1; i < m; ++i) {
        for (let j = 1; j < n; ++j) {
            dp[i][j] = obstacleGrid[i][j] === 1 ? 0 : dp[i - 1][j] + dp[i][j - 1]
        }
    }
        
    return dp[m - 1][n - 1]
    // return dp.pop().pop() 这样写是不是有点意思
};

64.最短路径

function fn(arr) {
  const dp = [arr[0][0]];
  for (let col = 1; col < arr[0].length; col++) {
    dp[col] = dp[col - 1] + arr[0][col];
  }
  for (let row = 1; row < arr.length; row++) {
    dp[0] = dp[0] + arr[row][0];
    for (let col = 1; col < arr[0].length; col++) {
      dp[col] = Math.min(dp[col - 1], dp[col]) + arr[row][col];
    }
  }
  return dp.pop();
}

console.log(fn([
  [1, 3, 4],
  [1, 1, 2],
  [4, 2, 1]
]));

72.编辑距离 hard不要求掌握

// 二维数组
function fn(word1, word2) {
  const dp = Array.from(Array(word1.length + 1), () =>
    Array(word2.length + 1).fill(0)
  );
  for (let i = 1; i <= word1.length; i++) {
    dp[i][0] = i;
  }
  for (let j = 1; j <= word2.length; j++) {
    dp[0][j] = j;
  }
  for (let i = 1; i <= word1.length; i++) {
    for (let j = 1; j <= word2.length; j++) {
      if (word1[i - 1] === word2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1];
      } else {
        // 三选一  哪个操作最后得到的编辑距离最小,就选谁
        dp[i][j] = Math.min(
          dp[i - 1][j],// 删除
          dp[i][j - 1],// 插入
          dp[i - 1][j - 1]// 替换
        ) + 1;
      }
    }
  }

  return dp[word1.length][word2.length];
}

console.log(fn("horse", "ros"));
// 一维数组
// 通过pre变量保存dp[i-1][j-1]可以优化成一维数组
function fn(word1, word2) {
  const dp = Array(word2.length + 1);
  for (let j = 0; j <= word2.length; j++) {
    dp[j] = j;
  }
  for (let i = 1; i <= word1.length; i++) {
    let temp = dp[0];
    dp[0] = i;
    for (let j = 1; j <= word2.length; j++) {
      let pre = temp;
      temp = dp[j];
      if (word1[i - 1] === word2[j - 1]) {
        dp[j] = pre;
      } else {
        // 三选一  哪个操作最后得到的编辑距离最小,就选谁
        dp[j] = Math.min(
          dp[j - 1],// 删除
          dp[j],// 插入
          pre// 替换
        ) + 1;
      }
    }
  }
  return dp[word2.length];
}

console.log(fn("horse", "ros"));

95. 不同的二叉搜索树 II

var generateTrees = function (n) {
  if (n == 0) return [];
  // 备忘录,避免重复计算
  let memo = new Map();
  /* 构造闭区间 [lo, hi] 组成的 BST */
  const build = (lo, hi) => {
    let res = [];
    // base case,显然当lo > hi闭区间[lo, hi]肯定是个空区间,也就对应着空节点 null,
    if (lo > hi) {
      res.push(null);
      return res;
    }
    let memoKey = `${lo}&${hi}`;
    // 如果缓存当中有就直接拿
    if (memo.has(memoKey)) return memo.get(memoKey);
    // 1、穷举 root 节点的所有可能。
    for (let i = lo; i <= hi; i++) {
      // 2、递归构造出左右子树的所有合法 BST。
      let leftTree = build(lo, i - 1);
      let rightTree = build(i + 1, hi);
      // 3、给 root 节点穷举所有左右子树的组合。
      for (let left of leftTree) {
        for (let right of rightTree) {
          res.push(new TreeNode(i, left, right));
        }
      }
    }
    // 将结果集放入到缓存中
    memo.set(memoKey, res);
    return res;
  };
  // 构造闭区间 [1, n] 组成的 BST
  return build(1, n);
}

198 打家劫舍1

// 数组解法
function fn(nums) {
  const dp = [];
  dp[0] = nums[0];
  dp[1] = Math.max(nums[0], nums[1]);
  for (let i = 2, len = nums.length; i < len; i++) {
    dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
  }
  return dp[nums.length - 1];
}
console.log(fn([1, 2, 3, 4]));
// 变量解法
function fn(nums) {
  const len = nums.length;
  if (!len) {
    return 0;
  } else if (len === 1) {
    return nums[0];
  }
  let first = nums[0], second = Math.max(nums[0], nums[1]);
  for (let i = 2; i < len; i++) {
    let temp=second
    second = Math.max(first + nums[i], second);
    first=temp
  }
  return second;
}
console.log(fn([1, 2, 3, 4]));

213 打家劫舍2

function fn(nums) {
  const length = nums.length;
  if (length === 1) {
    return nums[0];
  } else if (length === 2) {
    return Math.max(nums[0], nums[1]);
  }
  function fun(nums, start, end){
    let first = nums[start], second = Math.max(nums[start], nums[start + 1]);
    for (let i = start + 2; i <= end; i++) {
      const temp = second;
      second = Math.max(first + nums[i], second);
      first = temp;
    }
    return second;
  }
  // 偷第一间房则最后一间不能偷,不偷第一间房则最后一间可偷
  return Math.max(fun(nums, 0, length - 2), fun(nums, 1, length - 1));
}

337 打家劫舍3

function fn(root) {
  // 当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱
  // 当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
  function fun(node) {
    if (!node) {
      return [0, 0];
    }
    const res = [];// 0位置不偷,1位置偷
    const left = fun(node.left);
    const right = fun(node.right);
    res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
    res[1] = node.value + left[0] + right[0];
    return res
  }
  const result = fun(root);
  return Math.max(...result);
}

121 买卖股票1

function fn(prices) {
  let minPrice = Number.MAX_SAFE_INTEGER;
  let maxProfit = 0;
  for (let i = 0, len = prices.length; i < len; i++) {
    if (prices[i] < minPrice) {
      minPrice = prices[i];
    } else if (prices[i] - minPrice > maxProfit) {
      maxProfit = prices[i] - minPrice;
    }
  }
  return maxProfit;
}

122 买卖股票2

// 数组
function fn(prices) {
  const n = prices.length;
  const dp = Array(5).fill().map(v =>Array(2).fill(0)); dp[0][0] = 0, dp[0][1] = -prices[0];
  // 0表示当天手里没有股票
  // 1表示当天手里有股票
  for (let i = 1; i < n; ++i) {
    dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
    dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
  }
  return dp[n - 1][0];
 // return dp.pop().shift()
}
// 变量
function fn(prices) {
  const n = prices.length;
  let dp0 = 0, dp1 = -prices[0];
  // 0表示当天手里没有股票
  // 1表示当天手里有股票
  for (let i = 1; i < n; ++i) {
    let newDp0 = Math.max(dp0, dp1 + prices[i]);
    let newDp1 = Math.max(dp1, dp0 - prices[i]);
    dp0 = newDp0;
    dp1 = newDp1;
  }
  return dp0;
}

123 买卖股票3

function fn(prices) {
  // buy1第一次买入状态
  // buy2第二次买入状态
  // sell1第一次卖出状态
  // sell2第二次卖出状态
  const n = prices.length;
  let buy1 = -prices[0], buy2 = -prices[0];
  let sell1 = 0, sell2 = 0;
  for (let i = 1; i < n; i++) {
    buy1 = Math.max(buy1, -prices[i]);
    sell1 = Math.max(sell1, buy1 + prices[i]);
    buy2 = Math.max(buy2, sell1 - prices[i]);
    sell2 = Math.max(sell2, buy2 + prices[i]);
  }
  return sell2;
}

300. 最长递增子序列

var lengthOfLIS = function (nums) {
  const dp = new Array(nums.length).fill(1);
  for (let i = 0; i < nums.length; i++) {
    // i与i前面的元素比较
    for (let j = 0; j < i; j++) {
      // 找比i小的元素,找到一个,就让当前序列的最长子序列长度加1
      if (nums[i] > nums[j]) {
        dp[i] = Math.max(dp[i], dp[j] + 1);
      }
    }
  }
  // 找出最大的子序列
  return Math.max(...dp);
};

152.乘积最大子数组

var maxProduct = function (nums) {
  let n = nums.length;
  let dp = new Array(n).fill(0).map(() => new Array(2).fill(0));
  /* base case */
  // 从第 0 项到第 i 项范围内的子数组的最小乘积
  dp[0][0] = nums[0];
  // 从第 0 项到第 i 项范围内的子数组的最大乘积
  dp[0][1] = nums[0];
  for (let i = 1; i < n; i++) {
    dp[i][0] = Math.min(
      // 不和别人乘就nums[i]自己
      nums[i],
      // nums[i]是负数,希望乘上前面的最小乘积
      dp[i - 1][0] * nums[i],
      // nums[i]是正数,希望乘上前面的最大乘积
      dp[i - 1][1] * nums[i]
    );
    dp[i][1] = Math.max(
      // 不和别人乘就nums[i]自己
      nums[i],
      // nums[i]是负数,希望乘上前面的最小乘积
      dp[i - 1][0] * nums[i],
      // nums[i]是正数,希望乘上前面的最大乘积
      dp[i - 1][1] * nums[i]
    );
  }
  return Math.max(...dp.map((item) => item[1]));
};

回溯算法

背包问题(cache)

function fn(goods, target) {
  let max = 0//最大重量
  const cacheArr=[]
  function backTracing(sum, idx) {
    if (idx === goods.length - 1 || sum === target) {
      if (sum > max) {
        max = sum
      }
      return
    }
    if(cacheArr[sum]&& cacheArr[sum][idx]){return}
    (cacheArr[sum]=cacheArr[sum] || [])[idx]=true
    idx++
    if (sum + goods[idx] <= target) {
      backTracing(sum + goods[idx], idx)//选择当前货物
    }
    backTracing(sum, idx)//不选择当前货物
  }
  backTracing(0, 0)
  return max
}
const goods = [2, 5, 3,9]
const target = 9
console.log(fn(goods, target))

51 N皇后1

function fn(n) {
  const results = [];

  function isValid(row, col,oneResult) {
    let lCol = col - 1, rCol = col + 1;
    for (let i = row - 1; i >= 0; i--) {
      if (oneResult[i] === col) {
        return false;
      }
      if (oneResult[i] === lCol) {
        return false;
      }
      if (oneResult[i] === rCol) {
        return false;
      }
      lCol--;
      rCol++;
    }
    return true;
  }

  function output(oneResult) {
    const arr = oneResult.map(col => {
      let str = "";
      for (let i = 0; i < n; i++) {
        str += i == col ? "Q" : ".";
      }
      return str;
    });
    results.push(arr);
  }

  function backtracing(row,oneResult) {
    if (row === n) {
      output(oneResult);
      return;
    }
    for (let col = 0; col < n; col++) {
      if (isValid(row, col,oneResult)) {
        oneResult[row] = col;
        backtracing(row + 1,oneResult);
      }
    }
  }

  backtracing(0,[]);
  return results;
}

console.log(fn(4));

1091 矩阵最短路径

// 矩阵最短路径(不能被缓存)
function fn(arr) {
  const rowLength = arr.length
  const colLength = arr[0].length
  if (!rowLength || !colLength) {
    return 0
  }
  let max = Number.MAX_SAFE_INTEGER
  function backTracing(row, col, sum) {
    if (row === rowLength - 1 && col === colLength - 1) {
      if (sum < max) {
        max = sum
      }
      return
    }
    if (row + 1 < rowLength) {
      backTracing(row + 1, col, sum + arr[row + 1][col])
    }
    if (col + 1 < colLength) {
      backTracing(row, col + 1, sum + arr[row][col + 1])
    }
  }

  backTracing(0, 0, arr[0][0])
  return max
}
console.log(fn([
  [1, 1, 3, 4, 5],
  [2, 1, 2, 5, 6],
  [3, 1, 1, 1, 1],
]))

46 全排列 不重复

function fn(arr) {
  const result = []
  function backTracing(nums) {
    if (nums.length === arr.length) {
      result.push(nums.slice())
      return
    }
    for (let i = 0; i < arr.length; i++) {
      if (!nums.includes(arr[i])) {
        nums.push(arr[i])
        backTracing(nums)
        nums.pop()
      }
    }
  }

  backTracing([])
  return result
}
console.log(fn([1, 2,3]))

47 全排列 重复

function fn(arr) {
  arr.sort((a, b) => a - b)
  const result = []

  function backTracing(nums, idxs) {
    if (nums.length === arr.length) {
      result.push(nums.slice())
      return
    }
    for (let i = 0; i < arr.length; i++) {
      // 当前数和前一个数相同并且前一个数已经放入,当前下标已放入则跳过
      if ((arr[i] === arr[i - 1] && idxs.includes(i - 1)) || idxs.includes(i)) {
        continue
      }
      nums.push(arr[i])
      idxs.push(i)
      backTracing(nums, idxs)
      nums.pop()
      idxs.pop()
    }
  }

  backTracing([], [])
  return result
}

console.log(fn([1, 2, 1]))

78.子集 不重复

function fn(arr){
  const result=[]
  function backTracing(nums,idx){
    if(idx===arr.length-1){
      result.push(nums.slice())
      return
    }
    idx++
    nums.push(arr[idx])
    backTracing(nums,idx)
    nums.pop()
    backTracing(nums,idx)
  }
  backTracing([],-1)
  return result
}
console.log(fn([1, 2, 3, 4]))

90 子集 重复数组

function fn(arr) {
  arr.sort((a, b) => a - b);
  const result = [];

  function backTracing(nums, idx, idxs) {
    if (idx === arr.length - 1) {
      result.push(nums.slice());
      return;
    }
    idx++;
    if (arr[idx] === arr[idx - 1] && idxs.includes(idx - 1)) {
      return;
    }
    nums.push(arr[idx]);
    idxs.push(idx);
    backTracing(nums, idx, idxs);
    nums.pop();
    idxs.pop();
    backTracing(nums, idx, idxs);
  }
  backTracing([], -1, []);
  return result;
}
console.log(fn([1, 1, 3]));

77. 组合1

function fn(n, k) {  
  if(n===0 ||k===0){return []}  
  const result = [];
  function backTracing(nums, idx) {
    if (nums.length === k) {
      return result.push(nums.slice());
    }
    if(idx===n){
      return
    }

    idx++;
    nums.push(idx);
    backTracing(nums, idx);
    nums.pop();
    backTracing(nums, idx);
  }

  backTracing([], 0);
  return result;
}

console.log(fn(4,2));

39.组合总和1

function fn(arr, target) {
  arr.sort((a, b) => a - b);
  const result = [];

  function backTracing(total,path) {
    if (total === target) {
      return result.push(path.slice());
    }
    if (total > target) {
      return;
    }
    for (let i = 0; i < arr.length; i++) {
      if(arr[i]>=path[path.length-1] || !path.length){
        path.push(arr[i])
        total += arr[i];
        backTracing(total,path);
        path.pop()
        total -= arr[i];
      }
    }
  }

  backTracing(0,[]);
  return result;
}

console.log(fn([2, 3, 6, 7], 7));

40. 组合总和2

var combinationSum2 = function(candidates, target) {
  let res = [];
  // 记录回溯的路径
  let track = [];
  // 回溯算法主函数
  const backtrack = (start, sum) => {
    // 找到目标和
    if (sum == target) {
      return res.push(track.slice());
    }
    // 超过目标和,直接结束
    if (sum > target) return;
    for (let i = start; i < candidates.length; i++) {
      // 当前元素跟上一个元素相同且上一个元素在索引选择范围内则当前元素就不需要加入
      if (i - 1 >= start && candidates[i - 1] == candidates[i]) continue;
      // 选择 candidates[i]
      track.push(candidates[i]);
      sum += candidates[i];
      // 递归遍历下一层回溯树
      backtrack(i + 1, sum);
      // 撤销选择 candidates[i]
      track.pop();
      sum -= candidates[i];
    }
  };
  // 先将数组升序排列
  candidates.sort((a, b) => a - b);
  backtrack(0, 0);
  return res;
};

剑指 Offer 38. 字符串的排列

function fn(str) {
  const result = [];

  function backTracing(current) {
    if (current.length === str.length) {
      return result.push(current);
    }
    for (let i = 0, len = str.length; i < len;i++ ) {
      if(current.includes(str[i])){
        continue
      }
      current=current+str[i]
      backTracing(current)
      current=current.slice(0,-1)
    }
  }

  backTracing("");
  return result;
}

console.log(fn("asd"));

面试题 08.07. 无重复字符串的排列组合

function fn(str) {
  str = str.split("").sort().join("");
  const result = [];

  function backTracing(current,idxs) {
    if (current.length === str.length) {
      return result.push(current);
    }
    for (let i = 0, len = str.length; i < len;i++ ) {
      if((i>0 && str[i]===str[i-1] && idxs.includes(i-1)) || idxs.includes(i)){
        continue
      }
      idxs.push(i)
      current=current+str[i]
      backTracing(current,idxs)
      idxs.pop()
      current=current.slice(0,-1)
    }
  }

  backTracing("",[]);
  return result;
}

console.log(fn("asa"));console.log(fn("aas"));//[ 'aas', 'asa', 'saa' ]
// 结果中的aas下标为[1,0,2];saa下标为[2,1,0]

1291 顺次数

function fn(low, height) {
  const result = [];
  for (let i = 1; i <= 9; i++) {
    backTracing(i, i);
  }

  function backTracing(currentNum, currentNumTail) {
    if (currentNum >= low && currentNum <= height) {
      result.push(currentNum);
    }
    if (currentNum > height) {
      return;
    }
    const nextCurrentNumTail = ++currentNumTail;
    if (nextCurrentNumTail <= 9) {
      backTracing(currentNum * 10 + nextCurrentNumTail, nextCurrentNumTail);
    }
  }

  result.sort((a, b) => a - b);
  return result;
}

console.log(fn(1000, 13000));

17. 电话号码的字母组合

function fn(str) {
  if (!str) return [];
  const phone = { 2: "abc", 3: "def", 4: "ghi", 5: "jkl", 6: "mno", 7: "pqrs", 8: "tuv", 9: "wxyz" };
  const result = [], arr = [];
  for (let digit of str) {
    arr.push(phone[digit]);
  }

  function backTracing(current, idx) {
    if (idx === str.length) {
      return result.push(current);
    }
    for (let item of arr[idx]) {
      backTracing(current+item, idx + 1);
    }

  }

  backTracing("", 0);
  return result;
}

console.log(fn("23"));

21 括号生成

function fn(n) {
  if(n===0){
    return
  }
  const result = [];

  function backTracing(str,lIdx, rIdx) {
    if(lIdx===n && rIdx===n){
      result.push(str)
    }
    if(rIdx>lIdx){
      return
    }
    lIdx<n && backTracing(str+'(',lIdx+1,rIdx)
    rIdx<n && backTracing(str+')',lIdx,rIdx+1)
  }
  backTracing('',0,0)

  return result;
}

console.log(fn(3));

784 字母大小写全排列

function fn(str) {
  const result = [];

  function backTracing(start,current) {
    result.push(current)
    for (let i = start; i < str.length; i++) {
      if(str[i] >= 'a' && str[i] <= 'z') {
        backTracing(i+1, str.slice(0, i) + str[i].toUpperCase() + str.slice(i+1));
      }
      if(str[i] >= 'A' && str[i] <= 'Z') {
        backTracing(i+1, str.slice(0, i) + str[i].toLowerCase() + str.slice(i+1));
      }
    }
  }
  backTracing(0,str)

  return result;
}

console.log(fn('a1b2'));

131.分割回文串

function fn(str) {
  const result = [];

  function backTracing(start, current) {
    if (start === str.length) return result.push(current.slice());
    for (let i = start; i < str.length; i++) {
      if (!isValid(start, i)) continue;
      current.push(str.slice(start, i + 1));
      backTracing(i + 1, current);
      current.pop();
    }
  }

  function isValid(start, end) {
    let l = start, h = end;
    while (l < h) {
      if (str[l++] !== str[h--]) return false;
    }
    return true;
  }

  backTracing(0, []);

  return result;
}

console.log(fn("aab"));

93.复原IP地址

function fn(str) {
  const result = [];

  function backTracing(start, current) {
    if (current.length == 4 && start === str.length) {
      return result.push(current.join("."));
    }
    for (let i = start; i < str.length; i++) {
      const temp = str.substring(start, i + 1);
      if (Number(temp) > 255 || temp.startsWith("0") && temp!=="0") {
        break;
      }
      current.push(temp);
      backTracing(i + 1, current);
      current.pop();
    }
  }


  backTracing(0, []);

  return result;
}

console.log(fn("25525511135"));

200. 岛屿数量

const numIslands = (grid) => {
  function turnZero(i, j, grid) {
    if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] === '0'){
      return
    }
    grid[i][j] = '0'
    turnZero(i, j + 1, grid)
    turnZero(i, j - 1, grid)
    turnZero(i + 1, j, grid)
    turnZero(i - 1, j, grid)
  }
  let count = 0
  for (let i = 0; i < grid.length; i++) {
    for (let j = 0; j < grid[0].length; j++) {
      if (grid[i][j] === '1') {
        count++
        turnZero(i, j, grid)
      }
    }
  }
  return count
}

二叉树

先序遍历 DFS

function fn(root) {
  if(!root){
    return
  }
  function order(node){
    if(!node){
      return
    }
    console.log(node.value)
    order(node.left)
    order(node.right)
  }
  order(root)
}

中序遍历

function fn(root) {
  if(!root){
    return
  }
  function order(node){
    if(!node){
      return
    }
    order(node.left)
    console.log(node.value)
    order(node.right)
  }
  order(root)
}

后序遍历

function fn(root) {
  if(!root){
    return
  }
  function order(node){
    if(!node){
      return
    }
    order(node.left)
    order(node.right)
    console.log(node.value)
  }
  order(root)
}

层次遍历 BFS

function fn(root) {
  if (!root) {
    return;
  }
  const stack = [root];
  while (stack.length) {
    const node=stack.shift()
    if(node){
      console.log(node.value);
      stack.push(node.left)
      stack.push(node.right)
    }
  }
}

103. 二叉树的锯齿形层序遍历

var zigzagLevelOrder = function(root) {
    if (!root) {
        return [];
    }

    const ans = [];
    const nodeQueue = [root];

    let isOrderLeft = true;

    while (nodeQueue.length) {
        let levelList = [];
        const size = nodeQueue.length;
        for (let i = 0; i < size; ++i) {
            const node = nodeQueue.shift();
            if (isOrderLeft) {
                levelList.push(node.val);
            } else {
                levelList.unshift(node.val);
            }
            if (node.left !== null) {
                nodeQueue.push(node.left);
            }
            if (node.right !== null) {
                nodeQueue.push(node.right);
            }
        }            
        ans.push(levelList);
        isOrderLeft = !isOrderLeft;
    }
    return ans;
};

104. 二叉树的最大深度

function fn(root) {
  if (root === null) {
    return 0;
  }
  const leftHeight = fn(root.left);
  const rightHeight = fn(root.right);
  return Math.max(leftHeight, rightHeight) + 1;
}

226. 翻转二叉树

var invertTree = function(root) {
  function transform(root){
    if(!root){
      return
    }
    const temp=root.left
    root.left=root.right
    root.right=temp
    transform(root.left)
    transform(root.right)
  }
  transform(root)
  return root
};

236. 二叉树的最近公共祖先

var lowestCommonAncestor = function(root, p, q) {
    let ans;
    const dfs = (root, p, q) => {
        if (root === null) return false;
        const lson = dfs(root.left, p, q);
        const rson = dfs(root.right, p, q);
        if ((lson && rson) || ((root.val === p.val || root.val === q.val) && (lson || rson))) {
            ans = root;
        } 
        return lson || rson || (root.val === p.val || root.val === q.val);
    }
    dfs(root, p, q);
    return ans;
};

543. 二叉树的直径

var diameterOfBinaryTree = function (root) {
  let maxDiameter = 0;
  const dfs = (root) => {
    if (root == null) return 0;
    let leftMax = dfs(root.left);
    let rightMax = dfs(root.right);
    maxDiameter = Math.max(maxDiameter, leftMax + rightMax);
    return Math.max(leftMax, rightMax) + 1;
  };
  dfs(root);
  return maxDiameter;
}

617. 合并二叉树

var mergeTrees = function(root1, root2) {
  function fn(node1,node2){
    if(!node1 && !node2){
      return null
    }
    if(!node1){
      return node2
    }
    if(!node2){
      return node1
    }
    return new TreeNode(node1.val+node2.val,fn(node1.left,node2.left),fn(node1.right,node2.right))
  }
  return fn(root1, root2)
};

98. 验证二叉搜索树

var isValidBST = function(root) {
  const helper = (root, lower, upper) => {
    if (root === null) {
      return true
    }
    if (root.val <= lower || root.val >= upper) {
      return false
    }
    return helper(root.left, lower, root.val) && helper(root.right, root.val, upper)
  }
  return helper(root, -Infinity, Infinity)
}

105. 从前序与中序遍历序列构造二叉树

var buildTree = function (preOrder, inOrder) {
  const build = (preStart, preEnd, inStart, inEnd) => {
    if (preStart > preEnd) return null;
    // root 节点对应的值就是前序遍历数组的第一个元素
    let rootVal = preOrder[preStart];
    // rootVal 在中序遍历数组中的索引
    let index = inOrder.indexOf(rootVal);
    // 左子树个数
    let leftSize = index - inStart;
    // 先构造出当前根节点
    let root = new TreeNode(rootVal);
    // 递归构造左右子树
    root.left = build(preStart + 1, preStart + leftSize, inStart, index - 1);
    root.right = build(preStart + leftSize + 1, preEnd, index + 1, inEnd);
    return root;
  };
  return build(0, preOrder.length - 1, 0, inOrder.length - 1);
};

114. 二叉树展开为链表

var flatten = function(root) {
    const list = [];
    preorderTraversal(root, list);
    const size = list.length;
    for (let i = 1; i < size; i++) {
        const prev = list[i - 1], curr = list[i];
        prev.left = null;
        prev.right = curr;
    }
};

const preorderTraversal = (root, list) => {
    if (root != null) {
        list.push(root);
        preorderTraversal(root.left, list);
        preorderTraversal(root.right, list);
    }
}

其他

二分查找

function fn(arr, x) {
  let low = 0, height = arr.length - 1;
  while (low <= height) {
    let mid = Math.floor((low + height) / 2);
    if (x == arr[mid]) {
      return mid;
    } else if (x < arr[mid]) {
      height = mid - 1;
    } else {
      low = mid + 1;
    }
  }
  return -1;
}

1. 两数之和

function fn(arr, target) {
  const map = new Map();
  for (let i = 0, len = arr.length; i < len - 1; i++) {
    if (map.has(target - arr[i])) {
      return [i, map.get(target - arr[i])];
    } else {
      map.set(arr[i], i);
    }
  }
  return [];
}
console.log(fn([1, 2, 3, 4, 5, 6, 7, 8, 9], 10));

3. 无重复字符的最长子串

var lengthOfLongestSubstring = function(s) {
  let res = 0;
  for (let i = 0, j = 0, len = s.length; i < len; i++) {
    const idx = s.indexOf(s[i], j);
    if (idx !== i && idx !== -1) {
      j = idx + 1;
    } else {
      res = Math.max(res, i - j + 1);
    }
  }
  return res;
};
console.log(lengthOfLongestSubstring("sdfshfdghdss"));

6. Z 字形变换

var convert = function(s, numRows) {
  if (s.length <= numRows || numRows === 1) {
    return s
  }
  const arr = new Array(numRows).fill('')
  let num = 0
  let plus = true
  for (let i = 0; i < s.length; i++) {
    arr[num] += s[i]
    if (plus) {
      num += 1
    } else {
      num -= 1
    }
    if (num === 0) {
      plus = true
    }
    if (num === numRows - 1) {
      plus = false
    }
  }
  return arr.join('')
}

7. 整数反转

var reverse = function(x) {
  let result = Number(
    (Math.abs(x) + '')
      .split('')
      .reverse()
      .join('')
  )
  result = x > 0 ? result : -result
  if (result < Math.pow(-2, 31) || result > Math.pow(2, 31) - 1) {
    return 0
  }
  return result
}

9.回文数

function isPalindrome(x) {
  if (x < 0 || (x % 10 === 0 && x !== 0)) {
    return false
  }

  let revertedNumber = 0
  while (x > revertedNumber) {
    revertedNumber = revertedNumber * 10 + (x % 10)
    x = Math.floor(x / 10)
  }

  // 当数字长度为奇数时,我们可以通过 revertedNumber/10 去除处于中位的数字。
  // 例如,当输入为 12321 时,在 while 循环的末尾我们可以得到 x = 12,revertedNumber = 123,
  // 由于处于中位的数字不影响回文(它总是与自己相等),所以我们可以简单地将其去除。
  return x === revertedNumber || x === Math.floor(revertedNumber / 10)
}

14. 最长公共前缀

function longestCommonPrefix(strs) {
    let str = '', i = 0;
    while(strs[0][i]){
        for(let s of strs){
            if(s[i] !== strs[0][i]){
                return str;
            }
        }
        str += strs[0][i];
        i++;
    }
    return str;
};

19. 删除链表的倒数第 N 个结点

// 方法1:顺序遍历+倒序遍历n
// 方法2:顺序遍历放到栈中,出栈第n个节点
// 方法3:双指针法,second比first超前n个节点

20. 有效的括号

var isValid = function(s) {
    const n = s.length;
    if (n % 2 === 1) {
        return false;
    }
    const pairs = new Map([
        [')', '('],
        [']', '['],
        ['}', '{']
    ]);
    const stk = [];
    for (let ch of s){
        if (pairs.has(ch)) {
            if (!stk.length || stk[stk.length - 1] !== pairs.get(ch)) {
                return false;
            }
            stk.pop();
        } 
        else {
            stk.push(ch);
        }
    };
    return !stk.length;
};

21. 合并两个有序链表

var mergeTwoLists = function (l1, l2) {
  if (l1 === null) {
    return l2;
  } else if (l2 === null) {
    return l1;
  } else if (l1.val < l2.val) {
    l1.next = mergeTwoLists(l1.next, l2);
    return l1;
  } else {
    l2.next = mergeTwoLists(l1, l2.next);
    return l2;
  }
}

26. 删除有序数组中的重复项

function fn(arr) {
  let i = 0, j = 0, len = arr.length;
  while (j < len) {
    if (arr[j] === arr[j - 1]) {
      j++;
      continue;
    }
    arr[i] = arr[j];
    i++;
    j++;
  }
  return arr.slice(0, i);
}

27. 移除元素

var removeElement = function(nums, val) {
    const len = nums.length
    let j = 0
    for (let i = 0; i < len; i++) {
        if (nums[i] !== val) {
            nums[j++] = nums[i]
        }
    }
    return j
};

34. 在排序数组中查找元素的第一个和最后一个位置

const binarySearch = (nums, target, lower) => {
  let left = 0, right = nums.length - 1, ans = nums.length;
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (nums[mid] > target || (lower && nums[mid] >= target)) {
      right = mid - 1;
      ans = mid;
    } else {
      left = mid + 1;
    }
  }
  return ans;
}
var searchRange = function (nums, target) {
  let ans = [-1, -1];
  const leftIdx = binarySearch(nums, target, true);
  const rightIdx = binarySearch(nums, target, false) - 1;
  if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] === target && nums[rightIdx] === target) {
    ans = [leftIdx, rightIdx];
  }
  return ans;
};

45. 跳跃游戏 II

function fn(nums){
  let position = nums.length - 1;
  let steps=0
   while (position > 0) {
      for (let i = 0; i < position; i++) {
        if (i + nums[i] >= position) {
          position = i;
          steps++;
          break;
        }
      }
  }
 return steps;
}

53. 最大子数组和

var maxSubArray = function(nums) {
  let pre = 0, maxAns = nums[0];
  nums.forEach((x) => {
    pre = Math.max(pre + x, x);    maxAns = Math.max(maxAns, pre);
  });  return maxAns;
};
console.log(maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4]));

56. 合并区间

var merge = function(intervals) {
  if (!intervals.length) {
    return [];
  }
  intervals.sort((a, b) => a[0] - b[0]);
  const result = [];
  for (let i = 0; i < intervals.length; i++) {
    if (i === 0 || intervals[i][0] > result[result.length - 1][1]) {
      result.push(intervals[i]);
    } else {
      result[result.length - 1][1] = Math.max(result[result.length - 1][1], intervals[i][1]);
    }
  }
  return result;
};
console.log(merge([[1, 3], [0, 6], [8, 10], [15, 18]]));

58. 最后一个单词的长度

var lengthOfLastWord = function(s) {
    let index = s.length - 1;
    while (s[index] === ' ') {
        index--;
    }
    let wordLength = 0;
    while (index >= 0 && s[index] !== ' ') {
        wordLength++;
        index--;
    }
    return wordLength;
};

75. 颜色分类

function fn(nums){
  const n = nums.length;
  let ptr = 0;
  for (let i = 0; i < n; ++i) {
    if (nums[i] == 0) {
      let temp = nums[i];
      nums[i] = nums[ptr];
      nums[ptr] = temp;
      ++ptr;
    }
  }
  for (let i = ptr; i < n; ++i) {
    if (nums[i] == 1) {
      let temp = nums[i];
      nums[i] = nums[ptr];
      nums[ptr] = temp;
      ++ptr;
    }
  }
  return nums
}
console.log(fn([2,0,2,1,1,0]));

76. 最小覆盖子串

var minWindow = function(s, t) {
  // 需要的
  let need = {};
  // 窗口中的字符
  let window = {};
  for (let a of t) {
    // 统计需要的字符
    need[a] = (need[a] || 0) + 1;
  }
  // 左右指针
  let left = 0,
    right = 0;
  let valid = 0;
  // 最小覆盖子串的起始索引及长度
  let start = 0,
    len = Number.MAX_VALUE;
  while (right < s.length) {
    // 即将移入窗口的字符
    let c = s[right];
    // 右移窗口
    right++;
    if (need[c]) {
      // 当前字符在需要的字符中,则更新当前窗口统计
      window[c] = (window[c] || 0) + 1;
      if (window[c] == need[c]) {
        // 当前窗口和需要的字符匹配时,验证数量增加1
        valid++;
      }
    }
    // 当验证数量与需要的字符个数一致时,就应该收缩窗口了
    while (valid == Object.keys(need).length) {
      // 更新最小覆盖子串
      if (right - left < len) {
        start = left;
        len = right - left;
      }
      //即将移出窗口的字符
      let d = s[left];
      // 左移窗口
      left++;
      if (need[d]) {
        if (window[d] == need[d]) {
          valid--;
        }
        window[d]--;
      }
    }
  }
  return len == Number.MAX_VALUE ? "" : s.substr(start, len);
};

136. 只出现一次的数字

function fn(nums) {
  // 0^0=0  a^a=0 异或运算满足交换律和结合律
  let result = 0;
  for (let i = 0, len = nums.length; i < len; i++) {
    result ^= nums[i];
  }
  return result;
}

136. 只出现一次的数字

function fn(nums) {
  // 0^0=0  a^a=0 异或运算满足交换律和结合律
  let result = 0;
  for (let i = 0, len = nums.length; i < len; i++) {
    result ^= nums[i];
  }
  return result;
}

141. 环形链表

function fn(head) {
  const cache = new Set();
  while (head) {
    if (cache.has(head)) {
      return true;
    } else {
      cache.add(head);
      head = head.next;
    }
  }
  return false;
}

160. 相交链表

var getIntersectionNode = function(headA, headB) {
    const cache = new Set()
    while(headA){
        cache.add(headA)
        headA=headA.next
    }
    while(headB){
        if(cache.has(headB)){
            return headB
        }else{
            headB=headB.next
        }
    }
    return null
};

206. 反转链表

var reverseList = function (head) {
  let temp = new ListNode();
  let next = null;
  while (head) {
    next = head.next;//下一个节点
    head.next = temp.next;
    temp.next = head;//head接在temp的后面
    head = next;//head向后移动一位
  }
  return temp.next;
};

215. 数组中的第K个最大元素

var findKthLargest = function(nums, k) {
  const topKArr = nums.slice(0, k);
  topKArr.sort((a, b) => b - a);
  for (let i = k, len = nums.length; i < len; i++) {
    if (nums[i] > topKArr[k - 1]) {
      let idx = k - 1;
      while (nums[i] > topKArr[idx]) {
        idx--;
      }
      topKArr.splice(idx + 1, 0, nums[i]);
    }
  }
  return topKArr[k - 1];
};
console.log(findKthLargest([3, 2, 3, 1, 2, 4, 5, 5, 6], 4));

283. 移动零

var moveZeroes = function (nums) {
  let fast = 0, slow = 0;
  while (fast < nums.length) {
    if (nums[fast] != 0) {
      nums[slow++] = nums[fast];
    }
    fast++;
  }
  while (slow < nums.length) {
    nums[slow++] = 0;
  }
  return nums;
};

448. 找到所有数组中消失的数字

var findDisappearedNumbers = function (nums) {
  const n = nums.length;
  for (const num of nums) {
    const x = (num - 1) % n;
    nums[x] += n;
  }
  const ret = [];
  for (const [i, num] of nums.entries()) {
    if (num <= n) {
      ret.push(i + 1);
    }
  }
  return ret;
};

128. 最长连续序列

var longestConsecutive = function (nums: number[]): number {
  let num_set: Set<number> = new Set();
  for (const num of nums) {
    num_set.add(num);
  }
  let longestStreak = 0;
  for (const num of num_set) {
    if (!num_set.has(num - 1)) {
      let currentNum = num;
      let currentStreak = 1;
      while (num_set.has(currentNum + 1)) {
        currentNum += 1;
        currentStreak += 1;
      }
      longestStreak = Math.max(longestStreak, currentStreak);
    }
  }
  return longestStreak;
};

165. 比较版本号

var compareVersion = function(version1, version2) {
    const v1 = version1.split('.');
    const v2 = version2.split('.');
    for (let i = 0; i < v1.length || i < v2.length; ++i) {
        let x = 0, y = 0;
        if (i < v1.length) {
            x = parseInt(v1[i]);
        }
        if (i < v2.length) {
            y = parseInt(v2[i]);
        }
        if (x > y) {
            return 1;
        }
        if (x < y) {
            return -1;
        }
    }
    return 0;
};

叨叨一下

这次面试之前我没刷八股文和节流防抖这些,一来是我以前都刷过有点腻了,二来是我觉得5年的前端不至于问这些吧。没想到字节一面就吃了个大亏。。。真是尴尬。手写柯力化竟然没写出来,一个字,绝!搞得我当场心态崩了,后面拉跨。

所以下面也整理了一些基础手写题,希望后来者不要踩坑。

JS手写题

AOP

Function.prototype.before=function(fn){
  return (...arg)=>{
    fn.call(this,...arg)
    this(...arg)
  }
}
Function.prototype.after=function(fn){
  return (...arg)=>{
    const result =this(...arg)
    fn.call(this,...arg)
    return result
  }
}

柯里化 去柯里化

function curring(){
  // 使用...arg接受参数则不能使用arguments.callee
  this._cache = this._cache || (this._cache=[])
  if(arguments.length){
    this._cache=[...this._cache,...arguments]
    return arguments.callee
  }else{
    const result = this._cache.reduce((current,item)=>current+item,0)
    this._cache = []
    return result
  }
}
Function.prototype.uncurrying = function () {
  let self = this;
  return function () {
    // (Function.prototype.call).apply(self,arguments)
    return Function.prototype.call.apply(self, arguments);
  }
};

偏函数

function partial(fn, ...argFix) {
  return function(...argCurr) {
    let arg = [], idx = 0
    for (let i = 0; i < argFix.length; i++) {
      if (argFix[i] === undefined) {
        arg[i] = argCurr[idx++]
      } else {
        arg[i] = argFix[i]
      }
    }
    //将剩余的参数拼接到一起
    arg = arg.concat(argCurr.slice(idx))
    return fn(...arg)
  }
}
function add(a,b,c,d,e){
  return a*b+c*d+e;
}
let partial_add=partial(add,1,undefined,2,undefined);
console.log(partial_add(3,4,5));
//1*3+2*4+5 = 16

节流

function throttle(fn,timeout){
  let timer
  let first
  return ()=>{
    if(!first){
      fn()
      first=true
    }else if(timer){
      return
    }else{
      timer=setTimeout(()=>{
        fn()
        timer=null
      },timeout)
    }
  }
}

防抖

function debounce(fn,timeout){
  let timer
  function timeoutFn(){
    timer=setTimeout(()=>{
      fn()
      timer=null
    },timeout)
  }
  return ()=>{
    if(timer){
      clearTimeout(timer)
    }
    timeoutFn()
  }
}

分时函数

function timeChunk(arr, fn, count, interval) {
  for (let i = 0; i < Math.min(count, arr.length); i++) {
    fn(arr.shift())
  }
  if (arr.length) {
    setTimeout(() => {
      arguments.callee(arr, fn, count, interval)
    }, interval)
  }
}
timeChunk([1,2,3,4,5,6],(val)=>{console.log(val)},2,1000)

eventEmitter

class eventEmitter {
  constructor() {
    this._events = [];//存储自定义事件
  }

  /**
   * 注册事件和处理函数
   * @param event
   * @param fn
   */
  on(event, fn) {
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.on(event[i], fn)
      }
    } else {
      // 存在直接push, 不存在创建为空数组再push
      (this._events[event] || (this._events[event] = [])).push(fn)
    }
  }

  /**
   * 注册事件和处理函数,触发一次后销毁
   * @param event
   * @param fn
   */
  once(event, fn) {
    let _self=this;
    function handler() {
      _self.off(event, handler);
      fn.apply(null,arguments);//emit里面调用时会给on方法传参
    }

    handler.fn = fn;//off里面根据这个判断销毁事件
    this.on(event, handler);
  }

  /**
   * 销毁事件和处理函数
   * @param event
   * @param fn
   */
  off(event, fn) {
    //不传参数表示清空所有
    if (!arguments.length) {
      this._events = [];
    }
    //数组循环清空
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.off(event[i], fn)
      }
    }
    const cbs = this._events[event];
    if (!cbs) {
      return;
    }
    //不传第二参表示清空某事件所有监听函数
    if (arguments.length == 1) {
      this._events[event] = null
    }
    let cb, i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) { //cb.fn===fn用来移除once注册的事件
        cbs.splice(i, 1)
        break
      }
    }
  }

  /**
   * 触发某事件所有回调并带参数
   * @param event
   */
  emit(event) {
    //once删除事件会导致下面循环过程中this._events内fn前移, 所以此处复制成新数组
    let cbs = [...this._events[event]];
    if (cbs) {
      for (let i = 0, l = cbs.length; i < l; i++) {
        try {
          cbs[i].apply(null,[...arguments].slice(1))
        } catch (e) {
          new Error(e, `event handler for "${event}"`)
        }
      }
    }
  }
}
//测试用例
let eb=new eventEmitter();
eb.once('event1',params=>console.log(11,params));
eb.on('event1',params=>console.log(22,params));
eb.emit('event1',33)

发布订阅

//主题
class Dep{
  constructor(callback){
    this.subs=[];
    //每个主题更新数据的方法不一样,所以需要传进来
    this.callback=callback;
  }
  addSub(sub){
    this.subs.push(sub);
    return this;
  }
  notify(){
    //通知此主题所有订阅者更新数据
    this.subs.forEach(item=>item.update(this.callback))
  }
}
//订阅者(Watcher)
class Sub{
  constructor(val) {
    this.val=val
  }
  update(callback){
    this.val=callback(this.val);
    //相当于更新视图操作
    console.log(this.val);
  }
}
//发布者
class Pub{
  constructor(){
    this.deps=[];
  }
  addDep(dep){
    this.deps.push(dep)
  }
  removeDep(dep){
    let index=this.deps.indexOf(dep);
    if(index!==-1){
      this.deps.splice(index,1);
      return true
    }else{
      return false
    }
  }
  publish(dep){
   //发布某个主题的更新
    this.deps.forEach(item=>item == dep && item.notify());
  }
}

//新建主题, 并向主题中增加订阅者
let dep1=new Dep(item=>item*item);
dep1.addSub(new Sub(1)).addSub(new Sub(2)).addSub(new Sub(3));
//新建发布者, 并向发布者中增加主题
let pub=new Pub();
pub.addDep(dep1);
//发布者发布, 通知所有主题的所有订阅者更新
pub.publish(dep1);
//发布者发布, 通知所有主题的所有订阅者更新
pub.publish(dep1);
console.log("===========================");
//新建主题, 并向主题中增加订阅者
let dep2=new Dep(item=>item+item);
dep2.addSub(new Sub(1)).addSub(new Sub(2)).addSub(new Sub(3));
//向发布者中增加主题
pub.addDep(dep2);
//发布者发布, 通知所有主题的所有订阅者更新
pub.publish(dep2);
//发布者发布, 通知所有主题的所有订阅者更新
pub.publish(dep2);

//结果
1
4
9
1
16
81

2
4
6
4
8
12

观察者模式

// 目标对象
class Subject {
  constructor() {
    this.observerList = [];
  }

  addObserver(observer) {
    this.observerList.push(observer);
  }

  notify() {
    this.observerList.forEach((observer) => {
      observer.update();
    });
  }
}

// 观察者
class Observer {
  constructor(cb) {
    if (typeof cb === "function") {
      this.cb = cb;
    } else {
      throw new Error("Observer构造器必须传入函数类型!");
    }
  }
  update() {
    this.cb();
  }
}

const observerCallback = function () {
  console.log("我被通知了");
};
const observer = new Observer(observerCallback);

const subject = new Subject();
subject.addObserver(observer);
subject.notify();

实现new

function _new(...args) {
  let constructor = args[0];//获取构造函数
  let obj = Object.create(constructor.prototype);//创建空对象,并将原型指向构造函数的原型
  let res = constructor.call(obj, ...args.slice(1));//call强行将this指向第一个参数
  if ((typeof res === 'object' || typeof res === 'function') && res != null) {
    return res;
  } else {
    return obj;
  }
}

深clone

// 递归暴栈(迭代循环)
// 循环引用(记录已复制的引用类型)
function clone(data) {
  let target
  if (Array.isArray(data)) {
    target = []
  } else {
    target = Object.create(Object.getPrototypeOf(data))
  }
  for (let [key, val] of Object.entries(data)) {
    if (data.hasOwnProperty(key)) {
      if (val === null) {
        target[key] = null
      } else if (typeof val !== 'object') {
        Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(data, key))
      } else {
        const type = Object.prototype.toString.call(val).slice(8, -1)
        switch (type) {
          case 'RegExp':
            target[key] = new RegExp(val)
            break
          case 'Date':
            target[key] = new Date(val.getTime())
            break
          case 'Function':
            target[key] = val
            break
          default:
            target[key] = clone(val)
        }
      }
    }
  }
  return target
}

console.log(clone({ a: 1, b: 2, c: 3, d: new Date(), h: /123/, f: {}, g: [1, 2, 3] }))
// 简单写法
Object.prototype.clone=function(){
  //原型指向保持一致
  var newobj=Object.create(Object.getPrototypeOf(this));
  //自身属性保持一样
  var propNames=Object.getOwnPropertyNames(this);
  propNames.forEach(function(item){
    //保持每个属性的特性也一样
    var des=Object.getOwnPropertyDescriptor(this,item);
    Object.defineProperty(newobj,item,des);
  },this);
  return newobj;
}

实现instanceof

function MyInstanceof(child, parent) {
  let proto = Object.getPrototypeOf(child)
  while (true) {
    if (proto === null) {
      return false
    }
    if (parent.prototype === proto) {
      return false
    }
    proto = Object.getPrototypeOf(proto)
  }
}

手写promise

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    // 防止执行报错,报错直接走reject
    try {
      executor(this.resolve, this.reject)
    } catch (error) {
      this.reject(error)
    }
  }

  status = PENDING
  value = null
  reason = null
  onFulfilledCallbacks = []
  onRejectedCallbacks = []
  resolve = (value) => {
    if (this.status === PENDING) {
      this.status = FULFILLED
      this.value = value
      // 当promise异步调用resolve时,then会先于resolve执行,需要将then第一个参数封装成回调存储并在此处执行
      while (this.onFulfilledCallbacks.length) {
        this.onFulfilledCallbacks.shift()(value)
      }
    }
  }
  reject = (reason) => {
    if (this.status === PENDING) {
      this.status = REJECTED
      this.reason = reason
      // 当promise异步调用resolve时,then会先于resolve执行,需要将then第二个参数封装成回调存储并在此处执行
      while (this.onRejectedCallbacks.length) {
        this.onRejectedCallbacks.shift()(reason)
      }
    }
  }

  then(onFulfilled, onRejected) {
    // 防止then的参数缺失
    const realOnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
    const realOnRejected = typeof onRejected === 'function' ? onRejected : reason => {
      throw reason
    }
    // 为了实现MyPromise链式调用,此处返回一个新的promise
    const promise2 = new MyPromise((resolve, reject) => {
      const fulfilledMicrotask = () => {
        queueMicrotask(() => {
          try {
            const x = realOnFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        })
      }
      const rejectedMicrotask = () => {
        queueMicrotask(() => {
          try {
            const x = realOnRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        })
      }
      // 判断当前promise状态
      if (this.status === FULFILLED) {
        // resolve已经执行,直接将then的第一个参数封装成异步回调执行即可
        fulfilledMicrotask()
      } else if (this.status === REJECTED) {
        // reject已经执行,直接将then的第而个参数封装成异步回调执行即可
        rejectedMicrotask()
      } else {
        // new Promise(executor)的executor是异步执行的,先将then回调存储,等状态改变再执行
        this.onFulfilledCallbacks.push(fulfilledMicrotask)
        this.onRejectedCallbacks.push(rejectedMicrotask)
      }
    })

    return promise2
  }

  static resolve(parameter) {
    if (parameter instanceof MyPromise) {
      return parameter
    }
    return new MyPromise(resolve => resolve(parameter))
  }

  static reject(reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason)
    })
  }
  static all(promiseArr){
    let index = 0, result = []
    return new MyPromise((resolve, reject) => {
      promiseArr.forEach((p, i) => {
        MyPromise.resolve(p).then(val => {
          index++ //计数,看完成了几个
          result[i] = val //对应数组下标位置保存结果
          if (index === promiseArr.length) {
            resolve(result)
          }
        }, err => {
          reject(err) //报错立刻返回
        })
      })
    })
  }
  static race(promiseArr){
    return new MyPromise((resolve, reject) => {
      promiseArr.forEach(p => {
        MyPromise.resolve(p).then(val => {
          resolve(val)
        }, err => {
          reject(err)
        })
      })
    })
  }
  static any(promiseArr){
    let index = 0
    return new MyPromise((resolve, reject) => {
      if (promiseArr.length === 0) return
      promiseArr.forEach((p, i) => {
        MyPromise.resolve(p).then(val => {
          resolve(val)
        }, err => {
          index++
          if (index === promiseArr.length) {
            reject(new AggregateError('All promises were rejected'))
          }
        })
      })
    })
  }
}

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  if(x instanceof MyPromise) {
    x.then(resolve, reject)
  } else{
    resolve(x)
  }
}

img lazy load

let imgList = [...document.querySelectorAll('img')]
let length = imgList.length

const imgLazyLoad = function() {
    let count = 0
    return function() {
        let deleteIndexList = []
        imgList.forEach((img, index) => {
            let rect = img.getBoundingClientRect()
            if (rect.top < window.innerHeight) {
                img.src = img.dataset.src
                deleteIndexList.push(index)
                count++
                if (count === length) {
                    document.removeEventListener('scroll', imgLazyLoad)
                }
            }
        })
        imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
    }
}

// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad)

十进制转2进制

const BaseConversion = (number) => {
   const arr = [];
    let baseString = ``;
   
   while(number > 0) {
      arr.push(Math.floor(number % 2));
      number = Math.floor(number / 2);
   }
   
   while(arr.length != 0) baseString += arr.pop().toString();

   return baseString;
}

十进制转任意进制

const BaseConversion = (number,base) => {
   const arr = [];
    const string = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    let baseString = ``;
   
   if ( base < 2 || base > 36 ) return baseString;
   
   while(number > 0) {
      arr.push(Math.floor(number % base));
      number = Math.floor(number / base);
   }
   
   while(arr.length != 0) baseString += string[arr.pop()];

   return baseString;
}

hash算法

function hashFunc(str, size){
      //定义一个变量来存储hashCode
      var hashCode = 0;
      // 利用霍纳法则计算出hashCode的值
      // give -> Unicode编码
      for (var i = 0; i < str.length; i++) {
        hashCode = 37 * hashCode + str.charCodeAt(i); 
      }
      // 利用hashCode与哈希表的长度取余得到下标值
      var index = hashCode % size;
      return index;
}

叨叨一下

最后再叨叨两句,算法在字节前端面试中还是比较重要的,一面2题二面2题三面1题,但是都是简单和中等题。遇到hard的可能性很小很小。算法在阿里只有一面有,而且不会很难。如果其他知识都准备很充分可以冲一冲算法,如果其他知识还没准备充分建议刷完高频100题就OK了(困难题跳过),如果时间很不充分可以直接看题解把解题思路记熟,面试的时候不卡思路就行。

啦啦啦,祝在座的各位前端er都能拿到满意的offer啦~ 别忘了点赞点关注撒

字节阿里前端面经2022年2月