JS leetcode 合並兩個有序數組 解題分析


壹 ❀ 引

今天做的一題是前兩周博客園一粉絲在面試360時遇到的算法題,題目來自leetcode88. 合並兩個有序數組,理解起來可能有些費勁,不過我盡量用圖的形式給大家解釋它,題目描述如下:

給你兩個有序整數數組 nums1nums2,請你將 nums2 合並到 nums1 中,使 nums1 成為一個有序數組。

說明:

初始化 nums1 nums2 的元素數量分別為 m 和 n 。
你可以假設 nums1 有足夠的空間(空間大小大於或等於 m + n)來保存 nums2 中的元素。

示例:

輸入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6],       n = 3

輸出: [1,2,2,3,5,6]

我們先來簡單分析題目,再來看看如何解決它。

貳 ❀ 題解分析

首先,數組nums1nums2都是有序數組,有些奇怪的是,nums1中的剩余空間都是以0表示,而這些位置就是為nums2准備的,我們要做的就是將nums2放進nums1中,當然,我們還得保證合並之后nums1的有序性。

由於題目不需要return新數組,而是在原數組nums1上做修改,所以我第一想到的暴力做法就是將nums1中的0全部裁剪掉,並將nums2加入進去,再做排序,那么這里就可以使用splice方法,略微暴力的做法:

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function (nums1, m, nums2, n) {
    // 從m位開始裁剪n個元素后,並將nums2的元素加入進去
    nums1.splice(m, n, ...nums2)
    // 重排nums1
    nums1.sort((a, b) => a - b);
};

這樣能解決問題,不過有些違背題目本意,數組的有序性我們並未利用,確實有些投機取巧了。而且在360面試中,該粉絲也被問到雙指針優化問題,比較巧的是官方推薦做法也是雙指針,所以我們還是站在雙指針角度來重新看待這個問題。

由於nums1中的0其實就是預留給nums2的空間,准確來說,我們要做的就是將0替換成nums1nums2的元素,這個據排序大小而定。

m和n分別代表了nums1nums2的有效元素個數,因此合並完成后的新nums1長度為m+n-1

由於數組nums1nums2都是有序數組,所以不難想到,如果num2中的一個元素比nums1的最后一個元素大,那么一定比nums1的其它元素都大,這樣相比正序比較,倒序遍歷耗時會大大減少。

我們先來看一張過程圖,再來解釋做了什么:

第一次比較

第二次比較

第三次比較

第四次比較

那么我們現在要對nums1倒序更新元素,同時需要兩個指針,分別指向nums1m-1處與nums2n-1處,然后開始比較,如果nums2的最后一個元素比nums1的最后一個元素大(注意,這里的最后是m-1),那么nums1索引為m+n-1處的0就應該被替換成nums2的最后一個元素,為啥呢,首先數組都是有序的,nums2的最后一個元素相對自己是最大的一個數,如果它比nums1的最后一個元素大,自然也會比前面其它所有數都大,放到最后是毋庸置疑的。

在經過這次比較后,因為nums2最后一個元素已經被使用了,所以nums2的指針左移,進行下次比較。如果遇到nums1的元素比nums2大的情況,我們還是一樣的將nums1的元素添加到后面,同理nums1的指針要開始左移。

其實不難想象,一共有三個指針,指針p1(指針的單詞是pointer)與p2分別指向nums1nums2的有效元素位,指針p指向nums1的最后一位,經過比較,我們使用將較大的放到nums1的p位,此時p就得左移,p做的工作就是負責不斷的替換nums1中的元素。

我覺得我也是夠啰嗦,思路說清楚了,我們來實現它:

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function(nums1, m, nums2, n) {
    // p初始指向nums1最后一位
    let p = m + n - 1,
        p1 = m - 1,
        p2 = n - 1;
    //如果其中有小於0,說明直接是空數組,不用比較直接裁剪
    if (p1 < 0 || p2 < 0) {
        nums1.splice(0, n, ...nums2);
    };
    while (p2 >= 0) {
        // 如果p1比p2大
        if (nums1[p1] > nums2[p2]) {
            // 將p1的值丟到p位置
            nums1[p] = nums1[p1];
            // p與p1都左移
            p--;
            p1--
        } else {
            // 反之把p2的值丟到p位置
            nums1[p] = nums2[p2];
            // p和p2左移
            p--;
            p2--
        };
    };
};

這段代碼其實有些極限,比如當例子是[2,0],1,[1],1時,由於第一次比較2>1所以經過修改nums1變成了[2,2],緊接着p與p1遞減。由於條件p2還是0滿足條件,所以繼續了第二次比較,而此時p1變成了負一,nums[-1]>nums2[p2]比較肯定失敗,這才走了else分支,於是將nums2的1復制到了p位置,sums1變成了[1,2]

2021.4.5修正上述思路

var merge = function (nums1, m, nums2, n) {
    var p = m + n - 1;//0
    var p1 = m - 1;//-1
    var p2 = n - 1;//0
  	// 理論上來說,nums2應該全部填充進去,所以這里以p2作為條件
    while (p2 >= 0) {
        // nums1里面全是0的情況,比如[0], 0, [1], 1
        if (p1 < 0) {
            // 直接用nums2去填補nums1就好了
            nums1[p--] = nums2[p2--]
        // 只有nums2比nums1大才用nus2填補
        } else if (nums2[p2] > nums1[p1]) {
            nums1[p] = nums2[p2];
            p--;
            p2--;
        // 反之用nums1填補
        } else {
            nums1[p] = nums1[p1];
            p--;
            p1--;
        }
    };
};

這里參考靈魂畫手解題思路再補充一種解法:

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function(nums1, m, nums2, n) {
    let len1 = m - 1;
    let len2 = n - 1;
    let len = m + n - 1;
    while(len1 >= 0 && len2 >= 0) {
        // 注意--符號在后面,表示先進行計算再減1,這種縮寫縮短了代碼
        nums1[len--] = nums1[len1] > nums2[len2] ? nums1[len1--] : nums2[len2--];
    }
    function arrayCopy(src, srcIndex, dest, destIndex, length) {
        dest.splice(destIndex, length, ...src.slice(srcIndex, srcIndex + length));
    }
    // 表示將nums2數組從下標0位置開始,拷貝到nums1數組中,從下標0位置開始,長度為len2+1
    arrayCopy(nums2, 0, nums1, 0, len2 + 1);
};

這里的arrayCopy其實做了兩件事,第一假設兩個指針一開始有一個不滿足大於等於0情況,while跳過直接裁剪,與我之前想法一樣。

第二是考慮了p1越界情況,只要p1小於0,說明p1所有元素都找到了對應位置,由於全程都是在進行雙指針元素比較,即使nums2還有元素沒安排,那也一定是最小的幾個元素,又因為nums2是有序的,所以直接裁剪過去就好了。

為什么不考慮p2越界情況呢?因為p2越界,說明nums2中所有元素都在nums1中找到了何時的位置了,同理nums1也是有序的,即使剩下的元素沒比較完,那也是有序的了!

還有,while循環中的賦值與遞減確實讓我眼前一亮....代碼實現也比我邏輯性更強,加油吧,那么本文就到這里了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM