壹 ❀ 引
今天做的一題是前兩周博客園一粉絲在面試360時遇到的算法題,題目來自leetcode88. 合並兩個有序數組,理解起來可能有些費勁,不過我盡量用圖的形式給大家解釋它,題目描述如下:
給你兩個有序整數數組
nums1
和nums2
,請你將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]
我們先來簡單分析題目,再來看看如何解決它。
貳 ❀ 題解分析
首先,數組nums1
與nums2
都是有序數組,有些奇怪的是,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替換成nums1
或nums2
的元素,這個據排序大小而定。
m和n分別代表了nums1
與nums2
的有效元素個數,因此合並完成后的新nums1
長度為m+n-1
。
由於數組nums1
與nums2
都是有序數組,所以不難想到,如果num2
中的一個元素比nums1
的最后一個元素大,那么一定比nums1
的其它元素都大,這樣相比正序比較,倒序遍歷耗時會大大減少。
我們先來看一張過程圖,再來解釋做了什么:
第一次比較
第二次比較
第三次比較
第四次比較
那么我們現在要對nums1
倒序更新元素,同時需要兩個指針,分別指向nums1
的m-1
處與nums2
的n-1
處,然后開始比較,如果nums2
的最后一個元素比nums1
的最后一個元素大(注意,這里的最后是m-1),那么nums1
索引為m+n-1
處的0就應該被替換成nums2
的最后一個元素,為啥呢,首先數組都是有序的,nums2
的最后一個元素相對自己是最大的一個數,如果它比nums1
的最后一個元素大,自然也會比前面其它所有數都大,放到最后是毋庸置疑的。
在經過這次比較后,因為nums2
最后一個元素已經被使用了,所以nums2
的指針左移,進行下次比較。如果遇到nums1
的元素比nums2
大的情況,我們還是一樣的將nums1
的元素添加到后面,同理nums1
的指針要開始左移。
其實不難想象,一共有三個指針,指針p1(指針的單詞是pointer)與p2分別指向nums1
與nums2
的有效元素位,指針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循環中的賦值與遞減確實讓我眼前一亮....代碼實現也比我邏輯性更強,加油吧,那么本文就到這里了。