LeetCode #001# Two Sum詳解(js描述)
索引
- 思路1:暴力搜索
- 思路2:聰明一點的搜索
- 思路3:利用HashMap巧解
問題描述:https://leetcode.com/problems/two-sum/
思路1:暴力搜索
一個很自然的想法就是暴力搜索了。根據排列組合原理,列舉Cn取2對數字,逐對進行判斷,效率是O(n^2-1/2n),代碼如下:
var twoSum = function(nums, target) { for (let i = 0; i != nums.length; ++i) { for (let j = i + 1; j != nums.length; ++j) {if (nums[i] + nums[j] == target) { return [i, j]; } } } };
思路2:聰明一點的搜索
最開始感覺就這麽簡單一個問題,哪還有別的什麽方法啊。。但是仔細想想輸入規模一變大的話n^2也太恐怖了,應該有更快一點的方法。另外一個比較自然的思路就是:先將數組排序,這個nlgn內可以完成,然後弄兩個下標分別指向頭和尾,邊做判斷邊根據判斷結果縮小搜索範圍,這樣一般在掃描完整個數組之前(n以內)就可以找到相應元素了,不過需要額外的空間來保存元素和原下標間的映射關系。最終效率是O(nlgn)尾巴有點大,先放代碼:
// O(nlgn) var twoSum2 = function(nums, target) { let copy = nums.map((x, i) => { return { val: x, index: i, }; }); // 保存元素的下標值 copy.sort((a, b) => a.val - b.val); // 按照元素值從小到大排序 let i = 0, j = copy.length - 1; let sum;while ((sum = copy[i].val + copy[j].val) != target) { sum > target ? j-- : i++; } i = copy[i].index; j = copy[j].index; return i < j ? [i, j] : [j, i]; };
正確性我也是糾結了一會兒,畢竟數學很菜。。。所以給出一個亂七八糟。。又不嚴謹的。。。“證明”↓
我們證明這個循環不變式:按照從有序數組的外圍朝向內側的搜索順序,如果(sum = copy[i].val + copy[j].val) != target那麽要找的元素一定在i+1..j或者i..j-1範圍中,至於變化i還是j取決於copy[i].val + copy[j].val相對target是大了還是小了:大了一定減j,小了一定加i。
初始化:單看i=0以及j=copy.length - 1;的話是顯然成立的,不過這屬於特殊情況,屬於沒路走了,只能i+1或者j-1。選取一個稍普通些的情形i+1..j-1(i=0且j=copy.length - 1),這個時候i和j都是可進可退的,那麽當sum > target的時候為什麽一定是j-1-1而不是i+1-1呢(這兩種操作都會讓sum更接近target)?一個直覺且沒毛病的理由是i..j-1在上個(或者上上個)叠代已經判斷過了,所以此路明顯不通,所以就只能是j-1-1了。然而我們需要考察更普通的情形,連續n次叠代sum都小於target,於是i先加了n,之後m次叠代sum都大於target,j才隨後減去m,那麽在j逐步-1的過程中i可不可以嘗試回退一步(-1,回退n步和1步情形都是一樣的,假設回退一步方便討論)呢?因為這麽做確實也讓sum變小了,並且容易知道i+n-1..j-m這對數並沒有判斷過(i累計加了n的時候j仍可能還在原地踏步)。不妨假設[i+n-1, j-m]就是原問題的解,即i前進到i+n又回退到i+n-1並且copy[i+n-1].val + copy[j-m].val == target成立。仔細考察“有序數組”這一前置條件,會發現倘若i和j是原問題的解,也就是copy[i].val + copy[j].val == target成立,那麽copy[i].val + copy[任何大於j的下標值].val總是大於target,這是因為copy[任何大於j的下標值].val > copy[j].val,言下之意,考慮最終的情形,必定i和j是有一方會先到達要找的元素之一且在原地等待,不存在什麽走過頭再回頭的情形,於是就推翻假設了。。。此時已經考慮了所有情況,因此循環不變式成立。當sum < target時類似。
保持:假設對於某個i..j(i>0且j<copy.length - 1)循環不變式成立,也就是要找的元素一定在i+1..j或者i..j-1中。那麽再進行一次叠代,也就是在i+1..j或者i..j-1中搜索,這又回到了初始化的情形,所以循環不變式仍然成立。
終止:因為題目確保了一定有那麽一對元素存在,叠代終止的時候就是sun == target找到答案的時候。
PS. 我扯了一堆網上看了一下發現好像。。。似乎有一個什麽數學定理。。。。。。。。。。。。。。。。。
思路3:利用HashMap巧解
知道這個思路後感覺很easy。。然而一開始並不容易想到(僅限我。。哈哈。。。。),事實上在給出target和一個元素後,另外一個元素就已經隨之確定了。因此可以構建一個map,用另外一個元素的值作為key(或者自己本身的值),而value部分可以用來存當前元素的下標,這樣一來,只需要至多掃描一遍數組(或者一遍以內),就可以得到答案了。效率是O(n),典型的用空間換時間。js代碼如下:
// O(n) var twoSum3 = function(nums, target) { let obj = {}; nums.forEach((x, i) => obj[target - x] = i); for (let i = 0; i != nums.length; ++i) { let j = obj[nums[i]]; if (j != undefined && j != i) { return i < j ? [i, j] : [j, i]; } } }; // O(n),漸進性沒有改善,不過去掉了一些尾巴 // Runtime: 52 ms, faster than 100.00% of JavaScript online submissions for Two Sum. var twoSum4 = function(nums, target) { let obj = {}, j; // 用obj作為map for (let i = 0; i != nums.length; ++i) { if ((j = obj[nums[i]]) != undefined) { return [j, i]; } obj[target - nums[i]] = i; } }; // 這個和上一個是一樣的,純粹為了測試一下用obj作map快還是new Map()快。結果是慢了一點。 var twoSum5 = function(nums, target) { let map = new Map(); let obj = {}, j; for (let i = 0; i != nums.length; ++i) { if ((j = map.get(nums[i])) != undefined) { return [j, i]; } map.set(target - nums[i], i); } };
LeetCode #001# Two Sum詳解(js描述)