第十六~第二十章:全排列,跳臺階,奇偶排序,第一個只出現一次等問題
作者:July、2011.10.16。
出處: http://blog.csdn.net/v_JULY_v 。
引言
最近這幾天閑職在家,一忙著投簡歷,二為準備面試而搜集整理各種面試題。故常常關注個人所建的Algorithms1-14群內朋友關于筆試,面試,宣講會,offer,薪資的討論以及在群內發布的各種筆/面試題,常感言道:咱們這群人之前已經在學校受夠了學校的那種應試教育,如今出來找工作又得東奔西走去參加各種筆試/面試,著實亦不輕松。幻想,如果在企業與求職者之間有個中間面試服務平臺就更好了。
ok,閑話少扯。在上一篇文章中,已經說過,“個人正在針對那100題一題一題的寫文章,多種思路,不斷優化,即成程序員編程藝術系列。”現本編程藝術系列繼續開始創作,你而后自會和我有同樣的感慨:各種面試題千變萬化,層出不窮,但基本類型,解決問題的思路基本一致。
本文為程序員編程藝術第十六章~第二十章,包含以下5個問題:
- 全排列;
- 跳臺階;
- 奇偶排序;
- 第一個只出現一次的字符;
- 一致性哈希算法。
同時,本文會在解答去年微軟面試100題的部分題目時,盡量結合今年最近各大IT公司最新的面試題來講解,兩相對比,彼此對照,相信你會更加贊同我上面的話。且本文也不奢望讀者能從中學到什么高深技術之類的東西,只求讀者看此文看著 舒服 便可 , 通順流暢以致一口氣讀完而無任何壓力。ok,有任何問題,歡迎不吝指正。謝謝。
第十六章、全排列問題
53.字符串的排列。
題目:輸入一個字符串,打印出該字符串中字符的所有排列。
例如輸入字符串abc,則輸出由字符a、b、c 所能排列出來的所有字符串
abc、acb、bac、bca、cab 和cba。
分析:此題最初整理于去年的微軟面試100題中第53題,第二次整理于
微軟、Google等公司非常好的面試題及解答[第61-70題]
第67題。無獨有偶,這個問題今年又出現于今年的2011.10.09百度筆試題中。ok,接下來,咱們先好好分析這個問題。
-
一、遞歸實現
從集合中依次選出每一個元素,作為排列的第一個元素,然后對剩余的元素進行全排列,如此遞歸處理,從而得到所有元素的全排列。以對字符串abc進行全排列為例,我們可以這么做:以abc為例
固定a,求后面bc的排列:abc,acb,求好后,a和b交換,得到bac
固定b,求后面ac的排列:bac,bca,求好后,c放到第一位置,得到cba
固定c,求后面ba的排列:cba,cab。代碼可如下編寫所示:
- template < typename T>
- void CalcAllPermutation_R(Tperm[], int first, int num)
- {
- if (num<=1){
- return ;
- }
- for ( int i=first;i<first+num;++i){
- swap(perm[i],perm[first]);
- CalcAllPermutation_R(perm,first+1,num-1);
- swap(perm[i],perm[first]);
- }
- }
-
二、字典序排列
把升序的排列(當然,也可以實現為降序)作為當前排列開始,然后依次計算當前排列的下一個字典序排列。
對當前排列從后向前掃描,找到一對為升序的相鄰元素,記為i和j(i < j)。如果不存在這樣一對為升序的相鄰元素,則所有排列均已找到,算法結束;否則,重新對當前排列從后向前掃描,找到第一個大于i的元素k,交換i和k,然后對從j開始到結束的子序列反轉,則此時得到的新排列就為下一個字典序排列。這種方式實現得到的所有排列是按字典序有序的,這也是C++ STL算法next_permutation的思想。算法實現如下:
- template < typename T>
- void CalcAllPermutation(Tperm[], int num)
- {
- if (num<1)
- return ;
- while ( true ){
- int i;
- for (i=num-2;i>=0;--i){
- if (perm[i]<perm[i+1])
- break ;
- }
- if (i<0)
- break ; //已經找到所有排列
- int k;
- for (k=num-1;k>i;--k){
- if (perm[k]>perm[i])
- break ;
- }
- swap(perm[i],perm[k]);
- reverse(perm+i+1,perm+num);
- }
- }
第十七章、跳臺階問題
27.跳臺階問題
題目:一個臺階總共有n 級,如果一次可以跳1 級,也可以跳2 級。
求總共有多少總跳法,并分析算法的時間復雜度。
分析:在 九月騰訊,創新工場,淘寶等公司最新面試十三題 中第23題又出現了這個問題,題目描述如下:23、人人筆試1:一個人上臺階可以一次上1個,2個,或者3個,問這個人上n層的臺階,總共有幾種走法?咱們先撇開這個人人筆試的問題(其實差別就在于人人筆試題中多了一次可以跳三級的情況而已),先來看這個第27題。
首先考慮最簡單的情況。如果只有1級臺階,那顯然只有一種跳法。如果有2級臺階,那就有兩種跳的方法了:一種是分兩次跳,每次跳1級;另外一種就是一次跳2級。
現在我們再來討論一般情況。我們把n級臺階時的跳法看成是n的函數,記為f(n)。當n>2時,第一次跳的時候就有兩種不同的選擇:一是第一次只跳1級,此時跳法數目等于后面剩下的n-1級臺階的跳法數目,即為f(n-1);另外一種選擇是第一次跳2級,此時跳法數目等于后面剩下的n-2級臺階的跳法數目,即為f(n-2)。因此n級臺階時的不同跳法的總數f(n)=f(n-1)+(f-2)。
我們把上面的分析用一個公式總結如下:
/ 1 n=1
f(n)= 2 n=2
\ f(n-1)+(f-2) n>2
原來上述問題就是我們平常所熟知的 Fibonacci 數列問題。 可編寫代碼,如下:
那么,如果是人人筆試那道題呢?一個人上臺階可以一次上1個,2個,或者3個,豈不是可以輕而易舉的寫下如下公式:
/ 1 n=1
f(n)= 2 n=2
4 n=3 //
111, 12, 21, 3
\ f(n-1)+(f-2)+f(n-3) n>3
行文至此,你可能會認為問題已經解決了,但事實上沒有:
-
用遞歸方法計算的時間復雜度是以
n
的指數的方式遞增的,我們可以嘗試用遞推方法解決。具體如何操作,讀者自行思考。
- 有一種方法,能在O(logn)的時間復雜度內求解Fibonacci數列問題,你能想到么?
-
同時,有朋友指出對于這個臺階問題只需求冪就可以了(求復數冪C++庫里有),不用任何循環且復雜度為O(1),如下圖所示,是否真如此?:
第十八章、奇偶調序
題目:輸入一個整數數組,調整數組中數字的順序,使得所有奇數位于數組的前半部分,
所有偶數位于數組的后半部分。要求時間復雜度為O(n)。
分析:
-
你當然可以從頭掃描這個數組,每碰到一個偶數時,拿出這個數字,并把位于這個數字后面的所有數字往前挪動一位。挪完之后在數組的末尾有一個空位,這時把該偶數放入這個空位。由于碰到一個偶數,需要移動
O(n)
個數字,只是這種方法總的時間復雜度是
O(n
2
),不符合要求,pass
。
-
很簡單,維護兩個指針,一個指針指向數組的第一個數字,向后移動;一個個指針指向最后一個數字,向前移動。如果第一個指針指向的數字是偶數而第二個指針指向的數字是奇數,我們就交換這兩個數字。
第十九章、第一個只出現一次的字符
代碼,可編寫如下:
- #include<iostream>
- using namespace std;
- //查找第一個只出現一次的字符,第1個程序
- //copyright@Sorehead&&July
- //July、updated,2011.04.24.
- char find_first_unique_char( char *str)
- {
- int data[256];
- char *p;
- if (str==NULL)
- return '\0' ;
- memset(data,0, sizeof (data)); //數組元素先全部初始化為0
- p=str;
- while (*p!= '\0' )
- data[(unsigned char )*p++]++; //遍歷字符串,在相應位置++,(同時,下標強制轉換)
- while (*str!= '\0' )
- {
- if (data[(unsigned char )*str]==1) //最后,輸出那個第一個只出現次數為1的字符
- return *str;
- str++;
- }
- return '\0' ;
- }
- int main()
- {
- char *str= "afaccde" ;
- cout<<find_first_unique_char(str)<<endl;
- return 0;
- }
- //查找第一個只出現一次的字符,第2個程序
- //copyright@yansha
- //July、updated,2011.04.24.
- char FirstNotRepeatChar( char *pString)
- {
- if (!pString)
- return '\0' ;
- const int tableSize=256;
- int hashTable[tableSize]={0}; //存入數組,并初始化為0
- char *pHashKey=pString;
- while (*(pHashKey)!= '\0' )
- hashTable[*(pHashKey++)]++;
- while (*pString!= '\0' )
- {
- if (hashTable[*pString]==1)
- return *pString;
- pString++;
- }
- return '\0' ; //沒有找到滿足條件的字符,退出
- }
第二十章、一致性哈希算法
tencent2012筆試題附加題
問題描述: 例如手機朋友網有n個服務器,為了方便用戶的訪問會在服務器上緩存數據,因此用戶每次訪問的時候最好能保持同一臺服務器。
已有的做法是根據ServerIPIndex[QQNUM%n]得到請求的服務器,這種方法很方便將用戶分到不同的服務器上去。但是如果一臺服務器死掉了,那么n就變為了n-1,那么ServerIPIndex[QQNUM%n]與ServerIPIndex[QQNUM%(n-1)]基本上都不一樣了,所以大多數用戶的請求都會轉到其他服務器,這樣會發生大量訪問錯誤。
問: 如何改進或者換一種方法,使得:
(1)一臺服務器死掉后,不會造成大面積的訪問錯誤,
(2)原有的訪問基本還是停留在同一臺服務器上;
(3)盡量考慮負載均衡。(
思路:往分布式一致哈希算法方面考慮。
)
-
最土的辦法還是用模余方法:做法很簡單,假設有N臺服務器,現在完好的是M(M<=N),先用N求模,如果不落在完好的機器上,然后再用N-1求模,直到M.這種方式對于壞的機器不多的情況下,具有更好的穩定性。
- 一致性哈希算法。
下面,本文剩下部分重點來講講這個一致性哈希算法。
應用場景
在做服務器負載均衡時候可供選擇的負載均衡的算法有很多,包括: 輪循算法(Round Robin)、哈希算法(HASH)、最少連接算法(Least Connection)、響應速度算法(Response Time)、加權法(Weighted )等。其中哈希算法是最為常用的算法.
典型的應用場景是: 有N臺服務器提供緩存服務,需要對服務器進行負載均衡,將請求平均分發到每臺服務器上,每臺機器負責1/N的服務。
常用的算法是對hash結果取余數 (hash() mod
N
):對機器編號從0到N-1,按照自定義的hash()算法,對每個請求的hash()值按N取模,得到余數i,然后將請求分發到編號為i的機器。但這樣的算法方法存在致命問題,如果某一臺機器宕機,那么應該落在該機器的請求就無法得到正確的處理,這時需要將當掉的服務器從算法從去除,此時候會有(N-1)/N的服務器的緩存數據需要重新進行計算;如果新增一臺機器,會有N /(N+1)的服務器的緩存數據需要進行重新計算。對于系統而言,這通常是不可接受的顛簸(因為這意味著大量緩存的失效或者數據需要轉移)。那么,如何設計一個負載均衡策略,使得受到影響的請求盡可能的少呢?
在Memcached、Key-Value Store、Bittorrent DHT、LVS中都采用了Consistent Hashing算法,可以說Consistent Hashing 是分布式系統負載均衡的首選算法。
Consistent Hashing算法描述
下面以Memcached中的Consisten Hashing算法為例說明。
consistent hashing算法早在1997年就在論文 Consistent hashing and random trees 中被提出,目前在cache系統中應用越來越廣泛;
1基本場景
比如你有N個cache服務器(后面簡稱cache),那么如何將一個對象object映射到N個cache上呢,你很可能會采用類似下面的通用方法計算object的hash值,然后均勻的映射到到N個cache;
hash(object)%N
一切都運行正常,再考慮如下的兩種情況;
- 一個cache服務器m down掉了(在實際應用中必須要考慮這種情況),這樣所有映射到cache m的對象都會失效,怎么辦,需要把cache m從cache中移除,這時候cache是N-1臺,映射公式變成了hash(object)%(N-1);
- 由于訪問加重,需要添加cache,這時候cache是N+1臺,映射公式變成了hash(object)%(N+1);
1和2意味著什么?這意味著突然之間幾乎所有的cache都失效了。對于服務器而言,這是一場災難,洪水般的訪問都會直接沖向后臺服務器;再來考慮第三個問題,由于硬件能力越來越強,你可能想讓后面添加的節點多做點活,顯然上面的hash算法也做不到。
有什么方法可以改變這個狀況呢,這就是consistent hashing。
2 hash算法和單調性
Hash算法的一個衡量指標是單調性(Monotonicity),定義如下:
單調性是指如果已經有一些內容通過哈希分派到了相應的緩沖中,又有新的緩沖加入到系統中。哈希的結果應能夠保證原有已分配的內容可以被映射到新的緩沖中去,而不會被映射到舊的緩沖集合中的其他緩沖區。
容易看到,上面的簡單hash算法hash(object)%N難以滿足單調性要求。
3 consistent hashing算法的原理
consistent hashing是一種hash算法,簡單的說,在移除/添加一個cache時,它能夠盡可能小的改變已存在key映射關系,盡可能的滿足單調性的要求。
下面就來按照5個步驟簡單講講consistent hashing算法的基本原理。
3.1環形hash空間
考慮通常的hash算法都是將value映射到一個32為的key值,也即是0~2^32-1次方的數值空間;我們可以將這個空間想象成一個首(0)尾(2^32-1)相接的圓環,如下面圖1所示的那樣。
圖1環形hash空間
3.2把對象映射到hash空間
接下來考慮4個對象object1~object4,通過hash函數計算出的hash值key在環上的分布如圖2所示。
hash(object1) = key1;
… …
hash(object4) = key4;
圖2 4個對象的key值分布
3.3把cache映射到hash空間
Consistent hashing的基本思想就是將對象和cache都映射到同一個hash數值空間中,并且使用相同的hash算法。
假設當前有A,B和C共3臺cache,那么其映射結果將如圖3所示,他們在hash空間中,以對應的hash值排列。
hash(cache A) = key A;
… …
hash(cache C) = key C;
圖3 cache和對象的key值分布
說到這里,順便提一下cache的hash計算,一般的方法可以使用cache機器的IP地址或者機器名作為hash輸入。
3.4把對象映射到cache
現在cache和對象都已經通過同一個hash算法映射到hash數值空間中了,接下來要考慮的就是如何將對象映射到cache上面了。
在這個環形空間中,如果沿著順時針方向從對象的key值出發,直到遇見一個cache,那么就將該對象存儲在這個cache上,因為對象和cache的hash值是固定的,因此這個cache必然是唯一和確定的。這樣不就找到了對象和cache的映射方法了嗎?!
依然繼續上面的例子(參見圖3),那么根據上面的方法,對象object1將被存儲到cache A上;object2和object3對應到cache C;object4對應到cache B;
3.5考察cache的變動
前面講過,通過hash然后求余的方法帶來的最大問題就在于不能滿足單調性,當cache有所變動時,cache會失效,進而對后臺服務器造成巨大的沖擊,現在就來分析分析consistent hashing算法。
3.5.1移除cache
考慮假設cache B掛掉了,根據上面講到的映射方法,這時受影響的將僅是那些沿cache B逆時針遍歷直到下一個cache(cache C)之間的對象,也即是本來映射到cache B上的那些對象。
因此這里僅需要變動對象object4,將其重新映射到cache C上即可;參見圖4。
圖4 Cache B被移除后的cache映射
3.5.2添加cache
再考慮添加一臺新的cache D的情況,假設在這個環形hash空間中,cache D被映射在對象object2和object3之間。這時受影響的將僅是那些沿cache D逆時針遍歷直到下一個cache(cache B)之間的對象(它們是也本來映射到cache C上對象的一部分),將這些對象重新映射到cache D上即可。
因此這里僅需要變動對象object2,將其重新映射到cache D上;參見圖5。
圖5添加cache D后的映射關系
4虛擬節點
考量Hash算法的另一個指標是平衡性(Balance),定義如下:
平衡性
平衡性是指哈希的結果能夠盡可能分布到所有的緩沖中去,這樣可以使得所有的緩沖空間都得到利用。
hash算法并不是保證絕對的平衡,如果cache較少的話,對象并不能被均勻的映射到cache上,比如在上面的例子中,僅部署cache A和cache C的情況下,在4個對象中,cache A僅存儲了object1,而cache C則存儲了object2、object3和object4;分布是很不均衡的。
為了解決這種情況,consistent hashing引入了“虛擬節點”的概念,它可以如下定義:
“虛擬節點”(virtual node)是實際節點在hash空間的復制品(replica),一實際個節點對應了若干個“虛擬節點”,這個對應個數也成為“復制個數”,“虛擬節點”在hash空間中以hash值排列。
仍以僅部署cache A和cache C的情況為例,在圖4中我們已經看到,cache分布并不均勻。現在我們引入虛擬節點,并設置“復制個數”為2,這就意味著一共會存在4個“虛擬節點”,cache A1, cache A2代表了cache A;cache C1, cache C2代表了cache C;假設一種比較理想的情況,參見圖6。
圖6引入“虛擬節點”后的映射關系
此時,對象到“虛擬節點”的映射關系為:
objec1->cache A2;objec2->cache A1;objec3->cache C1;objec4->cache C2;
因此對象object1和object2都被映射到了cache A上,而object3和object4映射到了cache C上;平衡性有了很大提高。
引入“虛擬節點”后,映射關系就從{對象->節點}轉換到了{對象->虛擬節點}。查詢物體所在cache時的映射關系如圖7所示。
圖7查詢對象所在cache
“虛擬節點”的hash計算可以采用對應節點的IP地址加數字后綴的方式。例如假設cache A的IP地址為202.168.14.241。
引入“虛擬節點”前,計算cache A的hash值:
Hash(“202.168.14.241”);
引入“虛擬節點”后,計算“虛擬節”點cache A1和cache A2的hash值:
Hash(“202.168.14.241#1”);// cache A1
Hash(“202.168.14.241#2”);// cache A2
后記
- 以上部分代碼思路有參考自此博客: http://zhedahht.blog.163.com/blog/ 。特此注明下。
-
上文第五部分來源:
http://blog.csdn.net/sparkliang/article/details/5279393
;
- 行文倉促,若有任何問題或漏洞,歡迎不吝指正或賜教。謝謝。轉載,請注明出處。完。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
