leetcode1260 凌晨三点,面试官从终端里爬出来问我要 O(1) 解法

凌晨三点,面试官从终端里爬出来问我要 O(1) 解法

凌晨两点四十七,我还在改那个破游戏的截图生成器——明天产品要上线“通关炫耀图”,结果不管怎么存,截出来的网格都像被谁踹了一脚似的,集体往右窜了一格。右下角那个像素还总瞬移到左上角,害得测试小姐姐在群里连发三条“这截图是在玩华容道吗”。

盯了一晚上代码,循环、索引、数组边界全查了个遍,愣是没找到 bug。就在我准备把锅甩给“显卡驱动不兼容”的时候,屏幕上那张 3×3 的像素小人突然自己动了起来——每个格子像装了马达,咔咔整齐地往右挪一格;最右边那个掉了下去,从下一行最左边冒出来;右下角那个更离谱,直接一个闪现回到左上角。整个过程行云流水,比我手写的 for 循环利索多了。

“卧槽,Bug 成精了?”

领头那个白得晃眼的像素 0xFF 抬头白了我一眼:“你才 bug,我们这是在玩‘需求迁移’——没看出这跟你们站立会一模一样吗?所有人的需求都往右传一个人,最后一个传给产品经理,产品经理的需求再原地起飞扔回给你。这都认不出来,还程序员呢,你司 oncall 没教你?”

我一口老血差点喷屏幕上:合着我 debug 了三天的“截图错位”,就是你们这帮像素半夜开趴,把队形当接力棒传着玩?

像素 0xFF 叉着腰:“ 对,而且今晚还得接着跳,老板像素说必须跳满 k 次才算团建结束。但我们实在跳不动了——你帮我们算算最终队形长啥样,我们好一步到位站好,省得真跳。作为交换,我告诉你昨晚我们乱跳了几次,你就能还原回原始截图,明天早上不用再在这破电脑前 cos 侦探了。”

说完,它递过来一张小纸条,上面写着今晚的舞步次数:k = 4。


一:NumPy 侠,一行超人

我把指关节按得咔咔响,切回 IDE:“就这?看我一招调包侠之怒。”

import numpy as np

def shift_grid_numpy(grid, k):
    return np.roll(array(grid).flatten(), k).reshape(len(grid), len(grid[0])).tolist()

flatten 把你们全拍扁成一维长龙,roll 让末尾 k 个直接卷到龙头,reshape 再捏回二维小格子。全靠 NumPy 爸爸在 C 语言层暴力搬运,比我手写 for 循环快出一个数量级。”

角落里一个绿豆大小的像素弱弱举手:“那……这个魔法耗不耗内存啊?”

我沉默了一秒:“嗯……flattenreshape内部都会生成全新数组,空间 O(mn),等于给你们开了个平行宇宙的克隆人工厂。不过没事,我机器 32G 内存,拢共九个像素,还没我一个未使用的指针占的地儿大,洒洒水。”

话音刚落,像素们集体炸锅:“可我们明天搞不好要上 8K 超清截图啊!几千万个兄弟姐妹,你也用这招?!”

我嘴角抽了抽:“……确实可能直接 OOM 爆炸……”


二:原地狂魔,三次反转

话音刚落,屏幕右下角毫无征兆地弹出一个 Terminal 窗口,光标闪了两下。我还没来得及把鼠标挪到关闭按钮上,一张脸竟然从 ASCII 字符的噪点里慢慢浮现了出来——那张脸我可太熟悉了。那是去年问我八遍“反转链表还能怎么优化”的面试官的脸,当时他面带微笑听我写完了递归和迭代两个版本,然后又淡淡说了句“可以用头插法再写一遍吗”。那场面试我面了九十分钟,出去的时候嘴唇都是白的。

现在是凌晨三点,屏幕里的脸再次露出了那个熟悉的核善笑容。“同学,刚才那个解法,空间复杂度是 O(mn),不符合我们公司的招聘标准。请你原地实现,额外空间 O(1)。”

像素们吓得集体褪色,九个像素缩成一个 3×3 的小疙瘩。我后背瞬间湿透,手指僵在键盘上,大脑一片空白——就像每次面试官说“你有什么想问我的吗”的时候一模一样。

但是,奇迹发生了。

被 LeetCode 毒打了三百题的那部分脑细胞,突然像被电击了一样集体苏醒。一段远古记忆冲破咖啡因和生物钟的双重封锁,在我脑子里炸开——“靠!这不就是轮转数组吗!三次 reverse,原地搞定!”

像素 0xFF 从疙瘩里探出半个脑袋,颜色都吓淡了:“轮……转数组?那是什么远古黑魔法?

我对着像素们招手:“听好了,这招叫反转侠三板斧,把你们这个二维网格,在脑子里当作一个一维长串,索引从 0 到 m*n-1,然后——”

def shift_grid_inplace(grid, k):
    m = len(grid)
    n = len(grid[0])
    total = m * n
    k %= total
    if k == 0:
        return grid

    # 假装网格是一个一维数组,用 divmod 定位任意像素
    def get(idx):
        return grid[idx // n][idx % n]
    def set_(idx, val):
        grid[idx // n][idx % n] = val

    # 反转 [start, end] 区间的像素
    def reverse_range(start, end):
        while start < end:
            tmp = get(start)
            set_(start, get(end))
            set_(end, tmp)
            start += 1
            end -= 1

    # 三次反转,原地完成循环右移
    reverse_range(0, total - 1)   # 整个倒过来
    reverse_range(0, k - 1)       # 前 k 个再倒回去
    reverse_range(k, total - 1)   # 后面再倒回去
    return grid

像素 0xFF 看了半天,小脑袋跟着比划了三遍:“等等,你这不就是先把整个队伍倒立,再把前半截倒立,再把后半截倒立吗?跟拧魔方似的,拧巴拧巴居然就对了?!”

“对。这就是传说中原地 O(1) 空间的数组旋转法。三次翻转 等于 一次循环右移,数学上严丝合缝。不用 new 任何东西,全程只靠一个 temp 变量在两个像素间倒手。空间 O(1),时间 O(n),面试官最爱看这个——”

说着我下意识瞄了一眼 Terminal。面试官的脸还在,嘴角好像翘起了一个像素级别的微小弧度——大概 1px 那么宽,不能再多了。我还没来得及确认那到底是不是笑,窗口就啪地消失了,只留下一行惨白的命令行提示符在闪,仿佛刚才的一切都是我的幻觉。

像素 0xFF 瘫坐在地上,抬头看我:“你们人类面试都这么刺激的吗?”

“习惯了。每次都觉得在被公开处刑,后来发现——他们可能只是单纯享受看人出汗。”

像素们齐刷刷按反转侠的咒语站好了新队形,完美得像达芬奇的画。我瘫在椅子上,默默打开 Git,把 commit message 改成了:

fix: 截图偏移bug,根因是像素半夜团建跳需求迁移舞,已用三次反转原地回滚。

关掉 IDE 之前,像素 0xFF 从格子里探出半个身子,冲我眨了眨眼:“你那个反转侠的咒语,我们打算编成团建的官方舞步,以后就不用一步一步挪了,直接三次翻转集体到位。省下来的时间可以多摸鱼。”

知识共享许可协议
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。