先來詳細描述下這道題。在一個全爲正整數的數組中找到總和爲給定值的子數組,給出子數組的起始下標(閉區間),舉個例子:html
在[3 2 1 2 3 4 5]這個數組中,和爲10的子數組是[1 2 3 4],因此答案應該是[2,5]。
和爲15的子數組是[1 2 3 4 5],答案爲[2,6]。
這是一道很是有意思的題,爲何這麼說?最簡單的解法只要具有基本的編程知識就能寫出,更優的解法須要你有數據結構和算法能力,越高效的解法越巧妙,可能你一會兒沒法想出全部的解法,但我相信你看完這篇博客必定會感嘆算法的神奇。 java
回到這道題上來,實際上它有着O(n^3)
、O(n^2)
、O(nlogn)
、O(n)
4種時間複雜度的解法,若是算上空間複雜度的差別的話總共5種解法,我以爲仍是比較能考察到一我的的算法水平的。接下來讓我帶領你們由簡入難看下從青銅到王者的5種解法,帶你們吊打面試官。 面試
這裏咱們設輸入參數爲(arr[],target),後續代碼中我會用s和e來分別表示起始和終止位置。另外爲了簡化代碼思路,咱們假設給定的參數裏最多隻有一個解(實際上多個解也不難,但會讓代碼變長,不利於描述思路,多解的狀況就留給你當課後做業了)。算法
首先固然是最簡單的暴力求解了,遍歷起始位置s和結束位置e,而後求s和e之間全部數字的和。三層循環簡單粗暴,不須要任何的技巧,相信你大一剛學會編程就能解出來。編程
public int[] find(int[] arr, int target) { for (int s = 0; s < arr.length; s++) { for (int e = s+1; e < arr.length; e++) { int sum = 0; for (int k = s; k <= e; k++) { // 求s到e之間的和 sum += arr[k]; } if (target == sum) { return new int[]{s, e}; } } } return null; }
咱們來分析下時間複雜度,很明顯是O(n^3),當n超過1000時就會出現肉眼可見的慢,想一想如何優化?數組
上面代碼中,咱們每次都須要算從s到e之間的數組的和sum[s,e],假設我以前已經求過了[1,10]之間的和sum[1,10],如今要求[2,10]之間的和sum[2,10],顯然這中間有很大一部分是重疊的(sum[2,10]),能不能把這部分重複掃描給消除掉?這裏就須要作下巧妙的變換了。
數據結構
實際上sum[s, e] = sum[0, e] - sum[0, s-1]
, sum[0,i]咱們能夠預先保存下來,而後重複使用。實際上sum數組咱們能夠經過一遍數據預處理獲取到。上圖中,arr藍色區域的和正好等於sum數組中紅色減去綠色,即sum(arr[3]-arr[7]) = sum[7]-sum[2]
。數據結構和算法
回到代碼上來,編碼實現中我用了額外一個數組arrSum來存儲0到i(0<=i<n)之間全部的和,爲了處理方便sumArr下標從1開始,sumArr[i]表示遠數組中sum[0, i-1]。有了sumArr以後,sum[s,e]就能夠經過sumArr[e+1]-sumArr[s]間接獲取到。完整代碼以下:優化
public int[] find(int[] arr, int target) { int[] sumArr = new int[arr.length + 1]; for (int i = 1; i < sumArr.length; i++) { sumArr[i] = sumArr[i-1] + arr[i-1]; // 預處理,獲取累計數組 } for (int s = 0; s < arr.length; s++) { for (int e = s+1; e < arr.length; e++) { if (target == sumArr[e+1] - sumArr[s]) { return new int[]{s, e}; } } } return null; }
經過上述用空間換時間的方式,咱們能夠直接將時間複雜度從O(n^3)
下降到O(n^2)
。編碼
細心的你可能已經發現了,由於給出的arr都是正整數,因此sumArr必定是遞增且有序的,對於有序的數組,咱們能夠直接採用二分查找。對於這道題而已,咱們能夠遍歷起點s,然在sumArr中二分去查找是否有終點e,若是s對於的e存在,那麼sumArr[e]必定等於sumArr[s] + target,改造後的代碼以下,相比於上面代碼,增長了二分查找。
public int[] find(int[] arr, int target) { int[] sumArr = new int[arr.length + 1]; for (int i = 1; i < sumArr.length; i++) { sumArr[i] = sumArr[i-1] + arr[i-1]; } for (int s = 0; s < arr.length; s++) { int e = bSearch(sumArr, sumArr[s] + target); if (e != -1) { return new int[]{s, e}; } } return null; } // 二分查找 int bSearch(int[] arr, int target) { int l = 1, r = arr.length-1; while (l < r) { int mid = (l + r) >> 1; if (arr[mid] >= target) { r = mid; } else { l = mid + 1; } } if (arr[l] != target) { return -1; } return l - 1; }
由此,咱們又繼續將時間複雜從O(n^2)下降到了O(nlogn)。
有序數組的查找除了能夠用二分優化,還能夠用hashMap來優化,藉助HashMap O(1)的查詢時間複雜度。咱們又一次用空間來換取了時間。
public int[] find(int[] arr, int target) { int[] sumArr = new int[arr.length + 1]; Map<Integer, Integer> map = new HashMap<>(); for (int i = 1; i < sumArr.length; i++) { sumArr[i] = sumArr[i-1] + arr[i-1]; map.put(sumArr[i], i-1); } for (int s = 0; s < arr.length; s++) { int e = map.getOrDefault(sumArr[s]+target, -1); if (e != -1) { return new int[]{s, e}; } } return null; }
咱們終於將時間複雜度下降到了O(n),這但是質的飛躍。
別急,還沒結束,對於這道題還有王者解法。上文中咱們經過不斷的優化,將時間複雜度從O(n^3)一步步下降到了,但咱們卻一步步增長了存儲的使用,從開始新增的sumArr數字,到最後的又增長的HashMap,空間複雜度從O(1)變爲了O(n)。有沒有辦法把空間複雜度也給將下來?我能寫到這那必然是有的。
這種算法叫作尺取法。尺取法,這個名字有點難理解。咱們直接舉個具體的例子,假設有n調長度不一的繩子並列放在一塊兒,你須要找出其中連續的一部分繩子組成一條長度爲target的繩子,這裏須要注意是連續。這時候你能夠找一個長度爲target的尺子,而後把繩子一段段往尺子上放,若是發現短了就日後面再接一根,若是發現長了,就把最頭上的一根扔掉,直到長度剛好合適。
在使用中咱們並不須要這把尺子,只須要拿target做爲標尺便可。提及來可能比較難理解,直接舉個例子,下圖演示了從數組中找到和爲22的子數組的過程。
只要小了就右加,大了就左減,直到找到目標。
爲何尺取法是對的?我理解尺取法實際上是解法二白銀解法的一種優化,也是遍歷了起點s,可是對終點e不作無效的遍歷,若是e到某個位置後已經超了,由於數組裏都是正數,再日後確定是超的,也就不必繼續遍歷e了。轉而去調整s,若是s右移到某個位置後總和小了,s再往右總和只會更小,也就不必繼續調整s了…… 整個過程就像是先固定s去遍歷e,而後固定e再去遍歷s ……,直到獲得結果。
尺取法可用的基礎在於e往右移動總和必定是增的,s往右移總和必定是減的,也就是說數組中全部的數必須是正的。 沒有完美的算法能夠解決任何問題,但對於特定的問題必定有最完美的解法。
說完尺取法,咱們來看下用尺取法是如何解決這道題的,代碼比較簡單,以下:
public int[] find(int[] arr, int target) { int s = 0, e = 0; int sum = arr[0]; while (e < arr.length) { if (sum > target) { sum -= arr[s++]; } else if (sum < target) { sum += arr[++e]; } else { return new int[]{s, e}; } } return null; }
只有一層循環,時間複雜度O(n)。沒有額外的空間佔用,空間複雜度O(1),這就是最完美的解法。
這道算法題乍看簡單,細看其實真的不簡單。可能你面試遇到,沒辦法一會兒想到最優的解,但給出一個可行的解總比沒有解強。我以前面試問別人這個題,他一上來就是想着怎麼最優解決,反而連最簡單的青銅解法都沒寫出來。記得下次面試,實在是解不出來就先給個60分的答案,而後再想辦法把分數提高上去,別最後交了白卷。 給出一個可行解,而後再持續迭代優化,我以爲這也是解決一個複雜問題比較好的思路。
最後送你們一句雞湯,沒有人生下來就是王者,只是不斷的努力成爲了王者罷了。
歡迎關注個人面試專欄 中高級程序猿面試題精選, 持續更新,永久免費,本專欄會持續收錄我遇到的中高級程序猿經典面試題,除了提供詳盡的解題思路外,還會從面試官的角度提供擴展題,是你們面試進階的不二之選,但願能幫助你們找到更好的工做。另外,也徵集面試題,若是你遇到了不會的題 私信告訴我,有價值的題我會給你出一篇博客。