第7章 兩個面試案例
在第一章中,我們討論了面試的流程。通常一輪面試是從面試官對照著簡歷了解應聘者的項目經歷及掌握的技能開始的。在介紹自己的項目經歷時,應聘者可以參照STAR模型,著重介紹自己完成的工作(包括基于什么平臺、用了哪些技術、實現了哪些算法等),以及最終對項目組的貢獻。
接著進入重頭戲技術面試環節。在這一環節中面試官會從編程語言、數據結構和算法等方面考查應聘者的基礎知識是否扎實全面(詳見第2章),并且很有可能會要求應聘者編程實現一兩個函數。如果碰到的面試題很簡單,應聘者也不能掉以輕心,一定要從基本功能、邊界條件和錯誤處理等方面確保代碼的完整性和魯棒性(詳見第3章)。如果碰到的題目很難,應聘者可以嘗試畫圖讓抽象的問題變得形象化,也可以嘗試舉幾個具體的例子去分析隱含的規律,還可以嘗試把大的問題分解成兩個或者多個小問題再遞歸地解決小問題。這3種方法能夠幫助應聘者形成清晰的思路,從而解決復雜的難題(詳見第4章)。很多面試題都不止一種解決方案,應聘者可以從時間復雜度和空間復雜度兩個方面選擇最優的解法(詳見第5章)。在面試過程中,面試官除了關注應聘者的編程能力外,他還會關注應聘者的溝通能力和學習能力,并有可能考查應聘者的知識遷移能力、抽象建模能力和發散思維能力(詳見第6章)。
在面試結束前的幾分鐘,面試官會給應聘者機會問幾個最感興趣的問題。應聘者可以從當前招聘的項目及其團隊等方面提出幾個問題。不建議應聘者在技術面試的時候向面試官詢問薪資情況,或者立即打聽面試結果。
接下來是兩個典型的面試案例,我們從中可以直觀地感受到面試的整個過程。在第一個案例(詳見7.1節)中,我們將看到面試過程中很多應聘者都曾犯過的錯誤;而在第二個案例(詳見7.2節)中,我們將看到面試官所認可的表現。我們希望應聘者能夠少犯甚至不犯錯誤,在面試過程中充分表現出自己的綜合素質,同時也衷心祝愿每個應聘者都能拿到自己心儀的Offer。
7.1 案例一:(面試題49)把字符串轉換成整數
面試官:看你簡歷上寫的是精通C/C++語言,這兩門語言你用了幾年了?
應聘者:從大一算起的話,快六、七年了。
面試官:也是C/C++的老程序員了嘛(微笑),那先問一個C++的問題(遞給應聘者一張A4紙,上面有一段打印的代碼,如下面所示)。你能不能分析一下這段代碼的輸出?

應聘者:(看了一下代碼,略作思考)n1是2,而n2是0。
面試官:為什么?
應聘者:在構造函數的初始化列表中,n2先被初始化為0,n2的值就是0了。接下來再用n2+2初始化n1,所以n1的值就是2。
[注:應聘者這個問題的回答是錯誤的,詳見后面的“面試官點評”]
面試官:C++是按照在初始化列表中的順序初始化成員變量的嗎?
應聘者:(一臉困惑)不是這樣嗎?我不太清楚。
面試官心理:
對成員變量的初始化順序完全沒有概念就號稱自己“精通”C++,也太言過其實了。算了,C++就不接著問了,看看你的編程能力。
面試官:沒關系,我們換一個題目。能不能介紹一下C語言的庫函數中atoi的作用?
應聘者:atoi用來把一個字符串轉換成一個整數。比如輸入字符串"123",它的輸出是數字123。
面試官:對的。現在就請你寫一個函數StrToInt,實現把字符串轉換成整數這個功能。當然,不能使用atoi或者其他類似的庫函數。你看有沒有問題?
應聘者:(嘴角出現一絲自信的笑容)沒有問題。
應聘者馬上開始在白紙上寫出了如下代碼:

應聘者:(放下筆)我已經寫好了。
面試官心理:
我出的題目有這么簡單嗎?你也太小看我了。
面試官:這么快?(稍微看了看代碼)你覺得這代碼有沒有問題?仔細檢查一下看看。
應聘者:(從頭開始讀代碼)哦,不好意思,忘了檢查字符串是空指針的情況。
應聘者拿起筆,在原來代碼上添加兩行新的代碼。修改之后的代碼如下:

面試官:改好了?(看了一下新的代碼)當字符串為空的時候,你的返回是0。如果輸入的字符串是"0"的時候,返回是什么?
應聘者:也是0。
面試官:兩種情況都得到返回值0,那么當這個函數的調用者得到返回值0的時候,他怎么知道是哪種情況?
應聘者:(臉上表情有些困惑)不知道。
面試官:你知道atoi是怎么區分的嗎?
應聘者:(努力回憶,有些慌張)不記得了。
面試官:atoi是通過一個全局變量來區分的。如果是非法輸入,返回0并把這個全局變量設為一個特殊標記。如果輸入是"0",則返回0,不會設置全局變量。這樣當atoi的調用者得到返回值0的時候,可以通過檢查全局變量得知輸入究竟是非法輸入還是字符串"0"。
應聘者:哦。(拿起筆準備寫代碼)我馬上修改。
面試官:等一下,除了空字符串之外,還有沒有可能有其他類型的非法輸入?
應聘者:(陷入思考,額頭上出現汗珠)如果字符串中含有'0'到'9'之外的字符,那么這樣的輸入也是非法的。
面試官:所有'0'到'9'之外的字符都是非法的嗎?
應聘者:加號和減號應該也是合法的輸入字符。
面試官:對的。先好好想想,想清楚了再開始寫代碼。
應聘者思考幾分鐘之后,寫下了如下代碼:

面試官:(看到應聘者寫完了)能不能簡要地解釋一下你的代碼?
應聘者:我定義了一個全局變量g\_Status來標記是不是遇到了非法輸入。如果輸入的字符串指針是空指針,則標記該全局變量然后直接返回。接下來我開始遍歷字符串中的所有字符。由于正負號只有可能出現在字符串的第一個字符,我們先處理字符串的第一個字符。如果第一個字符是符號,則標記當前的數字是負數,并在最后確保返回值是負數。在處理后續字符時,當遇到'0'到'9'之外的字符時,終止遍歷。如果遇到了數字,則把數值累加上去。
面試官心理:
這段代碼已經寫得不錯,你的編程能力看起來還不錯,只是編程的習慣不太好,不會在編碼之前想好可能有哪些輸入,從而在代碼中留下太多的漏洞。這次修改的幾個問題都是我已經提醒你的,沒有提醒的你自己沒有找出一個。
面試官:不錯。覺得功能上還有什么遺漏嗎?
應聘者:還有遺漏?(思索良久)要不要考慮溢出?
面試官:你覺得呢?如果輸入的是一個空字符串"",你覺得應該輸出什么?
應聘者:""不是個數字,我想應該是返回0,同時把g\_nStatus設為非法輸入。
面試官:那你能分析一下你現在的輸出是什么嗎?
應聘者:(緊張,聲音有些發抖)好像返回值是0,但沒有設置g\_nStatus為非法輸入?
面試官:嗯。我們再考慮一些有意思的輸入,比如輸入的字符串只有一個正號或者負號,你期待的輸出是什么?
應聘者:如果只有一個正號或者負號,后面沒有跟著數字,我想也不是有效的輸入。(開始分析代碼)我的返回值是0,但不會設置g\_nStatus。
面試官:由于時間也差不多了,已經沒有時間給你再做修改了。我的問題問完了,你有什么問題需要問我的嗎?
應聘者:你們公司工資待遇怎么樣?
面試官:你的期望值是多少呢?
應聘者:我有不少同學的月薪稅前超過8000,我不想低于他們。
面試官心理:
我不是HR,別和我談工資。
面試官:我們公司由人事部門統一確定應屆畢業生的工資,所以你的這個問題我不能直接回答,不過你的期望值我倒可以轉告HR。
應聘者:好的,謝謝。
面試官:還有其他問題嗎?
應聘者:沒有了。
面試官:那這輪面試就到這里結束吧。
面試官點評:
這名應聘者在簡歷中寫他精通C/C++,本來我對他的表現是充滿了期待的。但在他回答錯了第一個C++的語法題之后,他給我留下的印象就不是很好了。這就是希望越大失望越大吧。實際上,構造函數的初始化列表是C++中經常使用的一個概念。在C++中,成員變量的初始化順序只與它們在類中聲明的順序有關,而與在初始化列表中的順序無關。在前面的問題中,n1先于n2被聲明,因此n1也會在n2之前被初始化,所以我們先會用n2+2去初始化n1。由于n2這個時候還沒有被初始化,因此它的值是隨機的。用此時的n2加上2去初始化n1,n1的值只是一個隨機值。接下來再用0初始化n2,因此最終n2的值是0。
接下來是要求應聘者把一個字符串轉換成整數,這看起來是道很簡單的題目,實現其基本功能,大部分人都能用10行之內的代碼解決。可是,當我們要把很多特殊情況即測試用例都考慮進去,卻不是一件容易的事情。解決數值轉換問題本身不難,但我希望在寫轉換數值的代碼之前,應聘者至少能把空指針NULL、空字符串""、正負號、溢出等方方面面的測試用例都考慮到,并在寫代碼的時候對這些特殊的輸入都定義好合理的輸出。當然,這些輸出并不一定要和atoi完全保持一致,但必須要有顯式的說明,和面試官溝通好。
這個應聘者最大的問題就是還沒有養成在寫代碼之前考慮所有可能的測試用例的習慣,邏輯不夠嚴謹,因此一開始的代碼只處理了最基本的數值轉換。后來我每次提醒他一處特殊的測試用例之后,他改一處代碼。盡管他已經做了兩次修改,但仍然有不少很明顯的漏洞,特殊輸入空字符串"",邊界條件比如最大的正整數與最小的負整數等。由于這道題思路本身不難,因此我希望他能把問題考慮得盡可能周到,代碼盡量寫完整。下面是參考代碼:


在前面的代碼中,把空字符串""和只有一個正號或者負號的情況都考慮到了。同時正整數的最大值是0x7FFF FFFF,最小的負整數是0x8000 0000,因此我們需要分兩種情況來分別判斷整數是否發生上溢出或者下溢出。
最后他在提問環節給我留下的印象不是很好。他只有一個問題,是關于薪水方面。是不是反映出他找工作僅僅關心工資?通常只關心工資待遇的員工是非常容易流失的,而且他把期望值設在8000的唯一理由是他有同學的工資超過這個數。他對自己有沒有一個定位?
雖然從他后來改寫代碼的過程來看,他的編程能力還是不錯的,但我擔心以他現在的編程習慣,由于沒有一個全面的考慮,他寫出的代碼將會漏洞百出,魯棒性也得不到保證。總的來說,我的意見是我們不能錄取這名應聘者。
源代碼:
本題完整的源代碼詳見49\_StringToInt項目。
測試用例:
● 功能測試(輸入的字符串表示正數、負數和0)。
● 邊界值測試(最大的正整數、最小的負整數)。
● 特殊輸入測試(輸入字符串為NULL指針、輸入字符串為空字符串、輸入的字符串中有非數字字符等)。
7.2 案例二:(面試題50)樹中兩個結點的最低公共祖先
面試官:前面兩輪面試下來感覺怎么樣?
應聘者:感覺還好,只是大腦連續轉了兩個小時,有點累。
面試官:面試是個體力活,是挺累的。不過程序員這個行當本身也是體力活,沒有好的身體還真撐不住。面試中也要看看你們來應聘的體力怎么樣。
應聘者:(笑笑并點點頭)說得有道理。
面試官:開個玩笑。現在可以開始面試了吧?
應聘者:好的,我準備好了。
面試官:請簡要介紹一下你最近的一個項目。
應聘者:我最近完成的項目是Civil 3D(一款基于AutoCAD的土木設計軟件)中的Multi-Target。這個Target指的是道路的邊緣。之前道路的邊緣只能是Civil中的一個數據類型叫Alignment,我的工作是讓Civil支持其他類型的數據做道路的邊緣,比如AutoCAD中的Polyline等。
面試官:有沒有考慮到以后有可能會添加新的數據類型作為道路的邊緣?
應聘者:這個在開發的過程中就發生過。在第二版的需求文檔中添加了一種叫做Pipeline的道路邊緣。由于我的設計中考慮了擴展性,最后只要添加新的class就行了,幾乎不需要對已有的代碼做任何修改。
面試官:你是怎么做到的?
應聘者在白紙上用UML畫了一張類型關系圖(圖略)。
應聘者:(指著圖解釋)從這張圖我們可以看出,一旦需要支持新的道路邊緣比如Pipeline,我們只需繼承出新的class就可以了,對已有的其他class沒有影響。
面試官心理:
對自己的工作講得很細致、很深入,這個項目的確是你設計和實現的。看得出來,你對面向對象的設計和開發有著較深的理解。
面試官:(點點頭)的確是這樣的。接下來我們做一個編程題吧。我的題目是輸入兩個樹結點,求它們的最低公共祖先。
應聘者:這樹是不是二叉樹?
面試官:是又怎么樣,不是又怎么樣?
應聘者:如果是二叉樹,并且是二叉搜索樹,是可以找到公共結點的。
面試官:那假設是二叉搜索樹,你怎么查找呢?
應聘者:(有些激動,說得很快)二叉搜索樹是排序過的,位于左子樹的結點都比父結點小,而位于右子樹的結點都比父結點大,我們只需要從樹的根結點開始和兩個輸入的結點進行比較。如果當前結點的值比兩個結點的值都大,那么最低的共同父結點一定是在當前結點的左子樹中,于是下一步遍歷當前結點的左子結點。如果當前結點的值比兩個結點的值都小,那么最低共同父結點一定在當前結點的右子樹中,于是下一步遍歷當前結點的右子結點。這樣在樹中從上到下找到的第一個在兩個輸入結點的值之間的結點,就是最低的公共祖先。
面試官:看起來你對這個題目很熟悉,是不是以前做過啊?
應聘者:(面露尷尬)這個……碰巧……
面試官:(笑)那咱們把題目稍微換一下。如果這棵樹不是二叉搜索樹,甚至連二叉樹都不是,而只是普通的樹,又該怎么辦呢?
應聘者:(停下來想了十幾秒)樹的結點中有沒有指向父結點的指針?
面試官心理:
反應挺快的,而且提的問題針對性很強。你的溝通能力不錯。
面試官:為什么需要指向父結點的指針?
應聘者在白紙上畫了一張圖,如圖7.1所示。

圖7.1 樹中的結點有指向父結點的指針,用虛線箭頭表示
應聘者:(指著自己畫的圖7.1解釋)如果樹中的每個結點(除根結點之外)都有一個指向父結點的指針,這個問題可以轉換成求兩個鏈表的第一個公共結點。假設樹結點中指向父結點的指針是pParent,那么從樹的每一個葉結點開始都有一個由指針pParent串起來的鏈表,這些鏈表的尾指針都是樹的根結點。輸入兩個結點,那么這兩個結點位于兩個鏈表上,它們的最低公共祖先剛好就是這兩個鏈表的第一個公共結點。比如輸入的兩個結點分別為F和H,那么F在鏈表F->D->B->A上,而H在鏈表H->E->B->A上,這兩個鏈表的第一個交點B剛好也是它們的最低公共祖先。
面試官:求兩個鏈表的第一個共同結點這個題目你是不是之前也做過?
應聘者:(摸摸后腦勺,尷尬地笑笑)這個……又被你發現了……
面試官心理:
能夠把這個題目轉換成求兩個鏈表的第一個公共結點,你的知識遷移能力不錯。感覺你對數據結構很熟悉,基本上達到錄用標準了。不過我很有興趣看看你的極限在哪里。再加大點難度試試吧。
面試官:(笑)那只好再把題目的要求改變一下了。現在假設這棵樹是普通的樹,而且樹中的結點沒有指向父結點的指針。
應聘者:(稍微流露出一絲抓狂的表情,語氣中透出失望)好吧,我再想想。
面試官:這個題目也只比前面的兩個稍微難一點點,你能搞定的。
應聘者:(靜下來思考了兩分鐘)所謂兩個結點的公共祖先,指的是這兩個結點都出現在某個結點的子樹中。我們可以從根結點開始遍歷一棵樹,每遍歷到一個結點時,判斷兩個輸入結點是不是在它的子樹中。如果在子樹中,則分別遍歷它的所有子結點,并判斷兩個輸入結點是不是在它們的子樹中。這樣從上到下一直找到的第一個結點,它自己的子樹中同時包含兩個輸入的結點而它的子結點卻沒有,那么該結點就是最低的公共祖先。
面試官:能不能舉個具體的例子說明你的思路?
應聘者:(一邊在紙上畫圖7.2一邊解釋)假設還是輸入結點F和H。我們先判斷A的子樹中是否同時包含結點F和H,得到的結果為true。接著我們再先后判斷A的兩個子結點B和C的子樹是不是同時包含F和H,結果是B的結果是true而C的結果是false。接下來我們再判斷B的兩個子結點D和E,發現這兩個結點得到的結果都是false。于是B是最后一個公共祖先,即我們的輸出。

圖7.2 一棵普通的樹,樹中的結點沒有指向父結點的指針
面試官:聽起來不錯。很明顯,當我們判斷以結點A為根的樹中是否含有結點F的時候,我們需要對D、E等結點遍歷一遍;接下來判斷以結點B為根的樹中是否含有結點F的時候,我們還是需要對D、E等結點再遍歷一遍。這種思路會對同一結點重復遍歷很多次。你想想看還有沒有更快的算法?
應聘者:(雙肘抵住桌子,雙手抱住頭頂,苦苦思索兩分鐘)可以用輔助內存嗎?
面試官:你需要多大的輔助內存?
應聘者:我的想法是用兩個鏈表分別保存從根結點到輸入的兩個結點的路徑,然后把問題轉換成兩個鏈表的最后公共結點。
面試官:(點頭,面露贊許)嗯,具體說說。
應聘者:我們首先得到一條從根結點到樹中某一結點的路徑,這就要求在遍歷的時候,有一個輔助內存來保存路徑。比如我們用前序遍歷的方法來得到從根結點到H的路徑的過程是這樣的:(1)遍歷到A,把A存放到路徑中去,路徑中只有一個結點A;(2)遍歷到B,把B存到路徑中去,此時路徑為A->B;(3)遍歷到D,把D存放到路徑中去,此時路徑為A->B->D;(4)遍歷到F,把F存放到路徑中去,此時路徑為A->B->D->F;(5)F已經沒有子結點了,因此這條路徑不可能到達結點H。把F從路徑中刪除,變成A->B->D;(6)遍歷G。和結點F一樣,這條路徑也不能到達H。遍歷完G之后,路徑仍然是A->B->D;(7)由于D的所有子結點都遍歷過了,不可能到達結點H,因此D不在從A到H的路徑中,把D從路徑中刪除,變成A->B;(8)遍歷E,把E加入到路徑中,此時路徑變成A->B->E,(9)遍歷H,已經到達目標結點,A->B->E就是從根結點開始到達H必須經過的路徑。
面試官:然后呢?
應聘者:同樣,我們也可以得到從根結點開始到達F必須經過的路徑是A->B->D。接著,我們求出這兩個路徑的最后公共結點,也就是B。B這個結點也是F和H的最低公共祖先。
面試官:這種思路的時間和空間效率是多少?
應聘者:為了得到從根結點開始到輸入的兩個結點的兩條路徑,需要遍歷兩次樹,每遍歷一次的時間復雜度是O(n)。得到的兩條路徑的長度在最差情況時是O(n),通常情況下兩條路徑的長度是O(logn)。
面試官心理:
顯然,你對數據結構的理解比大多數人要深刻得多,期待你的代碼。
面試官:(微笑,點頭)不錯。根據這個思路寫出C/C++代碼,怎么樣?
應聘者:好的,沒問題。
應聘者先后下了三個函數:

應聘者:代碼中GetNodePath用來得到從根結點pRoot開始到達結點pNode的路徑,這條路徑保存在path中。函數LastCommonNode用來得到兩個路徑path1和path2的最后一個公共結點。函數GetLastCommonParent先調用GetNodePath得到pRoot到達pNode1的路徑path1,再得到pRoot到達pNode2的路徑path2,接著調用GetLastCommonPath得到path1和path2的最后一個公共結點,即我們要找的最低公共祖先。
面試官:嗯,很好。這輪面試的時間已經很長了,我的問題就到這里。你有什么需要問我的嗎?
應聘者:(略作思考)我想問問關于項目合作的事情。你們項目組和美國總部的同事是怎么合作的?是美國人做好設計,然后交給中國這邊做具體實現嗎?
面試官:理論上說中國同事與美國同事之間的合作是平等的,不全是美國說了算。我們中國的團隊也有自己的項目經理做產品設計。現在兩邊的團隊都有自己負責的功能。只是我們的團隊成員和美國同事比起來,經驗還不夠,對產品的理解沒有美國同事那么深刻。因此當兩邊意見出現分歧的時候,他們的意見更能得到上層的重視。
應聘者:兩邊的團隊都有哪些溝通的方式?
面試官:平時做相關工作的同事之間會有大量的E-mail交流。每周二的早上(美國時間是星期一的下午)我們有一個例會,所有同事都會參加。在會上大家會討論項目的進度、遇到的困難等事項。另外,由于最近我們這邊招了不少新員工,美國那邊正計劃選派一個資深的工程師過來給新員工做培訓。
應聘者:那中國這邊的員工有機會去美國嗎?
面試官:我們的人力資源部門有一個項目,讓新員工在兩年之內至少有機會去美國接受一次培訓,以熟悉公司總部的文化。只是最近由于大的經濟環境不是很好,公司在嚴格控制差旅費用,因此這個項目的執行受到了一點影響。還有其他問題嗎?
應聘者:(想了一會兒)沒有了。
面試官心理:
從最后幾個問題可以看出,你對我們的項目和團隊很有興趣。同樣,我也希望你能加入我們的團隊一起做項目。這輪面試你通過了。
面試官:由于時間關系,這輪面試就到這里,怎么樣?
應聘者:(摸摸額頭,微笑中略顯疲憊)謝謝。
面試官點評:
求樹中兩個結點的最低公共祖先,不能說只是一個題目,而應該說是一組題目,不同條件下的題目是完全不一樣的。一開始的時候,我有意沒有說明樹的特點,比如樹是不是二叉樹、樹中的結點是不是有一個指向父結點的指針。我把題目說得模棱兩可是希望應聘者能夠主動向我提出問題,一步一步弄清我的意圖。如果一個應聘者能夠在面試過程中主動問出高質量的問題以弄清楚題目的要求,我會覺得他態度積極并且具有較強的溝通能力。
在這輪面試中,該應聘者表現得比較積極主動。一開始聽到題目之后,他馬上詢問我樹是不是二叉樹。在我答復可以是二叉樹之后他立即給出了當樹是二叉排序樹時的解法。我看出了他之前做過這個問題,于是就把題目的要求設為樹只是普通的樹而不一定是二叉樹。他的反應很快,立即又問我樹中的結點有沒有指向父結點的指針。在第二個問題得到肯定的答復之后,他把問題轉換成求兩個鏈表的第一個公共結點。他這段的表現很好,問的兩個問題都很有針對性,表明他對這種類型的問題有很深的理解,給我留下了很好的印象。
通常面試的時候讓應聘者寫出有指向父結點的指針這種情況的代碼也就差不多了,但考慮到他之前做過類似的問題,同時我覺得他反應很快,功底不錯,以他的能力應該可以挑戰一下更高的難度。于是我接下來把指向父結點的指針去掉,決定再加大難度測試一下他的水平到底有多深。他再一次表現出很快的反應能力,思考了一兩分鐘之后就想出了一個需要重復遍歷一個結點多次的算法。在我提示出還有更快的算法之后,他再次把題目轉換成求鏈表的共同結點的問題。期間在他解釋其思路的過程中,可以看出他對樹的遍歷算法理解得很透徹,接下來寫出的代碼也很規范。綜合這名應聘者在本輪面試中的表現,我強烈建議我們公司錄用他。
如果面試官在面試的過程中逐步加大面試題的難度,通常對應聘者來說是件好事,這說明應聘者一開始表現得很好,面試官對他的印象很好,并很有興趣看看他的水平有多深,于是一步一步加大題目的難度。雖然最后應聘者可能不能很好地解決高難度的問題,但最終仍有可能拿到Offer。與此相反的是,有些應聘者覺得面試的時候很多問題都回答出來了,可最終被拒,覺得難以理解。其實這是因為一開始問的問題他回答得很不好,面試官已經出判斷他的能力有限,心里已經默默給出了NO的結論。但為了照顧應聘者的情面,也會問幾個簡單的問題。雖然這些簡單的問題應聘者可能都能答對,但前面的結果已經不會改變。
在這輪面試中,由于該應聘者一開始的表現很好,我才決定加大難度考考他。假如他最后普通樹中結點沒有指向父結點的指針這個問題沒有很好地解決,我會讓他回頭去寫普通樹中結點有指向父結點的指針這個問題的代碼。只要他的代碼寫得完整正確,我仍然會讓他通過我的這輪面試,盡管我對他的評價可能沒有現在這么高。
- 目錄
- 扉頁
- 版權頁
- 推薦序一
- 推薦序二
- 前言
- 第1章 面試的流程
- 1.1 面試官談面試
- 1.2 面試的三種形式
- 1.2.1 電話面試
- 1.2.2 共享桌面遠程面試
- 1.2.3 現場面試
- 1.3 面試的三個環節
- 1.3.1 行為面試環節
- 1.應聘者的項目經驗
- 2.應聘者掌握的技能
- 3.回答“為什么跳槽”
- 1.3.2 技術面試環節
- 1.扎實的基礎知識
- 2.高質量的代碼
- 3.清晰的思路
- 4.優化效率的能力
- 5.優秀的綜合能力
- 1.3.3 應聘者提問環節
- 1.4 本章小結
- 第2章 面試需要的基礎知識
- 2.1 面試官談基礎知識
- 2.2 編程語言
- 2.2.1 C++
- 面試題1:賦值運算符函數
- 經典的解法,適用于初級程序員
- 考慮異常安全性的解法,高級程序員必備
- 2.2.2 C#
- 面試題2:實現Singleton模式
- 不好的解法一:只適用于單線程環境
- 不好的解法二:雖然在多線程環境中能工作但效率不高
- 可行的解法:加同步鎖前后兩次判斷實例是否已存在
- 強烈推薦的解法一:利用靜態構造函數
- 強烈推薦的解法二:實現按需創建實例
- 解法比較
- 2.3 數據結構
- 2.3.1 數組
- 面試題3:二維數組中的查找
- 2.3.2 字符串
- 面試題4:替換空格
- 時間復雜度為O(n2)的解法,不足以拿到Offer
- 時間復雜度為O(n)的解法,搞定Offer就靠它了
- 2.3.3 鏈表
- 面試題5:從尾到頭打印鏈表
- 2.3.4 樹
- 面試題6:重建二叉樹
- 2.3.5 棧和隊列
- 面試題7:用兩個棧實現隊列
- 2.4 算法和數據操作
- 2.4.1 查找和排序
- 面試題8:旋轉數組的最小數字
- 2.4.2 遞歸和循環
- 面試題9:斐波那契數列
- 效率很低的解法,挑剔的面試官不會喜歡
- 面試官期待的實用解法
- 時間復雜度O(logn)但不夠實用的解法
- 解法比較
- 2.4.3 位運算
- 面試題10:二進制中1的個數
- 可能引起死循環的解法
- 常規解法
- 能給面試官帶來驚喜的解法
- 2.5 本章小結
- 第3章 高質量的代碼
- 3.1 面試官談代碼質量
- 3.2 代碼的規范性
- 3.3 代碼的完整性
- 1.從3方面確保代碼的完整性
- 2.3種錯誤處理的方法
- 面試題11:數值的整數次方
- 自以為題目簡單的解法
- 全面但不夠高效的解法,我們離Offer已經很近了
- 全面又高效的解法,確保我們能拿到Offer
- 面試題12:打印1到最大的n位數
- 跳進面試官陷阱
- 在字符串上模擬數字加法的解法,繞過陷阱才能拿到Offer
- 把問題轉換成數字排列的解法,遞歸讓代碼更簡潔
- 面試題13:在O(1)時間刪除鏈表結點
- 面試題14:調整數組順序使奇數位于偶數前面
- 只完成基本功能的解法,僅適用于初級程序員
- 考慮可擴展性的解法,能秒殺Offer
- 3.4 代碼的魯棒性
- 面試題15:鏈表中倒數第k個結點
- 面試題16:反轉鏈表
- 面試題17:合并兩個排序的鏈表
- 面試題18:樹的子結構
- 3.5 本章小結
- 第4章 解決面試題的思路
- 4.1 面試官談面試思路
- 4.2 畫圖讓抽象問題形象化
- 面試題19:二叉樹的鏡像
- 面試題20:順時針打印矩陣
- 4.3 舉例讓抽象問題具體化
- 面試題21:包含min函數的棧
- 面試題22:棧的壓入、彈出序列
- 面試題23:從上往下打印二叉樹
- 面試題24:二叉搜索樹的后序遍歷序列
- 面試題25:二叉樹中和為某一值的路徑
- 4.4 分解讓復雜問題簡單化
- 面試題26:復雜鏈表的復制
- 面試題27:二叉搜索樹與雙向鏈表
- 面試題28:字符串的排列
- 4.5 本章小結
- 第5章 優化時間和空間效率
- 5.1 面試官談效率
- 5.2 時間效率
- 面試題29:數組中出現次數超過一半的數字
- 解法一:基于Partition函數的O(n)算法
- 解法二:根據數組特點找出O(n)的算法
- 解法比較
- 面試題30:最小的k個數
- 解法一:O(n)的算法,只有當我們可以修改輸入的數組時可用
- 解法二:O(nlogk)的算法,特別適合處理海量數據
- 解法比較
- 面試題31:連續子數組的最大和
- 解法一:舉例分析數組的規律
- 解法二:應用動態規劃法
- 面試題32:從1到n整數中1出現的次數
- 不考慮時間效率的解法,靠它想拿Offer有點難
- 從數字規律著手明顯提高時間效率的解法,能讓面試官耳目一新
- 面試題33:把數組排成最小的數
- 5.3 時間效率與空間效率的平衡
- 面試題34:丑數
- 逐個判斷每個整數是不是丑數的解法,直觀但不夠高效
- 創建數組保存已經找到的丑數,用空間換時間的解法
- 面試題35:第一個只出現一次的字符
- 面試題36:數組中的逆序對
- 面試題37:兩個鏈表的第一個公共結點
- 5.4 本章小結
- 第6章 面試中的各項能力
- 6.1 面試官談能力
- 6.2 溝通能力和學習能力
- 1.溝通能力
- 2.學習能力
- 3.善于學習、溝通的人也善于提問
- 6.3 知識遷移能力
- 面試題38:數字在排序數組中出現的次數
- 面試題39:二叉樹的深度
- 需要重復遍歷結點多次的解法,簡單但不足以打動面試官
- 每個結點只遍歷一次的解法,正是面試官喜歡的
- 面試題40:數組中只出現一次的數字
- 面試題41:和為s的兩個數字VS和為s的連續正數序列
- 面試題42:翻轉單詞順序VS左旋轉字符串
- 6.4 抽象建模能力
- 面試題43:n個骰子的點數
- 解法一:基于遞歸求骰子點數,時間效率不夠高
- 解法二:基于循環求骰子點數,時間性能好
- 面試題44:撲克牌的順子
- 面試題45:圓圈中最后剩下的數字
- 經典的解法,用環形鏈表模擬圓圈
- 創新的解法,拿到Offer不在話下
- 6.5 發散思維能力
- 面試題46:求1+2+…+n
- 解法一:利用構造函數求解
- 解法二:利用虛函數求解
- 解法三:利用函數指針求解
- 解法四:利用模板類型求解
- 面試題47:不用加減乘除做加法
- 面試題48:不能被繼承的類
- 常規的解法:把構造函數設為私有函數
- 新奇的解法:利用虛擬繼承,能給面試官留下很好的印象
- 6.6 本章小結
- 第7章 兩個面試案例
- 7.1 案例一:(面試題49)把字符串轉換成整數
- 7.2 案例二:(面試題50)樹中兩個結點的最低公共祖先