鹅厂员工吐槽:毕业5年,感觉码农的路愈发难走,刚进公司拿2万感觉很轻松,买了房之后,现在拼命还很慌!
鹅厂员工吐槽:毕业5年,感觉码农的路愈发难走,刚进公司拿2万感觉很轻松,买了房之后,现在拼命还很慌!
房贷一扣,生活费一算,存款再看一眼,人瞬间清醒。以前加班是为了涨薪,现在加班是怕掉队;以前跳槽是想多拿点,现在跳槽都得先掂量行情。码农这条路不是突然难走,是大家慢慢发现,工资高不等于安全感高。
最难受的是,你明明已经很拼了,但还是慌。怕业务黄,怕优化,怕年龄上来,怕技术没跟上,怕下一次绩效轮到自己。
以前觉得进大厂就是上岸,后来才发现,只是换了个地方继续游泳。还不能停,停一下水就灌进来了。
算法题:1 比特与 2 比特字符
最后一位明明是 0,程序却把它当成了 2 比特字符的一部分。
这种问题我第一眼不会去怀疑 Python,也不会去怀疑数组越界。先看编码规则。
规则很短:
0 -> 1 比特字符
10/11 -> 2 比特字符
也就是说,只要遇到 0,它自己就是一个字符;遇到 1,它必须带着后面一位一起走,不管后面是 0 还是 1。
现场数据大概长这样:
[1, 0, 0]
最后一个 0 到底是不是单独的 1 比特字符?
按规则从左往右扫:
1 0 -> 2 比特字符
0 -> 1 比特字符
所以答案是:是。
但换成这个:
[1, 1, 1, 0]
扫一遍:
1 1 -> 2 比特字符
1 0 -> 2 比特字符
最后那个 0 被前面的 1 带走了,它不是单独字符。
这个题别想复杂,别上来就递归、DP、状态机。它就是一个小型协议解析。协议解析最怕什么?最怕你不按游标走。
我一般会先写成这种版本,能看清楚游标怎么跳:
deflast_is_one_bit(bits: list[int]) -> bool:
pos = 0
last_start = -1
while pos < len(bits):
last_start = pos
if bits[pos] == 0:
pos += 1
else:
pos += 2
return last_start == len(bits) - 1
这个代码没什么花活,关键就一件事:记录最后一个字符是从哪里开始的。
如果最后一个字符的起点正好是数组最后一位,那它就是 1 比特字符。
拿几组数据跑一下:
cases = [
[0],
[1, 0, 0],
[1, 1, 1, 0],
[1, 1, 0],
[1, 0, 1, 0, 0],
]
for item in cases:
print(item, last_is_one_bit(item))
输出会是:
[0] True
[1, 0, 0] True
[1, 1, 1, 0] False
[1, 1, 0] True
[1, 0, 1, 0, 0] True
这里有个细节,很多人会在 while pos < len(bits) - 1 上绕半天。
比如写成这样:
defcheck_tail(bits: list[int]) -> bool:
pos = 0
while pos < len(bits) - 1:
if bits[pos] == 1:
pos += 2
else:
pos += 1
return pos == len(bits) - 1
这个也能过。
但我平时不太喜欢一开始就这么写。原因很简单,它把“最后一位是否被解析到”这件事藏到了循环条件里,新人改代码时容易改歪。
比如后面有人想加日志:
print("read", pos, bits[pos])
他没意识到 pos 有可能跳过最后一位,日志一加,判断一改,很容易把本来清楚的游标逻辑搞乱。
协议解析里,我更愿意让循环完整消费整段数据。哪怕多一个 last_start,后面排查时一眼能看出来最后一个字符从哪开始。
还有一种写法,是从尾巴往前看。
因为最后一位一定是 0。真正决定它是不是独立字符的,不是它自己,而是它前面连续有多少个 1。
看这个:
... 0
前面没有连续的 1,最后的 0 单独站着。
... 1 0
前面有 1 个连续的 1,最后的 0 被这个 1 带走。
... 1 1 0
前面有 2 个连续的 1,从左到右配对后,最后的 0 反而又单独站着。
所以规律是:最后一个 0 前面连续 1 的个数,如果是偶数,它就是 1 比特字符;如果是奇数,它就是 2 比特字符的一部分。
代码可以这么写:
deflast_is_one_bit_by_tail(bits: list[int]) -> bool:
ones = 0
pos = len(bits) - 2
while pos >= 0and bits[pos] == 1:
ones += 1
pos -= 1
return ones % 2 == 0
这段代码短,但它有点“技巧味”。面试手写可以,线上业务解析我不会优先用它。
原因还是那个老毛病:它依赖题目保证最后一位是 0。如果哪天输入不是 LeetCode 题目,而是真实设备报文、日志标记、压缩位流,最后一位不一定干净,这种写法就会误判。
我会加一个很土的校验:
defparse_device_bits(bits: list[int]) -> bool:
ifnot bits:
raise ValueError("empty bit stream")
if bits[-1] != 0:
raise ValueError(f"bad tail bit: {bits[-1]}")
pos = 0
last_start = -1
while pos < len(bits):
last_start = pos
if bits[pos] == 0:
pos += 1
continue
if pos + 1 >= len(bits):
raise ValueError(f"broken 2-bit char at index {pos}")
pos += 2
return last_start == len(bits) - 1
别嫌这个啰嗦。
真到线上,最值钱的不是那一行 return pos == len(bits) - 1,而是这句:
raise ValueError(f"broken 2-bit char at index {pos}")
它能告诉你数据到底坏在哪。
我见过太多解析代码,异常只吐一个:
parse failed
然后人开始翻日志、抓包、重放数据。最后发现就是某一位少了,或者上游截断了。早把坏点打出来,半小时的事不用拖到半天。
这个题最后可以压成一句话:
从左往右扫,遇到 0 走 1 步,遇到 1 走 2 步。最后一个字符如果从最后一位开始,它就是 1 比特字符。
别背答案,记游标。游标没乱,这类编码题基本就不会乱。