es6 解构面试题
这是系列文章中的第二篇,我将利用我在Google担任工程师和面试官的经验,分享对应聘科技公司候选人的建议。 如果还没有,请看一下本系列的介绍 。
在开始之前,我有一个免责声明:面试应聘者是我的专业职责之一,但此博客代表了我的个人观察,个人轶事和个人见解。 请不要将此误认为是Google,Alphabet或任何其他个人或组织的官方声明。
这是我在面试生涯中使用的第一个问题,也是第一个泄漏和被禁止的问题。 我喜欢它,因为它有很多优点:
如果您是一名学生或以其他方式申请技术工作,我希望您能从阅读本书中了解对面试问题的期望。 如果您是一名面试官,我想分享一下我的面试过程和风格,以更好地告知他人和征求意见。
注意,我将用Python编写代码。 我喜欢Python,因为它易于学习,紧凑且具有绝对庞大的标准库。 候选人也喜欢它:尽管我们没有施加语言限制,但我采访的90%的人还是使用Python。 我也使用Python 3,因为来吧,那是2018年。
假设您将骑士棋子放在电话拨号盘上。 此棋子以大写“ L”形移动:水平两步,然后垂直一步,或者水平一步,然后垂直两步:
假设您仅使用骑士能跳的键盘来拨打键盘上的按键。 骑士每次按下某个键时,我们都会拨该键并进行另一跳。 起始位置计为已拨。
您可以从一个特定的起始位置拨入N个跃点多少个不同的号码?
我进行的每次面试基本上都分为两个部分:首先,我们找到一种算法解决方案,然后候选人用代码来实现它。 我说“我们”是找到解决方案,因为我不是沉默寡言的观众:45分钟的时间并不是在最佳情况下设计和实施任何东西的时间,也不必在压力之下。 我让求职者带头进行讨论,提出想法,解决问题实例等,但是我很乐意向正确的方向提出建议。 候选人越好,我倾向于给出的提示就越少,但是我还没有看到候选人根本不需要我提供任何意见。
我应该强调这一点,因为它很重要:作为一名面试官,我不应该袖手旁观,看着别人失败。 我想写尽可能多的积极反馈,我尝试给您机会让我写关于您的好事。 提示是我说的方式:“好吧,我会向您介绍这一点,但只有这样您才能继续前进,并向我展示您对问题其他部分的理解。”
话虽这么说,您在听完问题后的第一步应该是走上白板,然后手工解决问题的小实例。 切勿深入研究代码! 解决小实例可以让您发现图案,观察到的情况和边缘情况,还可以使解决方案明确化。 例如,假设您从6开始并进行两次跳跃。 您的顺序将是…
…总共六个序列。 如果您要继续学习,请尝试拿铅笔和纸将它们推导出来。 这并不能很好地转化为博客文章,但是当我说手工解决问题时有些神奇之处,这可以使我获得很多见识,而不仅仅是凝视并静静思考。
综上所述,您可能会想到一个解决方案。 但是在我们到达那里之前...
当我开始使用此问题时,令我感到惊讶的是,候选人经常被困在计算我们从给定位置(也称为邻居)可以跳到的键上的时间。 我的建议是:如有疑问,请写一个空的占位符,并询问面试官以后是否可以实施。 这个问题的复杂性不在于邻居的计算; 我正在注意您对整数的计算方式。 有效浪费在邻居计算上的任何时间。
我会接受“让我们假设有一个可以给我邻居的功能”以及以下存根。 当然,我可能会要求您再做一遍并稍后再实施,但前提是我们有时间。 您可以像这样简单地编写一个存根并继续:
def neighbors(position):
...
而且,您不会因为要求使用存根而损失太多:如果问题的复杂性在其他地方,我将允许它。 如果没有,我将要求您实际实施。 我不介意应聘者没有意识到问题的复杂性所在,尤其是在他们可能尚未充分探讨问题的早期阶段。
至于这里的邻居函数,鉴于它永远不会改变,您可以简单地创建一个地图并返回适当的值:
编辑说明:我在此代码的原始版本中出错。 它曾经是 4: (3, 9, 0)
从那以后我已对其进行了更正。 对于那个很抱歉。
无论如何,继续解决。 也许您已经注意到可以通过枚举所有可能的数字并对其进行计数来解决此问题。 您可以使用递归来生成这些值:
这行之有效,这是我在采访中看到的一个共同起点。 但是请注意,我们会生成数字,并且从不实际使用它们。 此问题要求对数字进行计数 ,而不是数字本身。 一旦我们计算了一个数字,便再也不会重新访问它。 作为一般的经验法则,我建议您注意解决方案计算不使用的内容时的情况。 通常,您可以将其删除并获得更好的解决方案。 现在开始吧。
我们如何计算电话号码而不生成电话号码? 可以做到,但并非没有其他见识。 请注意,可以从N个跃点中的给定起始位置生成的数目计数等于可以从N-1个跃点中的每个邻居开始生成的跃点计数的总和。 用数学方式表示为递归关系,它看起来像这样:
当您考虑一跳会发生什么时,这在直觉上很明显:6有3个邻居(1、7和0),在零跳中,每个邻居可以到达一个号码,因此只能拨打三个号码。
您可能会问,如何获得这种见解? 如果您已经研究过递归,那么在白板上进行一些探索之后,这一点应该变得显而易见。 许多练习递归的应聘者立即注意到,此问题分解为较小的子问题,这是一个致命的礼物。 如果您正在接受我的采访,而您似乎无法获得这种见解,我通常会给您一些提示,以帮助您到达目的地,甚至可以在尝试失败时直接提供帮助。
一旦掌握了这些见解,您就可以继续前进并再次解决此问题。 有许多使用此事实的实现,但让我们从我在访谈中最常看到的实现开始:天真的递归方法:
而已! 将此与函数结合起来以计算邻居,就可以得出有效的解决方案! 此时,您应该轻拍一下自己的背部。 如果向下滚动,您会注意到我们还有很多基础,但这是一个里程碑。 生产任何可行的解决方案已经使您与众多候选人脱颖而出。
下一个问题是您将从我这里听到很多东西:该解决方案的Big-O复杂性是什么? 对于那些不知道的人,Big-O复杂度(非正式地)是一种速记,表示解决方案所需的计算量随输入大小而增长的速率。 对于此问题,输入的大小为跳数。 如果您对正确的数学定义感兴趣,可以在此处阅读更多内容。
对于此实现,每次对count_sequences()
调用都将递归调用count_sequences()
至少两次,因为每个键至少具有两个邻居。 由于我们递归的次数等于所需的跳数,并且每次调用对count_sequences()
的调用count_sequences()
至少翻倍,因此,我们的运行时复杂度至少为指数时间。
这不好。 要求增加一跳将使运行时间加倍,甚至不会增加三倍。 对于1到20之类的小数字,这是可以接受的,但是随着我们要求越来越多的啤酒花,我们遇到了麻烦。 例如,要等到宇宙热死后很久才能完成500跳。
我们可以做得更好吗? 仅使用上面的数学关系,不使用其他任何关系,不是真的。 我之所以喜欢这个问题,原因之一是它具有洞察力,可以提供越来越有效的解决方案。 为了找到下一个,让我们映射出该函数调用的函数。 让我们考虑count_sequences(6, 4)
。 注意为了简洁起见,我使用C
作为函数名称:
您可能会注意到一些奇怪的情况: C(6, 2)
调用执行了3次,每次执行相同的计算并返回相同的值。 这里的关键见解是,这些函数调用重复执行,每次返回相同的值。 计算完结果后,无需重新计算它们。
如果您想知道如何实现此目的,最简单的方法是通过良好的老式白板:盯着摘要中的此问题陈述很好,但我始终鼓励候选人在解决方案上提出示例解决方案板。 解决像这样的问题并像上面一样绘制树,将会看到您多次为C(6, 2)
6,2)编写子树,您一定会注意到。 有时,这足以使求职者完全绕开解决方案1和2,直接进入此阶段。 不用说,在一次访谈中,您只有45分钟的时间来解决问题,这可以节省大量时间。
有了这些见识,我们如何解决这个问题? 我们可以使用记忆化(不备忘录[R化),这基本上意味着我们的,我们以前见过的函数调用记录结果,并使用这些替代重做的工作。 这样,当我们在调用图中遇到不必要地重新计算整个子树的位置时,我们将立即返回已经计算出的结果。 这是一个实现:
好了,现在的运行时复杂度(Big-O)是多少? 这很难回答。 对于先前的实现,运行时间的计算就像计数递归函数被调用的次数一样简单,每次调用总是两次或三次。 由于递归调用由条件保护,因此计时更加复杂。 从表面上看,没有明显的方法可以计算函数调用。
我们可以通过查看缓存来解决这个难题。 每个函数调用的结果都存储在缓存中,并且仅在其中插入一次。 这使我们可以将问题重新构造为“缓存的大小如何随输入的大小增长?” 假定高速缓存由位置和跃点数作为关键字,并且恰好有十个位置,我们可以得出结论,高速缓存与请求的跃点数成正比增长。 这是基于信鸽原理的:一旦我们在缓存中为位置和跳转计数的每个组合输入了一个条目,所有调用都会命中缓存,而不是导致新的函数调用。
线性时间! 不错 实际上,这很了不起:添加简单的缓存将算法的运行时间从指数更改为线性。 在我那古老的MacBook Air上,递归实现大约需要45秒才能运行20跳。 此实现可以在大约50毫秒内处理500个跃点。 一点也不差。
这样我们做对了吗? 好吧,不完全是。 该解决方案具有两个缺点,一个主要(ish)和一个次要。 主要的缺点是它是递归的。 大多数语言都限制了它们的调用堆栈的最大大小,这意味着实现始终可以支持最大跳数。 在我的机器上,经过大约1000跳后它失败了。 这是一个主要的限制,而不是主要的限制,因为可以以迭代方式重新实现任何递归函数,但这仍然很麻烦。 至于次要的限制,这实际上使我们进入了下一个解决方案……
从上面查看递归关系时,递归备注解决方案的次要限制很明显:
请注意,N跳的结果仅取决于具有N-1跳的呼叫的结果。 同时,缓存包含每个(非零)跳数的条目。 我称这是一个小问题,因为考虑到缓存仅随跳数线性增长,实际上并不会引起任何实际问题。 要求线性空间不是世界末日,但仍然没有效率。
是什么赋予了? 同样,当您查看已写好的解决方案和代码时,结果将很明显。 请注意,代码以最大跳数开始,然后直接递归到最小跳数:
如果您将整个函数调用图想象成一种虚拟树,您会很快看到我们正在执行深度优先遍历。 很好,它提供了适当的解决方案,但没有利用我上面指出的浅依赖性属性。
您是否可以执行广度优先遍历,从顶部开始,只有在访问了N跳之后才“访问”函数调用N-1跳? 可悲的是没有。 非零跳的函数调用的值绝对需要较小跳数的值,因此,直到到达零跳层并开始返回数字,而不是其他函数调用,您才能得到任何结果(请注意零跳层不是此处未显示)。
但是,您可以颠倒顺序:仅在访问了具有N-1个跃点的图层之后才访问具有N个跃点的图层。 那些学习或正在研究离散数学的人将认识到归纳的所有必要成分:我们知道零跳函数调用的值始终为1(基本情况)。 我们还知道如何使用递归关系(归纳步骤)组合N-1个跃点值以获得N个跃点值。 我们可以从零跳的基本情况开始,并得出所有大于零的值。 这是一个实现:
那么,此版本比深度优先的递归解决方案更好吗? 不是一吨,但有一些好处。 首先,它不是递归的,这意味着它可以为很大的值运行而不会崩溃。 其次,它使用恒定的内存,因为它只需要两个固定大小的数组,而不需要备忘录解决方案不断增长的缓存。 最后,它仍然是线性时间:我可以在不到20秒的时间内计算出200,000跳。
这样我们就完成了,对吧? 差不多了 在工作面试中设计和实施线性时间恒定空间解决方案是一个很好的结果。 当我使用这个问题时,我给提供动态编程解决方案的候选人一个很好的评价。
您可能会问其他解决方案呢? 不幸的是,不可能给抽象的候选人打分。 面试是很混乱的事情。 他们可能会迟到,人们可能会感到紧张,并且他们常常在会议后期才能获得见解和解决方案,从而使他们几乎没有时间编写任何代码。 也有一场对话在发生:我注意应聘者如何交流自己的想法,并融合想法和反馈。 在提出聘用/不聘用建议之前,我总是将这些因素考虑在内,而您根本无法做到这一点。
除了潜在的建议,我将重点放在我想说的事情上。 在评估算法和数据结构时,我想说的是“ TC(候选人)探索了这个问题并提出了解决所有极端情况的解决方案,并在出现缺点时对其进行了改进。 最后,他们得出了最佳解决方案。 我还想说一句“ TC为解决方案选择了合适的数据结构,并正确回答了有关解决方案运行时间和空间要求的Big-O问题。 ”
在评估编码时,我的理想选择是“ TC快速,简洁地将他们的想法转化为代码。 该代码使用标准的语言构造,并且易于阅读。 解决了所有的极端情况,TC遍历了他们的代码以对其进行调试并验证其正确性。 对于入门级角色,如果进行某种测试,我会给您加分,但是经验更丰富的角色会惩罚那些至少没有列出相关测试用例的候选人。
至于进步的速度,我很想能够说:“ TC推动了问题的解决过程:他们开发了自己的大多数解决方案,并且能够找出并解决缺点而无需我指出。 TC只需极少的提示即可使它们朝正确的方向发展。 ”
我可以说这些话的任何人都可以在我的书中得到“坚强的聘用”。 但是,“雇用”和“聘用”也是正面认可。 如果您在一个领域中表现欠佳,但在另一个方面却表现出色,那么我可能仍然可以提出一个积极的建议。
这个问题似乎令人生畏,尤其是考虑到这个职位已经成立了那么久。 但是请记住,这篇文章的目的是比任何面试都要彻底得多。 我并没有列出我希望看到的所有内容,而是将一个问题分解为最详细的细节,以至于什么也没保留。
为此,这里列出了此问题涵盖的技能和您应养成的习惯:
如果您喜欢这篇文章,请 称赞或 发表评论 ! 没有什么能让我感到内在的温暖和模糊,就像从读者那里听到的那样。 另外,如果这是您喜欢阅读的东西,并且如果您一路走到这里,那么很有可能,请 跟随我 ! 这有更多的来源。
好的,我说我们已经完成了,但是事实证明,这个问题还有一个解决方案。 在采访这个问题的所有时间中,我从未见过有人提供过这个问题。 我什至不知道它的存在,直到我的一位同事带着震惊的表情回到他的办公桌,并宣布他刚刚面试了他见过的最好的候选人。
我将很快发布详细的后续消息,但与此同时,我会让大家都想知道如何在对数时间内解决此问题……
翻译自: https://hackernoon.com/google-interview-questions-deconstructed-the-knights-dialer-f780d516f029
es6 解构面试题