規(guī)劃與回溯的刷題復(fù)盤筆記)
2026年剛開年那陣子我給自己定了個小目標(biāo)把算法題的手感重新?lián)炱饋怼Uf實話工作之后寫業(yè)務(wù)代碼的時間多了很多基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)和經(jīng)典題型反而生疏了碰到稍微繞一點的題就總想去看題解這讓我有點慌。尤其是看到不少人靠著系統(tǒng)刷題拿到了心儀的 offer或者在代碼質(zhì)量上明顯更穩(wěn)我就更覺得這件事值得認(rèn)真做一次。所以 1 月 21 號到 22 號這兩天我把自己關(guān)在書房里專門集中刷了兩天題順便把過程中的思路、踩坑、時間分配全部記了下來就是標(biāo)題里這份“刷題筆記”。這篇筆記不適合打算三天速成的人它更適合那些跟我一樣想穩(wěn)扎穩(wěn)打把算法基礎(chǔ)補回來、并且真的愿意花時間做復(fù)盤的人。接下來我把這兩天的完整過程、題型總結(jié)、調(diào)試教訓(xùn)和一些真實感受都寫出來希望能給你一些參考。1. 刷題前的準(zhǔn)備目標(biāo)、范圍與節(jié)奏1.1 為什么把目標(biāo)定成“恢復(fù)手感”而不是“題海戰(zhàn)術(shù)”這次刷題之前我認(rèn)真想了一下自己的短板在哪里。如果只是漫無目的地每天刷幾道題看起來努力實際上效率往往很低。因為刷題這件事最關(guān)鍵的不是“量”而是“題型識別”和“思路轉(zhuǎn)換”。比如拿到一道題你能不能第一時間判斷它屬于雙指針、動態(tài)規(guī)劃、回溯還是圖論這決定了后續(xù)的解題速度。所以我給這兩天定的目標(biāo)很簡單不追求一天寫幾十道而是把高頻的核心題型重新過一遍每道題都做透做完之后寫出完整的思路復(fù)盤。具體操作上我把目標(biāo)拆成了三層。第一層是基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)數(shù)組、鏈表、棧、隊列、哈希表這部分主要是恢復(fù)記憶力刷一些經(jīng)典題鞏固一下 API 使用第二層是高頻算法思想二分查找、雙指針、滑動窗口、遞歸、回溯、基礎(chǔ)動態(tài)規(guī)劃這部分是重頭戲花的時間最多第三層是快速過一遍二叉樹相關(guān)的問題因為樹的題目面試出現(xiàn)頻率實在太高了而且很多復(fù)雜問題都能化歸到樹的遍歷上面。兩天的時間我給自己定的量是 20 道題左右每一道都要求能獨立完成并且整理出筆記。1.2 兩天的題目范圍與時間安排我按照自己的精力曲線把時間分成了幾個小塊。上午頭腦清醒適合啃動態(tài)規(guī)劃和回溯這種需要深度思考的題目下午稍微有點疲憊主要做雙指針、哈希表這類套路比較固定的題晚上則用來做二叉樹和整體復(fù)盤。這里我列一下我實際執(zhí)行的時間表時間段刷題主題目標(biāo)完成量第一天上午數(shù)組、哈希表、雙指針6 道第一天下午滑動窗口、前綴和4 道第一天晚上二叉樹遍歷與遞歸4 道第二天上午動態(tài)規(guī)劃入門背包、爬樓梯4 道第二天下午回溯算法組合、排列、子集4 道第二天晚上錯題復(fù)盤與筆記整理整理全部記錄這種安排最大的好處是每個時間段都有一個相對聚焦的主題大腦不需要頻繁切換思維模式進(jìn)入狀態(tài)會更快。很多刷題新手容易犯的一個錯誤是“一天之內(nèi)什么題都刷”比如剛做完一個鏈表題馬上又跳到一個圖論題結(jié)果每道題都剛開了個頭就換了方向第二天回頭一看全忘光了。我用的這種主題式刷法某種程度上相當(dāng)于做了“批量練習(xí)”同一類型的題連續(xù)做幾道以后套路感會非常明顯進(jìn)步也會更快。2. 高頻題型的套路拆解與實操解析2.1 雙指針與滑動窗口從暴力到 O(n) 的思維轉(zhuǎn)變第一天上午我先刷了雙指針。說實話雙指針本身不是一個特定的數(shù)據(jù)結(jié)構(gòu)它是一種優(yōu)化思路。很多數(shù)組、字符串相關(guān)的題目暴力的做法是兩層循環(huán)枚舉復(fù)雜度 O(n^2)但當(dāng)數(shù)據(jù)范圍超過 10^4 的時候O(n^2) 往往就會超時。雙指針的核心思想是利用數(shù)組的有序性或者某種單調(diào)性讓兩個指針分別從兩端或者同向移動在一次遍歷的過程中完成計算。我拿“三數(shù)之和”這道經(jīng)典題舉例。暴力做法需要三層循環(huán)枚舉 i、j、k結(jié)果不僅要處理超時還得費勁去重。用雙指針之后思路變成了先把數(shù)組排序然后固定一個數(shù) nums[i]再用一左一右兩個指針去查找剩下兩個數(shù)。左指針從 i1 開始右指針從末尾開始如果三數(shù)之和大于 0說明右邊的數(shù)太大了右指針左移如果小于 0說明左邊的數(shù)太小了左指針右移等于 0 就記錄答案。排序的復(fù)雜度是 O(n log n)雙指針部分因為是固定一個點再掃一輪整體是 O(n^2)但比三層循環(huán)還是要穩(wěn)定很多而且去重邏輯也更清晰。這道題我印象很深的地方在于去重。很多人寫出來功能是對的但輸出結(jié)果里有重復(fù)的三元組原因就是沒有在找到一個答案之后主動跳過重復(fù)元素。這個細(xì)節(jié)只有在手寫一遍、看到錯誤輸出之后才能真正記住?;瑒哟翱诤碗p指針的關(guān)系非常緊密我下午刷的“無重復(fù)字符的最長子串”就是典型代表。這道題維護(hù)一個窗口右指針不斷向右擴(kuò)展把新字符加入窗口左指針則根據(jù)情況收縮保證窗口內(nèi)沒有重復(fù)字符。這里的關(guān)鍵是你用哈希表記錄每個字符最后出現(xiàn)的位置當(dāng)遇到重復(fù)字符時直接把左指針跳到 max(左指針, 上次出現(xiàn)位置1)而不是老老實實一步一步挪。我在實際寫代碼的時候第一次漏掉了取最大值這一步結(jié)果在某些用例上左指針會往回退直接導(dǎo)致結(jié)果偏大。后來調(diào)試時打印出每一步的左右指針才發(fā)現(xiàn)了問題。這個細(xì)節(jié)其實就是滑動窗口題里最容易出錯的地方。2.2 動態(tài)規(guī)劃狀態(tài)定義比轉(zhuǎn)移方程更重要第二天的重頭戲是動態(tài)規(guī)劃。學(xué) DP 的時候很多人第一反應(yīng)是去背“背包九講”里的轉(zhuǎn)移方程但我自己的體會是如果你能清楚地回答“dp[i] 到底代表什么”這個問題后面代碼幾乎是水到渠成的事。我在刷題筆記里反復(fù)強(qiáng)調(diào)一句話“狀態(tài)定義錯了后面全是白做?!?這句話是怎么來的我拿最經(jīng)典的“爬樓梯”舉例假設(shè)要爬到第 n 階每次可以走 1 步或者 2 步問有多少種不同的方法。如果不假思索很容易寫出 dp[i] dp[i-1] dp[i-2] 這樣的轉(zhuǎn)移但如果問你 dp[i] 是“走到第 i 階的方案總數(shù)”那么 dp[0] 該初始化為 1 還是 0 就會成為第一個坑。很多題解直接說 dp[0]1理由是“0 階也算一種方案”但這對新手來說并不自然。換個定義方式dp[i] 表示“從第 0 階走到第 i 階的方案數(shù)”然后讓 dp[0]1dp[1]1你會發(fā)現(xiàn)遞推關(guān)系依然成立而且更好理解。這就是狀態(tài)定義帶來的連鎖影響。動態(tài)規(guī)劃里第二重要的東西是遍歷順序。還是拿我刷的“最小路徑和”來說給定一個二維網(wǎng)格從左上角走到右下角每次只能向右或者向下走要讓路徑上的數(shù)字總和最小。這是一道非常經(jīng)典的二維 DP 題。狀態(tài)定義很簡單dp[i][j] 表示走到網(wǎng)格 (i, j) 時的最小路徑和轉(zhuǎn)移方程就是從左邊或者上邊較小的那一個再加上當(dāng)前格子的值。但這里有一個新手很容易搞錯的地方初始化邊界時第一行只能從左往右累加第一列只能從上往下累加不能混著來。我曾經(jīng)為了圖快直接在一個雙重循環(huán)里處理所有格子結(jié)果第一行和第一列的值計算錯了整個結(jié)果全盤崩掉。后來我的做法是先把邊界單獨初始化再處理內(nèi)部格子這樣思維負(fù)擔(dān)小代碼也更不容易出 bug。2.3 回溯算法把“撤銷選擇”想明白第二天下下午我集中刷的是回溯?;厮荼举|(zhì)上是深度優(yōu)先搜索的一種特例它的核心就是三個步驟做選擇、遞歸、撤銷選擇。很多人在學(xué)習(xí)時會被“遞歸”勸退覺得太燒腦但我用了一個生活化的類比來幫助理解想象你要走出一個迷宮你每到一個岔路口就做一個標(biāo)記選定一個方向往前走如果走不通你就按原路退回這個岔路口把標(biāo)記擦掉再試另一條路。擦掉標(biāo)記這個動作就是代碼里的“撤銷選擇”。我刷“全排列”這道題時第一次寫出來的代碼只加了遞歸沒有恢復(fù)狀態(tài)結(jié)果輸出的排列里很多元素重復(fù)出現(xiàn)。因為當(dāng)你遞歸完一條路回到上一層時如果不撤銷當(dāng)前的“已使用”狀態(tài)下一層循環(huán)就會認(rèn)為這個元素已經(jīng)被占用了。這個撤銷動作代碼上就三五行但邏輯上非常關(guān)鍵。我自己的經(jīng)驗是寫回溯題的時候拿筆在紙上畫出遞歸樹把每一步的“選擇”“遞歸”“撤銷”分別標(biāo)出來真的比悶頭寫代碼要快得多。雖然動筆看起來很慢實際上很頂用。我這次刷題時幾乎每道回溯題都會先畫遞歸樹思路直接清晰了一個檔次。3. 刷題過程中的高頻失誤與排查實錄3.1 邊界條件一半以上的 bug 都出在這里兩天刷下來我統(tǒng)計了一下自己犯的錯發(fā)現(xiàn)一個扎心的事實真正因為“解法不會”而卡住的情況大概只占三成剩下的七成全部是邊界條件處理出了問題。比如數(shù)組越界、空輸入、只有一個元素、整數(shù)溢出、字符串為空這些情況不專門去測的話代碼很容易在“看起來沒什么問題”的狀態(tài)下掛掉。我之前寫“二分查找”就翻過車默寫下來很簡單但真正在 LeetCode 上跑的時候死循環(huán)了。后來定位了半天發(fā)現(xiàn)是 while 條件是 left right 還是 left right以及 mid 更新的時候要不要加一的問題。這兩個問題不搞清楚二分查找就是“玄學(xué)”。我整理出一個規(guī)律供大家參考循環(huán)條件mid 更新方式適用場景l(fā)eft rightright mid - 1left mid 1標(biāo)準(zhǔn)區(qū)間查找直接返回索引left rightright midleft mid 1尋找左邊界left rightright mid - 1left mid尋找右邊界mid 需向上取整這個表很重要因為我發(fā)現(xiàn)很多邊界問題不是能力問題純粹是“沒有系統(tǒng)整理過”。比如你用的是左閉右閉區(qū)間那 left right 是對的選擇否則空區(qū)間時循環(huán)直接退出根本不會進(jìn)入判斷。但如果你寫的是左閉右開區(qū)間那循環(huán)條件又不一樣了。寫二分查找之前先想清楚“我維護(hù)的這個區(qū)間到底長什么樣”再動筆這樣基本上就能避開很多坑。我把這段總結(jié)直接寫進(jìn)了筆記里標(biāo)注為“反復(fù)要看的重點”。3.2 復(fù)雜度誤判一場隱藏的面試?yán)讌^(qū)除了邊界條件還有一個容易被忽視的問題是對時間復(fù)雜度的誤判。我在刷“最長回文子串”這道題時最開始想到的是暴力枚舉所有子串再逐個判斷是否是回文邏輯倒是簡單但總復(fù)雜度是 O(n^3)數(shù)據(jù)范圍稍微大一點就直接超時。后來我改成中心擴(kuò)展法枚舉每個中心位置然后向兩邊擴(kuò)散復(fù)雜度直接降到 O(n^2)。這道題讓我意識到刷題的時候不能只滿足于“做出來”還要逼自己想一想“如果數(shù)據(jù)范圍擴(kuò)大十倍我的代碼還能跑嗎”我自己的做法是在每道題的筆記里都額外寫一行“時間復(fù)雜度 空間復(fù)雜度”并且在提交通過之后刻意看一眼最優(yōu)解的復(fù)雜度是多少然后思考差距在哪里。像“最長回文子串”這種題如果只看暴力解你永遠(yuǎn)學(xué)不到 O(n) 的馬拉車算法但你如果已經(jīng)掌握了 O(n^2) 的中心擴(kuò)展法再去看馬拉車?yán)斫馄饋砭洼p松不少。這個學(xué)習(xí)路徑比一上來就背馬拉車要扎實得多。這也是為什么我特別建議刷題時不要一次性追求最優(yōu)解而是先有暴力解做保底再一步步優(yōu)化這個“漸進(jìn)優(yōu)化”的過程本身就是寶貴的能力。3.3 工具與環(huán)境的折騰本地調(diào)試到底值不值得刷題過程中還有一個很多人糾結(jié)的問題到底是在網(wǎng)頁編輯器里直接寫還是拉到本地 IDE 里配合測試用例調(diào)試我個人這兩天的體驗是本地調(diào)試非常值得但要想清楚成本。我用的方法是在本地建一個簡單的測試腳本把題目里的示例用例寫進(jìn)去然后配合斷點和 print 逐步觀察變量變化尤其是雙指針、回溯這類需要觀察“狀態(tài)什么時候被修改了”的題目這種方式能幫你秒級定位問題。當(dāng)然本地調(diào)試也有坑。我之前在本地寫得好好的代碼一粘到在線評測系統(tǒng)就報編譯錯誤后來發(fā)現(xiàn)是本地沒有嚴(yán)格按照題目的輸入輸出格式來處理比如沒有處理多組輸入、沒有 trim 掉多余的空格等。所以我現(xiàn)在養(yǎng)成一個習(xí)慣代碼通過本地測試以后還會專門檢查一下輸入輸出的格式再提交到評測系統(tǒng)里。這個習(xí)慣讓我避免了很多“毫無意義的失敗提交”。4. 刷題效率工具體驗與筆記方法分享4.1 我的錯題本分層記錄法這次刷題我做了一個很有效的改變分層記錄錯題而不是把所有東西混在一個文檔里。我分成了三層第一層是“思路錯誤”這類錯題意味著我完全沒方向需要在筆記里重新寫清楚這類題型的套路與思考路徑第二層是“邊界條件錯誤”這類題我解法大方向是對的但在特殊用例上出了問題因此我會在筆記里用紅字專門標(biāo)注邊界值比如空數(shù)組、負(fù)數(shù)、最大整數(shù)等第三層是“代碼實現(xiàn)錯誤”比如變量名寫錯、索引不對、忘了修改循環(huán)變量這種我會隨手寫一句“低級失誤下次注意”而不需要花太多時間。這個分層記錄法幫我節(jié)省了很多復(fù)習(xí)時間。因為第一層錯題才是真正值得反復(fù)看的第二層只需要考前掃一眼第三層干脆可以忽略。刷題到最后最忌諱的其實不是錯得太多而是錯得太多但從不分類結(jié)果復(fù)習(xí)的時候被海量信息淹沒什么都想抓什么都抓不住。4.2 筆記里的“一句話總結(jié)”有多重要我在每一道題的筆記末尾都會逼自己寫一句“一句話總結(jié)”。比如“三數(shù)之和排序 雙指針注意跳過重復(fù)元素”“爬樓梯斐波那契狀態(tài)壓縮空間優(yōu)化到 O(1)”。別小看這一句話它其實是在強(qiáng)迫我用最精煉的話把這道題的核心講清楚。如果一句話說不清說明我對這道題的理解還不夠深那就得再回去看看。而且這種一句話總結(jié)等到后面復(fù)習(xí)的時候特別有用。兩天結(jié)束之后我翻看自己寫下的這二十多句話基本就能在十幾分鐘內(nèi)把所有題目的套路在腦子里過一遍相當(dāng)于做了一個快速索引。如果當(dāng)時沒寫這句話那我復(fù)習(xí)時重新看每道題就得從完整筆記里重新讀一遍效率會低很多。5. 實操模板與通用解法清單5.1 經(jīng)典題型的“最小可用代碼”模板刷完這兩天我最大的收獲之一是整理出了一批“最小可用代碼”模板。所謂最小可用就是刪掉所有花哨的寫法只保留最核心的邏輯結(jié)構(gòu)方便在面試緊張的時候快速回憶起來。這里我挑幾個分享出來二分查找模版左閉右閉def binary_search(nums, target): left, right 0, len(nums) - 1 while left right: mid (left right) // 2 if nums[mid] target: return mid elif nums[mid] target: left mid 1 else: right mid - 1 return -1回溯模版def backtrack(path, choices, result): if 滿足結(jié)束條件: result.append(path[:]) return for choice in choices: 做選擇 backtrack(path, choices, result) 撤銷選擇滑動窗口模版left 0 for right in range(len(s)): 更新窗口狀態(tài) while 窗口不滿足條件: 收縮左邊界 left 1 更新結(jié)果這幾個模板我寫在了筆記最前面作為一個快速參考區(qū)。面試前我通常會把這些模板快速過一遍讓自己進(jìn)入狀態(tài)。要注意的是模板是幫你在緊張時有東西可以寫但不代表你不需要理解背后的原理。有幾次我只是機(jī)械地套模板結(jié)果題目稍微變形就寫了很久后來才意識到模板必須配著原理解釋一起記憶才真正有用。5.2 兩天 20 道題的完整清單與復(fù)盤這里把我實際完成的部分題目列表列出來給粉絲們一個參照。當(dāng)然題目數(shù)量不是核心關(guān)鍵是覆蓋面分類題目核心考點我的錯誤/收獲哈希表兩數(shù)之和空間換時間字典存儲想到了哈希表但忘記處理重復(fù)元素雙指針三數(shù)之和排序 雙指針 去重去重邏輯不熟練第一次漏掉滑動窗口無重復(fù)字符的最長子串窗口左邊界更新忘記取 max左指針回退二分查找搜索旋轉(zhuǎn)排序數(shù)組二分變體區(qū)間判斷條件寫反調(diào)試很久動態(tài)規(guī)劃爬樓梯斐波那契狀態(tài)壓縮老題但要考慮 dp[0] 的定義動態(tài)規(guī)劃最小路徑和二維 DP邊界初始化第一行第一列處理錯誤回溯全排列撤銷選擇漏掉撤銷結(jié)果重復(fù)回溯子集選或不選兩種視角迭代法思路更清晰二叉樹二叉樹的中序遍歷遞歸與迭代棧迭代寫法忘記維護(hù) left 鏈二叉樹二叉樹的最大深度后序遍歷遞歸思路清晰一行代碼解決復(fù)盤完這張表我最大的感受就是我犯的錯真的很典型而且大多集中在“對狀態(tài)變化的理解不夠深”上。比如滑動窗口左指針回退、回溯漏撤銷、DP 邊界初始化錯誤歸根結(jié)底都是沒有想清楚“每一步操作之后狀態(tài)應(yīng)該是什么”。所以后面的刷題復(fù)習(xí)里我也調(diào)整了策略每道題寫完之后多花三十秒在腦子里把“狀態(tài)變化”過一遍而不是急著去做下一道題。這種方法比單純追求“今天刷了十道題”的成就感要有效得多。6. 兩天刷題后的直接經(jīng)驗與建議6.1 給不同基礎(chǔ)的人兩條刷題路線建議刷題這件事其實很講究“匹配度”。我之前也走過彎路剛開始的時候一上來就刷 hard 題結(jié)果被虐得體無完膚自信心嚴(yán)重受挫。按照這兩天的體驗我給不同基礎(chǔ)的朋友兩條路線參考。如果你基礎(chǔ)比較薄弱連樹的遍歷都還有點陌生我建議先從“數(shù)據(jù)結(jié)構(gòu)基礎(chǔ)題”入手比如數(shù)組刪除元素、鏈表反轉(zhuǎn)、用棧實現(xiàn)隊列這一階段目標(biāo)不是會做難題而是熟悉語言 API、掌握常見數(shù)據(jù)結(jié)構(gòu)的基本操作至少刷三十道左右再進(jìn)入下一階段。如果你已經(jīng)能獨立做出簡單題只是中難題容易卡住那我的建議是主攻“高頻算法專題”就是我今天寫的雙指針、滑動窗口、動態(tài)規(guī)劃、回溯這幾個方向每個專題至少連續(xù)刷十道再交叉混合訓(xùn)練強(qiáng)迫自己識別題型。6.2 時間管理與心態(tài)調(diào)整的碎碎念兩天高強(qiáng)度的刷題說實話挺累的。尤其是第二天下午寫回溯的時候我的腦子明顯感覺轉(zhuǎn)不動了看遞歸調(diào)用??戳税胩於紱]繞出來。后來我站起來休息了十五分鐘洗了把臉再回來重新畫遞歸樹才成功把思路理清。這個過程讓我意識到刷題本質(zhì)上也是一個體力活集中刷題時一定要給自己留出足夠的休息時間不要硬撐。我還發(fā)現(xiàn)一個小技巧把手機(jī)放到另一個房間只留一臺電腦和一本書在書桌上減少外界干擾這樣進(jìn)入心流狀態(tài)的速度會明顯變快。這種專注狀態(tài)一旦建立刷題效率能翻倍而且對思路連續(xù)性的幫助特別大。那兩天里我還有一個心態(tài)上的變化接受“不會做”是常態(tài)而不是恥辱。很多人在刷題時遇到不會的題第一反應(yīng)是煩躁然后馬上去看題解。我的做法是先自己硬想十五分鐘如果十五分鐘后還是毫無頭緒再去看題解。但看完題解之后不是直接復(fù)制粘貼而是把題解關(guān)上用自己的話從頭寫一遍代碼哪怕寫出來的和題解不完全一樣也沒關(guān)系。這個過程很殘忍但見效極快。刷完兩天題我能明顯感覺到自己面對陌生題目時的第一反應(yīng)已經(jīng)從“好難”變成了“先分析一下題型和可能的解法”這種思維方式的轉(zhuǎn)變可能比多會做幾道題本身更重要。我個人在實際操作中的體會是刷題筆記這類東西最重要的不是記了多少而是回頭看多少。很多人的筆記寫完就吃灰那就失去了意義。我這兩天的筆記雖然只是簡單的 Markdown 文檔但我在里面加了大量的復(fù)盤和標(biāo)注目的就是讓一周后的自己還能快速看懂當(dāng)時卡殼的地方。如果你也想嘗試集中刷題我真心建議你也建一個自己的錯題分層體系哪怕只是拿個記事本也比什么都記不住要強(qiáng)得多。希望這份刷題筆記能給你一點啟發(fā)讓你找到適合自己的節(jié)奏。