Python技术迷

HR:“面试通过了,麻烦提供一下你过去12个月的银行流水和薪资证明,我们要走定薪流程。

刚看到个贴子,说有网友面试通过后,HR开口就要“近12个月银行流水+薪资证明,说是走定薪流程”,他怀疑这是在用过去工资把自己牢牢压价。

Image

我觉得这事吧,关键在信息差和边界感。对求职者来说,你是去买菜,不是被明码标价的猪肉,谈薪最好围绕岗位价值和市场价,而不是被过去的死数字拴住。流水一旦交出,HR心里有杆秤,很难给你超出太多。

但换个角度想,有些公司确实有合规要求,那也行:先谈清楚薪资区间和涨幅,再考虑给“证明”,别一上来就掏底牌。

总的来说还是那句——提升自己的价值,守住信息边界,谈薪才有底气,不至于一开始就输在起跑线。

算法题:括号生成

昨晚十一点多,我在公司楼下便利店门口啃着关东煮,手机里一个同学突然问我: “东哥,那个括号生成怎么写啊,我每次都想成暴力枚举 2^(2n) 种情况,写着写着脑子就浆糊了。”

我嘴里还叼着个丸子,就在那儿给他比划,干脆今天整理一下,用 Python 说人话聊聊这个题。

题目大意先说清楚 括号生成这个题,一般是这么说的:给你一个数字 n,生成所有由 n 对括号组成的合法括号串。

比如:

  • n = 1:只有 "()"
  • n = 2:"(())" 和 "()()"
  • n = 3:就会多一堆:"((()))", "(()())", "(())()", "()(())", "()()()"

啥叫“合法”?简单粗暴的记法: 从左往右扫,任何时候 ")" 的数量不能比 "(" 多,最后两边数量还得一样。你可以脑补成:一个人左括号是“进门”,右括号是“出门”,你不可能还没进门就先出门,对吧。

暴力为什么不太行 你想啊,长度是 2n 的串,每一位不是 "(" 就是 ")",光组合就有 2^(2n) 种。 再一条条检查是不是合法,能做,但 n 稍微大一点,就开始浪费生命。

关键问题是: 其实大部分串,一眼就能看出不可能合法,比如前两位就是 "))",那后面不管怎么补都没戏,这种还要生成出来再扔掉,就很浪费。

所以正常人的做法是:边构造,边剪掉不可能的情况,这就是典型的回溯(DFS)套路。

用回溯怎么想这事 你可以想象在拼一个字符串 path,一格格往后写,每一步只有两种选择:

  • 写 "("
  • 写 ")"

但是不能乱写,要满足两个约束:

  1. 左括号 "(" 的总数不能超过 n
  2. 任何时刻,右括号 ")" 的数量不能超过左括号数量

第二条就是刚才说的“不能先出门再进门”。

那伪代码脑补一下就是:

  • 维护三个变量:

    • path:当前已经拼好的字符串
    • left:已经用了多少个 "("
    • right:已经用了多少个 ")"
  • 当 len(path) == 2 * n,说明凑满了,就收集一次答案

  • 还能加 "(" 的时候就往下递归一层

  • 能加 ")" 的时候(右边没超过左边)也递归一层

就这么简单粗暴。

直接上 Python 代码(可运行) 我当时发给同学的是这个版本,你可以直接丢到 LeetCode 22 用:

from typing import List

classSolution:
defgenerateParenthesis(self, n: int) -> List[str]:
        res = []

defdfs(path: str, left: int, right: int):
# 如果长度已经是 2n,收集结果
if len(path) == 2 * n:
                res.append(path)
return

# 还能放左括号,就放一个试试
if left < n:
                dfs(path + "(", left + 1, right)

# 只能在右括号数量 < 左括号数量时,才可以放右括号
if right < left:
                dfs(path + ")", left, right + 1)

        dfs("", 0, 0)
return res

if __name__ == "__main__":
    s = Solution()
    print(s.generateParenthesis(3))

你注意看几个点:

  • dfs 这个函数,其实就是在一棵“决策树”上往下走,每一层多一个括号。
  • left < n 的时候才能加 "(",否则左括号超标。
  • right < left 才能加 ")",否则会出现 ")(" 这种前缀不合法的情况,直接被剪掉。

这种写法的妙处在于:任何一个走到一半就“不合法的前缀”,根本不会继续往下长大。所以虽然理论上有 2^(2n) 种串,但真正走到底的只有那些合法的括号组合,效率就高多了。

顺手说点复杂度 严格来说,这个题答案个数是第 n 个卡特兰数,记作 C_n,大概是 4^n / (n * sqrt(n)) 这个量级,反正是指数的,谁也跑不掉。 但是我们已经把“明显错误”的路径砍掉了,所以在同样数量级的题目里,这个方法是比较靠谱的写法,面试官看了也会点头的那种。

如果要再进阶一点点 有时候面试官会追问:

  • 能不能不用字符串拼接,减少一下中间对象?
  • 能不能用列表存字符,最后 "".join(path)?

比如这个版本,把 path 换成列表,稍微省点内存分配:

classSolution:
defgenerateParenthesis(self, n: int):
        res = []
        path = []

defdfs(left: int, right: int):
if len(path) == 2 * n:
                res.append("".join(path))
return

if left < n:
                path.append("(")
                dfs(left + 1, right)
                path.pop()

if right < left:
                path.append(")")
                dfs(left, right + 1)
                path.pop()

        dfs(0, 0)
return res

这里的 path.append / path.pop 就是典型的“回溯姿势”: 下去之前先改状态,回来之后再把状态撤销。你每次画一棵小树,自己推一遍,很快就熟了。

写在最后随便唠两句 这种题其实特别适合练“状态 + 约束”的思维: 不是上来就想怎么把所有答案一股脑列出来,而是想——每一步我到底有哪些合法选择。

你要是刷题的话,建议这道题亲手敲三遍: 一遍照着写,一遍关掉代码自己默写,一遍试着改成别的语言(比如 Java / Go),那差不多就真的掌握了。

行了,今天就到这,我去弄杯咖啡,醒醒脑。