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

往期回顾:
[见题拆题] 大厂面试算法真题解析 - 第一期开张


这不是标题党,今天我们真的要上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的驾驶座~

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

[转载请注明]


欢迎关注课程:

一站式学习Java网络编程 全面理解BIO/NIO/AIO

全面掌握MongoDB4.0 完成从小白到达人的蜕变

MongoDB 4.0新特性

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • 如今想要收获大厂offer,在面试的前几轮,总是躲不开算法这座大山。 常听人说,算法很难。这话没错。算法本身是是一...
    _Stannum_阅读 673评论 2 6
  • 1.把二元查找树转变成排序的双向链表 题目: 输入一棵二元查找树,将该二元查找树转换成一个排序的双向链表。 要求不...
    曲终人散Li阅读 3,293评论 0 19
  • 在C语言中,五种基本数据类型存储空间长度的排列顺序是: A)char B)char=int<=float C)ch...
    夏天再来阅读 3,323评论 0 2
  • 小梦儿一直在家里的沙发上安安静静地等着我回家,每天陪伴我画画,心中十分高兴。 毕竟美是一种欣赏,是一种生活态度,就...
    梦舞蝶馨阅读 492评论 0 2
  • 我们都生活在自己思想观念的牢笼中,却浑然不觉。想要走出自己的观念,你首先要看到自己有观念,而且你的观念是阻挡你...
    洁思漫讲阅读 655评论 0 0