計)
前兩天在社群里看到有人問零基礎學Python到底刷什么題我打開LeetCode第一題就看不下去。這個問題我太有發(fā)言權了。我?guī)н^的零基礎同學幾乎全是從同一組題入門的李白打酒和英文詞頻統(tǒng)計。這兩道python編程題一道練邏輯、一道練數(shù)據(jù)難度剛好卡在能看懂、要動腦、寫完有成就感的位置。文章不長但我把從讀題、推導、寫代碼到踩坑的完整過程都放進來了無論你是剛開始學還是已經(jīng)會寫點基礎語法想找手感都可以按這個流程走一遍。1. 為什么是這兩道題一本邏輯題一本數(shù)據(jù)題1.1 李白打酒一道連初中生都能看懂、程序員卻能翻車的題李白打酒是網(wǎng)上流傳很廣的一道古典算術題原文大概是李白無事街上走提壺去買酒。遇店加一倍見花喝一斗。三遇店和花喝光壺中酒。試問壺中原有多少酒這道題最妙的地方在于題目本身只有幾句話不需要任何專業(yè)背景小學文化就能讀懂。但真正動手寫出代碼后你會發(fā)現(xiàn)自己面臨的不只是算出來這么簡單而是怎么把文字邏輯轉成循環(huán)和分支結構怎么處理最后一次狀態(tài)已知初始狀態(tài)未知的逆向推導。很多人第一反應是設未知數(shù)列方程但題目換一個表述比如遇店加兩倍四遇店和花方程就亂套了。用代碼來解決其實更通用、更穩(wěn)。1.2 詞頻統(tǒng)計把工程能力藏進一道小學題里英文詞頻統(tǒng)計這題看起來也簡單給你一段英文文本統(tǒng)計每個單詞出現(xiàn)多少次輸出出現(xiàn)次數(shù)最多的前幾個單詞。但真正動手做的時候你會同時遇到字符串清洗、大小寫歸一化、字典計數(shù)、排序、切片五個基礎任務。這些任務單獨拿出來都不難合在一起偏偏能卡住一大片初學者——不是語法不會而是先干什么后干什么的流程感沒有建立。這兩道題放在一起恰好覆蓋了Python學習路徑上最重要的兩條分支一條是邏輯推導一條是數(shù)據(jù)處理。前者訓練你的腦子后者訓練你的工程手感。很多教程讓你刷五十道題其實不如把這兩道題老老實實推一遍、寫一遍、改三遍。1.3 兩道題覆蓋的語法點對照我做了個簡單的對照表方便你自查知識點李白打酒詞頻統(tǒng)計掌握等級循環(huán)結構倒推循環(huán)、窮舉驗證遍歷單詞列表必會分支結構判斷遇店還是遇花判斷字符類型必會函數(shù)定義函數(shù)封裝與參數(shù)傳遞構造可復用統(tǒng)計函數(shù)必會列表操作正向序列模擬split切分、列表推導必會數(shù)組切片不涉及items[:top_n] 取前三必會字典操作不涉及單詞計數(shù)、get默認值必會類型轉換整數(shù)和浮點數(shù)、分數(shù)lower、字符串與列表常踩坑正則/高級模塊不涉及re模塊、Counter進階如果你能不看答案把這兩道題都寫出來說明你至少可以獨立開始做小型數(shù)據(jù)處理和簡單的算法練習題了。2. 第一道李白打酒逆向遞推的完整推導2.1 題意里的那個坑三遇店和花到底怎么理解我第一次做這道題就栽在了題意理解上。題目說三遇店和花很多人的第一反應是先遇三次店再遇三次花。但細想一下如果連著遇到三次店酒量翻倍翻倍再翻倍再連續(xù)喝三次也能喝光但這不符合見花喝一斗的隨機性。更符合文字原意的理解是李白每走一段路遇到一次店酒量加倍走一段路遇到一次花喝掉一斗這個過程重復三次也就是操作順序是店、花、店、花、店、花。這個理解非常關鍵因為它直接決定你要寫的循環(huán)結構是什么樣。市面上有些版本的題把三遇店和花解釋成一次店一次花算一遇三次就是三種店花交替那代碼里的循環(huán)就要寫成偶數(shù)次操作。還有版本說遇店加一倍遇花喝一斗不分順序隨機遇到六次。所以拿到編程題的第一步永遠是把題意中的隱含規(guī)則敲死不然代碼寫得再漂亮都是錯的。2.2 從喝光壺中酒往回收逆向推演已知最后酒量為0斗最后一步是見花喝一斗。那我就把整個流程倒過來看原來遇店加一倍的逆操作是除以2原來見花喝一斗的逆操作是加上1斗。從最終狀態(tài)0倒著往前推遇到花的逆操作就加一遇到店的逆操作就除以2。具體推一下初始未知假設為x。第一次遇店酒變2x。第一次見花酒變2x-1。第二次遇店酒變4x-2。第二次見花酒變4x-3。第三次遇店酒變8x-6。第三次見花酒變8x-7 0。解得x 7/8斗。也就是說李白出門時壺里只有不到一斗酒。用逆向推更直接最后一次喝光前酒量必須是1斗再往前推一步遇店加一倍之前酒量必須是0.5斗繼續(xù)往回收最后能得到7/8斗。這個過程用循環(huán)來表達比解方程更貼合編程思維。2.3 一口氣寫出能跑的版本我習慣先把操作序列顯式寫出來再去寫循環(huán)。代碼如下sequence [店, 花, 店, 花, 店, 花] wine 0.0 for step in reversed(sequence): if step 店: wine wine / 2 # 遇店加一倍倒推時除以2 else: wine wine 1 # 見花喝一斗倒推時加回一斗 print(wine) # 輸出 0.875這里用reversed(sequence)把正向流程倒過來遍歷。我每次給同學講這段代碼都要強調(diào)一點倒推時先判斷是店還是花再決定做除法還是加法別把順序寫反了。有人直接把操作序列寫成[花, 店, 花, 店, 花, 店]再正向遍歷效果是一樣的但思路一定要說清楚。2.4 把解法打包成函數(shù)再寫一個正向驗證光算出7/8斗還不夠穩(wěn)我還會加一個正向驗證函數(shù)從7/8開始模擬六步確認最后結果為0。這也是老手寫代碼的習慣逆向算完正向驗證雙重保險。def li_bai_wine(rounds3): 通過逆向遞推計算初始酒量 wine 0.0 for _ in range(rounds): wine 1 # 逆花 wine / 2 # 逆店 return wine def verify_wine(initial, rounds3): 正向模擬店花交替驗證酒是否喝光 wine initial for i in range(rounds * 2): if i % 2 0: wine * 2 # 遇店 else: wine - 1 # 見花 return wine init li_bai_wine(3) print(init) # 0.875 print(verify_wine(init)) # 0.0這里當i % 2 0時執(zhí)行遇店操作剛好模擬店花店花店花的順序。如果把rounds3換成4就變成四遇店和花代碼不需要大改這就是封裝成參數(shù)的價值。你在寫這類題的時候也可以試著把可變的部分全部抽成參數(shù)以后題目稍微一變你改一行就行。3. 第二道英文文本詞頻統(tǒng)計Top N3.1 需求看起來簡單拆開其實有四步詞頻統(tǒng)計的核心需求一句話就能說清楚輸入一段英文文本輸出出現(xiàn)次數(shù)最多的前N個單詞。但這句話背后藏著四個獨立步驟清洗文本把標點符號、換行符去掉統(tǒng)一大小寫讓Python和python算同一個詞切分單詞并計數(shù)用字典記錄每個詞出現(xiàn)次數(shù)排序并切片按頻次降序拿到Top N。我見過太多人直接拿一個變量count從頭數(shù)到尾數(shù)到一半發(fā)現(xiàn)單詞太多了根本沒法數(shù)。這是因為沒有先把整體流程拆成輸入、處理、輸出三個環(huán)節(jié)。寫代碼之前先問自己三個問題輸入長什么樣中間要做哪些變換最終要得到什么格式的結果這一步想清楚后面基本不會跑偏。3.2 第一版實現(xiàn)換標點、切詞、字典計數(shù)、排序切片我給出一個很樸素的版本完全不用任何高階模塊適合零基礎理解def top_words(text, top_n3): # 第一步把標點和換行統(tǒng)一替換成空格 for ch in ,.!?;:()\-\n: text text.replace(ch, ) # 第二步統(tǒng)一小寫并切分成單詞列表 words text.lower().split() # 第三步字典計數(shù) freq {} for word in words: freq[word] freq.get(word, 0) 1 # 第四步排序頻次高的在前頻次相同按字母順序 items sorted(freq.items(), keylambda x: (-x[1], x[0])) # 第五步切片取前 top_n 個 return items[:top_n]測試用的文本我故意寫了一個需要清洗的版本sample_text Python is powerful, Python is easy, and Python is fun. Python is also popular. Python makes data science easier! for word, count in top_words(sample_text, 3): print(f{word}: {count})運行結果python: 5 is: 3 and: 1注意第三名的處理。頻次為1的詞有一堆我給排序規(guī)則加上了(頻次降序, 字母升序)所以and排在了easy、fun、popular前面。如果你不在意并列詞的順序也可以只用keylambda x: x[1]加一個reverseTrue但推薦做成穩(wěn)定的排序方便復現(xiàn)結果。3.3 借這道題把切片和排序的細節(jié)徹底搞明白items[:top_n]這行代碼就是數(shù)組切片最典型的用法取列表前N個元素。切片的完整語法是列表[start:stop:step]省略start表示從開頭取省略stop表示取到結尾[:3]就是取索引0、1、2三個元素。很多教程把切片講得很玄其實你把它理解成復制一份列表的指定區(qū)間就行。排序這里其實還有個小坑freq.items()返回的是字典的鍵值對視圖直接對它調(diào)用sorted()會得到一個按字典序排序的鍵值對列表而不是按頻次排序。所以必須用key參數(shù)告訴Python按哪個字段排。lambda x: (-x[1], x[0])的意思是對每個元素x一個元組x[1]是頻次x[0]是單詞頻次取負就能實現(xiàn)降序頻次相同時再按單詞升序。這個技巧在數(shù)據(jù)分析、日志統(tǒng)計里特別常用值得記下來。3.4 換用Counter和正則代碼少一半當你理解了上面的基礎版就可以換個更高效的寫法了。Python的collections.Counter專門用來統(tǒng)計可哈希對象的頻次re.findall可以一次性提取所有符合條件的單詞from collections import Counter import re def top_words_fast(text, top_n3): words re.findall(r[A-Za-z], text.lower()) return Counter(words).most_common(top_n)這段代碼只有三行但做的事和前面的函數(shù)一模一樣。re.findall用正則表達式把字母和撇號全部挑出來跳過數(shù)字和其他符號text.lower()統(tǒng)一轉為小寫Counter(words).most_common(top_n)一行完成計數(shù)和取TopN。如果你在別人的項目里看到這種寫法別懵它背后就是剛才那五步的壓縮版。3.5 我在這道題上見過最多的三類錯誤第一類不做大小寫歸一化。Python.lower()變成python否則同一個詞會被統(tǒng)計成兩個詞。第二類沒清洗標點fun.和fun會被當成兩個詞導致明明只有一個單詞卻出現(xiàn)兩次。這類錯誤特別隱蔽因為你看輸出時容易忽略句號的存在。第三類手動計數(shù)時忘了處理字典中不存在的鍵直接寫freq[word] 1程序一運行就報KeyError。正確姿勢是freq.get(word, 0) 1get方法在鍵不存在時返回默認值0。我建議新手第一版用最原始的方式逐步寫出看看每一步的輸出是什么比如先只執(zhí)行text.lower().split()并打印再執(zhí)行計數(shù)并打印最后再排序。逐步調(diào)試一遍比你悶頭寫十遍都有用。4. 兩道題背后的通用解題框架和排坑清單4.1 五步解題流程兩道題都在驗證同一套打法這兩道題看完了你會發(fā)現(xiàn)它們的解題流程驚人地一致。我已經(jīng)把這套流程固定成了做編程題的五步分享給你列出已知條件。李白打酒已知終態(tài)為0、店花交替三次詞頻統(tǒng)計已知輸入是文本、輸出是前N個單詞。把條件列出來等于把題目翻譯成了程序語言的變量。確定核心數(shù)據(jù)結構。李白打酒需要一個浮點數(shù)變量存酒量詞頻統(tǒng)計需要一個字典存頻次。數(shù)據(jù)結構選對了寫出來就是順暢的選錯了就會越寫越亂。先寫最樸素的版本。不要一開始就想我要寫得多優(yōu)雅先用if加for把邏輯跑通結果對了再談優(yōu)化??紤]邊界情況。詞頻統(tǒng)計里空文本怎么辦只有標點沒有單詞怎么辦李白打酒里rounds傳0會怎樣。加了邊界處理你的代碼才敢放到真實場景。反向驗證結果。李白打酒逆向算完接著正向跑一遍詞頻統(tǒng)計打印一下原始words列表看一眼。驗證不是浪費時間是讓你睡個好覺。4.2 高頻BUG復盤表我在答疑過程中收集了一些初學者特別容易踩的坑做成表格供你對照錯誤類型典型表現(xiàn)原因解決方式索引越界IndexError: list index out of range循環(huán)里取words[i1]沒判斷長度用for word in words遍歷少用手動索引變量覆蓋統(tǒng)計結果永遠是0或1在循環(huán)體里重置了計數(shù)變量打印循環(huán)內(nèi)變量值檢查語句縮進層級類型拼接TypeError: can only concatenate str試圖用拼接字符串和數(shù)字先str(count)再拼接或用f-string原地刪除遍歷列表時刪元素導致跳過for w in words里直接remove(w)改用新列表保存結果或倒序遍歷字典鍵不存在KeyError對不存在的鍵做運算用get(key, default)大小寫不統(tǒng)一Python和python兩個詞沒有調(diào)用lower()切詞前統(tǒng)一小寫這里特別提一下for w in words里直接remove的問題很多人覺得這是小問題實際上它會讓你漏掉元素因為刪除后列表索引前移循環(huán)卻還在按原索引向后走后面一半元素會被跳過。我的建議是要過濾就構造新列表別在原列表上邊遍歷邊刪。4.3 寫代碼時讓自己像老手的幾個小習慣老手和新手寫的代碼在能跑的前提下最大的區(qū)別是三個一個有名字一個全是a、b、c一個能分步驟驗證一個只能一把梭子從頭跑到尾一個主流程清晰一個所有邏輯全堆在全局代碼里。所以我的建議是變量名用word、freq、wine不用x、y、z每個關鍵步驟后加一個print看一眼中間值確認沒問題再繼續(xù)把核心流程封裝成函數(shù)主程序只留輸入輸出。這些習慣現(xiàn)在看起來有點浪費時間但等你開始寫超過一百行的腳本、需要協(xié)同調(diào)試的時候你就知道它們有多值錢了。5. 做完這兩道題接下來往哪走5.1 李白打酒的變式訓練想要把這道題的價值榨干最簡單的變式就是改參數(shù)。把三遇店和花改成四遇店和花測試你封裝的函數(shù)能否直接復用把見花喝一斗改成見花喝兩斗檢查逆向遞推的加一操作是否改對把遇店加一倍改成遇店加百分之五十驗證倒推時除以1.5而不是除以2。這些變式不需要新知識但能幫助你徹底掌握逆向遞推的邊界條件。如果還想進階可以試試把正向模擬做成一個可交互程序輸入初始酒量程序模擬每一次遇店遇花的過程實時打印壺中剩余酒量。這樣一個命令行小工具就出來了比單純做數(shù)學題有趣得多。5.2 詞頻統(tǒng)計的升級方向詞頻統(tǒng)計的升級方向就很鮮明了。第一步是換成中文文本Python處理中文需要先分詞常用的是jieba庫切出來的詞存進列表之后后面的字典計數(shù)、排序、切片邏輯完全不變。第二步是結合數(shù)據(jù)可視化把Top10單詞畫成柱狀圖用matplotlib幾十行就能出來一張漂亮的圖。第三步是做一個文件版詞頻統(tǒng)計支持傳入一個txt文件路徑自動讀取全文并返回統(tǒng)計結果這樣它就不再是練習題而是一個可以用起來的工具。我個人的經(jīng)驗是一道題刷完至少做一次變式、一次場景升級才算真的吃透了。很多人刷題只求做出來做完就扔過兩周再看代碼跟看天書一樣。你試著把這兩道題改一改、擴展一下你對代碼還能這么寫的體會會比刷十道新題還深。這兩道題最大的價值不在于它們本身有多難而在于它們逼你走完一遍讀題、拆解、實現(xiàn)、驗證、優(yōu)化的完整流程。我?guī)н^的同學里凡是認認真真把這兩道題寫成函數(shù)、加好驗證、跑通變式的后面學Python都明顯順利很多。編程這東西最終拼的不是記了多少API而是你把多少基礎過程變成了手感和直覺。這兩道題就是練手感和直覺的起點。