网友分享了一段职场趣事:leader让他用SQL更新数据,他刚拿到账号密码准备登录,leader突然来了一句“你先别动”。这估计是怕实习生把生产库当成练习本乱写乱画吧。更有意思的是,leader还特地让他站在旁边“观摩学习”。
结果,我就亲眼看着leader的手速瞬间起飞,鼠标光标在屏幕上轻轻一飘,精准地点中了那个红得发亮的按钮——“清空表”。我刚张开嘴,那句“快回滚!”还在喉咙里打转,他已经行云流水地顺手点了 commit。整个过程丝滑流畅,不知道的还以为他在打游戏排位赛。空气凝固了三秒,leader自己发出了一声“啊……”,然后补上致命一击:“完了,没备份。”

我觉得这件事最大的价值,是让我深刻理解了运维安全的两条铁律:权限别乱给,备份要常做。至于leader那惊人的手速,我觉得他可能更适合去玩音游,真的,别再轻易碰生产环境的数据了。
算法题:移除 9
那天面试一个新人,问了一道听起来有点“刁钻”的题目,叫《移除 9》。他一听名字,第一反应是链表删除节点,结果把题目描述一看就懵了:给你一个正整数 n,求从小到大第 n 个“不含数字 9”的正整数。他当场愣住,小声嘀咕:“这谁想得出来的题目啊……”
大家先别急着骂出题人,这道题其实挺有意思的,关键在于你能不能跳出惯性思维,换个角度看世界。
我和那个面试者一开始的思路是一样的:最朴素、最暴力的方法。从 1 开始逐个往上数,遇到带数字 9 的就跳过,一直数到第 n 个为止。用伪代码表示大概是这样:
def kth_no_nine(n: int) -> int:
x = 0
count = 0
while count < n:
x += 1
if '9' in str(x):
continue
count += 1
return x
然后你一估算这个算法的时间复杂度……如果 n 达到 10^9 这种量级,程序还没跑完,人可能就先“猝死”在工位上了。面试时写出这种解法,面试官一般会礼貌地微笑一下,然后问你:“有没有更快一点的办法?”——翻译一下就是:这个思路可以放弃了。
真正巧妙的地方在于:当你把所有包含数字 9 的数都“移除”后,剩下的世界里就只包含 0 到 8 这 9 个数字了。是不是感觉有点眼熟?没错,这就是 9 进制 的世界啊!
所以,开一下脑洞,就能得到一个非常优雅的结论:第 n 个不含数字 9 的十进制数,就等于把 n 转换成 9 进制数后,再把每一位当成普通的十进制数字来解读。
光听理论可能有点抽象,我们举几个小例子:
- n = 1, 对应的9进制是
1, 当成十进制输出就是 1。
- n = 8, 对应的9进制是
8, 当成十进制输出就是 8。
- n = 9, 对应的9进制是
10, 当成十进制输出就是 10(注意,这个10里面没有9)。
- n = 10,对应的9进制是
11, 当成十进制输出就是 11,同样不含9。
如果你真的按照“暴力筛选不含9的数”去列一个表,前面就是 1, 2, 3, ..., 8, 10, 11, ... 完全和上述转换结果对得上。所以这道题的本质就一句话:把 n 转换成 9 进制,然后把那串“9 进制字符串”直接当作一个普通的十进制数字输出。
面试的时候我一般不会讲得这么学术化,而是边写代码边说。你看,可以这样实现:
def remove_9(n: int) -> int:
# 把 n 当作是 9 进制数系统的“序号”,先转成 9 进制,再“伪装”成十进制数
if n == 0:
return 0
res = 0
base = 1 # 位权,初始为个位(10^0)
# 手动进行“9进制转十进制伪装”的过程
while n > 0:
digit = n % 9 # 取当前9进制位,范围是0~8,天然避开了9
res += digit * base # 将这一位按位权累加到结果中
n //= 9 # n 去掉最低位,继续处理下一位
base *= 10 # 重要:位权乘10,因为我们是在构造一个“十进制外壳”的数字
return res
这里有两个特别容易写错的点,我当时还故意给面试者挖了个小坑:
第一个是 base *= 10。很多人会下意识地写成乘以9,心想我们不是在处理9进制嘛。但冷静想一想:我们最终要构造的是一个“看起来像十进制”的数字,它的每一位权重应该是 1(10^0), 10(10^1), 100(10^2)……所以每次当然是乘以10。
第二个是边界条件 n == 0。虽然原题通常从1开始,但作为通用的库函数,总会有人传个0进来测试。像我这种被线上事故“教育”过的人,看到代码没处理0就觉得浑身不舒服。在实际面试中写不写这句,可能取决于面试官的严谨程度。
那这个算法的优势到底在哪里呢?你可以这么解释:它的时间复杂度是 O(log₉ n),本质上就是n转换成9进制后的位数。而之前那个暴力解法是 O(n),两者完全不是一个数量级的。如果面试官继续追问,你就指着 n //= 9 这行代码说:“看,每次循环n都除以9,循环次数就是9进制下的位数”,用清晰的逻辑把气势拿捏住。
后来我还顺手写了个对拍的小脚本,用来验证这个“9进制伪装法”没有翻车,你们也可以参考一下:
def brute(n: int) -> int:
x, cnt = 0, 0
while cnt < n:
x += 1
if '9' in str(x):
continue
cnt += 1
return x
def fast(n: int) -> int:
return remove_9(n)
for i in range(1, 2000):
if brute(i) != fast(i):
print("boom at", i)
break
else:
print("all good")
这种简易的对拍脚本在工作中超级实用,尤其是当你写出一个“看起来非常聪明”的算法时,一定要用最笨的办法来验证一下。否则,等到代码上线出了问题,你连甩锅……哦不,是分析问题根源都会很麻烦。
说到底,这道《移除 9》的真正难点并不在编码,而在于那个关键的思维转换:“哎?不含9的数只用了0-8这9个数字,那不就是个9进制系统嘛!”一旦你习惯了这种“换一个数字系统视角看问题”的思路,后面遇到很多类似的“变态”面试题都会觉得顺眼多了。
工作里除了敲代码,也免不了要和数据库打交道,权限管理和备份意识真的至关重要,可别像我故事里的那位leader一样。好了,技术杂谈就到这里,我得去给同事救火了,他刚又在群里问“这个接口为啥总返回 500”……
像这样在实战中踩坑、在思考中解题的经历,非常欢迎你来 云栈社区 和大家一起分享交流。