刷題筆記 — Array (2)

黃翌軒
CityChaser
Published in
7 min readFeb 17, 2021

本系列文章是分享我在 Leetcode 及 Algoexpert 的刷題經驗,此篇文章是 Array 篇的第二篇文章。

本系列文章都假定讀者具有基礎的 complexity analysis 概念以及基礎的 Java 能力。為了不讓閱讀時間過長,每篇都只會講解 3 道題目。

每道題目我都會分為 Description(問題描述)、Examples(測資範例)、Solution(解法)、Implementation(實作) 等區塊來進行講解。但如果礙於篇幅問題或是該解法比較不理想(太複雜、效率太差)我可能就只會進行概念上的講解,並不會實際寫出 code implementation。

另外,為了讓讀者能更佳地了解,當我提及程式碼的變數時,我會將該字反白(如 variable )。

👉 上一篇:刷題筆記 — Array (1)

如欲觀看所有系列文章,可點下面連結前往。

https://medium.com/citychaser/coding-questions/home

好了,廢話不多說,讓我們開始吧!

1. Remove Element

Description

給定一個 array of integers nums 和一個 integer val,計算 nums1 中不等於 val 的個數有幾個(假設為 N),並且修改 nums,讓其前 N 個元素是那些不等於 val 的元素,後面的元素是什麼並不需要考慮。

此題必須要修改原有的 nums,不可額外建立新的 array 或其他 data structures,space complexity 必須為 O(1)

Examples

Input: nums = [3, 2, 2, 3], val = 3
Output: 2, nums = [2, 2, 3, 3]
# 只要前兩個是 2 就行,nums = [2, 2, 0, 0] 一樣也是正確答案
Input: nums = [0, 1, 2, 2, 3, 0, 4, 2], val = 2
Output: 5, nums = [0, 1, 4, 0, 3] (這五個元素可用任何排列順序,只要保證所有不是 val 的元素都有在前 N 個位置即可)

Solution 1 — Two Pointer Swap

valnums 裡面有 m 個,而 nums 總共有 n 個元素,我們要做的就是把不是 val 的元素放在 nums 的前 n-m 個。

要做到這件事,我們可以使用兩個 pointer,分別從頭尾兩端往內走(以下稱為 head, tail),因為我們要把等於 val 的元素往後送、不等於 val 的元素往前送,所以當 head 指到的元素是 valtail 指到的元素不是 val 時,我們就把兩者互換。

如果上面條件不成立的話,那還要再根據兩個條件判斷一次。如果 head 指到的元素是 val,那 head 不能繼續往後走,要等 tail 指到不是 val 的元素時要互換;但如果 head 指到的元素不是 val,那就繼續往後走。 而 tail 則跟 head 相反,指到不是 val 的元素時要停下來,否則繼續往前。

Solution 2 — Two Pointer Override

題目其實有提到,只需要讓不等於 val 的元素在 nums 的前 n-m 個就可以,後面的元素是什麼其實不重要。

因此,我們可以利用一個比上面更好的方法來解決這題。

我們一樣使用兩個 pointer p1, p2,兩者初始時都指到 nums 的第一格。如果 p2 指到的元素不是 val,那就把值 assign 給 p1 所在的那格。接著,兩者都往後一格;如果 p2 指到的元素是 val,那就把 p2 往後移一格。

這個解法乍聽下來不知道為什麼可以 work,但其實很簡單, p2nums 第一格一直往後,最終會迭代每一個元素。當它指到的元素不是 val 時,就會把這個值往前塞,總共會塞幾次?答案是 n-m 次,因為 nums 總共有 n 個元素,其中有 m 個等於 val,所以不等於 val 的就是 n-m。而 p1 一開始也是指到 nums 第一格(index 為 0),它只有在 p2 指到的元素不是 val 時才會將指到的該格往下,總共會進行 n-m 次,所以會將 numsn-m 格換成不是 val 的元素,而且不會有重複的問題,因為 p2 只會指到每個元素一次。

以上兩個解法的 time complexity 其實都是 O(n) ,不過第二個解法概念上更為簡潔,也不用處理 edge case 的問題。

2. Replace Elements with Greatest Element on Right Side

Description

給定一 array of integer arr,將每個元素替換成該元素右邊所有元素中最大值,最後一個元素則替換成 -1。

Examples

Input: arr = [17, 18, 5, 4, 6, 1]
Output: [18, 6, 6, 6, 1, -1]

Solution 1 — Look Right Every Time

既然要找右邊最大的元素,那麼最簡單的方法就是每當迭代到某個元素時,就從他下一個開始找到 arr 的最後一個,看最大的是誰。每個元素都重複這個動作,直到倒數第二個元素,最後一個則直接置換成 -1

雖然簡單明瞭,但是可以想見,這個解法的 time complexity 不會太好,因為它疊了兩層的 for-loop,第一層先迭代每個元素,第二層又要從該元素的右邊一格一直找到最後一格。總共的 time complexity 會是 O(N²),是一個 brute force 的解法。

Solution 2 — Trace Backwards

有沒有辦法只迭代一次 arr 就完成這項任務呢?是可以的!我們只要從最後面開始就辦得到。

如果從前面開始的話會遇到一個問題,第一次迭代整個 arr 我們可以找到最大值,但這個最大值不能應用到所有元素身上,因為隨著往右走的過程中,會越過這個成為最大值的元素,而在這個最大值右邊的元素,它們右邊的最大值就不會是整個 arr 的 global maximum 了,於是又要針對每個元素重新去找。

但如果從末端開始就不會有這個問題,從末端往前找的過程中一直紀錄著目前找到的最大值,到了某個位置時,我們先將該位置替換成目前的最大值,除此之外,還要檢查這個元素是不是大於目前的最大值,如果是的話,就要把最大值替換成這個元素。

在這個解法中,我們只將 arr 所有的元素迭代一次,在每個元素的操作中也都是 constant time operation,因此總共的 time complexity 為 O(N)

我分別將這兩個解法都 submit,前者花了 143ms 的 runtime,後者只花了 1ms,可見差別是非常大的。

3. Sort Array by Parity

Description

給定一個 array A,裡面的每個元素都為 non-negative integers,回傳一個 array,其中出現在 A 中的所有偶數元素必須排列在所有奇數元素前面(不用考慮偶數與偶數間、奇數與奇數間的順序)。

Examples

Input: [3, 1, 2, 4]
Output: [2, 4, 1, 3] / [2, 4, 3, 1] [4, 2, 1, 3] / [4, 2, 3, 1] (四組皆為正確答案)

Solution 1 — Two Pointer with New Array

建立一個新的 array B,長度與 A 相同,接著 loop through A,遇到奇數的時候放到 B 的尾端,並且把尾端往左一格;遇到偶數的時候放到 B 的頭端,並且把頭端往右一格,直到迭代完 A 的所有元素。

這個解法的 time complexity 為 O(N),但是因為我們使用了額外的 array,因此 space complexity 也為 O(N)

Solution 2 — Two Pointer without New Array

上面的解法是將兩個 pointer 放在「新的」array,而這個解法則是將兩個 pointer 放在原 array A 的頭 (head) 與尾 (tail)。我們的目標是要將偶數全部放到前面、奇數全部放到後面,所以也就是說,當 head 遇到奇數時,應該要把它往後丟、當 tail 遇到偶數時,應該要把它往前丟。

所以當這兩個條件都成立的時候,我們就把兩者位置互換,讓奇數去後面、後數去前面。那如果只有一個條件成立呢?那就是讓條件不成立的那個 pointer 繼續往中間移動,直到兩個條件成立。什麼時候會停?當兩個 pointer 交會的時候就停下來。

今天的刷題分享就到這邊了,如果有什麼指教或是建議,歡迎在這篇文章底下留言;如果覺得這篇文章不錯,也歡迎給我點掌聲!

👉 下一篇:刷題筆記 — Array (3)

--

--