
壹 ❀ 引
十天前做的一道題了,一直沒整理,今天才花時間去讀了官方題解思路,這道題也凸顯出了算法思路的重要性,執行耗時差的真不是一點半點。題目來自448. 找到所有數組中消失的數字,題目描述如下:
給定一個范圍在 1 ≤ a[i] ≤ n ( n = 數組大小 ) 的 整型數組,數組中的元素一些出現了兩次,另一些只出現一次。
找到所有在 [1, n] 范圍之間沒有出現在數組中的數字。
您能在不使用額外空間且時間復雜度為O(n)的情況下完成這個任務嗎? 你可以假定返回的數組不算在額外空間內。
示例:
輸入: [4,3,2,7,8,2,3,1] 輸出: [5,6]
我們簡單分析題目,看看有哪些方式可以做到。官方思路理解起來開始有那么點繞,沒關系,我還是會畫圖把解題思路說清楚,那么本文開始。
貳 ❀ 憨憨做法,循環嵌套n²
我們來提取題目信息,一個包含n個數字的數組,每個數字的范圍為1 ≤ a[i] ≤ n
,也就是說,假設n為5,那么數組元素一定有5個,每個數字可取范圍是[1,5]
,再加上某些元素會出現2次,所以必定會導致某些元素沒有空間去存放它,從而導致缺失(有點抽屜原理的意思),而我們的目的就是要找到缺失的元素。比如:
[1,2,2,3,5]//缺了4
[1,1,2,2,3]//缺了4,5
以上面例子來說,因為我們已經知道a[i]
范圍為[1,n],所以只要知道知道1-n之間每個數有沒有在數組中出現即可了,上代碼:
/**
* @param {number[]} nums
* @return {number[]}
*/
var findDisappearedNumbers = function(nums) {
let len = nums.length,
ans = [];
// 注意這里是從1開始,因為范圍是[1,n]
for(let i =1;i<=len;i++){
//判斷nums里面有沒有包含i,沒有就加入ans
if(!nums.includes(i)){
ans.push(i);
};
};
return ans;
};
當然將上文中的includes
替換成indexof
來查找也是可以的。那么這種做法效率怎么樣呢?當然不咋地,我們在外層需要遍歷n次,而每次都得去數組里面查找一次誰沒有,非常典型的O(n²)
時間負責度。隨着n范圍越大,所需耗時會劇增。
叄 ❀ 使用哈希表,復雜度降為2n
有沒有其它更好的做法呢,當然有,比如使用哈希表。我們可以將數組中每個出現的元素記錄在哈希表中,設置為true,表示它出現過,這樣遍歷一次我們就得到了一份記錄了所有出現過數字的哈希表了。
之后拿這個哈希表去和[1,n]
對比,只要不為true的很明顯是沒出現的,那就是我們想要找到的缺失元素了,直接上代碼:
/**
* @param {number[]} nums
* @return {number[]}
*/
var findDisappearedNumbers = function(nums) {
let len = nums.length,
hash = {},
ans = [];
if (!len) {
return ans;
};
// 在哈希表中記錄出現過的數字
nums.forEach(num => hash[num] = true);
for (let i = 1; i <= len; ++i) {
// 沒有就是undefined
if (!hash[i]) {
ans.push(i)
};
};
return ans;
};
有同學就要問了,這思路不是跟上面差不多嗎,思路是差不多,但是從此時時間上已經降為2n了,按照規律省去系數2,時間復雜度其實是O(n)
。同學們,這個時間提升那就是質的飛躍了,我們可以假象n為1000,即便是2n也才2000,但上面的做法就是10000000了。
肆 ❀ 原地修改數組
還有沒有其它做法呢,當然有,通過原地修改數組的做法,理解起來可能有點繞,我盡量說清楚點。
在上文中我們已知,這個數組有幾個元素比如5個,那么數組中最大的數不會超過5。而我們知道數組長度為5,最后一個數的索引其實是4。也就說,我們可以找到任意一個數,減去1,都能對應到數組中某個元素。
可能大家還沒get到這個點,拿個例子來說,比如現在有數組[1,2,3,3,3]
;
我們從索引0開始遍歷,拿到第一個元素,1,減去一個1。為什么要減去1?因為以a[i]
此時的范圍是[1,5],減去1不正好對應了數組索引0-4嗎?好了,此時a[0]
其實就是自己,我們將它變成負數,也就是-1。此時數組為[-1,2,3,3,3]
。
繼續第二次遍歷,第二個數字是2,減去1,a[1]
正好是2,於是我們也將其變成負數,此時數組為[-1,-2,3,3,3]
。
緊接着第三次遍歷,我們拿到3,減去1,將其索引的數變負數,此時數組就是[-1,-2,-3,3,3]
。
繼續第四次遍歷,我們又拿到3,減去1,a[2]
此時已經是負數了,不做操作。
最后一次遍歷,我們還是拿到3,由於是負數,一樣不作操作。
於是我們拿到最終數組[-1,-2,-3,3,3]
。而通過前面知道,凡是負數的說明其索引加1的數都是存在的。而正數的這兩個的索引加1,正好是我們所缺的[4,5]
,這不就是我們想要的答案嗎。
讓我們上代碼:
/**
* @param {number[]} nums
* @return {number[]}
*/
var findDisappearedNumbers = function(nums) {
let len = nums.length,
ans = [];
if (!len) {
return ans;
};
// 1-n開始,有n個,那么n-1一定能對應到一個索引
for (let i = 0; i < len; i++) {
let num = Math.abs(nums[i]);
// 注意,如只有是正數的情況我們才轉負數
if (nums[num - 1] > 0) {
nums[num - 1] *= -1;
}
};
for (let i = 0; i < len; i++) {
// 正數的索引加1,就是我們缺失的數了
if (nums[i] > 0) {
ans.push(i + 1);
};
};
return ans;
};
最后,上一張三種方案時間耗時對比圖。

你看,讓數組遍歷降維,真的是巨大的優化啊。