Code Monkey home page Code Monkey logo

fe-learning's Introduction

fe-learning's People

Contributors

2016messi avatar metroluffy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

legend80s

fe-learning's Issues

一般DP问题的快速解

一般DP问题的快速解

思路

一个是找出问题的一个一个子“ 状态”,再一个就是建立“ 状态转移方程”(就是所谓的“ 递推关系式”),可以手写枚举几个结果出来找关系推出式子,然后先递归写法,再向迭代写法的方向优化空间复杂度。

“ 循环还是递归”,这只是实现的办法而已,不是动态规划的本质;再比如空间换时间,把子问题的解答结果(就是上面说的子“ 状态”)存放起来,减少重复计算,这也是优化的办法,也并非动态规划本质。

实现

// 伪代码
function helper(data,map, ...params) {
    // 递归终止条件
    // for example
     if(param === xxx) return xx
    // 缓存key
    // for example,如二维问题
     const key = `${i}-${j}`
    // 获取缓存
    if(map.has(key)) {
        return map.get(key)
    }
    // 根据状态转移方程计算
    const result = helper(data, map,...params)
    // 缓存计算结果
    map.set(key, result)
    return result
}
function minPathSum(grid) {
    const map = new Map()
    return helper(grid, map, ...initParam)
};

斐波那契数列

让我们从最简单的斐波那契数列开始,一般问题会让求取数列的某一项,并有

F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.

其中关于 F(N)的表达式就是此问题的递推关系式,当然从斐波那契数列(0,1,1,2,3,5,8....)也可以推出, 符合上述思路,子问题就是单个数由其前一项和前两项相加求和而来。那么可以有:

// f(i) = f(i - 1) + f(i - 2)
function helper(i,map) {
    // 递归终止条件
    // for example
    if(i < 2) return i
    // 获取缓存, 一维问题的key直接用参数即可
    if(map.has(i)) {
        return map.get(i)
    }
    // 根据状态转移方程计算
    const result = helper(i - 1, map) + helper(i - 2, map)
    // 缓存计算结果
    map.set(i, result)
    return result
}
function fib(N) {
    const map = new Map()
    return helper(N, map)
};

/** test
 * fib(0) = 0
 * ...
 * fib(50) = 6765 // passed
 */

一般教程都会拿上述例子作为函数记忆化的例子。上述解法思路也差不多就是递推关系式+缓存。

以上解法的时间复杂度:O(N),空间复杂度:O(N),内存中使用的堆栈大小。空间复杂度还可以优化,因为我们只需要前两个状态。

先把实现方法改写为迭代写法:

function fib(N) {
    if(N < 2) return N
    const dp = [1, 1]
    for(let i = 2; i <= N; i++) {
        dp[i] = dp[i - 1] + dp[i - 2]
    }
    return dp[N-1]
}

这里使用数组来缓存结果,保存了从1~N的结果,优化之:

function fib(N) {
    if (N <= 1) return N
    let current = 0, prev1 = 1, prev2 = 1;

    for (let i = 2; i <= N; i++) {
        current = prev1 + prev2;
        prev2 = prev1;
        prev1 = current;
    }
    return current;
}

如此,我们就做到了O(1)的空间复杂度。

零钱兑换

来看一个稍难的问题,零钱兑换, 典型的背包问题。

我们先拿出分别一枚硬币,从总额减去币值的消耗,作为一种方案(+1),然后重复该过程,拿每种硬币去凑剩余的余额,从方案中选出最小数量即可。

根据思路可以得出:

f(0) = 0, //金额为0不能由硬币组成
f(n > 0) = min{f(n−coin)+1∣coin∈coins}

具体思路参见:零钱兑换-官方题解

// 递归方程式
// f(i) = min(f(i - cost in costs)) + 1
function helper(amount, coins, map) {
    // 金额消耗完毕,递归终止
    if(amount <= 0) return 0
    // 获取缓存
    if(map.has(amount)) {
        return map.get(amount)
    }
    // 根据状态转移方程计算
    const re = []
    for (let i of coins ) {
        const t = amount - i
        if( t >= 0) re.push(helper(t, coins, map))
    }
    const result = Math.min(...re) + 1
    // 缓存计算结果
    map.set(amount, result)
    return result
}
function coinChange(coins, amount) {
    const map = new Map()
    const result = helper(amount, coins, map)
    return result === Infinity ? -1 : result
};

同样可以改写成迭代写法:

function coinChange(coins, amount) {
  const dp = new Array(amount+1).fill(Infinity)
  dp[0] = 0
  for (let coin of coins ) {
    for (let i = 1; i <= amount; i++) {
      if (i - coin >= 0) {
        dp[i] = Math.min(dp[i], dp[i - coin] + 1)
      }
    }
  }
  return dp[amount] === Infinity ? -1 : dp[amount]
}

总结

一般错误出现在递归边界设置错误或者递推关系式上,如果遇到爆栈等问题,可以从这两方面着手。

递归写法最接近关系式,更容易写出求解,故作为快速解的一个思路。但是学习不应以结果为导向,要更关注过程,例如递归写法的时间复杂度也可以根据问题类型进行优化,或用迭代写法来优化空间复杂度等等。

题目

动态规划题目参见: 动态规划-Leetcode

TODO

  • 补充其他动规类型,如二维问题

剑指offer —— 二进制中1的个数

点击查看原文
点击查看原题

题目

请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。

解题思路

暴力法

直接将数字转成二进制格式,然后计算 $1$ 的个数。
PS:StringtoString 方法可以转换进制,利用这点我们少写点代码……

代码

/**
 * @param {number} n - a positive integer
 * @return {number}
 */
var hammingWeight = function(n) {
    const str = n.toString(2)
    const N = str.length
    let res = 0
    for (let i = 0; i < N; i++) {
        if (str[i] == 1) {
            res++
        }
    }
    return res
}

位运算

以输入的数字是 $9$ 为例子(二进制为 1001),每次与 $1$ 进行按位与运算,如果结果为 $1$ 说明最低位为 $1$,否则为 $0$;每次做完按位与运算就将数字右移,直到数字为0为止。

代码

/**
 * @param {number} n - a positive integer
 * @return {number}
 */
var hammingWeight = function(n) {
    let res = 0
    while (n) {
        if (n & 1) {
            res++
        }
        n >>= 1
    }
    return res
};

CSS实现一个响应式布局

CSS实现一个响应式布局

题目

请用CSS实现一个1行4列占满宽度的布局,每个子项要求有 1px 的黑色边框。当设备宽度小于 720px 时,布局变换为2行2列,当设备宽度小于 360px 时,布局变换为4行1列。

<div class="container">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

实现

  1. 使用 flex 布局,搭配 MediaQuery 实现响应式
html,
body {
  margin: 0;
  padding: 0;
}

.container {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  /* 以上两项可以简写为 flex-flow属性 */
}

.item {
  height: 100px;
  box-sizing: border-box;
  border: 1px solid black;
  flex: 0 0 25%
}
@media screen and (max-width: 720px) {
  .item {
    flex: 0 0 50%
  }
}

@media screen and (max-width: 360px) {
 .item {
    flex: 0 0 100%
  }
}
  1. 也可以使用grid布局
.container {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}
@media screen and (max-width: 720px) {
  .container {
    grid-template-columns: 1fr 1fr;
  }
}

@media screen and (max-width: 360px) {
  .container {
    grid-template-columns: 1fr;
  }
}

可以通过以下链接调试:

https://codepen.io/liusw/pen/WNQaarP

CSS绘制常见的图形

利用border相关属性。

三角形

.triangle {
    width: 0;
    height: 0;
    border-right: 50px solid transparent;
    border-top: 50px solid blue;
    border-left: 50px solid transparent;
}

梯形

.trapezoid {
  width:100px;
  height:0;
  border-right: 50px solid transparent;
  border-bottom: 50px solid blue;
  border-left: 50px solid transparent;
}

扇形

其实就是三角形加了一个border-radius:50%

.sector {
    width: 0;
    height: 0;
    border-right: 50px solid transparent;
    border-top: 50px solid blue;
    border-left: 50px solid transparent;
    border-radius: 50%;
}

箭头

如代码,就是两个三角形叠起来,再用定位做个offset(控制线的粗细),方向还是border相关属性控制的。

.arrow{
    position:relative;
}
/*黑色三角形   */
.arrow:before{
    content: "";
    display: block;
    position: absolute;
    top: 50%;
    right: 0;
    width: 0;
    height: 0;
    border:10px solid;
    border-color: transparent transparent transparent #000;
}
/*背景色三角形*/
.arrow:after{
    content: "";
    display: block;
    position: absolute;
    top: 50%;
    right: 1px;
    width: 0;
    height: 0;
    border:10px solid;
    border-color: transparent transparent transparent #fff;
}

椭圆

.oval {
    width: 200px;
    height: 100px;
    background: red;
    -moz-border-radius: 100px / 50px;
    -webkit-border-radius: 100px / 50px;
    border-radius: 100px / 50px;
}

JS实现函数无限操作

JS实现函数无限操作

题目

用Javascript实现一个无限极函数,形如:

operator (1)(2)(3) => f(x)...;
operator (1)(2)(3)() => 6;

注意:执行operator的时候如果最后不是以()结尾(如operator (1)(2)),则这个结果会一直缓存到闭包里。如果下次直接operator (3)(4)的话结果是10.因为他会累加之前的结果。如果你不想这样,那可以通过加()消费缓存的结果。

实现

闭包的一个应用。

function add(x, y) {
    if (isNaN(+x)) {
        x = 0;
    }
    if (isNaN(+y)) {
        y = 0;
    }
    return x + y;
}
var operator = (function(op) {
    let result = null;
    return function (x) {
        if (x) {
            result = op(x, result);
            return arguments.callee; // 在严格模式下无效, 你可以给定函数一个名字
        } else {
            let ret = result;
            result = null;
            return ret;
        }
    }
})(add);

实现Promise.all及finally

实现Promise.all

也算是常备知识点,理解这几个常用API的概念。

Promise.all

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise 的结果。

Promise._all = (iterable) => {
  return new Promise(function(resolve, reject) {
    if (!Array.isArray(iterable)) {
      return reject(new TypeError('arguments must be an array'));
    }
    var resolvedCounter = 0;
    var remaining = iterable.length;
    var resolvedValues = new Array(remaining);
    for (var i = 0; i < remaining; i++) {
      (function(i) {
        Promise.resolve(iterable[i]).then(function(value) {
          resolvedCounter++
          resolvedValues[i] = value
          if (resolvedCounter == remaining) {
            return resolve(resolvedValues)
          }
        }, function(reason) {
          return reject(reason)
        })
      })(i)
    }
  })
}

多提一嘴 Promise.allSettled,它会返回一个在所有给定的 promise 已被决议或被拒绝后决议的
promise ,并带有一个对象数组,每个对象表示对应的 promise 结果。通俗点,相较于 all, 他会等所有的任务跑完才返回,即便某个任务失败。

Promise.race

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。

Promise._race = (iterable)=>{
  return new Promise((resolve, reject) => {
    for (const p of iterable) {
      Promise.resolve(p).then(resolve).catch(reject)
    }
  })
}

Promise.finally

finally() 方法返回一个 Promise。在 promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。

这避免了同样的语句需要在 then()catch() 中各写一次的情况。

Promise.prototype._finally = function (callback) {
  var constructor = this.constructor;
  return this.then(
    function(value) {
      // @ts-ignore
      return constructor.resolve(callback()).then(function() {
        return value;
      });
    },
    function(reason) {
      // @ts-ignore
      return constructor.resolve(callback()).then(function() {
        // @ts-ignore
        return constructor.reject(reason);
      });
    }
  );
};

用箭头函数美化一下:

Promise.prototype._finally = function (callback) {
  let constructor = this.constructor;
  return this.then(
    value  => constructor.resolve(callback()).then(() => value),
    reason => constructor.resolve(callback()).then(() => constructor.reject(reason))
  );
};

参考文档

  1. MDN-Promise
  2. package/promise-polyfill

将list数据转换成树形结构

将list数据转换成树形结构

题目

以下数据结构中,id 代表部门编号,name 是部门名称,parentId 是父部门编号,为 0 代表一级部门,现在要求实现一个 convert 方法,把原始 list 转换成树形结构,要求尽可能降低时间复杂度。parentId 为多少就挂载在该 id 的属性 children 数组下,结构如下:

// 原始 list 如下
let list =[
    {id:1,name:'部门A',parentId:0},
    {id:2,name:'部门B',parentId:0},
    {id:3,name:'部门C',parentId:1},
    {id:4,name:'部门D',parentId:1},
    {id:5,name:'部门E',parentId:2},
    {id:6,name:'部门F',parentId:3},
    {id:7,name:'部门G',parentId:2},
    {id:8,name:'部门H',parentId:4}
];
const result = convert(list, ...);

// 转换后的结果如下
let result = [
    {
      id: 1,
      name: '部门A',
      parentId: 0,
      children: [
        {
          id: 3,
          name: '部门C',
          parentId: 1,
          children: [
            {
              id: 6,
              name: '部门F',
              parentId: 3
            }, {
              id: 16,
              name: '部门L',
              parentId: 3
            }
          ]
        },
        {
          id: 4,
          name: '部门D',
          parentId: 1,
          children: [
            {
              id: 8,
              name: '部门H',
              parentId: 4
            }
          ]
        }
      ]
    },
  ···
];

思路

使用Map保存id和对象的映射,循环list,根据parentId在Map里取得父节点,如果父节点有children属性,就直接push当前的子节点,如果没有就添加children属性。

function convert(list) {
	const res = []
	const map = new Map()
        list.forEach(el => {
            map.set(el.id, el);
        });
	for (const item of list) {
		if (item.parentId === 0) {
			res.push(item)
			continue
		}
                // 获取引用,设置子节点
		if (map.has(item.parentId)) {
			const parent = map.get(item.parentId)
			parent.children = parent.children || []
			parent.children.push(item)
		}
	}
	return res
}
// 关键:Map保存引用关系
// test
// map.get(1) === res[0] // true

树形数据结构扁平化

编写两个函数,实现如下两个数据结构互相转换

const data = {
  a: {
    b: {
      c: {
        dd: 'abcdd'
      }
    },
    d: {
      xx: 'adxx'
    },
    e: 'ae'
  }
}
const output = {
  'a.b.c.dd': 'abcdd',
  'a.d.xx': 'adxx',
  'a.e': 'ae'
}

实现Lodash函数系列(一):_.get

实现Lodash函数系列(一):_.get

题目

/**
 * object (Object): 要检索的对象。
 * path (string): 要获取属性的路径。
 * [defaultValue] (*): 如果解析值是 undefined ,这值会被返回。
 */
function _get(object, path, default) {

}

实现类似 lodashget 函数,根据 object对象的path路径获取值。 如果解析 value 是 undefined 会以 defaultValue 取代。

示例:

const object = { 'a': [{ 'b': { 'c': 3 } }] };

_get(object, 'a[0].b.c');
// => 3

_get(object, 'a.b.c', 'default');
// => 'default'

实现

function _get (source, path, defaultValue = undefined) {
  // 将数组格式的路径转化成dot格式的,再拆分成key数组
  // a[0].b -> a.0.b -> ['a', '0', 'b']
  const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.')
  let result = source
  for (const p of paths) {
    result = Object(result)[p] // null 与 undefined 取属性会报错, 用Object包装一下
    // if (result === undefined) {
    //    return defaultValue
    // }
  }
  return result || defaultValue
}

参考文档

  1. 如何实现类似 lodash 的 get

微信面试题 LazyMan

微信面试题 LazyMan

要求实现一个函数,需要满足以下功能

LazyMan('Tony');
// Hi I am Tony
LazyMan('Tony').sleep(10).eat('lunch');
// Hi I am Tony
// 等待了10秒...
// I am eating lunch

LazyMan('Tony').eat('lunch').sleep(10).eat('dinner');
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner

LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
// 实现
class lazyMan {
    constructor (name) {
        this.name = name
        this.sleepTime = 0
        this.sleepFirstTime = 0
        this.taskList = []
        console.log(`Hi I am ${this.name}`);
        setTimeout(() => {
            this.next()
        }, 0)
    }
    next() {
        var fn = this.taskList.shift();
        fn && fn();
    }
    eat (f) {
        var that = this;
        var fn = (function (n) {
            return function () {
                console.log(`I am eating ${n}`)
                that.next();
            }
        })(name);
        this.taskList.push(fn);
        return this;
    }
    sleep (time) {
        var that = this;
        var fn = (function (t) {
            return function () {
                setTimeout(() => {
                    console.log(`等待了${t}秒...`)
                    that.next();
                }, t * 1000);  
            }
        })(time);
        this.taskList.push(fn);
        return this;
    }
    sleepFirst(time) {
        var that = this;
        var fn = (function (t) {
            return function () {
                setTimeout(() => {
                    console.log(`等待了${t}秒...`)
                    that.next();
                }, t * 1000);  
            }
        })(time);
        this.taskList.unshift(fn);
        return this;
    }
}
function LazyMan(name) {
    return new LazyManClass(name);
}

实现字符串反转

实现字符串反转

题目

实现一个字符串反转:输入:www.toutiao.com.cn 输出:cn.com.toutiao.www

要求:1.不使用字符串处理函数 2.空间复杂度尽可能小

实现

要实现字符串反转,应用双指针前后交换位置即可,这里需要注意字符串是基本类型,需要先转换成字符数组,反转后再拼接回去即可。

// 方法一, 不合题意,却可以大致整理出思路

function _swap(s) {
    return s.split('.').reverse().join('.')
}

// swap('www.toutiao.com.cn')
// "cn.com.toutiao.www"

// 分别实现上述函数

function split(s, op='') {
    let sa = [], ss = ''
    for(let si = 0; si < s.length; si++){
        if(s[si] === op){
            sa.push(ss);ss = ''
        } else {
            ss+= s[si]
        }
    }
    if(ss) sa.push(ss)
    return sa
}

function join(sa, op=',') {
    let s = '', l = 0
    while(l < sa.length) {
        s += sa[l] + (l == sa.length - 1 ? '' : op)
        l++
    }
    return s
}

function swap(s){
    if(!s) return ''
    s = split(s,'.')
    let i = 0, j = s.length - 1;
    while(i < j) {
        let t = s[i]
        s[i] = s[j]
        s[j] = t
        i++;
        j--;
    }
    return join(s, '.')
}

// swap('www.toutiao.com.cn')
// "cn.com.toutiao.www"

你也可以考虑在单次遍历中实现,空间复杂度应该差不多。

判断二叉树是否相同/对称/子树

判断二叉树是否相同/对称

题目

给定两个二叉树,编写一个函数来检验它们是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

100. 相同的树

思路

处理二叉树的问题,递归比较直观,逐级判断节点是否一致即可。

实现

const isSameTree = (p, q) => {
    if(p == null && q == null) 
        return true;
    if(p == null || q == null) 
        return false;
    if(p.val != q.val) 
        return false;
    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
};

也可以借助队列通过迭代来实现,每棵树分别一个队列,相同位置的节点逐级入队,再出队判断即可。

const isSameTree = (p, q) => {
    const queue1 = [];
    const queue2= [];
    queue1.push(q);
    queue2.push(p);
    while (queue1.length > 0){
        const tempQ = queue1.shift();
        const tempP = queue2.shift();
      	// 判断逻辑是一样的 
        if (tempQ === null && tempP === null) continue;
        if (tempQ === null || tempP === null) return false;
        if(tempP.val !== tempQ.val) return false;
        queue1.push(tempQ.left);
        queue1.push(tempQ.right);
        queue2.push(tempP.left);
        queue2.push(tempP.right);
    }
    return true;
}

类似的应用还有判断一棵树B是否为另一棵树A的子树,如果根节点相同则从根节点判断,如果不相同则递归判断A的左子树、右子树是否包含B。

面试题26. 树的子结构

var isSubStructure = function(A, B) {
    let result = false;
    // 边界条件,因题不同
    if(A != null && B != null){
        if(A.val  == B.val)
            result = isSameTree(A,B);
        if(!result)
            result = isSubStructure(A.left, B);
        if(!result)
            result = isSubStructure(A.right, B);
    }
    return result
};

类似的题

想到一道类似的题,检查一棵二叉树是否对称,101. 对称二叉树

const isSymmetric = root => {
    const q = [];
    q.push(root);
    q.push(root);
    while (q.length > 0) {
        const t1 = q.shift();
        const t2 = q.shift();
        if (t1 == null && t2 == null) continue;
        if (t1 == null || t2 == null) return false;
        if (t1.val != t2.val) return false;
        q.push(t1.left);
        q.push(t2.right);
        q.push(t1.right);
        q.push(t2.left);
    }
    return true;
};

这里每次都检验对称位置的节点是否相同,然后再把其子节点按对称的顺序入队,思路上也是模拟人手工遍历。

剑指offer —— 重建二叉树

点击查看原文
点击查看原题

题目

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如,给出

前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]

返回如下的二叉树:

    3
   / \
  9  20
    /  \
   15   7

解题思路

  • 前序遍历:每次先遍历二叉树的根节点,然后再分别遍历二叉树的左右子树
  • 中序遍历:每次先遍历二叉树的左子树,然后再遍历根节点,最后遍历右子树

结合以上特点,preorder 数组中的第一个值 val 为二叉树的根节点。与之对应的是,我们需要找到 valinorder 数组中的位置 inorderIndex,其中 $[0, inorderIndex - 1]$ 之间的数字分布在左子树,$[inorderIndex + 1, N]$ 之间的数字在右子树;当然,我们还需要找到 preorder 数组中的所有关于左子树的值的分布和所有关于右子树的值的分布,其中 $[1, left + 1]$ 之间的数字分布在左子树,$[right, N]$ 之间的数字分布在右子树。
对于以上推论,我们只要递归的进行下去,这颗树就能被完整的构建出来了。

代码

/**
 * @param {number[]} preorder
 * @param {number[]} inorder
 * @return {TreeNode}
 */
var buildTree = function(preorder, inorder) {
    if (!preorder.length || !inorder.length) {
        return null
    }
    // 根节点的值为 preoder[0]
    const root = new TreeNode(preorder[0])
    // 找到根节点的值在 inorder 中的位置
    // 我们借此来划分 inorder 中分布在左右子树的值
    const inorderIndex = inorder.indexOf(preorder[0])
    // 找到 inorder 中左右子树的边界的值
    // 当然在 preorder 中它们对应左右子树的边界
    // 我们借此来划分 preoder 中分布在左右子树的值
    const left = preorder.indexOf(inorder[inorderIndex - 1])
    const right = preorder.indexOf(inorder[inorderIndex - 1]) + 1
    // 递归构建左右子树
    root.left = buildTree(preorder.slice(1, left + 1), inorder.slice(0, inorderIndex))
    root.right = buildTree(preorder.slice(right), inorder.slice(inorderIndex + 1))
    // 返回结果
    return root
};

汇总区间

汇总区间

题目

给定一个无重复元素的有序整数数组,返回数组区间范围的汇总。

示例 1:

输入: [0,1,2,4,5,7]
输出: ["0->2","4->5","7"]
解释: 0,1,2 可组成一个连续的区间; 4,5 可组成一个连续的区间。

汇总区间

题解

双指针,假设为 iji 位于开始,j+1 后移,否则生成序列。

function summaryRanges (nums) {
    const ans = []
    for (let i = 0, j = 0; j < nums.length; ++j) {
        // 判断下一位是否为当前值+1,注意边界
        if (j + 1 < nums.length && nums[j + 1] == nums[j] + 1)
            continue;
        // 否则生成序列
        if (i == j)
            ans.push(nums[i] + "");
        else
            ans.push(nums[i] + "->" + nums[j]);
        i = j + 1;
    }
    return ans
};

二叉树的路径

二叉树的路径

整理

一般地,处理二叉树路径相关的问题,使用递归更直观,诸如输出二叉树所有的路径、路径和或者左/右叶子节点之和等等,以下举几个例子。

题一

给定一个二叉树,返回所有从根节点到叶子节点的路径。

257. 二叉树的所有路径

最直观的方法是使用递归。在递归遍历二叉树时,需要考虑当前的节点和它的孩子节点。如果当前的节点不是叶子节点,则在当前的路径末尾添加该节点,并递归遍历该节点的每一个孩子节点。如果当前的节点是叶子节点,则在当前的路径末尾添加该节点后,就得到了一条从根节点到叶子节点的路径,可以把该路径加入到答案中。

作者:LeetCode

// 二叉树节点等实现略去
function constructPath(node, path, collection) {
    if(node !== null) {
        path += node.val
      	// 叶子结点,将路径加入答案集合
        if(node.left === null && node.right === null) {
            collection.push(path)
        } else {
            path += '->'
            // 处理左右节点
            constructPath(node.left, path, collection)
            constructPath(node.right, path, collection)
        }
    }
}
const binaryTreePaths = function(root) {
    if(root === null) return []
    const collection = []
    constructPath(root, '', collection)
    return collection
};

题二

很容易关联到另一题, 求根到叶子节点数字之和。在此题中,二叉树的每条路径都作为一个数字,求和,所以处理方法也可以类似,构建路径数字,最后遍历求和。

function constructPath(node, path, collection) {
    if(node !== null) {
        path += node.val
        if(node.left === null && node.right === null) {
            collection.push(path)
        } else {
            constructPath(node.left, path, collection)
            constructPath(node.right, path, collection)
        }
    }
}
var sumNumbers = function(root) {
    if(root === null) return 0
    const collection = []
    constructPath(root, '', collection)
    return collection.reduce((pre, curr) => pre+= Number(curr), 0)
};

实现要先把数字转字符串最后遍历转回来,效率略低,可以放到每次遍历中去累加:

const helper = (root, cur, ans) => {
    if(root !== null) {
        cur = cur*10 + root.val
        if (root.left === null && root.right === null) {
          ans.num += cur
        } else {
          helper(root.left, cur, ans)
          helper(root.right, cur, ans)
	}
    }
}
const sumNumbers = function(root) {
    if(!root) return 0
    let ans = {
        num: 0
    }
    helper(root, 0, ans)
    return ans.num
};

题三

404. 左叶子之和

function mapLeftLeaves(root,isLeft) {
    if (root == null) return 0;
    if (isLeft && root.left == null && root.right == null) return root.val;
    return mapLeftLeaves(root.left,true) + mapLeftLeaves(root.right, false);
}
const sumOfLeftLeaves = function(root) {
    if(root === null) return 0
    return mapLeftLeaves(root, false)
};

或者使用迭代,效率更高

const sumOfLeftLeaves = function(root) {
    if(!root) return 0
    const stack = [root]
    let ans = 0
    // 借助栈实现先序遍历
    while(stack.length > 0) {
        let cur = stack.pop()
        if(cur.right) stack.push(cur.right)
        if(cur.left) stack.push(cur.left)
      	// 处理左叶子结点
        if(cur.left && !cur.left.left && !cur.left.right) ans += cur.left.val
    }
    return ans
};

以上。

实现一个深克隆函数

实现一个深克隆函数

思路

要求实现一个深克隆的函数。

对于引用类型,例如常见的Object,都是通过引用指向同一块堆内存,因此无法直接赋值的方式拷贝一个对象。有一些方法可以实现浅克隆,例如通过遍历的方式复制每一个属性,或者使用 Object.assign / Object.create,对于数组对象,可以使用 Array.prototype.slice 方法,这里不深究,主要来看下深克隆。

一般地,我们可以使用JSON序列化的方式完成拷贝,缺点是:

  • 无法实现对函数 、RegExp等特殊对象的克隆
  • 对象有循环引用,会报错
  • 会抛弃对象的constructor,所有的构造函数会指向Object
  • ...

那么常见的方式就是递归遍历复制属性,需要处理的就是上述几种情况。

实现

function deepClone(obj) {
  // 应对循环引用设置的缓存
  const objs = new WeakMap()

  function helper(obj) {
    if (obj === null) return null
    if (typeof obj !== 'object') return obj // 值类型直接返回即可

    let child
    // 获取被克隆对象的类型
    const type = Object.prototype.toString.call(obj)
    // 处理特殊对象
    switch(type) {
      case '[object Array]':
        // 处理数组对象
        child = []
        break
      case '[object RegExp]':
        // 对正则对象做特殊处理
        child = new RegExp(obj.source, obj.flags)
        if(obj.lastIndex) child.lastIndex = obj.lastIndex
        break
      case '[object Date]':
        // 对Date对象做特殊处理
        child = new Date(parent.getTime())
        break
      default:
        // 这里没有处理Map、Set, 下述方式无法处理这两者
        // 获取对象原型
        proto = Object.getPrototypeOf(parent)
        child = Object.create(proto)
        break
    }
    // 处理循环引用
    // 这里也可以用数组来缓存
    if(objs.has(obj)) {
      return objs.get(obj)
    }
    objs.set(obj, obj)

    // 递归遍历属性
    for (let i in obj) {
      child[i] = helper(obj[i])
    }
    return child
  }

  return helper(obj)
}

记一次字节前端笔试

字节跳动前端面经

其他与个人项目相关的就不说了,直接来看笔试题。

题目

看代码写输出

var result = [];
var a = 3;
var total = 0;
function foo(a) {
  var i = 0;
  // 注意这里的i后置加
  for (; i < 3; i++) {
    result[i] = function() {
      total += i * a;
      console.log(total);
    }
  }
}

foo(1);
result[0](); // 3
result[1](); // 6
result[2](); // 9
  • 如果将 var i = 0 改为 let, 输出是否会变化

    不会,这里 i 是定义在foo函数作用域内的。

注解:

用闭包来解释这题会更好,因为i是定义在foo函数作用域里的,result数组里的函数运行时相当于从foo函数作用域外引用i、a变量,这个时候foo循环已经退出了,i因为后置加的存在在退出循环以后等于3,a因为参数传入为1,所以输出就是3、6、9。如果这里参数不指定a的话,就会直接沿着作用域链往上查找到定义在全局作用域的a = 3,最后结果为9、18、27。如果foo不传入参数,a还是在foo函数作用域,相当于写了一条 var a; // undefined,最后输出三个NaN。

二进制加法

实现一个二进制加法,输入输出均为二进制字符串

function binaryAdd(num1: string, num2: string): string {
  // TODO
}
//Example
binaryAdd('1010', '111') // '10001'

leetcode原题,二进制求和

和链表求和、字符串加法差不多,进位进入第二次运算即可,计算完成如进位有余记得补位

function addBinary(a,b) {
    let ans = '', carry = 0
    let pa = a.length-1;
    let pb = b.length-1;
    while (pa >=0 || pb >= 0) {
        const sum = Number(a[pa] || 0) + Number(b[pb] || 0) + carry
        carry = Math.floor(sum / 2);
        ans = sum % 2 + ans
        pa--;
        pb--;
    }
    if(carry !== 0) ans = '1' + ans
    return  ans
}

实现一个带并发限制的异步队列

实现一个带并发限制的异步调度器Scheduler,保证同时运行的任务最多有两个。完善代码中Scheduler类,使得以下程序能正确输出

class Scheduler {
  add(promiseCreator) {
    // TODO
  }
  // TODO
}
const timeout = (time) => new Promise(resolve => {
  setTimeout(resolve, time)
})
const scheduler = new Scheduler();
const addTask = (time, order) => {
  scheduler.add(() => timeout(time))
    .then(() => console.log(order))
}

addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')
// output: 2 3 1 4
// 一开始,1、2两个任务进入队列
// 500ms时,2完成,输出2,任务3进队
// 800ms时,3完成,输出3,任务4进队
// 1000ms时,1完成,输出1
// 1200ms时,4完成,输出4
// 实现如下
class Scheduler {
  constructor () {
    this.tasks = [] // 任务缓冲队列
    this.runningTask = [] // 任务队列
  }

  // promiseCreator 是一个异步函数,return Promise
  add (promiseCreator) {
    return new Promise((resolve, reject) => {
      promiseCreator.resolve = resolve
      if (this.runningTask.length < 2) {
        this.run(promiseCreator)
      } else {
        this.tasks.push(promiseCreator)
      }
    })
  }

  run (promiseCreator) {
    this.runningTask.push(promiseCreator)
    promiseCreator().then(() => {
      promiseCreator.resolve()
      // 删除运行完的任务
      this.runningTask.splice(this.runningTask.findIndex(promiseCreator), 1)
      if (this.tasks.length > 0) {
        this.run(this.tasks.shift())
      }
    })
  }
}

小结

字节还是很考基础,面了两次下来感觉最大的问题就是这块,平时实践较多但是深度不够,比如第三题那个带并发限制的异步队列,读完题后没有较好的思路。

如果单纯从准备面试的角度,除开基础知识预备,算法题这些,JS这块可以关注一下事件循环、作用域、原型链继承,再就是各种花式异步操作等等。

关于async和await的“同步”

关于async和await的“同步”

题目

要求看代码写输出。

function wait(){
  return new Promise(resolve =>
    setTimeout(resolve, 10*1000)
  )
}
async function func1() {
  console.time('time-func1')
  const x = await wait()
  const y = await wait()
  const z = await wait()
  console.timeEnd('time-func1')
}
async function func2() {
  console.time('time-func2')
  const x = wait()
  const y = wait()
  const z = wait()
  await x
  await y
  await z
  console.timeEnd('time-func2')
}
func1()
func2()

/** 输出如下
 * time-func2:10002.4ms
 * time-func1: 32261.5ms
 */

解析

async、await语法糖让我们可以把Promise的异步回调处理写出“同步”的方式,即代码看起来是同步并且整洁很多,但其目的是简化使用多个 promise 时的同步行为,并非是真同步。

await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。

如此相邻的两个 await wait() 会形成继发关系(串行)。要写成并发方式,可以如 func2函数所示用变量先缓存Promise,再一起执行,或者你也可以使用 Promise.all / Promise.allSettled,没有依赖关系的函数最好让它们同时触发。

注意:Promise.all 有短路效应,如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise 的结果。

async function func3() {
  console.time('time-func3')
  await Promise.all([wait(),wait(),wait()])
  console.timeEnd('time-func3')
}
func3()
// time-func3: 10455.867919921875ms

更多请参见: MDN-async function

flex相关属性的计算方法

flex属性的计算方法

这里主要是flex-grow、flex-shrink,分别对应在空间有剩余时的分配、空间不足时的收缩

flex-grow

剩余空间按flex-grow指定的值比例分配即可

举个例子,父容器的宽度为600,两个子项A(300, 1)、B(200, 2),求具体宽度:

剩余宽度为100

子项增长宽度A = 100 * 1/3 = 33.333, 则实际宽度 = 333.333
子项增长宽度B = 100 * 2/3 = 66.667, 则实际宽度 = 266.667

Demo

flex-shrink

子项收缩宽度 = 子项收缩比例 *溢出宽度
子项收缩比例 = (子项宽度* 收缩系数) / 所有子项的(宽度  *收缩系数)之和

* 收缩系数指flex-shrink的值

举个例子,父容器的宽度为600,两个子项A(500, 2)、B(400, 1),求具体宽度:

溢出宽度为300

子项收缩比例A = (500 *2) / (500 × 2 + 400 × 1) ≈ 0.71
子项收缩比例B = (400* 1) / (500 × 2 + 400 × 1) ≈ 0.29

子项收缩宽度A = 300 * 0.71 = 213, 则实际宽度 = 287
子项收缩宽度B = 300 * 0.29 = 87, 则实际宽度 = 313

* 实际宽度略有出入,与收缩比例取整有关

关键在于收缩比例的计算,和flex-grow不一样

Demo

字符串相加和相乘

字符串相加

题目

给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和。不能直接将输入的字符串转换为整数形式。

415. 字符串相加

实现

模拟笔算加法,逐位相加

/**
 * @param {string} num1
 * @param {string} num2
 * @return {string}
 */
function addStrings (num1, num2) {
    let i = num1.length - 1,j = num2.length - 1
    let carry = 0
    let ans = ''
    while(i >= 0 || j >= 0) {
        // 考虑进位的相加,长度不同的数字,取不到则标0
        const sum = (Number(num1[i]) || 0) + (Number(num2[j]) || 0) + carry
        // 进位处理
        carry = Math.floor(sum / 10)
        ans = sum % 10 + ans
        i--;
        j--
    }
    // 如果有未处理的进位
    if(carry > 0) ans = '1'+ ans
    return ans
};

类似的题

同样四则运算,来看下字符串相乘,字符串相乘

给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。

// 与字符串加法类似,模拟笔算乘法-竖版乘法
function multiply (num1, num2) {
    // 如有一方为 0 直接返回 0 作为结果
    if(num1 === '0' || num2 === '0') return '0'
    // 用于保存结果及进位
    let res = Array(num1.length + num2.length).fill(0)
    // 逐位相乘
    for (let i = num2.length - 1; i >= 0; i--) {
        let n1 = Number(num2[i]);
        for (let j = num1.length - 1; j >= 0; j--) {
            let n2 = Number(num1[j]);
            // 单数相乘+进位
            let sum = res[i + j + 1] + n1 * n2;
            res[i + j + 1] = sum % 10;
            // 保存进位
            res[i + j] += Math.floor(sum / 10);
        }
    }
    let ans = ''
    for (let i = 0; i < res.length; i++) {
        if (i == 0 && res[i] == 0) continue;
        ans+=res[i];
    }
    return ans
};

实现简单的并发请求控制

题目

请实现以下的函数,可以批量请求数据,所有的URL地址在urls参数中,同时可以通过 max 参数控制请求的并发度,当所有请求结束之后,需要执行 callback 回调函数。发请求的函数可以直接使用 fetch 即可

function sendRequest(urls: sring[],max:number,callback:()=>void){
    //TODO
}

实现

这里收藏网上一个比较好的实现。

function sendRequest(urls, max, callback) {
  const len = urls.length;
  let idx = 0;
  let counter = 0;

  function _request() {
    // 有请求,有通道
    while (idx < len && max > 0) {
      max--; // 占用通道
      fetch(urls[idx++]).finally(() => {
        max++; // 释放通道
        counter++;
        if (counter === len) {
          return callback();
        } else {
          _request();
        }
      });
    }
  }
  _request();
}

手写eventbus

eventbus实际上就是一个发布订阅模式、也用到了闭包,在回答类似问题的时候都可以提及

class Eventbus {
    constructor () {
        this.eventbus = {}
    }
    /**
     * 事件发布
     * @param {*} name 事件名字
     * @param {*} slef 自身作用域
     * @param {*} cb 回掉函数
     */
    $on(name,slef,cb) {
        let tuple = [slef,cb]
        if (Object.prototype.hasOwnProperty.call(this.eventbus, name)){
            this.eventbus[name].push(tuple)
        } else {
            this.eventbus[name] = [tuple]
        }
    }
    /**
     * 触发事件
     * @param {*} name 事件名字
     * @param {*} data 数据
     */
    $emit(name,data) {
        if (Object.prototype.hasOwnProperty.call(this.eventbus, name)) {
            let cbs = this.eventbus[name]
            // console.log(this.eventbus)
            cbs.map(item=>{
                let [slef, cb] = [item[0], item[1]]
                cb.call(slef, data)
            })
        }
    }

    /**
     * 取消事件
     * @param {*} name 事件名字
     * @param {*} fn 取消事件的回调
     */
    $off(name, fn) {
        if (Object.prototype.hasOwnProperty.call(this.eventbus, name)) {
            fn()
            delete this.eventbus[name]
        } 
    }

    /**
     * 当前事件被触发后只执行一次
     * @param {*} name 
     * @param {*} slef 
     * @param {*} fn 
     */
    $once(name, slef,fn) {
        let that = this
        function onceOn (data){
            fn.call(slef,data)
            console.log(that.eventbus[name])
            that.eventbus[name] = that.eventbus[name].filter(item=>{
                return item[0] !== slef  
            })
        }
        this.$on(name, slef, onceOn)
    }
}

将数字转换为人民币单位格式

请根据面向对象编程的**,设计一个类型 Cash用于表达人民币,使得:

class Cash {
}
const cash1 = new Cash(105)
const cash2 = new Cash(66)
const cash3 = cash1.add(cash2)
const cash4 = Cash.add(cash1, cash2)
const cash5 = new Cash(cash1 + cash2)

console.log(`${cash3}`, `${cash4}`, `${cash5}`)
// 1元7角1分 1元7角1分 1元7角1分

京东面经

CSS:

  • 垂直居中的方式
  • 移动端的适配方案,1px的线如何画
  • BFC, 原因,渲染机制
  • 清除浮动的方法
  • 输入URL,浏览器的渲染机制(很深,深入dom渲染(dom2,dom3),css渲染)
  • eventloop(宏任务和微任务)

js和框架

  1. 跨域和解决方案
  2. 网络安全(具体措施和解决方案,最好项目中用到)
  3. VUE如何把tempale模版编译成VDOM树的
  4. diff算法的过程和复杂度
  5. vue和react的区别.
  6. 项目最难的点,怎么解决的
  7. webpack用过多少,多深,做过哪些优化
  8. vue的dom如何变异的(指令,dom树)
  9. vue的响应式原理(watcher创建和分配)$nextTick如何实现和原理

后端和网络编程

  • http的三次握手的具体过程
  • 用到了https吧,谈谈对https了解
  • KOA用过,说一下中间件原理
  • 说一下浏览器缓存机器,304的状态码表示什么含义

将JS对象表示的DOM结构渲染为真实DOM树

将JS对象表示的DOM结构渲染为真实DOM树

题目

要求写一个函数将以下调用渲染成真实DOM

var el = require('./element')

var ul = el('ul', {id: 'list'}, [
  el('li', {class: 'item'}, ['Item 1']),
  el('li', {class: 'item'}, ['Item 2']),
  el('li', {class: 'item'}, ['Item 3'])
])

var ulRoot = ul.render()
document.body.appendChild(ulRoot)

实现

// element.js
function Element (tagName, props, children) {
  this.tagName = tagName
  this.props = props
  this.children = children
}
// 将函数调用转为JS对象表达的DOM结构
module.exports = function (tagName, props, children) {
  return new Element(tagName, props, children)
}

// render方法
Element.prototype.render = function () {
  // 根据tagName构建真实节点
  var el = document.createElement(this.tagName)
  var props = this.props
  // 设置节点的DOM属性
  for (var propName in props) {
    var propValue = props[propName]
    el.setAttribute(propName, propValue)
  }

  var children = this.children || []
  // 递归处理子节点
  children.forEach(function (child) {
    var childEl = (child instanceof Element)
      ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
      : document.createTextNode(child) // 如果字符串,只构建文本节点
    el.appendChild(childEl)
  })

  return el
}

参考

哨兵节点的应用-从移除链表元素说起

移除链表元素

题目

203. 移除链表元素

删除链表中等于给定值 val 的所有节点。

示例:

输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5

题解

问题看起来很简单,可以如此考虑:

  • 选择要删除节点的前一个结点 prev
  • prevnext 设置为要删除结点的 next 即可

实现如下:

var removeElements = function(head, val) {
    let curr = head;
    while (curr !== null) {
      let temp = curr
      curr = curr.next;
      if (curr && curr.val === val) temp.next = curr.next;
    }
    return head
};
/**
* input: [1,2,6,3,4,5,6], 6
* output: [1,2,3,4,5] // pass
*
* // 待删除元素在表头的情况
* input: [6,1,2,3,4,5,6], 6
* output: [6,1,2,3,4,5] // failed
*
* // 待删除元素重复的情况
* input: [1,2,3,6,6,4,5,6], 6
* output: [1,2,3,6,4,5] // failed
*/

可以看到当元素在链表中间仅出现一次时,以上代码运行良好,但是面对待删除元素在表头或者重复出现时,就搞不定了。因为我们要选中待删除节点的前一个节点来完成删除,从而忽略了当前节点。你也可以画图分析一下原因。

那么解决思路就是保存前一个节点的引用,同时判断当前节点是否待删除即可。如何做?增加一个哨兵节点。这里所谓的哨兵节点,其实就是给链表增加一个伪头,让一个指针指向它,然后随当前指针一起遍历。

LeetCode官方题解:哨兵节点广泛应用于树和链表中,如伪头、伪尾、标记等,它们是纯功能的,通常不保存任何数据,其主要目的是使链表标准化,如使链表永不为空、永不无头、简化插入和删除。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
    let sentinel = new ListNode(-1);
    sentinel.next = head;
    let prev = sentinel, curr = head;
    while (curr !== null) {
      if (curr.val === val) prev.next = curr.next;
      else prev = curr;
      curr = curr.next;
    }
    return sentinel.next
};

更详细的题解(带图):移除链表元素-LeetCode官方题解

更多一点

关于哨兵节点的应用,我们还可以来看一题,19. 删除链表的倒数第N个节点

有返回链表倒数第N个节点的经验(双指针-快慢指针),我们很快就可以写出如下代码:

var removeNthFromEnd = function(head, n) {
    let slow = fast = head
    // -1,指向待删除节点的前一个节点
    while(n > -1) {
        fast = fast.next
        n--
    }
    while(fast !== null) {
        slow = slow.next
        fast = fast.next
    }
    slow.next = slow.next.next // 删除下一个节点
    return head
};
/**
* input: [1,2,3,4,5,6], 2
* output: [1,2,3,4,6] // pass
*
* // 待删除元素在表头的情况
* input: [1,2], 2
* output: Error in read property `next` of null // failed
*/

可以看到在待删除元素位于表头时,以上代码运行失败,在第一个 while 循环处判断条件超出链表右边界。在这里如果试图通过加上 fast.next != null来规避的话, 在下方删除元素操作时依旧会遇到边界情况。

有了上面的例子,推一及百,应用哨兵节点如下:

var removeNthFromEnd = function(head, n) {
    let dummy = new ListNode(-1)
    dummy.next = head
    let slow = fast = dummy
    // 这里你可能会有疑问,为何增加了一个头结点后,-1不用改变
    // 实际通过画图可以知道,快慢指针的应用其实是在于保证其距离为n即可
    while(n > -1) {
        fast = fast.next
        n--
    }
    while(fast !== null) {
        slow = slow.next
        fast = fast.next
    }
    slow.next = slow.next.next
    return dummy.next
};

成功解决。

实现一个repeat函数

实现一个repeat函数

题目

实现一个repeat函数,每次间隔时间调用被包裹的函数,重复指定的次数

function repeat (func, times, wait) {
  // ...
}
// 调用
const repeatFunc = repeat(console.log, 4, 500)
repeatFunc('hello~')
// 输出
// hello~ // * 4 by interval 500ms

实现

  1. 延长setTimeout的时间
function repeat (func, times, wait) {
  if (typeof func !== 'function') throw Error('The first param for repeat is not a function!')
  return (...args) => {
    for (let i = 0; i < times; i++) {
      setTimeout(() => {
        console.log(new Date())
        func.apply(null, args)
      }, (i + 1) * wait)
    }
  }
}
  1. 借助Promise实现一个sleep函数
function sleep (wait) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(window.performance.now())
    }, wait)
  })
}

function repeat (func, times, wait) {
  if (typeof func !== 'function') throw Error('The first param for repeat is not a function!')
  return async (...args) => {
    for (let i = 0; i < times; i++) {
      console.log(await sleep(wait))
      func.apply(null, args)
    }
  }
}

以上点了一些思路,抛砖引玉,仅供参考。

N 数之和系列解题模板

解题套路:

  1. 直接用哈希表来找(只适合两数之和)

  2. 先排序,然后固定首个数字,剩下的交给双指针来找

哈希表解「两数之和」

题目

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

暴力解法

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    let res = []
    let N = nums.length
    for (let i = 0; i < N; i++) {
        for (let j = i + 1; j < N; j++) {
            if (nums[i] + nums[j] === target) {
              	return [i, j]
            }
        }
    }
    return []
};
  • 时间复杂度:$O(n^2)$
  • 空间复杂度:$O(1)$

显而易见,我们还能再优化一下。

哈希表解法

直接将 num 和对应的下标存到 哈希表 里面,遍历一次 nums,我们只要在 哈希表 里找到 target - num 即可。

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    let map = new Map()
    nums.forEach((num, index) => {
      	map.set(num, index)
    })
    nums.forEach((num, index) => {
        let val = target - num
        if (map.has(val)) {
            return [index, map.get(val)]
        }
    })
};
  • 时间复杂度:$O(n)$
  • 空间复杂度:$O(n)$

哈希表解法优化

上面我们遍历了两次 nums,其实也可以只遍历一次。

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    nums.forEach((num, index) => {
      let val = target - num
      if (map.has(val)) {
      		return [index, map.get(val)]
      }
      map.set(val, index)
    })
  	return []
};
  • 时间复杂度:$O(n)$

  • 空间复杂度:$O(n)$

双指针解三(三或三以上)数之和

题目

参考上面的「两数之和」,这里不再赘述。

解法

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums, target) {
    let res = []
    let N = nums.length
    nums.sort((a, b) => a - b)
    for (let i = 0; i < N; i++) {
        // 跳过重复的元素
        if (nums[i] === nums[i - 1]) {
            continue
        }
        let left = i + 1
        let right = N - 1
        // 找到 nums[i] + nums[left] + nums[right] === 0
        // 将结果加到 res 中
        // 过程注意剔除重复的数字
        while (left < right) {
            let sum = nums[i] + nums[left] + nums[right]
            if (sum === target) {
                res.push([nums[i], nums[left], nums[right]])
                while (left < right && nums[left] === nums[left + 1]) {
                    left++
                }
                while (left < right && nums[right] === nums[right - 1]) {
                    right--
                }
                left++
                right--
            } else if (sum < target) {
                left++
            } else {
                right--
            }
        }
    }
    return res
};

四数之和

「四数之和」解法如下(跟上面的思路是一样的,只不过是多了一个循环,这里不再注释):

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var fourSum = function(nums, target) {
  let res = []
  let N = nums.length
  nums.sort((a, b) => a - b)
  for (let i = 0; i < N; i++) {
    if (nums[i] === nums[i - 1]) {
      continue
    }
    for (let j = i + 1; j < N; j++) {
      if (nums[j] === nums[j - 1]) {
        continue
      }
      let left = j + 1
      let right = N - 1
      while (left < right) {
        let sum = nums[i] + nums[j] + nums[left] + nums[right]
        if (sum === target) {
          res.push([nums[i], nums[j], nums[left], nums[right]])
          while (left < right && nums[left] === nums[left + 1]) {
            left++
          }
          while (left < right && nums[right] === nums[right - 1]) {
            right--
          }
          left++
          right--
        } else if (sum < target) {
          left++
        } else {
          right--
        }
      }
    }
  }
}

那么对于 $N$ 数之和,我们只要利用一下 DFS 就可以解决了,参考以下代码:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[][]}
 */
var nSum = function(nums, target) {
    const helper = (index, N, temp) => {
        // 如果下标越界了或者 N < 3 就没有必要在接着走下去了
        if (index === len || N < 3) {
            return
        }
        for (let i = index; i < len; i++) {
            // 剔除重复的元素
            if (i > index && nums[i] === nums[i - 1]) {
                continue
            }
            // 如果 N > 3 的话就接着递归
            // 并且在递归结束之后也不走下边的逻辑
            // 注意这里不能用 return
            // 否则循环便不能跑完整
            if (N > 3) {
                helper(i + 1, N - 1, [nums[i], ...temp])
                continue
            }
            // 当走到这里的时候,相当于在求「三数之和」了
            // temp 数组在这里只是把前面递归加入的数组算进来
            let left = i + 1
            let right = len - 1
            while (left < right) {
                let sum = nums[i] + nums[left] + nums[right] + temp.reduce((prev, curr) => prev + curr)
                if (sum === target) {
                    res.push([...temp, nums[i], nums[left], nums[right]])
                    while (left < right && nums[left] === nums[left + 1]) {
                        left++
                    }
                    while (left < right && nums[right] === nums[right - 1]) {
                        right--
                    }
                    left++
                    right--
                } else if (sum < target) {
                    left++
                } else {
                    right--
                }
            }
        }
    }
    let res = []
    let len = nums.length
    nums.sort((a, b) => a - b)
    helper(0, 4, [])
    return res
};

总而言之,要点是:先排序,然后把循环降低到两个之后,利用双指针来找最后两个值。

相关题目:

剑指offer —— 青蛙跳台阶问题

点击查看原文
点击查看原题

题目

一只青蛙一次可以跳上$1$级台阶,也可以跳上$2$级台阶。求该青蛙跳上一个 $n$级的台阶总共有多少种跳法。

答案需要取模 $1e9+7$($1000000007$),如计算初始结果为:$1000000008$,请返回 $1$

提示:$n$ 的取值为 $[0, 100]$

解题思路

$n$ 级台阶的跳法为 $Sn$
$n$$0$ 时,$S\mathop{{}}\nolimits_{{0}} = 1$
$n$$1$ 时,$S\mathop{{}}\nolimits_{{1}} = 1$
$n$$2$ 时,$S\mathop{{}}\nolimits_{{2}} = 2$
$n$$3$ 时,$S\mathop{{}}\nolimits_{{3}} = 3$
$n$$4$ 时,$S\mathop{{}}\nolimits_{{4}} = 5$
...
当台阶数为 $n$ 时,$S\mathop{{}}\nolimits_{{n}}=S\mathop{{}}\nolimits_{{n - 1}}+S\mathop{{}}\nolimits_{{n-2}}\text{(}n \ge 2\text{)}
$

第一版代码

根据上面推出的状态转移方程,我们很容易写出如下代码

/**
 * @param {number} n
 * @return {number}
 */
var numWays = function(n) {
    if (n === 0 || n === 1) {
        return 1
    }
    const mod = 1000000007
    const res = [1, 1]
    for (let i = 2; i <= n; i++) {
        res[i] = (res[i - 1] + res[i - 2]) % mod
    }
    return res[n]
};

优化代码

在上面的代码中我们发现,res[i] 取决于 res[i - 1]res[i - 2],所以我们大可不必使用数组,直接改用三个变量也能将代码实现:

/**
 * @param {number} n
 * @return {number}
 */
var numWays = function(n) {
    if (n === 0 || n === 1) {
        return 1
    }
    const mod = 1000000007
    let first = 1
    let second = 1
    let res = 0
    for (let i = 2; i <= n; i++) {
        res = (first + second) % mod
        first = second
        second = res
    }
    return res
};

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.