研發(fā)筆試卷復(fù)盤:基礎(chǔ)考點(diǎn)與方法論)
前幾天整理舊硬盤翻出一個命名為“筆試整理”的文件夾里面靜靜躺著一份2015年人人網(wǎng)研發(fā)筆試卷E的掃描版。說實(shí)話看到它的第一反應(yīng)是懷念第二反應(yīng)是“這玩意兒現(xiàn)在還有人看嗎”。但當(dāng)我真的一張一張重新過完后發(fā)現(xiàn)一個挺有意思的事實(shí)這套卷子里的考點(diǎn)和出題思路放到今天依然有很強(qiáng)的參考價(jià)值。不是說要你去背原題而是它代表了一類典型的、以“基礎(chǔ)扎實(shí)度”為核心考察目標(biāo)的研發(fā)筆試題。如果你正在準(zhǔn)備校招或者工作幾年后想回來補(bǔ)一補(bǔ)底層的短板花點(diǎn)時(shí)間復(fù)現(xiàn)這套卷子的考查邏輯比盲目刷幾十道LeetCode更能幫你建立知識體系。很多人拿到一套舊卷子容易走兩個極端要么覺得過時(shí)了沒價(jià)值要么試圖在網(wǎng)上找答案背下來。我都不推薦。更務(wù)實(shí)的做法是把試卷當(dāng)成一份“考點(diǎn)清單”逐題拆解它到底在考察什么然后對照自己的知識樹查漏補(bǔ)缺。這篇博文我也不會去逐題貼答案——網(wǎng)上找不到完整版而且意義不大——而是把這套卷子里的核心題型、答題思路、踩坑點(diǎn)整理成一套可以復(fù)用的方法論。無論你考不考人人網(wǎng)這套方法都能遷移到其他公司的研發(fā)筆試?yán)铩?. 為什么2015年這套題到現(xiàn)在還有復(fù)盤價(jià)值1.1 一套試卷的時(shí)代背景基礎(chǔ)能力永遠(yuǎn)在考察名單上人人網(wǎng)在2015年正處于移動端轉(zhuǎn)型的關(guān)鍵期后端技術(shù)棧從傳統(tǒng)的LAMP架構(gòu)向分布式方向演進(jìn)。這份研發(fā)筆試卷E的題型構(gòu)成其實(shí)非常典型地反映了那個階段互聯(lián)網(wǎng)公司對研發(fā)崗位的期待你不僅要會用框架還得懂底層原理你不僅要能寫業(yè)務(wù)代碼還得能處理高并發(fā)、大數(shù)據(jù)量下的性能問題。具體到試卷結(jié)構(gòu)大致是五個板塊選擇題、填空題、簡答題、編程題、設(shè)計(jì)題。選擇題主要覆蓋數(shù)據(jù)結(jié)構(gòu)、C/Java語法細(xì)節(jié)、操作系統(tǒng)、網(wǎng)絡(luò)協(xié)議填空題則偏重于輸出結(jié)果題比如給一段代碼讓你寫運(yùn)行結(jié)果考察的是對語言機(jī)制的理解深度簡答題通常會問“進(jìn)程和線程的區(qū)別”“TCP三次握手為什么是三次”這類經(jīng)典問題編程題一般是兩道左右鏈表和動態(tài)規(guī)劃是常客設(shè)計(jì)題壓軸往往是一道系統(tǒng)設(shè)計(jì)或方案設(shè)計(jì)。這套結(jié)構(gòu)放在今天依然不過時(shí)。你去看現(xiàn)在各大廠的筆試卷子除了多了些機(jī)器學(xué)習(xí)、云原生的題目之外底層考察邏輯幾乎沒變。原因很簡單基礎(chǔ)知識是面試官唯一能快速判斷你“潛力”的指標(biāo)。項(xiàng)目經(jīng)驗(yàn)可以包裝但操作系統(tǒng)、網(wǎng)絡(luò)、數(shù)據(jù)結(jié)構(gòu)這些硬知識問幾句就能看出真實(shí)水平。1.2 這套卷子考察的三個核心維度復(fù)盤完整份卷子我總結(jié)出它想考察的三個核心維度這三個維度也是所有研發(fā)筆試的通用標(biāo)尺。第一個維度是“編碼基本功”。包括語法細(xì)節(jié)是否扎實(shí)、邊界條件是否考慮周全、代碼風(fēng)格是否整潔。比如C的虛函數(shù)機(jī)制、構(gòu)造析構(gòu)順序、指針與引用的區(qū)別這類題目如果平時(shí)只是“用過”而沒有“思考過”很容易翻車。第二個維度是“算法思維”。不是看你會不會背題而是看你在限定時(shí)間內(nèi)能否從暴力解推進(jìn)到最優(yōu)解并正確分析復(fù)雜度。第三個維度是“系統(tǒng)視野”。簡答題和設(shè)計(jì)題考察的是綜合能力比如緩存的一致性、分布式鎖的實(shí)現(xiàn)、數(shù)據(jù)庫索引的選擇。能答好這類題目的人通常不是靠刷題刷出來的而是真的寫過、踩過坑、總結(jié)過。所以復(fù)盤這套卷子的關(guān)鍵不在于“記住正確答案”而在于“理解考察者為什么這么問”。一旦想通了這一點(diǎn)你就擁有了出題人視角以后再遇到新題也不會慌。2. 算法與數(shù)據(jù)結(jié)構(gòu)題拿滿分的答題節(jié)奏2.1 鏈表和樹的基礎(chǔ)題先從手寫邊界條件說起這套試卷的編程題里鏈表相關(guān)題目幾乎從不缺席。原因有兩點(diǎn)第一鏈表能考察指針操作的基本功第二鏈表的邊界條件多很容易暴露“眼高手低”的問題。我印象中這類題的經(jīng)典考法是“判斷鏈表是否有環(huán)”和“反轉(zhuǎn)鏈表”。如果你覺得這兩道題簡單不妨問自己三個問題。第一個問題快慢指針判斷鏈表有環(huán)快指針每次走兩步而不是三步為什么很多人答不上來。其實(shí)核心原因是兩步可以保證慢指針進(jìn)入環(huán)后在一圈內(nèi)必然被快指針追上。如果走三步追上可能需要多圈雖然最終也能判斷但邊界分析就復(fù)雜得多。第二個問題反轉(zhuǎn)鏈表時(shí)如果要求不能用遞歸迭代寫法里需要幾個指針答案是三個prev、cur、next。少一個就會斷鏈。第三個問題鏈表的入環(huán)節(jié)點(diǎn)怎么找這涉及Floyd判圈算法的擴(kuò)展——快慢指針相遇后一個指針從頭出發(fā)一個指針從相遇點(diǎn)出發(fā)每次各走一步再次相遇的位置就是入環(huán)點(diǎn)。樹的考察點(diǎn)則集中在遍歷上。層序遍歷看起來簡單但很多人的寫法有隱患用nullptr作為層分隔符當(dāng)節(jié)點(diǎn)值本身就可能是空指針時(shí)容易出錯。更好的做法是每次記錄當(dāng)前隊(duì)列的大小然后一次性處理完這一層這樣既不需要額外標(biāo)記也不容易出錯。前序、中序、后序的遞歸版本大家都熟但要求你寫非遞歸版本時(shí)就需要用棧手動模擬這里經(jīng)常有人卡住。2.2 動態(tài)規(guī)劃的套路從暴力遞歸到狀態(tài)壓縮動態(tài)規(guī)劃是每年筆試的壓軸??瓦@套卷子也不例外。遇到動態(tài)規(guī)劃題我推薦的標(biāo)準(zhǔn)步驟是先寫暴力遞歸再改成記憶化搜索最后再優(yōu)化成遞推。不要一上來就追求最優(yōu)解那樣反而容易卡殼。舉個例子最長不重復(fù)子串這道題。暴力做法是枚舉所有子串檢查是否有重復(fù)字符復(fù)雜度O(n^2)。這能幫你厘清問題模型確保思路正確。接著用滑動窗口優(yōu)化維護(hù)一個窗口右指針不斷右移如果遇到重復(fù)字符左指針就跳到上次出現(xiàn)位置的下一位。這時(shí)復(fù)雜度降為O(n)并且不需要額外數(shù)組來標(biāo)記字符是否出現(xiàn)過只需要一個長度為128或256的數(shù)組記錄每個字符上次出現(xiàn)的位置。還有一個高頻考點(diǎn)是背包類問題。0-1背包的遞推公式大部分人能寫出來但問到“如何優(yōu)化成一維數(shù)組”時(shí)容易忽略內(nèi)層循環(huán)必須倒序。為什么因?yàn)橐痪S數(shù)組滾動更新時(shí)正序會讓同一個物品被重復(fù)使用而0-1背包每個物品只能用一次所以必須倒序。這個細(xì)節(jié)也是面試官最愛挖的坑。2.3 字符串處理的邊界陷阱字符串題看似簡單實(shí)際是失分重災(zāi)區(qū)。這套卷子的字符串題大多圍繞“子串”“子序列”“反轉(zhuǎn)”這幾個關(guān)鍵詞展開。比如“反轉(zhuǎn)字符串中的單詞順序”要求原地操作。很多人能寫出整體反轉(zhuǎn)再局部反轉(zhuǎn)的思路但漏掉了兩個關(guān)鍵邊界單詞間可能有多個空格輸入字符串首尾可能有空格。C里用istringstream可以省事但筆試環(huán)境不一定允許你依賴這些庫函數(shù)的語義Java里用split( )會丟掉連續(xù)空格需要手動處理。最穩(wěn)妥的寫法是先去除首尾空格再用雙指針從后往前逐詞提取。再比如判斷兩個字符串是否為變位詞。常規(guī)解法是用哈希表統(tǒng)計(jì)字符頻率這沒錯。但如果你能用長度為26的數(shù)組代替哈希表代碼會更簡潔而且面試時(shí)能體現(xiàn)你對“有限字符集”的敏感度。2.4 代碼風(fēng)格是隱形評分項(xiàng)這一點(diǎn)很多人會忽略筆試的編程題通常是人工閱卷代碼風(fēng)格直接影響印象分。好的代碼風(fēng)格包括變量命名有意義而不是a、b、c關(guān)鍵步驟有注釋函數(shù)邊界檢查充分復(fù)雜度分析寫在代碼塊旁邊或注釋里。我見過不少人代碼邏輯全對但整個函數(shù)寫的密密麻麻沒有空行變量名全部是拼音縮寫閱卷人一眼就不想看。反過來一份結(jié)構(gòu)清晰、注釋得當(dāng)?shù)拇a即使有小bug閱卷人也更愿意相信你只是筆誤而不是不會。3. 語言基礎(chǔ)與計(jì)算機(jī)系統(tǒng)題送分題里的暗坑3.1 C/Java基礎(chǔ)容易被問倒的三個點(diǎn)這套卷子的選擇題里C和Java的基礎(chǔ)題占了很大比例但很多“感覺會”的題實(shí)測一做就錯。我挑三個最典型的點(diǎn)說一下。第一個是C虛函數(shù)相關(guān)的機(jī)制。題目可能會問你“構(gòu)造函數(shù)是否可以聲明為虛函數(shù)”“析構(gòu)函數(shù)為什么通常要聲明為虛函數(shù)”。前者是因?yàn)樘摵瘮?shù)表在構(gòu)造期間尚未完全建立調(diào)用虛函數(shù)會失去多態(tài)意義后者是因?yàn)橥ㄟ^基類指針刪除派生類對象時(shí)析構(gòu)函數(shù)不虛就無法正確調(diào)用派生類的析構(gòu)邏輯導(dǎo)致內(nèi)存泄漏。很多有幾年經(jīng)驗(yàn)的人第一反應(yīng)是“我平時(shí)不手動管理內(nèi)存不關(guān)我事”但寫底層庫的人絕對躲不開這個問題。第二個是Java集合框架里的坑。比如HashMap的擴(kuò)容機(jī)制、ConcurrentHashMap為什么線程安全、ArrayList和LinkedList的區(qū)別為什么不能只看“查詢快慢”。很多人背過結(jié)論但被問到“HashMap在JDK 1.7和1.8中擴(kuò)容有什么不同”時(shí)就露餡了。1.7是頭插法并發(fā)擴(kuò)容可能形成環(huán)1.8改成了尾插法并且在鏈表長度超過8且數(shù)組長度大于64時(shí)轉(zhuǎn)成紅黑樹。這些不是考題細(xì)節(jié)而是設(shè)計(jì)思想值得花時(shí)間真正理解。第三個是內(nèi)存管理。C的RAII、智能指針的引用計(jì)數(shù)機(jī)制Java的GC分代回收、Minor GC和Full GC觸發(fā)的條件。這些題看起來是概念題但背后關(guān)聯(lián)著JVM調(diào)優(yōu)、服務(wù)性能排查面試官問出來其實(shí)是想知道你有沒有排查線上問題的基礎(chǔ)。3.2 操作系統(tǒng)與網(wǎng)絡(luò)考點(diǎn)不能只背結(jié)論操作系統(tǒng)和網(wǎng)絡(luò)協(xié)議是筆試中“看似送分、實(shí)則暗坑”的重災(zāi)區(qū)因?yàn)轭}目經(jīng)常把結(jié)論包裝在具體場景里考。比如進(jìn)程與線程的區(qū)別老生常談。但換成“一個進(jìn)程內(nèi)多個線程共享哪些資源、獨(dú)占哪些資源”就有很多人說不全。共享的是地址空間、文件描述符、信號處理器獨(dú)占的是棧和寄存器。再比如死鎖的四個必要條件很多人能背出來但題目給一個具體場景問你“破壞的是哪個條件”就要費(fèi)一番思量了。網(wǎng)絡(luò)部分這道卷子幾乎必然出現(xiàn)TCP狀態(tài)圖。高頻題包括TIME_WAIT為什么是2MSL、為什么不能是1MSLSYN Flood的原理和防護(hù)HTTP和HTTPS的區(qū)別以及TLS握手過程。這里我建議不要死背狀態(tài)名而是把每個狀態(tài)背后的“為什么”想清楚。比如2MSL這個數(shù)字是因?yàn)橐WC最后一個ACK能重傳同時(shí)確保本次連接的所有報(bào)文在網(wǎng)絡(luò)中消失避免干擾新連接。理解了設(shè)計(jì)意圖任何變著花樣的題都繞不開你。3.3 數(shù)據(jù)庫索引B樹是重點(diǎn)中的重點(diǎn)數(shù)據(jù)庫相關(guān)的題目在這套卷子里占了兩個方向一是SQL寫法二是索引原理。SQL寫法相對容易多表關(guān)聯(lián)、聚合函數(shù)、子查詢掌握基本語法就能應(yīng)付。索引原理才是真正拉分的地方。經(jīng)典問題為什么索引選用B樹而不是B樹、紅黑樹或者哈希表標(biāo)準(zhǔn)答案講三點(diǎn)。第一點(diǎn)B樹的所有數(shù)據(jù)都在葉子節(jié)點(diǎn)葉子節(jié)點(diǎn)之間用指針連接非常適合范圍查詢和排序。第二點(diǎn)非葉子節(jié)點(diǎn)不存數(shù)據(jù)每層能容納更多索引鍵讓整棵樹更矮減少磁盤I/O次數(shù)。第三點(diǎn)紅黑樹是二叉搜索樹樹高太高不適合磁盤存儲。哈希索引雖然單點(diǎn)查詢是O(1)但完全無法支持范圍查詢。除了B樹還有一個面試官愛問的細(xì)節(jié)復(fù)合索引的最左匹配原則。給一個復(fù)合索引(a, b, c)查詢條件WHERE a1 AND c3時(shí)能用到索引嗎答案是能用到a這一列但c用不到。原因是索引排序規(guī)則是先按a排再按b排再按c排跳過了b就沒有辦法用c來定位到精確區(qū)間。這類題做錯的人非常多本質(zhì)原因是沒理解索引的底層排序結(jié)構(gòu)。4. 設(shè)計(jì)題和開放題給面試官一條清晰的思考路徑4.1 系統(tǒng)設(shè)計(jì)題的回答框架這套試卷的壓軸設(shè)計(jì)題通常會給你一個具體場景比如“設(shè)計(jì)一個短URL系統(tǒng)”或“設(shè)計(jì)一個帶過期時(shí)間的緩存”。很多人一看到設(shè)計(jì)題就慌了因?yàn)槠綍r(shí)只寫業(yè)務(wù)代碼沒系統(tǒng)想過架構(gòu)。我總結(jié)了一個四步框架基本可以應(yīng)對80%的設(shè)計(jì)題。第一步是需求澄清。問清楚是讀多寫多還是讀多寫少、預(yù)估QPS多少、數(shù)據(jù)量多大。筆試雖然沒法提問但你可以在回答中先定義這些假設(shè)。第二步是容量估算。根據(jù)假設(shè)算一下QPS峰值、存儲空間、帶寬需求。這里不需要精確數(shù)量級對就行。第三步是核心鏈路設(shè)計(jì)。畫出來請求從客戶端到服務(wù)端經(jīng)過哪些組件每個組件的職責(zé)是什么。第四步是容錯與擴(kuò)展。如果某個組件掛了怎么保證可用性如果需要擴(kuò)容怎么做水平擴(kuò)展。拿短URL系統(tǒng)舉例。核心問題有兩個長URL轉(zhuǎn)短URL的算法以及短URL跳轉(zhuǎn)長URL的存儲方案。常見算法是發(fā)號器思想——用自增ID或雪花算法生成唯一ID再轉(zhuǎn)成62進(jìn)制字符串作為短URL后綴。也有人用哈希算法比如MD5取前8位但存在碰撞風(fēng)險(xiǎn)需要加鹽或布隆過濾器輔助。存儲層通常用KV數(shù)據(jù)庫Redis作為緩存加速M(fèi)ySQL做持久化。這些思路不需要你寫完整代碼但整個鏈路必須清晰。4.2 場景落地帶過期時(shí)間的本地緩存如果是設(shè)計(jì)一個本地緩存需要注意的點(diǎn)就更細(xì)了。首先是數(shù)據(jù)結(jié)構(gòu)的選擇??梢杂肏ashMap加雙向鏈表實(shí)現(xiàn)LRU淘汰也可以直接用LinkedHashMap但要注意線程安全性。筆試時(shí)如果要求手寫通常不強(qiáng)制考慮并發(fā)但如果能提到“加鎖粒度、分段鎖、讀寫鎖”等優(yōu)化方案會加分不少。關(guān)于過期時(shí)間經(jīng)典實(shí)現(xiàn)是懶刪除加定期清理的組合。懶刪除就是在get的時(shí)候檢查過期時(shí)間過期了返回null并刪除定期清理則是啟動一個后臺線程周期性掃描一部分key刪除過期數(shù)據(jù)。這兩種方式結(jié)合既保證了讀操作的實(shí)時(shí)性又能及時(shí)釋放內(nèi)存。4.3 設(shè)計(jì)題中的“溝通感”筆試怎么體現(xiàn)設(shè)計(jì)題和編程題不同沒有唯一的“標(biāo)準(zhǔn)答案”閱卷人更看重的是你的思考路徑是否完整。所以答題時(shí)不要把每個細(xì)節(jié)都鋪開寫而是要有主次先給結(jié)論再給理由最后補(bǔ)充邊界情況。比如“緩存和數(shù)據(jù)庫怎么保證一致性”這種問題很多人知道要“先更新數(shù)據(jù)庫再刪緩存”但不知道為什么要這樣。如果面試官追問“刪緩存失敗怎么辦”你能提到消息隊(duì)列重試或者訂閱binlog異步刪除緩存就說明你是真思考過而不是背了面經(jīng)。這種追問式的細(xì)節(jié)才是閱卷人打高分的依據(jù)。5. 考后復(fù)盤一套卷子怎么榨出三套的價(jià)值5.1 錯題歸因而不是對答案做完一套模擬卷最忌諱的就是簡單對完答案就翻篇。我給自己定的規(guī)矩是每道錯題必須寫出歸因——是“知識沒學(xué)”還是“學(xué)了沒記住”還是“記住但不會用”三種歸因?qū)?yīng)的補(bǔ)救措施完全不同。知識沒學(xué)就去補(bǔ)基礎(chǔ)找教材對應(yīng)章節(jié)重新過一遍。學(xué)了沒記住說明缺少系統(tǒng)整理需要做知識點(diǎn)卡片或思維導(dǎo)圖。記住但不會用這是最遺憾的說明刷題量不夠?qū)︻}型不敏感。你需要做的是找類似知識點(diǎn)的不同考法反復(fù)練習(xí)到形成條件反射。5.2 建立考點(diǎn)-題目映射表復(fù)盤這套2015年的試卷時(shí)我做了一個很簡單卻很高效的表格推薦給你。表格有三列考點(diǎn)名稱、考察方式、我的掌握程度。比如“鏈表有環(huán)判斷”這一行考察方式是“快慢指針時(shí)間復(fù)雜度O(n)”掌握程度是“熟練”。再比如“B樹索引最左匹配”這一行考察方式是“寫SQL判斷是否走索引”掌握程度是“會用但說不清原理”。這張表做完之后你的薄弱點(diǎn)一目了然。每天花20分鐘只看“掌握程度為不熟練”的行堅(jiān)持一周提升非常明顯。5.3 復(fù)盤后的模擬訓(xùn)練限時(shí)、手寫、不查資料復(fù)盤完之后一定要安排一次限時(shí)模擬。環(huán)境盡量貼近真實(shí)筆試白紙手寫不進(jìn)IDE不查函數(shù)文檔完全靠記憶。時(shí)間分配上選擇和填空控制在30分鐘內(nèi)簡答和設(shè)計(jì)題40分鐘編程題50分鐘最后留一點(diǎn)時(shí)間檢查。手寫這件事真的很重要。在IDE里你可能靠補(bǔ)全提示和編譯器報(bào)錯發(fā)現(xiàn)代碼問題但筆試題沒有這個條件。手寫一遍能逼你重新記憶類名、方法簽名和語法細(xì)節(jié)。我當(dāng)年手寫ArrayList的擴(kuò)容邏輯時(shí)才發(fā)現(xiàn)自己居然記不清數(shù)組拷貝時(shí)System.arraycopy的參數(shù)順序這種細(xì)節(jié)在IDE里永遠(yuǎn)不會暴露但考場上一定會失分。最后再分享一個小技巧。復(fù)盤一套題不要只看自己錯在哪還要想想這道題是怎么被設(shè)計(jì)出來的。既然問了“為什么快指針走兩步”面試官就能從你的回答里聽出你是“用過算法”還是“理解算法”。帶著出題人視角去學(xué)習(xí)從“背答案的人”變成“懂原理的人”這才是這套老試卷能帶給你的最大增量。