Leetcode題解 Python:Remove Linked List Elements

Remove all elements from a linked list of integers that have value val.

給一串 Linked List ,要刪除其中節點等於 val 的節點,並回傳頭節點。

這題不難,如果用快慢指針,就需要針對刪除頭部的狀況做判斷。

對我來說,要針對非題目所求的特別情況去做 if 判斷,這種寫法在我心中的「泛用性」就在及格邊緣搖搖欲墜。

這題已經離開了雙指針的子章節,應該是可以不用雙指針去解。

用了遞迴寫法,雖然版面簡潔許多,不過時間複雜度是紮實的 O(N),每個節點都會修改。

class Solution:
def removeElements(self, head: ListNode, val: int) -> ListNode:
def nextNode(node):
if node:
node.next = nextNode(node.next)
if node.val == val: return node.next
return node
return nextNode(head)

上一題說到將 Linked List 反序,如果用反序的概念,也就是把目標值丟到最前方去。如此,最後只需要從頭跑 Linked List 直到不為目標值的時候,再把該節回傳。

class Solution:
def removeElements(self, head: ListNode, val: int) -> ListNode:
if not head: return head
FP = SP = head
FP = FP.next
while FP:
if FP.val == val:
SP.next = SP.next.next
FP.next = head
head = FP
FP = SP.next
else:
SP = SP.next
FP = FP.next
#
while head and head.val == val:
head = head.next
return head

如果頭部移除的情形會很困擾,看了前段班的寫法,可以加入了暫時根節變成新頭根節,這樣可以把一切都當成子根節處理,最後回傳暫時根節的下一個(原頭根節)即可,就不需要針對頭根節移除自身做額外寫法。
# 前段班大神寫法

class Solution:
def removeElements(self, head: ListNode, val: int) -> ListNode:
head_holder = ListNode(0) # the value does not matter
head_holder.next = head

cur = head_holder
while cur.next:
if cur.next.val == val:
cur.next = cur.next.next
else:
cur = cur.next

return head_holder.next

Leetcode題解 Python:Intersection of Two Linked Lists

給兩串 ListNode,回傳 Intersection 的第一個點,如果沒有則回傳 None。

用 memo(hashset) 去紀錄 headA 所有走過的節。再換 headB 走,如果該節在 memo 裡,就是第一個交會點。

class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
memo = set()
PointerA, PointerB = headA, headB
while PointerA:
memo.add(PointerA)
PointerA = PointerA.next
while PointerB:
if PointerB in memo: return PointerB
PointerB = PointerB.next
return None

有效歸有效,不過會消耗掉大量記憶體,在一個提示空間複雜度 O(1) 之下,這方法似乎不是最好的。

思考一下,如果要第一個交會,那麼兩者行走距離應該是最短的。但是這樣怎麼寫?

[左1右1]、[左1右2, 左2右1]、….。這樣的安排,太沒有效率了。

我一開始本來想用 traceback,這樣空間複雜度會超出 O(1),就沒有把它寫完了。


看了一下前段班,共同點就是 headA跑完換到headB、headB跑完換到headA。

為什麼可以這樣做?

如果有交會,headA = A part + C part、headB = B part + C part。

兩者一起往下跑,跑完換別的跑,會變成 headA->headB = A part + C part + B part + C part、headB->headA = B part + C part + A part + C part。

A part + C part + B part =  B part + C part + A part,於是兩者將會在 C part 交會。

這是一個循環的技巧應用,如果有想到,就會快很多。

class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
PointerA, PointerB = headA, headB
for _ in range(2):
while PointerA and PointerB:
PointerA = PointerA.next
PointerB = PointerB.next
if PointerA: PointerB = headA
else: PointerA = headB

while PointerA and PointerB:
if PointerA == PointerB: return PointerA
PointerA = PointerA.next
PointerB = PointerB.next
return None

Leetcode題解 Python:四月挑戰DAY6 Group Anagrams

Given an array of strings, group anagrams together.

Input: [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”],
Output:
[
  [“ate”,”eat”,”tea”],
  [“nat”,”tan”],
  [“bat”]
]

把文字做分類,anagrams 意思是「 a word, phrase, or name formed by rearranging the letters of another, such as cinema , formed from iceman.」(google翻譯)

可以透過移動字母,去拼成另外一個字詞。

於是兩詞若是同一個字謎,兩詞中字母出現的種類是相同的,且字母的出現次數是相同。

要保留這兩個數值,就不能破壞詞的長度,於是去掉 set,接下來想到的是 sorted() ,同一字謎的詞都會跑出相同結果。


不過 sorted() 回傳的類型是 list,list是無法作為 set 或是 dict 的key。用 “”.join() 或是 tuple轉換後就可以當作key。

確定了鍵從何而來 ,該鍵的值要保存同一字謎的詞,所以是 dict(),dict[key] : list(str)。

class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
memo = dict()
for word in strs:
anagram = "".join(sorted(word))
if anagram not in memo: memo[anagram] = [word]
else: memo[anagram].append(word)
return memo.values()

這種寫法跟前段班有明顯的差距,前段班不外乎使用 defaultdict()

class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
import collections
memo = collections.defaultdict(list)
for word in strs:
memo[tuple(sorted(word))].append(word)
return memo.values()

就效率上,使用模組是有幫助的,不過 Leetcode 沒有寫到是否可以使用,或者是能夠載入甚麼模組,對我來說,能夠不用就不用到。如果一定要用的部分,像是取隨機值,才會使用。

解過多次題目之後發現,如果是可用的模組,不使用import也可以直接使用。

Leetcode題解 Python: Linked List Cycle I & II

來到了 Linked List 章節,很快就遇到這個問題,問你ListNode是否有裡面有循環。

此題位於子章節 Two Pointer Technique,所以我不用直覺想到的 memo 紀錄步驟的遞迴解法。

示範提到,使用兩個 Pointer,一個快一個慢,快的一次走兩步,慢的走一步。

兩個 Pointer 的起點都是 head,如果有循環,Fast Pointer 遲早會追上 Slow Pointer

class Solution:
def hasCycle(self, head: ListNode) -> bool:
if not head: return head
fastPointer, slowPointer = head, head
while 1:
# FastPointer go 2 steps forward
for _ in range(2):
if fastPointer.next:
fastPointer = fastPointer.next
if fastPointer == slowPointer:
return True
else:
return False
# SlowPointer go 1 steps forward
for _ in range(1):
slowPointer = slowPointer.next


這種寫法可以減少記憶體使用,空間複雜度為 O(1)。

另外一種用 memo 紀錄步驟的方法。

紀錄步驟是需要用到不會重複的值,如果只是 val,除非保證不會有相同 val,不然可能會出錯。

最保險起見的方法,是使用記憶體位置,作為是否曾經走過的依據。

幸好物件可以直接加入set,而物件在set中做為比較的數值是記憶體位置。

class Solution:
def hasCycle(self, head: ListNode) -> bool:
if not head: return head
memo = set()
while head.next:
if head in memo: return True
memo.add(head)
head = head.next
return False

我在想,如果要得加入一個有用的變數在裡面,到底要怎麼加?這才是勾起我的興致的發想。 想出來了,硬是要在裡面加入的 steps 紀錄步數。

class Solution:
def hasCycle(self, head: ListNode) -> bool:
if not head: return head
fastPointer, slowPointer = head, head
steps = 0
while fastPointer.next:
fastPointer, steps = fastPointer.next, steps+1
if fastPointer == slowPointer: return True
if steps%2==1: slowPointer = slowPointer.next
return False

接著看到 II,這次要回傳循環起點,如果用 memo 紀錄,這個問題只是將回傳False改成當前的節點。

class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
if not head: return head
memo = set()
while head.next:
if head in memo: return head
else: memo.add(head)
head = head.next
return None

如果用快慢指針來解要怎麼辦?在兩者相遇之後,把慢指針移回原點,快慢指針每次都指往前一步,就會在循環起點相遇。

解釋:快2步,慢1步,當慢指針到了循環起點時,快指針在循環中也走了循環起點距離。而兩者的所剩距離是 循環長度 – 循環起點距離。
因此,慢指針往前「循環長度 – 循環起點距離」,兩者就會在循環內的 (循環長度 – 循環起點距離)位置上相遇。
此時把慢指針挪回起點,慢快指針同時走一步,前進 循環起點距離 步時,慢指針會來到 循環起點, 快指針會來到 循環長度 的位置,也是循環起點,兩者在此相遇。

手稿示意圖
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
if not head: return head
FastPointer, SlowPointer = head, head
while 1:
# SlwoPointer go 1 step forward
SlowPointer = SlowPointer.next
# FastPointer go 2 steps forward
if FastPointer.next and FastPointer.next.next:
FastPointer = FastPointer.next.next
if FastPointer == SlowPointer: break
else:
return None
# FP SP meet in cycle(CycleLength - pos)
SlowPointer = head
while 1:
if FastPointer == SlowPointer: return FastPointer
SlowPointer = SlowPointer.next
FastPointer = FastPointer.next

Leetcode題解 Python: Palindrome Pairs

給一串文字串列,問說哪些兩個組合可以變成 Palindrome 回文。

這兩兩組合會有前後順序的差異,如果用最直觀的暴力破解法,跑遍每個組合,時間複雜度會是 O(n*(n-1)),O(n**2)。

列在 trie 的範圍內,這提示我們要使用 trie 去建立搜尋樹。要如何建立能找出回文的 trie ?

想一下回文的特性,如果是回文,頭跟尾會是相等的,頭跟尾往中間移動的字也會兩兩相等。

abcd|dcba, s|lls, lls|sssll => abcd|abcd., s|s.ll, lls|lls.ss

如果用顛倒的字詞建立 trie ,在搜尋過程中就可以前尾兩兩對消,如果過程找到字詞,只要確保「 . 」之後找到底也是回文的就可以。

aa|a => a.a|a.

萬一輸入的字詞找完之前,就先找到比較短的顛倒字詞,也代表後面不會再有字可以添加,先後對消後,只需要針對搜尋詞還沒找部分做回文判斷即可。
class Solution:

    def palindromePairs(self, words):
result = []
if words:
trie = {} #反向
# create Trie
n = len(words)
for i in range(n):
cur = trie
for char in words[i][::-1]:
if char not in cur:
cur[char] = {}
cur = cur[char]
cur['end'] = i
# 檢查對稱
def isSymmetry(word):
n = len(word)
for i in range(n//2):
if word[i] != word[n-1-i]: return False
return True
#
def search1(idx, temp, cur):
"""搜尋當前字詞"""
if temp:
# 如果過程中找到字詞,檢查當前的剩餘部分是否回文
if 'end' in cur and isSymmetry(temp) and idx != cur['end']:
result.append([idx, cur['end']])
if temp[0] in cur:
search1(idx, temp[1:], cur[temp[0]])
else:
search2(idx, "", cur)

def search2(idx, temp, cur):
"""搜尋剩餘部分是否回文"""
for key in cur:
if key == "end":
if isSymmetry(temp) and idx != cur['end']:
result.append([idx, cur['end']])
else:
search2(idx, temp+key, cur[key])
#
for i in range(n):
search1(i, words[i], trie)

return result

在效率上還是太慢,我搜尋是用更廣的對稱,涵蓋更多的可能性。看了一下前段班的寫法,去找更好的思路。

如果在搜尋上,是把字文拆出「可能」組合成回文的部分搜尋,就能提升效率,但我那時有思考到這種方式,但沒有寫想到要怎麼寫,因為我trie總是用成一層層,所以被侷限了。

字詞可以整個直接當成字典的鍵使用,該鍵的值就是位置。

像是 abcd,如果要拆成回文形式,就得找出左右之分。

abcd 前組後:
abcd | “” => “”是回文 => dcba(前面部分反序) => [0,1] #前組後
abc | d => d是回文 => cba => not found
ab | cd => cd不是回文 => X
a | bcd => cbd不是回文 => X
“” | abcd => abcd不是回文 => X

換個字詞

s 前組後:
s | “” => “”是回文 => s => [3,3] => 不合規則
“” | s => s是回文 => “” => not found

lls 前組後:
lls | “” => “”是回文 => sll => not found
ll | s =>> s是回文 => ll => not found
l | ls => ls不是回文 => X
“” | lls => lls不是回文 => X

這樣的搜尋只有「前組後」,沒有考慮到「後組前」,要考慮 後組前 只要把字詞反序做搜尋。
lls 後組前:
sll(字詞反序) | “” => “”是回文 => lls => [2,2] => 不合規則
sl | l => l是回文 => ls => not found
s | ll => ll是回文 => s => [3,2] #後組前
“” | sll => sll不是回文 => X

這樣搜尋速度快不少,不過會有答案會重複,所以要想辦法去掉。

class Solution:
def palindromePairs(self, words):
table = dict(map(reversed, enumerate(words)))
res = set()
for i, word in enumerate(words):
for k in range(len(word)+1):
a = word[:k]
b = word[k:]

if a == a[::-1] and table.get(b[::-1], i) not in [-1, i]:
res.add((table.get(b[::-1], -1), i))

if b == b[::-1] and table.get(a[::-1], i) not in [-1, i]:
res.add((i, table.get(a[::-1], -1)))

return list(res)

Leetcode題解 Python: Word Search II

Given a 2D board and a list of words from the dictionary, find all words in the board.

很像找詞遊戲,看看版面上有沒有要搜尋的詞。不過跟一般的規則不同的是,它可以轉彎。

此題列在 trie 章節內,所以自然而然將目標字詞寫入 trie 內,再用 y x 遍歷整個版面,把board[y][x]放入搜尋就可以了。

重複出現的字詞只算一個,所以找到的字詞放入 set 或是 dict當key,最後再取出來。

這題我是很快就解出來的,關鍵之處不是如何把詞放入 trie,而是搜尋要如何進行。

如果學過回溯法,應該很快就知道找路徑的寫法是該怎麼進行。


當遍歷整個版面,取得 y x,如果 board[y][x] 在 trie 內,就繼續往四週找,看四週的字是否有出現下一層。

因為限制不能回頭,所以用一個 memo 去紀錄步驟(座標),接下來就避免重複尋找已經走過的路徑。(if (y,x) in memo)

回溯法的關鍵寫法:是在往下遞迴之前才加入步驟,在遞迴結束後要刪除之前加入的步驟。這就像是回到上一步,再往下一個方向嘗試。

最終會將全部可走的都走過,再一一收回步驟,因此當搜尋完畢之後 memo 會是空的。

class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
if board and board[0]:
rows, cols = len(board), len(board[0])
# create trie
trie = {}
for word in words:
cur = trie
for char in word:
if char not in cur:
cur[char] = {}
cur = cur[char]
cur['end'] = word
#
memo = {*""}
findWords = {*""}
def search(y, x, level):
if board[y][x] in level:
level = level[board[y][x]]
if 'end' in level:
findWords.add(level['end'])
if y-1 >= 0 and (y-1,x) not in memo:
memo.add((y,x))
search(y-1, x, level)
memo.discard((y,x))
if x-1 >= 0 and (y,x-1) not in memo:
memo.add((y,x))
search(y, x-1, level)
memo.discard((y,x))
if y+1 < rows and (y+1,x) not in memo:
memo.add((y,x))
search(y+1, x, level)
memo.discard((y,x))
if x+1 < cols and (y,x+1) not in memo:
memo.add((y,x))
search(y, x+1, level)
memo.discard((y,x))

for y in range(rows):
for x in range(cols):
memo.clear()
search(y, x, trie)

return list(findWords)

Leetcode題解 Python: 四月挑戰DAY5 Best Time to Buy and Sell Stock II

給一串價格數列,要求出最大利潤(先買後賣)為多少?

這題沒有涵蓋手續費,所以變得超簡單。

在價格遞增的時候不斷獲利,在價格遞減的時候不出手。所謂獲利就是一買一賣且賺錢的。

爬到價格巔峰的過程,每個階梯的高度加總就是最高到最低的差距,於是只要不斷地後減前再加到獲利。

當價格走跌時,後減前就變成負值,如果是負值就不做買賣,收益為 0。

整理之後,後減前只要 > 0 就加入,< 0 就跳過。

判斷相當簡單,於是可以濃縮成一句就解決掉了:

class Solution:
def maxProfit(self, prices: List[int]) -> int:
return sum([max(prices[i]-prices[i-1], 0) for i in range(1,len(prices))])


不過真的要買賣,當然是減少進場次數,買要判斷沒有持有,賣要判斷有持有才能進行。
(真實買賣的情況下,減少進場次數是方向,不管有沒有持有,都可以買賣。

class Solution:
def maxProfit(self, prices: List[int]) -> int:
profit = 0
if prices:
isBuy, cost = 0, 0
lastPrice = prices[0]
for i in range(1, len(prices)):
nowPrice = prices[i]
if lastPrice <= nowPrice and not isBuy:
cost = lastPrice
isBuy = 1
elif lastPrice > nowPrice and isBuy:
profit += lastPrice - cost
isBuy = 0
lastPrice = nowPrice
if isBuy: profit += lastPrice - cost

return profit

Leetcode題解 Python: 四月挑戰DAY4 Move Zeroes

Given an array nums, write a function to move all 0’s to the end of it while maintaining the relative order of the non-zero elements.

簡單來說,把數列中的零移到最後方,其他各數字的順序維持不變。

依舊很簡單,不過題目有要求:in-place。在數列直接修改,試著不用其他空間或是複製排好的數列。

如果用 for 且順序,那麼遇到0使用 append( pop() )會出錯。詳情不寫,改用倒敘法,這樣遇到 0 就能使用append( pop() )。

一次取出一次加到最後面,嫌太麻煩?不如用個 空串列 保存 pop() 出來的 0,最後再 extend() 保存 0 的串列即可恢復數列。


不過這樣沒有順著題目所期望的解法前進,用了額外的空間保存數列。

A two-pointer approach could be helpful here. The idea would be to have one pointer for iterating the array and another pointer that just works on the non-zero elements of the array.

這裡說用兩個指針,一個是迭代數列,另一個針對非0的元素。

針對迭代數列不用多心,要跑完數列自然就會用上。另外一個針對非0元素的要怎麼寫?

說到要針對非0元素,想必就要使用 if 去區分出來。遇到不是0的要怎麼處理?遇到0的又要怎麼處理?

如果要把 0 移動到最後方,路線有兩種:
1. 把0元素移動到最後方。
2. 把非0元素往前移動到最前方。

說到移動,就是把元素兩兩交換。交換就是一種 in-place 的作法,那要如何交換呢?

這裡使用 順序 去迭代數列,由前而後,所以被往前換的數字是不會再被迭代到而換到。

因此選擇把非0元素往前換,接下來只要不要動到換去前面的非0元素順序,最後就能把 0 都移動到最後方。

往前換,要往前多少?

從頭模擬起,如果一開始沒有碰到 0,就不需要換。接下來要是碰到0,也不用交換。在 0 之後又碰到非0元素,要往前換,要跟前面的0交換。

在遇到非0元素前,碰到一次0,跟前一格(0)換,碰到兩次0,跟前兩格的0交換。推理出,往前交換的位置跟碰到 0 次數是相同的。

碰到 0 ,指針 +1 。碰到 非0 ,就交換。132

class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
zeroPointer = 0
for i, num in enumerate(nums):
if num == 0:
zeroPointer += 1
else:
nums[i], nums[i-zeroPointer] = nums[i-zeroPointer], nums[i]

以下是我看其他大神的最貼切題目要求的寫法。

class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
last_nnz = 0
for i in range(len(nums)):
if nums[i] != 0:
nums[last_nnz], nums[i] = nums[i], nums[last_nnz]
last_nnz += 1

Leetcode題解 Python: 四月挑戰DAY3 Maximum Subarray

給一串數列,要找出當中各連續部分(contiguous subarray)的最大總和為何。

數字的範圍是甚麼?整數,包含正負整數。

按照題目規則,先用暴力破解法的思維:找出所有連續子串的總和,再從中找出最大值即可。
這樣時間複雜度會是 O(n(n-1)/2) → O(n**2)。

至少能夠先有一個可行的方法,接著繼續思考有沒有比 O(n**2) 更少次的方法?

Follow up:
「If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.」

存在著 O(n) 的方法,不過題目希望能夠拆散數列再各個處理的方式(分治法)。


先提 O(n) 的方法。

既然是 O(n),架構通常是一個「 for in 」,先寫上去再思考就好。需要回傳最大總和,所以可以命名一個 largestSum,最後 return largestSum。

如果要跑完一回就能找到答案,得在過程中記錄甚麼,或是判斷甚麼。

觀察一下數列,思考:遇到正的就累加起來,如果數列都是正整數,加完就是最大總和。那遇到負的部分呢?

這裡想到了累加,所以命名一個 accu 的變數,存放當前累加的結果。

如果遇到負整數,會使得當前累加會減少,所以先把當前的累加值保存下來,然後再把數字加上去。

將下來再做一些細部條件判斷,就能完成 O(n) 的解法。

class Solution:
def maxSubArray(self, nums: List[int]) -> int:
largestSum = nums.pop(0)
nowAccu = largestSum
for num in nums:
if num < 0:
if nowAccu < 0:
largestSum = max(largestSum, num)
else:
largestSum = max(largestSum, nowAccu)
nowAccu += num
else:
if nowAccu < 0: nowAccu = num
else: nowAccu += num
largestSum = max(largestSum, nowAccu) #最後一項
return largestSum

至於分治法要怎麼寫?覺得麻煩就跳過。目前的概念是分成正負組合,再處理成各區域最大總和,從中回傳最大值。

Leetcode題解 Python: 四月挑戰DAY2 Happy Number

給一個整數,按照十進位分切出各單個數字,這些數字的平方加總後,繼續分切…,如果最後總合等於 1,就是快樂數字。

好吧,到底哪裡快樂我也不知道。這依舊是一個非常簡單的題目。

可以分成三個步驟:
1.分切數字
2.數字平方和
3.判斷為1,否則回到步驟1.

要是碰到無限循環怎麼辦?把出現的數字記錄下來,只要有數字重複出現那就是無限循環。

用一個 memo ,字典或是集合(set),之後用 in 就能看出是否有重複出現過。

還有更偷機的做法,就是把False的數字回收進memo。這樣會相當Happy。

class Solution:
def isHappy(self, n: int) -> bool:
memo = {2: False, 3: False, 4: False, 5: False, 6: False, 8: False, 9: False,
11: False, 12: False, 14: False, 15: False, 16: False, 17: False, 18: False,
20: False, 21: False, 22: False, 24: False, 25: False, 26: False, 27: False,
29: False, 30: False, 33: False, 34: False, 35: False, 36: False, 37: False,
38: False, 39: False, 40: False, 41: False, 42: False, 43: False, 45: False,
46: False, 47: False, 48: False, 50: False, 51: False, 52: False, 53: False,
54: False, 55: False, 56: False, 57: False, 58: False, 59: False, 60: False,
61: False, 62: False, 63: False, 64: False, 65: False, 66: False, 67: False,
69: False, 71: False, 72: False, 73: False, 74: False, 75: False, 76: False,
77: False, 78: False, 80: False, 81: False, 83: False, 84: False, 85: False,
87: False, 88: False, 89: False, 90: False, 92: False, 93: False, 95: False,
96: False, 98: False, 99: False, 106: False, 113: False, 117: False, 128: False,
145: False, 162: False}
if n in memo: return False
num2 = 0
while n>1:
if n in memo: return False
else: memo[n]=None
while n>0:
num2 += (n % 10)**2
n = n//10
n, num2 = num2, 0
return True

如果用字典是比較容易去偷機,如果不偷機,用集合會比較好增加資料(可以少打幾個字)。

class Solution:
def isHappy(self, n: int) -> bool:
memo = {*''}
num2 = 0
while n>1:
if n in memo: return False
else: memo.add(n)
while n>0:
num2 += (n % 10)**2
n = n//10
n, num2 = num2, 0
return True

Leetcode題解 Python: Largest Rectangle in Histogram

直方圖中最大的長方形面積

這題感到棘手,寫好的暴力破解法會超時,只能不斷看題目去領悟點甚麼。

找出最大面積不是件難事,要怎麼才能加速呢?

看了搜尋的兩前項,別人寫著遞增數列,找局部峰值,為什麼?到底有甚麼關係?

我簡單一句講明目的:「把這張直方圖分切出各個好處理的部分。」


在這個遞迴II章節,有一個部分是在說到把問題切碎,處理後再重組,得到答案。這就是需要這樣做的例子。

直方圖分解示意圖

把局部峰值也就是左大右小之處,切下去,左邊的值會遞減。得到一個遞增數列再往右搜尋。

碰到下一個峰值,往左遇到實際較高的直方,因為它已經被切進上一個遞增數列,所以可以忽視掉突出部分,維持一樣的值繼續遞減。

一旦變成遞增數列就可以很快速地找到最大面積。

把直方圖的每直方看成座標,遞增數列的右下角為原點,找出端點x和y絕對值相乘最大的就是該部分最大面積。

得到各部分的最大面積,再相互比較,就能得到直方圖中最大的長方形面積。

class Solution:
"""遞增數列"""
def largestRectangleArea(self, heights) -> int:
if not heights: return 0
peaks = []
p = None
for idx, each in enumerate(heights):
if p and each < p:
peaks.append(idx-1)
p = each
peaks.append(n-1)

maxAreas = []
while peaks:
# 從右而左搜尋
endpoint = peaks.pop()
height = heights[endpoint]
maxArea = 0
for i in range(endpoint,-1,-1):
width = endpoint+1-i
height = min(height, heights[i])
maxArea = max(maxArea, width*height)
maxAreas.append(maxArea)
return max(maxAreas)

先找峰值,紀錄峰值出現在哪,最後一個也是峰值,最後從記錄找起一次把各種切出遞增數列找出最大長方形面積,再得到當中的最大值。

接下來就是優化了,對於一個 N 個數列,首先先減少遍例次數,求各遞增數列的最大面積可以放在找峰值的時候進行。

而且如果左右兩個點一樣高,可以忽略右邊的不計,減少要找的端點。在遍歷各端點時,如果剩下的最大可能面積沒辦法比較大,就可以跳出迴圈。

經過一番優化之後,就變成這樣。

這讓我深刻體會到,unfold 的好處在哪裡。如果沒有標上註解,可讀性遠遠不如上一個。

class Solution:
"""遞增數列"""
def largestRectangleArea(self, heights) -> int:
if not heights: return 0
def getMaxArea():
area = 0
endpoint, height = hArray.pop()
for i in range(len(hArray)-1,-1,-1):
idx, h2 = hArray[i]
width = endpoint+1-idx
height = min(height, h2)
area = max(area, width*height)
if (endpoint+1)*height <= area:break
return area


n = len(heights)
hArray= []
maxAreas = []

for idx, h in enumerate(heights):
if idx:
if h < preH: #出現峰值,即刻求該遞增數列的最大面積
# 添加EndPoint
hArray.append((idx-1, preH))
# 搜尋當前面積
maxAreas.append(getMaxArea())
# 砍掉前面鋒值凸出的部分,最後要添加端點紀錄修改
stack = [idx, h]
while hArray and hArray[-1][1] > h:
stack[0] = hArray[-1][0]
del hArray[-1]
hArray.append(stack)
elif h > preH: #紀錄遞增變化
hArray.append((idx, h))
else:
hArray.append((idx, h))
preH = h
# 最後一搜,其中一個為最後一直方的左上端點 另一個是右上
hArray.extend([(idx, h), (idx, h)])
maxAreas.append(getMaxArea())
return max(maxAreas)

Leetcode題解 Python: Generate Parentheses

「Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.」

給一個數字 n ,要產生有 n 個左右成對「 ( ) 」的所有組合 。

說實在並不難,如果有想到左右成對的規則下,任何位置的左邊的 ( 數量一定得大於等於 ),不然會無法成對。

利用這特性,很快就可以寫出一個遞迴函數。

class Solution:
def generateParenthesis(self, n):
result = []
def nextIdx(ln, rn, text):
if ln == 0 and rn == 0:
result.append(text)
if ln - 1 >= 0:
nextIdx(ln-1, rn, text+"(")
if rn - 1 >= ln:
nextIdx(ln, rn-1, text+")")
nextIdx(n, n, "")
return result

這是最直觀的寫法,速度也不錯。


換另外一種寫法 BackTracking。

class Solution:
def generateParenthesis(self, n):
"""BackTracking"""
result = []
path = []
def nextIdx(ln, rn):
if ln == 0 and rn == 0:
result.append("".join(path))
if ln - 1 >= 0:
path.append("(")
nextIdx(ln-1, rn)
del path[-1]
if rn - 1 >= ln:
path.append(")")
nextIdx(ln, rn-1)
del path[-1]

nextIdx(n, n)
return result

這種寫法就慢了一點,畢竟不會碰到下一步不合規則的判斷,如果真的盲選 ( 或 )寫入再判斷,應該只會更慢。

這題在子章節「unfold」,意思是不要寫得太精簡難懂。順著前進時,已經很習慣並不會覺得異樣,如果要倒退著,跟往前相比,就會覺得有點費力。

在前幾題有個求 1, 2, 3, …, N個數有 K 種組合情形。那題使用尾呼叫會比較快速,這題也來嘗試使用那種模式回答。

class Solution:
def generateParenthesis(self, n):
"""尾呼叫"""
def nextIdx(ln, rn, text):
res = []
if ln == 0 and rn == 0:
res.append(text)
return res
if ln - 1 >= 0:
res.extend(nextIdx(ln-1, rn, text+"("))
if rn - 1 >= ln:
res.extend(nextIdx(ln, rn-1, text+")"))
return res
return nextIdx(n, n, "")