今天刷leetcode第三題:
給定一個排序數組,你需要在原地刪除重復出現的元素,使得每個元素只出現一次,返回移除后數組的新長度。
不要使用額外的數組空間,你必須在原地修改輸入數組並在使用 O(1) 額外空間的條件下完成。
示例 1:
給定數組 nums = [1,1,2],
函數應該返回新的長度 2, 並且原數組 nums 的前兩個元素被修改為 1, 2。
你不需要考慮數組中超出新長度后面的元素。
示例 2:給定 nums = [0,0,1,1,1,2,2,3,3,4],
函數應該返回新的長度 5, 並且原數組 nums 的前五個元素被修改為 0, 1, 2, 3, 4。
你不需要考慮數組中超出新長度后面的元素。
說明:為什么返回數值是整數,但輸出的答案是數組呢?
請注意,輸入數組是以“引用”方式傳遞的,這意味着在函數里修改輸入數組對於調用者是可見的。
你可以想象內部操作如下:
// nums 是以“引用”方式傳遞的。也就是說,不對實參做任何拷貝
int len = removeDuplicates(nums);// 在函數里修改輸入數組對於調用者是可見的。
// 根據你的函數返回的長度, 它會打印出數組中該長度范圍內的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array
著作權歸領扣網絡所有。商業轉載請聯系官方授權,非商業轉載請注明出處。
解題思路(該思路有缺陷,並且我寫的代碼的邏輯相對官方題解來說比較復雜,不夠直觀,建議直接看最后的官方題解的代碼):
1. 從左往右遍歷數組,使用兩個指針,嵌套循環,一外一內,內層指針從外層指針的下一個開始遍歷,
2. 兩個指針對應的值進行比較,如果相等,刪除內層指針對應的值,內層指針往右移一位,外層指針不變;如果兩者的值不相等,則外層指針往右移一位,內層指針重置為外層指針的下一個
3. 重復步驟2,直到外層指針遍歷值數組倒數第2項,結束。
public class RemoveDuplicates { /** * 1. 保存數組有效數據長度為len,外層循環,從左往右遍歷數組,遍歷邊界條件為len-2;再嵌套一層內循環,一外一內,而內層指針等於外層指針+1, * 2. 兩個指針對應的值進行比較,如果相等,刪除內層指針對應的值(刪除方式:通過把內層指針右邊的所有值,全部往左邊移動一位;刪除操作之后,內層指針此時指向的值已經是原數組下一個位置的值),並且len-1, * 內外層指針均不變。重復該步驟,直到兩個指針的值不相等,或者內層指針已經超出len-1 * 3. 外層指針往右移一位,內層指針重置為外層指針的下一個,重復步驟2,直到外層指針遍歷到達邊界,才結束 * * @param nums 目標數組 * @return int 數組的新長度(其實該值是數組有效數據的長度,從存儲角度來看,數組長度是不變的) * @author Zxz * @date 2019/10/23 11:46 **/ public static int mySolution(int[] nums) { //該算法計算的次數 int counts = 0; int len = nums.length; //循環邊界條件:len-2,len在內部會隨着重復元素的刪除而不斷的遞減,因此邊界條件也隨着縮小, for (int i = 0; i <= len - 2; i++) { int j = i + 1; while (nums[i] == nums[j]) { removeFromArr(nums, j); len--; counts++; //注意這里要加上這個跳出循環的條件,否則當最后一項和倒數第2項相等時,會陷入死循環 if (j > len - 1) { break; } } } System.out.println(counts); return len; } public static int removeFromArr(int[] ints, int i) { if (i > ints.length - 1) { //索引下標越界 return ints.length; } else if (i == ints.length - 1) { //索引下標等於數組長度-1 return ints.length - 1; } //手動復制數組可以用系統函數代替:System.arraycopy /*for (int j = i + 1; j < ints.length; j++) { ints[j - 1] = ints[j]; }*/ //以下式子中的最后一個參數的計算方式:(ints.length-1) - (i+1) + 1 System.arraycopy(ints, i + 1, ints, i, ints.length - i - 1); return ints.length - 1; } public static void main(String[] args) { int[] ints = {1, 2, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 6, 7, 8, 8, 8, 9, 10, 11}; int i = mySolution(ints); for (int j = 0; j < i; j++) { System.out.println(ints[j]); } } }
總結:
1. 當使用while時,要注意判斷條件是否會到達終點,也就是false,否則有可能陷入死循環,程序無法正常運行下去。
2. 對數組進行操作時,因為數組定義出來之后,長度是無法更改的(不像java的集合類List,長度可以動態調整),因此數組嚴格意義上是沒有元素刪除操作的,除非把整個數組的數據拷貝到一個新數組中。但是該題要求是原地操作,對於空間使用有限制,因此數組的刪除操作只能是把對應索引右邊的所有數據往左移動一位,假刪除。這樣數組最右邊的數據就不是有效數據了,因此最后還需要返回數組中有效數據的長度。
LeetCode官方題解
看了官方題解之后,頓時覺得自己寫出來的代碼簡直難以直視,上代碼:
/** * 官方題解:雙指針法 * * 數組完成排序后,我們可以放置兩個指針i 和j,其中i 是慢指針,而j 是快指針。只要 nums[i] = nums[j],我們就增加 j 以跳過重復項。 * 當我們遇到 nums[j] != nums[i],跳過重復項的運行已經結束,因此我們必須把它(nums[j])的值復制到 nums[i + 1]。然后遞增 i,接着我們將再次重復相同的過程,直到 j 到達數組的末尾為止。 * * 復雜度分析 * 時間復雜度:O(n),假設數組的長度是 n,那么 i 和 j 分別最多遍歷 n 步。 * 空間復雜度:O(1) * * 作者:LeetCode * 鏈接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/solution/shan-chu-pai-xu-shu-zu-zhong-de-zhong-fu-xiang-by-/ * 來源:力扣(LeetCode) * 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。 * * @param nums 目標數組 * @return int * @author Zxz * @date 2019/10/23 19:21 **/ public static int removeDuplicates(int[] nums) { if (nums.length == 0) return 0; int i = 0; for (int j = 1; j < nums.length; j++) { if (nums[j] != nums[i]) { i++; nums[i] = nums[j]; } } return i + 1; }
二次總結(包含復雜度分析):
雙指針法,真的是容易理解,而且時間復雜度也很低。相比之下,我的算法多出來許多步驟,比如,我的解法每次只要遇到相等值的時候,就馬上把右邊所有的值往左移動了,這樣遇到n個連續相等值的時候,會有n-1次的批量數據移動操作,當n的值很大時,時間復雜度成指數增長。而雙指針的算法,並不是相等的時候觸發移動操作的,正相反,是不相等的時候,才會去移動數值,並且一次只移動一個,而不是像我的算法那樣,一次移動一堆,如此笨重。
現在假設最壞的情況,整個數組所有值都相等,則需要移動數據的總次數為: counts = (n-1) + (n-2) + (n-3) + ... + 2 + 1 = n * (n-1) /2,時間復雜度為 O(n2),真是糟糕的算法。我用20w個相等的數進行測試,我的算法總共耗時10秒左右,而官方題解的算法耗時3毫秒,差了何止千萬倍啊,而且數量越大,差距真是越大。算法真的要學好。