數據結構與算法之美專欄學習體會

10 遞歸:如何用三行代碼找到「最終推薦人」

  遞歸的含義:一種很是簡潔、高效的編碼技巧,方法/函數調用自身的方式稱之爲遞歸,調用爲「遞」,返回爲「歸」。算法

  全部的遞歸問題均可以用遞推公式來表達。數據庫

  優勢:代碼表達能力強,編碼簡潔。編程

  缺點:(1)空間複雜度高,存在棧溢出風險(策略:能夠設置遞歸次數強行終止條件);(2)存在重複計算,針對這一點能夠(策略:能夠額外增長哈希表來快速查找結果而減小重複計算);(3)過多函數調用耗時較長。數組

  針對上述缺陷,任何一個遞歸問題(籠統來說)均可以轉化爲非遞歸實現方式。方法:抽象出遞推公式、初始值、邊界條件,而後用循環來實現。緩存

11 排序(上):爲何插入排序比冒泡排序更受歡迎?

  有序度:在一個數列中,符合順序排列的數據對個數。(滿有序度:所有數據均有序的數據對個數)安全

  逆序度:在一個數列中,不符合順序排列的數據對個數。服務器

  有序度 + 逆序度 = 滿有序度。  負載均衡

  冒泡排序和插入排序都是穩定算法(選擇排序是不穩定算法),不論怎麼優化,二者的元素移動的次數都等於數列的逆序度。可是,冒泡排序的賦值操做是3個,而插入排序是1個。因此理論上來說,冒泡排序時間複雜度是3K,而插入排序是1K,若是但願性能更好確定要首選插入而不是冒泡。基礎的插入排序還有很大優化空間,進一步的改進可參考:希爾排序。編程語言

  這三種基礎排序算法實際的用途不大,但插入排序的思想在某些編程語言的實現原理中仍是有可能用到。分佈式

12 排序(下):如何用快排思想在O(n)內查找第K大元素?

  歸併排序:將數列分紅先後兩部分分別進行排序,而後將兩個有序數列合併起來。(優勢:時間複雜度低且穩定排序;缺陷:因爲合併函數沒法原地執行,因此是非原地排序算法,空間複雜度較高O(n)不如快排)

   快速排序:與歸併排序看起來有點相似,都是藉助「分治」思想。(優勢:時間複雜度低,空間複雜度僅O(n);缺點:不穩定排序)

  快速排序的時間複雜度依賴於Partition選擇的合理性,在極端狀況下分區不合理會致使時間複雜度退化爲O(n*n)。

  選擇第K大的思路,每一次分區後,看看分區較小的大小是否符合K-1?若是是那麼Guard元素就是了,若是不是,那麼繼續在分區內查找便可。

13 線性排序:如何根據年齡給100萬用戶數據排序?

  (1)桶排序

  將n個數均勻分到m個桶內,每一個桶內有k=n/m個元素,桶內用快速排序O(k*logk),整個桶排序的時間複雜度就是O(m*k*logk)=O(n*logk),當m接近n時,該時間複雜度接近O(n)。

  看似很棒,實際有不少限制約束條件:<1>須要數列可劃分且桶之間自然有序;<2>各桶內數據分佈均勻;<3>數據範圍不能太大,不然桶太多了。

  適用場景:外部排序(內存較小,將數據劃分到外部大空間「桶」)。

  (2)計數排序

  能夠理解爲桶排序的一種特殊狀況:按照n個數據的數據範圍[0, k] 劃分k 個桶,省去桶內排序時間。其餘步驟跟桶排序同樣。

  最巧妙的關鍵點:對數列A進行統計獲得C,C中每個位置表示該數值的不大於(<=)的元素個數,經過還原掃描C中每個位置的數據(每一次計數減一)來獲得排序結果R。

  約束條件與桶排序一致,數據量不能太大。

  (3)基數排序

  對於數據位數一致(例如手機號碼)的排序,可能數據量很是大到不能用桶排序和計數排序。這裏有一個很巧妙的排序能夠接近達到O(n):對每一位從低到高依次進行穩定O(n)排序。例如:

  若是數據位數不等,能夠經過補齊空位的方式來對齊排序。每一位的排序能夠用上面的桶排序或者計數排序來逼近O(n),而後每一位順序操做下來就是k*O(n)。總之,當位數k不太大時,時間複雜度就是O(k*n),接近O(n)。

  綜上,基數排序約束條件爲:<1>數據要可以劃分出獨立的「位」;<2>位之間有遞進關係,例如高位對比以後低位能夠不用比較了;<3>每一位數據範圍不能太大,不然不能使用O(n)的桶排序/計數排序。總時間複雜度沒法達到O(n)。

14 排序優化:如何實現一個通用的、高性能的排序函數?

  優化要點:

  (1)讓快速排序儘量接近O(n*logn) 的時間複雜度而不是O(n*n):分區點的選擇最重要,能夠用「三數取中法」、「隨機法」來優化;

  (2)對於數據量不大的狀況,能夠選擇O(n)算法來排序,消耗O(n)的空間來換取時間是個很好的優化點;

  (3)通常數據量採用快速排序O(n);

  (4)對於快速排序中區間小於4個數的狀況,退化採用插入排序,雖然是O(n*n)複雜度但實際操做更快。從理論上來解釋就是O(n*logn)實際上剪去了一些常量係數,若是加上常量係數後則實際比O(n*n)更慢;

  (5)利用哨兵技巧減小一次對比判斷,實際效果很好;

15 二分查找(上):如何用最省內存的方式實現快速查找功能?

  二分查找時間複雜度O(logn),但有約束條件:(1)連續內存空間支持隨機訪問;(2)數據有序;(3)不適合處理小規模數據,順利遍歷就OK了,也不適合太大規模數據由於連續內存申請不到;(4)不適合處理動態變化的數據,由於頻繁插入刪除影響效率;

  二分查找算法實現的注意點:(1)(high + low) / 2 有可能由於序號太大而加法溢出,應改爲 low + (high - low) >> 1,這裏 >> 操做效率比 / 更高。(2)下標移動的時候注意 low = mid + 1,high = mid - 1;

16 二分查找(下):如何快速定位IP對應的省份地址?

   二分查找算法的難點在於如何編寫正確又能處理變種狀況的代碼,例如:

  (1)查找第一個值等於給定值的元素

  一個精簡的「漂亮」的寫法:

 1 int bsearch(int a[], int n, int val) {
 2     int low = 0;
 3     int high = n - 1;
 4     while (low <= high) {
 5         int mid = low + ((high - low) >> 1);
 6         if (a[mid] >= val) {
 7             high = mid - 1;
 8         }
 9         else {
10             low = mid + 1;
11         }
12     }
13     if (a[low] == val) {
14         return low;
15     }
16     else {
17         return -1;
18     }
19 }
View Code

  一個更容易理解的寫法:

 1 int bsearch(int a[], int n, int val) {
 2     int low = 0;
 3     int high = n - 1;
 4     while (low <= high) {
 5         int mid = low + ((high - low) >> 1);
 6         if (a[mid] > val) {
 7             high = mid - 1;
 8         }
 9         else if (a[mid] < val) {
10             low = mid + 1;
11         }
12         else {
13             if ((mid == 0) || (a[mid - 1] != val)) {
14                 return mid;
15             }
16             else {
17                 high = mid - 1;
18             }
19         }
20     }
21     return -1;
22 }
View Code

  能夠看到第二種寫法只是更直觀一點,但整體思路都是:不斷縮短二分查找的範圍。讓mid==val的時候不是二分折半逼近,而是採起單步逼近,最後選擇範圍內知足數值相等的最低位便可

  (2)查找最後一個值等於給定值的元素(在上面「更容易理解的寫法」代碼中稍稍變動便可)

 1 int bsearch(int a[], int n, int val) {
 2     int low = 0;
 3     int high = n - 1;
 4     while (low <= high) {
 5         int mid = low + ((high - low) >> 1);
 6         if (a[mid] > val) {
 7             high = mid - 1;
 8         }
 9         else if (a[mid] < val) {
10             low = mid + 1;
11         }
12         else {
13             if ((mid == n - 1) || (a[mid + 1] != val)) {
14                 return mid;     // 邊界檢查
15             }
16             else {
17                 low = mid + 1;  // 縮減範圍
18             }
19         }
20     }
21     return -1;
22 }
View Code

  (3)找到第一個大於等於給定值的元素(想一想怎麼縮減二分查找的範圍邊界便可)

 1 int bsearch(int a[], int n, int val) {
 2     int low = 0;
 3     int high = n - 1;
 4     while (low <= high) {
 5         int mid = low + ((high - low) >> 1);
 6         if (a[mid] >= val) {
 7             if ((mid == 0) || (a[mid - 1] < val)) {
 8                 return mid;
 9             }
10             else {
11                 high = mid - 1;
12             }
13         }
14         else {
15             low = mid + 1;
16         }
17     }
18     return -1;
19 }
View Code

  (4)找到最後一個小於等於給定值的元素(同上,想一想怎麼縮減二分查找的範圍邊界便可)

 1 int bsearch(int a[], int n, int val) {
 2     int low = 0;
 3     int high = n - 1;
 4     while (low <= high) {
 5         int mid = low + ((high - low) >> 1);
 6         if (a[mid] <= val) {
 7             if ((mid == n - 1) || (a[mid + 1] > val)) {
 8                 return mid;
 9             }
10             else {
11                 low = mid + 1;
12             }
13         }
14         else {
15             high = mid - 1;
16         }
17     }
18     return -1;
19 }
View Code

   我以爲最後的思考題更值得思考研究:若是是一個循環有序數組(如:4,5,6,1,2,3)那麼如何實現上述的二分查找?網友給出來三種思路,第三種很棒:

  (1)找到分界點(時間複雜度O(n)),判斷目標元素在哪一個區間,而後在區間內進行普通二分查找;

  (2)找到分界點(時間複雜度O(n))下標x,全部元素下標+x偏移量,超出數組範圍則取模。對找到的元素點再作下標-x處理。

  (3)將數組mid折半劃分爲一個有序部分和循環有序部分(時間複雜度O(logN)):若是a[0] < a[mid] 則證實前半部分有序然後半部分爲循環有序(反之亦然),若是目標元素在有序範圍內則普通二分查找;反之則對循環有序部分繼續作上一步查找。

17 跳錶:爲何Redis必定要用跳錶來實現有序集合?

   跳錶基本結構以下(對原始鏈表創建索引層,加快查找速度):

  這裏以每N=2個元素創建索引爲例(若是N>2也是同樣的):

  (1)查找效率 O(logN):索引層高度logN,每一層訪問不超過3個,所以是3 * logN

  (2)空間複雜度O(N):索引空間和爲 N/2 + N/4 + ... + 2 = N-2

  (3)插入時間複雜度O(logN):查找的時間複雜度就是上面的O(logN),實際插入操做時間複雜度O(1)

  (4)跳錶索引更新:採起「隨機函數」的方式來維護平衡性。若是隨機函數返回值K,就表示從1~K級插入索引數據。這個函數很講究,要保證性能不至於過分退化。

  爲何Redis採用跳錶而不是紅黑樹呢?可能有如下緣由:(1)按照區間來查找數據,跳錶能夠在O(logN)找到起點再順序遍歷,比紅黑樹快;(2)跳錶比紅黑樹更易實現,可讀性好不易出錯;(3)跳錶更靈活,能夠經過改變索引構建策略有效地平衡效率和內存消耗。

  跳錶相比紅黑樹的不足:(1)紅黑樹出現的更早,有現成庫支持。跳錶只能本身動手實現;

18 散列表(上):Word文檔中的單詞拼寫檢查功能是如何實現的?

   散列表來源於數據,利用散列函數對數據進行改造,利用的是數組支持隨機訪問的特色。核心兩個問題:(1)散列函數設計;(2)衝突解決方法 --> 經常使用兩種:開放尋址法、鏈表法。散列函數是好壞決定了散列表的衝突機率(即性能)。

19 散列表(中):如何打造一個工業級水平的散列表?

  「散列表碰撞攻擊」:攻擊者利用精心構造的數據所有散列到同一個槽內,讓查找效率從O(1)退化爲O(n),達到DDOS效果。

  如何設計好的散列函數?(1)不能態複雜,不然計算量大會下降效率;(2)裝載因子不能太大。

  裝載因子太大怎麼辦?擴容下降因子。一次性擴容會形成瞬間耗時巨大,能夠改爲分批次擴容,將擴容動做分攤到每個插入動做中去。查找的時候能夠先查新表,再查舊錶。

  衝突解決辦法的優缺點對比:(1)開放尋址法:易序列化,且內存集中可藉助CPU緩存加速。但衝突代價太大,耗內存(適用於小數據低裝載因子的場景);(2)鏈表法:內存利用率高,當存儲大數據時指針的消耗可忽略,內存不連續對CPU緩存不友好(適用於大數據量大對象場景)。鏈表法能夠進一步優化成「跳錶」、「紅黑樹」達到O(logN)的效果,有效抵禦「散列表碰撞攻擊」。

20 散列表(下):爲何散列表和鏈表常常會一塊兒使用?

  原來的LRU淘汰算法用鏈表實現,時間複雜度是O(n),若是加上哈希表就會好不少,雖然操做看起來複雜了一點,可是時間複雜度能夠是O(1)。散列表負責快速定位,而後增長一個雙向鏈表能夠快速連接到隊列頭(尾),實現淘汰。

  另外,若是將雙向鏈表換成跳錶,就具備了跳錶的能力了,能夠快速定位區間數據。

21 哈希算法(上):如何防止數據庫中的用戶信息被脫庫?

  哈希算法四個應用場景:(1)安全加密,如MD五、SHA、DES、AES;(2)惟一標識,做爲ID快速定位再仔細對比確認;(3)數據校驗;(4)散列函數。

22 哈希算法(下):哈希算法在分佈式系統中有哪些應用?

  哈希算法在分佈式系統中應用場景:(5)負載均衡,利用哈希算法把訪問者IP+ID散列到固定值%服務器個數,就能夠算出目標服務器而不須要映射表了;(6)數據分片,將同類數據計算哈希並分配到指定機器。藉助分片的思路,能夠突破單機內存、CPU等資源限制;(7)分佈式存儲,如何解決分佈式系統擴容、縮容致使大量數據搬遷的問題?此時要用到「一致性哈希」:將原有的分隔區間進行進一步細分,把新增節點插入進來轉移部分子區間數據便可,不影響原有散列位置。

23 二叉樹基礎(上):什麼樣的二叉樹適合用數組來存儲?

  樹的基本概念,滿二叉樹是所有節點都是二叉的,主要是記住「徹底二叉樹」的概念就行。爲什麼單獨把葉子節點所有靠左的稱之爲「徹底二叉樹」呢?又爲什麼把這個概念單獨拿出來說呢?看看按數組的二叉樹存儲方式就知道了——空間利用率很高!(堆其實就是徹底二叉樹,最經常使用的是數組存儲)

  思考題有點難度:(1)怎麼實現按層遍歷?藉助棧或者層數控制?(2)給定一個數列怎麼算出能夠構建多少種樹?

24 二叉樹基礎(下):有了如此高效的散列表,爲何還須要二叉樹?

  二叉排序樹的查找、插入、刪除操做,其中刪除略複雜一點點,注意刪除包含左右子節點的方式(將右子數最小節點替換刪除節點)就好了。更取巧的作法是:僅標記節點DEL而並不真正操做節點刪除。若是是支持重複節點的二叉排序樹呢,插入的方案能夠是單節點存儲多個,也能夠採用一種巧妙方案(將重複節點繼續插入右子樹),查找和刪除重複節點的二叉樹就須要一直找到葉子節點爲止。

  爲了有了O(n)的散列表方案還須要搞一個二叉排序樹呢?緣由有:(1)散列表不利於輸出有序數據,而二叉排序樹採用中序遍歷就能O(n)輸出有序數列;(2)散列表擴容耗時長且衝突時性能不穩定,而採用「平衡二叉排序樹」很是穩定在O(logN);(3)哈希衝突的查找時間與哈希函數計算的時間累加,可能並不必定比平衡二叉排序樹的效率高;(4)散列表的構造比二叉查找樹複雜,要考慮效率、衝突、擴容等諸多問題,平衡二叉查找樹只須要考慮平衡一個問題。

25 紅黑樹(上):爲何工程中都用紅黑樹這種二叉樹?

  平衡二叉查找樹中「平衡」的定義是任意節點的左右子樹高度差不大於1。但究其本質「平衡」的目的是但願二叉排序樹不要由於頻繁動態更新而致使性能退化,實際上不必定要嚴格遵循高度差不大於1的定義要求。例如紅黑樹,就是一種非嚴格的平衡二叉排序樹屬於「近似平衡」。

  爲何工程中都喜歡紅黑樹這種非嚴格的平衡二叉排序樹呢?由於Treap、Splay Tree儘管絕大多數狀況下效率很高,但沒法避免極端狀況下時間退化,所以對於單次操做比較敏感的場景並不適用;AVL樹是一種高度平衡的二叉查找樹,朝趙很高效,可是維護成本很是高昂,不適用於頻繁插入刪除操做的場合。因此,爲了支撐複雜的工業應用場景,更傾向於使用性能穩定的近似平衡的二叉查找樹(紅黑樹)。

26 紅黑樹(下):掌握這些技巧,你也能夠實現一個紅黑樹 

  插入刪除的操做會對樹的平衡產生影響,先來複習下基礎概念「左旋」「右旋」。

  紅黑色的調整步驟很是複雜,下面分別來區分CASE對照說明便可:

26.1 紅黑樹的節點插入

  全部調整的方法就是兩種基本操做「左右旋轉」和「改變顏色」。兩種特殊狀況:(1)插入節點的父節點是黑色的,那麼什麼都不用作;(2)插入節點自身就是根節點,那麼把顏色變成黑色便可。

  其餘狀況,劃分爲三種固定CASE來處理便可:

26.1 紅黑樹的節點刪除

  紅黑樹的刪除操做比插入要難不少。分爲兩步走:(1)第一步對刪除節點初步調整,目標是確保知足最後一條定義約束:即整個樹在刪除節點後,每一個節點到達葉子節點的全部路徑都包含相同數量的黑色節點;(2)針對關注節點作二次調整,目標是確保知足第三條定義約束:即不存在相鄰紅色節點。

相關文章
相關標籤/搜索