之因此想寫這一系列文章,主要是由於最近有在看算法導論。爲何要看這本書,雖然有些人會認爲對前端來講,數據結構和算法可能並非那麼重要。其實否則,這一塊的知識其實應該算是重中之重,只有真正理解這一塊知識,才能提高咱們的核心競爭力。不少時候,咱們與別人的差距可能就差在這些通用知識上,而不只僅是某一塊具體的知識領域。前端
根據我目前的閱讀狀況來講,對其中的內容其實也都處於一種只知其一;不知其二的狀態吧,因此纔打算經過寫文章的方式來進行鞏固和梳理,也算是一種複習吧。固然這本書我也還沒看完,純粹就是一邊看一邊寫,因此更新進度徹底隨緣。算法
在開始以前,先來了解如下幾個函數增加的漸進記號。漸近記號值得是對於給定的函數g(n),用漸近記號來表示如下函數的集合:數組
Θ(g(n))={ f(n):存在正常量c1、c2和n0,使的對全部n >= n0,有0 <= c1g(n) <= f(n) <= c2g(n) }數據結構
Ο(g(n))={ f(n):存在正常量c和n0,使的對全部n >= n0,有0 <= f(n) <= cg(n) }cors
o(g(n))={ f(n):對任意正常量c > 0,存在常量n0 > 0,使的對全部n >= n0,有0 <= f(n) < cg(n) }數據結構和算法
Ω(g(n))={ f(n):存在正常量c和n0,使的對全部n >= n0,有0 <= cg(n) <= f(n) }函數
ω(g(n))={ f(n):對任意正常量c > 0,存在常量n0 > 0,使的對全部n >= n0,有0 <= cg(n) < f(n) }優化
更直觀地,能夠經過圖來查看 ui
咱們能夠經過下面的表格來加深理解spa
記號 | 含義 | 簡單理解 |
---|---|---|
Θ | 漸近緊確 | 至關於"=" |
Ο | 漸近緊確上界 | 至關於"<=" |
o | 非漸近緊確上界 | 至關於"<" |
Ω | 漸近緊確下界 | 至關於">=" |
ω | 非漸近緊確下界 | 至關於">" |
在平時,咱們更多地探討的實際上是O,由於在大多數算法的複雜度中,咱們只須要關心其上界,而不是其下界,而對於Θ而言,咱們須要同時證實其上界和下界。(PS:這句話的意思,指的並非算法的最好狀況和最差狀況,而是對於某一具體狀況下,其時間複雜度T(n)是一個多項式,而咱們只關心多項式的上界,例如:在平均狀況下,某算法的時間複雜度計算出來爲T(n)=c1n2+c2n+c0,則其複雜度爲O(n2),固然也能夠用Θ(n2)表示其複雜度)
分治法即將原問題分解爲幾個規模較小,但相似於原問題的子問題,遞歸地求解這些子問題,而後再合併這些子問題的解來創建原問題的解。通常的,其有如下三個步驟:
比較典型的的例子有歸併排序和快速排序,關於這兩個排序,下面將會說起。這裏,我先從另外一個經典的例子開始:給定一個數組A=[13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7],求解其最大子數組Max。(最大子數組Max定義:Max爲A的子數組,而且對於任意A的子數組a來講,都有sum(Max) >= sum(a)。數組A一定是同時包含正負值的,由於若是全是正值或全是負值,此問題將毫無心義。映射到現世生活中的一個典型的例子就是給定某一個時間段內股票的漲跌,計算在該時間段內如何買入賣出才能得到最大收益。)
假定咱們須要尋找a[low...high]的最大子數組,使用分治法就要求咱們將其分解爲兩個規模儘可能相等的子數組,假設mid爲其中央位置。那麼其最大子數組Max必然屬於如下狀況之一:
那麼對於狀況1和2而言,其實仍是求解最大子數組,只是規模更小而已,因此遞歸這一過程便可。那麼對於狀況3來講,相對左部來講,只需從mid開始,逐級遞減至low,求出最大值;右部則相反。而後將左右部的結果合併便可。
js代碼:
const arr = [13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7];
// 跨越左右數組
function findCrossMaxSubArr(arr, low, mid ,high) {
let leftMax = rightMax = sum = leftIndex = rightIndex = 0;
// 求左部最大
for(let i = mid; i >= low; i--) {
sum += arr[i];
if(sum >= leftMax) {
leftMax = sum;
leftIndex = i;
}
}
// 求右部最大
sum = 0;
for(let i = mid + 1; i <= high; i++) {
sum += arr[i];
if(sum >= rightMax) {
rightMax = sum;
rightIndex = i;
}
}
return {
sum: leftMax + rightMax,
leftIndex,
rightIndex
};
}
function findMaxSubArr(arr, low, high) {
if(low === high) return {
sum: arr[low],
leftIndex: low,
rightIndex: high
};
const mid = Math.floor((low + high) / 2);
// 不跨越左右數組時,直接遞歸便可
const leftMax = findMaxSubArr(arr, low, mid);
const rightMax = findMaxSubArr(arr, mid + 1, high);
// 跨越左右數組時,求此狀況下的最大值
const corssMax = findCrossMaxSubArr(arr, low, mid, high);
// 找出三種狀況下的最大值
let max = leftMax;
if(rightMax.sum > max.sum) max = rightMax;
if(corssMax.sum > max.sum) max = corssMax;
return max;
}
// 返回
// {
// leftIndex: 7,
// rightIndex: 10,
// sum: 43
// }
console.log(findMaxSubArr(arr, 0, 15))
複製代碼
接下來,就是對複雜度進行分析。可是由於完整的過程比較複雜,因此這裏就不進行分析,有興趣瞭解的能夠去查閱一下完整的推導過程。如下爲我我的的簡單理解:首先,由於二分策略,因此二分的層級爲log2n,而後比較的操做次數爲cn次,因此大體估算其複雜度爲O(nlogn)。
單單就這一問題而言,大多數人可能想到的第一種方法就是兩層循環暴力求解,可是這樣複雜度就是O(n2)。而上面所講到的分治法的複雜度只需O(nlogn),因而可知分治法的優點。可是,除了分治法之外,還能夠經過動態規劃解決這一問題,其時間複雜度僅需O(n),其對應的狀態轉移方程爲:dp[i] = Max(dp[i-1] + A[i], A[i]),dp[i]表示以arr[i]爲結尾的最大連續子數組之和,只需求dp[i]的最大值便可。這一部分打算到後面動態規劃的章節再詳細講解,這裏先貼一下代碼。
// 如需獲取下標等信息,只需在此基礎上擴展
function findMaxSubArr(arr) {
const dp = [];
for(let i = 0; i < arr.length; i++) {
if(i === 0) dp[i] = arr[i];
else dp[i] = Math.max(dp[i - 1] + arr[i], arr[i]);
}
return Math.max(...dp);
}
複製代碼
此算法時間複雜度爲O(n),空間複雜度也爲O(n)
此外,還有另外一種聯機算法也能處理該問題。我的感受應該算是上面算法的優化了空間複雜度的版本。其時間複雜度爲O(n),空間複雜度爲O(1)
聯機算法是在任意時刻算法對要操做的數據只讀入(掃描)一次,一旦被讀入並處理,它就不須要在被記憶了。而在此處理過程當中算法能對它已經讀入的數據當即給出相應子序列問題的正確答案。
function findMaxSubArr(arr) {
// curSum表示到當前i爲止的累加和
let maxSum = curSum = 0;
for(let i = 0; i < arr.length; i++) {
curSum += arr[i];
if(curSum > maxSum) maxSum = curSum;
// 若是累加和出現小於0的狀況,
// 則最大和子序列確定不可能包含前面的元素,
// 這時將累加和置0,從下個元素從新開始累加
else if(curSum < 0) curSum = 0;
}
return maxSum;
}
複製代碼
此章內容先到這裏爲止,以爲有用的話麻煩點個贊呦,謝謝。下一節內容預告:比較排序算法及其下界。