前端进阶算法 --002(编程题)

131 阅读4分钟

1. 模版渲染

实现一个 render(template, context) 方法,将 template 中的占位符用 context 填充。

示例:

var template = "{{name}}很厉害,才{{age}}岁"
var context = {name:"bottle",age:"15"}
输入:template context
输出:bottle很厉害,才15岁

要求:

  • 级联的变量也可以展开
  • 分隔符与变量之间允许有空白字符

1.1 解答:使用正则 + trim

  • 利用非贪婪匹配 /{{(.*?)}}/g 匹配到到所有的 {{name}}{{age}}
  • 利用 str.replace(regexp|substr, newSubStr|function) ,其中第二个参数可以是 fucntion (replacement) ,该函数的返回值将替换掉第一个参数匹配到的结果,将所有匹配到的字符替换成指定的字符
  • 最后,String.prototype.trim() 去除分隔符与变量之间空白字符
var template = "{{name}}很厉害,才{{age}}岁"
var context = {name:"bottle",age:"15"}
function render(template, context) {
  return template.replace(/{{(.*?)}}/g, (match, key) => context[key.trim()])
}
render(template, context)
// "bottle很厉害,才15岁"

2. Object 的深拷贝

2.1 递归

遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深拷贝

// 递归判断是否对象和数组
function deepClone(obj, target) {
    if(!obj) return

    for(let key in obj) {
        if(obj.hasOwnProperty(key)) {
            if(Array.isArray(key) || (typeof obj[key] === 'object' && obj[key] !== null)) {
                target[key] = []
                
                deepClone(obj[key], target[key])
            } else {
                target[key] = obj[key]
            }
        }
    }

    return target
}

3. 手写 async/await 的实现

await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。

/**
 * async/await 实现
 * @param {*} generatorFunc 
 */
function asyncToGenerator(generatorFunc) {
  // 返回的是一个新的函数
  return function(...args) {
    // 先调用generator函数 生成迭代器
    // 对应 var gen = testG()
    const gen = generatorFunc.apply(this, args)

    // 返回一个promise 因为外部是用.then的方式 或者await的方式去使用这个函数的返回值的
    // var test = asyncToGenerator(testG)
    // test().then(res => console.log(res))
    return new Promise((resolve, reject) => {
    
      // 内部定义一个step函数 用来一步一步的跨过yield的阻碍
      // key有next和throw两种取值,分别对应了gen的next和throw方法
      // arg参数则是用来把promise resolve出来的值交给下一个yield
      function step(key, arg) {
        let genResult
        
        // 这个方法需要包裹在try catch中
        // 如果报错了 就把promise给reject掉 外部通过.catch可以获取到错误
        try {
          genResult = gen[key](arg)
        } catch (error) {
          return reject(error)
        }

        // gen.next() 得到的结果是一个 { value, done } 的结构
        const { value, done } = genResult

        if (done) {
          // 如果已经完成了 就直接resolve这个promise
          // 这个done是在最后一次调用next后才会为true
          // 以本文的例子来说 此时的结果是 { done: true, value: 'success' }
          // 这个value也就是generator函数最后的返回值
          return resolve(value)
        } else {
          // 除了最后结束的时候外,每次调用gen.next()
          // 其实是返回 { value: Promise, done: false } 的结构,
          // 这里要注意的是Promise.resolve可以接受一个promise为参数
          // 并且这个promise参数被resolve的时候,这个then才会被调用
          return Promise.resolve(
            // 这个value对应的是yield后面的promise
            value
          ).then(
            // value这个promise被resove的时候,就会执行next
            // 并且只要done不是true的时候 就会递归的往下解开promise
            // 对应gen.next().value.then(value => {
            //    gen.next(value).value.then(value2 => {
            //       gen.next() 
            //
            //      // 此时done为true了 整个promise被resolve了 
            //      // 最外部的test().then(res => console.log(res))的then就开始执行了
            //    })
            // })
            function onResolve(val) {
              step("next", val)
            },
            // 如果promise被reject了 就再次进入step函数
            // 不同的是,这次的try catch中调用的是gen.throw(err)
            // 那么自然就被catch到 然后把promise给reject掉啦
            function onReject(err) {
              step("throw", err)
            },
          )
        }
      }
      step("next")
    })
  }
}

var getData = () => new Promise(resolve => setTimeout(() => resolve('data'), 1000));
function* testG() {
  const data = yield getData();
  console.log('data: ', data);
  const data2 = yield getData();
  console.log('data2: ', data2);
  return 'success';
}

var gen = asyncToGenerator(testG);
gen().then(res => console.log(res));

4. 用最简洁代码实现 indexOf 方法

4.1 indexOf 有两种:

String.prototype.indexOf()

返回从 fromIndex 处开始搜索第一次出现的指定值的索引,如果未找到,返回 -1

str.indexOf(searchValue [, fromIndex])
// fromIndex 默认值为 0

Array.prototype.indexOf()

返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1

arr.indexOf(searchElement[, fromIndex])

4.2 String.prototype.indexOf()

解题思路:正则,字符串匹配

function sIndexOf(str, searchStr, fromIndex = 0){
    var regex = new RegExp(`${searchStr}`, 'ig')
    regex.lastIndex = fromIndex
    var result = regex.exec(str)
    return result ? result.index : -1
}

// 测试
var paragraph = 'The quick brown fox jumps over the lazy dog. If the dog barked, was it really lazy?'
var searchTerm = 'dog'
// 测试一:不设置 fromIndex
console.log(sIndexOf(paragraph, searchTerm))
// 40
console.log(paragraph.indexOf(searchTerm));
// 40
// 测试二:设置 fromIndex
console.log(sIndexOf(paragraph, searchTerm, 41))
// 52
console.log(paragraph.indexOf(searchTerm, 41));
// 52

4.3 Array.prototype.indexOf()

解题思路:遍历匹配

function aIndexOf(arr, elem, fromIndex = 0){
    if(!elem) return -1
    for(let i = fromIndex; i < arr.length; i++) {
        if(arr[i] === elem) return i
    }
    return -1
}

// 测试
var beasts = ['ant', 'bison', 'camel', 'duck', 'bison']
// 测试一:不设置 fromIndex
console.log(aIndexOf(beasts, 'bison'))
// 1
console.log(beasts.indexOf('bison'))
// 1
// 测试二:设置 fromIndex
console.log(aIndexOf(beasts, 'bison', 2))
// 4
console.log(beasts.indexOf('bison', 2))

4.4 总结indexOf:

function indexOf(items, item, fromIndex = 0) {
    let isArray = Array.isArray(items);
    let isString = Object.prototype.toString.call(items) == '[object String]';
    if (!isArray && !isString) throw new SyntaxError();
    if(isArray) return sIndexOf(items, item, fromIndex)
    else return aIndexOf(items, item, fromIndex)
}

5. 实现一个方法,拆解URL参数中queryString

入参格式参考:

const url = 'http://sample.com/?a=1&b=2&c=xx&d=#hash';

出参格式参考:

const result = { a: '1', b: '2', c: 'xx', d: '' };

5.1 正则匹配:

const url = 'http://sample.com/?a=1&b=2&c=xx&d=2#hash';

const queryString = (str)=>{
    const obj = {}
    str.replace(/([^?&]+)=([^&#]*)/g, (_, k, v)=>{
        obj[k] = v
    })
    return obj
}
queryString(url)
//{a: '1', b: '2', c: 'xx', d: '2'}

5.2

function getParams(u: URL) {
  const s = new URLSearchParams(u.search)
  const obj = {}
  s.forEach((v, k) => (obj[k] = v))
  return obj
}

const url = 'http://sample.com/?a=1&b=2&c=xx&d=2#hash';
getParams(new URL(url))
//{a: '1', b: '2', c: 'xx', d: '2'}

5.3 万能reduce

const getQuery = (queryStr) => {
    //const [, query] = queryStr.split('?')
    // 去除hash标识
    const [query,] = queryStr.split('?')[1].split('#')
    if (query) {
        return query.split('&').reduce((pre, cur) => {
            const [key, val] = cur.split('=')
            pre[key] = decodeURIComponent(val)
            return pre
        }, {})
    }
    return {}
}
const url = 'http://sample.com/?a=1&b=2&c=xx&d=2#hash';
getQuery(url)
//{a: '1', b: '2', c: 'xx', d: '#hash'}
// 去除hash标识结果
//{a: '1', b: '2', c: 'xx', d: '2'}

6. 按照以下要求,实现 createFlow 函数

6.1

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const subFlow = createFlow([() => delay(1000).then(() => console.log("c"))]);

createFlow([
  () => console.log("a"),
  () => console.log("b"),
  subFlow,
  [() => delay(1000).then(() => console.log("d")), () => console.log("e")],
]).run(() => {
  console.log("done");
});

// 需要按照 a,b,延迟1秒,c,延迟1秒,d,e, done 的顺序打印

按照上面的测试用例,实现 createFlow

  • flow 是指一系列 effects 组成的逻辑片段。
  • flow 支持嵌套。
  • effects 的执行只需要支持串行。

6.2 解题步骤:

createFlow 以一个数组作为参数,数组参数可有以下几种类型:

  • 普通函数
() => console.log("a")
  • 异步函数
() => delay(1000).then(() => console.log("c"))
  • 嵌套 createFlow
const subFlow = createFlow([() => delay(1000).then(() => console.log("c"))])
  • 数组
[() => delay(1000).then(() => console.log("d")), () => console.log("e")]

步骤一:扁平化

因为 effects 的执行只需要支持串行,所以我们可以把数组扁平化一下

function createFlow(effects = []) {
  // 浅拷贝一下,今年不影响传入的参数
  const queue = [...effects.flat()]
}

步骤二:run执行

createFlow 并不是直接执行,而是 .run() 之后才会开始执行

function createFlow(effects = []) {
  const queue = [...effects.flat()]
  const run = async function(cb) {
    for(let task of queue) {
    }
    if(cb) cb()
  }
}

步骤三:异步函数

因为参数中有异步函数,这里使用 await 执行

function createFlow(effects = []) {
  const queue = [...effects.flat()]
  const run = async function(cb) {
    for(let task of queue) {
      await task()
    }
    if(cb) cb()
  }
}

步骤四:支持嵌套

使用 isFlow 来判断当前是否是嵌套执行

function createFlow(effects = []) {
  const queue = [...effects.flat()]
  const run = async function(cb) {
    for(let task of queue) {
      if(task.isFlow) { // 如果是嵌套,执行嵌套函数
        await task.run()
      } else {
        await task()
      }
    }
    if(cb) cb()
  }
  return {
    run,
    isFlow: true
  }
}

6.3 完整代码:两种

  • function
function createFlow(effects = []) {
  const queue = [...effects.flat()]
  const run = async function(cb) {
    for(let task of queue) {
      if(task.isFlow) {
        await task.run()
      } else {
        await task()
      }
    }
    if(cb) cb()
  }
  return {
    run,
    isFlow: true
  }
}

// 测试
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const subFlow = createFlow([() => delay(1000).then(() => console.log("c"))]);

createFlow([
  () => console.log("a"),
  () => console.log("b"),
  subFlow,
  [() => delay(1000).then(() => console.log("d")), () => console.log("e")],
]).run(() => {
  console.log("done");
});

// a,b,延迟1秒,c,延迟1秒,d,e, done 的顺序打印
  • class
class Flow {
  constructor(effects) {
    this.queue = [...effects.flat()]
  }
  async run(cb) {
    for(let task of this.queue) {
      if(task instanceof Flow) {
        await task.run()
      } else {
        await task()
      }
    }
    if(cb) cb()
  }
};

function createFlow(effects = []) {
    return new Flow(effects)
};

// 测试
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const subFlow = createFlow([() => delay(1000).then(() => console.log("c"))]);

createFlow([
  () => console.log("a"),
  () => console.log("b"),
  subFlow,
  [() => delay(1000).then(() => console.log("d")), () => console.log("e")],
]).run(() => {
  console.log("done");
});
// a,b,延迟1秒,c,延迟1秒,d,e, done 的顺序打印

7. 如何判断两个变量相等

7.1 使用 API: Object.is() 方法判断两个值是否为同一个值

Object.is(x, y)

Polyfill:

if (!Object.is) {
  Object.is = function(x, y) {
    // SameValue algorithm
    if (x === y) { // Steps 1-5, 7-10
      // Steps 6.b-6.e: +0 != -0
      return x !== 0 || 1 / x === 1 / y;
    } else {
      // Step 6.a: NaN == NaN
      return x !== x && y !== y;
    }
  };
}

7.2 扩展:

JavaScript提供三种不同的值比较操作:

  • 严格相等比较:使用 ===
  • 抽象相等比较:使用 ==
  • 以及 Object.is (ECMAScript 2015/ ES6 新特性):同值相等

其中:

  • ===:进行相同的比较,不进行类型转换 (如果类型不同, 只是总会返回 false )
  • ==:执行类型转换,比较两个值是否相等
  • Object.is :与 === 相同,但是对于 NaN 和 -0 和 +0 进行特殊处理, Object.is(NaN, NaN) 为 true , Object.is(+0, -0) 为 false

8. 找出字符串中连续出现最多的字符和个数

8.1 new Map求解:

const mostChars = function(str) {
   if (!str) return '';
   let stack = [];
   let map = new Map();
   str.split('').forEach(c => {
       let last = stack.pop();
       if(last === c) {
           map.set(c, map.get(c) + 1);
           stack.push(c);
       } else {
           stack.push(c);
           map.set(c, 1);
       }
   })
   let max = 0;
   [...map.values()].forEach(n => {
       if (max < n) max = n;
   })
   for (let key of map.keys()) {
       if (map.get(key) < max) {
           //删除非连续出现次数最多的字符
           map.delete(key);
       }
   }
   // console.log(Object.fromEntries(map));
   return Object.fromEntries(map);
}

8.2 普通求解:

function fn(str) {
    if (!str) return {}
    //vals存储当前满足的字符,max是数量,prev是上个字符,prevNum是上个字符的数量
    let vals = [], max = 1
    let prev = '', prevNum = 1
    for (const le of str) {
        if (le === prev) {
            prevNum++
            if (prevNum > max) {
                vals = [prev]
                max = prevNum
            } else if(prevNum === max){
                vals.push(prev)
            }
        } else {
            prev = le
            prevNum = 1
        }
    }
    const res = vals.map(val => ({ [val]: max }))
    return res
    // assign数组转对象
    // return Object.assign({},...res)
}

9. 找出一个字符串中的不匹配括号的位置,以json形式输出,位置index从0开始

9.1 解答:利用栈结构

解题思路:  将字符串中的字符依次入栈,遍历字符依次判断:

  • 首先判断该元素是否括号,不是则遍历下一个字符

  • 是 { 、 ( 、 [ ,直接入栈

  • 否则该字符为 } 、 ) 、 ] 中的一种,

    • 如果栈为空,则当前右括号无匹配左括号,直接写进结果数组,并遍历下一个字符
    • 栈顶元素出栈,判断当前元素是否与出栈元素匹配,例如栈中元素有 ({, 如果继续遍历到的元素为 ), 那么当前元素序列为 ({) 是不可能有效的,所以此时与出栈元素匹配失败,将出栈元素写进结果数组,并继续匹配当前元素

当遍历完成时,所有已匹配的字符都已匹配出栈,如果栈不为空,说明字符串中还有未匹配的左括号字符,则将栈元素直接写进结果数组

代码实现:

let getUnmatchJson = function(s) {
    let map = {
        '{': '}',
        '(': ')',
        '[': ']'
    }
    let stack = [], 
        brackets = '{[()]}', 
        result = {}
    for(let i = 0; i < s.length ; i++) {
        // 如果不是括号,跳过
        if(brackets.indexOf(s[i]) === -1) continue
        // 如果是左括号,则进栈
        if(map[s[i]]) {
            stack.push({
                char: s[i],
                index: i
            })
        } else {
            // 如果是右括号,则出栈匹配
            if(!stack.length) {
                //如果栈为 null ,则表示没有匹配的左括号,则当前有括号直接进结果数组
                result[i] = s[i]
                continue
            }
            // 出栈
            let temp = stack.pop()
            // 括号不匹配
            if (s[i] !== map[temp.char]) {
                // 不匹配左括号进结果数组,并i--,继续匹配当前字符
                result[temp.index] = temp.char
                i --
            }
        }
    }
    // 如果匹配结束,依然有剩余的左括号,则直接进结果数组
    while(stack.length) {
        let temp = stack.pop()
        result[temp.index] = temp.char
    }
    return result
};

let s1 = '${{(3+5)*2+(5/(24)}'
let s2 = '[a+b]/${x}'
let s3 = '${(3+5)*2+(5/(24)}(}'
console.log(getUnmatchJson(s1)) // {1: "{", 11: "("}
console.log(getUnmatchJson(s2)) // {}
console.log(getUnmatchJson(s3)) // {10: "(", 18: "(", 19: "}"}

时间复杂度:O(n)

空间复杂度:O(n)

9.2 解答:正则

/**
 * 力扣20 是字符串匹配判断
 * 输入:${{(3+5)*2+(5/(24)}
 * 输出:
 * {
 *   1: '{',    
 *   11: '(',    
 * }
 */
let str = '${{(3+5)*2+(5/(24)}'
const getUnmatchJson = function (s) {
    let stack = []
    const res = {}
    let map = {
        '{': '}',
        '(': ')',
        '[': ']'
    }
    let reg = /[^{[()]}]/
    for (let i = 0, len = s.length; i < len; i++) {
        if (reg.test(s[i])) continue
        if(map[s[i]]) {
            stack.push({
                char:s[i],
                index:i
            })
        } else {
            //右括号
            if(stack.length===0) {
                //没有匹配的左括号
                res[i] = s[i]
                continue
            }
            let temp = stack.pop()
            if(s[i]!==map[temp.char]) {
                res[temp.index] = temp.char
                i--
            }
        }
    }
    //字符串匹配结束
    while(stack.length) {
        let temp = stack.pop()
        res[temp.index] = temp.char
    }
    return res
}
let str2 = '[a+b]/${dg'
console.log(getUnmatchJson(str))
console.log(getUnmatchJson(str2))
console.log(/[^{[()]}]/.test(str[1]))

10. 字符串相加

10.1

function add(str1, str2) {
  let result = ''
  let tempVal = 0
  let arr1 = str1.split('')
  let arr2 = str2.split('')

  while (arr1.length || arr2.length || tempVal) {
    tempVal += ~~arr1.pop() + ~~arr2.pop()
    result = tempVal % 10 + result
    tempVal = ~~(tempVal / 10)
  }

  return result.replace(/^0+/, '')
}

10.2

1、新建立双指针 i 、j,倒序循环两个数组
2、判断两数相加是否大于10,大于则进1,缓存 (result % 10)
3、循环结束判断move是否有值,有则加上,没有直接return
空间复杂度: 循环num1, num2,则空间复杂度O(n)
空间复杂度: result缓存结果,常数级复杂度O(1)

var addStrings = function(num1, num2) {
    var i = num1.length - 1,
        j = num2.length - 1,
        move = 0,
        result = '';
    while (i >= 0 || j >= 0) {
        n1 = i >= 0 ? num1[i] : 0;
        n2 = j >= 0 ? num2[j] : 0;
        var tmp = +n1 + +n2 + move;
        move = tmp >= 10 ? 1 : 0;
        result = (tmp % 10) + result;
        i--;
        j--
    }
    return move ? move + result : result;
};