贰.1.5 实战:字节跳动前端面试2道算法题

有关数组和二叉树的题高频出现

笔者在写作本书期间,曾经受邀参加字节跳动北京地区的“WEB前端高级工程师”岗位的面试。面试官是一位前端小姐姐,女程序员在其他公司很少见,所以觉得字节跳动的极客文化很有特色。一番流程性的自我介绍之后,小姐姐提了两道笔试算法题。

贰.1.5.1 多维数组扁平化

题目

原数组[[0],[2,3,4],1,[1,[2,3]]],写一段代码,将该数组扁平化,输出[0,2,3,4,1,1,2,3]

分析

如果读过本书壹.1.1 新版 ECMAScript 特性分析,就会发现 ES10 最新的特性Array.prototype.flat()可以完美解题,但是考虑到这个新特性是2019年(面试时)才出来不久,很多浏览器没有支持这个特性,所以也要考虑用原生JavaScript代码来实现这个扁平化数组的功能。

既然是处理数组,那就要熟悉JavaScript数组对象的各种方法,并恰当的利用它们来实现我们的目的。

解法一

请认真阅读注释,很多关键性提示都在注释里面详细指出了。

(() => {
//题目中给的测试数据
let sourceArr = [[0], [2, 3, 4], 1, [1, [2, 3]]];
//结果数组,用来保存最终输出的数据
let resultArr = [];
(function doFunc(arr) {
//若无子数组,直接把arr的数据导入结果数组resultArr
if (!hasChildArray(arr)) {
//用concat方法,将resultArr与arr合并后再返回给resultArr
resultArr = resultArr.concat(arr);
}
//若有子数组
else {
for (let i = 0, l = arr.length; i < l; i++) {
//如果子元素类型是数字
if (typeof arr[i] == "number") {
resultArr.push(arr[i]); //放入结果数组中保存起来
}
//否则,如果子元素类型是数组
else if (Array.isArray(arr[i])) {
doFunc(arr[i]); //递归调用
}
}
}
})(sourceArr);
console.log(resultArr); //>> [0, 2, 3, 4, 1, 1, 2, 3]
/**
* 定义一个工具性子函数,判断参数中传来的数组是否有子数组
* @param {Array} 数组
* @return {Boolean} true:有;false:没有
*/
function hasChildArray(arr) {
/*
arr.some() 方法测试数组arr中是不是有元素通过了被提供的函数callback
的测试。它返回的是一个Boolean类型的值。只要数组中有元素通过callback的
测试就会返回true;所有元素都没有通过callback的测试,返回值才会为false。
*/
return arr.some(function callback(item) {
//判断arr中的子元素item是否数组
if (Array.isArray(item)) {
return true; //返回true表明通过了测试,让外层some也返回true
}
});
}
})();

解法二

这个解法是后来补充的,面试时并没有想出来,后来觉得挺简洁的,所以分享出来。

(() => {
//题目中给的测试数据
let sourceArr = [[0], [2, 3, 4], 1, [1, [2, 3]]];
//结果数组,用来保存最终输出的数据
let resultArr;
(function doFunc(arr) {
resultArr = arr.toString().split(",");//利用toString和split取巧得到扁平数组
resultArr = resultArr.map(item=>Number(item));//将数组项由字符串转换为数字
})(sourceArr);
})();

贰.1.5.2 二叉树&完整路径和

经过上面的数组算法题热身之后,小姐姐比较满意,还想继续验证一下笔者在数据结构和算法这块儿的基本功,于是喝了一口茶之后,她紧接着出了一个更开放性的、二叉树有关的题。

题目

对于给定的二叉树,判断是否存在一条完整路径(从根节点开始,到叶节点结束的连线),其路径上节点的值之和为target,输出布尔值。举例:下面的树,是否存在一条完整路径,该路径上各个节点之和target=7?

6
/ \
2 3
/\ /
-1 3 0

分析

这个题目出得确实很开放,如果对树、二叉树的概念不熟悉,会有点无从下手,好在我们已经经过二叉树相关的学习训练了。开放性命题需要做一些假设,把前提条件确定了,才能解题。先确定树、结点的数据结构,有什么样的数据结构,就有后面什么样的算法。二叉树的遍历,离不开递归。

解法一

笔者多年的OOP经(毒)验(素)派上用场了,首先就会想到用两个类定义树和结点,然后写一个递归遍历树,代码如下:

(() => {
//定义结点的数据结构
class Node {
/**
* 构造函数
* @param {Number} value 当前结点的值
* @param {Node} left 当前结点的左子结点
* @param {Node} right 当前结点的右子结点
*/
constructor(value, left, right) {
this.value = value;
this.left = left;
this.right = right;
}
}
//定义二叉树的数据结构
class BTree {
/**
* 构造函数
*/
constructor() {
//结点列表,存放所有结点
this.list = [];
}
/**
* 添加根结点
* @param {Node} node 被添加的结点
*/
addRoot(node) {
if (node != null) {
this.list.push(node);
}
}
/**
* 添加子结点
* @param {Node} pNode 父结点
* @param {Node} node 当前结点
* @param {Boolean} isLeft 是否左侧添加,如果为false则是右侧添加
*/
addChildNode(pNode, node, isLeft) {
this.list.push(node);
if (isLeft) {
pNode.left = node;
} else {
pNode.right = node;
}
}
/**
* 根据数组中的索引(下标)返回node
* @param {Number} index 索引(下标)
*/
getNode(index) {
return this.list[index];
}
}
//创建示例中的二叉树
let bTree = new BTree();
//第一层 根结点
bTree.addRoot(new Node(6, null, null));
//第二层
bTree.addChildNode(bTree.getNode(0), new Node(2, null, null), true);
bTree.addChildNode(bTree.getNode(0), new Node(3, null, null), false);
//第三层
bTree.addChildNode(bTree.getNode(1), new Node(-1, null, null), true);
bTree.addChildNode(bTree.getNode(1), new Node(3, null, null), false);
bTree.addChildNode(bTree.getNode(2), new Node(0, null, null), true);
/**
* 主逻辑函数
* @param {Node} node 树的结点
* @param {Number} target 题目要求的目标值
*/
function hasPathSum(node, target) {
//若根节点无子结点
if (!node.left && !node.right) {
//直接判断根结点额值是否等于target
return node.value == target;
}
//若有子结点
else {
//如有左结点 左侧递归
if (!!node.left)
return hasPathSum(node.left, /*关键*/ target - node.value);
//如有右结点 右侧递归
if (!!node.right)
return hasPathSum(node.right, /*关键*/ target - node.value);
}
}
console.log(hasPathSum(bTree.getNode(0), 7)); //>> true
})();

看到笔者很快交出了答案,面试官小姐姐嘴角露出了一丝微笑。经过大概1分钟的review之后,她说:“二叉树BTree和结点Node本质上可以是相同的数据结构的,根结点其实就可以代表一棵树了,你可仔细想想。你的答题虽然实现了目的,但是不需要定义2个类来分别放树和结点,能否精简一下呢?”

的确,她说的对,完全可以精简。如果要求树和结点的数据结构一样,那可以考虑用数组的形式来表示它们,用多维数组来代表一颗二叉树即可,于是有了第二种解法。

解法二

(() => {
//用多维数组代表题中示例的树,这个数组是按“前序遍历”后排序
let arrSouce = [6, [2, [-1], [3]], [3, [0]]];
/**
* 定义二叉树的结点。根结点和树的数据结构一样,
* 因此只需要定义一个结点(Node)的数据结构即可
*/
class Node {
/**
* 构造函数
* @param {Number} value 当前结点的值
* @param {Node} left 左子结点
* @param {Node} right 右子结点
*/
constructor(value, left, right) {
if (value != undefined) {
this.value = value;
if (left != undefined) this.left = left;
if (right != undefined) this.right = right;
}
}
}
/**
* 创建一个树
* @param {Array} arr 一个代表二叉树的多维数组
*/
function makeBTree(arr) {
if (arr) {
if (arr.length == 1) {
return new Node(arr[0]);
}
//递归,创建二叉树
return new Node(arr[0], makeBTree(arr[1]), makeBTree(arr[2]));
}
}
//创建示例中的二叉树,简洁多了!
let bTree = makeBTree(arrSouce);
/**
* 主逻辑函数,与第一种解法里代码一样
* @param {Node} node 树的结点
* @param {Number} target 题目要求的目标值
*/
function hasPathSum(node, target) {
//若根节点无子结点
if (!node.left && !node.right) {
//直接判断根结点额值是否等于target
return node.value == target;
}
//若有子结点
else {
//如有左结点 左侧递归
if (!!node.left)
return hasPathSum(node.left, /*关键*/ target - node.value);
//如有右结点 右侧递归
if (!!node.right)
return hasPathSum(node.right, /*关键*/ target - node.value);
}
}
console.log(hasPathSum(bTree, 7)); //>> true
})();

面试官小姐姐很满意,在填写面试评估的那一栏里写下了“通过技术面试”。

贰.1.5.3 本篇结语

由此可见,在一线互联网公司面试,碰到数据结构和算法这块儿的面试题是避免不了的,而且考的是触类旁通,题目都是基础数据结构和算法的变体,所以更要扎扎实实把最常见的数据结构和算法题研究熟悉,你一定可以顺利通过技术面试。