零、前言
最大子序列和問題
這個問題是《資料結構和演算法分析》一書中的一個問題,書中給了四種演算法
我感覺它是入手演算法很不錯的一個問題,本文演算法源於書中,但文中包含了我的分析和理解
2.題目的分析
也許很多人看到題目就懵圈了,這裡解釋一下:
拿一個例子來說: -2,11,-4,13,-5,-2
它的子序列是什麼意思: 連續的若干個元素, 比如:11,-4和-4,13,-5,-2等
該問題也就是說:這些子序列元素的和最大是多少,以下列出了所有子序列的情況及子序列和
複製程式碼
一、第一種演算法
1.具體演算法
根據上面分析的圖,很容易可以想到第一種演算法:
private static int maxSonNum(int[] a) {
int maxSum = 0;
for (int i = 0; i < a.length; i++) {
for (int j = i; j < a.length; j++) {
int sum = 0;
for (int k = i; k <= j; k++) {
sum += a[k];
}
if (sum > maxSum) {
maxSum = sum;
}
}
}
return maxSum;
}
複製程式碼
2.演算法分析
可以說下面是我發明的for迴圈的簡單圖示:簡稱
for圖
[1].一條線代表一個for迴圈
[2].線的左邊是for迴圈中索引的變數名
[3].線兩端數字表示迴圈的範圍,空心代表不包含,實心表示包含
[4].箭頭表示某個時刻的遍歷情況
[5].內部迴圈在下邊,可以看成底部箭頭向右走,走到頭上面的箭頭右動一格(類似時鐘,分針,秒針)
複製程式碼
想像一下,看腦中是否可以出現for迴圈中運動的場景:
k 箭頭向右走,計算`i~j`子序列的和,並維護maxSum的值。當走到 j 時,發出"刺啦"一聲
j 箭頭向右移一格,發出一聲"嗒"。然後k箭頭再跑,直到j跑到a.length之後
i 箭頭右移一格, 發出一聲"叮",j 箭頭在k箭頭的推動下一點點跑,直到i跑到了a.length
交響樂大概就是這樣的吧:
...
刺啦,嗒,刺啦,嗒,刺啦,嗒,叮
刺啦,嗒,刺啦,嗒,刺啦,嗒,刺啦,嗒,叮
刺啦,嗒,刺啦,嗒,刺啦,嗒,刺啦,嗒,刺啦,嗒,叮
...
複製程式碼
從上面來看,這個演算法雖然可以解決問題,使用了三層,每層都是N的複雜度
時間複雜度為O(N^3),可想而知,非常耗時
二、第二種演算法
1.具體演算法
private static int maxSonNum(int[] a) {
int maxSum = 0;
for (int i = 0; i < a.length; i++) {
int sum = 0;
for (int j = i; j < a.length; j++) {
sum += a[j];
if (sum > maxSum) {
maxSum = sum;
}
}
}
return maxSum;
}
複製程式碼
2.演算法分析
剛才:在j的迴圈中新開了一個k迴圈計算i~j的元素和
如 i=0,j=4 時:子序列 -2,11,-4,13,-5 用一個k迴圈就算他們的和
這裡:在j的迴圈中維護sum變數也能達到一樣的效果:
如 i=0,j=4 時:sum= sum + -5 即 -2+11-4+13-5,然後維護maxSum
這樣就減少一層for迴圈:時間複雜度為O(N^2)
複製程式碼
三、第三種演算法
1.具體演算法
分治的思想,也是本文最想講的內容
private static int maxSumRec(int[] a, int start, int end) {
if (start == end) {
return a[start];//一個元素的最大子序列是其本身
}
int center = (start + end) / 2;
int maxLeftSum = maxSumRec(a, start, center);//1.左半最大子序列和
int maxRightSum = maxSumRec(a, center + 1, end);//2.右半最大子序列和
/*
3.如果最大子序列和貫穿左右時:
|--- 1.子序列是連續的
|--- 2.中點和中點的後面元素在最大子序列中
*/
//尋找左半中含左半一個元素的最大子序列和
int maxLeftSBorderSum = 0, leftBorderSum = 0;
for (int i = center; i >= start; i--) {
leftBorderSum += a[i];
if (leftBorderSum > maxLeftSBorderSum) {
maxLeftSBorderSum = leftBorderSum;
}
}
//判斷右半中含右半第一個元素的最大子序列和
int maxRightSBorderSum = 0, rightBorderSum = 0;
for (int i = center + 1; i <= end; i++) {
rightBorderSum += a[i];
if (rightBorderSum > maxRightSBorderSum) {
maxRightSBorderSum = rightBorderSum;
}
}
return max3(maxLeftSum, maxRightSum, maxLeftSBorderSum + maxRightSBorderSum);
}
private static int max3(int a, int b, int c) {
int max;
if (a > b && a > c) {
max = a;
} else if (c > a && c > b) {
max = c;
} else {
max = b;
}
return max;
}
複製程式碼
2.演算法分析
將一個大問題拆解成若干個小問題,使用遞迴來解決 雖然演算法複雜了很多,但執行的時間複雜度降到了O(NlongN),還是很有價值的
最大子序列和可能存在於:
1.左半的序列:maxLeftSum
2.由半的序列:maxRightSum
3.該序列貫穿左右
|--- 判斷左半(子序列含:左半最後一個元素):maxLeftSBorderSum
|--- 判斷右半(子序列含:右半第一個元素):maxRightSBorderSum
尋找子序列即(含左半最後一個元素),又含(右半第一個元素),說明兩半序列可連續
int maxLeftSBorderSum = 0, leftBorderSum = 0;
for (int i = center; i >= start; i--) {//遍歷包含center點的最側子序列取最大和
leftBorderSum += a[i];
if (leftBorderSum > maxLeftSBorderSum) {
maxLeftSBorderSum = leftBorderSum;
}
}
int maxRightSBorderSum = 0, rightBorderSum = 0;
for (int i = center + 1; i <= end; i++) {//遍歷包含center+1點的右側子序列取最大和
rightBorderSum += a[i];
if (rightBorderSum > maxRightSBorderSum) {
maxRightSBorderSum = rightBorderSum;
}
}
maxLeftSBorderSum和maxRightSBorderSum由於包含center點和center+1點
所以是貫穿左右的子序列,並且其和是[貫穿左右的子序列]中最大的
複製程式碼
具體來分析一下問題的分解
Q1: 求 -2 11 -4 13 -5 -2 的最大子序列和
Q1可以分解為下面三個問題的最大值:
|---Q1.1: -2 11 -4 的最大子序列和
|---Q1.2: 13 -5 -2 的最大子序列和
|---Q1.3: 序列和最大值貫穿左右時的最大值:
|--- 判斷左半:序列含左半最後一個元素的子序列最大值
|--- 判斷右半:序列含右半第一個元素子序列最大值
Q1.1可以分解為下面三個問題的最大值:
|---Q1.1.1: -2 11 的最大子序列和
|---Q1.1.2: -4 的最大子序列和 -4
|---Q1.1.3: 序列和最大值貫穿左右時的最大值:
|--- 判斷左半:序列含左半最後一個元素的子序列最大值 11
|--- 判斷右半:序列含右半第一個元素子序列最大值 -4
Q1.1.1可以分解為下面三個問題的最大值:
|---Q1.1.1.1: -2 的最大子序列和 -2
|---Q1.1.1.2: 11 的最大子序列和 11
|---Q1.1.1.3: 序列和最大值貫穿左右時的最大值:
|--- 判斷左半:序列含左半最後一個元素的子序列最大值 -2
|--- 判斷右半:序列含右半第一個元素子序列最大值 11
所以 Q1.1.1 = 11 Q1.1.2 = -4 Q1.1.3 = 7
所以 Q1.1 = 11 同理分解 Q1.2 = 13 Q1.3 = 7 + 13 =20
所以 Q1 = 20 得解
複製程式碼
四、 第四種演算法
1.具體演算法
private static int maxSonNum4(int[] a) {
int maxSum = 0, sum = 0;
for (int i = 0; i < a.length; i++) {
sum += a[i];
if (sum > maxSum) {
maxSum = sum;
} else if (sum < 0) {
sum = 0;
}
}
return maxSum;
}
複製程式碼
2.分析
反正我是很難想像第一個寫出這個演算法的人腦子是怎麼想的,也很難去說明這個演算法的正確性
這在O(N)的時間完成了最大子序列和問題,這種"簡潔和聰明以及高效"也許就是演算法的迷人之處。
-2 11 -4 13 -5 -2
sum maxSum sum
i = 0 -2 0 0
i = 1 11 11 11
i = 2 7 11 7
i = 3 20 20 20
i = 4 15 20 15
i = 5 13 20 13
複製程式碼
一種演算法可以從O(N^3)優化到O(N^2),再用分治優化到O(NlogN)
最後被一個O(N)的演算法亮瞎的的鈦合金言,所以這個問題真的挺有意思。