一、題目描述
> 在一個長度為 n 的數組 nums 里的所有數字都在 0~n-1 的范圍內。數組中某些數字是重復的,但不知道有幾個數字重復了,也不知道每個數字重復了幾次。請找出數組中任意一個重復的數字。
二、思路分析
- 算法(Algorithm)指的是解題的方案,是一系列解決問題的明確動作。所以說算法沒有語言區分,只要我們的方案是完整的任何語言都可以實現它。我是C++出身但是從事Java多年,下面將是通過java來實現算法
考察點
- 任何算法基本上都可以通過暴力枚舉來解決,但那僅僅是理論上。解決問題不僅要考慮理論最終還得取決於硬件和時間的支持。所以我們面對一個問題首先得確定方案。想要確定方案就得知道問題的痛點或者說問題的考點在哪里
- 此題是要找出重復的數字,想要找出重復的數字就得有一個對比的操作,想要有一個對比的操作就得將舊數據存放在一定規則的區域中。關於規則的區域這就引入了哈希表(HashTable)。
三、代碼+解析
初版
public int findRepeatNumber(int[] nums) {
//構建hash表 。 Java中Map天生的Hash表
Map<integer, object=""> map = new HashMap<>();
for (int num : nums) {
//已經在hash表中存在的說明數據重復
if (map.containsKey(num)) {
return num;
}
//沒有重復的數據需要添加到hash表中
map.put(num, num);
}
return 0;
}
- 此題是leetcode中簡單類型的題目。既然是刷題首先得找簡單的找找自信。正好也確定下自己的刷題風格。結果很明顯是沒有問題也是一次性通過。
- 雖然題目簡單但是我們不能僅僅滿足於完成。回過頭來想想我們這樣做有啥缺點是不是還有進步的空間呢?
首次升級
升級點
- 在上面的那個版本中我們借助於hash表來實現數據的存儲從而進行數據的比對是否重復。這里因為引入了hash表而hash表就需要在內存中開辟空間這就導致了我們的程序在內存上開辟的比較大。會隨着數組的重復性后偏導致我們的hash表內存越來越大,極端情況下我們的hash表中的元素和數組中的元素趨近於相等。
- 其次是每次都需要從hash中獲取數據和數組中的數據進行對比。我們知道hash表尤其是Java中的Map的實現在獲取數據是需要先根據hashcode值定位到hash槽,然后在從槽頭開始遍歷鏈表或者是樹進行數據尋找。這個過程雖然已經很快了但是和數組直接尋址法相比就弱爆了。
- 基於上面兩個痛點,我決定取消Hash表的引入。上面說了本題的考點是Hash表,但是並不意味着必須使用Hash表來實現是最優的。所謂條條大路通羅馬實現是有很多種的,善於利用周遭的環境是我們人類的本能。
優化落地
- 既然是查找重復數據如果是有序的數組的話只需要逐個對相鄰的兩個進行比較就可以了。因為有序狀態的數組每個元素會起着隔離的效果,這樣就避免的Hash表的存在在內存上肯定比Hash表低的,而且上面也提到了數組的尋址比
HashMap
快的多,所以在速度上應該也會快很多的
public int findRepeatNumber(int[] nums) {
Arrays.sort(nums);
for (int i = 1; i < nums.length; i++) {
if (nums[i] == nums[i - 1]) {
return nums[i];
}
}
return 0;
}
- 上圖中左邊是Hash表的方式運行效果,右圖是相鄰比較的運行效果。兩者在執行時間和內存消耗上不是在同一個量級的。提升了兩倍之多
再次升級
升級點
- 上面排序后相鄰位置比較運行的結果我覺得還是挺滿意的,但是在代碼的是實現上有個邊界的問題。而且我們需要逐個進行比較,逐個比較在時間上應該是比較耗時的。
- 基於上面逐個比較,筆者這里再次進行優化。將進行跳位比較
- 跳位就避免了逐個比較,將比較的次數控制下來。
public int findRepeatNumber(int[] nums) {
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
int index =i;
while (index != nums[index]) {
index = nums[index];
}
if (index != i) {
return index;
}
}
return 0;
}
- 效果對比看一下,兩者基本沒有區別。在執行速度上基本一致。內存消耗上后者應該比前者高一點的,可能是leetcode統計內存沒有那么細致再結合運行期間不穩定因素所以執行出來的結果雖然是后者高但是實際上筆者這里認為逐位相鄰比較才是最優的。
- 本次的升級實際上是失敗的,充其量就是逐位相鄰比較的一種變形。但是本次的變形卻引入另外一個概念---跳位交換
最終升級
升級點
-
其實仔細思考下為什么跳位尋址比較沒有逐位相鄰比較有什么顯著的提升呢。說到底是因為我們已經排好順序了在已經排好的順序中我們跳位進行比對是沒有起到太大的作用的。
-
這里筆者又查閱了官方的推薦解法--原地交換。這里的【原地交換】和筆者提出的【跳位尋址比較】不謀而合。下面我將翻譯下官網的推薦解法(官網是真的強大)
-
這里官網推薦的代碼就不貼了。大家可以直接在官網題解中找到原地交換講解 。 但是筆者嘗試了很多次都沒有題解中說的100% 。 可能語言的差異所以他的實現並不支持java的。筆者這里對他進行稍微的帶動
public int findRepeatNumber(int[] nums) {
for (int i = 0; i < nums.length; i++) {
while (i != nums[i]) {
if (nums[i] == nums[nums[i]]) {
return nums[i];
}
nums[i] = nums[i] ^ nums[nums[i]];
nums[nums[i]] = nums[nums[i]]^nums[i];
nums[i] = nums[i] ^ nums[nums[i]];
}
}
return 0;
}
- 官網中引入了一個臨時變量用於交換數據暫存。這里內存就會一直被占用。理論上內存也不會太受影響的。但是結果在java中運行卻不是那么完美
- 筆者這里將通過異或的方式實現數據的交換。運行結果相對會高點 。這里筆者在此提醒下leetcode每次運行因為大環境的問題並不能准確反映性能的問題
- 下面是筆者在leetcode連續運行三次的效果圖
四、總結
- 不能僅僅依賴leetcode的運行結果作為衡量程序好壞的依據。筆者這里只是從個人的角度出發區分出程序的優劣
- 雖然leetcode不能作為唯一標准,但是多次運行的結果可以做一個參考價值。
- 算法的實現並不是一層不變的。我們學習算法是基礎面對實際的問題還是得在算法的基礎上進行擴展,結合實際的場景觸發才是最正確的選擇
> 最后還得送各位兄弟一句話,關注、點贊、收藏不能忘。萬一哪天你找不到我了呢
</integer,>