Python技术迷

40岁总监,拥有10年+经验的财务总监,在北京求职屡屡碰壁,甚至做好了“离开北京”的最坏打算。。

44岁,财务总监,10多年经验,按理说不该这么难看吧,结果在北京投简历投到怀疑人生,连“要不要离开北京”都开始想了。

Image

你以为自己攒的是资历,公司那边看见的可能是年龄、薪资、稳定但不够便宜。财务岗位又不像销售那样立马拉业绩,很多老板嘴上说要经验,真到出钱的时候,又想找个年轻点、便宜点、能熬点的。

北京更现实,机会多,但筛人也狠。你稍微慢一步,就有人比你年轻、比你便宜,还愿意加班。中年人不是没能力,是突然发现自己被放到另一个价目表里了。

算法题:词典中最长的单词

banana 明明最长,代码却不该返回它。

这个题最容易写错的地方就在这:它问的不是“词典里长度最长的单词”,而是“能一步一步从短前缀长出来的最长单词”。

比如词典是这样:

words = ["a", "banana", "app", "ap", "appl", "apple", "apply"]

很多人第一眼就会写个排序,按长度倒序拿第一个。

这地方我第一眼就不太信。

因为 banana 虽然长,但它的前缀链不完整:

b
ba
ban
bana
banan
banana

这些前缀词典里都没有,所以它不合格。

而 apple 就不一样:

a
ap
app
appl
apple

每一层都在词典里。

这个题真正要查的是:一个单词的所有前缀,是否都出现过。

先看一个容易错的版本:

deflongest_word_bad(words):
    words.sort(key=lambda x: len(x), reverse=True)
return words[0]

这段代码跑上面的数据,直接返回 banana。

看着短,错得也干脆。

这种写法只看长度,不看“生长路径”。题目里那个“built one character at a time”才是关键,不是装饰词。

我平时处理这种题,一般先把词典塞进 set。原因很简单,查前缀要频繁查,如果每次都在 list 里扫,数据一大就很难看。

第一版可以写得直接一点:

deflongest_word(words):
    bag = set(words)
    ans = ""

for word in words:
        ok = True

for end in range(1, len(word)):
if word[:end] notin bag:
                ok = False
break

ifnot ok:
continue

if len(word) > len(ans):
            ans = word
elif len(word) == len(ans) and word < ans:
            ans = word

return ans

这段代码有两个判断。

第一个判断长度:

len(word) > len(ans)

谁长用谁。

第二个判断字典序:

len(word) == len(ans) and word < ans

长度一样,取字典序更小的。

比如:

words = ["a", "ar", "art", "ap", "app"]

art 和 app 都是 3 个字符,如果它们的前缀都合法,那返回 app,不是 art。

这里别手滑写成 word > ans。这个坑很小,但测试用例会精准打脸。

跑一下:

cases = [
    ["w", "wo", "wor", "worl", "world"],
    ["a", "banana", "app", "ap", "appl", "apple", "apply"],
    ["z", "za", "zac", "a", "ap", "app"]
]

for item in cases:
    print(item, "=>", longest_word(item))

输出应该是:

['w', 'wo', 'wor', 'worl', 'world'] => world
['a', 'banana', 'app', 'ap', 'appl', 'apple', 'apply'] => apple
['z', 'za', 'zac', 'a', 'ap', 'app'] => app

这版没问题,但我不太喜欢它。

每个单词都要切片查前缀,word[:end] 会生成新字符串。题目数据不大没事,真放到词库校验脚本里,我会稍微换个写法。

更顺手的办法是先排序。

按字典序排序后,再从短词往长词处理。只要一个单词去掉最后一个字符的前缀已经合法,那这个单词也合法。

比如 apple 只需要看 appl 合不合法,不用每次重新查 a、ap、app、appl。

代码这样写:

defpick_longest_word(words):
    words.sort()

    ready = {""}
    best = ""

for word in words:
        parent = word[:-1]

if parent notin ready:
continue

        ready.add(word)

if len(word) > len(best):
            best = word

return best

这里有个小细节:

ready = {""}

空字符串先放进去。

这样长度为 1 的单词,比如 a、b,它们的 word[:-1] 都是空字符串,可以自然通过,不用单独写一堆 if。

还有一个细节,为什么只判断长度,不判断字典序?

因为前面已经 words.sort() 了。

同样长度的单词,字典序小的会先被遍历到。后面字典序大的即使合法,长度也不会超过 best,所以不会替换。

这块当时我也怀疑过,会不会漏掉更小的答案。实际不会,因为排序已经把这个规则提前处理掉了。

拿刚才的数据再跑:

tests = [
    ["w", "wo", "wor", "worl", "world"],
    ["a", "banana", "app", "ap", "appl", "apple", "apply"],
    ["b", "br", "bre", "brea", "break", "a", "ar", "arc"]
]

for words in tests:
    print(pick_longest_word(words))

输出:

world
apple
break

这版复杂度主要在排序,O(n log n)。

后面每个单词只查一次 set,整体比较稳。

不过有个前提要记住:它不是找“最长字符串”,它找的是“每一层前缀都能在词典里找到的最长字符串”。

所以看到这题,我一般先写两个反例:

["banana", "ban", "bana"]

不能返回 banana。

再写一个平局反例:

["a", "ap", "app", "b", "ba", "ban"]

应该返回 app,不是 ban。

这两个用例过了,基本方向就没歪。

最后把代码收一下:

deflongest_word_in_dictionary(words):
    words.sort()

    built = {""}
    answer = ""

for word in words:
if word[:-1] notin built:
continue

        built.add(word)

if len(word) > len(answer):
            answer = word

return answer

这个题不难,难的是别被“最长”两个字带跑。

真正要看的,是这个单词有没有从第一块砖开始,一层一层垒起来。