Python 技巧與慣用寫法

Language ToolkitPriority 4 of 5 — High value — a gap here costs you roundsHigh value 更新於 Sep 18, 2026
Section priorityPriority 5 of 5 — Must know — expect it in almost every loopMust knowPriority 4 of 5 — High value — a gap here costs you roundsHigh valuePriority 3 of 5 — Worth knowing — usually a variant of a must-know patternWorth knowingPriority 2 of 5 — Niche — read once, revisit only if a company is known to askNicheMarked on the sections that carry it — unmarked sections are background/reference.

範圍 — 解題過程中會用到的 Python 語言慣用寫法 — 內建操作的成本、複製、字串處理、排序 key、整數運算、生成式(comprehension)與作用域 — 依「你想做什麼」來分組。標準函式庫與索引運算另有專屬速查表。 另見python_trick_stdlib.mdheapqbisectSortedDictcollectionsitertoolsfunctoolspython_trick_indexing.md — 插入、切片(slicing)以及背後的差一(off-by-one)運算;python_gotchas.md — 那些「令人意外」而不只是「好用」的行為;complexity_cheatsheet.md — 同一份 Big-O 查表,但對象是資料結構與經典演算法而非 Python 內建;java_trick.md — 同一片領域的 Java 版。

LeetCode 題目清單

總覽

這份速查表以前是 3,672 行、全部塞在單一個 ## 1) Examples 標題底下,68 個條目以 0-1)1-11''')1-27-3) 這種毫無規律的編號排列。編號已經拿掉了; 請用「它在做什麼」來找東西。

我想要… 前往
知道某個內建操作要花多少成本 — lenmaxin+split、切片、dict/set 操作 常見操作的複雜度
複製一份 list 或 dict,而且不是別名 複製與參考
對字串做切片、補零、去空白、切分或重組 字串
用自然順序以外的規則排序 排序與比較
做除法、取整、取餘數,或避開溢位的意外 數字與數學
同時走訪兩個東西,或用一行建出一個 list 走訪、生成式與函數式工具
計數,或用預設值取代 KeyError Dict 與 Set,或 python_trick_stdlib.md 裡的 Counter / defaultdict
在巢狀函式裡面寫入外層變數 結構、作用域與回傳值
用堆積(heap)、二分搜尋、有序 map 或 itertools python_trick_stdlib.md
插入 list、切出子陣列,或把差一算對 python_trick_indexing.md

常見操作的複雜度 Priority 5 of 5 — Must know — expect it in almost every loop

這一節為什麼存在:幾乎每一次「你的解法是 O(n²),能更快嗎?」的時刻,都來自把某個內建操作 當成了 O(1)。len() 確實是免費的;x in my_listres += charr[1:] 都不是。

n = 被操作物件的大小,m = 另一個運算元的大小,k = 切片的大小。

內建函式

操作 時間 空間 為什麼(底層)
len(x) O(1) O(1) 不是走一遍 — 長度就存在物件標頭的欄位裡(list/tuple/str 的 ob_size、dict 的 ma_used、set 的 used
max(it) / min(it) O(n) O(1) 掃一遍,途中保留目前最佳值
max(it, key=f) O(n · f) O(1) f每個元素呼叫一次(Schwartzian transform),不是每次比較呼叫一次
sum(it) O(n) O(1) 線性掃描。⚠️ sum(list_of_lists, [])O(n²) — 每個 + 都建一個新 list
any(it) / all(it) 最壞 O(n) O(1) 遇到第一個 True / False短路離開 — 最好情況 O(1)
sorted(it) / list.sort() O(n log n) O(n) / O(n) Timsort。sorted() 還要另外配置新 list;.sort() 是原地的,但合併時仍需暫存緩衝區
reversed(seq) O(1) O(1) 回傳的是惰性迭代器 — 什麼都沒複製。對照 seq[::-1],那是 O(n) 時間加上 O(n) 空間
enumerate(it) / zip(*its) 建立是 O(1) O(1) 惰性迭代器;O(n) 是由把它走完的那個迴圈付的
list(it) / set(it) / dict(it) O(n) O(n) 把迭代器實體化
abs()divmod()ord()chr() O(1) O(1) 面試規模的整數就是單一機器字的運算
pow(a, b, mod) O(log b) O(1) 平方-相乘法。⚠️ (a ** b) % mod 不一樣 — 它會先把完整的 a**b 算出來
str(n) / int(s) O(d) O(d) d = 位數。對超大整數是超線性的(CPython 3.11+ 甚至預設上限 4300 位)
bin(n) / hex(n) O(log n) O(log n) 每個 bit / nibble 一個字元

list — 一個指標的動態陣列

CPython 的 list 是一塊連續的 PyObject ** 記憶體,外加長度與容量。不管裝什麼,每個元素都佔一個 指標寬度 — 這就是為什麼索引是 O(1),也是為什麼往中間插入必須把後半段 memmove 一次。

操作 時間 空間 為什麼(底層)
l[i]l[i] = v O(1) O(1) 在那塊記憶體上做指標運算
l.append(x) 攤還 O(1) O(1) 以幾何倍率預留空間(約 1.125×),所以重新配置雖是 O(n),但只發生 O(log n) 次
l.pop() O(1) O(1) 丟掉最後一個指標
l.pop(0)l.insert(i, x)del l[0] O(n) O(1) 洞後面的東西全部 memmove。⚠️ 這就是經典的 BFS 效能 bug — 請用 collections.deque
l.remove(x) O(n) O(1) 先線性找到它,再做上面那次搬移
x in ll.index(x)l.count(x) O(n) O(1) 逐一用 == 比對。⚠️ 這是最常見的「不小心變成 O(n²)」
l[i:j](切片) O(k) O(k) k 個指標複製到一個新的 list
l[::-1] O(n) O(n) 產生新 list。只是要走訪的話請用 reversed(l)
l1 + l2 O(n + m) O(n + m) 產生新 list。⚠️ 在迴圈裡用 += 是 O(n²);改用 append
l * k O(n · k) O(n · k) ⚠️ [[0] * n] * m 會共用同一個列物件 — 見 2D array initialization
l[:] / l.copy() / list(l) O(n) O(n) 淺複製 — 複製 n 個指標,不是複製物件本身
copy.deepcopy(l) O(全部節點) O(全部節點) 走遍整張物件圖,並用一個 memo dict 處理共用參考
l.sort() O(n log n) O(n) Timsort;已排序的輸入是 O(n)(它會偵測既有的遞增段 run)
l.reverse() O(n) O(1) 原地交換指標
text
list = [10, 20, 30]           capacity 4, size 3

  ob_size = 3   ─────────────► len() reads this, O(1)
  ob_item ──► [ *10 | *20 | *30 | ___ ]     <- contiguous pointers
                 ^                   ^
                 l[0] is one         spare slot from the
                 offset away         geometric over-allocation
                 -> O(1)             -> append() is amortized O(1)

  l.pop(0)  ->  [ *20 | *30 | ___ | ___ ]   <- everything memmove'd left: O(n)

str — 一個不可變的碼位陣列

字串是不可變的,所以每一次「修改」都是配置一個新字串並複製過去。CPython 以精簡格式儲存 (PEP 393:依最大碼位決定每字元 1、2 或 4 bytes),所以 s[i] 仍然是 O(1) 索引,而不是走一遍 UTF-8。

操作 時間 空間 為什麼(底層)
len(s)s[i] O(1) O(1) 長度在標頭裡;碼元寬度固定
s1 + s2 O(n + m) O(n + m) 配置新緩衝區,把兩邊 memcpy 過去。⚠️ 在迴圈裡 s += chO(n²)
"".join(parts) O(總長度) O(總長度) 兩趟:先加總長度,只配置一次,再逐段 memcpy。這就是 join 贏過 += 的原因
s[i:j] O(k) O(k) 新的字串緩衝區
s[::-1] O(n) O(n) 新字串
s[:i] + ch + s[i+1:] O(n) O(n) 「換掉一個字元」的慣用寫法會重建整個字串 — 做一次沒問題,放在迴圈裡就是 O(n²)
sub in ss.finds.index 最壞 O(n · m),一般約 O(n) O(1) 帶 Bloom filter 的 Boyer–Moore–Horspool;CPython 3.10+ 對較長的樣式改用 two-way 演算法(O(n + m))
s.replace(a, b)s.count(a) 最壞 O(n · m) O(n) 先做上面的搜尋,再建一個新字串
s.split(sep) O(n) O(n) 掃一遍;結果所有片段加起來共 n 個字元
s.strip()s.lstrip() 最壞 O(n) O(n) 只掃兩端,但仍然回傳新字串
s.lower()s.upper() O(n) O(n) 新字串
sorted(s) / "".join(sorted(s)) O(n log n) O(n) 字母異位詞 key 的慣用寫法(LC 49)
s * k O(n · k) O(n · k)
hash(s) 第一次 O(n),之後 O(1) O(1) hash 要掃過所有 bytes,算完後會快取在字串物件裡
text
res = ""
for ch in s:          #  n iterations
    res += ch         #  each one allocates a buffer of the current length
                      #  1 + 2 + 3 + ... + n  =  O(n^2) bytes copied

parts = []
for ch in s:
    parts.append(ch)  #  amortized O(1) each
res = "".join(parts)  #  ONE allocation of exactly n bytes  ->  O(n) total

CPython 確實有一個 += 的原地最佳化(當字串的 refcount 是 1 時可以 realloc 而不必複製), 有時候會把 O(n²) 藏起來。不要依賴它 — 只要有第二個名字指向這個字串就失效,而且 PyPy/Jython 上根本沒有。面試時就講 join

dictset — 開放定址的雜湊表

兩者都是開放定址表(碰撞時往下一個 slot 探測,沒有 bucket 串鏈)。3.6 之後的 dict 是分離式的: 一個稀疏的索引陣列,加上一個依插入順序排列的緊密 (hash, key, value) 條目陣列 — 順序保證與 省記憶體都是從這裡來的。

操作 時間(平均) 時間(最壞) 空間 為什麼(底層)
d[k]k in dd.get(k) O(1) O(n) O(1) hash → slot → 探測。最壞情況是每個 key 都碰撞(對抗式輸入)
d[k] = vs.add(x) 攤還 O(1) O(n) O(1) 超過負載係數就重新配置(dict 是 2/3 滿,set 是 3/5)— 重新配置會把所有 key 重新 hash,O(n)
del d[k]s.discard(x) O(1) O(n) O(1) 會留下一個 dummy 標記,免得後面的探測鏈斷掉
hash(key) O(key 的長度) O(1) ⚠️ 對字串或 tuple 當 key 而言 hash 是 O(len) 而非 O(1) — 用長度 L 的字串當 key,每次操作就是 O(L)
走訪 d / d.items() O(n) O(1) 走的是緊密條目陣列(view 本身的建立是 O(1))
走訪 set O(容量) O(1) set 沒有緊密陣列 — 走訪時連空 slot 也要走過
d.keys() | other、set 聯集 a | b O(n + m) O(n + m)
a & b(交集) O(min(n, m)) O(min) 走訪比較小的那個集合,到大的那個去探測
a - b(差集) O(n) O(n)
a ^ b(對稱差) O(n + m) O(n + m)
Counter(it)collections.defaultdict O(n) O(n) 同一種表,每個元素插入一次
max(d, key=d.get) O(n) O(1) 整個掃一遍 — 雜湊表裡沒有「最大 key」的捷徑

面試用的一句話版本:雜湊表是用 O(n) 空間換來 O(1) 的成員查詢。把 x in nums(list,O(n)) 換成 x in set(nums)(O(1)),就是多數暴力 O(n²) 解法塌縮成 O(n) 的關鍵 — 而 set(nums) 本身的建立也是 O(n)。

其他值得知道的容器

型別 操作 時間 為什麼
tuple t[i]len(t) O(1) 和 list 同樣的排版,只是沒有多餘容量 — 不可變,所以可雜湊
collections.deque append / appendleft / pop / popleft O(1) 由 64 格區塊串成的雙向鏈結串列 — 這就是 list.pop(0) 的解法
collections.deque 中間的 dq[i] O(n) 必須一塊一塊走過去 — deque 不是隨機存取的
heapq heappush / heappop O(log n) 在一個普通 list 上做二元堆積的上浮/下沉
heapq heapify(l) O(n) 由下而上下沉 — 不是 n 次 push。nlargest(k, it) 是 O(n log k)
bisect bisect_left / bisect_right O(log n) 在已排序 list 上做二分搜尋
bisect insort O(n) 找位置 O(log n),但搬移後半段是 O(n) — 這是陷阱
int a + ba - b O(d) 任意精度:以 30-bit 為一「位」,共 d 位。面試規模的整數視為 O(1)
int a * b O(d²) 直式乘法,約 70 位以上改用 Karatsuba。階乘/大次方類的題目才會有感
任何序列 a == b O(n) 先做 O(1) 的同一性與長度短路,再逐一元素比較

真的會在面試中扣分的陷阱

python
# 1) membership on the wrong container  ->  O(n^2) instead of O(n)
for x in nums:
    if x in seen_list:   # ❌ O(n) each
        ...
seen = set(seen_list)    # ✅ O(n) once, then O(1) each

# 2) building a string with +=  ->  O(n^2)
res = ""
for ch in s: res += ch          # ❌
res = "".join(ch for ch in s)   # ✅

# 3) list as a queue  ->  O(n) per dequeue
q = [root]; node = q.pop(0)             # ❌ O(n) memmove every pop
from collections import deque
q = deque([root]); node = q.popleft()   # ✅ O(1)

# 4) slicing inside recursion  ->  O(n^2) time AND O(n^2) space
def f(arr):
    return f(arr[1:])            # ❌ a full copy per level
def f(arr, i):
    return f(arr, i + 1)         # ✅ pass an index

# 5) sorting inside a loop  ->  O(n^2 log n)
for w in words: key = "".join(sorted(w))   # ⚠️ fine (sorts each word once)
for i in range(n): arr.sort()              # ❌ sorts the WHOLE array n times

# 6) recomputing max/min/sum over the same window
for i in range(n): cur = max(nums[i:i+k])  # ❌ O(n*k) -> use a monotonic deque, O(n)

# 7) `in` on a dict's items vs keys
if k in d:            # ✅ O(1) — hashes the key
if k in d.keys():     # ✅ O(1) too — a view, not a list
if k in list(d):      # ❌ O(n) — materialises the keys first

要講出口的話:先講容器,再講成本。seen 是一個 set,所以成員查詢平均是 O(1) — 這讓整個 迴圈是 O(n) 時間、O(n) 空間。」 講出容器是什麼,才讓你的複雜度宣稱是可被檢驗的,而這正是 面試官在等的那一句。

想看資料結構與經典演算法層級的同一份查表,見 complexity_cheatsheet.mdheapq / bisect / collections 的深入說明見 python_trick_stdlib.md

複製與參考

賦值 vs 淺複製 vs 深複製

text
# LC 138

#-------------------------------------------------
# CASE 1) assignment : point to the same instance
#-------------------------------------------------

In [112]: z = [1,2,3]

In [113]: x = y = z

In [114]: x
Out[114]: [1, 2, 3]

In [115]: y
Out[115]: [1, 2, 3]

In [116]: z
Out[116]: [1, 2, 3]

In [117]: z.append(4)

In [118]: z
Out[118]: [1, 2, 3, 4]

In [119]: x
Out[119]: [1, 2, 3, 4]

In [120]: y
Out[120]: [1, 2, 3, 4]

In [121]: z
Out[121]: [1, 2, 3, 4]


#-------------------------------------------------
# CASE 2) shallow copy : copy "parent" instance, but NOT sub instance
#-------------------------------------------------

# https://docs.python.org/zh-tw/3/tutorial/datastructures.html
# form 1
a.copy()

# form 2
a[:]

# demo
In [90]: x = [1,2,3]

In [91]: y = x[:]

In [92]: y
Out[92]: [1, 2, 3]

In [93]: x.append(4)

In [94]: x
Out[94]: [1, 2, 3, 4]

In [96]: y
Out[96]: [1, 2, 3]


# LC 77 Combinations
class Solution(object):
    def combine(self, n, k):
        result = []
        
        def dfs(current, start):
            if(len(current) == k):
                result.append(current[:])
                return
            
            for i in range(start, n + 1):
                current.append(i)
                dfs(current, i + 1)
                current.pop()
            
        dfs([], 1)
        return result

#-------------------------------------------------
# CASE 3) deep copy : copy "parent" instance, AND sub instance
#-------------------------------------------------

import copy

In [25]: import copy
    ...:
    ...: x = [1,2,3]
    ...: z =  copy.deepcopy(x)
    ...:
    ...: x
Out[25]: [1, 2, 3]

In [26]:

In [26]: z
Out[26]: [1, 2, 3]

In [27]: x.append(4)

In [28]: x
Out[28]: [1, 2, 3, 4]

In [29]: z
Out[29]: [1, 2, 3]

In [31]: z.append(5)

# NOTE : x, z NOT affect on each other

In [32]: z
Out[32]: [1, 2, 3, 5]

In [33]: x
Out[33]: [1, 2, 3, 4]

哪一種複製才會得到「獨立」的物件

問題是: 要怎麼複製,才能讓改動原本那個不會影響到複本(反之亦然)?

python
# ── The idiom you'll see everywhere (shallow copy of a list) ──
# NOTE !!! how we make copy in py
intervals_cache = intervals[:]        # copy 1) slice
intervals_cache = intervals.copy()    # copy 2) .copy()  (same effect)
intervals_cache = list(intervals)     # copy 3) list()   (same effect)

關鍵差異 — 取決於這個 list 是「扁平」還是「巢狀」:

python
#----------------------------------------------------------
# CASE A) FLAT list (ints, strings, …) → shallow copy IS enough
#----------------------------------------------------------
original = [1, 2, 3]
copy_    = original[:]         # independent copy

original.append(4)
original[0] = 99
print(original)   # [99, 2, 3, 4]
print(copy_)      # [1, 2, 3]     ← NOT affected  ✅

#----------------------------------------------------------
# CASE B) NESTED list (list of lists / objects) → shallow copy is NOT enough
#----------------------------------------------------------
original = [[1, 2], [3, 4]]
shallow  = original[:]        # copies OUTER list only; inner lists are SHARED

original.append([5, 6])       # outer-level change → safe
print(shallow)                # [[1, 2], [3, 4]]   ← NOT affected  ✅

original[0][0] = 99           # inner-level change → LEAKS through!
print(shallow)                # [[99, 2], [3, 4]]  ← AFFECTED  ❌

# → to be FULLY independent, use deepcopy:
import copy
deep = copy.deepcopy(original)
original[0][0] = -1
print(deep)                   # inner list unchanged  ✅

決策表 —「我要一份原件動不到的複本」

資料形狀 用什麼 獨立嗎?
x = y(賦值) ❌ 同一個物件,所有改動都會外洩
扁平 list [1,2,3] x[:] / x.copy() / list(x) ✅ 完全獨立
巢狀 list [[..],[..]] x[:](淺) ⚠️ 只有外層 — 內層會外洩
巢狀 list / dict / 物件 copy.deepcopy(x) ✅ 完全獨立
Dict(值是不可變的) d.copy() / dict(d) / {**d} ✅(值若可變則仍共用)

經驗法則

  • [:].copy()list()淺複製:只有在元素是不可變的 (int、str、tuple)或你只動最上層時才安全。
  • copy.deepcopy()深複製:任何巢狀都安全,但比較慢 — 只有在你真的會 修改巢狀元素時才用。

經典的回溯用法(見上面的 LC 77):result.append(current[:]) 會把當前路徑快照下來, 後續的 current.pop() / current.append() 就不會弄壞已存的結果 — 這能成立是因為路徑元素是不可變的 int。

字串

對字串的字元做排序

python
def _sort(x):
    _x = list(x)
    _x.sort()
    return "".join(_x)

x = "bca"
print (x)
x_ = _sort(x)
print (x_)

依索引取代某個字元(切片 + 串接)

重點:Python 的字串是不可變的 — 你不能s[i] = ch (會丟出 TypeError: 'str' object does not support item assignment)。 要「改掉索引 i 上的字元」,就用 i 前後的切片重組出一個字串。

python
# ── The idiom ──
#   s[:i]      -> everything BEFORE index i   (i excluded)
#   ch         -> the new character to place at index i
#   s[i+1:]    -> everything AFTER index i    (i excluded, i.e. drop old s[i])
new_s = s[:i] + ch + s[i+1:]
python
# demo
In [1]: s = "AACCGGTT"

In [2]: s[:3] + "X" + s[3+1:]     # replace index 3 ('C') with 'X'
Out[2]: 'AACXGGTT'

# original is untouched (immutable) — a NEW string is returned
In [3]: s
Out[3]: 'AACCGGTT'

為什麼是 i+1 s[i+1:]i 之後開始,所以舊字元 s[i] 被丟掉、 換成 ch。如果改用 s[i:],舊字元會被保留(變成插入而不是取代):

python
s = "abc"
s[:1] + "X" + s[1+1:]   # 'aXc'  ← REPLACE index 1  (skip old 'b')
s[:1] + "X" + s[1:]     # 'aXbc' ← INSERT before index 1 (old 'b' kept)

經典 LC 用法 — LC 433 Minimum Genetic Mutation(BFS,一次變異一個基因字元):

python
# for each position i, try each candidate char ch
for i in range(len(cur_gene)):
    for ch in "ACGT":
        if ch == cur_gene[i]:
            continue
        # build the neighbor gene with position i mutated to ch
        new_gene = cur_gene[:i] + ch + cur_gene[i+1:]
        ...

替代做法:如果你要改很多個位置,先轉成 listarr = list(s); arr[i] = ch; s = "".join(arr))— list 是可變的, 所以可以就地做索引賦值,也免去一再重組字串。 只改一個位置的話,上面的切片寫法最乾淨。

split

python
# python
# syntax : split(separator, number_of_split_result) 
# example 1
# In [17]: x = 'dig1 8 1 5 1'

# In [18]: x
# Out[18]: 'dig1 8 1 5 1'

# In [19]: x.split(" ")
# Out[19]: ['dig1', '8', '1', '5', '1']

# In [20]: x.split(" ", 1)
# Out[20]: ['dig1', '8 1 5 1']

# In [21]: x.split(" ", 2)
# Out[21]: ['dig1', '8', '1 5 1']

# In [22]: x.split(" ", 3)
# Out[22]: ['dig1', '8', '1', '5 1']

# In [23]: x.split(" ", 4)
# Out[23]: ['dig1', '8', '1', '5', '1']

# In [24]: x.split(" ", 100)
# Out[24]: ['dig1', '8', '1', '5', '1']

# example 2
# LC 937 Reorder Data in Log Files
class Solution:
    def reorderLogFiles(self, logs):
        def f(log):
            id_, rest = log.split(" ", 1)
            return (0, rest, id_) if rest[0].isalpha() else (1,)

        logs.sort(key = lambda x : f(x))
        return logs #sorted(logs, key = f)

把字串補零

python
# LC 67. Add Binary
#NOTE : zfill syntax
#    -> fill n-1 "0" to a string at beginning

#example :
In [10]: x = '1'

In [11]: x.zfill(2)
Out[11]: '01'

In [12]: x.zfill(3)
Out[12]: '001'

In [13]: x.zfill(4)
Out[13]: '0001'

In [14]: x.zfill(10)
Out[14]: '0000000001'

lstrip / rstrip / strip

python
# python
# syntax : s.lstrip(chars) / s.rstrip(chars) / s.strip(chars)
#   - lstrip : remove matching chars from the LEFT  (leading)
#   - rstrip : remove matching chars from the RIGHT (trailing)
#   - strip  : remove matching chars from BOTH ends
#   - `chars` is a SET of characters to remove (NOT a substring/prefix!)
#   - no arg -> strips whitespace (space, \t, \n, \r ...)
#   - it does NOT touch chars in the MIDDLE, and returns a NEW string
python
#----------------------------
# example 1 : strip leading zeros (lstrip)
#----------------------------
In [1]: x = "0000123"
In [2]: x.lstrip("0")
Out[2]: '123'

# no leading zeros -> unchanged (safe, no error)
In [3]: y = "123"
In [4]: y.lstrip("0")
Out[4]: '123'

# all chars match -> empty string (WATCH OUT!)
In [5]: "0000".lstrip("0")
Out[5]: ''

#----------------------------
# example 2 : rstrip (trailing)
#----------------------------
In [6]: "12300".rstrip("0")
Out[6]: '123'

In [7]: "hello!!!".rstrip("!")
Out[7]: 'hello'

# common: drop trailing newline / whitespace when reading input
In [8]: "  line\n".rstrip()
Out[8]: '  line'

#----------------------------
# example 3 : strip (both ends) + whitespace default
#----------------------------
In [9]: "  hi  ".strip()
Out[9]: 'hi'

In [10]: "xxhixx".strip("x")
Out[10]: 'hi'

#----------------------------
# example 4 : GOTCHA — `chars` is a char SET, not a prefix string
#----------------------------
# removes ANY leading char that is 'a','b', or 'c' (in any order),
# NOT the literal prefix "abc"
In [11]: "cabbage".lstrip("abc")
Out[11]: 'ge'          # 'c','a','b','b','a' all stripped, stops at 'g'

# to remove a real PREFIX/SUFFIX (py3.9+), use removeprefix / removesuffix:
In [12]: "test.py".removesuffix(".py")
Out[12]: 'test'
In [13]: "img_001".removeprefix("img_")
Out[13]: '001'

常見 LC 用法 — 組出數字之後把前導零去掉

python
# LC 402. Remove K Digits
# after building the result digits in a stack, the front may have leading zeros
# e.g. stack -> "0200"  (must return "200", and "" must become "0")

res = "".join(stack).lstrip('0')   # "0200" -> "200",  "0000" -> ""
return res if res else "0"         # handle the all-zero / empty case

經驗法則lstrip('0') 是把數字字串正規化(去掉前導零)的慣用寫法 — 但一定要處理空字串的結果(res or "0"), 因為 "0"/"0000" 會被剝成 ""。反向操作(補零)見 zfill

ord()chr()isalpha()isdigit()

python
#-------------------------------
# ord() : char -> ASCII int
# chr() : ASCII int -> char
#-------------------------------
print(ord('a'))   # 97
print(ord('A'))   # 65
print(ord('0'))   # 48

print(chr(97))    # 'a'
print(chr(65))    # 'A'

# Common pattern: normalize letter to 0-25 index
ch = 'c'
idx = ord(ch) - ord('a')   # 2

# Shift a character by n positions
def shift(ch, n):
    return chr((ord(ch) - ord('a') + n) % 26 + ord('a'))

#-------------------------------
# String check methods
#-------------------------------
"abc".isalpha()     # True  — all letters
"abc123".isalpha()  # False
"123".isdigit()     # True  — all digits
"abc123".isalnum()  # True  — letters + digits
"  ".isspace()      # True
"ABC".isupper()     # True
"abc".islower()     # True

# LC 125 Valid Palindrome
def isPalindrome(s):
    filtered = [c.lower() for c in s if c.isalnum()]
    return filtered == filtered[::-1]

# NOTE: lower() has NO effect on digits — safe to call on any alphanumeric char
# >>> "0".lower()
# '0'
# >>> "A".lower()
# 'a'
# So the one-liner below works for both letters and numbers:
fixed_s = ''.join(c.lower() for c in s if c.isalnum())

字串方法速查

python
s = "  Hello, World!  "

# Strip whitespace (or specific chars)
s.strip()          # "Hello, World!"
s.lstrip()         # "Hello, World!  "
s.rstrip()         # "  Hello, World!"
s.strip("!")       # "  Hello, World!  " (only strips specified chars from ends)

# Case
s.lower()          # "  hello, world!  "
s.upper()          # "  HELLO, WORLD!  "
s.title()          # "  Hello, World!  "
s.swapcase()       # "  hELLO, wORLD!  "

# Search
s.find("World")    # 9  (-1 if not found)
s.index("World")   # 9  (raises ValueError if not found)
s.count("l")       # 3
s.startswith("  H")  # True
s.endswith("!  ")    # True

# Replace and join
s.replace("World", "Python")   # "  Hello, Python!  "
", ".join(["a", "b", "c"])     # "a, b, c"
"a,b,c".split(",")             # ['a', 'b', 'c']

# Reverse a string
rev = s[::-1]

# String multiplication
"ab" * 3   # "ababab"
"-" * 10   # "----------"

# Check if string is a palindrome
def is_palindrome(s):
    return s == s[::-1]

從字串中抓出數字字元

python
# LC 008
s = '4193 with words'
res = re.search('(^[\+\-]?\d+)', s).group()
print (res)

反向走訪字串

關鍵差異:range() 的 stop 值是「不含」的

python
x = "332"   # indices: 0, 1, 2

# ── Form 1: range(len(x)-1, -1, -1)  → stop = -1 (exclusive) → covers 2, 1, 0 (ALL indices)
for i in range(len(x)-1, -1, -1):
    print(i)
# 2
# 1
# 0   ← index 0 IS included

# ── Form 2: range(len(x)-1, 0, -1)   → stop = 0 (exclusive) → covers 2, 1 (MISSES index 0)
for i in range(len(x)-1, 0, -1):
    print(i)
# 2
# 1   ← index 0 is NOT included
寫法 stop 值 走訪到的索引 含索引 0 嗎?
range(len(x)-1, -1, -1) -1(不含) len-1 … 0
range(len(x)-1, 0, -1) 0(不含) len-1 … 1

經驗法則: 要反向走訪「所有」索引,stop 值一律用 -1

python
# Equivalent ways to iterate a string/array in reverse (all indices)
x = "abc"

# Form A: range
for i in range(len(x) - 1, -1, -1):
    print(x[i])

# Form B: reversed() — cleaner, no index needed
for ch in reversed(x):
    print(ch)

# Form C: slice — creates a reversed copy
for ch in x[::-1]:
    print(ch)

當你「確實」想跳過索引 0 時(例如要拿 x[i]x[i-1] 比較):

python
# Safe to start from index 1 in forward loops, or stop before 0 in reverse loops
for i in range(len(x) - 1, 0, -1):   # compares x[i] vs x[i-1]; never i-1 = -1
    if x[i] == x[i - 1]:
        print(f"duplicate at {i}")

排序與比較

lambda 當 key 的 sort

text
# example 1
# LC 973. K Closest Points to Origin
# IDEA : sort + lambda
class Solution(object):
    def kClosest(self, points, K):
        points.sort(key = lambda x : x[0]**2 +  x[1]**2)
        return points[:K]


# example 2
In [28]: def my_func(x):
    ...:     return x**2
    ...:
    ...: x = [-4,-5,0,1,2,5]
    ...: x.sort(key=lambda x: my_func(x))
    ...: print (x)
[0, 1, 2, -4, -5, 5]
text
# LC 937
# https://leetcode.com/problems/reorder-data-in-log-files/solution/
def my_func(input):
    # do sth
    if condition:
        return key1, key2, key3....
    else:
        return key4, key5, key6....

my_array=["a1 9 2 3 1","g1 act car","zo4 4 7","ab1 off key dog","a8 act zoo"]
my_array.sort(key=lambda x : my_func(x))

降冪:key=lambda x: -x[0] vs reverse=True vs [::-1]

降冪排序有三種寫法,各有各適用的場合。

python
# ── Context: LC 853 Car Fleet ──
# We have pos_speed = [[pos, speed, time], ...] and want to sort by position DESC.

# ── Form 1: negate the key  (in-place, fine-grained control) ──
pos_speed.sort(key=lambda x: -x[0])
# Use when:
#   • You need MIXED direction: primary DESC, secondary ASC
#     e.g. sort(key=lambda x: (-x[0], x[1]))  ← impossible with reverse=True alone
#   • Works for int and float keys

# ── Form 2: reverse=True  (cleaner for single-direction reversal) ──
pos_speed.sort(key=lambda x: x[0], reverse=True)
sorted_cars = sorted(cars, reverse=True)   # creates a NEW list
# Use when:
#   • ALL keys go the same direction (all DESC)
#   • More readable for simple cases
#   • sorted() is preferred over sort() when you need to keep the original

# ── Form 3: sort ASC then reverse/slice  (separate steps) ──
times = [(target - pos) / spe for pos, spe in sorted(cars)]   # ASC sort
for time in times[::-1]:        # iterate in reverse  — does NOT mutate list
    ...
# -- or --
for time in reversed(times):    # same effect, no extra list copy
    ...
# Use when:
#   • You want to keep the sorted-ASC list around for other uses
#   • reversed() is O(1) memory; [::-1] creates a new list copy

# ── Quick comparison ──
# Method              | In-place? | New list? | Mixed direction? | Readability
# -x[0]               |   yes     |    no     |      YES         |  moderate
# reverse=True        |   yes     |    no     |      no          |  high
# sorted(reverse=True)|   no      |    YES    |      no          |  high
# sort ASC + reversed |   yes     |    no     |      no          |  moderate

# ── Multi-key mixed direction example (only negation works here) ──
# Sort by position DESC, then by speed ASC as tiebreaker:
data.sort(key=lambda x: (-x[0], x[1]))

多重 key 的 tuple 排序:key=lambda x: (x[0], x[1]) Priority 5 of 5 — Must know — expect it in almost every loop

關鍵想法:讓 key 回傳一個 tuple。Python 比較 tuple 是由左到右, 遇到第一個不相等的元素就短路。所以 (-x[0], x[1]) 的意思是*「主要依 x[0] 降冪,平手時依 x[1] 升冪」*。

快速決策表

目標 寫法
key1 升冪 key = lambda x : x[0]
key1 降冪 key = lambda x : -x[0] (僅限數值)reverse = True
key1 升冪、key2 升冪 key = lambda x : (x[0], x[1])
key1 降冪、key2 降冪 key = lambda x : (x[0], x[1]), reverse = True
key1 降冪、key2 升冪 key = lambda x : (-x[0], x[1]) (key1 為數值)
key1 升冪、key2 降冪 key = lambda x : (x[0], -x[1]) (key2 為數值)
方向混合且 key 非數值 兩次穩定排序 — 先依最後一個 key 排(見下文)
需要兩兩比較的自訂規則 functools.cmp_to_key(my_cmp)
python
# python
y = [[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]
print (y)
# sort by x[0] DESC, then x[1] ASC
y.sort(key = lambda x : (-x[0], x[1]))
print (y)
# [[7, 0], [4, 4], [7, 1], [5, 0], [6, 1], [5, 2]]
# [[7, 0], [7, 1], [6, 1], [5, 0], [5, 2], [4, 4]]

視覺化追蹤 — tuple key 是怎麼把上面的資料排出來的:

text
elem     | key = (-x[0], x[1])
---------------------------------
[7,0]    | (-7, 0)
[7,1]    | (-7, 1)
[6,1]    | (-6, 1)
[5,0]    | (-5, 0)
[5,2]    | (-5, 2)
[4,4]    | (-4, 4)

sort keys ASC  ->  (-7,0) < (-7,1) < (-6,1) < (-5,0) < (-5,2) < (-4,4)
                    ^^^^^^^^^^^^^^ 1st slot tied (-7) -> compare 2nd slot (0 < 1)
=> [[7,0], [7,1], [6,1], [5,0], [5,2], [4,4]]

reverse = True vs 把 key 取負

python
# python
arr = [[1,'b'], [1,'a'], [2,'a']]

# (a) reverse=True flips the WHOLE ordering (every key), not just the 1st
sorted(arr, key = lambda x : (x[0], x[1]), reverse = True)
# [[2,'a'], [1,'b'], [1,'a']]   <-- x[0] DESC *and* x[1] DESC

# (b) negation flips only the negated slot
sorted(arr, key = lambda x : (-x[0], x[1]))
# [[2,'a'], [1,'a'], [1,'b']]   <-- x[0] DESC, x[1] ASC

# NOTE !!! `-` only works on numbers
sorted(arr, key = lambda x : (-x[0], -x[1]))   # ❌ TypeError : bad operand type for unary -: 'str'

非數值 key 又要方向混合 → 兩次穩定排序

Timsort 是穩定的,所以你可以把排序串起來。規則:先依最後(最不重要)的 key 排。

python
# python
# want : len(s) ASC, then s DESC  (can't do -s)
words = ["bb", "a", "ab", "c", "ba"]

words.sort(reverse = True)          # step 1 : least significant key (s DESC)
words.sort(key = len)               # step 2 : most  significant key (len ASC) - stable
print (words)
# ['c', 'a', 'bb', 'ba', 'ab']
#  ^^^^^^^^ len 1, DESC        ^^^^^^^^^^^^^^ len 2, DESC

⚠️ 千萬不要把這兩趟的順序顛倒 — 先依 len 再依 s 排,會把 len 的分組整個丟掉。

functools.cmp_to_key — 當根本寫不出 key 函式時

當順序取決於把兩個元素拿來一起比較(沒有任何單一元素的值能表達這個規則)時就用它。

python
# python
# LC 179 Largest Number : concat digits to form the biggest number
# rule : a before b  iff  a+b > b+a  -> not expressible as a single key
import functools

class Solution(object):
    def largestNumber(self, nums):
        # time = O(n log n * k), space = O(n)   (k = digit length)
        def cmp(a, b):
            if a + b > b + a:
                return -1      # a comes FIRST
            elif a + b < b + a:
                return 1       # b comes FIRST
            return 0

        strs = [str(x) for x in nums]
        strs.sort(key = functools.cmp_to_key(cmp))
        res = "".join(strs)
        return "0" if res[0] == "0" else res

# nums = [3,30,34,5,9] -> "9534330"

其他好用的排序 key

python
# python
# 1) sort dict by VALUE desc, then KEY asc  (LC 692 Top K Frequent Words)
from collections import Counter
cnt = Counter(["i","love","leetcode","i","love","coding"])
res = sorted(cnt.keys(), key = lambda w : (-cnt[w], w))
# ['i', 'love', 'coding', 'leetcode']

# 2) sort by "distance" (LC 973 K Closest Points to Origin)
points = [[1,3],[-2,2],[5,8],[0,1]]
points.sort(key = lambda p : p[0]**2 + p[1]**2)
# [[0,1], [-2,2], [1,3], [5,8]]

# 3) sort intervals by start (LC 56 / 57 / 253 / 435)
intervals = [[1,3],[8,10],[2,6],[15,18]]
intervals.sort(key = lambda x : x[0])
# [[1,3], [2,6], [8,10], [15,18]]

# 4) sort by end   (LC 435 Non-overlapping Intervals - greedy)
intervals.sort(key = lambda x : x[1])

# 5) sort chars of a string as the canonical key (LC 49 Group Anagrams)
"".join(sorted("tea"))    # 'aet'

# 6) sort with index preserved (need original position afterwards)
nums = [5,2,8]
idx_sorted = sorted(range(len(nums)), key = lambda i : nums[i])
# [1, 0, 2]   <-- indices in value order

# 7) sort intervals by BOTH ends (LC 56 Merge Intervals, LC 252 / 253 Meeting Rooms)
intervals.sort(key = lambda x : (x[0], x[1]))

# 8) primary ASC, secondary DESC -- negate only the secondary slot
events.sort(key = lambda x : (x[0], -x[1]))

# 9) sort a list of dicts by two fields
people = [{"age": 30, "name": "bob"}, {"age": 30, "name": "amy"}]
people.sort(key = lambda p : (p["age"], p["name"]))

# 10) three keys, same rule -- tuples compare left to right for any length
tasks.sort(key = lambda t : (t[0], t[1], t[2]))

# 11) sorted() returns a NEW list; .sort() mutates in place and returns None
sorted_events = sorted(event_list, key = lambda x : (x[0], x[1]))

Java 對應寫法備註:

java
// java
int[][] people = {{7,0},{4,4},{7,1},{5,0},{6,1},{5,2}};

// V1 : explicit comparator - x[0] DESC, tie-break x[1] ASC
Arrays.sort(people, (a, b) -> {
    if (a[0] != b[0]) {
        return b[0] - a[0];   // NOTE !!! b - a  => DESC
    }
    return a[1] - b[1];       //          a - b  => ASC
});

// V2 : Comparator chaining (more readable, Java 8+)
Arrays.sort(people,
        Comparator.<int[]>comparingInt(a -> a[0]).reversed()   // key1 DESC
                  .thenComparingInt(a -> a[1]));               // key2 ASC

// List version
List<int[]> list = new ArrayList<>();
list.sort(Comparator.<int[]>comparingInt(a -> a[0]).reversed()
                    .thenComparingInt(a -> a[1]));

// ⚠️ `b[0] - a[0]` can OVERFLOW for large/negative values
//    -> use Integer.compare(b[0], a[0]) instead
Arrays.sort(people, (a, b) -> Integer.compare(b[0], a[0]));

// ⚠️ `.reversed()` after `.thenComparing(...)` reverses the WHOLE chain
//    (same trap as python's reverse=True)
Comparator.comparingInt((int[] a) -> a[0])
          .thenComparingInt(a -> a[1])
          .reversed();          // <-- key1 DESC *and* key2 DESC

🚫 常見錯誤:

python
# 1) Expecting reverse=True to flip only the primary key
arr.sort(key = lambda x : (x[0], x[1]), reverse = True)   # ❌ flips BOTH keys
arr.sort(key = lambda x : (-x[0], x[1]))                  # ✅ only key1 DESC

# 2) Negating a string / tuple
arr.sort(key = lambda x : (-x[0], -x[1]))   # ❌ TypeError if x[1] is str
# ✅ use 2 stable sorts, or cmp_to_key

# 3) Comparing mixed types inside the key
sorted([1, "a"])            # ❌ TypeError : '<' not supported between 'str' and 'int'

# 4) Multi-pass sorts in the WRONG order
words.sort(key = len); words.sort(reverse = True)   # ❌ len grouping destroyed
words.sort(reverse = True); words.sort(key = len)   # ✅ least significant key FIRST

# 5) Forgetting the sort is what makes the greedy/2-pointer step valid
#    -> for interval problems, ALWAYS state which key you sorted on

💡 面試提示:

  • 先把規則唸出來 —— 「依 A 降冪排,平手時依 B 升冪」 —— 然後照著寫出 tuple key。
  • 複雜度O(n log n) 次比較;k 個 key 的話每次組 tuple key 是 O(k)O(n log n * k)
  • list.sort()sorted() 都是穩定的 — 這正是多趟排序技巧和 LC 406 那種插入法能成立的原因。
  • 如果你沒辦法把規則寫成「每個元素各自算一個 key」,那就是該用 cmp_to_key(py)/自訂 Comparator(java)的訊號。

相關 LeetCode 題目:

題目 LC# 排序 key
Queue Reconstruction by Height 406 (-h, k) — 高的先,再依 k 升冪
Largest Number 179 cmp_to_key(a+b vs b+a)
Top K Frequent Words 692 (-count, word)
K Closest Points to Origin 973 x² + y²
Merge Intervals 56 start 升冪
Meeting Rooms II 253 start 升冪(+ 對 end 用最小堆積)
Non-overlapping Intervals 435 end 升冪(貪婪)
Group Anagrams 49 "".join(sorted(word))
Custom Sort String 791 order.index(ch)
Sort Array By Parity 905 x % 2
Relative Sort Array 1122 (rank.get(x, len), x)
Car Fleet 853 position 降冪(+ 堆疊)
Boats to Save People 881 weight 升冪(+ 雙指標)

總結:

  • ✅ tuple key = 多重 key 排序,比較方式是由左到右
  • -key 只翻轉一個欄位(僅限數值);reverse=True 翻轉全部欄位
  • ✅ 非數值又要方向混合 → 兩次穩定排序,先排最不重要的 key
  • ✅ 寫不出單一元素的 key → functools.cmp_to_key / java Comparator
  • ✅ Java:用 Integer.compare(b, a) 而不是 b - a 以避開溢位;.reversed() 會作用在整條鏈上

用具名 key 函式寫條件式的 tuple key

當排序 key 取決於某個條件(A 群 vs B 群、合法 vs 不合法等)時, 一行 lambda 會變得很難讀。改寫成一個回傳 tuple 的具名 key 函式 — tuple 依然是逐元素(由左到右)比較,所以第一個欄位就是主要排序依據、 下一個是平手時的判準,以此類推。

模式:開頭放「群組標籤」+ 各群組自己的排序規則

python
# Return a tuple of sort keys; the FIRST element groups items,
# the rest order items WITHIN each group.
def compare(item):
    if condition:
        return (0, item.value, item.name)   # group 0 first; ASC by value, then name
    else:
        return (1, -item.priority, item.id)  # group 1 next; DESC by priority, ASC by id

items.sort(key=compare)        # in-place
# items = sorted(items, key=compare)   # or build a new list

開頭那個 0 / 1 是幹嘛的? 那是群組標籤 — 所有第 0 群的項目都會排在 所有第 1 群之前(因為 tuple 比較會先看第一個元素)。剩下的 tuple 欄位只在 同一群之內才有意義,所以每一群都可以有自己的排序規則(升冪、取負做降冪、 甚至用完全不同的欄位)。

關鍵規則

  • 所有分支必須回傳長度相同的 tuple,而且每個位置的型別要可互相比較 (別在同一個欄位裡混用 strint)。
  • 把某個數值欄位取負(-item.priority),就能讓那個欄位降冪、其餘維持升冪 — 跟 [1-11’] 節同一招。
  • key 函式每個元素只會被呼叫一次(Schwartzian transform),所以就算裡面 邏輯比較重也還是有效率。

經典 LC 用法 — LC 937 Reorder Data in Log Files(字母 log 排在數字 log 之前, 字母 log 先依內容再依 id 排序):

python
class Solution:
    def reorderLogFiles(self, logs):
        def compare(log):
            id_, rest = log.split(" ", 1)
            if rest[0].isalpha():
                return (0, rest, id_)   # letter-logs: group 0, by content, then id
            else:
                return (1,)             # digit-logs: group 1, keep original order

        return sorted(logs, key=compare)

經驗法則:只要排序規則出現分支,就該改用回傳 tuple 的具名 key 函式 — 這比把 if/else 硬塞進 lambda 好讀太多。

依出現次數排序一個 dict

python
# LC 451 Sort Characters By Frequency
# V1
import collections
class Solution(object):
    def frequencySort(self, s):
        d = collections.Counter(s)
        d_dict = dict(d)
        res = []
        for x in sorted(d_dict, key=lambda k : -d_dict[k]):
            res.append(x)
        return res

x= [1, 2, 3, 1, 2, 1, 2, 1]
s = Solution()
r = s.frequencySort(x)

# V2
# https://stackoverflow.com/questions/613183/how-do-i-sort-a-dictionary-by-value
import collections
class Solution(object):
    def frequencySort(self, s):
        d = collections.Counter(s)
        d_dict = dict(d)
        res = []
        #for x in sorted(d_dict.items(), key=lambda items: -items[1]):
        for _ in sorted(d_dict.items(), key=lambda x: -x[1]):
            res.append(_)
        return res

x= [1, 2, 3, 1, 2, 1, 2, 1]
s = Solution()
r = s.frequencySort(x)

keymin() / max()

python
# key= works exactly like sort's key= parameter
nums = [-3, -1, 2, 4]
print(max(nums, key=abs))   # -3  (largest absolute value)
print(min(nums, key=abs))   # -1  (smallest absolute value)

# With iterable of tuples
points = [(1, 5), (3, 2), (2, 8)]
print(max(points, key=lambda p: p[1]))  # (2, 8)

# min/max with default (avoids error on empty iterable)
print(min([], default=0))   # 0

# clamp a value between lo and hi
val = max(lo, min(val, hi))

sorted()reversed()sum()abs()

python
# sorted() returns a NEW list; list.sort() is in-place
nums = [3, 1, 4, 1, 5]
print(sorted(nums))            # [1, 1, 3, 4, 5]  — nums unchanged
print(sorted(nums, reverse=True))  # [5, 4, 3, 1, 1]

# Sort list of tuples: primary key asc, secondary key desc
pairs = [(1, 3), (2, 1), (1, 5)]
pairs.sort(key=lambda x: (x[0], -x[1]))
# [(1, 5), (1, 3), (2, 1)]

# reversed() returns an iterator
for x in reversed([1, 2, 3]):
    print(x)  # 3 2 1

list(reversed([1,2,3]))  # [3, 2, 1]

# sum() with start
sum([1, 2, 3], 10)   # 16

# sum of 2D list
matrix = [[1,2],[3,4]]
total = sum(sum(row) for row in matrix)  # 10

# abs()
abs(-5)    # 5
abs(3+4j)  # 5.0  (complex magnitude)

數字與數學

一次拿到商和餘數 — divmod

text
In [1]: x,y = divmod(100, 3)

In [2]: x
Out[2]: 33

In [3]: y
Out[3]: 1

除以某個數之後的餘數

text
#-----------------
# V1 : %=
#-----------------

In [7]: x = 100

In [8]: x %= 60

In [9]: x
Out[9]: 40

In [10]: y = 120

In [11]: y %= 60

In [12]: y
Out[12]: 0

#-----------------
# V2 : divmod
#-----------------
In [13]: a = 100

In [14]: q, r = divmod(a, 60)

In [15]: q
Out[15]: 1

In [16]: r
Out[16]: 40

In [17]: b = 120

In [18]: q2, r2 = divmod(b, 60)

In [19]: q2
Out[19]: 2

In [20]: r2
Out[20]: 0
python
# LC 1010
# V0
# IDEA : dict
class Solution(object):
    def numPairsDivisibleBy60(self, time):
        rem = {}
        pairs = 0
        for t in time:
            #print ("rem = " + str(rem))
            t %= 60
            if (60 - t) % 60 in rem:
                pairs += rem[(60 - t) % 60]
            if t not in rem:
                rem[t] = 1
            else:
                rem[t] += 1
        return pairs

math.ceil

text
# https://www.runoob.com/python/func-number-ceil.html
# https://www.runoob.com/python/func-number-ceil.html

"""
The method ceil(x) in Python returns a ceiling value of x 
-> i.e., the SMALLEST integer GREATER than or EQUAL to x.
"""
In [9]:
   ...: import math
   ...:
   ...: # prints the ceil using ceil() method
   ...: print ("math.ceil(-23.11) : ", math.ceil(-23.11))
   ...: print ("math.ceil(300.16) : ", math.ceil(300.16))
   ...: print ("math.ceil(300.72) : ", math.ceil(300.72))
math.ceil(-23.11) :  -23
math.ceil(300.16) :  301
math.ceil(300.72) :  301


# LC 875. Koko Eating Bananas
#...
# Iterate over the piles and calculate hour_spent.
# We increase the hour_spent by ceil(pile / middle)
for pile in piles:
    # python ceil : https://www.runoob.com/python/func-number-ceil.html
    hour_spent += math.ceil(pile / middle)
# Check if middle is a workable speed, and cut the search space by half.
if hour_spent <= h:
    right = middle
else:
    left = middle + 1
#...

math.floor

text
# https://www.geeksforgeeks.org/floor-ceil-function-python/

"""
floor() method in Python returns the floor of x 
-> i.e., the LARGEST integer NOT GREATER than x. 
"""

# This will import math module
import math   
  
In [8]: import math
   ...:
   ...: # prints the ceil using floor() method
   ...: print ("math.floor(-23.11) : ", math.floor(-23.11))
   ...: print ("math.floor(300.16) : ", math.floor(300.16))
   ...: print ("math.floor(300.72) : ", math.floor(300.72))
math.floor(-23.11) :  -24
math.floor(300.16) :  300
math.floor(300.72) :  300

pow(x, n, mod) — 快速模冪

python
# Built-in 3-arg pow is O(log n), much faster than (x**n) % mod
MOD = 10**9 + 7

print(pow(2, 10, MOD))    # 1024
print(pow(2, 100, MOD))   # 976371285  (computed efficiently)

# Modular inverse (when mod is prime): pow(a, mod-2, mod)
inv = pow(3, MOD - 2, MOD)   # modular inverse of 3

# LC 50 Pow(x, n) — manual fast power
def myPow(x, n):
    if n < 0:
        x, n = 1 / x, -n
    res = 1
    while n:
        if n % 2 == 1:
            res *= x
        x *= x
        n //= 2
    return res

整數除法 // 與位元運算

python
#-------------------------------
# Integer division (floor division)
#-------------------------------
7 // 2    # 3
-7 // 2   # -4  (rounds toward -inf, NOT toward 0!)
int(-7/2) # -3  (truncation toward 0)

# Safe mid-point (avoids overflow in other languages)
lo, hi = 0, 100
mid = (lo + hi) // 2

#-------------------------------
# Bit operations (common in LC)
#-------------------------------
# AND, OR, XOR, NOT
5 & 3    # 1   (101 & 011 = 001)
5 | 3    # 7   (101 | 011 = 111)
5 ^ 3    # 6   (101 ^ 011 = 110)
~5       # -6  (bitwise NOT: ~x = -(x+1))

# Shift
5 << 1   # 10  (multiply by 2)
5 >> 1   # 2   (floor divide by 2)

# Check/set/clear bit i
x = 13          # 1101
x & (1 << i)    # check bit i (non-zero if set)
x | (1 << i)    # set bit i
x & ~(1 << i)   # clear bit i
x ^ (1 << i)    # flip bit i

# Count set bits
bin(13).count('1')   # 3
# or: use Brian Kernighan
def count_bits(n):
    count = 0
    while n:
        n &= n - 1   # clear lowest set bit
        count += 1
    return count

# x & (x-1) removes lowest set bit — useful for power-of-2 check
def is_power_of_two(n):
    return n > 0 and (n & (n - 1)) == 0

把 N 進位的整數轉成十進位

python
# math.html

# How does int(x[,base]) work?
# -> https://stackoverflow.com/questions/33664451/how-does-intx-base-work
# -> int(string, base) accepts an arbitrary base. You are probably familiar with binary and hexadecimal, and perhaps octal; these are just ways of noting an integer number in different bases:
# exmaple :
# In [76]: int('10',2)      # transform '10' from 2 based to 10 based                                                  
# Out[76]: 2
#
# In [77]: int('11',2)      # # transform '11' from 2 based to 10 based                                                 
# Out[77]: 3
#
# In [78]: int('100',2)     # # transform '100' from 2 based to 10 based                                                 
# Out[78]: 4

# LC 089

無限大與邊界值

python
# Use float('inf') / float('-inf') instead of sys.maxsize for clarity
INF = float('inf')
NEG_INF = float('-inf')

# Works with min/max comparisons
min_val = float('inf')
for x in [3, 1, 4, 1, 5]:
    min_val = min(min_val, x)
print(min_val)  # 1

# Common in DP initialization
dp = [[float('inf')] * n for _ in range(m)]

# Python int has no overflow — safe to use large numbers
# But float('inf') is cleaner for "unbounded" semantics

走訪、生成式與函數式工具

all()

  • 會針對 list 中「所有」元素檢查條件,回傳 Boolean(true 或 false)
python
# example 1
In [36]: a = "000"

In [37]: all( i == "0" for i in a )
Out[37]: True

# example 2
In [38]: b = "abc123"

In [39]: all ( i == "a" for i in b )
Out[39]: False

# LC 763. Partition Labels
class Solution(object):
    def partitionLabels(self, s):
        d = {val:idx for idx, val in enumerate(list(s))}
        #print (d)
        res = []
        tmp = set()
        for idx, val in enumerate(s):
            """
            NOTE : below condition
            """
            if idx == d[val] and all(idx >= d[t] for t in tmp):
                res.append(idx+1)
            else:
                tmp.add(val)
        _res = [res[0]] + [ res[i] - res[i-1] for i in range(1, len(res)) ]
        return _res

any()

python
# Returns True if ANY element in iterable is True (short-circuits)
In [1]: any([False, False, True])
Out[1]: True

In [2]: any([False, False, False])
Out[2]: False

In [3]: any(x > 3 for x in [1, 2, 5])
Out[3]: True

# Complement to all():
# all() -> every element must be True
# any() -> at least one element must be True

not 邏輯

python
#----------------------------
# can be either None, [], ""
#----------------------------
In [32]: x = None

In [34]: not x
Out[34]: True

In [35]: y = []

In [36]: not y
Out[36]: True

In [37]: z = ""

In [38]: not z
Out[38]: True

enumerate()

python
# Returns (index, value) pairs — avoids manual index tracking
fruits = ['apple', 'banana', 'cherry']

for i, v in enumerate(fruits):
    print(i, v)
# 0 apple
# 1 banana
# 2 cherry

# start parameter
for i, v in enumerate(fruits, start=1):
    print(i, v)
# 1 apple  2 banana  3 cherry

# Build index map (very common in LC)
s = "abcba"
idx_map = {v: i for i, v in enumerate(s)}
print(idx_map)  # {'a': 4, 'b': 3, 'c': 2}  (last occurrence)

zip()

text
# python
In [1]: for x, y in zip([-1, 1, 0, 0], [0, 0, -1, 1]):
   ...:     print (x, y)
   ...:
-1 0
1 0
0 -1
0 1

In [2]: for x, y, z in zip([-1, 1, 0, 0], [0, 0, -1, 1], [0,0,0,0]):
   ...:     print (x,y,z)
   ...:
-1 0 0
1 0 0
0 -1 0
0 1 0

In [3]: for x, y, z, u in zip([-1, 1, 0, 0], [0, 0, -1, 1], [0,0,0,0], [9,9,9,9]):
   ...:     print (x,y,z,u)
   ...:
-1 0 0 9
1 0 0 9
0 -1 0 9
0 1 0 9

走訪 dict

python
d = {'a':1, 'b':2, 'c': 3}
# loop over key, value
for k, v in d.items():
    print (k, v)

# loop over key
for k in d.keys():
    print (k)

# loop over value
for v in d.values():
    print (v)

星號(*)運算式

text
# Extended Iterable Unpacking

# https://www.python.org/dev/peps/pep-3132/
# http://swaywang.blogspot.com/2012/01/pythonstarred-expression.html

# example 1
In [38]: a, *b, c = range(5)

In [39]: a
Out[39]: 0

In [40]: b
Out[40]: [1, 2, 3]

In [41]: c
Out[41]: 4

# example 2
In [43]: for a, *b in [(1, 2, 3), (4, 5, 6, 7)]:
    ...:     print ("a = " + str(a) + " b = " + str(b))
    ...:
a = 1 b = [2, 3]
a = 4 b = [5, 6, 7]

# example 3
In [44]: first, *rest = [1, 2, 3, 4, 5]

In [45]: first
Out[45]: 1

In [46]: rest
Out[46]: [2, 3, 4, 5]

# example 4
In [47]: *directories, executable = "/usr/local/bin/vim".split("/")
    ...: print (directories)
    ...: print (executable)
['', 'usr', 'local', 'bin']
vim

# example 5
args = [1,3]
print (range(*args))

filter()

text
# https://www.runoob.com/python/python-func-filter.html

#-----------------------------------------------
# syntax : filter(<filter_func>, <iterable>)
#-----------------------------------------------

# note !!! : in py 3, it will return iterable instance; while in py 2, it will return a list directly

#----------------------------
# example 1
#----------------------------
In [13]: def is_odd(n):
    ...:     return n % 2 == 1
    ...:
    ...: newlist = filter(is_odd, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    ...: print(newlist)
<filter object at 0x7fc71c3dced0>

In [14]:

In [14]: list(newlist)
Out[14]: [1, 3, 5, 7, 9]


#----------------------------
# example 2
#----------------------------
In [15]: import math
    ...: def is_sqr(x):
    ...:     return math.sqrt(x) % 1 == 0
    ...:
    ...: newlist = filter(is_sqr, range(1, 101))
    ...: print(newlist)
<filter object at 0x7fc71bb10450>

In [16]:

In [16]: list(newlist)
Out[16]: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

List 生成式

text
#----------------------------
# example 1
#----------------------------
# https://stackoverflow.com/questions/4260280/if-else-in-a-list-comprehension

In [8]: [ x for x in range(5) ]
Out[8]: [0, 1, 2, 3, 4]

# NOTE this !!!!
In [9]: [ x if x % 2 == 0 else -1 for x in range(5) ]
   ...:
   ...:
Out[9]: [0, -1, 2, -1, 4]

In [10]: def my_func(x):
    ...:     if x % 2 ==0:
    ...:         return True
    ...:     return False
    ...:
    ...: [ x if my_func(x) else 999  for x in range(5)]
Out[10]: [0, 999, 2, 999, 4]

map() 與生成器運算式

python
# map(func, iterable) — lazy, returns iterator
nums = ["1", "2", "3"]
ints = list(map(int, nums))    # [1, 2, 3]

# map with lambda
doubled = list(map(lambda x: x * 2, [1, 2, 3]))  # [2, 4, 6]

# Generator expression (lazy list comprehension) — memory efficient
gen = (x**2 for x in range(1000000))   # nothing computed yet
total = sum(x**2 for x in range(1000000))  # computed on the fly

# Prefer generator expression over list comprehension inside sum/any/all/max/min
max_val = max(abs(x) for x in nums)
has_neg = any(x < 0 for x in nums)

三元(條件)運算式

python
# syntax: <value_if_true> if <condition> else <value_if_false>
x = 5
result = "even" if x % 2 == 0 else "odd"   # "odd"

# Nested ternary (keep shallow — hard to read beyond two levels)
sign = "positive" if x > 0 else ("zero" if x == 0 else "negative")

# Common LC use
ans = left if left else right          # return whichever is not None
val = node.val if node else 0

Dict 與 Set

Dict 的 get()setdefault()、生成式

python
d = {'a': 1, 'b': 2}

# get(key, default) — safe access
d.get('c', 0)     # 0  (no KeyError)
d.get('a', 0)     # 1

# setdefault(key, default) — insert if missing, return value
d.setdefault('c', []).append(3)   # d['c'] = [3]
d.setdefault('c', []).append(4)   # d['c'] = [3, 4]

# dict comprehension
squares = {x: x**2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# invert a dict (assuming unique values)
inv = {v: k for k, v in squares.items()}

# filter dict
evens = {k: v for k, v in squares.items() if v % 2 == 0}

Set 運算

python
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

# Basic ops
a | b    # union        {1, 2, 3, 4, 5, 6}
a & b    # intersection {3, 4}
a - b    # difference   {1, 2}
a ^ b    # symmetric diff {1, 2, 5, 6}

# Membership: O(1) average
3 in a   # True

# Mutation
a.add(5)
a.discard(99)   # no error if missing (vs remove() which raises KeyError)
a.remove(1)     # raises KeyError if missing

# NOTE: can directly remove a specific element from a set by value (not index)
# Common in sliding window problems (LC 3)
seen = set()
seen.add('a')
seen.remove('a')   # removes 'a' directly — no index needed

# Set comprehension
squares = {x**2 for x in range(5)}   # {0, 1, 4, 9, 16}

# Freeze (hashable, usable as dict key)
fs = frozenset([1, 2, 3])

or 檢查「其中一個元素存在」

text
In [8]: def test(l1, l2):
   ...:     if l1 or l2:
   ...:         return l1 or l2
   ...:
   ...: res = test("l1", None)
   ...: print (res)
   ...:
   ...: res2 = test(None, "l2")
   ...: print (res2)
l1
l2

結構、作用域與回傳值

二維陣列(矩陣)初始化

python
#-------------------------------------------------
# CORRECT way — use list comprehension (independent rows)
#-------------------------------------------------
m, n = 3, 4
grid = [[0] * n for _ in range(m)]
grid[0][0] = 1
# Only grid[0][0] is changed

#-------------------------------------------------
# WRONG way — all rows share the same list!
#-------------------------------------------------
bad = [[0] * n] * m
bad[0][0] = 1
# ALL rows become [1, 0, 0, 0]  — common bug!

#-------------------------------------------------
# Common DP patterns
#-------------------------------------------------
# 1D DP
dp = [0] * (n + 1)

# 2D DP (m rows, n cols, filled with False)
dp = [[False] * (n + 1) for _ in range(m + 1)]

# Fill with infinity
dp = [[float('inf')] * n for _ in range(m)]

Building a row with fixed ends — [1] + middle + [1]

python
# python
# List `+` makes a NEW list, so the ends can be glued on in one expression:
#   [1]        -> the fixed first element
#   new_val    -> the computed middle (may be empty)
#   [1]        -> the fixed last element
#
# NOTE : this is concatenation, NOT append —
#        [1] + [2, 3] + [1]  ->  [1, 2, 3, 1]
#        [1].append([2, 3])  ->  None (mutates in place, returns nothing)

# an empty middle still works — no special case needed
[1] + [] + [1]        # [1, 1]
python
# python
# LC 118 Pascal's Triangle
# IDEA: 1D DP — row i is built from row i-1
class Solution(object):
    def generate(self, numRows):
        # edge
        if numRows == 0:
            return []
        if numRows == 1:
            return [[1]]
        if numRows == 2:
            return [[1], [1, 1]]

        n = numRows

        # dp[i] = the i-th row
        dp = [None] * n

        dp[0] = [1]
        dp[1] = [1, 1]

        for i in range(2, n):

            new_val = []

            # previous row
            prev = dp[i - 1]

            # calculate middle values
            for j in range(len(prev) - 1):
                new_val.append(prev[j] + prev[j + 1])

            # NOTE !!! how we add [1] at beginning and end
            # first and last are always 1
            dp[i] = [1] + new_val + [1]

        return dp

# time = O(n^2), space = O(n^2)

Same shape shows up whenever a sequence needs sentinel ends: [1] + nums + [1] (LC 312 Burst Balloons, so the out-of-range neighbours multiply as 1), [0] + heights + [0] (LC 84 Largest Rectangle, so the stack always drains), or [float('-inf')] + arr + [float('-inf')] to kill the edge cases in a peak scan.

巢狀函式中的 nonlocalglobal

python
# nonlocal: modify a variable in the ENCLOSING (not global) scope
def outer():
    count = 0
    def inner():
        nonlocal count
        count += 1
    inner()
    inner()
    print(count)  # 2

# Without nonlocal, count += 1 raises UnboundLocalError

# global: modify a module-level variable inside a function
total = 0
def add(x):
    global total
    total += x

# Common LC pattern: DFS with mutable result
def maxDepth(root):
    res = [0]
    def dfs(node, depth):
        if not node:
            return
        res[0] = max(res[0], depth)  # list trick avoids nonlocal
        dfs(node.left, depth + 1)
        dfs(node.right, depth + 1)
    dfs(root, 1)
    return res[0]

isinstance() 與型別檢查

python
isinstance(3, int)        # True
isinstance(3.0, float)    # True
isinstance("hi", str)     # True
isinstance([], list)      # True
isinstance({}, dict)      # True

# Check multiple types at once
isinstance(3, (int, float))   # True

# type() for exact type (no inheritance)
type(3) == int    # True
type(True) == int # False  (bool is subclass of int, but type() is exact)
isinstance(True, int)  # True  (True IS an int!)

用小型類別承載多個回傳值

當一個 DFS/遞迴需要一次回傳好幾個值(例如高度 + 大小 + 一個旗標)時,Java 的慣用寫法是開一個小的 private static class SubtreeInfo。在 Python 裡最接近的做法是 @dataclass、普通類別,或 NamedTuple

java
// java — the pattern we want to port
private static class SubtreeInfo {
    int height;
    int size;
    boolean isPerfect;
}

選項 1:@dataclass(推薦)

@dataclass 會自動產生 __init____repr____eq__ — 樣板程式碼最少、可讀性最高。

python
# python
from dataclasses import dataclass

@dataclass
class SubtreeInfo:
    height: int
    size: int
    is_perfect: bool


def dfs(node):
    # returns SubtreeInfo carrying 3 values up the recursion
    if node is None:
        return SubtreeInfo(0, 0, True)

    left = dfs(node.left)
    right = dfs(node.right)

    is_perfect = (
        left.is_perfect
        and right.is_perfect
        and left.height == right.height
    )

    size = left.size + right.size + 1
    height = max(left.height, right.height) + 1

    if is_perfect:
        perfect_sizes.append(size)

    return SubtreeInfo(height, size, is_perfect)

# usage
info = dfs(root)
print(info.height, info.size, info.is_perfect)

選項 2:傳統類別(不需 import)

python
# python
class SubtreeInfo:
    def __init__(self, height, size, is_perfect):
        self.height = height
        self.size = size
        self.is_perfect = is_perfect

# usage is identical
info = dfs(root)
print(info.height)

選項 3:NamedTuple(輕量 + 不可變)

當這組資料應該是唯讀時就用它(也能像 tuple 一樣解包)。

python
# python
from typing import NamedTuple

class SubtreeInfo(NamedTuple):
    height: int
    size: int
    is_perfect: bool

info = dfs(root)
print(info.height)          # attribute access
h, s, p = info              # tuple unpacking also works

快速比較

選項 樣板程式碼 可變嗎? 最適合
@dataclass 可(要不可變就加 frozen=True 預設選擇 — 乾淨又好讀
普通類別 不允許 import/非常舊的 Python
NamedTuple 不可 不可變的資料組,同時能 tuple 解包

偷懶的替代做法:一次性的 DFS 你其實可以直接 return (height, size, is_perfect) 再解包 — 但一旦欄位到 3 個以上,具名類別/NamedTuple 好讀太多了。對 LeetCode 風格的解法而言,@dataclass 通常是取代 Java private static class 最乾淨的選擇。

經驗法則: 如果你修改的是一個共用容器(append/add),你就必須把它復原(pop/remove)。如果你每次呼叫都建立一個物件(字串串接、tmp + [x]、tuple),那份複本本身就是回溯 — 沒有東西需要復原。另見 0-2) 賦值 vs 淺/深複製

eval()

text
# https://www.runoob.com/python/python-func-eval.html
# https://www.programiz.com/python-programming/methods/built-in/eval
# The eval() method parses the expression passed to this method and runs python expression (code) within the program.

# LC 640
# LC 150

# syntax : eval(expression[, globals[, locals]])

# eample
In [51]: x = 7
    ...: eval('3 * x')
    ...:
Out[51]: 21

In [52]: eval ('2 + 2')
    ...:
Out[52]: 4

In [53]: n = 81
    ...: eval('n + 4')
Out[53]: 85

對較長的陣列做交換

python
if len(l1) < len(l2):
   l1, l2 = l2, l1

DFS 路徑:str(不可變、不用回溯)vs list(可變、需要回溯)

在 DFS/回溯遞迴中往下傳 path 時,資料型別決定了你必不必須復原(回溯)

text
- if path is `str` type, we DON'T need to undo (backtrack)
     -> since it's IMMUTABLE.

- if path is `array` ([]) type, we NEED to undo (backtrack)
     -> since it's MUTABLE.

為什麼?

  • str 是不可變的:path + "->" + str(node.val) 每次呼叫都會建出一個全新的字串。父層的 path 完全沒被動到,所以每個分支自動拿到自己那份獨立的複本 — 沒有東西要復原。
  • list 是可變的:path.append(...) 修改的是所有遞迴呼叫共用的那同一個物件。探索完一個分支之後,你必須 path.pop() 把狀態還原給兄弟分支 — 否則殘留資料會跨分支外洩。
python
# LC 257 - Binary Tree Paths
# https://github.com/yennanliu/CS_basics/blob/master/leetcode_python/Depth-First-Search/binary-tree-paths.py

#-------------------------------------------------
# CASE 1) path as ARRAY (mutable) -> MUST backtrack (path.pop())
#-------------------------------------------------
class Solution(object):
    def binaryTreePaths(self, root):
        self.res = []
        self.helper(root, [])      # use array ([]) as tmp cache
        return self.res

    def helper(self, root, path):
        if not root:
            return
        path.append(str(root.val))            # mutate shared list
        # leaf node
        if not root.left and not root.right:
            self.res.append("->".join(path))
        else:
            self.helper(root.left, path)
            self.helper(root.right, path)
        # NOTE !!! backtrack at the final stage (undo the append)
        path.pop()

#-------------------------------------------------
# CASE 2) path as STRING (immutable) -> NO backtrack needed
#-------------------------------------------------
class Solution(object):
    def binaryTreePaths(self, root):
        res = []
        def dfs(node, cur):
            if not node:
                return
            cur = str(node.val) if cur == "" else cur + "->" + str(node.val)  # new string each call
            if not node.left and not node.right:
                res.append(cur)
                return
            # NOTE !!! no path.pop() — each branch got its own string copy
            dfs(node.left, cur)
            dfs(node.right, cur)
        dfs(root, "")
        return res

同樣的道理,其他不可變的載體 — tuple 和「傳一個新 list 進去」也同樣不用顯式 pop,因為它們交給每個子節點的是一個全新的物件,而不是共用同一個:

python
# trick: build a NEW list per call (tmp + [x]) instead of append+pop
def dfs(r, tmp):
    if not r.left and not r.right:
        ans.append("->".join(tmp))
    if r.left:
        dfs(r.left, tmp + [str(r.left.val)])    # tmp + [..] -> new list, no pop
    if r.right:
        dfs(r.right, tmp + [str(r.right.val)])

int 累加器(cur_sum)遵循「同一條」不可變規則 — 不用回溯

一個很常見的困惑:在一個同時帶著累加和(cur_sum,一個 int路徑 list(cache)的 DFS 裡,為什麼我們要 cache.pop(),卻從來不用把 cur_sum 「加回去」?因為整數是不可變的cur_sum += root.val 並不會就地修改父層那個 整數;它是把區域cur_sum 重新綁定到一個全新的 int 物件。子層的堆疊框架 被銷毀時,父層的 cur_sum 毫髮無傷。

變數 Python 怎麼傳它 需要回溯嗎? 為什麼
cur_sumint 傳值(不可變的複本) ❌ 否 += val 會產生一個新的 int 綁到區域名稱上;父層的值從未被覆寫,所以子層框架結束時它就自動還原了。
cachelist 傳參考(同一個共用物件) ✅ 是 整棵遞迴樹共用同一個 list 實例。子層的 append 父層看得到,所以我們「必須」pop() 清乾淨。
python
# LC 113 - Path Sum II
# https://github.com/yennanliu/CS_basics/blob/master/leetcode_python/Depth-First-Search/path-sum-ii.py
class Solution(object):
    def pathSum(self, root, targetSum):
        self.res = []
        if not root:
            return self.res
        self.helper(root, targetSum, 0, [])
        return self.res

    def helper(self, root, targetSum, cur_sum, cache):
        if not root:
            return

        cur_sum += root.val        # int  -> rebinds LOCAL name to a new int (immutable)
        cache.append(root.val)     # list -> mutates the ONE shared list

        if not root.left and not root.right and cur_sum == targetSum:
            self.res.append(cache[:])    # snapshot — cache[:] copies, else later pops corrupt it

        self.helper(root.left,  targetSum, cur_sum, cache)
        self.helper(root.right, targetSum, cur_sum, cache)

        cache.pop()                # MUST backtrack the list ...
        # NOTE: NO `cur_sum -= root.val` — the int never changed for the parent

記憶體逐步演練 — 父層處於 cur_sum = 5cache = [5],往值為 3 的子節點走:

往下進入子層 往上回到父層
cache(list) cache.append(3)[5, 3](同一個物件) 沒有 pop() 就會一直是 [5, 3]父層被汙染 → 必須回溯
cur_sum(int) cur_sum + 38(新的 int,區域的) 子層框架被銷毀 → 父層的 cur_sum 仍是 5不需要回溯
path /累加器型別 可變嗎? 每次呼叫都是新物件? 需要回溯(pop)?
intcur_sum 是(n + x 重新綁定)
str 是(s + x
tuple 是(t + (x,)
list + tmp + [x] 否(重新綁定)
list + append 否(共用) 是 — path.pop()

實作對照 — LC 445 Add Two Numbers II 與 LC 394 Decode String

  • String -> Int
text
# 445 Add Two Numbers II
# 394 Decode String
def str_2_int(x):
    r=0
    for i in x:
        r = int(r)*10 + int(i)
        print (i, r)
    return r

def str_2_int_v2(x):
    res = 0
    for i in x:
        res = (res + int(i) % 10) * 10
    return int(res / 10)

# example 1
x="131"
r=str_2_int(x)
print (r)
# 1 1
# 3 13
# 1 131
# 131

# examle 2
In [62]: z
Out[62]: '5634'

In [63]: ans = 0

In [64]: for i in z:
    ...:     ans = 10 * ans + int(i)
    ...:

In [65]: ans
Out[65]: 5634