为了账号安全,请及时绑定邮箱和手机立即绑定

[见题拆题] 大厂面试算法真题解析 - 第二期上车


这不是标题党,今天我们真的要上Uber的车~

题目

[来自Uber]

Given an array of integers, return a new array such that each element at index i of the new array is the product of all the numbers in the original array except the one at i

给定一个整数数组,返回一个新数组,使得位于位置i的元素,等于给定数组中位置i以外的所有元素的乘积。

举个例子:
如果给定的数组是[1, 2, 3, 4, 5],那么你输出的数组就应该是[120, 60, 40, 30, 24]
如果给定的数组是[3, 2, 1],那么你返回的数组就是[2, 3, 6]

解析

将题干中的叙述直接翻译成代码就可以得到最“淳朴”的暴力解法了(为了表达的方便,在以下伪码中,result代表算法返回的新数组,并且新数组中每一个元素的初始值都是1):

function arrayOfProducts(array, result):  
for i=1 to n  
 for j=1 to n 
   if j != i 
     result[i] = result[i] * array[j]
return result  

在计算新数组中每一个元素的时候,我们都要遍历给定数组,以便求出相应的乘积,这样一来,暴力解法的时间复杂度就定格在了缓慢的O(n^2)

观察暴力解法的计算过程,反复遍历给定数组显然是最浪费时间的步骤。头脑灵活的小伙伴肯定已经想到了:为什么不用除法来计算新数组中元素的值呢!

####优化
对于一个给定的数组,只要遍历一次,就可以得到整个数组全部元素的乘积。如果我们用a[i]来代表给定数组中位于位置i的元素,P代表给定数组中全部元素的乘积;用t[i]来代表新数组中位于位置i的元素;那么我们可以用下面这个表达式来表达t[i]: t[i] = P / a[i]

有了这个简洁漂亮的表达式,我们就可以改写暴力解法的“丑陋”伪码了:

function arrayOfProducts(array, result):
let P = 1
for i=1 to n
  P = P * array[i]
for j=1 to n
  result[j] = P / array[j]
return result

我们只需要遍历两次给定数组,就可以生成新数组了。所以经过优化的算法,它的时间复杂度直接下降为O(n)

那么我们就大功告成了吧?且慢!在上一期的文章里,我们已经强调过,在进行算法面试时,务必要考虑输入、输出以及操作中的边缘情况、极端情况和异常情况。

####改进
在除法运算中,最常见的一种异常情况,就是除数为零所造成的运算错误。重读一遍题目,给定的数组是一个整数数组,所以数组中出现等于零的元素是很合理的情况。而当这种情况出现的时候,我们的优化算法就会抛出异常,无法给出正确答案了!为了挽救我们的面试,接下来,我们就要进一步改写算法,来正确的应对元素值等于零的特殊情况。

当给定数组a的所有元素乘积P不等于零时,我们可以确定a中不包含任何等于零的元素,此时我们可以放心的使用上面讲解过的优化算法。

而当P等于零时,则说明a中包含至少一个等于零的元素,这时我们就需要新的方法来处理题目了。

在这种情况下,我们仍然要在遍历a数组的过程中来生成新数组t。如果a[i]不等于零,那就说明t[i]一定等于零,因为t[i]的值是a中除了a[i]以外所有元素的乘积,而这些元素一定包含至少一个等于零的元素,不然总乘积P就不会等于零了。这个情况其实也可以用我们上面讲过的优化解法来解答:t[i] = P / a[i] = 0 / a[i] = 0

而如果a[i]等于零,那么由于除数不能为零的原则,我们无法使用t[i] = P / a[i]的表达式来计算t[i],而只能像暴力解法那样,遍历一遍数组a,使用乘法运算来计算出t[i]的值了。

这样一来,大家可能就有点泄气了。辛苦了半天,我们怎么又回到暴力解法去了… 那我们这个改进版的“优化”解法的时间复杂度是不是又退化到O(n^2)了?其实不是的。让我们再仔细的看一下解法。在我们遇到了第一个等于零的a[i],并且使用乘法运算计算出了t[i]的值之后,我们就没有必要继续计算t中其他元素的值了。因为数组t中余下的所有元素都必定会因为乘以a[i]这个等于零的值而变成零。也就是说,在我们遇到第一个等于零的a[i]之后,我们就可以停止遍历数组a,而将数组t中剩余的元素全部设为零就大功告成了。所以,这个改进后的算法,至多也不过是多遍历了一次数组a而已。算法的整体时间复杂度仍然保持在O(n)

思路理清了,接下来就让我们上伪码:

function arrayOfProducts(array, result):
let P = 1
for i=1 to n
  P = P * array[i]
let j = 1
while j <= n
  if array[j] != 0
    result[j] = P / array[j]
    j++
  else
    for k=1 to n
      if k != j
        result[j] = result[j] * array[k]
     break
for l=j+1 to n
  result[l] = 0
return result

算法优化过了,异常情况也处理了,听完你眉飞色舞的讲解,面试官频频点头。正在你松了一口气的时候,面试官的嘴角露出了一丝狡黠的微笑~事情并没有那么简单,面试官问到:如果不允许你使用除法运算呢?


别慌!在算法面试中,面试官常常会在你给出了一个优化解法之后,再提出更高的要求,或是再改动题目的某些条件,使得题目的难度加大。这样不仅可以进一步考察候选人的解题能力,还能测试候选人在突然出现的压力之下,是否能保持冷静思考的能力。

既然除法运算被禁止了,那我们就再回归到暴力解法的乘法运算,想一想除了利用除法运算来减少遍历给定数组的次数之外,还有什么其他方法可以避免重复的遍历给定数组呢?

在算法设计过程中,有一个一般原则,就是**“用空间换时间”**。也就是说,在条件允许的情况下,可以尝试计算和存储更多的“中间值”,或是使用更复杂的数据结构,来达到缩短运算时间的目的。这道题目并没有限制解法的空间复杂度,所以我们不妨使用“以空间换时间”这个思路来试一试。

举个例子来分析,假设给定的数组包含四个元素,即a = [ a1, a2, a3, a4 ]
那么最终生成的新数组就应该是t = [ a2*a3*a4, a1*a3*a4, a1*a2*a4, a1*a2*a3 ]
a直接跳跃到t(并且不能使用除法运算)需要花费较长的时间。那么有没有可能在这中间增加一些“中间值”,以便来加速我们的算法呢?

让我们试着把t中的元素分解一下。t数组中有些元素是a数组中几个相邻的元素的乘积,比如说t1 = a2*a3*a4。还有些元素包含了a中几个不相邻的元素的乘积,比如说t2 = a1*a3*a4。如果我们根据a中元素是否相邻这个标准,把t的元素分解一下,那么t数组就会变成这样:
t1 = 1 * (a2*a3*a4)
t2 = a1 * (a3*a4)
t3 = (a1*a2) * a4
t4 = (a1*a2*a3) * 1

根据这个分解,如果我们可以事先准备好两个数组,一个是left = [ 1, a1, a1*a2, a1*a2*a3 ],另一个是right = [ a2*a3*a4, a3*a4, a4, 1 ],那么我们只要将leftright中的元素分别相乘,就可以得到最终的数组t了!

这里的leftright两个数组,就是我们为了降低暴力解法的时间复杂度,而计算出的“中间值”。只要遍历给定数组a两次,就可以计算出这两个中间值的数组了:第一次从左到右遍历a,生成left = [ 1, a1, a1*a2, ... , a1*a2*...*a(n-1) ];第二次从右到左遍历a,生成right = [ a2*...*a(n-1)*an, ... , a(n-1)*an, an, 1 ]。所以,这个算法最终的空间复杂度和时间复杂度都是O(n),和可以使用除法运算的解法一样快,完美应对了面试官的“刁难”~

####总结
这道题目和上一期Google的题目有异曲同工之妙。它们都不涉及像动态规划那样“隆重”的解题套路,但是题目的编排中却有不少陷阱等着你。
一开始使用除法运算来优化暴力解法是题目的第一道关卡,不过这个关卡难度不高,相信大部分应聘者都能答出来。
而第二个关卡就是对于“除数可能为零”这个异常情况的处理。经验少的应聘者很可能栽倒在这一关。成功优化了暴力解法所带来的兴奋,很容易让应聘者忽视了思维的缜密性,这样你就要输给更冷静更严谨的竞争对手啦。
而题目最后加上的限制条件(禁止使用除法运算)则是直线拉升了题目的难度。坦白地说,最后这一个优化算法确实有难度,如果有应聘者在面试的紧张状态下能讲出这个解法,那我只能说佩服佩服,你值得坐上Uber的驾驶座~

以上就是这一期的算法拆解。大家跟着一起做题辛苦啦~好好总结一下吧~

[转载请注明]


欢迎关注课程:

点击查看更多内容
7人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
软件工程师
手记
粉丝
800
获赞与收藏
923

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消