1. 程式人生 > >資料結構演算法常見面試考題

資料結構演算法常見面試考題

(1) 紅黑樹的瞭解(平衡樹,二叉搜尋樹),使用場景

把資料結構上幾種樹集中的討論一下:

1.AVLtree

定義:最先發明的自平衡二叉查詢樹。在AVL樹中任何節點的兩個子樹的高度最大差別為一,所以它也被稱為高度平衡樹。查詢、插入和刪除在平均和最壞情況下都是O(log n)。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。

節點的平衡因子是它的左子樹的高度減去它的右子樹的高度(有時相反)。帶有平衡因子1、0或 -1的節點被認為是平衡的。帶有平衡因子 -2或2的節點被認為是不平衡的,並需要重新平衡這個樹。平衡因子可以直接儲存在每個節點中,或從可能儲存在節點中的子樹高度計算出來。
一般我們所看見的都是排序平衡二叉樹。

AVLtree使用場景:AVL樹適合用於插入刪除次數比較少,但查詢多的情況。插入刪除導致很多的旋轉,旋轉是非常耗時的。AVL 在linux核心的vm area中使用。

2.二叉搜尋樹

二叉搜尋樹也是一種樹,適用與一般二叉樹的全部操作,但二叉搜尋樹能夠實現資料的快速查詢。

二叉搜尋樹滿足的條件:

1.非空左子樹的所有鍵值小於其根節點的鍵值
2.非空右子樹的所有鍵值大於其根節點的鍵值
3.左右子樹都是二叉搜尋樹

二叉搜尋樹的應用場景:如果是沒有退化稱為連結串列的二叉樹,查詢效率就是lgn,效率不錯,但是一旦退換稱為連結串列了,要麼使用平衡二叉樹,或者之後的RB樹,因為連結串列就是線性的查詢效率。

3.紅黑樹的定義

紅黑樹是一種二叉查詢樹,但在每個結點上增加了一個儲存位表示結點的顏色,可以是RED或者BLACK。通過對任何一條從根到葉子的路徑上各個著色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出兩倍,因而是接近平衡的。

當二叉查詢樹的高度較低時,這些操作執行的比較快,但是當樹的高度較高時,這些操作的效能可能不比用連結串列好。紅黑樹(red-black tree)是一種平衡的二叉查詢樹,它能保證在最壞情況下,基本的動態操作集合執行時間為O(lgn)。

紅黑樹必須要滿足的五條性質:

性質一:節點是紅色或者是黑色; 在樹裡面的節點不是紅色的就是黑色的,沒有其他顏色,要不怎麼叫紅黑樹呢,是吧。

性質二:根節點是黑色; 根節點總是黑色的。它不能為紅。

性質三:每個葉節點(NIL或空節點)是黑色;

性質四:每個紅色節點的兩個子節點都是黑色的(也就是說不存在兩個連續的紅色節點); 就是連續的兩個節點不能是連續的紅色,連續的兩個節點的意思就是父節點與子節點不能是連續的紅色。

性質五:從任一節點到其每個葉節點的所有路徑都包含相同數目的黑色節點。從根節點到每一個NIL節點的路徑中,都包含了相同數量的黑色節點。

紅黑樹的應用場景:紅黑樹是一種不是非常嚴格的平衡二叉樹,沒有AVLtree那麼嚴格的平衡要求,所以它的平均查詢,增添刪除效率都還不錯。廣泛用在C++的STL中。如map和set都是用紅黑樹實現的。

4.B樹定義

B樹和平衡二叉樹稍有不同的是B樹屬於多叉樹又名平衡多路查詢樹(查詢路徑不只兩個),不屬於二叉搜尋樹的範疇,因為它不止兩路,存在多路。

B樹滿足的條件:

(1)樹種的每個節點最多擁有m個子節點且m>=2,空樹除外(注:m階代表一個樹節點最多有多少個查詢路徑,m階=m路,當m=2則是2叉樹,m=3則是3叉);
(2)除根節點外每個節點的關鍵字數量大於等於ceil(m/2)-1個小於等於m-1個,非根節點關鍵字數必須>=2;(注:ceil()是個朝正無窮方向取整的函式 如ceil(1.1)結果為2)
(3)所有葉子節點均在同一層、葉子節點除了包含了關鍵字和關鍵字記錄的指標外也有指向其子節點的指標只不過其指標地址都為null對應下圖最後一層節點的空格子
(4)如果一個非葉節點有N個子節點,則該節點的關鍵字數等於N-1;
(5)所有節點關鍵字是按遞增次序排列,並遵循左小右大原則;

B樹的應用場景:構造一個多階的B類樹,然後在儘量多的在結點上儲存相關的資訊,保證層數儘量的少,以便後面我們可以更快的找到資訊,磁碟的I/O操作也少一些,而且B類樹是平衡樹,每個結點到葉子結點的高度都是相同,這也保證了每個查詢是穩定的。

5.B+樹

B+樹是B樹的一個升級版,B+樹是B樹的變種樹,有n棵子樹的節點中含有n個關鍵字,每個關鍵字不儲存資料,只用來索引,資料都儲存在葉子節點。是為檔案系統而生的。

相對於B樹來說B+樹更充分的利用了節點的空間,讓查詢速度更加穩定,其速度完全接近於二分法查詢。為什麼說B+樹查詢的效率要比B樹更高、更穩定;我們先看看兩者的區別

(1)B+跟B樹不同,B+樹的非葉子節點不儲存關鍵字記錄的指標,這樣使得B+樹每個節點所能儲存的關鍵字大大增加;
(2)B+樹葉子節點儲存了父節點的所有關鍵字和關鍵字記錄的指標,每個葉子節點的關鍵字從小到大連結;
(3)B+樹的根節點關鍵字數量和其子節點個數相等;
(4)B+的非葉子節點只進行資料索引,不會存實際的關鍵字記錄的指標,所有資料地址必須要到葉子節點才能獲取到,所以每次資料查詢的次數都一樣;

特點:
在B樹的基礎上每個節點儲存的關鍵字數更多,樹的層級更少所以查詢資料更快,所有指關鍵字指標都存在葉子節點,所以每次查詢的次數都相同所以查詢速度更穩定;

應用場景: 用在磁碟檔案組織 資料索引和資料庫索引。

6.Trie樹(字典樹)

trie,又稱字首樹,是一種有序樹,用於儲存關聯陣列,其中的鍵通常是字串。與二叉查詢樹不同,鍵不是直接儲存在節點中,而是由節點在樹中的位置決定。一個節點的所有子孫都有相同的字首,也就是這個節點對應的字串,而根節點對應空字串。一般情況下,不是所有的節點都有對應的值,只有葉子節點和部分內部節點所對應的鍵才有相關的值。

在圖示中,鍵標註在節點中,值標註在節點之下。每一個完整的英文單詞對應一個特定的整數。Trie 可以看作是一個確定有限狀態自動機,儘管邊上的符號一般是隱含在分支的順序中的。
鍵不需要被顯式地儲存在節點中。圖示中標註出完整的單詞,只是為了演示 trie 的原理。

trie樹的優點:利用字串的公共字首來節約儲存空間,最大限度地減少無謂的字串比較,查詢效率比雜湊表高。缺點:Trie樹是一種比較簡單的資料結構.理解起來比較簡單,正所謂簡單的東西也得付出代價.故Trie樹也有它的缺點,Trie樹的記憶體消耗非常大.

其基本性質可以歸納為:

  1. 根節點不包含字元,除根節點外每一個節點都只包含一個字元。
  2. 從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串。
  3. 每個節點的所有子節點包含的字元都不相同。

典型應用是用於統計,排序和儲存大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。字典樹與字典很相似,當你要查一個單詞是不是在字典樹中,首先看單詞的第一個字母是不是在字典的第一層,如果不在,說明字典樹裡沒有該單詞,如果在就在該字母的孩子節點裡找是不是有單詞的第二個字母,沒有說明沒有該單詞,有的話用同樣的方法繼續查詢.字典樹不僅可以用來儲存字母,也可以儲存數字等其它資料。

(2) 紅黑樹在STL上的應用

STL中set、multiset、map、multimap底層是紅黑樹實現的,而unordered_map、unordered_set 底層是雜湊表實現的。

multiset、multimap: 插入相同的key的時候,我們將後插入的key放在相等的key的右邊,之後不管怎麼進行插入或刪除操作,後加入的key始終被認為比之前的大。

(3) 瞭解並查集嗎?(低頻)

什麼是合併查詢問題呢?

顧名思義,就是既有合併又有查詢操作的問題。舉個例子,有一群人,他們之間有若干好友關係。如果兩個人有直接或者間接好友關係,那麼我們就說他們在同一個朋友圈中,這裡解釋下,如果Alice是Bob好友的好友,或者好友的好友的好友等等,即通過若干好友可以認識,那麼我們說Alice和Bob是間接好友。隨著時間的變化,這群人中有可能會有新的朋友關係,這時候我們會對當中某些人是否在同一朋友圈進行詢問。這就是一個典型的合併-查詢操作問題,既包含了合併操作,又包含了查詢操作。

並查集,在一些有N個元素的集合應用問題中,我們通常是在開始時讓每個元素構成一個單元素的集合,然後按一定順序將屬於同一組的元素所在的集合合併,其間要反覆查詢一個元素在哪個集合中。

並查集是一種樹型的資料結構,用於處理一些不相交集合(Disjoint Sets)的合併及查詢問題。

並查集也是使用樹形結構實現。不過,不是二叉樹。每個元素對應一個節點,每個組對應一棵樹。在並查集中,哪個節點是哪個節點的父親以及樹的形狀等資訊無需多加關注,整體組成一個樹形結構才是重要的。類似森林

(4) 貪心演算法和動態規劃的區別

貪心演算法:區域性最優,劃分的每個子問題都最優,得到全域性最優,但是不能保證是全域性最優解,所以對於貪心演算法來說,解是從上到下的,一步一步最優,直到最後。

動態規劃:將問題分解成重複的子問題,每次都尋找左右子問題解中最優的解,一步步得到全域性的最優解.重複的子問題可以通過記錄的方式,避免多次計算。所以對於動態規劃來說,解是從小到上,從底層所有可能性中找到最優解,再一步步向上。

分治法:和動態規劃類似,將大問題分解成小問題,但是這些小問題是獨立的,沒有重複的問題。獨立問題取得解,再合併成大問題的解。

例子:比如錢幣分為1元3元4元,要拿6元錢,貪心的話,先拿4,再拿兩個1,一共3張錢;實際最優卻是兩張3元就夠了。

(5) 判斷一個連結串列是否有環,如何找到這個環的起點

給定一個單鏈表,只給出頭指標h:
1、如何判斷是否存在環?
2、如何知道環的長度?
3、如何找出環的連線點在哪裡?
4、帶環連結串列的長度是多少?

解法:
1、對於問題1,使用追趕的方法,設定兩個指標slow、fast,從頭指標開始,每次分別前進1步、2步。如存在環,則兩者相遇;如不存在環,fast遇到NULL退出。
2、對於問題2,記錄下問題1的碰撞點p,slow、fast從該點開始,再次碰撞所走過的運算元就是環的長度s。
3、問題3:有定理:碰撞點p到連線點的距離=頭指標到連線點的距離,因此,分別從碰撞點、頭指標開始走,相遇的那個點就是連線點。(證明在後面附註)
4、問題3中已經求出連線點距離頭指標的長度,加上問題2中求出的環的長度,二者之和就是帶環單鏈表的長度
http://blog.sina.com.cn/s/blog_725dd1010100tqwp.html

(6) 實現一個strcpy函式(或者memcpy),如果記憶體可能重疊呢

——大家一般認為名不見經傳strcpy函式實現不是很難,流行的strcpy函式寫法是:

  1. char *my_strcpy(char *dst,const char *src)  
  2. {  
  3.     assert(dst != NULL);  
  4.     assert(src != NULL);  
  5.     char *ret = dst;  
  6.     while((* dst++ = * src++) != '\0')   
  7.         ;  
  8.     return ret;  
  9. }  

如果注意到:
1,檢查指標有效性;
2,返回目的指標des;
3,源字串的末尾 ‘\0’ 需要拷貝。

記憶體重疊

記憶體重疊:拷貝的目的地址在源地址範圍內。所謂記憶體重疊就是拷貝的目的地址和源地址有重疊。
在函式strcpy和函式memcpy都沒有對記憶體重疊做處理的,使用這兩個函式的時候只有程式設計師自己保證源地址和目標地址不重疊,或者使用memmove函式進行記憶體拷貝。
memmove函式對記憶體重疊做了處理。
strcpy的正確實現應為:

  1. char *my_strcpy(char *dst,const char *src)  
  2. {  
  3.     assert(dst != NULL);  
  4.     assert(src != NULL);  
  5.     char *ret = dst;  
  6.     memmove(dst,src,strlen(src)+1);  
  7.     return ret;  
  8. }  

memmove函式實現時考慮到了記憶體重疊的情況,可以完成指定大小的記憶體拷貝

(7) 快排存在的問題,如何優化

快排的時間複雜度

時間複雜度最快平均是O(nlogn),最慢的時候是O(n2);輔助空間也是O(logn);最開始學快排時最疑惑的就是這個東西不知道怎麼得來的,一種是通過數學運算可以的出來,還有一種是通過遞迴樹來理解就容易多了

這張圖片別人部落格那裡弄過來的,所謂時間複雜度最理想的就是取到中位數情況,那麼遞迴樹就是一個完全二叉樹,那麼樹的深度也就是最低為Logn,這個時候每一次又需要n次比較,所以時間複雜度nlogn,當快排為順序或者逆序時,這個數為一個斜二叉樹,深度為n,同樣每次需要n次比較,那那麼最壞需要n2的時間

優化:

1.當整個序列有序時退出演算法;
2.當序列長度很小時(根據經驗是大概小於 8),應該使用常數更小的演算法,比如插入排序等;
3.隨機選取分割位置;
4.當分割位置不理想時,考慮是否重新選取分割位置;
5.分割成兩個序列時,只對其中一個遞迴進去,另一個序列仍可以在這一函式內繼續劃分,可以顯著減小棧的大小(尾遞迴):
6.將單向掃描改成雙向掃描,可以減少劃分過程中的交換次數

優化1:當待排序序列的長度分割到一定大小後,使用插入排序
原因:對於很小和部分有序的陣列,快排不如插排好。當待排序序列的長度分割到一定大小後,繼續分割的效率比插入排序要差,此時可以使用插排而不是快排

優化2:在一次分割結束後,可以把與Key相等的元素聚在一起,繼續下次分割時,不用再對與key相等元素分割

優化3:優化遞迴操作
快排函式在函式尾部有兩次遞迴操作,我們可以對其使用尾遞迴優化

優點:如果待排序的序列劃分極端不平衡,遞迴的深度將趨近於n,而棧的大小是很有限的,每次遞迴呼叫都會耗費一定的棧空間,函式的引數越多,每次遞迴耗費的空間也越多。優化後,可以縮減堆疊深度,由原來的O(n)縮減為O(logn),將會提高效能。

(8) Top K問題(可以採取的方法有哪些,各自優點?)

1.將輸入內容(假設用陣列存放)進行完全排序,從中選出排在前K的元素即為所求。有了這個思路,我們可以選擇相應的排序演算法進行處理,目前來看快速排序,堆排序和歸併排序都能達到O(nlogn)的時間複雜度。

2.對輸入內容進行部分排序,即只對前K大的元素進行排序(這K個元素即為所求)。此時我們可以選擇氣泡排序或選擇排序進行處理,即每次冒泡(選擇)都能找到所求的一個元素。這類策略的時間複雜度是O(Kn)。

3.對輸入內容不進行排序,顯而易見,這種策略將會有更好的效能開銷。我們此時可以選擇兩種策略進行處理:

用一個桶來裝前k個數,桶裡面可以按照最小堆來維護
a)利用最小堆維護一個大小為K的陣列,目前該小根堆中的元素是排名前K的數,其中根是最小的數。此後,每次從原陣列中取一個元素與根進行比較,如大於根的元素,則將根元素替換並進行堆調整(下沉),即保證小根堆中的元素仍然是排名前K的數,且根元素仍然最小;否則不予處理,取下一個陣列元素繼續該過程。該演算法的時間複雜度是O(nlogK),一般來說企業中都採用該策略處理top-K問題,因為該演算法不需要一次將原陣列中的內容全部載入到記憶體中,而這正是海量資料處理必然會面臨的一個關卡。

b)利用快速排序的分劃函式找到分劃位置K,則其前面的內容即為所求。該演算法是一種非常有效的處理方式,時間複雜度是O(n)(證明可以參考演算法導論書籍)。對於能一次載入到記憶體中的陣列,該策略非常優秀。

(9) Bitmap的使用,儲存和插入方法

BitMap從字面的意思

很多人認為是點陣圖,其實準確的來說,翻譯成基於位的對映。
在所有具有效能優化的資料結構中,大家使用最多的就是hash表,是的,在具有定位查詢上具有O(1)的常量時間,多麼的簡潔優美。但是資料量大了,記憶體就不夠了。
當然也可以使用類似外排序來解決問題的,由於要走IO所以時間上又不行。
所謂的Bit-map就是用一個bit位來標記某個元素對應的Value, 而Key即是該元素。由於採用了Bit為單位來儲存資料,因此在儲存空間方面,可以大大節省。
其實如果你知道計數排序的話(演算法導論中有一節講過),你就會發現這個和計數排序很像。

bitmap應用

   1)可進行資料的快速查詢,判重,刪除,一般來說資料範圍是int的10倍以下。
   2)去重資料而達到壓縮資料

還可以用於爬蟲系統中url去重、解決全組合問題。

BitMap應用:排序示例

假設我們要對0-7內的5個元素(4,7,2,5,3)排序(這裡假設這些元素沒有重複)。那麼我們就可以採用Bit-map的方法來達到排序的目的。要表示8個數,我們就只需要8個Bit(1Bytes),首先我們開闢1Byte的空間,將這些空間的所有Bit位都置為0(如下圖:)

然後遍歷這5個元素,首先第一個元素是4,那麼就把4對應的位置為1(可以這樣操作 p+(i/8)|(0×01<<(i%8)) 當然了這裡的操作涉及到Big-ending和Little-ending的情況,這裡預設為Big-ending。不過計算機一般是小端儲存的,如intel。小端的話就是將倒數第5位置1),因為是從零開始的,所以要把第五位置為一(如下圖):

然後再處理第二個元素7,將第八位置為1,,接著再處理第三個元素,一直到最後處理完所有的元素,將相應的位置為1,這時候的記憶體的Bit位的狀態如下:

然後我們現在遍歷一遍Bit區域,將該位是一的位的編號輸出(2,3,4,5,7),這樣就達到了排序的目的。

bitmap排序複雜度分析

Bitmap排序需要的時間複雜度和空間複雜度依賴於資料中最大的數字。
bitmap排序的時間複雜度不是O(N)的,而是取決於待排序陣列中的最大值MAX,在實際應用上關係也不大,比如我開10個執行緒去讀byte陣列,那麼複雜度為:O(Max/10)。也就是要是讀取的,可以用多執行緒的方式去讀取。時間複雜度方面也是O(Max/n),其中Max為byte[]陣列的大小,n為執行緒大小。
空間複雜度應該就是O(Max/8)bytes吧

BitMap演算法流程

假設需要排序或者查詢的最大數MAX=10000000(lz:這裡MAX應該是最大的數而不是int資料的總數!),那麼我們需要申請記憶體空間的大小為int a[1 + MAX/32]。
其中:a[0]在記憶體中佔32為可以對應十進位制數0-31,依次類推:
bitmap表為:
a[0]--------->0-31
a[1]--------->32-63
a[2]--------->64-95
a[3]--------->96-127

我們要把一個整數N對映到Bit-Map中去,首先要確定把這個N Mapping到哪一個陣列元素中去,即確定對映元素的index。我們用int型別的陣列作為map的元素,這樣我們就知道了一個元素能夠表示的數字個數(這裡是32)。於是N/32就可以知道我們需要對映的key了。所以餘下來的那個N%32就是要對映到的位數。

1.求十進位制數對應在陣列a中的下標:

先由十進位制數n轉換為與32的餘可轉化為對應在陣列a中的下標。
如十進位制數0-31,都應該對應在a[0]中,比如n=24,那麼 n/32=0,則24對應在陣列a中的下標為0。又比如n=60,那麼n/32=1,則60對應在陣列a中的下標為1,同理可以計算0-N在陣列a中的下標。
i = N>>K % 結果就是N/(2^K)
Note: map的範圍是[0, 原陣列最大的數對應的2的整次方數-1]。

2.求十進位制數對應陣列元素a[i]在0-31中的位m:

十進位制數0-31就對應0-31,而32-63則對應也是0-31,即給定一個數n可以通過模32求得對應0-31中的數。
m = n & ((1 << K) - 1) %結果就是n%(2^K)

3.利用移位0-31使得對應第m個bit位為1

如a[i]的第m位置1:a[i] = a[i] | (1<<m)
如:將當前4對應的bit位置1的話,只需要1左移4位與B[0] | 即可。

Note:

1 p+(i/8)|(0×01<<(i%8))這樣也可以?
2 同理將int型變數a的第k位清0,即a=a&~(1<<k)

BitMap演算法評價

優點:
1. 運算效率高,不進行比較和移位;
2. 佔用記憶體少,比如最大的數MAX=10000000;只需佔用記憶體為MAX/8=1250000Byte=1.25M。
3.
缺點:
1. 所有的資料不能重複,即不可對重複的資料進行排序。(少量重複資料查詢還是可以的,用2-bitmap)。
2. 當資料類似(1,1000,10萬)只有3個數據的時候,用bitmap時間複雜度和空間複雜度相當大,只有當資料比較密集時才有優勢。
http://blog.csdn.net/pipisorry/article/details/62443757

(10) 字典樹的理解以及在統計上的應用

Trie的核心思想是空間換時間。利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。Trie樹也有它的缺點,Trie樹的記憶體消耗非常大.當然,或許用左兒子右兄弟的方法建樹的話,可能會好點.

就是在海量資料中找出某一個數,比如2億QQ號中查找出某一個特定的QQ號。。

(11) N個骰子出現和為m的概率

典型的可以用動態規劃的思想來完成

1.現在變數有:骰子個數,點數和。當有k個骰子,點數和為n時,出現次數記為f(k,n)。那與k-1個骰子階段之間的關係是怎樣的?

2.當我有k-1個骰子時,再增加一個骰子,這個骰子的點數只可能為1、2、3、4、5或6。那k個骰子得到點數和為n的情況有:
(k-1,n-1):第k個骰子投了點數1
(k-1,n-2):第k個骰子投了點數2
(k-1,n-3):第k個骰子投了點數3

(k-1,n-6):第k個骰子投了點數6
在k-1個骰子的基礎上,再增加一個骰子出現點數和為n的結果只有這6種情況!
所以:f(k,n)=f(k-1,n-1)+f(k-1,n-2)+f(k-1,n-3)+f(k-1,n-4)+f(k-1,n-5)+f(k-1,n-6)

3.有1個骰子,f(1,1)=f(1,2)=f(1,3)=f(1,4)=f(1,5)=f(1,6)=1。
用遞迴就可以解決這個問題:

用迭代來完成

(19) 海量資料問題(可參考左神的書)

目前關於海量資料想到的解決辦法:
1.bitmap
2.桶排序,外部排序,將需要排序的放到外存上,不用全部放到記憶體上

(20) 一致性雜湊

說明

http://www.zsythink.net/archives/1182

優點

1.當後端是快取伺服器時,經常使用一致性雜湊演算法來進行負載均衡。使用一致性雜湊的好處在於,增減叢集的快取伺服器時,只有少量的快取會失效,回源量較小。
2.儘量減少資料丟失問題,減少移動資料的風險