01

電腦科學在研究什麼

電腦科學是研究問題與解法的學問;演算法是有限步驟的解法,而有些問題根本無解。

先讀原文開場,旁邊就是白話

這是一本英文書。左邊放原文、右邊放白話導讀——你既讀得懂,也順手碰了原文。

原文 · 演算法與資料結構入門 CHAPTER ONE INTRODUCTION 1. 1 Objectives • To review the ideas of computer science, programming, and problem-solving. • To understand abstraction and the role it plays in the problem-solving process. • To understand and implement the notion of an abstract data type.
白話導讀

電腦科學是研究問題與解法的學問;演算法是有限步驟的解法,而有些問題根本無解。

問題、演算法與可計算性

電腦科學是研究問題與解法的學問;演算法是有限步驟的解法,而有些問題根本無解。

STEP 1

深度探秘

電腦科學其實不是『學電腦』

名字騙了你

「電腦科學 (Computer Science)」這個名字其實很容易誤導人。它不是研究電腦這台機器本身的學問,電腦只是過程中很重要的工具而已。

電腦科學是研究問題 (problems)解決問題的過程 (problem-solving),以及這個過程產出的解法 (solutions) 的學問。

演算法 = 解法

面對一個問題,電腦科學家的目標是發展出一個 演算法 (algorithm)

  • 一份一步一步的指令清單
  • 能解決這個問題的任何一個實例 (instance)
  • 而且是有限的過程(照著做一定會停、會得到答案)

換句話說,演算法就是解法本身,它跟用哪台機器、哪種程式語言無關。

有些問題根本無解

要小心一件事:不是所有問題都有解法。有些問題已被證明不存在任何演算法可以解它。所以更完整的定義是:電腦科學同時研究『有解的問題』與『無解的問題』。

💡
關鍵

電腦科學研究的是問題與解法,演算法就是解法,電腦只是工具。

STEP 2

生活妙喻

演算法就像一份食譜

食譜與料理

問題想成「我想吃一盤蛋炒飯」,那麼演算法就是那份食譜

  • 食譜有明確的步驟:先打蛋、熱油、下飯、翻炒、調味、起鍋。
  • 它能應付任何一鍋飯(不同分量、不同鍋子)——這就是『解決任何實例』。
  • 它一定會結束:照著做完就有飯吃,不會永遠炒下去。

而真正的爐子、鍋子就像電腦——只是讓食譜得以執行的工具。同一份食譜換一個廚房也能做出同樣的菜。

有些料理做不出來

如果有人要你『煮出一鍋永遠吃不完又永遠是熱的飯』,再厲害的食譜也寫不出來——這就對應到『無解的問題』。電腦科學家的一部分工作,就是分辨哪些『料理』根本做不出來。

💡
關鍵

演算法像食譜:明確步驟、能應付任何實例、保證會結束,而電腦只是廚房。

STEP 3

可計算性

computable 是什麼意思

可計算 (computable)

當一個問題存在能解它的演算法時,我們說這個問題是可計算的 (computable)。於是電腦科學還有另一種定義方式:

電腦科學是研究『可計算與不可計算問題』、研究『演算法存在與不存在』的學問。

注意:在這整個定義裡,『電腦』這個詞根本沒出現過——因為解法與機器是獨立的

三種問題,三種命運

flowchart TD
    A[一個問題] --> B{存在演算法嗎}
    B -->|存在且夠快| C[可計算且實用]
    B -->|存在但太慢| D[難解 intractable]
    B -->|不存在| E[無解問題]

學會分辨這三種,是後面學分析演算法的重要起點:有解、無解、以及『有解但代價太高』。

💡
關鍵

問題若存在解它的演算法就是可計算的;解法獨立於機器之外。

🔆
生活妙喻:演算法 ≈ 食譜

食譜用明確且有限的步驟,能料理任何一鍋食材,正如演算法用有限步驟解決問題的任何實例。

🔆
生活妙喻:電腦 ≈ 廚房的爐子與鍋子

爐具只是讓食譜得以執行的工具,同一份食譜換廚房也能做菜,正如解法獨立於機器之外。

本節字彙

演算法 (algorithm)
一份一步一步、有限的指令清單,照著做就能解決某類問題的任何實例。
🧠 把它想成『解法的食譜』:步驟明確、保證做得完。
可計算 (computable)
若一個問題存在能解它的演算法,這個問題就是可計算的。
🧠 computable = 有食譜可煮;找不到食譜就不可計算。
實例 (instance)
同一類問題的某個具體版本,例如『排序這串特定數字』是『排序問題』的一個實例。
🧠 問題是『題型』,實例是『這一題』。
依本節的定義,下列哪一句最貼近『電腦科學』真正研究的核心?
下列哪個敘述『不』符合演算法的特性?
某問題目前找不到任何演算法可以解它,且已被證明這樣的演算法不存在。依本節說法,這代表什麼?

抽象化:邏輯觀點與實體觀點

用開車與數學模組的比喻,分清楚『介面』與『底層細節』,理解程序抽象化與黑盒子概念。

STEP 1

深度探秘

把『怎麼用』和『怎麼做』分開

抽象化 (abstraction)

解決問題時,複雜度常常讓人迷失在細節裡。抽象化是電腦科學最重要的武器之一,它讓我們把同一件事拆成兩種觀點:

  • 邏輯觀點 (logical perspective):使用者怎麼用這個東西。
  • 實體觀點 (physical perspective):底層怎麼運作的所有細節。

介面是兩者的橋

連接使用者與底層複雜度的橋樑,就是介面 (interface)。只要你懂介面怎麼用,就不需要知道內部細節。

使用抽象的人有時稱為客戶 (client)

觀點 是誰 關心什麼
邏輯觀點 使用者 / 客戶 介面怎麼用、能做什麼
實體觀點 實作者 / 維護者 內部如何運作的細節
💡
關鍵

抽象化把『邏輯觀點(怎麼用)』與『實體觀點(怎麼做)』分開,介面是兩者的橋。

STEP 2

生活妙喻

開車的人 vs. 修車的技師

同一台車,兩種眼光

想想你今天開的那台車:

  • 身為駕駛,你只需要轉鑰匙、踩油門、打方向盤——你看到的是車子的邏輯觀點,用設計者提供的功能把自己從 A 載到 B。這些功能就是車子的介面
  • 技師看到的是實體觀點:引擎怎麼運轉、變速箱怎麼換檔、溫度怎麼控制——也就是『引擎蓋底下』的細節。

共通點

兩個角色的共通點是:駕駛(客戶)不需要懂引擎蓋底下的事,只要會用介面就好

flowchart LR
    A[駕駛 客戶] -->|透過介面 油門方向盤| B[車子]
    B -.隱藏.-> C[引擎 變速箱 等實體細節]
💡
關鍵

駕駛只需懂介面(邏輯觀點),技師才需懂引擎蓋底下(實體觀點)。

STEP 3

實用超能力

程序抽象化與黑盒子

數學模組的例子

在 Python 裡呼叫平方根,就是一個程序抽象化 (procedural abstraction) 的範例:

>>> import math
>>> math.sqrt(16)
4.0

我們不知道它內部到底怎麼算平方根,但我們知道:

  • 函式叫什麼名字(sqrt
  • 要餵它什麼(參數 16)
  • 它會回傳什麼(4.0)

黑盒子 (black box)

這就是所謂的黑盒子觀點:只描述介面(名稱、參數、回傳值),把細節藏在裡面。

這是你未來寫程式的超能力:把複雜的東西包成一個你信任、會用就好的盒子,專心去解更大的問題。

💡
關鍵

程序抽象化讓你像用黑盒子一樣:只需知道名稱、參數、回傳值就能使用。

🔆
生活妙喻:邏輯觀點 vs. 實體觀點 ≈ 開車的駕駛 vs. 修車的技師

駕駛只透過油門方向盤等介面用車(邏輯觀點),技師才需要懂引擎蓋底下的運作(實體觀點)。

🔆
生活妙喻:程序抽象化 ≈ 黑盒子

math.sqrt 像個黑盒子,你只看得到輸入與輸出,看不到也不需要看到內部如何運算。

本節字彙

抽象化 (abstraction)
把『怎麼用』與『怎麼做』分開的思考方式,讓人專注於大方向而不迷失在細節裡。
🧠 抽象化 = 只看『要做什麼』,先不管『怎麼做到』。
介面 (interface)
使用者與底層複雜實作之間溝通的方式,例如函式的名稱、參數與回傳值。
🧠 介面就是『使用說明書』,懂它就會用,不必拆開機器。
程序抽象化 (procedural abstraction)
把一段運算包成函式,使用者只需知道名稱、參數與回傳值,不必知道內部實作。
🧠 把運算裝進黑盒子,貼上『名稱+輸入+輸出』的標籤。
你呼叫 `math.sqrt(25)` 得到 5.0,卻完全不知道它內部用什麼方法算出來的。這最能說明哪個觀念?
用『駕駛』與『技師』的比喻來看,下列對應何者正確?
一個設計良好的『介面』最重要的價值是什麼?
02

為什麼要學資料結構與演算法

ADT 是資料的邏輯描述(封裝與資訊隱藏),資料結構則是它的實作;兩者分離帶來實作獨立性。

抽象資料型別 ADT 與資料結構

ADT 是資料的邏輯描述(封裝與資訊隱藏),資料結構則是它的實作;兩者分離帶來實作獨立性。

STEP 1

深度探秘

ADT 是『資料的黑盒子』

從程序抽象化到資料抽象化

前面看到的程序抽象化是『隱藏一個函式的細節』;現在我們把同樣的想法用在資料上,這叫資料抽象化 (data abstraction)

抽象資料型別 (Abstract Data Type, 簡稱 ADT) 是:

對『我們如何看待這份資料』以及『允許對它做哪些操作』的邏輯描述,完全不管它將來怎麼被實作

換句話說,ADT 只關心資料代表什麼、能做什麼,不關心怎麼蓋出來

封裝與資訊隱藏

  • 封裝 (encapsulation):在資料外圍包一層殼,使用者只透過殼上的操作來互動。
  • 資訊隱藏 (information hiding):把實作細節藏在殼裡,使用者看不到。
flowchart TD
    U[使用者 client] -->|只用介面操作| S[ADT 外殼 介面]
    S -.資訊隱藏.-> I[實作 資料結構]
💡
關鍵

ADT 是資料的邏輯描述:只說『是什麼、能做什麼』,用封裝與資訊隱藏把細節藏起來。

STEP 2

生活妙喻

點餐單 vs. 廚房

餐廳的菜單與廚房

ADT 想成餐廳的菜單,把資料結構想成廚房

  • 菜單 (ADT) 告訴你:有哪些餐點、你可以點什麼、會端出什麼。這是『邏輯描述』。
  • 廚房 (資料結構) 才是真正用鍋碗瓢盆把菜做出來的地方,那是『實體實作』。

你身為客人(client),只看菜單點餐,完全不需要知道廚房怎麼運作——這就是資訊隱藏。

換廚房,不換菜單

最妙的是:餐廳今天換了一批新廚具、改了烹調流程(換掉資料結構的實作),只要菜單不變,你點餐的方式完全不用改。這就是接下來要講的『實作獨立性』。

💡
關鍵

ADT 像菜單(你怎麼點、會得到什麼),資料結構像廚房(實際怎麼做出來)。

STEP 3

實用超能力

資料結構與實作獨立性

資料結構 = ADT 的實作

資料結構 (data structure) 就是 ADT 的實作:它用程式語言的建構與基本型別,提供資料的實體觀點

概念 對應觀點 比喻
ADT 邏輯觀點(是什麼、能做什麼) 菜單
資料結構 實體觀點(怎麼蓋出來) 廚房

實作獨立性 (implementation independence)

一個 ADT 通常有很多種實作方式。把邏輯與實體分開,帶來一個超能力:

程式設計師可以換掉內部實作的細節(換一種更快的資料結構),而完全不影響使用者與資料互動的方式。

這讓使用者能持續專注在解題本身,而不被底層細節綁架。

💡
關鍵

資料結構是 ADT 的實作;分離兩者帶來實作獨立性,可換實作而不影響使用者。

🔆
生活妙喻:ADT 與資料結構的關係 ≈ 餐廳菜單與廚房

菜單(ADT)描述你能點什麼、會得到什麼;廚房(資料結構)才是真正做菜的地方。換廚房不換菜單,點餐方式照舊。

🔆
生活妙喻:資訊隱藏 ≈ 廚房門上的『非工作人員請勿進入』

客人看不到也不需要看到廚房內部,正如資訊隱藏把實作細節擋在介面之外。

本節字彙

抽象資料型別 (ADT)
對資料『是什麼、允許哪些操作』的邏輯描述,與將來如何實作無關。
🧠 ADT = 菜單:只列『你能點什麼』,不寫『廚房怎麼煮』。
資料結構 (data structure)
ADT 的實作,用程式語言的建構與基本型別提供資料的實體觀點。
🧠 資料結構 = 廚房:真正把菜做出來的地方。
資訊隱藏 (information hiding)
把實作細節封裝在介面後面,讓使用者看不到也不需要知道。
🧠 細節關進廚房,客人只看得到菜單。
實作獨立性 (implementation independence)
使用者透過 ADT 介面互動,因此可以替換底層實作而不影響使用方式。
🧠 換廚房不換菜單,客人點餐照舊。
下列哪一句最精準描述『抽象資料型別 (ADT)』?
用菜單與廚房的比喻,『資料結構』對應的是哪一個,為什麼?
團隊把某 ADT 底層的實作從一種資料結構換成另一種更快的,使用者的程式碼卻完全不用改。這展現了什麼?

為什麼要研究演算法

同一個問題常有多種解法,學會分析與比較演算法的好壞,並認識難解 (intractable) 問題與取捨。

STEP 1

深度探秘

同一題,多種解法

解法不只一種

電腦科學家是從經驗中學習的:看別人解題、自己動手解題,累積出模式辨識 (pattern recognition) 的能力,下次遇到類似問題就能更快上手。

關鍵體悟是:同一個問題往往有很多不同的演算法

以前面的 sqrt 為例,計算平方根可以有很多種實作方式:

  • 有的演算法用掉少很多的資源。
  • 有的演算法可能要花10 倍的時間才回傳結果。

兩個都『能跑、都對』,但其中一個顯然更好

我們需要一把尺

既然解法不只一種,我們就需要一套方法來比較與評估它們的好壞。

💡
關鍵

同一問題常有多種演算法,效率可能天差地遠,所以我們需要方法來比較好壞。

STEP 2

生活妙喻

從家到公司的多條路線

導航 App 的多條路線

『從家到公司』是一個問題,而每一條路線都是一個演算法:

  • 走高速公路:里程長但通常快。
  • 走市區小路:里程短但一堆紅綠燈。
  • 走山路:風景好但耗油又耗時。

每一條路線都能到達公司(都正確),但成本(時間、油錢)差很多。導航 App 做的事,就是幫你比較與評估這些路線。

公平的比較

更重要的是:我們想比較的是路線本身的特性(距離、紅綠燈數),而不是你今天開的是哪台車或路上塞不塞——也就是要排除掉與『解法本身』無關的干擾因素。

分析演算法,就是只看演算法本身的特性來比較,不被『程式怎麼寫』或『用哪台機器』影響。

💡
關鍵

多條路線都能到達,但成本不同;分析演算法就是比較路線本身,而非車子或路況。

STEP 3

實用超能力

難解問題與取捨

難解 (intractable) 問題

最糟的情況是:問題有解法,但這個解法需要不切實際的大量時間或資源才能跑完。這種問題稱為難解 (intractable)

所以我們要能分辨三種狀況:

flowchart TD
    P[一個問題] --> A{有演算法嗎}
    A -->|有且實用| OK[可有效解決]
    A -->|有但太耗資源| HARD[難解 intractable]
    A -->|沒有| NO[無解]

取捨 (trade-offs)

真實世界裡常有取捨要做:

  • 一個演算法跑得快,但很吃記憶體。
  • 另一個省記憶體,但比較慢。

身為解題者,除了會解問題,還要懂解法評估技術:找到一個解法之後,再判斷它是不是一個『好』解法——這件事你會一遍又一遍地做。

💡
關鍵

要分辨有解、無解與難解三種問題,並學會在時間與空間等取捨間做評估。

🔆
生活妙喻:比較不同演算法 ≈ 導航 App 比較多條路線

從家到公司有很多路線,都能到達但成本不同;分析演算法就是比較路線本身的特性,而非車子或路況。

🔆
生活妙喻:難解問題 ≈ 理論上能走但要走一萬年的路

難解問題有解法,但需要不切實際的時間,就像一條理論存在卻久到沒人走得完的路。

本節字彙

演算法分析 (analysis)
只根據演算法本身的特性(如所需時間、記憶體)來比較不同解法的好壞,不受程式寫法或機器影響。
🧠 比的是『路線本身』,不是『你的車』。
難解 (intractable)
問題雖有演算法可解,但需要不切實際的大量時間或資源才能完成。
🧠 走得到,但要走一萬年的那種路。
取捨 (trade-off)
在不同目標(如速度與記憶體)之間做出權衡的選擇,魚與熊掌常不可兼得。
🧠 快一點通常就吃記憶體一點,反之亦然。
兩個演算法都能正確算出平方根,但一個比另一個慢 10 倍。本節想傳達的主要訊息是?
『演算法分析』強調只看演算法本身的特性來比較。用導航比喻,這對應到?
某問題存在演算法,但跑完需要數百萬年。依本節術語,這類問題稱為?
03

Python 資料型別大複習

int、float、bool 與運算子,以及『變數持有的是資料的參考』這個關鍵觀念。

數值、布林與變數參考

int、float、bool 與運算子,以及『變數持有的是資料的參考』這個關鍵觀念。

STEP 1

深度探秘

int、float、bool 與運算子

Python 的原子型別

Python 把資料看成核心,是物件導向語言。最基本的『原子 (atomic)』型別有:

  • int:整數(如 23、-19)
  • float:浮點數(如 6.5)
  • bool:布林值,只有 TrueFalse

算術運算子的眉角

print(2 + 3 * 4)    # 14, 先乘後加
print((2 + 3) * 4)  # 20, 括號改變順序
print(2 ** 10)      # 1024, ** 是次方
print(6 / 3)        # 2.0, 兩整數相除得 float
print(7 // 3)       # 2, // 是整數除法 取整
print(7 % 3)        # 1, % 是取餘數 modulo

重點:兩個整數用 / 相除,結果是浮點數;想取整數部分要用 //,想取餘數要用 %

布林與比較

比較運算(==><= 等)的結果是布林值,可用 andornot 組成更複雜的判斷:

print((5 >= 1) and (5 <= 10))  # True
print(not (False or True))     # False
💡
關鍵

int、float、bool 是原子型別;// 取整、% 取餘、** 次方,比較與邏輯運算產生布林值。

STEP 2

生活妙喻

變數是貼在物品上的標籤

變數持有的是『參考』

這是 Python 一個關鍵又容易誤會的觀念:變數持有的不是資料本身,而是指向資料物件的『參考 (reference)』

把變數想成一張便利貼標籤,資料物件則是倉庫裡的箱子:

  • the_sum = 0:貼一張寫著 the_sum 的標籤到『0』這個箱子上。
  • the_sum = the_sum + 1:算出『1』這個新箱子,把標籤撕下來改貼到『1』上。
  • the_sum = True:再把同一張標籤改貼到『True』這個箱子上。
flowchart LR
    L[標籤 the_sum] -->|現在指向| B1[資料物件 True]
    B0[資料物件 0]
    B2[資料物件 1]

動態型別

因為標籤可以隨時改貼到不同型別的箱子上,所以同一個變數可以先是整數、後變布林。這就是 Python 的動態 (dynamic) 特性。

💡
關鍵

變數像可重貼的標籤,指向資料物件;改值就是改貼標籤,型別也隨之改變。

STEP 3

實用超能力

命名與賦值的好習慣

識別字 (identifier) 規則

變數名稱(識別字)的規則:

  • 字母或底線 _ 開頭
  • 大小寫有別Sumsum 是兩個不同名字)
  • 長度不限

取有意義的名字

# 不好:別人看不懂
x = 3.14159 * r * r

# 好:一看就懂
area = 3.14159 * radius * radius

用能表達意義的名字,程式碼會好讀很多——這是寫給『未來的你』看的禮物。

賦值的運作順序

賦值時,Python 會先算右邊,再把結果物件的參考『指派』給左邊的名字:

the_sum = the_sum + 1  # 先算右邊 舊值+1, 再把標籤改貼過去
💡
關鍵

識別字以字母或底線開頭、大小寫有別;賦值先算右邊,再把名字指向結果。

🔆
生活妙喻:變數持有參考 ≈ 可重貼的便利貼標籤

變數是貼在資料箱子上的標籤,改值就是把標籤撕下來改貼到另一個箱子,因此同一變數能指向不同型別的資料。

🔆
生活妙喻:動態型別 ≈ 同一張標籤換貼不同箱子

標籤可貼整數箱也可貼布林箱,所以變數型別會隨它當下指向的資料而改變。

本節字彙

參考 (reference)
變數實際持有的東西:一個指向某個資料物件的『連結』,而不是資料本身。
🧠 變數是標籤,標籤上的箭頭指向資料箱子。
整數除法 //
回傳商的整數部分,捨去小數,例如 7 // 3 得到 2。
🧠 雙斜線『砍掉』小數,只留整數。
取餘數 %
回傳除法的餘數,例如 7 % 3 得到 1。
🧠 % 問的是『除不盡剩多少』。
動態型別 (dynamic typing)
變數的型別由它當下指向的資料決定,可隨賦值而改變。
🧠 標籤改貼哪種箱子,型別就跟著變。
在 Python 中,`print(7 // 3)` 與 `print(7 % 3)` 分別會印出什麼?
執行 `the_sum = 0` 後再執行 `the_sum = True`,關於 `the_sum` 的型別,正確的是?
為什麼說『變數持有的是參考,而不是資料本身』這個觀念很重要?

有序集合:list、string、tuple

三種有序序列共用索引、切片、串接等操作;list 可變、string 與 tuple 不可變。

STEP 1

深度探秘

三種有序序列

序列家族

Python 有三種有序集合 (ordered collection),又稱序列 (sequence)

型別 寫法 範例 可變?
list 方括號 [] [1, 3, True, 6.5] 可變
string 引號 '' "" 'David' 不可變
tuple 圓括號 () (2, True, 4.96) 不可變

list 與 tuple 是異質的 (heterogeneous)——同一個集合裡可以放不同型別的資料。

序列共用的操作

因為都是序列,它們共用一組操作:

my_list = [1, 3, True, 6.5]
my_list[0]        # 索引: 取第 0 個
my_list[1:3]      # 切片: 取索引 1 到 2
len(my_list)      # 長度
[1, 2] + [3, 4]   # 串接 -> [1,2,3,4]
[0] * 6           # 重複 -> [0,0,0,0,0,0]
6.5 in my_list    # 成員測試 -> True

索引從 0 開始;切片 [1:3] 取到索引 3 之前(不含 3)。

💡
關鍵

list、string、tuple 都是有序序列,共用索引、切片、串接、重複、len、in 等操作。

STEP 2

生活妙喻

可變 vs. 不可變:白板與石碑

mutability(可變性)

list 與 string/tuple 最大的差別是可變性 (mutability)

  • list 是可變的 (mutable):像一塊白板,可以隨時擦掉某格重寫。
  • string 與 tuple 是不可變的 (immutable):像刻好的石碑,刻上去就不能改某個字。
my_list[0] = 1024   # OK, list 可改某格

my_name = 'David'
my_name[0] = 'X'    # 報錯 TypeError: 不支援 item assignment

為什麼要分這個?

想改某一格內容時,用 list;想要一份保證不會被偷改的資料時,用 tuple 或 string 更安全。這個差別在後面實作資料結構時會非常重要。

💡
關鍵

list 可變(像白板可改某格),string 與 tuple 不可變(像石碑不能改某字)。

STEP 3

實用超能力

list 方法、range 與小陷阱

常用的 list 方法

my_list = [1024, 3, True, 6.5]
my_list.append(False)   # 尾端加一個
my_list.insert(2, 4.5)  # 在索引 2 插入
my_list.pop()           # 移除並回傳最後一個
my_list.pop(1)          # 移除並回傳索引 1
my_list.sort()          # 就地排序
my_list.reverse()       # 就地反轉
my_list.index(4.5)      # 找第一個出現的索引

有些方法(如 pop)會回傳值並修改 list;有些(如 reverse)只修改、不回傳。

range 產生數列

list(range(10))       # [0,1,...,9]
list(range(5, 10))    # [5,6,7,8,9]
list(range(5, 10, 2)) # [5,7,9] 每次跳 2
list(range(10,1,-1))  # [10,9,...,2] 倒著走

重複運算子的陷阱

my_list = [1, 2, 3, 4]
A = [my_list] * 3   # A 裝的是 3 個指向同一個 list 的參考
my_list[2] = 45     # 改一個, 三份都跟著變!

因為 * 複製的是參考而非資料本身,這是初學者很容易踩到的坑。

💡
關鍵

list 有 append/insert/pop/sort 等方法;range 產生數列;重複運算子複製的是參考要小心。

🔆
生活妙喻:list 可變 vs. string/tuple 不可變 ≈ 白板 vs. 石碑

list 像白板可隨時擦改某一格;string 與 tuple 像刻好的石碑,內容固定不能改某個字。

🔆
生活妙喻:重複運算子複製參考 ≈ 三面鏡子照同一個人

[my_list]*3 像三面鏡子映出同一個人,本人一動三個影像都跟著動,因為它們指向同一份資料。

本節字彙

序列 (sequence)
有序的集合,元素有先後順序與索引;list、string、tuple 都是序列。
🧠 排好隊的資料,每個都有編號(索引)。
可變 / 不可變 (mutable / immutable)
可變物件(如 list)能就地修改內容;不可變物件(如 string、tuple)建立後不能改某個元素。
🧠 可變=白板,不可變=石碑。
切片 (slicing)
用 [i:j] 取出序列的一段,從索引 i 到 j 之前(不含 j)。
🧠 [1:3] 取 1、2,到 3 就停(不含 3)。
range
產生一段整數數列的物件,可指定起點、終點與間隔,終點不包含在內。
🧠 range(5,10) 是 5 到 9,10 永遠到不了。
下列哪一組型別『全部』都是不可變 (immutable) 的?
`my_name = 'David'` 後執行 `my_name[0] = 'X'` 會發生什麼?
`my_list = [10, 20, 30, 40, 50]`,那麼 `my_list[1:3]` 的結果是?

無序集合:set 與 dictionary

set 不允許重複、支援集合運算;dictionary 以 key-value 配對儲存、用鍵存取。

STEP 1

深度探秘

set:不重複的無序集合

set 是什麼

set(集合) 是一個無序不允許重複的集合,元素必須是不可變的物件。寫法是用大括號 {} 包起來、逗號分隔:

my_set = {3, 6, 'cat', 4.5, False}

注意:空集合要寫 set(),不能寫 {}(那會變成空字典)。

set 的運算(像數學集合)

my_set = {3, 6, 'cat'}
your_set = {99, 3, 100}
my_set | your_set   # 聯集 union
my_set & your_set   # 交集 intersection -> {3}
my_set - your_set   # 差集 difference
{3, 100} <= your_set  # 子集判斷 -> True
len(my_set)         # 元素個數
3 in my_set         # 成員測試

還有 addremovepopclear 等方法。因為不重複,set 很適合用來『去除重複』。

💡
關鍵

set 是無序、不重複的集合,支援聯集、交集、差集、子集等數學運算;空集合用 set()。

STEP 2

生活妙喻

dictionary:通訊錄查號簿

dictionary(字典)

dictionary(字典) 是無序的集合,由一組組鍵-值配對 (key-value pair) 組成,寫成 key:value

capitals = {'Iowa': 'DesMoines', 'Wisconsin': 'Madison'}
print(capitals['Iowa'])         # 用鍵查值 -> DesMoines
capitals['Utah'] = 'SaltLakeCity'  # 新增一對

像一本通訊錄

把字典想成通訊錄查號簿

  • 鍵 (key) 是『人名』(Iowa、Wisconsin)。
  • 值 (value) 是對應的『電話/首府』(DesMoines、Madison)。
  • 你用人名(鍵)去查電話(值),而不是用編號。
flowchart LR
    K1[鍵 Iowa] --> V1[值 DesMoines]
    K2[鍵 Wisconsin] --> V2[值 Madison]

字典沒有固定順序,鍵的擺放位置取決於一種叫雜湊 (hashing) 的技術(第 4 章會細講)。

💡
關鍵

dictionary 用鍵-值配對儲存,像通訊錄用人名查電話;無固定順序,靠雜湊決定位置。

STEP 3

實用超能力

字典的方法與安全取值

常用操作與方法

phone_ext = {'david': 1410, 'brad': 1137}
'brad' in phone_ext          # 鍵是否存在 -> True
del phone_ext['david']       # 刪除一對
list(phone_ext.keys())       # 所有鍵
list(phone_ext.values())     # 所有值
list(phone_ext.items())      # 所有 鍵,值 配對

get:安全地取值

直接用 my_dict[k] 取一個不存在的鍵報錯get 比較安全:

phone_ext.get('kent')             # 鍵不存在 -> 回傳 None, 不報錯
phone_ext.get('kent', 'NO ENTRY') # 鍵不存在 -> 回傳預設值 'NO ENTRY'

小技巧:當你不確定鍵在不在時,用 get 搭配預設值,可以避免程式因 KeyError 中斷。

💡
關鍵

用 in 判斷鍵、del 刪除、keys/values/items 取內容;get 可安全取值並設預設,避免報錯。

🔆
生活妙喻:dictionary 鍵值對 ≈ 通訊錄查號簿

用人名(鍵)查電話(值),而非用流水號;字典也是用鍵去查對應的值。

🔆
生活妙喻:set 不允許重複 ≈ 派對賓客名單

同一個人不管被邀請幾次,名單上只會出現一次,正如 set 自動去除重複元素。

本節字彙

set(集合)
無序、不允許重複的集合,支援聯集、交集、差集等運算;空集合寫成 set()。
🧠 像賓客名單:每個人只列一次。
dictionary(字典)
由鍵-值配對組成的無序集合,用鍵來存取對應的值。
🧠 通訊錄:用人名(鍵)查電話(值)。
鍵-值配對 (key-value pair)
字典的基本單位,一個鍵對應一個值,寫成 key:value。
🧠 鍵是『問題』,值是『答案』。
get 方法
安全取值的方法:鍵不存在時回傳 None 或指定的預設值,而不會報錯。
🧠 查不到時不發脾氣,回傳 None 或你給的備案。
把清單 `[3, 3, 6, 6, 6, 'cat']` 轉成 set 後,最可能的結果特性是?
`capitals = {'Iowa': 'DesMoines'}`,要取得 Iowa 的首府,應該怎麼寫?
`{3, 6} & {6, 9}` 與 `{3, 6} | {6, 9}` 的結果分別是?
04

Python 控制流程與程式組織

input 讀進來都是字串、print 的彈性輸出,以及用格式運算子排版輸出。

輸入輸出與字串格式化

input 讀進來都是字串、print 的彈性輸出,以及用格式運算子排版輸出。

STEP 1

深度探秘

input 讀進來永遠是字串

input 函式

程式常需要跟使用者互動。Python 用 input 函式請使用者輸入資料,它接受一個字串參數作為提示 (prompt)

user_name = input('請輸入你的名字:')

使用者打的東西會存進 user_name

關鍵陷阱:回傳的永遠是 str

input 回傳的永遠是字串,即使使用者打的是數字!

想當數字算術,必須自己明確轉型

user_radius = input('請輸入半徑:')
radius = float(user_radius)   # 字串轉浮點數
diameter = 2 * radius          # 現在才能算術

如果忘了轉型直接 user_radius * 2,會變成把字串重複兩次而不是乘以二,這是很常見的 bug。

💡
關鍵

input 回傳的永遠是字串,要做數字運算前必須用 int() 或 float() 明確轉型。

STEP 2

生活妙喻

print 像可調整的印章

print 的彈性

print 可印零個或多個值,預設用一個空白分隔、結尾換行。這兩個行為都能調:

print('Hello', 'World')                # Hello World
print('Hello', 'World', sep='***')     # Hello***World
print('Hello', 'World', end='***')     # Hello World***(不換行)

像一台可換設定的印章機

print 想成一台印章機

  • sep(分隔):決定多個字之間蓋什麼分隔符(預設是空格)。
  • end(結尾):決定每次蓋完印章後接什麼(預設是換行)。

換掉這兩個設定,就能控制輸出長相,而不必改動要印的內容本身。

💡
關鍵

print 可印多個值,用 sep 調整分隔符、end 調整結尾(預設換行),靈活控制輸出外觀。

STEP 3

實用超能力

格式字串:填空模板

格式字串 (formatted string)

想更精細控制輸出排版時,用格式字串:一個帶佔位符的模板,再把變數填進去。%格式運算子

name = 'Ann'
age = 18
print('%s is %d years old.' % (name, age))
# Ann is 18 years old.
  • 左邊是模板,含轉換規格:%s(字串)、%d(整數)、%f(浮點數)。
  • 右邊是要填入的值(依序對應)。

加上格式修飾子

% 與轉換字元之間可加修飾子,控制欄寬與小數位:

item = 'banana'; price = 24
print('The %+10s costs %5.2f cents' % (item, price))
# %5.2f: 欄寬 5、小數 2 位 -> 24.00
規格 意思
%d 整數
%f 浮點數
%s 字串
%5.2f 欄寬 5、小數 2 位

右邊值的個數要與模板中 % 的個數一致,否則會出錯。

💡
關鍵

格式字串用 % 把變數填進模板,%s/%d/%f 指定型別,並可加修飾子控制欄寬與小數位。

🔆
生活妙喻:print 的 sep 與 end ≈ 可換設定的印章機

sep 決定字與字之間蓋什麼分隔、end 決定每次印完接什麼,換設定就能改變輸出外觀而不動內容。

🔆
生活妙喻:格式字串 ≈ 填空題的模板

格式字串像一張留好空格的模板,%s、%d 是空格的型別標示,右邊的值依序填進去。

本節字彙

input
請使用者輸入資料的函式,接受一個提示字串,回傳值永遠是字串 (str)。
🧠 不管打什麼進去,拿回來的都是字串。
型別轉換 (type conversion)
把資料從一種型別轉成另一種,如 int()、float()、str()。
🧠 輸入要算數,先 float() 或 int() 一下。
格式運算子 %
把右邊的值依照左邊模板中的轉換規格填入字串的運算子。
🧠 % 左邊是模板,右邊是要填的值。
轉換規格 (%s/%d/%f)
格式字串中標示要填入值之型別的記號:%s 字串、%d 整數、%f 浮點數。
🧠 s=string、d=digit(整數)、f=float。
使用者在 `n = input('請輸入數字:')` 輸入了 5,接著執行 `print(n * 2)`,會印出什麼?
想把使用者輸入的半徑拿來做算術,正確的做法是?
`print('A', 'B', sep='-', end='!')` 會輸出什麼(用文字描述)?

迭代與選擇

while 與 for 兩種迴圈、if/else 選擇,以及一行搞定的串列推導式。

STEP 1

深度探秘

兩種迴圈:while 與 for

迭代 (iteration)

演算法需要『重複』的能力。Python 有兩種迴圈:

while:條件成立就一直做

counter = 1
while counter <= 5:
    print('Hello, world')
    counter = counter + 1

每次重複前都會先檢查條件,True 才執行本體。條件可以是複合的:

while counter <= 10 and not done:
    ...

for:走訪序列或 range

for item in [1, 3, 6, 2, 5]:
    print(item)

for item in range(5):     # 定次迭代 0~4
    print(item ** 2)

for 可走訪任何序列(list、tuple、string),也常搭配 range定次迭代 (definite iteration)

注意 Python 用縮排而非大括號來標示迴圈本體,縮排是語法的一部分。

💡
關鍵

while 依條件重複(每次先檢查),for 走訪序列或用 range 做定次迭代;縮排標示本體。

STEP 2

生活妙喻

選擇就是路口的紅綠燈

選擇 (selection)

選擇讓程式問問題、再依答案做不同的事。把它想成路口的紅綠燈與分岔路

if/else:雙向分岔

if n < 0:
    print('抱歉,值是負數')
else:
    print(math.sqrt(n))

像路口問『紅燈嗎?』——是就停,否就走,兩條路擇一

巢狀選擇:一連串的分岔

if score >= 90:
    print('A')
else:
    if score >= 80:
        print('B')
    else:
        if score >= 70:
            print('C')
        else:
            print('D 或 F')

單向 if:只有『要不要做』

if n < 0:
    n = abs(n)   # 是負數才取絕對值
print(math.sqrt(n))  # 不管怎樣都會執行這行

單向 if 像『前方施工才繞道,否則直接直行』。

💡
關鍵

if/else 是雙向分岔、可巢狀串接多個條件;單向 if 只決定『要不要做某事』,之後照常往下。

STEP 3

實用超能力

一行搞定的串列推導式

串列推導式 (list comprehension)

當你想『用迭代+選擇建立一個新 list』,串列推導式能把好幾行濃縮成一行。

從迴圈版到推導式版

# 迴圈版:前 10 個完全平方數
sq_list = []
for x in range(1, 11):
    sq_list.append(x * x)

# 推導式版:一行搞定
sq_list = [x * x for x in range(1, 11)]
# [1, 4, 9, ..., 100]

加上篩選條件

sq_list = [x * x for x in range(1, 11) if x % 2 != 0]
# 只留奇數的平方 -> [1, 9, 25, 49, 81]

結尾加 if 條件 就能只把符合的項目放進新 list。

[ch.upper() for ch in 'comprehension' if ch not in 'aeiou']
# ['C','M','P','R','H','N','S','N'] 去掉母音並轉大寫

任何支援迭代的序列都能用在推導式裡,是非常實用、Pythonic 的寫法。

💡
關鍵

串列推導式把迭代與選擇濃縮成一行來建立新 list,結尾加 if 可篩選項目。

🔆
生活妙喻:if/else 選擇 ≈ 路口的紅綠燈與分岔路

程式在路口問一個問題,依答案走不同的路;if/else 是雙向擇一,巢狀則是一連串分岔。

🔆
生活妙喻:for 定次迭代 ≈ 點名簿逐一唱名

for 走訪序列就像老師拿點名簿一個個唱名,每個元素都被輪到處理一次。

本節字彙

while 迴圈
只要條件為 True 就重複執行本體的迴圈,每次重複前先檢查條件。
🧠 『當…還成立時』就一直做。
for 迴圈
逐一走訪序列或 range 中每個元素的迴圈,適合定次迭代。
🧠 『對每一個元素』做一次。
定次迭代 (definite iteration)
事先知道要重複幾次的迭代,常用 for 搭配 range 實現。
🧠 次數先講好,跑 range 就對了。
串列推導式 (list comprehension)
用一行語法結合迭代(與可選的篩選)來建立新 list 的寫法。
🧠 [運算 for 元素 in 序列 if 條件]:一行造出新清單。
下列情境最適合用 `for` 搭配 `range` 的是?
關於單向 `if`(沒有 else),下列敘述何者正確?
`[x * x for x in range(1, 6)]` 的結果是?

例外處理

分清語法錯誤與邏輯錯誤,用 try/except 攔截例外,用 raise 主動拋出例外。

STEP 1

深度探秘

兩種錯誤:語法 vs. 邏輯

寫程式常見的兩類錯誤

語法錯誤 (syntax error)

程式碼結構寫錯,違反語言規則,連跑都跑不起來。例如 for 後面忘了冒號:

>>> for i in range(10)
SyntaxError: invalid syntax

剛學語言時最常犯這種錯。

邏輯錯誤 (logic error)

程式跑得起來,卻給出錯的結果——可能是演算法本身有問題,或翻譯成程式時出錯。

有些邏輯錯誤更嚴重,會在執行時引發致命狀況,例如除以零、或存取超出範圍的索引。這類執行期錯誤就稱為例外 (exception)

flowchart TD
    E[寫程式的錯誤] --> S[語法錯誤 結構寫錯 跑不起來]
    E --> L[邏輯錯誤 跑得起來但答案錯]
    L --> X[執行期例外 如除以零 索引越界]
💡
關鍵

語法錯誤是結構寫錯跑不起來;邏輯錯誤是跑得起來卻答案錯,嚴重時引發執行期例外。

STEP 2

生活妙喻

try/except 是安全網

例外被『raise(拋出)』

當例外發生,我們說它被**『raise(拋出)』了。預設情況下,例外會讓程式中斷**。

例如對負數開根號:

>>> print(math.sqrt(-23))
ValueError: math domain error

try/except:接住失誤的安全網

你可以用 try 區塊『嘗試』做某事,再用 except 區塊**『接住』萬一發生的例外,像走鋼索下方的安全網**:

try:
    print(math.sqrt(a_number))
except:
    print('開根號的值不合法')
    print('改用絕對值')
    print(math.sqrt(abs(a_number)))

如果 sqrt 拋出例外,程式不會中斷,而是掉進 except 區塊處理善後,再繼續往下跑。

安全網的價值:即使踩空了,表演(程式)也能優雅地繼續,而不是當場墜落(崩潰)。

💡
關鍵

例外發生時會被 raise,預設讓程式中斷;用 try/except 可像安全網般接住例外並優雅善後。

STEP 3

實用超能力

主動 raise 自己的例外

你也可以主動拋出例外

除了被動接住,程式設計師也能用 raise 主動拋出例外——當你偵測到一個不該發生的狀況時:

if a_number < 0:
    raise RuntimeError('不可以用負數')
else:
    print(math.sqrt(a_number))

這時程式一樣會因為例外而終止,但這個終止是你刻意設計的,而且訊息清楚說明了原因。

為什麼這是超能力

  • 提早攔截:在錯誤資料造成更大災難前就喊停。
  • 訊息明確:自訂的例外訊息讓除錯快很多。
  • 除了 RuntimeError,Python 還有 ValueErrorTypeError 等許多內建例外型別可用,也能自訂。

主動 raise 等於在程式裡裝『警報器』:一偵測到不合理的狀況,立刻響鈴並說清楚哪裡出問題。

💡
關鍵

用 raise 可主動拋出例外,在偵測到不合理狀況時提早喊停並給出清楚的錯誤訊息。

🔆
生活妙喻:try/except ≈ 走鋼索下方的安全網

try 區塊是走鋼索,except 是下方的安全網;萬一失誤(拋出例外),網子接住你,表演還能繼續而非墜落崩潰。

🔆
生活妙喻:主動 raise 例外 ≈ 裝在程式裡的警報器

偵測到不合理的狀況就主動拉警報(raise),立刻喊停並說明原因,避免錯誤資料釀成更大災難。

本節字彙

語法錯誤 (syntax error)
程式碼結構違反語言規則,導致無法執行,例如忘了冒號或括號不對。
🧠 句子文法寫錯,根本讀不通。
邏輯錯誤 (logic error)
程式能執行但結果錯誤,源自演算法或翻譯成程式時的錯誤。
🧠 跑得動,但答案不對。
例外 (exception)
執行期發生的錯誤(如除以零、索引越界),預設會中斷程式。
🧠 執行到一半突然『出狀況』。
try / except
try 嘗試執行可能出錯的程式碼,except 接住並處理發生的例外。
🧠 try 走鋼索,except 是安全網。
raise
主動拋出一個例外的敘述,常用於偵測到不合理狀況時提早喊停。
🧠 自己拉警報,主動喊停。
程式可以順利執行,但算出來的平均分數明顯是錯的。這屬於哪一類錯誤?
使用 `try/except` 包住 `math.sqrt(a_number)` 的主要好處是?
當一個例外發生卻沒有被任何 try/except 接住,預設會怎樣?

函式與類別

用函式隱藏計算細節,用 class 定義新型別來實作 ADT,認識建構子與 self。

STEP 1

深度探秘

用函式把細節裝進盒子

定義函式

函式讓我們把一段運算的細節藏起來(還記得程序抽象化嗎?)。一個函式定義需要:

  • 名稱
  • 一組參數 (parameters)
  • 本體 (body)
  • 可選擇地回傳 (return) 一個值
def square(n):
    return n ** 2

square(3)          # 9
square(square(3))  # 81 回傳值可再傳入

n形式參數 (formal parameter);呼叫時傳入的 3實際參數 (actual parameter)

用函式實作演算法

def square_root(n):
    root = n / 2          # 初始猜測為 n 的一半
    for k in range(20):   # 牛頓法 反覆逼近 20 次
        root = (1 / 2) * (root + (n / root))
    return root

牛頓法的更新公式為:

$$new_guess = \frac{1}{2}\left(old_guess + \frac{n}{old_guess}\right)$$

使用者只要呼叫 square_root(9) 得到 3.0,完全不需要知道牛頓法的細節——這就是抽象化的威力。# 之後的文字是註解,會被忽略。

💡
關鍵

函式有名稱、參數、本體與可選回傳值,能把運算細節藏進盒子,呼叫者不必懂內部實作。

STEP 2

生活妙喻

類別是『餅乾模具』

用 class 定義新型別

物件導向最強的能力,是讓你自訂新的類別 (class) 來模擬要解的資料。回想前面:ADT 描述資料的狀態 (state)行為 (method),而用 class 實作 ADT,就把抽象變成可用的東西。

餅乾模具與餅乾

class 想成餅乾模具,把 object(物件) 想成用模具壓出來的一塊塊餅乾

  • 模具(class)定義了餅乾長什麼樣、能做什麼
  • 每壓一次就產生一個實例 (instance)——一塊獨立的餅乾(object)。
class Fraction:
    def __init__(self, top, bottom):
        self.num = top      # 狀態 分子
        self.den = bottom   # 狀態 分母
flowchart LR
    C[class Fraction 模具] -->|壓一次| O1[物件 三分之五]
    C -->|再壓一次| O2[物件 二分之一]

同一個模具能壓出無數塊餅乾,每塊餅乾有自己的狀態(各自的分子分母)。

💡
關鍵

class 像餅乾模具,定義資料的狀態與行為;每個 object 是壓出來的一塊獨立餅乾(實例)。

STEP 3

實用超能力

建構子 __init__ 與 self

建構子 init

所有類別都該提供的第一個方法是建構子 (constructor),它定義物件如何被建立。在 Python 中建構子固定叫 __init__(init 前後各兩個底線):

class Fraction:
    def __init__(self, top, bottom):
        self.num = top
        self.den = bottom

my_fraction = Fraction(3, 5)  # 呼叫類別名 即觸發建構子

注意:我們用類別名來建立物件,而不直接呼叫 init

self 是什麼

  • self 是特殊參數,永遠是第一個形式參數,代表物件自己
  • 呼叫時不需要手動給 self 傳值。
  • self.num = top 表示『在這個物件身上,建立一個叫 num 的內部狀態』。

為什麼物件要會『自我介紹』

直接 print(my_fraction) 會印出像 <__main__.Fraction object at 0x...> 這種看不懂的東西,因為物件還不知道怎麼把自己變成漂亮的字串。要讓它好好顯示(如印成 3/5),就得告訴類別怎麼把自己轉成字串——這正是用方法擴充物件行為的起點。

💡
關鍵

建構子 __init__ 定義物件如何被建立並初始化狀態;self 永遠是第一個參數,代表物件自己。

🔆
生活妙喻:class 與 object ≈ 餅乾模具與餅乾

class 是模具,定義餅乾的樣子與功能;每壓一次就產生一塊獨立的餅乾(object),各有自己的狀態。

🔆
生活妙喻:函式隱藏細節 ≈ 自動販賣機

你投錢按鈕(給參數)就拿到飲料(回傳值),不必知道機器內部怎麼運作,正如函式把實作藏在裡面。

本節字彙

函式 (function)
有名稱、參數、本體並可回傳值的程式單位,用來封裝一段可重複使用的運算。
🧠 投入參數,吐出結果的小盒子。
類別 (class)
描述資料狀態與行為的藍圖,用來實作抽象資料型別 (ADT)。
🧠 餅乾模具:定義餅乾長怎樣、能做什麼。
物件 / 實例 (object / instance)
依類別建立出來的具體資料,每個物件有自己的狀態。
🧠 用模具壓出來的一塊塊餅乾。
建構子 __init__
建立物件時自動呼叫的方法,負責初始化物件的狀態。
🧠 物件出生時的『初始化儀式』。
self
方法中代表物件自己的特殊第一參數,呼叫時不需手動傳值。
🧠 self = 『我這個物件自己』。
呼叫 `square_root(9)` 得到 3.0,但你完全不需要知道它內部用了牛頓法。這再次體現了哪個觀念?
用『餅乾模具與餅乾』比喻,`class Fraction` 與 `Fraction(3, 5)` 分別對應?
關於建構子 `__init__`,下列何者正確?