Python技术迷

要不要从体制里重回互联网。。

刚刷到这个帖子,第一反应是:

35岁,疫情那年好不容易上岸,按理说该松口气了。结果队友降薪,房子跌一半,贷款、娃、老人,全挤到一起,喘气都贵。更扎心的是,现在基层也不是什么“喝茶看报”,886干着,钱还没多到能把窟窿补上。

外面那个机会呢,薪资翻三倍。至少眼前房贷能缓一口,早上还能送娃,这点对有娃的人太真实了。可互联网这玩意儿,谁敢拍胸口说稳?今天缺人,明天裁你,年纪还摆在那儿。

Image

我觉得最难的不是去不去,而是她已经被生活逼到必须拿风险换现金流了。体制像慢性缺氧,互联网像猛吸一口氧气但不知道瓶子啥时候空。

换我可能也会纠结到半夜,表格列一堆,最后还是看银行卡余额沉默。

算法题:有时间限制的缓存

缓存里明明只放了 3 个 key,count() 一查还是 3,过一秒再 get(),全是 -1。

这类题我第一眼不会先写 dict,而是先盯两个点:过期时间怎么算,过期数据什么时候清。很多人这题错,不是不会缓存,是把“存在”和“没过期”混成了一件事。

题目大概是这样:实现一个有时间限制的缓存。

它需要支持三个操作:

set(key, value, duration):写入 key,duration 毫秒后过期。如果 key 之前存在且没过期,返回 True,否则返回 False。

get(key):如果 key 存在且没过期,返回 value,否则返回 -1。

count():返回当前还没过期的 key 数量。

这题别急着搞什么定时器。算法题里真开后台线程清缓存,基本就是把简单问题写复杂了。

更稳的做法是:每次操作时看当前时间,顺手判断过期。

缓存结构我一般这样放:

{
    key: (value, expire_at)
}

expire_at 是一个绝对过期时间。比如当前时间是 1000ms,duration 是 500ms,那这个 key 的过期时间就是 1500ms。

代码可以这么写:

import time


classTimeLimitedCache:
def__init__(self):
        self.box = {}

def_now(self):
return int(time.monotonic() * 1000)

defset(self, key: int, value: int, duration: int) -> bool:
        now = self._now()
        existed = False

if key in self.box:
            _, expire_at = self.box[key]
if expire_at > now:
                existed = True

        self.box[key] = (value, now + duration)
return existed

defget(self, key: int) -> int:
        now = self._now()

if key notin self.box:
return-1

        value, expire_at = self.box[key]
if expire_at <= now:
del self.box[key]
return-1

return value

defcount(self) -> int:
        now = self._now()
        dead_keys = []

for key, (_, expire_at) in self.box.items():
if expire_at <= now:
                dead_keys.append(key)

for key in dead_keys:
del self.box[key]

return len(self.box)

这里我用了 time.monotonic(),没用 time.time()。

这个地方别嫌细。time.time() 取的是系统时间,机器时间如果被校准了,比如 NTP 往前拨了一下,你的缓存过期判断就可能怪怪的。monotonic() 更适合做耗时、超时、过期这类判断,它只管单调递增。

再看一个小现场:

cache = TimeLimitedCache()

print(cache.set(1, 100, 300))  # False,之前没有
print(cache.get(1))            # 100

time.sleep(0.4)

print(cache.get(1))            # -1,过期了
print(cache.count())           # 0

这题最容易漏的是 set() 的返回值。

不是说 key 在字典里就返回 True,而是 key 在字典里,并且还没过期,才返回 True。

比如这个例子:

cache = TimeLimitedCache()

print(cache.set(7, 10, 100))   # False
time.sleep(0.2)
print(cache.set(7, 20, 100))   # False,旧的已经过期了
print(cache.get(7))            # 20

如果你只写:

return key in self.box

这就埋雷了。key 虽然还躺在字典里,但它已经死了。缓存里最烦的就是这种“尸体数据”,看着存在,实际不能用。

count() 也一样,不能直接:

return len(self.box)

这行看着很香,实际错得很隐蔽。因为过期的 key 可能还没被 get() 访问过,它还留在字典里。count() 必须先扫一遍,把过期的删掉,再返回数量。

这份代码的复杂度也比较直接。

set() 和 get() 都是 O(1)。count() 是 O(n),因为它要检查所有 key。

能不能把 count() 也优化到 O(1)?可以,用小根堆维护过期时间。但这题一般没必要一上来就堆。除非题目明确卡 count() 很频繁,否则先把过期逻辑写对,比堆一坨结构更重要。

缓存题看着像写容器,其实考的是边界感。

key 存在,不代表有效。 时间到了,不代表马上被清掉。 清不清,取决于你在哪个操作里顺手处理。