1. 程式人生 > 其它 >提高模擬賽Day8T2最大匹配

提高模擬賽Day8T2最大匹配

https://labuladong.gitee.io/algo/4/30/118/

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

26.刪除有序陣列中的重複項(簡單)

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

27.移除元素(簡單)

283.移動零(簡單)

———–

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

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

那麼這篇文章我們換一個場景,來講一講如何在原地修改陣列,避免資料的搬移。

有序陣列/連結串列去重

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

函式簽名如下:

int removeDuplicates(int[] nums);

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

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

如果不是原地修改的話,我們直接 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

移動零

這是力扣第 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);

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

_____________