4-40.
如果給你1,000,000個整數來排序,你會選擇什么算法?消耗的時間和空間呢?
解析:
我個人傾向於用隨機化的快速排序。
首先是它在平均意義上來看比同樣O(nlogn)的歸並排序和堆排序快(見4-41)。
另外,和堆排序相比,快速排序的元素掃描是線性的,而且交換常被限制在一個有限范圍內。假如這所有的整數不能存入內存,那么發生缺頁中斷的次數也小於堆排序。當然,當數據量更大時,問題就會牽扯到內部排序(英文維基/百度百科)和外部排序(英文維基/百度百科)的討論。
同時,在《編程珠璣》上看到,如果這些數字有特征,如不重復出現,且范圍不是很大,那么可以設計出專門的算法來完成,比如使用位向量排序。
面試時的開放型題目,不妨盡可能廣泛而深入的探討。
4-41.
分析最常見的排序算法的優點和缺點。
解析:
這個問題老生常談了,相關文章特別多,不打算在這里解答。
p.s.,原書正文提到,雖然都是O(nlogn)的時間復雜度,而且最壞情況下快速排序退化為O(n2),但快速排序比歸並排序和對排序在多數情況下都快2~3倍,原因是它的最內層迭代語句最簡單和快速(原書4.6.3節)。
4-42.
實現一個算法,返回數組中只出現一次的元素。
解析:
如果先排序,再遍歷,時間復雜度O(nlogn)。
如果遍歷時進行hash,然后輸出整個hash表,那么時間復雜度O(n)。(《劍指Offer》面試題35:第一個只出現一次的字符,這種方法可以找出所有只出現一次的字符)
如果加上額外的條件:只有一個元素出現了一次,其他都出現了偶數次,那么把所有元素做異或,最終結果就是只出現一次的元素,時間復雜度O(n)。(《編程之美》1.5快速找出故障機器)
4-43.
限制2Mb內存,如何排序一個500Mb的文件?
解析:
正如4-40提到的,使用外部排序吧。常見的是多路歸並排序,以下摘自百度百科:
外部排序最常用的算法是多路歸並排序,即將原文件分解成多個能夠一次性裝人內存的部分,分別把每一部分調入內存完成排序。然后,對已經排序的子文件進行歸並排序。
4-44.
設計一個棧,支持O(1)內完成push、pop和獲得最小值min的操作。
解析:
一般思路是維護兩個棧,一個和一般的棧一樣,另一個用維護每個元素壓入第一個棧時的最小值。《劍指Offer》面試題21:包含min函數的棧有詳細分析,下面是它的代碼實現:

template <typename T> class StackWithMin { public: StackWithMin(void) {} virtual ~StackWithMin(void) {} T& top(void); const T& top(void) const; void push(const T& value); void pop(void); const T& min(void) const; bool empty() const; size_t size() const; private: std::stack<T> m_data; // 數據棧,存放棧的所有元素 std::stack<T> m_min; // 輔助棧,存放棧的最小元素 }; template <typename T> void StackWithMin<T>::push(const T& value) { // 把新元素添加到輔助棧 m_data.push(value); // 當新元素比之前的最小元素小時,把新元素插入輔助棧里; // 否則把之前的最小元素重復插入輔助棧里 if(m_min.size() == 0 || value < m_min.top()) m_min.push(value); else m_min.push(m_min.top()); } template <typename T> void StackWithMin<T>::pop() { assert(m_data.size() > 0 && m_min.size() > 0); m_data.pop(); m_min.pop(); } template <typename T> const T& StackWithMin<T>::min() const { assert(m_data.size() > 0 && m_min.size() > 0); return m_min.top(); } template <typename T> T& StackWithMin<T>::top() { return m_data.top(); }
4-45.
給定3個字母組成的字符串,比如ABC,和一篇文檔。找出文檔中的包含這3個字母的最短片段。同時,各個字母在文檔中出現位置的下標已經存放在一個排序數組中,比如A:[1,4,5]。
(補充說明)為了幫助理解原題題意,下面幾個典型輸入和輸出。
input1: [1,10], [2,20], [3,30]
output1:[1, 3],length=3
input2:[1,9,27], [6,10,19], [8,12,14]
output2:[8, 10],length=3
input3:[1,4,11,27], [3,6,10,19], [5,8,12,14]
output3:[3, 5],length=3
input4:[1,4,5], [3,9,10], [2,6,15]
output4:[1, 3],length=3
解析:
假定文檔是CxxxAxxxBxxAxxCxBAxxxC,其中x代表非ABC的其他字母或符號。掃描過程是這樣的:
C CA CAB - all words, length 9 (CxxxAxxxB...) CABA - all words, length 12 (CxxxAxxxBxxA...) CABAC - violates The Property, remove first C ABAC - violates The Property, remove first A BAC - all words, length 7 (...BxxAxxC...) BACB - violates The Property, remove first B ACB - all words, length 6 (...AxxCxB...) ACBA - violates The Property, remove first A CBA - all words, length 4 (...CxBA...) CBAC - violates The Property, remove first C BAC - all words, length 6 (...BAxxxC)
這個過程可以總結為:
對三個數組進行歸並,維護這個歸並字符串並統計歸並的字符串中A、B、C的個數;
歸並時,當新加入的字符導致滿足A、B、C都出現時,統計這時片段長度並與最小值比較,如果小於最小值則更新並記錄開始和結尾的索引;
當新加入的字符與歸並字符串第一個字符相同時,刪去第一個字符串。如果此時新的第一個字符在字符串出現次數非0,同樣刪去。這個過程遞歸進行直到首字母只出現了1次。
繼續歸並直到整片文檔掃描完畢。
方法來自於stackoverflow。
類似問題:《編程之美》3.5最短摘要的生成
4-46.
12個硬幣,其中11個是重量相同的真幣,另一個是假幣,重量與它們不同,但可能輕了也可能重了。請用天平只稱三次就確定哪個是假幣。
解析:
如果已知不標准的硬幣是輕還是重,那么很簡單,直接分3組,稱第一二組確定出硬幣在哪組,然后再組內對半稱,最后再對半稱。但此時不知是輕是重,這個方法不可行。
這里輕重未知,在每次稱量時,應該盡量利用上次稱量出的輕重關系。為了便於敘述,講12個硬幣標記為1、2、...12。先稱量1+2+3+4和5+6+7+8:
如果相等,那么假幣在9、10、11、12中。此時已知1~8是真幣,可以作為標准來判斷,那么使用1+10和2+12比較,如果相同則假幣在9和11中,9和1稱來判斷假幣是9還是11;否則用1和10來稱一次判斷假幣是10還是12。
如果不等,假設1+2+3+4>5+6+7+8(反之類似),此時9、10、11、12是真幣。那么將1、2、3去掉換成5、6、7,再在右邊加上標准的9、10、11,形成5+6+7+4和9+10+11+8比較。
如果相等,假幣只可能在1、2、3中,並且由第一次稱量的結果,假幣比真幣重。從1、2、3中選擇2個,若平衡,則剩余一個為假幣,不平衡時重的那個是假幣。
如果5+6+7+4<9+10+11+8,只可能因為輕的假幣來到了左邊。那么就在5、6、7中判斷那個輕的假幣,和上面類似。
如果5+6+7+4>9+10+11+8,5、6、7必然都不是假幣,那么只用判斷4和8哪個是假幣。使用1枚真幣和4稱量即可判斷。
擴展一下,可以發現這個方法也能判斷13枚的情況:先分成12枚和1枚,如果假幣在12枚中,分析同上;如果假幣是那分出來的1枚,上面第一種相等的情況每次都是相等,判斷完三次就可得出結論:假幣不在12枚中,只能是那額外的1枚。
另外,官方wiki answer里是一個萬能的分組解法,操作起來按部就班,直接根據結果查表即可,但分組理由沒有詳細解釋,就沒有深入研究。