1. 程式人生 > >LeetCode #001# Two Sum詳解(js描述)

LeetCode #001# Two Sum詳解(js描述)

還在 規模 容易 foreach 關系 比較 tro 去掉 fin

索引

    1. 思路1:暴力搜索
    2. 思路2:聰明一點的搜索
    3. 思路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描述)