打赌你无法解决这个Google面试题

2024-01-16 08:48

本文主要是介绍打赌你无法解决这个Google面试题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

英文 |  https://medium.com/free-code-camp/bet-you-cant-solve-this-google-interview-question-4a6e5a4dc8ee

TechLead 的问题

在这个问题中,TechLead 要求我们观察下面的网格,并获取所有颜色相同的最大连续块的数目。

当我听到他的问题并看到图片时,我在想:“天哪,我必须做一些2D图像建模才能弄清楚这一点”,而这在面试中是几乎不可能的。

但是在他进一步解释之后,我发现事实并非如此。我们需要处理的是图像的数据,而不需要解析图像并转化为数据。

数据模型

在编写代码之前,我们需要定义数据模型。

在我们的案例中,TechLead 给了我们这些限制:

  • 我们将其称为彩色正方形或“节点”的概念

  • 我们的数据集中有1万个节点

  • 节点分为列和行(2D)

  • 列和行的数量可以不均匀

  • 节点具有颜色和某种表示邻接的方式

我们还可以从数据中获取更多信息:

  • 没有两个节点会重叠

  • 节点永远不会与它们自己相邻

  • 节点永远不会有重复的邻接关系

  • 位于侧面和角落的节点将分别缺少一两个相邻关系

我们不知道的是:

  • 行与列的比例

  • 可能的颜色数

  • 所有节点只有一种颜色的机会

  • 颜色的粗略分布

职级越高,您将要问的问题越多。这也不是经验问题。虽然这样做有帮助,但是如果您不能挑出未知数,也不会使您变得更好。

我并没有指望大多数人都能发现这些未知项。直到我开始研究算法时,我也不全都知道。未知项需要时间才能弄清楚。需要与人进行大量讨论并来回查阅题目。

看他的图像,似乎分布是随机的。他只使用了3种颜色,并且从不说其他任何东西,所以我们也一样。我们还将假设所有颜色都可能相同。

由于它可能会破坏我们的算法,因此我假设我们正在使用 100x100 的网格。这样,我们不必处理1行和 10K 列的极端情况。

在典型的情况下,我会在问题提出的最初几个小时内发现所有这些问题。这就是 TechLead 真正关心的问题。你讲直接开始编码?还是会先分析题目找出问题所在?

创建数据模型

我们数据的基本模块:

  • 颜色

  • ID

  • X

  • Y

ID是干什么的?它是每个网格的唯一标志。

由于是在网格中整理数据,因此我假设我们将使用X和Y值描述数据。仅使用这些属性,我就能生成一些HTML,以确保我们生成的内容就像 TechLead 给我们的一样。

我们使用绝对定位来完成:

也适用于更大的数据集:

这里是代码:

const generateNodes = ({numberOfColumns,numberOfRows,
}) => (Array(numberOfColumns* numberOfRows).fill().map((item,index,) => ({colorId: (Math.floor(Math.random() * 3)),id: index,x: index % numberOfColumns,y: Math.floor(index / numberOfColumns),}))
)

我们取出列和行,从项目数量中创建一维数组,然后根据该数据生成节点。

我使用的是 colorId,而不是 color。首先,因为随机化比较容易。其次,我们通常必须自己检查颜色值。

尽管他从未明确声明,但他只使用了3种颜色值。我也将数据集限制为3种颜色。只需知道它可能是数百种颜色,并且最终的算法就无需更改。

举一个简单的例子,这里是2x2节点列表:

[{ colorId: 2, id: 0, x: 0, y: 0 },{ colorId: 1, id: 1, x: 1, y: 0 },{ colorId: 0, id: 2, x: 0, y: 1 },{ colorId: 1, id: 3, x: 1, y: 1 },
]

数据处理

无论我们要使用哪种方法,我们都想知道每个节点的邻接关系。X和Y值不会减少。

因此,给定X和Y,我们需要弄清楚如何找到相邻的X和Y值。这很简单。我们只需在X和Y上找到正负1的节点即可。

我为此逻辑编写了一个辅助函数:

const getNodeAtLocation = ({nodes,x: requiredX,y: requiredY,
}) => ((nodes.find(({x,y,}) => (x === requiredX&& y === requiredY))|| {}).id
)

我们生成节点的方式实际上是一种数学方法,用来找出相邻节点的ID。相反,我假设节点将以随机顺序进入我们的系统。

我通过第二遍运行所有节点以添加邻接关系:

const addAdjacencies = (nodes,
) => (nodes.map(({colorId,id,x,y,}) => ({color: colors[colorId],eastId: (getNodeAtLocation({nodes,x: x + 1,y,})),id,northId: (getNodeAtLocation({nodes,x,y: y - 1,})),southId: (getNodeAtLocation({nodes,x,y: y + 1,})),westId: (getNodeAtLocation({nodes,x: x - 1,y,})),})).map(({color,id,eastId,northId,southId,westId,}) => ({adjacentIds: ([eastId,northId,southId,westId,].filter((adjacentId,) => (adjacentId !== undefined))),color,id,}))
)

我避免在此预处理程序代码中进行任何不必要的优化。它不会影响我们的最终性能数据,只会帮助简化算法。

我继续将 colorId 更改为另一种颜色。对于我们的算法而言,这完全没有必要,但我想使其更易于可视化。

我们为每组相邻的X和Y值调用 getNodeAtLocation并找到我们的northId,eastId,southId和westId。不再传递X和Y值,因为它们不再需要。

在获得基本ID后,我们将其转换为一个仅包含值的ID数组。这样,如果我们有边角,就不必担心检查这些ID是否为空。它还允许我们循环一个数组,而不是手动注释算法中的每个基本ID。

这是另一个2x2的示例,该示例使用通过 addAdjacencies 运行的一组新节点:

[{ adjacentIds: [ 1, 2 ], color: 'red',  id: 0 },{ adjacentIds: [ 3, 0 ], color: 'grey', id: 1 },{ adjacentIds: [ 3, 0 ], color: 'blue', id: 2 },{ adjacentIds: [ 1, 2 ], color: 'blue', id: 3 },
]

处理优化

我想大大简化本文的算法,因此添加了另一个优化过程。这将删除与当前节点颜色不匹配的相邻ID。
重写addAdjacencies函数之后,现在是这样的:

const addAdjacencies = (nodes,
) => (nodes.map(({colorId,id,x,y,}) => ({adjacentIds: (nodes.filter(({x: adjacentX,y: adjacentY,}) => (adjacentX === x + 1&& adjacentY === y|| (adjacentX === x - 1&& adjacentY === y)|| (adjacentX === x&& adjacentY === y + 1)|| (adjacentX === x&& adjacentY === y - 1))).filter(({colorId: adjacentColorId,}) => (adjacentColorId=== colorId)).map(({id,}) => (id))),color: colors[colorId],id,})).filter(({adjacentIds,}) => (adjacentIds.length > 0))
)

我减少了 addAdjacencies 的代码量,同时添加了更多功能。

通过删除颜色不匹配的节点,我们的算法可以100%确保 adjacentIds 中的ID都是连续的节点。

最后,我删除了没有相同颜色邻接关系的所有节点。这进一步简化了我们的算法,并且将总节点缩减为仅关心的节点。

错误的方式—递归

TechLead 表示我们无法递归执行此算法,因为我们遇到了堆栈溢出的情况。

尽管他部分正确,但有几种方法可以缓解此问题。迭代或使用尾递归。我们将以迭代示例为例,但是JavaScript不再具有尾部递归作为本机语言功能。

虽然我们仍然可以在JavaScript中模拟尾部递归,但我们将保持这种简单性,而是创建一个典型的递归函数。

在编写代码之前,我们需要弄清楚我们的算法。对于递归,使用深度优先搜索是有意义的。不必担心知道计算机科学术语。当我向他展示我想出的不同解决方案时,一位同事说过。

算法

我们将从节点开始,并尽我们所能直到到达终点。然后,我们将返回并采用下一个分支路径,直到我们扫描了整个连续块。

这就是其中的一部分。我们还必须跟踪我们去过的地方以及最大的连续街区的长度。

我所做的就是将我们的功能分为两部分。一个将拥有最大的列表和先前扫描的ID,同时至少每个节点循环一次。另一个将从未扫描的根节点开始,然后进行深度优先遍历。

函数长这样:

const getContiguousIds = ({contiguousIds = [],node,nodes,
}) => (node.adjacentIds.reduce((contiguousIds,adjacentId,) => (contiguousIds.includes(adjacentId)? contiguousIds: (getContiguousIds({contiguousIds,node: (nodes.find(({id,}) => (id=== adjacentId))),nodes,}))),(contiguousIds.concat(node.id)),)
)const getLargestContiguousNodes = (nodes,
) => (nodes.reduce((prevState,node,) => {if (prevState.scannedIds.includes(node.id)) {return prevState}const contiguousIds = (getContiguousIds({node,nodes,}))const {largestContiguousIds,scannedIds,} = prevStatereturn {largestContiguousIds: (contiguousIds.length> largestContiguousIds.length? contiguousIds: largestContiguousIds),scannedIds: (scannedIds.concat(contiguousIds)),}},{largestContiguousIds: [],scannedIds: [],},).largestContiguousIds
)

疯了吧?这个代码简直太长了。

接下来我们一步步来缩减代码

递归函数

getContiguousIds 是我们的递归函数,每个节点调用一次。每次都会返回更新的连续节点列表。

此功能只有一个条件:列表中是否已存在我们的节点?如果不是,请再次调用

getContiguousIds。返回时,我们将获得一个更新的连续节点列表,该列表将返回到化简器,并用作下一个相邻ID的状态。

您可能想知道我们在哪里向contiguousIds添加值。当我们将当前节点连接到contiguousIds上时,就会发生这种情况。每次进一步递归时,我们都确保在循环其相邻ID之前将当前节点添加到contiguousId列表中。

始终添加当前节点可确保我们不会无限递归。

循环

此功能的后半部分还将遍历每个节点一次。

我们在递归函数周围使用了reducer。此人检查我们的代码是否已被扫描。如果是这样,请继续循环,直到找到一个尚未存在的节点,或者直到退出循环为止。

如果尚未扫描我们的节点,请调用getContiguousIds并等待完成。这是同步的,但可能需要一些时间。

返回带有contiguousIds的列表后,请对照largestantContiguousIds列表进行检查。如果更大,则存储该值。

同时,我们将这些contiguousId添加到我们的scandIds列表中,以标记我们去过的地方。
当您看到所有布局时,这非常简单。

执行

即使有1万个节点+3种随机颜色,也不会遇到堆栈溢出问题。但如果只有一种颜色,那么将会将遇到堆栈溢出的情况。那是因为我们的递归函数要进行1万次递归。

顺序迭代

由于内存大于函数调用堆栈,因此我的下一个想法是在单个循环中完成整个操作。

我们将跟踪节点列表列表。我们将继续添加它们并将它们链接在一起,直到脱离循环为止。

此方法要求我们将所有可能的节点列表保留在内存中,直到完成循环为止。在递归示例中,我们仅在内存中保留了最大的列表。

const getLargestContiguousNodes = (nodes,
) => (nodes.reduce((contiguousIdsList,{adjacentIds,id,},) => {const linkedContiguousIds = (contiguousIdsList.reduce((linkedContiguousIds,contiguousIds,) => (contiguousIds.has(id)? (linkedContiguousIds.add(contiguousIds)): linkedContiguousIds),new Set(),))return (linkedContiguousIds.size > 0? (contiguousIdsList.filter((contiguousIds,) => (!(linkedContiguousIds.has(contiguousIds)))).concat(Array.from(linkedContiguousIds).reduce((linkedContiguousIds,contiguousIds,) => (new Set([...linkedContiguousIds,...contiguousIds,])),new Set(adjacentIds),))): (contiguousIdsList.concat(new Set([...adjacentIds,id,]))))},[new Set()],).reduce((largestContiguousIds = [],contiguousIds,) => (contiguousIds.size> largestContiguousIds.size? contiguousIds: largestContiguousIds))
)

另一个疯狂的。让我们从顶部开始进行分解。我们将每个节点循环一次。但是现在我们必须检查我们的id是否在节点列表的列表中:contiguousIdsList。

如果它不在任何contiguousId列表中,我们将添加它及其相邻ID。这样,在循环时,其他东西将链接到它。

如果我们的节点在其中一个列表中,则可能其中有很多。我们希望将所有这些链接在一起,并从contiguousIdsList中删除未链接的链接。

而已。

提出节点列表列表后,我们检查最大的节点列表,然后完成。

执行

与递归版本不同,当所有10K项都具有相同的颜色时,该函数确实会完成。

除此之外,它非常慢;比我最初预期的要慢得多。我忘了在效果评估中考虑循环列表列表,这显然会对性能产生影响。

随机迭代

我想将方法学放在递归方法的后面,并迭代地应用它。

我花了一整夜的时间试图记住如何动态更改循环中的索引,然后想起了while(true)。

自从我写了传统的循环已经很久了,我完全忘记了它。

现在我有了武器,我就开始进攻了。由于我花了很多时间试图加快可观察版本的速度(稍后会详细介绍),因此我决定将数据懒散地进行老式修改。

该算法的目标是精确击中每个节点一次,并且仅存储最大的连续块:

const getLargestContiguousNodes = (nodes,
) => {let contiguousIds = []let largestContiguousIds = []let queuedIds = []let remainingNodesIndex = 0let remainingNodes = (nodes.slice())while (remainingNodesIndex < remainingNodes.length) {const [node] = (remainingNodes.splice(remainingNodesIndex,1,))const {adjacentIds,id,} = nodecontiguousIds.push(id)if (adjacentIds.length > 0) {queuedIds.push(...adjacentIds)}if (queuedIds.length > 0) {do {const queuedId = (queuedIds.shift())remainingNodesIndex = (remainingNodes.findIndex(({id,}) => (id === queuedId)))}while (queuedIds.length > 0&& remainingNodesIndex === -1)}if (queuedIds.length === 0&& remainingNodesIndex === -1) {if (contiguousIds.length> largestContiguousIds.length) {largestContiguousIds = contiguousIds}contiguousIds = []remainingNodesIndex = 0if (remainingNodes.length === 0) {break}}}return largestContiguousIds
}module.exports = getLargestContiguousNodesIterative2

即使我像大多数人一样写了这篇文章,但到目前为止,它的可读性最差。我什至无法告诉您这是怎么回事,除非自己先自上而下地进行。

我们没有添加到以前扫描的ID列表中,而是从剩余的Nodes数组中拼接出了值。

懒!我永远不会建议自己这样做,但是我在创建这些样本的最后阶段想尝试一些不同的尝试。

性能对比

随机颜色

耗时方法
229.481msRecursive
272.303msIterative Random
323.011msIterative Sequential
391.582msRedux-Observable Concurrent
686.198msRedux-Observable Random
807.839msRedux-Observable Sequential

单一颜色

耗时方法
1061.028msIterative Random
1462.143msRedux-Observable Random
1840.668msRedux-Observable Sequential
2541.227msIterative Sequential

本文完~

这篇关于打赌你无法解决这个Google面试题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/611979

相关文章

三国地理揭秘:为何北伐之路如此艰难,为何诸葛亮无法攻克陇右小城?

俗话说:天时不如地利,不是随便说说,诸葛亮六出祁山,连关中陇右的几座小城都攻不下来,行军山高路险,无法携带和建造攻城器械,是最难的,所以在汉中,无论从哪一方进攻,防守方都是一夫当关,万夫莫开;再加上千里运粮,根本不需要打,司马懿只需要坚守城池拼消耗就能不战而屈人之兵。 另一边,洛阳的虎牢关,一旦突破,洛阳就无险可守,这样的进军路线,才是顺势而为的用兵之道。 读历史的时候我们常常看到某一方势

如何解决线上平台抽佣高 线下门店客流少的痛点!

目前,许多传统零售店铺正遭遇客源下降的难题。尽管广告推广能带来一定的客流,但其费用昂贵。鉴于此,众多零售商纷纷选择加入像美团、饿了么和抖音这样的大型在线平台,但这些平台的高佣金率导致了利润的大幅缩水。在这样的市场环境下,商家之间的合作网络逐渐成为一种有效的解决方案,通过资源和客户基础的共享,实现共同的利益增长。 以最近在上海兴起的一个跨行业合作平台为例,该平台融合了环保消费积分系统,在短

荣耀嵌入式面试题及参考答案

在项目中是否有使用过实时操作系统? 在我参与的项目中,有使用过实时操作系统。实时操作系统(RTOS)在对时间要求严格的应用场景中具有重要作用。我曾参与的一个工业自动化控制项目就采用了实时操作系统。在这个项目中,需要对多个传感器的数据进行实时采集和处理,并根据采集到的数据及时控制执行机构的动作。实时操作系统能够提供确定性的响应时间,确保关键任务在规定的时间内完成。 使用实时操作系统的

pip-tools:打造可重复、可控的 Python 开发环境,解决依赖关系,让代码更稳定

在 Python 开发中,管理依赖关系是一项繁琐且容易出错的任务。手动更新依赖版本、处理冲突、确保一致性等等,都可能让开发者感到头疼。而 pip-tools 为开发者提供了一套稳定可靠的解决方案。 什么是 pip-tools? pip-tools 是一组命令行工具,旨在简化 Python 依赖关系的管理,确保项目环境的稳定性和可重复性。它主要包含两个核心工具:pip-compile 和 pip

【VUE】跨域问题的概念,以及解决方法。

目录 1.跨域概念 2.解决方法 2.1 配置网络请求代理 2.2 使用@CrossOrigin 注解 2.3 通过配置文件实现跨域 2.4 添加 CorsWebFilter 来解决跨域问题 1.跨域概念 跨域问题是由于浏览器实施了同源策略,该策略要求请求的域名、协议和端口必须与提供资源的服务相同。如果不相同,则需要服务器显式地允许这种跨域请求。一般在springbo

一些其他面试题

阿里二面:那你来说说定时任务?单机、分布式、调度框架下的定时任务实现是怎么完成的?懵了。。_哔哩哔哩_bilibili 1.定时算法 累加,第二层每一个格子是第一层的总时间400 ms= 20 * 20ms 2.MQ消息丢失 阿里二面:高并发场景下引进消息队列有什么问题?如何保证消息只被消费一次?真是捏了一把汗。。_哔哩哔哩_bilibili 发送消息失败

速盾高防cdn是怎么解决网站攻击的?

速盾高防CDN是一种基于云计算技术的网络安全解决方案,可以有效地保护网站免受各种网络攻击的威胁。它通过在全球多个节点部署服务器,将网站内容缓存到这些服务器上,并通过智能路由技术将用户的请求引导到最近的服务器上,以提供更快的访问速度和更好的网络性能。 速盾高防CDN主要采用以下几种方式来解决网站攻击: 分布式拒绝服务攻击(DDoS)防护:DDoS攻击是一种常见的网络攻击手段,攻击者通过向目标网

消除安卓SDK更新时的“https://dl-ssl.google.com refused”异常的方法

消除安卓SDK更新时的“https://dl-ssl.google.com refused”异常的方法   消除安卓SDK更新时的“https://dl-ssl.google.com refused”异常的方法 [转载]原地址:http://blog.csdn.net/x605940745/article/details/17911115 消除SDK更新时的“

zookeeper相关面试题

zk的数据同步原理?zk的集群会出现脑裂的问题吗?zk的watch机制实现原理?zk是如何保证一致性的?zk的快速选举leader原理?zk的典型应用场景zk中一个客户端修改了数据之后,其他客户端能够马上获取到最新的数据吗?zk对事物的支持? 1. zk的数据同步原理? zk的数据同步过程中,通过以下三个参数来选择对应的数据同步方式 peerLastZxid:Learner服务器(Follo

ORACLE 11g 创建数据库时 Enterprise Manager配置失败的解决办法 无法打开OEM的解决办法

在win7 64位系统下安装oracle11g,在使用Database configuration Assistant创建数据库时,在创建到85%的时候报错,错误如下: 解决办法: 在listener.ora中增加对BlueAeri-PC或ip地址的侦听,具体步骤如下: 1.启动Net Manager,在“监听程序”--Listener下添加一个地址,主机名写计