去年离职的一位同事,今天突然出现在公司,嘴上说“我又回来上班了”,同事们一个个面无表情,没有回应。
我在网上看到个帖子:去年离职的同事,今天突然杀回公司,进门先来一套“我又回来上班了”,笑得跟过年串门似的。结果全办公室安静得能听见键盘喘气,大家头都没抬,场面那叫一个干。
这事吧,真不稀奇。很多人走的时候都像开了无敌模式,嫌这嫌那,觉得外面遍地机会,老板看了都得主动递工牌。可真出去转一圈才发现,行情这东西,不是你自信它就给你面子。
回来上班不丢人,谁还没看走眼的时候。丢人的是当初把话说太满,像是要去收购全世界,结果四个月后自己先被现实优化了。
职场这地方能安稳发工资,已经算很实在了。
算法题:有序转化数组
有序转化数组这题,别一上来就平方排序
这题刚看题面容易走偏:给你一个有序数组 nums,再给一个二次函数 f(x)=ax^2+bx+c,要求把每个元素变换后,结果仍然按升序返回。
很多人第一反应是这样写:
defsort_transformed_array(nums, a, b, c):
arr = [a * x * x + b * x + c for x in nums]
arr.sort()
return arr
能过,大概率也没毛病,但这题真正想考的不是函数怎么套,而是你能不能利用“原数组本身有序”这个条件,把排序省掉。
先看现象。
如果 a > 0,抛物线开口朝上,越靠两边,函数值越大;越靠中间,函数值越小。 如果 a < 0,正好反过来,越靠两边越小,越靠中间越大。
这时候原数组虽然是升序,但变换后的值,不会简单保持原顺序。真正有用的信息是:最大值或者最小值,一定优先出现在两端。
这就很像我们平时做双指针题的味道了。
比如:
nums = [-4, -2, 2, 4]
a, b, c = 1, 3, 5
变换后分别是:
f(-4) = 9
f(-2) = 3
f(2) = 15
f(4) = 33
结果是 [3, 9, 15, 33]。
你会发现,两端 -4 和 4 里更容易先产出大值。既然这样,就没必要全量算完再排,直接左右两头比,把大的先塞到结果数组尾巴里就行。
核心代码我一般会这样写:
defsort_transformed_array(nums, a, b, c):
defcalc(x):
return a * x * x + b * x + c
n = len(nums)
ans = [0] * n
left, right = 0, n - 1
# a >= 0 时,大的在两端,从后往前填
# a < 0 时,小的在两端,从前往后填
idx = n - 1if a >= 0else0
while left <= right:
lv = calc(nums[left])
rv = calc(nums[right])
if a >= 0:
if lv > rv:
ans[idx] = lv
left += 1
else:
ans[idx] = rv
right -= 1
idx -= 1
else:
if lv < rv:
ans[idx] = lv
left += 1
else:
ans[idx] = rv
right -= 1
idx += 1
return ans
这段代码不用排序,时间复杂度是 O(n),空间复杂度 O(n)。
这题真正容易卡住的地方,不是双指针本身,而是很多人会在脑子里默认“有序数组经过函数变换,大概还是有序的”。这在一次函数里还有点机会,到了二次函数基本就不成立了。
你可以自己打个小样本感受一下:
defcalc(x, a, b, c):
return a * x * x + b * x + c
nums = [-3, -1, 0, 2]
print([calc(x, 1, -2, 1) for x in nums])
输出可能是:
[16, 4, 1, 1]
原数组是升序,结果已经完全不是升序了。
再往下拆,其实这题本质是“抛物线 + 双端选择”。 你不用真的去算顶点坐标,也不用分类讨论太复杂,只需要抓住一件事:两端一定更值得先比较。
面试里要是时间紧,我会先给出朴素解法,再补一句优化思路:
defsort_transformed_array_v1(nums, a, b, c):
return sorted(a * x * x + b * x + c for x in nums)
然后再说,既然 nums 已经有序,二次函数在数轴上的值分布有明显结构,可以用双指针把 O(n log n) 压到 O(n)。这样答,思路会更完整一点,不会显得只会背答案。