1. 程式人生 > 實用技巧 >如何去除有序陣列的重複元素

如何去除有序陣列的重複元素

如何去除有序陣列的重複元素

讀完本文,你不僅學會了演算法套路,還可以順便去 LeetCode 上拿下如下題目:

26.刪除排序陣列中的重複項

83.刪除排序連結串列中的重複元素

27.移除元素

283.移動零

-----------

我們知道對於陣列來說,在尾部插入、刪除元素是比較高效的,時間複雜度是 O(1),但是如果在中間或者開頭插入、刪除元素,就會涉及資料的搬移,時間複雜度為 O(N),效率較低。

所以上篇文章 O(1)時間刪除/查詢陣列中的任意元素 就講了一種技巧,把待刪除元素交換到最後一個,然後再刪除,就可以避免資料搬移。

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在

labuladong的演算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種演算法套路後投再入題海就如魚得水了。

有序陣列/連結串列去重

先講講如何對一個有序陣列去重,先看下題目:

函式簽名如下:

int removeDuplicates(int[] nums);

顯然,由於陣列已經排序,所以重複的元素一定連在一起,找出它們並不難,但如果毎找到一個重複元素就立即刪除它,就是在陣列中間進行刪除操作,整個時間複雜度是會達到 O(N^2)。

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的演算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種演算法套路後投再入題海就如魚得水了。

簡單解釋一下什麼是原地修改:

如果不是原地修改的話,我們直接 new 一個 int[] 陣列,把去重之後的元素放進這個新陣列中,然後返回這個新陣列即可。

但是原地刪除,不允許我們 new 新陣列,只能在原陣列上操作,然後返回一個長度,這樣就可以通過返回的長度和原始陣列得到我們去重後的元素有哪些了。

這種需求在陣列相關的演算法題中時非常常見的,通用解法就是我們前文 雙指標技巧 中的快慢指標技巧

我們讓慢指標 slow 走在後面,快指標 fast 走在前面探路,找到一個不重複的元素就告訴 slow 並讓 slow 前進一步。這樣當 fast 指標遍歷完整個陣列 nums 後,nums[0..slow]

就是不重複元素

int removeDuplicates(int[] nums) {
    if (nums.length == 0) {
        return 0;
    }
    int slow = 0, fast = 0;
    while (fast < nums.length) {
        if (nums[fast] != nums[slow]) {
            slow++;
            // 維護 nums[0..slow] 無重複
            nums[slow] = nums[fast];
        }
        fast++;
    }
    // 陣列長度為索引 + 1
    return slow + 1;
}

看下演算法執行的過程:

再簡單擴充套件一下,如果給你一個有序連結串列,如何去重呢?這是力扣第 83 題,其實和陣列去重是一模一樣的,唯一的區別是把陣列賦值操作變成操作指標而已:

ListNode deleteDuplicates(ListNode head) {
    if (head == null) return null;
    ListNode slow = head, fast = head;
    while (fast != null) {
        if (fast.val != slow.val) {
            // nums[slow] = nums[fast];
            slow.next = fast;
            // slow++;
            slow = slow.next;
        }
        // fast++
        fast = fast.next;
    }
    // 斷開與後面重複元素的連線
    slow.next = null;
    return head;
}

移除元素

這是力扣第 27 題,看下題目:

函式簽名如下:

int removeElement(int[] nums, int val);

題目要求我們把 nums 中所有值為 val 的元素原地刪除,依然需要使用 雙指標技巧 中的快慢指標:

如果 fast 遇到需要去除的元素,則直接跳過,否則就告訴 slow 指標,並讓 slow 前進一步。

這和前面說到的陣列去重問題解法思路是完全一樣的,就不畫 GIF 了,直接看程式碼:

int removeElement(int[] nums, int val) {
    int fast = 0, slow = 0;
    while (fast < nums.length) {
        if (nums[fast] != val) {
            nums[slow] = nums[fast];
            slow++;
        }
        fast++;
    }
    return slow;
}

注意這裡和有序陣列去重的解法有一個重要不同,我們這裡是先給 nums[slow] 賦值然後再給 slow++,這樣可以保證 nums[0..slow-1] 是不包含值為 val 的元素的,最後的結果陣列長度就是 slow

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的演算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種演算法套路後投再入題海就如魚得水了。

移動零

這是力扣第 283 題,我來描述下題目:

給你輸入一個數組 nums,請你原地修改,將陣列中的所有值為 0 的元素移到陣列末尾,函式簽名如下:

void moveZeroes(int[] nums);

比如說給你輸入 nums = [0,1,4,0,2],你的演算法沒有返回值,但是會把 nums 陣列原地修改成 [1,4,2,0,0]

結合之前說到的幾個題目,你是否有已經有了答案呢?

題目讓我們將所有 0 移到最後,其實就相當於移除 nums 中的所有 0,然後再把後面的元素都賦值為 0 即可。

所以我們可以複用上一題的 removeElement 函式:

void moveZeroes(int[] nums) {
    // 去除 nums 中的所有 0
    // 返回去除 0 之後的陣列長度
    int p = removeElement(nums, 0);
    // 將 p 之後的所有元素賦值為 0
    for (; p < nums.length; p++) {
        nums[p] = 0;
    }
}

// 見上文程式碼實現
int removeElement(int[] nums, int val);

至此,四道「原地修改」的演算法問題就講完了,其實核心還是快慢指標技巧,你學會了嗎?

相關推薦:

_____________

我的 線上電子書 有 100 篇原創文章,手把手帶刷 200 道力扣題目,建議收藏!對應的 GitHub 演算法倉庫 已經獲得了 70k star,歡迎標星!