前端面试系列六:手写程序算法题

453 阅读6分钟

快来加入我们吧!

"小和山的菜鸟们",为前端开发者提供技术相关资讯以及系列基础文章。为更好的用户体验,请您移至我们官网小和山的菜鸟们 ( xhs-rookies.com/ ) 进行学习,及时获取最新文章。

"Code tailor" ,如果您对我们文章感兴趣、或是想提一些建议,微信关注 “小和山的菜鸟们” 公众号,与我们取的联系,您也可以在微信上观看我们的文章。每一个建议或是赞同都是对我们极大的鼓励!

面试系列不定期更新,请随时关注

前言

本篇专栏重点在于讲解面试中 手写程序题/算法题 的面试题内容。

注意: 本篇专栏至只会涉及到重点内容,并不会进行拓展。某些题目需要拓展知识点的,我们会将拓展内容、整体详细信息放置与每个题目的最前面,可以自行查看。

手写程序题/算法题

手写程序题/算法题
程序输出题目:构造函数与实例对象间的属性问题
程序编程题:flat、拍平数组、自己实现拍平数组的效果
程序编程题:自己实现 promise all
程序编程题:自己实现 reducer
程序编程题:URL 解析为对象
程序编程题:使用 setTimeout 写一个 setInterval
算法题:无重复字符最大子串的问题
算法题:二叉树的前中后遍历
算法题:迷宫问题
算法题:手写冒泡排序
算法题:不完全的二叉树的倒置

题目解析

构造函数与实例对象间的属性问题

以下代码输出什么?

function Otaku() {
  this.b = 1
  this.c = 2
}
var person = new Otaku()
Otaku.prototype.b = 4
person.c = 5
console.log('1:', person.b)
console.log('2:', person.c)
person.__proto__.b = 10
console.log('3:', person.b)
console.log('4:', Otaku.prototype.b)

这道题目涉及到构造函数与实例对象之间的知识点,背后同样涉及原型与原型链的问题,详细见JavaScript 深入之从原型到原型链

我们先来揭晓答案:

1: 1
2: 5
3: 1
4: 10

你有全答对吗?下面我们一起来看看这道题。这道题首先给了一个构造函数 Otaku,这个构造函数有两个属性 bc,然后使用 new 创建了它的实例对象 person,如果你了解原型,那么你肯定知道实例对象 person 已经获得了构造函数中的属性。

function Otaku() {
  this.b = 1
  this.c = 2
}
var person = new Otaku() // person.b = 1; person.c = 2

看到这里你会发现构造函数 Otaku 有一个属性 prototype,这个构造函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person 原型。也就是说 Otaku.prototype.b = 4; 这条语句中的 b 实际指向的是原型的 b。

function Otaku() {
  this.b = 1
  this.c = 2
}
var person = new Otaku()
Otaku.prototype.b = 4 // 修改的是 person 的原型的属性 b
person.c = 5 // 修改的是 person 的 c
console.log('1:', person.b) // person.b = 1
console.log('2:', person.c) // person.c = 5

看到这里,我们又发现 person 的属性 __proto__, 这个属性也指向 person 的原型,所以这句话的 b 属性也是指向原型的 b

function Otaku() {
  this.b = 1
  this.c = 2
}
var person = new Otaku() // person.b = 1; person.c = 2
Otaku.prototype.b = 4 // 修改的是 person 的原型的属性 b
person.c = 5 // 修改的是 person 的 c
console.log('1:', person.b) // person.b = 1
console.log('2:', person.c) // person.c = 5
person.__proto__.b = 10 // 修改的事 preson 的原型的属性b
console.log('3:', person.b) // person.b = 1
console.log('4:', Otaku.prototype.b) // Otaku.prototype.b = 10

这就是结果了,关于这道题涉及的知识点,重点还是在原型以及原型链上。

程序编程题

1. flat、拍平数组、自己实现拍平数组的效果

这里只提供两个比较简单的实现,后面还会涉及到使用reduce和栈、使用Generator、原型链等等,详细见:面试官连环追问:数组拍平(扁平化) flat 方法实现 - 知乎 (zhihu.com)

  1. 最简单的遍历实现
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, 'string', { name: '弹铁蛋同学' }]
// concat + 递归
function flat(arr) {
  let arrResult = []
  arr.forEach((item) => {
    if (Array.isArray(item)) {
      arrResult = arrResult.concat(flat(item)) // 递归
      // 或者用扩展运算符
      // arrResult.push(...arguments.callee(item));
    } else {
      arrResult.push(item)
    }
  })
  return arrResult
}
flat(arr)
// [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { name: "弹铁蛋同学" }];
  1. 传入数组控制递归层数
// reduce + 递归
function flat(arr, num = 1) {
  return num > 0
    ? arr.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur, num - 1) : cur), [])
    : arr.slice()
}
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, 'string', { name: '弹铁蛋同学' }]
flat(arr, Infinity)
// [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { name: "弹铁蛋同学" }];

2. 自己实现 promise all

Promise.newAll = function (promiseArr) {
  let results = []

  return new Promise((reslove, reject) => {
    promiseArr.forEach((item_promise) => {
      item_promise.then((res) => results.push(res)).catch((err) => reject(err))
    })
    return reslove(results)
  })
}

3. 自己实现 reducer

注意:不能使用箭头函数,箭头函数中没有 this 所有会导致 sourcearr 为空对象

Array.prototype.fakereduce = function (fn, initnumber = 0) {
  let sum_increase = initnumber
  let sourcearr = this
  for (let i = 0; i < sourcearr.length; i++) {
    sum_increase = fn(sum_increase, sourcearr[i])
  }
  return sum_increase
}

这个只是最基本的,只包含前两个参数,也没有检测是否为函数。(下面加入判断情况)

// 判断调用对象是否为数组
if (Object.prototype.toString.call([]) !== '[object Array]') {
  throw new TypeError('not a array')
}
// 判断调用数组是否为空数组
const sourceArray = this
if (sourceArray.length === 0) {
  throw new TypeError('empty array')
}
// 判断传入的第一个参数是否为函数
if (typeof fn !== 'function') {
  throw new TypeError(`${fn} is not a function`)
}

4. URL 解析为对象

将以下输入的 URL 转化为对象:

http://www.baidu.com/s?wd=春节&name=justin

{
    wd: '春节',
    name: 'justin'
}

这道题重点在于分割,将 ? 后的键值对取出来,并放置于对象中,不同的键值对通过 & 符号分割。具体代码如下(这里也只给出一种解法):

let urlToJson = (url = window.location.href) => {
  // 箭头函数默认传值为当前页面url
  url = url.encodeURIComponent()
  let obj = {},
    index = url.indexOf('?'),
    params = url.substr(index + 1)

  if (index != -1) {
    let parr = params.split('&')
    for (let i of parr) {
      let arr = i.split('=')
      obj[arr[0]] = arr[1]
    }
  }
  return obj
}

5. 使用 setTimeout 写一个 setInterval

扩展: 使用 setInterval 实现一个 setTimeout

const mySetInterval = (callback, time) => {
  ;(function inner() {
    const timer = setTimeout(() => {
      callback()
      clearTimeout(timer)
      inner()
    }, time)
  })()
}

算法题

1. 无重复字符最大子串的问题

详细请见:3. 无重复字符的最长子串 - 力扣(LeetCode) (leetcode-cn.com)

大致有两种方法:暴力遍历、滑动窗口

这边给出滑动窗口代码,详情解释请看 leetcode 解析:

var lengthOfLongestSubstring = function (s) {
  // 哈希集合,记录每个字符是否出现过
  const occ = new Set()
  const n = s.length
  // 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
  let rk = -1,
    ans = 0
  for (let i = 0; i < n; ++i) {
    if (i != 0) {
      // 左指针向右移动一格,移除一个字符
      occ.delete(s.charAt(i - 1))
    }
    while (rk + 1 < n && !occ.has(s.charAt(rk + 1))) {
      // 不断地移动右指针
      occ.add(s.charAt(rk + 1))
      ++rk
    }
    // 第 i 到 rk 个字符是一个极长的无重复字符子串
    ans = Math.max(ans, rk - i + 1)
  }
  return ans
}

2. 二叉树的前中后遍历

二叉树结构如下:

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**

遍历方法有很多种,我们这边采用递归的方法。

前序遍历:

function preOrderReducer(head) {
  if (head === null) {
    return
  }
  console.log(head.val)
  preOrderReducer(head.left)
  preOrderReducer(head.right)
}

中序遍历

function preOrderReducer(head) {
  if (head === null) {
    return
  }
  preOrderReducer(head.left)
  console.log(head.val)
  preOrderReducer(head.right)
}

后序遍历

function preOrderReducer(head) {
  if (head === null) {
    return
  }
  preOrderReducer(head.left)
  preOrderReducer(head.right)
  console.log(head.val)
}

3. 迷宫问题

题目描述:

在一个 n * m 的迷宫内,起点为 【 0 ,0 】,终点为 【n -1 ,m -1 】,现需要你判断该迷宫是否有至少一条通路。如果有通路即返回 true,否则返回 false

n * m 迷宫内可能会遇到墙壁,如果当前位置为 1 ,则代表当前位置是墙壁,不能行走。

输入: n、m、maze(数组)

输出: true / false

例如:

input: 3 3 [ [0,1,1],[0,0,0],[0,1,0] ]
output: true

题目解析:

一般来说 BFSDFS 或者动态规划等等方法都可以解决,解决方法非常多样。这种问题也会有许多变种,比如输出路径等等。

样例代码:

// BFS
const findMazeWay = (n, m, maze) => {
  if (maze[(0, 0)] === 1) {
    //如果起点为墙壁则直接无解
    return false
  } else {
    return dfsSearch(0, 0, maze)
  }
  function dfsSearch(index_n, index_m, maze) {
    maze[index_n][index_m] = -1 //走过的路判定为-1
    if (index_n === n - 1 && index_m === m - 1) {
      return true
    }
    if (index_m + 1 <= m - 1 && maze[index_n][index_m + 1] === 0)
      dfsSearch(index_n, index_m + 1, maze)
    if (index_n + 1 <= n - 1 && maze[index_n + 1][index_m] === 0)
      dfsSearch(index_n + 1, index_m, maze)
    if (index_m - 1 >= 0 && maze[index_n][index_m - 1] === 0) dfsSearch(index_n, index_m - 1, maze)
    if (index_n - 1 >= 0 && maze[index_n - 1][index_m] === 0) dfsSearch(index_n - 1, index_m, maze)
  }
}

console.log(
  findMazeWay(3, 3, [
    [0, 1, 1],
    [0, 0, 1],
    [1, 0, 0],
  ]),
)

问题拓展: 如果成功了,请输出至少一条成功路径。(或是输出所有可成功的路径)

4. 手写冒泡排序

样例代码:

function mySort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    for (let j = i; j < arr.length; j++) {
      if (arr[i] > arr[j]) {
        let temp = arr[j]
        arr[j] = arr[i]
        arr[i] = temp
      }
    }
  }
}

5. 不完全的二叉树的倒置

题目描述:

假设有一棵树二叉树,我们需要将其左子树全部转化为右子树。(对于每一个子节点,都需要转化)

树形结构如下:

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**

题目解析:

这道题重点在于遍历树,并且在遍历的情况下,需要交换两个子树,所以不能采用 DFS 深度遍历。遍历方法有很多种,这里给出的解决方案为其中的一种。

样例代码:

const treeReBuild(tree: TreeNode){
    if(tree === null){
        return
    }
    let temp = tree.right
    tree.right = tree.left
    tree.left = temp

    treeReBuild(tree.left)
    treeReBuild(tree.right)
}