前端社招手撕代码
前言
最近面试了武汉的作业帮,一面二面很顺利以深挖项目为主,手撕代码给了两道很简单的题,比拼多多简单。
一面
一面给了一道求素数的题,当时惊到了我了,没想到这么简单,我直接实现了双循环暴力求解。
面试官问可以继续优化吗,我加了个if条件。
面试结束后我想起来了,这是大学谭浩强C语言那本书里面的老经典,优化方法就是限定内循环范围为根号n,以及只对已经求得的素数取模。
1. 暴力法
暴力法是最直接的方法,通过遍历每个数字并检查它是否为素数来构建数组。
function getPrimes(n) {
const primes = [];
for (let i = 2; i <= n; i++) {
let isPrime = true;
for (let j = 2; j < i; j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) primes.push(i);
}
return primes;
}
优点:
- 实现简单,容易理解。
缺点:
- 时间复杂度较高,为O(n²),效率较低。
2. 优化暴力法
对暴力法进行优化,减少不必要的检查。例如,只需要检查到√i即可。
function getPrimes(n) {
const primes = [];
for (let i = 2; i <= n; i++) {
let isPrime = true;
for (let j = 2; j * j <= i; j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) primes.push(i);
}
return primes;
}
优点:
- 时间复杂度降低到O(n√n),效率有所提升。
缺点:
- 仍然需要逐个检查每个数字。
3.暴力继续优化
素数其实就是只能拆成n=1*i,不能继续拆,素的不能再素。所以内循环可以只循环到根号n。那么其实还可以再缩小范围,因为根号n其实也可以是两个素数相乘,可以再拆。那么内循环只只需要遍历根号n以内的素数即可。
function getPrimes(n) {
const primes = [];
for (let i = 2; i <= n; i++) {
let isPrime = true;
for (let j = 0; j < primes.length && primes[j] * primes[j] <= i; j++) {
if (i % primes[j] === 0) {
isPrime = false;
break;
}
}
if (isPrime) primes.push(i);
}
return primes;
}
这种方法的时间复杂度仍然接近O(n√n),但实际运行效率会更高,因为减少了不必要的检查。
示例运行
假设n = 20,运行过程如下:
- 初始时
primes = []。 - 检查
i = 2,没有素数可以检查,直接加入primes,此时primes = [2]。 - 检查
i = 3,3 % 2 !== 0,加入primes,此时primes = [2, 3]。 - 检查
i = 4,4 % 2 === 0,不是素数。 - 检查
i = 5,5 % 2 !== 0且5 % 3 !== 0,加入primes,此时primes = [2, 3, 5]。 - 以此类推,最终结果为
[2, 3, 5, 7, 11, 13, 17, 19]。
4. 递归实现
虽然不常见,但也可以通过递归的方式实现素数筛选。
function isPrime(num) {
if (num <= 1) return false;
for (let i = 2; i * i <= num; i++) {
if (num % i === 0) return false;
}
return true;
}
function getPrimes(n) {
if (n < 2) return [];
const primes = getPrimes(n - 1);
if (isPrime(n)) primes.push(n);
return primes;
}
优点:
- 代码简洁,逻辑清晰。
缺点:
- 递归深度可能较大,效率较低。
二面
二面给了一道判断模块是否出现循环依赖的题。判断模块是否出现循环依赖是一个典型的图论问题,可以通过检测图中是否存在环来解决。在这个场景中,每个模块可以看作图中的一个节点,而模块的依赖关系可以看作是有向边。如果图中存在环,则说明存在循环依赖。
你说得有道理,使用一个Set来记录访问过的模块确实可以用于检测循环依赖,但这种方法需要结合递归调用和回溯机制来确保正确性。不过,这种方法在逻辑上相对简单,但需要注意一些细节,比如如何处理回溯以及如何避免误判。
使用Set实现循环依赖检测
我们可以使用Set来记录当前路径中访问过的模块。如果在递归过程中遇到已经在Set中的模块,说明存在循环依赖。递归结束后,需要从Set中移除当前模块,以便不影响其他路径的判断。
以下是实现代码:
function hasCircularDependency(modules) {
// 构建图的邻接表
const graph = {};
modules.forEach(module => {
graph[module.name] = module.dependencies;
});
// 用于记录当前路径中访问过的模块
const visiting = new Set();
// 深度优先搜索
function dfs(node) {
if (visiting.has(node)) return true; // 如果当前模块已经在访问路径中,说明存在循环依赖
if (!graph[node]) return false; // 如果没有依赖,直接返回
visiting.add(node); // 将当前模块加入访问路径
for (const neighbor of graph[node]) {
if (dfs(neighbor)) return true; // 递归检查依赖模块
}
visiting.delete(node); // 回溯:移除当前模块
return false;
}
// 遍历所有模块
for (const module of modules) {
if (dfs(module.name)) return true; // 如果从某个模块开始发现循环依赖,直接返回
}
return false; // 如果所有模块都遍历完成,没有发现循环依赖
}
// 示例用法
const modules = [
{ name: "A", dependencies: ["B", "C"] },
{ name: "B", dependencies: ["D"] },
{ name: "C", dependencies: ["B"] },
{ name: "D", dependencies: ["A"] }
];
console.log(hasCircularDependency(modules)); // 输出:true
关键点解析
-
visiting的作用:visiting是一个Set,用于记录当前递归路径中访问过的模块。- 如果在递归过程中遇到已经在
visiting中的模块,说明存在循环依赖。
-
回溯机制:
- 在递归调用结束后,需要从
visiting中移除当前模块,以便不影响其他路径的判断。 - 这是通过
visiting.delete(node)实现的。
- 在递归调用结束后,需要从
-
递归终止条件:
- 如果当前模块没有依赖(
graph[node]不存在或为空),直接返回false。 - 如果在递归过程中发现循环依赖,直接返回
true。
- 如果当前模块没有依赖(
示例分析
对于输入的模块数组:
[
{ name: "A", dependencies: ["B", "C"] },
{ name: "B", dependencies: ["D"] },
{ name: "C", dependencies: ["B"] },
{ name: "D", dependencies: ["A"] }
]
- 构建的图结构为:
{ A: ["B", "C"], B: ["D"], C: ["B"], D: ["A"] } - DFS遍历过程中:
- 从模块
A开始:- 访问
B,visiting = {A, B}。 - 访问
D,visiting = {A, B, D}。 - 访问
A,发现A已经在visiting中,说明存在循环依赖。
- 访问
- 从模块
优点:
- 实现相对简单,逻辑清晰。
- 使用
Set可以快速判断模块是否在当前路径中。
缺点:
- 需要手动处理回溯,稍不注意可能会遗漏。
- 如果模块数量非常多,递归深度可能会较大,可能会导致栈溢出。
三面
三面是给定m个数组,每个数组按照升序排列,现在要求从两个不同的数组取两个整数,求绝对值。要求绝对值最大。
输入:[[1,2,3],[4,5],[1,2,3]],
输出:4
解释:取1和5,求绝对值
解题思路
我们需要从多个已排序的数组中找到两个不同数组中的元素,使得它们的差的绝对值最大。为了高效地找到这个最大距离,可以考虑以下步骤:
- 遍历所有数组:记录每个数组的最大值和最小值,以及它们所在的数组索引。
- 比较不同数组的极值:通过比较不同数组之间的最大值和最小值,找到可能的最大距离。
- 处理特殊情况:例如当所有数组的最大值和最小值都在同一个数组中时,需要找到次大的最大值或次小的最小值。
解决代码
function maxDistance(arrays) {
let max1 = -Infinity, min1 = Infinity;
let maxIndex = -1, minIndex = -1;
let max2 = -Infinity, min2 = Infinity;
for (let i = 0; i < arrays.length; i++) {
const arr = arrays[i];
const currentMax = arr[arr.length - 1];
const currentMin = arr[0];
if (currentMax > max1) {
max2 = max1;
max1 = currentMax;
maxIndex = i;
} else if (currentMax > max2) {
max2 = currentMax;
}
if (currentMin < min1) {
min2 = min1;
min1 = currentMin;
minIndex = i;
} else if (currentMin < min2) {
min2 = currentMin;
}
}
if (maxIndex !== minIndex) {
return max1 - min1;
} else {
return Math.max(max1 - min2, max2 - min1);
}
}
// 示例1
const arrays1 = [[1,2,3],[4,5],[1,2,3]];
console.log(maxDistance(arrays1)); // 输出4
// 示例2
const arrays2 = [[1],[1]];
console.log(maxDistance(arrays2)); // 输出0
代码解释
- 初始化变量:
max1和min1分别记录当前遇到的最大值和最小值,maxIndex和minIndex记录它们所在的数组索引。max2和min2用于记录次大的最大值和次小的最小值。 - 遍历数组:对于每个数组,获取其最大值(数组最后一个元素)和最小值(数组第一个元素),并更新相关变量。
- 比较极值:如果最大值和最小值来自不同的数组,直接返回它们的差值。否则,比较最大值与次小值的差和次大值与最小值的差,取较大者作为结果。
这种方法确保我们能够高效地找到不同数组中的元素对的最大距离,同时处理了各种特殊情况。