Python技术迷

鹅厂员工吐槽:毕业5年,感觉码农的路愈发难走,刚进公司拿2万感觉很轻松,买了房之后,现在拼命还很慌!

鹅厂员工吐槽:毕业5年,感觉码农的路愈发难走,刚进公司拿2万感觉很轻松,买了房之后,现在拼命还很慌!

Image

房贷一扣,生活费一算,存款再看一眼,人瞬间清醒。以前加班是为了涨薪,现在加班是怕掉队;以前跳槽是想多拿点,现在跳槽都得先掂量行情。码农这条路不是突然难走,是大家慢慢发现,工资高不等于安全感高。

最难受的是,你明明已经很拼了,但还是慌。怕业务黄,怕优化,怕年龄上来,怕技术没跟上,怕下一次绩效轮到自己。

以前觉得进大厂就是上岸,后来才发现,只是换了个地方继续游泳。还不能停,停一下水就灌进来了。

算法题: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 比特字符。

别背答案,记游标。游标没乱,这类编码题基本就不会乱。