Python技术迷

同事月薪2万8,上有老下有小,房贷每月1万2。他跟我说:我现在出去面试,人家一看年龄就摇头。

刚看到个贴子,说一位38岁的老同事被公司“优化”,拿了N+3就被请走。原来月薪2万8,上有老人下有孩子,房贷月供1万2。结果出去面试,一看到年龄,对方直接摇头。

Image

网友们的回复我看了看,有的骂公司无情,有的感叹“35+就是职场高危线”,也有人劝他先降薪保住现金流。说句实话,这事真不稀奇,只是这次轮到他了,大家才有代入感。

我觉得关键有两点:一是别再幻想“干得久=铁饭碗”,现在更多是项目制、阶段性价值;二是普通人真的要早点为35+做预案:存够6–12个月现金、技能别断层,多准备几条退路。

不过话说回来,被裁不是人生失败,就是提醒你:游戏规则更新了。情绪可以有,但脚得赶紧动起来。只要人还在、心态不崩,路还是有的。

算法题:最长回文子串

昨天晚上快十一点,我在公司楼下拿着奶茶刷题,有个实习生跑过来跟我说:“东哥,这个最长回文子串也太阴间了吧?”我看了一眼题目,笑了:“这个题其实挺像谈恋爱,表面是字符串,底层全是细节。”

先把题目说人话版: 给你一个字符串 s,让你在里面找一段连续的子串,这段子串从左往右、从右往左读都是一样的,也就是回文,比如:

  • "aba" 是
  • "abba" 也是
  • "abc" 就不是了

要返回最长的那一段回文子串。

大部分人一上来都是这么干的: “我把所有子串都枚举一遍,一个个去判断是不是回文,顺便记个最长的。”

听起来没毛病对吧?问题在于子串数量有多少: 长度是 n 的字符串,子串个数大概是 n^2 级别,每个子串再从头到尾检查一遍是不是回文,又要 O(n),整体就变成 O(n^3) 了。

面试官一看你这个复杂度,心里想的可能是:这个同学平时是不是没怎么关心性能 —— 就像之前我写数据库压测那篇里吐槽的那种粗暴写法一样。

所以得换个思路。

真正好用的思路:从中心往两边扩

有个特别好理解的观察:

  • 任何一个回文串,都可以看成是从一个中心往两边对称扩散出来的

  • 这个中心可能是:

    • 一个字符中间(奇数长度回文,比如 "aba" 的中心是 'b')
    • 两个字符中间(偶数长度回文,比如 "abba" 的中心是在两个 'b' 中间)

那我们干啥呢? 就挨个把每个“中心”拿出来试一遍,从它为起点往两边扩,如果左边和右边字符一样,就继续扩,不一样就停下,这一圈扩完得到的就是“以这个中心为中心的最长回文串”。

整条字符串长度是 n:

  • 中心一共有多少个?

    • 奇数中心:n 个(每个字符)
    • 偶数中心:n-1 个(在相邻两个字符之间) 总共也就 2n-1 个,还是 O(n)
  • 每次扩的时候,最坏也就把串扫一遍,所以是 O(n)

  • 总复杂度:O(n^2),空间只用几个变量,是 O(1)

对 LeetCode 那个数据规模来说,这个完全够用了。

用 Python 写一下

直接上我平时会写的版本,里面加点注释,你可以照着改:

classSolution:
deflongestPalindrome(self, s: str) -> str:
# 字符串为空的边界情况
ifnot s:
return""

        n = len(s)
# 记录当前找到的最长回文的起始下标和长度
        start = 0
        max_len = 1# 至少有一个字符是回文

# 从中心向两边扩散的小函数
defexpand(l: int, r: int):
nonlocal start, max_len
# 只要左右没越界,而且字符相等,就继续扩
while l >= 0and r < n and s[l] == s[r]:
                cur_len = r - l + 1
if cur_len > max_len:
                    max_len = cur_len
                    start = l
                l -= 1
                r += 1

# 枚举每一个“中心”
for i in range(n):
# 1)以 i 为中心的奇数长度回文,比如 "aba"
            expand(i, i)
# 2)以 i 和 i+1 为中心的偶数长度回文,比如 "abba"
            expand(i, i + 1)

# 最后把最长那段切出来
return s[start:start + max_len]

这个写法几个细节你注意下:

  • nonlocal start, max_len 是为了在内部函数里修改外面的变量
  • expand 里更新的是全局当前最长,而不是返回局部结果再去比较,这样代码会干净一点
  • for 循环里两个 expand:一个负责奇数,一个负责偶数,两种情况都要考虑到

你可以自己在本地打个简单的测试,比如:

s = Solution()
print(s.longestPalindrome("babad"))  # "bab" 或 "aba"
print(s.longestPalindrome("cbbd"))   # "bb"
print(s.longestPalindrome("a"))      # "a"
print(s.longestPalindrome("ac"))     # "a" 或 "c"

有时候面试官会追问:“能不能再说个动态规划版本?” 大概思路是这样的:

  • 定义 dp[i][j] 表示子串 s[i:j+1] 是否是回文

  • 状态转移:

    • s[i] == s[j] 并且
    • 里面那一段要么长度 ≤ 2(比如 "aa" 或 "aba" 这种边界),要么已经是回文 dp[i+1][j-1] == True

这样就能从短串推长串,最后枚举所有 dp[i][j] == True 的位置,找到最长那一段。 不过 DP 要开一个 n * n 的二维数组,空间 O(n^2),实现上比中心扩展更啰嗦一点,实战我一般优先写中心扩展。

总之这个题你脑子里记两个关键词就行:“中心” + “扩散”。 下次再有人在工位后面嚎“回文子串好难啊”的时候,你就可以淡淡敲两行代码装个逼了,我昨天那个实习生现在已经会拿它去写博客了 😄

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

🔥虎哥私藏精品🔥

虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB