到金字塔打印的參考答案)
被問得最多的永遠是《Python語言程序設計基礎》第3章程序部分的練習題答案。這一章恰好是很多人從“看得懂語法”到“寫得出來程序”的分水嶺。前兩章背概念、抄代碼還能應付到了第3章題目開始要求你自己拆問題、寫邏輯、調(diào)錯很多人第一次在這里卡住。這篇博文把第3章里最常布置的程序題答案整理成一套能直接運行、帶逐行解釋的參考實現(xiàn)覆蓋水仙花數(shù)、九九乘法表、閏年判斷、斐波那契數(shù)列、最大公約數(shù)、素數(shù)輸出、猜數(shù)字、字符串統(tǒng)計和金字塔打印。所有代碼只用Python標準庫任意支持Python 3的編輯器都能直接跑。適合剛學完基礎語法、正在趕作業(yè)復習的同學也適合已經(jīng)寫完但想對比寫法的初學者。代碼可以抄但更重要的是看我為什么這么寫、哪些位置容易踩坑。1. 先說清楚第3章程序題到底在練什么1.1 這些題目為什么容易看懂卻寫不出來很多初學者有同樣的困惑教材里每個知識點都認識input、print、int()、if、for、while單獨拿出來都知道是干什么的可真拿到一道“求水仙花數(shù)”“輸出九九乘法表”的題腦子就一片空白。這是因為語法知識和程序題之間缺了一層“拆解能力”。程序題本質(zhì)上是在考“把解決過程翻譯成代碼步驟”的能力。我經(jīng)常用做飯來類比語法相當于認識各種食材和調(diào)味料但一道菜最終好不好吃取決于你什么時候下鍋、先炒什么、后放什么、火候怎么控。第3章的題就是這種“番茄炒蛋級”的練習看著簡單但少一個步驟、順序換一下結果就完全不對。拿水仙花數(shù)來說很多人第一反應是“我知道要判斷但不知道怎么寫”原因就是沒有先想清楚怎么從三位數(shù)里取出百位、十位、個位。取位這個動作本質(zhì)上就是整除和取余的組合。一旦你把“取百位”翻譯成num // 100把“取個位”翻譯成num % 10代碼自然就寫出來了。所以做這章題之前先別急著打開編輯器拿筆在紙上寫三件事輸入是什么、處理過程分幾步、輸出是什么。這三件事寫清楚了代碼只是翻譯問題。1.2 做題前必須準備好的思維工具箱我自己做這章題時會刻意準備一套固定的“思維工具包”。你可以在做題前先過一遍后面每道題都往這幾個方向靠。第一明確循環(huán)結構的選擇。如果知道循環(huán)次數(shù)比如“輸出1到100之間”“輸出9行”優(yōu)先用for如果不知道次數(shù)比如“一直猜直到猜對”“一直除直到余數(shù)為0”就用while。這個選擇不是死規(guī)矩但能幫你少走彎路。第二警惕邊界條件。第3章的題特別喜歡考邊界閏年判斷里1900和2000的區(qū)別、九九乘法表里內(nèi)層循環(huán)到i而不是永遠到9、斐波那契數(shù)列里n1和n2的情況。很多代碼看起來沒問題一測邊界就廢就是因為做題時沒有主動想“最特殊的情況是什么”。第三每寫一個循環(huán)先回答三個問題循環(huán)變量是誰每輪之后往哪走什么時候停下來這個習慣能從根源上避免死循環(huán)和循環(huán)次數(shù)錯亂。我后面拆題時會反復用這套工具你跟著過一遍就會感覺到它比死記代碼實用得多。2. 基礎經(jīng)典題參考答案與逐行拆解2.1 水仙花數(shù)取位運算第一次真正派上用場題目要求通常是這樣輸出100到999之間所有的水仙花數(shù)。所謂水仙花數(shù)就是一個三位數(shù)各位數(shù)字的立方之和等于它本身比如153因為 (1^3 5^3 3^3 153)。這道題的核心不在“立方”或者“判斷”而在“怎么把三位數(shù)的每一位取出來”。百位最好辦直接用整除num // 100比如153除以100取整得到1。個位也簡單用取余num % 10153對10取余得到3。容易出錯的是十位很多新手會寫num / 10 % 10在Python 3里這得到的是浮點數(shù)跟整數(shù)比較時容易出問題。正確寫法是num // 10 % 10先整除把數(shù)字變成兩位數(shù)再取余得到十位數(shù)。參考代碼for num in range(100, 1000): bai num // 100 shi num // 10 % 10 ge num % 10 if bai ** 3 shi ** 3 ge ** 3 num: print(num)運行后應該是四個數(shù)153、370、371、407。這里有一個特別常見的錯誤把寫成。是賦值才是判斷相等。如果你寫成了if bai ** 3 shi ** 3 ge ** 3 num:解釋器會直接報語法錯誤報錯位置通常在賦值號附近看到類似報錯先檢查這里。 另外判斷立方時個人更推薦**而不是連乘三個數(shù)因為可讀性好也少寫括號。2.2 九九乘法表嵌套循環(huán)不是靠死記九九乘法表的經(jīng)典問法是輸出左下三角結構第一行1個算式第二行2個算式直到第九行9個算式。這道題幾乎必考同時也很容易寫到一半就跑偏。核心思路是兩層循環(huán)外層循環(huán)控制行從1到9內(nèi)層循環(huán)控制每一行里打印幾個算式。內(nèi)層的循環(huán)范圍不是固定的1到9而是從1到當前行號i。這一步想通了整道題就通了。for i in range(1, 10): for j in range(1, i 1): print(f{j}x{i}{i * j}, end\t) print()這里有兩個細節(jié)值得注意。第一print(f{j}x{i}{i * j}, end\t)里的end\t表示每個算式之間用制表符分隔而不是默認的換行。很多新手把end忘了結果一個算式占一行整張表變成一列。第二內(nèi)層循環(huán)結束后必須有一個不帶end參數(shù)的print()來換行否則所有行都會擠在同一行里。這個換行的print()要放在內(nèi)層循環(huán)外面、外層循環(huán)里面位置錯了輸出就會亂。用f-string格式化時{j}x{i}{i * j}輸出的是“1x11”“1x22”這樣的格式。如果你的運行環(huán)境是Python 3.6以下不支持f-string可以改成print(str(j) x str(i) str(i * j), end\t)或者print({}x{}{}.format(j, i, i * j), end\t)。 但我還是建議直接換到Python 3.x新版本這種寫法簡潔得多。我見過很多人在這一題上反復改錯最后發(fā)現(xiàn)是外層range(1, 10)寫成了range(10)導致多出一行0開頭的算式或者i 1寫成了i導致每一行都少最后一個算式。這種邊界問題做的時候一定要心里默念循環(huán)范圍取不到終點想包含終點必須1。2.3 判斷閏年邏輯優(yōu)先級是這道題的唯一難點判斷閏年的規(guī)則大家都知道能被4整除但不能被100整除或者能被400整除。寫成代碼只有一行判斷但這一行里藏著這章最容易忽視的邏輯優(yōu)先級問題。year int(input(請輸入年份)) if (year % 4 0 and year % 100 ! 0) or (year % 400 0): print(year, 是閏年) else: print(year, 不是閏年)我建議在and和or組合的時候把括號寫清楚。雖然Python中and的優(yōu)先級高于or即使不寫括號year % 4 0 and year % 100 ! 0 or year % 400 0也會按你期望的順序執(zhí)行但寫成括號之后讀起來更直觀也不容易在多種條件下搞混。為了省一個字符而犧牲可讀性不值得。這一題的正確性必須用邊界年份驗證1900年能被4整除但不能被400整除所以不是閏年2000年能被400整除是閏年2024年能被4整除且不能被100整除是閏年。如果程序把1900判斷成閏年說明條件里丟掉了一半如果把2000判斷成不是閏年說明把“能被400整除”的條件寫到了and里。用這三個年份一測題目基本就能下結論了。另外有一點要注意year % 100 ! 0是“不能被100整除”的意思不要寫成not year % 100 0雖然邏輯等價但可讀性很差。代碼是給人看的盡量寫直觀的判斷條件。3. 算法遞進題循環(huán)、遞推與函數(shù)的組合應用3.1 斐波那契數(shù)列前n項怎么輸出最干凈斐波那契數(shù)列的題目很常見但版本很多有的是求第n項有的是輸出前n項。我先給前n項版本的參考答案因為它的寫法更經(jīng)典也能講清遞推思想。斐波那契數(shù)列的遞推關系是第一項和第二項都是1從第三項開始每一項等于前兩項之和。所以 (1, 1, 2, 3, 5, 8, 13, ...)n int(input(請輸入要輸出的項數(shù))) if n 0: print(n 必須是正整數(shù)) else: a, b 1, 1 for _ in range(n): print(a, end ) a, b b, a b print()這段代碼的核心是a, b b, a b。這一行的意思是先把a賦值為原來的b再把b賦值為原來的a b。兩個賦值是同時完成的Python會先算右邊的兩個值再分別賦給左邊的變量。這里有一個幾乎所有初學者都會踩的坑如果你習慣性地拆成兩行寫a b b a b那b就變成了b b整個數(shù)列會變成 (1, 1, 2, 4, 8, 16...)完全錯誤。原因是第二行執(zhí)行時a已經(jīng)不是原來的a而是被上一句改成了b。所以我每次講到這道題都會特別強調(diào)遞推更新需要“同時拿到舊值”Python的元組賦值就是為了解決這種問題。除了實現(xiàn)細節(jié)你還可以觀察一下為什么循環(huán)里能直接用print(a)因為a在每輪剛開始時正好是“當前這一項”打印完再推下一個循環(huán)次數(shù)正好等于項數(shù)。如果題目要求的是“求第n項”可以改成下面的寫法n int(input(請輸入 n)) if n 0: print(n 必須是正整數(shù)) elif n 1 or n 2: print(1) else: a, b 1, 1 for _ in range(n - 2): a, b b, a b print(b)這里循環(huán)n - 2次是因為第一項、第二項已經(jīng)直接確定了從第三項開始遞推循環(huán)正好推進到第n項。3.2 最大公約數(shù)輾轉(zhuǎn)相除比暴力遍歷好在哪最大公約數(shù)的題目最容易想到的解法是從較小的那個數(shù)開始往下降找到第一個能同時整除兩個數(shù)的數(shù)字。比如求12和8的最大公約數(shù)從8開始試8不行7不行6不行5不行4可以答案是4。這種方法叫暴力遍歷對小數(shù)字沒問題但遇到兩個比較大的數(shù)效率肉眼可見地差。教材第3章更想讓你體會的是輾轉(zhuǎn)相除法。它的原理用一句話說兩個數(shù)的最大公約數(shù)等于較小數(shù)和“較大數(shù)除以較小數(shù)的余數(shù)”的最大公約數(shù)。用數(shù)學式子寫就是 (gcd(a, b) gcd(b, a % b))不斷套用直到余數(shù)為0最后一個非零余數(shù)就是答案。a int(input(請輸入第一個數(shù))) b int(input(請輸入第二個數(shù))) x, y abs(a), abs(b) while y ! 0: x, y y, x % y print(f最大公約數(shù)是{x})代碼里的while y ! 0是這道題最容易繞暈的地方。每一輪循環(huán)里原來的y變成新的x原來的x % y變成新的y。可以拿18和24手動跑一遍第一輪x24, y1818不等于0執(zhí)行完變成x18, y6第二輪x18, y66不等于0執(zhí)行完變成x6, y0第三輪y0循環(huán)結束輸出x6。答案正確。為什么用abs()因為教材題目一般只給正整數(shù)但萬一遇到負數(shù)輸入直接取余在Python里的結果會讓你困惑。先轉(zhuǎn)換成絕對值保證計算過程全部在非負整數(shù)上進行。如果兩個數(shù)都是0數(shù)學上沒有意義這個程序會輸出0所以做輸入校驗時最好提醒自己限制為正整數(shù)也可以循環(huán)判斷直到用戶輸入合法為止。3.3 素數(shù)輸出范圍優(yōu)化與函數(shù)封裝素數(shù)題有各種問法最常見的是“輸出2到N之間所有的素數(shù)”。這道題能同時練到循環(huán)、判斷和函數(shù)封裝屬于第3章里綜合度較高的一道。先說判斷素數(shù)的樸素做法一個數(shù)如果只能被1和它本身整除就是素數(shù)。判斷時從2試到這個數(shù)本身減1有一個能整除就說明不是素數(shù)。但這樣試到n-1太浪費了。優(yōu)化點是不需要試到n-1只需要試到 (\sqrt{n}) 就夠了。原因是如果 (n a \times b)那么 (a) 和 (b) 中一定有一個不大于 (\sqrt{n})如果存在因子在 (\sqrt{n}) 之前就能找到。import math def is_prime(num): if num 2: return False if num 2: return True if num % 2 0: return False limit int(math.sqrt(num)) 1 for i in range(3, limit, 2): if num % i 0: return False return True n int(input(請輸入一個正整數(shù))) for num in range(2, n 1): if is_prime(num): print(num, end ) print()這里做了幾個層次的處理。先排除小于2的數(shù)和偶數(shù)再單獨處理2因為2是唯一的偶數(shù)素數(shù)也正好是素數(shù)判斷的起點。處理完這些特殊情況后循環(huán)從3開始步長為2只測試奇數(shù)循環(huán)次數(shù)進一步減半。如果你還沒學到函數(shù)也沒關系可以把is_prime內(nèi)部的邏輯直接展開在主循環(huán)里但代碼會顯得很亂。我更建議把“判斷一個數(shù)是否為素數(shù)”封裝成一個函數(shù)因為輸出素數(shù)這個任務本身就由兩層邏輯組成外層遍歷數(shù)字內(nèi)層判斷屬性。用函數(shù)把內(nèi)層邏輯獨立出來主程序就一行if is_prime(num)讀起來清爽得多。等學到后面章節(jié)你會越來越依賴這種“拆成小函數(shù)”的思維。4. 綜合實戰(zhàn)題隨機數(shù)、字符處理與循環(huán)嵌套4.1 猜數(shù)字游戲循環(huán)條件與輸入校驗的配合猜數(shù)字是綜合題里的熟面孔程序先生成一個1到100的隨機整數(shù)用戶去猜程序提示“大了”或“小了”猜對后顯示猜的次數(shù)。這題既練while循環(huán)的“不確定次數(shù)”結構也練輸入處理和比較判斷。import random target random.randint(1, 100) count 0 print(我想好了一個 1 到 100 之間的整數(shù)開始猜吧。) while True: text input(請輸入你的猜測) if not text.isdigit(): print(請輸入正整數(shù)) continue guess int(text) count 1 if guess target: print(太小了再大一點) elif guess target: print(太大了再小一點) else: print(你猜對了一共猜了, count, 次) breakrandom.randint(1, 100)生成的是包含1和100在內(nèi)的隨機整數(shù)很多初學者會寫成random.randrange(1, 100)那樣100就永遠不會出現(xiàn)了。這是非常典型的邊界錯誤值得記一下。循環(huán)結構上因為不知道用戶要猜多少次我用的是while True加break的寫法。每猜一次count 1就必須執(zhí)行一次。這句話的位置不要放在判斷猜對之后再執(zhí)行否則正確的那一次不會被計入。我在調(diào)試類似代碼時經(jīng)常看到這種情況最終答案往往是“次數(shù)少了一次”。代碼里還加了一層輸入校驗text.isdigit()用來判斷用戶輸入的字符串是不是全由數(shù)字組成。如果不是就提示用戶重新輸入并用continue跳回循環(huán)開頭不增加猜的次數(shù)。如果沒有這一步用戶輸入一個非數(shù)字字符int(text)會直接拋出異常。教材第3章可能還沒教異常處理所以isdigit()是現(xiàn)階段比較友好的防錯辦法。4.2 字符串統(tǒng)計字符類型的隱藏細節(jié)字符串統(tǒng)計題通常這樣出輸入一行字符統(tǒng)計其中英文字母、數(shù)字、空格和其他字符的個數(shù)。題目不難但有幾個隱藏細節(jié)特別容易讓“看起來對”的代碼實際出錯。比較穩(wěn)妥的寫法是text input(請輸入一行字符串) letters digits spaces others 0 for ch in text: if (a ch z) or (A ch Z): letters 1 elif 0 ch 9: digits 1 elif ch : spaces 1 else: others 1 print(英文字母個數(shù), letters) print(數(shù)字個數(shù), digits) print(空格個數(shù), spaces) print(其他字符個數(shù), others)為什么我不直接推薦ch.isalpha()和ch.isdigit()因為Python的isalpha()是按Unicode字母判斷的漢字也會被判為“字母”。如果題目要求統(tǒng)計“英文字母”用isalpha()統(tǒng)計出來的數(shù)字會把中文也算進去一看就錯。同理isdigit()對全角數(shù)字、上標數(shù)字等Unicode數(shù)字字符也可能返回True比如2.isdigit()結果是True。所以當題目明確要求“英文字母”和“數(shù)字字符”時直接用字符范圍比較更符合題目的語義。空格判斷我用的是ch 而不是ch.isspace()因為isspace()會把制表符\t、換行符\n等全部算進去如果題目只是讓統(tǒng)計空格用空格字面量最準確。當然如果老師要求所有空白字符都算“空格”那用isspace()也說得過去。做題時先看清題目要求這個習慣比代碼本身重要。4.3 金字塔打印嵌套循環(huán)的變式訓練九九乘法表練了嵌套循環(huán)金字塔打印是同一類思想的進階變式。常見題目是輸入層數(shù)n輸出一個由星號組成的等腰三角形。觀察規(guī)律是這道題的關鍵。第1行有1個星號第2行有3個星號第3行有5個星號。第 (i) 行星號數(shù)量是 (2i - 1)。為了讓三角形居中第 (i) 行前面還要放 (n - i) 個空格。n int(input(請輸入層數(shù))) for i in range(1, n 1): print( * (n - i) * * (2 * i - 1))這個代碼非常短但如果不理解“空格數(shù)加星號數(shù)”的規(guī)律第二反應就是想著用兩層循環(huán)去逐個打印字符。實際上Python的字符串乘法 * (n - i)可以直接生成指定數(shù)量的空格整個輸出一下子變得很干凈。等學到字符串這一章節(jié)你會覺得這個操作很自然但在第3章里能主動用上說明你已經(jīng)有了“先觀察規(guī)律再選工具”的意識。如果題目要求的是靠左的直角三角形那就更簡單n int(input(請輸入層數(shù))) for i in range(1, n 1): print(* * i)這兩道變式題的背后都是同一個思想把“每行輸出什么”抽象成一個與行號 (i) 有關的表達式而不是機械地一層層寫循環(huán)。5. 運行報錯排查與作業(yè)避坑速查5.1 縮進、類型、死循環(huán)三類高頻報錯每次一到第3章練習群里問報錯最多的問題基本可以歸成三類縮進錯誤、類型錯誤、死循環(huán)。我挨個說建議你對照自查。第一類是縮進錯誤。Python用縮進表示代碼塊if、for、while下面一行如果沒縮進解釋器會直接報IndentationError。更隱蔽的是混用Tab和空格明明看起來對齊了一運行就報錯。我的建議是編輯器統(tǒng)一設置成“把Tab自動轉(zhuǎn)換成4個空格”然后整個文件只用縮進不用Tab。絕大多數(shù)現(xiàn)代編輯器默認都這么干但如果你在寫作業(yè)時從網(wǎng)頁復制代碼最容易把Tab混進來。遇到縮進報錯先把該代碼塊全部選中用編輯器轉(zhuǎn)換成空格。第二類是類型錯誤。這種錯誤通常不是解釋器直接報TypeError而是邏輯悄悄錯。比如input()返回的是字符串直接拿去和整數(shù)比較Python 3里兩個類型不同會拋TypeError: not supported。但更隱蔽的是第2.1節(jié)提到的情況用/而不是//取十位得到浮點數(shù)導致后面的立方和比較結果不對。遇到類型問題先搞清楚每個變量的類型必要時用type()打印出來看一下。第三類是死循環(huán)。常見場景有while循環(huán)里忘了更新循環(huán)變量比如做最大公約數(shù)時少了x, y y, x % y程序卡住不動猜數(shù)字循環(huán)里忘了break猜對了也不跳出還有循環(huán)條件是while True但沒有出口。調(diào)試時如果程序一直不結束先按CtrlC強制中斷然后檢查循環(huán)體里是否有讓條件最終變?yōu)镕alse的語句。5.2 常見問題速查表第3章的程序題來來去去就是那幾個坑我整理了一張速查表做完題對照檢查一遍比自己瞎試快得多。問題現(xiàn)象常見原因解決方法IndentationError: unexpected indentTab與空格混用或不該縮進的地方縮進了全選代碼統(tǒng)一轉(zhuǎn)成空格檢查代碼塊層級TypeError: not supported between instances of str and intinput()返回值沒有轉(zhuǎn)成整數(shù)用int(input(...))包裹水仙花數(shù)十位算錯用了/而不是//十位寫成num // 10 % 10九九乘法表每個算式都換行內(nèi)層print沒寫end參數(shù)加end\t九九乘法表每行缺最后一個算式內(nèi)層循環(huán)范圍寫成range(1, i)改成range(1, i 1)閏年判斷1900年輸出是閏年漏了“不能被100整除”的條件用完整規(guī)則year % 4 0 and year % 100 ! 0斐波那契數(shù)列輸出變成1、1、2、4、8把a, b b, a b拆成兩行賦值用元組同時賦值或先保存舊值素數(shù)判斷把4也算成素數(shù)循環(huán)范圍沒加上1導致sqrt(4)開根后只試到2范圍寫int(math.sqrt(num)) 1猜數(shù)字猜對了但次數(shù)少1count 1放在break之后把計數(shù)語句放在判斷之前字符串統(tǒng)計中文字符被算成字母用isalpha()判斷英文字母改用a ch z or A ch Z這張表不可能覆蓋所有報錯但覆蓋了第3章練習題里出現(xiàn)頻率最高的幾種。如果你遇到不在這張表里的問題我強烈建議你用最笨的辦法把關鍵變量用print()打出來看每一步的值是否符合預期。程序就是一步一步跑出來的打印中間結果永遠是最直接的調(diào)試方式。5.3 我的做題習慣和復查順序最后分享一下我自己做這章題時的復查習慣。很多人寫完代碼運行一次看到輸出沒問題就交作業(yè)了但程序題的邊界往往不是第一眼能看出來的。我的復查順序很固定。第一步跑一遍標準答案。水仙花數(shù)要看到153、370、371、407四個結果九九乘法表要數(shù)一下第一行一個算式、第九行九個算式閏年判斷要分別輸入1900、2000、2024。第二步測邊界值。斐波那契分別輸入1、2、10素數(shù)輸出分別輸入2、3、4金字塔分別輸入1、5。這些邊界值最容易暴露問題。第三步如果時間充裕我會把同一道題換一種寫法再實現(xiàn)一遍比如把for改成while或者把判斷邏輯封裝成函數(shù)。換一種寫法不是為了炫技而是逼自己重新梳理一遍邏輯。很多時候你覺得代碼“能跑了”其實只是碰巧輸入沒有踩到雷換一種寫法就會發(fā)現(xiàn)自己對這道題的理解還不夠透徹。這個習慣說起來簡單但確實幫我避免了很多次“交作業(yè)時沒問題上課展示時當場翻車”的尷尬。你可以挑一兩道題試試尤其挑那些你做的時候有點猶豫的題。說回第3章本身。這些練習題看起來基礎但它們真正想訓練的是你對“輸入、處理、輸出”這一整套流程的把控能力。后面學到列表、字典、字符串方法時你會發(fā)現(xiàn)第3章練出來的循環(huán)思維和調(diào)試敏感度比任何一道具體的答案都值錢。所以就算你現(xiàn)在的代碼還寫得不漂亮只要能自己動手把每一題跑通、想明白為什么錯就已經(jīng)達到這一章的核心目標了。