1、第三章簡單回顧 html
中間略過了第三章, 第三章主要是介紹如何從數學層面上科學地定義算法複雜度,以至於可以以一套公有的標準來分析算法。其中,我認爲只要記住三個符號就能夠了,其餘的就看我的狀況,除非你須要對一個算法剖根問底,否則還真用不到,咱們只需有個印象,知道這玩意是用來分析算法性能的。三個量分別是:肯定一個函數漸近上界的Ο符號,漸近下屆Ω符號,以及漸近緊確界Θ符號,這是在分析一個算法的界限時經常使用的分析方法,具體的就詳看書本了,對於咱們更多關注上層算法的表達來講,這些顯得不是那麼重要,個人理解是Ο能夠簡單當作最壞運行時間,Ω是最好運行時間,Θ是平均運行時間。通常咱們在寫一個算法的運行時間時,大可能是以Θ符號來表示。參考下面這幅經典的圖:面試
2、第四章兩大板塊算法
第四章講遞歸,也是數學的東西太多了,我準備這樣來組織這章的結構:先用一個例子(最大子數組和)來說解用到遞歸的一個經典方法——分治法,而後在引入如何解遞歸式,即引入解遞歸式的三種方法。編程
一、由分治法引起的數組
這一章提出了一個在如今各大IT公司在今天依然很喜歡考的一道筆試面試題:網絡
求連續子數組的最大和 題目描述: 輸入一個整形數組,數組裏有正數也有負數。 數組中連續的一個或多個整數組成一個子數組,每一個子數組都有一個和。 求全部子數組的和的最大值。要求時間複雜度爲O(n)。 例如輸入的數組爲1, -2, 3, 10, -4, 7, 2, -5,和最大的子數組爲3, 10, -4, 7, 2, 所以輸出爲該子數組的和18。
要求時間複雜度是O(n),咱們暫且無論這個,由淺入深地分析一下這道題,時間複雜度從O(n^2)->O(nlgn)->O(n)。函數
1)、第一,大部分人想到的確定是暴力法,兩個for循環,時間複雜度天然是O(n^2),以下:工具
1 /************************************************************************/ 2 /* 暴力法 3 /************************************************************************/ 4 void MaxSubArraySum_Force(int arr[], vector<int> &subarr, int len) 5 { 6 if (len == 0) 7 return; 8 int nMax = INT_MIN; 9 int low = 0, high = 0; 10 for (int i = 0; i < len; i ++) { 11 int nSum = 0; 12 for (int j = i; j < len; j ++) { 13 nSum += arr[j]; 14 if (nSum > nMax) { 15 nMax = nSum; 16 low = i; 17 high = j; 18 } 19 } 20 } 21 for (int i = low; i <= high; i ++) { 22 subarr.push_back(arr[i]); 23 } 24 }
2)、第二,看了《算法導論》,你可能會想到分治法,看完以後你確定會爲該分治思想而驚歎,尤爲是「橫跨中點」的計算思想。簡單說下該分治思想,其實很簡單,最大和子數組無非有三種狀況:左邊,右邊,中間。性能
時間複雜度分析:學習
根據分治的思想,時間複雜度的計算包括三部分:兩邊+中間。因爲分治的依託就是遞歸,咱們能夠寫出下面的遞推式(和合並排序的遞推式是同樣的):
其中的Θ(n)爲處理最大和在數組中間時的狀況,通過計算(怎麼計算的,請看本節第二部分:解分治法的三種方法),能夠獲得分治法的時間複雜度爲Θ(nlgn)。代碼以下:
1 /************************************************************************/ 2 /* 分治法 3 最大和子數組有三種狀況: 4 1)A[1...mid] 5 2)A[mid+1...N] 6 3)A[i..mid..j] 7 /************************************************************************/ 8 //find max crossing left and right 9 int Find_Max_Crossing_Subarray(int arr[], int low, int mid, int high) 10 { 11 const int infinite = -9999; 12 int left_sum = infinite; 13 int right_sum = infinite; 14 15 int max_left = -1, max_right = -1; 16 17 int sum = 0; //from mid to left; 18 for (int i = mid; i >= low; i --) { 19 sum += arr[i]; 20 if (sum > left_sum) { 21 left_sum = sum; 22 max_left = i; 23 } 24 } 25 sum = 0; //from mid to right 26 for (int j = mid + 1; j <= high; j ++) { 27 sum += arr[j]; 28 if (sum > right_sum) { 29 right_sum = sum; 30 max_right = j; 31 } 32 } 33 return (left_sum + right_sum); 34 } 35 36 int Find_Maximum_Subarray(int arr[], int low, int high) 37 { 38 if (high == low) //only one element; 39 return arr[low]; 40 else { 41 int mid = (low + high)/2; 42 int leftSum = Find_Maximum_Subarray(arr, low, mid); 43 int rightSum = Find_Maximum_Subarray(arr, mid+1, high); 44 int crossSum = Find_Max_Crossing_Subarray(arr, low, mid, high); 45 46 if (leftSum >= rightSum && leftSum >= crossSum) 47 return leftSum; 48 else if (rightSum >= leftSum && rightSum >= crossSum) 49 return rightSum; 50 else 51 return crossSum; 52 } 53 }
3)、第三,看了《算法導論》習題4.1-5,你又有了另一種思路:數組A[1...j+1]的最大和子數組,有兩種狀況:a) A[1...j]的最大和子數組; b) 某個A[i...j+1]的最大和子數組,假設你如今不知道動態規劃,這種方法也許會讓你眼前一亮,確實是這麼回事,恩,看代碼吧。時間複雜度不用想,確定是O(n)。和暴力法比起來,咱們的改動僅僅是用一個指針指向某個使和小於零的子數組的左區間(當和小於零時,區間向左減少,當和在增長時,區間向右增大)。所以,咱們給這種方法取個名字叫區間法。
1 /************************************************************************/ 2 /* 區間法 3 求A[1...j+1]的最大和子數組,有兩種狀況: 4 1)A[1...j]的最大和子數組 5 2)某個A[i...j+1]的最大和子數組 6 /************************************************************************/ 7 void MaxSubArraySum_Greedy(int arr[], vector<int> &subarr, int len) 8 { 9 if (len == 0) 10 return; 11 int nMax = INT_MIN; 12 int low = 0, high = 0; 13 int cur = 0; //一個指針更新子數組的左區間 14 int nSum = 0; 15 for (int i = 0; i < len; i ++) { 16 nSum += arr[i]; 17 if (nSum > nMax) { 18 nMax = nSum; 19 low = cur; 20 high = i; 21 } 22 if (nSum < 0) { 23 cur += 1; 24 nSum = 0; 25 } 26 } 27 for (int i = low; i <= high; i ++) 28 subarr.push_back(arr[i]); 29 }
第四,你可能在日常的學習過程當中,據說過該問題最經典的解是用動態規劃來解,等你學習以後,你發現確實是這樣,而後你又一次爲之驚歎。動態規劃算法最主要的是尋找遞推關係式,大概思想是這樣的:數組A[1...j+1]的最大和:要麼是A[1...j]+A[j+1]的最大和,要麼是A[j+1],據此,能夠很容易寫出其遞推式爲:
sum[i+1] = Max(sum[i] + A[i+1], A[i+1])
化簡以後,其實就是比較sum[i] ?> 0(sum[i] + A[i+1] ?> A[i+1]),由此,就很容易寫出代碼以下:
1 /************************************************************************/ 2 /* 動態規劃(對應着上面的貪心法看,略有不一樣) 3 求A[1...j+1]的最大和子數組,有兩種狀況: 4 1)A[1...j]+A[j+1]的最大和子數組 5 2)A[j+1] 6 dp遞推式: 7 sum[j+1] = max(sum[j] + A[j+1], A[j+1]) 8 /************************************************************************/ 9 int MaxSubArraySum_dp(int arr[], int len) 10 { 11 if (len <= 0) 12 exit(-1); 13 int nMax = INT_MIN; 14 int sum = 0; 15 16 for (int i = 0; i < len; i ++) { 17 if (sum >= 0) 18 sum += arr[i]; 19 else 20 sum = arr[i]; 21 if (sum > nMax) 22 nMax = sum; 23 } 24 return nMax; 25 }
能夠看出,區間法和動態規劃有幾分類似,我以爲兩種方法的出發點和終點都是一致的,只不過過程不一樣。動態規劃嚴格遵循遞推式,而區間法是尋找使區間變化的標識,即和是否小於零,而這個標識正是動態規劃採用的。
因爲光這一部分就已經寫得足夠長了,爲了方便閱讀,因此本節第二部分:解遞歸式的三種方法 轉 算法導論第四章編程實踐(二)。
個人公衆號 「Linux雲計算網絡」(id: cloud_dev),號內有 10T 書籍和視頻資源,後臺回覆 「1024」 便可領取,分享的內容包括但不限於 Linux、網絡、雲計算虛擬化、容器Docker、OpenStack、Kubernetes、工具、SDN、OVS、DPDK、Go、Python、C/C++編程技術等內容,歡迎你們關注。