題目:在數組中,數字減去它右邊的數字得到一個數對之差。求所有數對之差的最大值。例如在數組{2, 4, 1, 16, 7, 5, 11, 9}中,數對之差的最大值是11,是16減去5的結果。
分析:看到這個題目,很多人的第一反應是找到這個數組的最大值和最小值,然後覺得最大值減去最小值就是最終的結果。這種思路忽略了題目中很重要的一點:數對之差是一個數字減去它右邊的數字。由於我們無法保證最大值一定位於數組的左邊,因此這個思路不管用。
於是我們接下來可以想到讓每一個數字逐個減去它右邊的所有數字,並通過比較得到數對之差的最大值。由於每個數字需要和它後面的O(n)個數字作減法,因此總的時間複雜度是O(n2)。
解法一:分治法
通常蠻力法不會是最好的解法,我們想辦法減少減法的次數。假設我們把數組分成兩個子數組,我們其實沒有必要拿左邊的子數組中較小的數字去和右邊的子數組中較大的數字作減法。我們可以想象,數對之差的最大值只有可能是下面三種情況之一:(1)被減數和減數都在第一個子數組中,即第一個子數組中的數對之差的最大值;(2)被減數和減數都在第二個子數組中,即第二個子數組中數對之差的最大值;(3)被減數在第一個子數組中,是第一個子數組的最大值。減數在第二個子數組中,是第二個子數組的最小值。這三個差值的最大者就是整個數組中數對之差的最大值。
在前面提到的三種情況中,得到第一個子數組的最大值和第二子數組的最小值不是一件難事,但如何得到兩個子數組中的數對之差的最大值。這其實是原始問題的子問題,我們可以遞迴地解決。下面是這種思路的參考代碼:
int MaxDiff_Solution1(int numbers[], unsigned length){ if(numbers == NULL || length < 2) return 0; int max, min; return MaxDiffCore(numbers, numbers + length - 1, &max, &min);} int MaxDiffCore(int* start, int* end, int* max, int* min){ if(end == start) { *max = *min = *start; return 0x80000000; } int* middle = start + (end - start) / 2; int maxLeft, minLeft; int leftDiff = MaxDiffCore(start, middle, &maxLeft, &minLeft); int maxRight, minRight; int rightDiff = MaxDiffCore(middle + 1, end, &maxRight, &minRight); int crossDiff = maxLeft - minRight; *max = (maxLeft > maxRight) ? maxLeft : maxRight; *min = (minLeft < minRight) ? minLeft : minRight; int maxDiff = (leftDiff > rightDiff) ? leftDiff : rightDiff; maxDiff = (maxDiff > crossDiff) ? maxDiff : crossDiff; return maxDiff;}
在函數MaxDiffCore中,我們先得到第一個子數組中的最大的數對之差leftDiff,再得到第二個子數組中的最大數對之差rightDiff。接下來用第一個子數組的最大值減去第二個子數組的最小值得到crossDiff。這三者的最大值就是整個數組的最大數對之差。
解法二:轉化成求解子數組的最大和問題
接下來再介紹一種比較巧妙的解法。如果輸入一個長度為n的數組numbers,我們先構建一個長度為n-1的輔助數組diff,並且diff[i]等於numbers[i]-numbers[i+1](0<=i<n-1)。如果我們從數組diff中的第i個數字一直累加到第j個數字(j > i),也就是diff[i] + diff[i+1] + … + diff[j] = (numbers[i]-numbers[i+1]) + (numbers[i + 1]-numbers[i+2]) + ... + (numbers[j] – numbers[j + 1]) = numbers[i] – numbers[j + 1]。
分析到這裡,我們發現原始數組中最大的數對之差(即numbers[i] – numbers[j + 1])其實是輔助數組diff中最大的連續子數組之和。我們在本系列的部落格的第3篇《求子數組的最大和》中已經詳細討論過這個問題的解決方案。基於這個思路,我們可以寫出如下代碼:
int MaxDiff_Solution2(int numbers[], unsigned length){ if(numbers == NULL || length < 2) return 0; int* diff = new int[length - 1]; for(int i = 1; i < length; ++i) diff[i - 1] = numbers[i - 1] - numbers[i]; int currentSum = 0; int greatestSum = 0x80000000; for(int i = 0; i < length - 1; ++i) { if(currentSum <= 0) currentSum = diff[i]; else currentSum += diff[i]; if(currentSum > greatestSum) greatestSum = currentSum; } delete[] diff; return greatestSum;}
解法三:動態規劃法
既然我們可以把求最大的數對之差轉換成求子數組的最大和,而子數組的最大和可以通過動態規劃求解,那我們是不是可以通過動態規劃直接求解呢。下面我們試著用動態規劃法直接求數對之差的最大值。
我們定義diff[i]是以數組中第i個數字為減數的所有數對之差的最大值。也就是說對於任意h(h < i),diff[i]≥number[h]-number[i]。diff[i](0≤i<n)的最大值就是整個數組最大的數對之差。
假設我們已經求得了diff[i],我們該怎麼求得diff[i+1]呢。對於diff[i],肯定存在一個h(h < i),滿足number[h]減去number[i]之差是最大的,也就是number[h]應該是number[i]之前的所有數位最大值。當我們求diff[i+1]的時候,我們需要找到第i+1個數字之前的最大值。第i+1個數字之前的最大值有兩種可能:這個最大值可能是第i個數字之前的最大值,也有可能這個最大值就是第i個數字。第i+1個數字之前的最大值肯定是這兩者的較大者。我們只要拿第i+1個數字之前的最大值減去number[i+1],就得到了diff[i+1]。
int MaxDiff_Solution3(int numbers[], unsigned length){ if(numbers == NULL || length < 2) return 0; int max = numbers[0]; int maxDiff = max - numbers[1]; for(int i = 2; i < length; ++i) { if(numbers[i - 1] > max) max = numbers[i - 1]; int currentDiff = max - numbers[i]; if(currentDiff > maxDiff) maxDiff = currentDiff; } return maxDiff;}
在上述代碼中,max表示第i個數字之前的最大值,而currentDiff表示diff[i] (0≤i<n),diff[i]的最大值就是代碼中maxDiff。
解法小結
上述三種代碼,雖然思路各不相同,但時間複雜度都是O(n)(第一種解法的時間複雜度可以用遞迴公式表示為T(n)=2(n/2)+O(1),所以總體時間複雜度是O(n))。我們也可以注意到第一種方法是基於遞迴實現,而遞迴調用是有額外的時間、空間消耗的(比如在調用棧上分配空間儲存參數、臨時變數等)。第二種方法需要一個長度為n-1的輔助數組,因此其空間複雜度是O(n)。第三種方法則沒有額外的時間、空間開銷,並且它的代碼是最簡潔的,因此這是最值得推薦的一種解法。
博主何海濤對本部落格文章享有著作權。網路轉載請註明出處 http://zhedahht.blog.163.com/。整理出版物請和作者聯絡。對解題思路有任何建議,歡迎在評論中告知,或者加我微博 http://weibo.com/zhedahht或者 http://t.163.com/zhedahht與我討論。謝謝。