注意點:html
- 通俗的講的時候,就是我的的理解了,僅做參考。
- 做爲一個Java程序員,有必要了解算法,若是有成爲一個優秀程序員的想法,算法和數據結構只是基礎。固然對於非CS專業,計算機網絡,操做系統,編譯原理等也是後面須要補充的基礎知識點。
- 關於閱讀《算法導論》的一些建議:
沒必要糾結於數學的證實,例如遞歸表達式的時間複雜度計算;把一些當前重要的知識點(好比從第一部分到動態規劃、貪心算法,高級數據結構B樹那裏)先看了,完成從0到1的過程,本文也將記錄到那裏。- 好記性不如爛筆頭,左思右想不如畫個圖...人的記憶力是有限的,關於計算機的知識點是不少的,把重要的記錄下來,之後忘了,回過頭再看看,因爲你記錄的是要點,因此沒必要再次翻看一遍全書,便可很快的複習一遍。
- 看書沒必要非得按照順序來看,連JMM都知道,除非兩個步驟都有依賴關係,不然能夠亂序執行,稱爲重排序
- Good luck to everyone who wants to be a not-so-bad programmer.
一般狀況下,當數據量足夠大時,通常知足java
θ(1)>θ(N)>θ(NlogN)>θ(N^2) (>表明優於)程序員
時間複雜度:O(N^2)
思想:每次從右至左跟已排序數列進行對比,放入合適位置。兩次遍歷,一次至關於摸牌,另外一次至關於具體的查找算法。
算法
將問題分解爲幾個規模較小但相似於原問題的子問題,遞歸的求解這些子問題,而後再合併這些子問題的解來創建原問題的解。(分解-解決-合併)數據庫
歸併排序
時間複雜度O(NlogN)(O仍是θ——theta,都差很少)數組
歸併排序算法以下:
緩存
O漸進上界Ω漸進下界,o和ω
具體看數學定義最清楚。
服務器
求解遞歸式的三種方法:網絡
主方法
可求解形如
的函數。數據結構
思路簡單,分紅兩個子序列,最大和要麼全在左側序列,要麼全在右側序列,要麼跨中點;簡單寫法的參考簡單版本,可是有個點須要注意一下,代碼給出的複雜度是O(N)
若是輸入數組中僅有常數個元素須要在排序過程當中存儲在數組以外,則程排序算法是原址的(in place)。例如,排序算法中的swap操做。
順序統計量:一個n個數的集合的第i個順序統計量就是集合中第i小的數。
堆排序的時間複雜度是O(NlgN),具備空間原址性。
二叉堆是一個數組,它能夠被當作是一個近似的徹底二叉樹(按層序排列,各個節點的序號同滿二叉樹相同)
二叉堆能夠分爲兩種形式:最大堆和最小堆(最大仍是最小取決於對應的二叉樹的全部雙親節點是否都大於或小於其孩子節點的值)。堆排序算法中,使用的是最大堆。最小對一般用於構造優先隊列。
維護堆的性質
通俗的講,就是保持數組的最大堆性質。思路比較簡單,比較parent節點和孩子節點,找到最大值,若是最大值是某個孩子節點,交換,遞歸運行。對於樹高爲h的節點來講,該程序時間複雜度爲O(h)
建堆
建堆過程,說白了就是對每個非葉節點進行(1)的操做。複雜度O(n)
堆排序算法
交換A[1]和A[n],此時最大的數已經位於最後一個位置。而後調整剩下的n-1個數據,調用(1)的方法;再交換A[1]和A[n-1],如此下去。複雜度O(lgn)
具體堆排序過程可參考圖解堆排序(不過也注意,其中有一些問題,哪些節點有子節點,從1開始標,應該是(n/2)向下取整)
優先隊列(priority queue)是一種用來維護由一組元素構成的集合S的數據結構,其中的每個元素都有一個相關的值,稱爲關鍵字(key)
最大優先隊列應用:例如在共享計算機系統的做業調度。
最小優先隊列應用:用於基於事件驅動的模擬器。
最壞狀況複雜度θ(n^2),元素互異的狀況下指望時間複雜度θ(NlgN),一般是實際排序應用中最好的選擇。
因爲是in place排序,因此不須要合併操做。
僞代碼:
數組的劃分
數組劃分一點分析:默認以A[r]——最後一個元素做爲比較中間點,for循環遍歷A數組,j指向每一個被遍歷元素下標;i指向小於A[r]的下標。
畫個圖理解一下最清楚了:
算導叫法,A[r]稱爲主元(pivot element)
最壞狀況:劃分產生子問題元素個數爲n-1和0時。此時,T(n)=θ(n^2)。當輸入數組徹底有序時,快排複雜度爲θ(n^2),插排只有O(n)
最好狀況:劃分獲得的兩個子問題的規模都不大於n/2.複雜度θ(nlgn)
這個思路簡單,隨機選取主元,與A[r]交換,再利用上面的算法計算便可。此種版本主要解決,近乎有序的狀況帶來的原有快排最壞狀況的發生。
在排序的最終結果中,各元素的次序依賴於它們之間的比較,這類排序算法稱爲比較排序。
三種線性時間複雜度的排序算法:計數排序、基數排序和桶排序。
比較算法採起決策樹模型:這裏的內部節點用i:j的形式表明A[i]和A[j]進行比較。≤則進左子樹比較,反之右子樹進行比較。葉子節點給出了最後的順序。
支持插入刪除操做的動態集合稱爲字典。
棧(stack),後進先出,LIFO
插入稱爲壓入(push),刪除稱爲彈出(pop)。能夠簡單的理解爲有底無蓋的杯子。
隊列(queue)插入稱爲入隊(enqueue),刪除稱爲出隊(dequeue)
鏈表:
單鏈表、雙向鏈表、循環鏈表,建議看下《大話數據結構》瞭解一下就能夠了。
算導以雙向鏈表爲例講解增刪查,雖然仍是能夠經過畫圖,注意維護涉及節點的prev和next指針指向以及邊界條件,可以明白其操做過程,但仍是記錄下,以備複習之用。
哨兵(sentinel)
哨兵是一個啞對象,其做用是簡化邊界條件的處理。
相似於之前學習時講到的頭節點。加入哨兵將原來的雙向鏈表轉變成一個有哨兵的雙向循環兩表。L.nil表明哨兵。一圖勝千言,以下:
若是有不少個很短的鏈表,慎用哨兵,由於哨兵所佔用的額外存儲空間會形成嚴重的存儲浪費。哨兵並無優化太多的漸進時間界,只是可使代碼更緊湊簡潔。
對象的多數組表示:
next和prev中的數字指的是對應數字的下標,有趣!
單數組表示:
對象的分配和釋放
略
有根樹的表示
分支無限制的有根樹能夠用左孩子右兄弟表示法。
散列表是普通數組概念的推廣。
直接尋址缺點:若是U全域很大,存儲大小爲U.size()的一張表T不太實際,並且對於T來說若是存儲的關鍵字集合K相對於U來講很兇,則T的大部分空間將會浪費掉。
在散列方式(hash)下,關鍵字k被放到槽(slot)h(k)中,及利用散列函數(hash function)h,根據k計算出槽的位置。這裏,函數h將關鍵字的全域U映射到散列表T[0...m-1]的槽位上(|U|>m,正因如此,徹底避免衝突是不可能的)
若是h(k1) = h(k2),則稱衝突(collision)
連接法(chaining)
關於連接法採用雙鏈的一些解釋:來自知乎
簡單講,刪除就是從x對應的鏈表裏刪除x,雙鏈表的刪除加入哨兵只需兩行,可是單鏈表的話只能指定x.next=null,可是在這以前須要先將x.prev.next指向x.next,因爲是單鏈,因此沒有prev,只能一直next找下去,相比雙鏈多了查找的時間耗費。
給定一個能存放n個元素的、具備m個槽位的散列表T,定義T的裝載因子(load factor)α爲n/m,即一個鏈的平均存儲元素數。
除法散列法
經過取k mod m得餘數,將關鍵字k映射到m個slot上的某一個,h(k) = k mod m
一個不太接近2的整數冪的素數,經常是m的一個較好的選擇。
爲了使用開放尋址法插入一個元素,須要連續的檢查散列表,或稱爲探查(probe),直到找到一個空槽來放置待插入的關鍵字爲止。
三種技術計算開放尋址法中的探查序列:線性探查、二次探查和雙重探查。
這個hash函數應該就是hash(k)= k mod m(m表明散列表的長度)
線性探查,通俗的講,過程是這樣的:利用一個hash函數,計算關鍵字key位於的槽位下標hash(key),若是T[hash(key)]已經有值,則探查T[hash(key)+1],T[hash(key) +2]直到最後。相似一人找廁所的過程,到一個廁所(hash(key)位置)跟前,看能不能打開門,打不開就挨着找下一個,直到找到對應的位置。(因爲就只有m個元素須要hash,則每一個都是能找到對應位置的。)
可是,線性探查存在一個問題,稱爲一次羣集(primary clustering)。例如100個槽位,假設後面95位已經被連續佔用,下一次hash出來,若是不幸恰好計算出位置在第6位,則須要連續95次才能找到對應的存儲槽位,剩下的4次一樣也很耗費時間。
二次探查(quadratic probing)
相比於線性探查,在於偏移量採用的是ax^2+bx這種形式
雙重散列(double hashing)
是用於開放尋址法的最好方法之一,由於它所產生的排列具備隨機選擇排列的許多特性。
徹底散列
採用兩級散列。當關鍵字集合是靜態(即關鍵字存到表中關鍵字集合就再也不變化了)的,採用徹底散列,一級散列於帶連接的散列表基本一致,二級散列的長度是存儲的關鍵字個數的平方(主要是爲了確保第二級上不出現衝突)。
關於樹:
徹底k叉樹:全部葉節點深度相同,且全部內部節點度爲k的k叉樹(全部節點有k個叉)
注意:《算導》的徹底二叉樹和滿二叉樹跟《大話數據結構》裏的兩者定義徹底不一樣,具體以哪一個爲準,暫不糾結,哪位朋友知道的,能夠告知一下
二叉搜索樹的性質:x是一個節點,則其左(右)子樹任意節點.key 分別≤(≥)x.key
中序遍歷(inorder tree walk)子樹根的關鍵字位於左右子樹的關鍵字之間。
前序遍歷(preorder tree walk)子樹根的關鍵字位於左右子樹的關鍵字以前。
後序遍歷(postorder tree walk)子樹根的關鍵字位於左右子樹的關鍵字以後。
這裏區分一下二叉搜索樹和最大堆,相同點:比較都是針對全部節點而言,不一樣點:二叉搜索樹,節點左子樹值均小於該節點的值,右子樹值均小於該節點的值;最大堆:節點值大於全部孩子的值。
O(h),h是這棵樹的高度,下面五種操做都是這個複雜度
兩種寫法:遞歸和while
最大值和最小值:分別一直查左子樹(右子樹)便可
後繼(successor)和前驅(predecessor):
後繼:兩種狀況,若是x的右子樹不爲空,則右子樹中的最小值就是x的後繼; 反之,一直找x的雙親節點,直到x是y的左子樹爲止。
插入和刪除會引發由二叉搜索樹表示的動態集合的變化,必定要修改數據結構來反映這個變化,但修改要保持二叉搜索樹性質的成立。
插入:
邊界條件,二叉搜索樹沒有元素;不然找到新插入節點z插入的位置的雙親節點。
刪除:
三種基本策略:
若是z有兩個孩子,那麼找z的後繼y(必定在z的右子樹中),並讓y佔據樹中z的位置。z的原來右子樹部分紅爲y的新的右子樹,而且z的左子樹成爲y的新的左子樹,這種狀況稍顯麻煩,由於還與y是否爲z的右孩子相關。
略
BST(binary search tree)的基本操做大都能在O(h)時間內完成。
對每一個節點,從該節點到其全部後代葉節點的簡單路徑上,均包含相同數目的黑色節點。
使用一個哨兵(sentinel)T.NIL表明全部的NIL
從某個節點x出發(不含該節點)到達一個葉節點的任意一條簡單路徑上的黑色節點個數稱爲該節點的黑高(black-height),記做bh(x)
進行增刪的時候可能會破壞上面提到的5條性質,所以爲了維護這些性質,必須改變某些節點的顏色及指針結構。
指針結構的修改是經過旋轉(rotation)來完成的。
這裏的左旋和右旋彷佛跟《大話數據結構》裏AVL樹的左旋右旋有類似之處。
一圖勝千言:
要理解各個指針的改變,下面這個圖好好看下:
插入耗費時間O(lgN),且該程序選擇不超過兩次
插入一個節點z,並將其着色爲紅色。
插入後的修補工做:
while的結束條件是當z的雙親節點顏色是黑色時
fixup例子:(陰影部分爲黑色)
插入操做只可能破壞紅黑樹性質2和性質4,而且只能破壞其中一條。
修補的三種狀況分析:
case 1:z的叔節點y是紅色的
此時,不須要旋轉,只須要改變顏色,z指向z.p.p便可
z.p.color = y.color = black;
z.p.p.color = red;
case2:z的叔節點y是黑色的且z是一個右孩子
case3:z的叔節點y是黑色的且z是一個左孩子
狀況2經過一次左旋轉成case3。
z.p.color = black;
z.p.p.color=red;
再一次右旋
刪除節點耗費O(lgN)時間。
須要提供一個讓某節點孩子來接替老子位置的一個方法transplant
刪除方法:
1-8行是子承父位,9行是找出z的後繼,10-20行維護了相關的一些指針指向(將y的右孩子移到y的位置,y移到z的位置),21-22行若是z孩子小於2個,z的顏色是黑色(這種狀況很簡單,結合紅黑樹性質5分析)或者z孩子有兩個,z的後繼是黑色,則進行修正(畫圖理解最清楚了,比較簡單就不畫了)。爲何修正呢,由於當是黑色的時候,會破壞紅黑樹性質5,影響黑高。
來看下刪除修復過程:
刪除修復例子:
刪除的四種case:
case1:x的兄弟節點w是紅色的
過程:w描黑,x.p描紅,x.p左旋,維持w兄弟指針指向
case2:x的兄弟節點w是黑色的,w的兩個子節點都是黑色的
w描紅,x指向x.p
case3:x的兄弟節點w是黑色的,w的孩子左紅右黑
w左孩子描黑,w描紅,w右旋,w指向x.p.right,仍舊是維持兄弟指針指向
case4:x的兄弟節點w是黑色的,w的右孩子是紅色的
修改w顏色同x.p顏色一致,x.p描黑,w右孩子描黑,結束循環。
關於刪除的一些深刻理解,參考圖解紅黑樹(別看評論,笑點低的會以爲搞笑的^_^)
瞭解了散列(hash)和紅黑樹,就能夠去愉快的看下Java裏面HashMap的源碼啦。
節點左右子樹高度相差至多爲1.
未深刻講解。
爲磁盤存儲而專門設計的一類平衡搜索樹。B樹相似於紅黑樹,但它們在下降磁盤I/O操做數方面要更好一些,許多數據庫系統使用B樹或者B樹變種來存儲信息。好比MySQL數據庫使用了B+樹的數據結構。
B+相比B樹來講,主要有幾個區別,B+樹葉子節點存儲了全部數據,能夠只通過一次遍歷;葉子節點構成了一個單向鏈表。至於B樹的插入刪除等,參考2-3樹更容易理解一些。
關於B樹和B+樹的區別等能夠參加B樹和B+樹的區別
B-tree,B+tree,想起之前還覺得是一個B+,一個B-呢,哈哈
計算機的主存(primary memory或main memory)一般由硅存儲芯片組成。相比輔存好比磁盤磁帶價格高,容量小,而輔存容量大價格低然而速度也要慢一些。(這方面的知識哪天還得看下計算機組成原理,雖然《計算機科學導論》也講過一些,但總以爲還差點東西)
磁盤慢,主要是由於有機械運動的部分:盤片旋轉和磁臂移動。
本書第三版,2009年出版,這時磁盤旋轉速度是5400~15000轉/分鐘(RPM),一般15000RPM的速度是用於服務器級的驅動器上,7200RPM的速度用於臺式機的驅動器上,5400RPM的速度用於筆記本的驅動器上。隨便在jd上看了機械硬盤和固態硬盤,機械硬盤緩存64MB左右,一款三星SSD緩存在512MB,讀寫在百兆/s。
7200RPM旋轉一圈須要8.33ms,比硅存儲的常見存取時間50ns要高出5個數量級(10的5次方)。也就是說,這個時間內,可能存取主存超過100000次。
爲了癱瘓機械移動所花費的等待時間,磁盤會一次存取多個數據項而不是一個。信息被分爲一系列相等大小的在柱面內連續出現的位頁面(page),而且每一個磁盤讀或寫一個或多個完整的頁面。對於一個典型的磁盤來講,一晚上的長度可能爲2^11~2^14字節。
這裏,對運行時間的兩個主要組成成分分別加以考慮:磁盤存取次數和CPU(計算)時間。
全部葉節點深度相同,即樹高h;每一個節點所包含的關鍵字個數有上界和下界,用一個被稱爲B樹的最小度數的固定證書t≥2來表示這些界:每一個節點除根節點外必須至少有t-1個關鍵字,至多能夠包含2t-1個關鍵字。
B+tree將衛星數據存儲到葉節點上,內部結點只存放關鍵字和孩子指針。對存儲在磁盤上的一顆大的B樹,一般看到分支因子在50~2000之間。
約定:1. B樹的根節點始終在主存中,這樣無需對根作DISK-READ操做;然而,當根節點被改變後,須要對根節點作一次DISK-WRITE操做。2. 任何被當作參數的節點在被傳遞以前,都要對他們先作一次DISK-READ操做。
搜索B樹
先從x節點內部關鍵字查找,找不到,而且x有孩子的話或不是葉節點,再從x.ci孩子節點查找。
分裂B樹中的節點
分裂是樹長高的惟一途徑。
以沿樹單程下行方式向B樹插入關鍵字
2-8行處理x是葉節點的狀況,9-12行找到合適的位置,若是ci子節點已滿,則進行split操做,15-16行肯定應該具體插入那個ci節點,17行遞歸插入。
這裏提到一點,insert-nonfull是尾遞歸的,因此它能夠用一個while循環來實現(這裏也是一個重要的知識點,改天再找資料嘗試一下)
圖解插入:
根節點容許有比最少關鍵字數t-1還少的關鍵字個數。
當要刪除關鍵字的路徑上節點(非根)有最少的關鍵字個數時,也可能須要向上回溯。
刪除實例:
刪除比較複雜一點,case1,case2a,2b,2c,case3a,3b.
遞歸調用自身時,必須保證該節點至少有t個關鍵字
----
利用計算出的信息構造一個最優解
最優子結構(optimal substructure)性質:問題的最優解由相關子問題的最優解組合而成,而這些子問題能夠獨立求解,主要緣由是:反覆求解相同的子問題,同斐波那契數列基本遞歸同樣。
動態規劃方法仔細安排求解順序,對每一個子問題只求解一次,並將結果保存下來。此乃時空權衡(time-memory-trade-off)。
自頂向下遞歸實現:時間複雜度(2^n)
動態規劃有兩種等價的實現方法:
public int getFibonacciWithTailRecursive(int num, int pp, int prev) { if (num == 0) { return pp; } return getFibonacciWithTailRecursive(num - 1, prev, pp + prev); }
public int getFibonacciNum(int num) { int[] tmp = new int[num + 1]; for (int i = 0; i <= num; i++) { switch (i) { case 0: tmp[i] = 0; break; case 1: tmp[i] = 1; break; default: tmp[i] = tmp[i - 1] + tmp[i - 2]; } } return tmp[num]; }
此種寫法有改進空間,因爲計算第n個數只須要前面n-1和n-2的值便可,因此能夠改進爲只用兩個變量存儲,相似於這位朋友所寫點擊查看
試了下a,b這種形式,發現速度更慢了,還不如原始上面這種版本。
關於斐波那契數列的這些寫法,用額外的數組存儲已經計算過的斐波那契數,利用額外的存儲空間換來的是時間上的飛躍,所謂空間換時間。
public int getFibonacciNum2(int num) { int pp = 0, prev= 1; int tm = 0; for (int i = 2; i < num; i++) { tm = prev + pp; pp = prev; prev = tm; } return pp + prev; }
另外寫了個測試類,比較了一下尾遞歸、自底向上和普通遞歸
@Test public void test() { int num = 300; long start2 = System.nanoTime(); int fibonacciNum2 = getFibonacciNum2(num); System.err.println(fibonacciNum2); System.err.println("自底向上花費:" + (System.nanoTime() - start2)); System.err.println("-----------------"); long start3 = System.nanoTime(); int fibonacciNum3 = getFibonacciWithTailRecursive(num, 0, 1); System.err.println(fibonacciNum3); System.err.println("尾遞歸花費:" + (System.nanoTime() - start3)); System.err.println("-----------------"); long start = System.nanoTime(); int fibonacciNum = getFibonacciNum(num); System.err.println(fibonacciNum); System.err.println("普通遞歸:" + (System.nanoTime() - start)); System.err.println("-----------------"); } /** * 自底向上 * @param num * @return */ public int getFibonacciNum2(int num) { int[] tmp = new int[num + 1]; for (int i = 0; i <= num; i++) { switch (i) { case 0: tmp[i] = 0; break; case 1: tmp[i] = 1; break; default: tmp[i] = tmp[i - 1] + tmp[i - 2]; } } return tmp[num]; } /** * 普通遞歸 * @param num * @return */ public int getFibonacciNum(int num) { if (num == 0) return 0; if (num == 1) return 1; return getFibonacciNum(num - 1) + getFibonacciNum(num - 2); } public int getFibonacciWithTailRecursive(int num, int pp, int prev) { if (num == 0) { return pp; } return getFibonacciWithTailRecursive(num - 1, prev, pp + prev); }
言歸正傳,繼續學習。
簡單講,Z={B,C,D,B}是X={A,B,C,B,D,A,B}的子序列,對應的下標序列是{2,3,5,7}
最長公共子序列問題(longest-common-subsequence problem)
求解過程:
首先,解釋一下「前綴」,給定一個序列X=(x1,x2,...,xm)對i=0,1,...m,定義X的第i前綴爲Xi= (x1,x2,...,xi)X0爲空串。
假定每一步都選取最優解,達到最終最優解。
《算法導論》閱讀學習,至此完結。還剩下幾個計數排序,再找時間學習了。