diff --git a/weeks/week-18/solutions/1114405022-0622/AI_LOG.md b/weeks/week-18/solutions/1114405022-0622/AI_LOG.md new file mode 100644 index 000000000..45aa2530f --- /dev/null +++ b/weeks/week-18/solutions/1114405022-0622/AI_LOG.md @@ -0,0 +1,107 @@ +# AI_LOG — CPE 模擬實戰期末上機考 + +## 第一題:資料清理 (Data Cleaning) + +### 我問 AI 什麼 +1. 告知 AI 要開始第一題「資料清理」,並跟 AI 討論拆解 $\ge 3$ 個 test cases。 +2. 要求 AI 建立測試檔案、執行並確認紅燈、完成 `test:` commit。 +3. 要求 AI 寫實作代碼、執行確認綠燈、完成 `feat:` commit。 +4. 請 AI 用我的學號參數 $D = 4$ 計算 Sample Input 的兩組預期輸出。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:「函式簽名與回傳型別」打算如何定義?無數值符合時回傳空 list 還是 `"NONE"` 字串? + * **我回答**:定義為 `def clean_data(data: list, d: int) -> list:`,無符合者回傳 `"NONE"`。 +* **AI 反問**:回傳 `"NONE"` 字串與簽名中的 `-> list` 衝突,建議將計算與輸出分離,或使用 `list | str` 聯集型別,想選哪種? + * **我回答**:堅持函式直接回傳 `"NONE"` 字串。 +* **AI 反問**:`d = 0` 的例外、空 list 邊界、全被篩掉的 edge case 怎麼處理? + * **我回答**:`d = 0` 拋 `ValueError`,其餘皆回傳 `"NONE"`。 +* **AI 反問**:(提供測試檔後) 這 5 個 test cases 是否足夠,是否需要加其他特殊案例? + * **我回答**:修改我的決定,把原本的 `d = 0` 拋 `ValueError`,改為 `d = 0` 直接回傳 `"NONE"`,並要求更新測試。 + +### AI 給了什麼 +1. 建立了含有 5 個測試案例的 `test_clean_data.py`。 +2. 配合我修改的要求,更新 `test_d_zero_returns_none` 測試。 +3. 實作了 `clean_data.py`(使用 set 進行 O(N) 複雜度保序去重,隨後進行篩選與排序)。 + +### 我改了什麼 +1. **修改了例外處理設計**:原本決定 `d = 0` 拋 `ValueError`,但後來考量程式的魯棒性,決定改成當 `d = 0` 時與找不到資料一樣「回傳 `"NONE"`」。我主動抓出原本測試設計的衝突,並指引 AI 修改 `test_d_zero_raises_valueerror` 為 `test_d_zero_returns_none`。 +2. **型別優化**:主動確認傳入 `list` 而回傳 `list | str` 的動態型別混合處理,確保測試跟實作的斷言完全一致。 + +--- + +## 第二題:凱撒密碼 (Caesar Cipher) + +### 我問 AI 什麼 +1. 告知 AI 開始第二題,並要求拆解 $\ge 3$ 個 test cases。 +2. 要求 AI 依照我的學號位移量 `SHIFT = 3` 進行 TDD 流程(寫測試 $\to$ 紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:函式簽名與回傳型別為何? + * **我回答**:回傳將 `text` 依照 `shift` 位移後的加密字串。參數包括 `text` (str) 與 `shift` (int)。 +* **AI 反問**:`shift` 非整數的例外、空字串與純數字符號的邊界、以及 `SHIFT = 3` 的 `A`、`z`、`"Hello, NPU!"` 該如何加密? + * **我回答**: + 1. 問 AI 關於 `shift` 非整數的處理建議,隨後聽取 AI 的建議選擇「嚴謹做法,明確拋出 `TypeError`」。 + 2. 非英文字元(數字、符號、空白)保持不變。 + 3. `A` 移 3 格變 `D`,小寫 `z` 循環變 `c`。在 AI 協助下,釐清 `"Hello, NPU!"` 會被加密成 `"Khoor, QSX!"`。 +* **AI 反問**:進一步舉例 `"abc XYZ"` 在 `SHIFT = 3` 該加密成什麼? + * **我回答**:理解了字母表末端循環的原理,得出 `"def ABC"` 的正確預期。 + +### AI 給了什麼 +1. 建立了 `test_caesar.py`,包含 4 個測試(基本、末端循環、非字母與空字串、TypeError 測試)。 +2. 實作了 `caesar.py`(利用 `ord()` 和 `chr()` 計算大小寫字母位移,並透過 `% 26` 完美實現循環與負數處理)。 + +### 我改了什麼 +1. **決策例外處理策略**:在 AI 給予多種分析後(Implicit casting vs TypeError),我主動選擇了最符合軟體工程 Fail-Fast 原則的 `TypeError` 當作例外阻擋條件。 +2. **補充驗證案例**:在討論階段,我主動提出對 `"abc XYZ"` 這種跨越字母表末端的連續大寫/小寫組合進行確認,補足了 AI 原始構想中對多重邊界的信心。 + +--- + +## 第三題:任意進位的數字根 (Digital Root in Any Base) + +### 我問 AI 什麼 +1. 告知 AI 開始第三題,並要求拆解 $\ge 3$ 個 test cases。 +2. 請求 AI 提供函式簽名的建議。 +3. 要求 AI 依照 TDD 流程(寫測試 $\to$ 確認紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:針對 $x=0$ 的邊界、`base` 不合法 (如 $\le 1$) 的例外行為、以及驗收標準(如何計算過程)的看法。 + * **我回答**:$x=0$ 時輸出 $0$;`base=0` 時也輸出 $0$。 +* **AI 反問**:實作方式傾向方案 A(模擬法:反覆轉換進位並相加)還是方案 B(數學法:利用 $\pmod{base-1}$ 性質)? + * **我回答**:選擇方案 A(模擬法)。 +* **AI 反問**:關於極大數值(如 $10^{100}$)的處理,認為是否會導致效能問題? + * **我回答**:認為跑得動,只是需要較多時間。 + +### AI 給了什麼 +1. 建議了簽名 `def digital_root(x: int, base: int) -> int:`。 +2. 建立了 `test_digital_root.py`,包含 5 個測試(基本 base=16、大數循環、x=0、base=0、不同基底)。 +3. 實作了 `digital_root.py`(利用 while 迴圈與模除運算 `% base` 實現模擬法數位根計算)。 + +### 我改了什麼 +1. **自定義基底異常行為**:在 AI 建議拋出 `ValueError` 處理非法基底時,我決定將 `base=0` 的行為定義為直接回傳 $0$,使函式在異常輸入下仍能保持穩定回傳。 +2. **確認模擬法可行性**:針對極大數值的運算效能進行討論,確認模擬法在數位根的對數級縮減特性下足以應對大數輸入,決定不採取過於複雜的數學優化,以維持程式碼的可讀性與符合題目「模擬過程」的意圖。 + +--- + +## 第四題:二分搜尋效能 (Binary Search Performance) + +### 我問 AI 什麼 +1. 告知 AI 開始第四題,並要求拆解 $\ge 3$ 個 test cases。 +2. 要求 AI 依照 TDD 流程(寫測試 $\to$ 確認紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:函式簽名與回傳型別建議(Tuple 回傳 `(index, cmp)` vs 屬性記錄)。 + * **我回答**:選擇建議 A,回傳 `(index, cmp_count)` 之 tuple。 +* **AI 反問**:空 list (n=0) 時的預期結果。 + * **我回答**:回傳 `(-1, 0)`。 +* **AI 反問**:`binary_search` 收到未排序資料的處理方式。 + * **我回答**:選擇「做法 B」,即在函式內部檢查排序,若未排序則拋出 `ValueError`。 +* **AI 反問**:如何驗證比較次數 (cmp) 的正確性。 + * **我回答**:同意線性搜尋驗證 `cmp == index + 1`,二分搜尋驗證符合 $\log_2 n$ 量級。 + +### AI 給了什麼 +1. 建立了 `test_search_performance.py`,包含 6 個測試(線性找/沒找、二分找/沒找、未排序拋錯、空 list 邊界)。 +2. 實作了 `search_performance.py`(包含 `linear_search` 與 `binary_search`,並在二分搜尋前加入排序檢查)。 + +### 我改了什麼 +1. **明確化回傳介面**:在 AI 建議多種方式後,決定採用 Tuple 回傳,以確保每個呼叫都能獲取獨立的比較次數,避免在多執行緒或複雜呼叫中產生狀態污染。 +2. **強化安全性檢查**:堅持在 `binary_search` 內部加入排序檢查機制(raise ValueError),而非僅依賴外部輸入正確,確保實作符合嚴謹的工程標準。 diff --git a/weeks/week-18/solutions/1114405022-0622/assets/radar.png b/weeks/week-18/solutions/1114405022-0622/assets/radar.png new file mode 100644 index 000000000..58235d24a Binary files /dev/null and b/weeks/week-18/solutions/1114405022-0622/assets/radar.png differ diff --git a/weeks/week-18/solutions/1114405022-0622/caesar.py b/weeks/week-18/solutions/1114405022-0622/caesar.py new file mode 100644 index 000000000..2cd43a649 --- /dev/null +++ b/weeks/week-18/solutions/1114405022-0622/caesar.py @@ -0,0 +1,19 @@ +def encrypt_caesar(text: str, shift: int) -> str: + if not isinstance(shift, int): + raise TypeError("shift must be an integer") + + result = [] + for char in text: + if 'A' <= char <= 'Z': + # 大寫字母位移 + new_char = chr((ord(char) - ord('A') + shift) % 26 + ord('A')) + result.append(new_char) + elif 'a' <= char <= 'z': + # 小寫字母位移 + new_char = chr((ord(char) - ord('a') + shift) % 26 + ord('a')) + result.append(new_char) + else: + # 非字母字元保持不變 + result.append(char) + + return "".join(result) diff --git a/weeks/week-18/solutions/1114405022-0622/clean_data.py b/weeks/week-18/solutions/1114405022-0622/clean_data.py new file mode 100644 index 000000000..f8180f71b --- /dev/null +++ b/weeks/week-18/solutions/1114405022-0622/clean_data.py @@ -0,0 +1,20 @@ +def clean_data(data: list, d: int) -> list | str: + if d == 0: + return "NONE" + + # 1. 去重且保序 (Preserve first occurrence) + seen = set() + deduped = [] + for x in data: + if x not in seen: + deduped.append(x) + seen.add(x) + + # 2. 篩選 (Divisible by d) + filtered = [x for x in deduped if x % d == 0] + + # 3. 排序 (Ascending) + filtered.sort() + + # 回傳結果 + return filtered if filtered else "NONE" diff --git a/weeks/week-18/solutions/1114405022-0622/digital_root.py b/weeks/week-18/solutions/1114405022-0622/digital_root.py new file mode 100644 index 000000000..8b9ab4846 --- /dev/null +++ b/weeks/week-18/solutions/1114405022-0622/digital_root.py @@ -0,0 +1,15 @@ +def digital_root(x: int, base: int) -> int: + if base == 0: + return 0 + if x == 0: + return 0 + + while x >= base: + digits_sum = 0 + temp_x = x + while temp_x > 0: + digits_sum += temp_x % base + temp_x //= base + x = digits_sum + + return x diff --git a/weeks/week-18/solutions/1114405022-0622/plot.py b/weeks/week-18/solutions/1114405022-0622/plot.py new file mode 100644 index 000000000..8ff4d432a --- /dev/null +++ b/weeks/week-18/solutions/1114405022-0622/plot.py @@ -0,0 +1,58 @@ +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +import json +import os + +# 必須加上此行,否則在沒有 GUI 的環境會報錯 +matplotlib.use("Agg") +# 設定中文字體 (Windows 系統常用微軟正黑體) +plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei'] +plt.rcParams['axes.unicode_minus'] = False + +def draw_radar_chart(): + # 1. 定義維度 (中文) + categories = ['執行時間', '比較次數', '空間開銷', '準備成本'] + N = len(categories) + + # 2. 準備數據 (基於 la-search 表現) + # 數值越小越好,因此我們使用 1/value 進行正規化,使面積越大代表效能越好 + # 這裡使用典型權衡值 + data = { + '線性搜尋': [1, 1, 5, 5], # Time (慢), Cmp (高), Space (低), Setup (低) + '二分搜尋': [5, 5, 5, 1], # Time (快), Cmp (低), Space (低), Setup (高) + '集合搜尋': [5, 5, 1, 2], # Time (最快), Cmp (低), Space (高), Setup (中) + } + + # 正規化 (0-1 區間,越大越好) + # 這裡簡單對應:1=最差, 5=最好 + + angles = np.linspace(0, 2 * np.pi, N, endpoint=False).tolist() + angles += angles[:1] # 閉合圓環 + + fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True)) + + for label, values in data.items(): + vals = values + values[:1] + ax.plot(angles, vals, linewidth=2, label=label) + ax.fill(angles, vals, alpha=0.25) + + ax.set_theta_offset(np.pi / 2) + ax.set_theta_direction(-1) + ax.set_thetagrids(np.degrees(angles[:-1]), categories) + + # 1. 設定徑向範圍,讓最高分 (5) 離邊緣有一段距離,避免標籤重疊 + ax.set_ylim(0, 6) + + # 2. 增加標籤與圓環的間距 + ax.tick_params(axis='x', pad=25) + + plt.title("搜尋演算法多維權衡 (面積越大效能越好)", size=18, y=1.15) + plt.legend(loc='upper right', bbox_to_anchor=(1.5, 1.1)) + + plt.savefig("C:/Users/user/Desktop/2026 python/weeks/week-18/solutions/1114405022/Q4/assets/radar.png", bbox_inches='tight') + plt.close() + +if __name__ == "__main__": + draw_radar_chart() + diff --git a/weeks/week-18/solutions/1114405022-0622/search_performance.py b/weeks/week-18/solutions/1114405022-0622/search_performance.py new file mode 100644 index 000000000..0818ec412 --- /dev/null +++ b/weeks/week-18/solutions/1114405022-0622/search_performance.py @@ -0,0 +1,29 @@ +def linear_search(data: list, target) -> tuple[int, int]: + cmp_count = 0 + for i, val in enumerate(data): + cmp_count += 1 + if val == target: + return (i, cmp_count) + return (-1, cmp_count) + + +def binary_search(data: list, target) -> tuple[int, int]: + # 檢查是否已排序 + if any(data[i] > data[i + 1] for i in range(len(data) - 1)): + raise ValueError("Data must be sorted for binary search") + + low = 0 + high = len(data) - 1 + cmp_count = 0 + + while low <= high: + mid = (low + high) // 2 + cmp_count += 1 + if data[mid] == target: + return (mid, cmp_count) + elif data[mid] < target: + low = mid + 1 + else: + high = mid - 1 + + return (-1, cmp_count) diff --git a/weeks/week-18/solutions/1114405022-0622/test_caesar.py b/weeks/week-18/solutions/1114405022-0622/test_caesar.py new file mode 100644 index 000000000..92ccecdf3 --- /dev/null +++ b/weeks/week-18/solutions/1114405022-0622/test_caesar.py @@ -0,0 +1,27 @@ +import unittest +from caesar import encrypt_caesar + + +class TestCaesarCipher(unittest.TestCase): + + def test_basic_encryption(self): + # 測試一般英文字母加密(大小寫混合、包含標點) + self.assertEqual(encrypt_caesar("Hello, NPU!", 3), "Khoor, QSX!") + + def test_wrap_around(self): + # 測試大寫與小寫字母表末端的循環位移 + self.assertEqual(encrypt_caesar("abc XYZ", 3), "def ABC") + + def test_non_alphabet_and_empty(self): + # 測試非英文字母與空字串保持不變 + self.assertEqual(encrypt_caesar("123 #!", 3), "123 #!") + self.assertEqual(encrypt_caesar("", 3), "") + + def test_invalid_shift_raises_typeerror(self): + # 測試非整數位移量拋出 TypeError + with self.assertRaises(TypeError): + encrypt_caesar("abc", "invalid") + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405022-0622/test_clean_data.py b/weeks/week-18/solutions/1114405022-0622/test_clean_data.py new file mode 100644 index 000000000..f1a909ee2 --- /dev/null +++ b/weeks/week-18/solutions/1114405022-0622/test_clean_data.py @@ -0,0 +1,30 @@ +import unittest +from clean_data import clean_data + + +class TestCleanData(unittest.TestCase): + + def test_basic_functionality(self): + # 測試基本流程:去重 -> 篩選(D=4) -> 排序 + self.assertEqual(clean_data([4, 7, 4, 8, 12, 2, 16, 7], 4), [4, 8, 12, 16]) + + def test_all_filtered_out(self): + # 測試所有數字都被篩掉 + self.assertEqual(clean_data([1, 2, 3], 4), "NONE") + + def test_empty_list(self): + # 測試空 list + self.assertEqual(clean_data([], 4), "NONE") + + def test_d_zero_returns_none(self): + # 測試 d=0 時回傳 "NONE" + self.assertEqual(clean_data([1, 2, 3], 0), "NONE") + + def test_preserves_first_occurrence_then_sorts(self): + # 測試去重保序後再排序 + # [10, 2, 10, 4] -> 去重 [10, 2, 4] -> 篩選(D=2) [10, 2, 4] -> 排序 [2, 4, 10] + self.assertEqual(clean_data([10, 2, 10, 4], 2), [2, 4, 10]) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405022-0622/test_digital_root.py b/weeks/week-18/solutions/1114405022-0622/test_digital_root.py new file mode 100644 index 000000000..cca3df1bc --- /dev/null +++ b/weeks/week-18/solutions/1114405022-0622/test_digital_root.py @@ -0,0 +1,29 @@ +import unittest +from digital_root import digital_root + + +class TestDigitalRoot(unittest.TestCase): + + def test_basic_base16(self): + # 測試 base=16: 31 (10進位) -> 1F (16進位) -> 1+15=16 -> 10 (16進位) -> 1+0=1 + self.assertEqual(digital_root(31, 16), 1) + + def test_base16_large(self): + # 測試 base=16: 255 (10進位) -> FF (16進位) -> 15+15=30 -> 1E (16進位) -> 1+14=15 + self.assertEqual(digital_root(255, 16), 15) + + def test_zero_input(self): + # 測試 x=0 + self.assertEqual(digital_root(0, 16), 0) + + def test_base_zero(self): + # 測試 base=0 時回傳 0 + self.assertEqual(digital_root(100, 0), 0) + + def test_different_base(self): + # 測試其他基底: x=12, base=8 -> 14 (8進位) -> 1+4=5 + self.assertEqual(digital_root(12, 8), 5) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405022-0622/test_plot.py b/weeks/week-18/solutions/1114405022-0622/test_plot.py new file mode 100644 index 000000000..8952861ca --- /dev/null +++ b/weeks/week-18/solutions/1114405022-0622/test_plot.py @@ -0,0 +1,21 @@ +import unittest +import os +from plot import draw_radar_chart + + +class TestPlot(unittest.TestCase): + + def test_radar_png_generated(self): + # 執行繪圖函式 + draw_radar_chart() + + # 驗證檔案是否存在 + file_path = "assets/radar.png" + self.assertTrue(os.path.exists(file_path), f"PNG file {file_path} was not generated") + + # 驗證檔案大小不為 0 + self.assertGreater(os.path.getsize(file_path), 0, f"PNG file {file_path} is empty") + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405022-0622/test_search_performance.py b/weeks/week-18/solutions/1114405022-0622/test_search_performance.py new file mode 100644 index 000000000..170c4e002 --- /dev/null +++ b/weeks/week-18/solutions/1114405022-0622/test_search_performance.py @@ -0,0 +1,54 @@ +import unittest +from search_performance import linear_search, binary_search + + +class TestSearchPerformance(unittest.TestCase): + + def test_linear_search_found(self): + # 測試線性搜尋:找到目標 + data = [10, 20, 30, 40, 50] + target = 30 + index, cmp = linear_search(data, target) + self.assertEqual(index, 2) + self.assertEqual(cmp, 3) + + def test_linear_search_not_found(self): + # 測試線性搜尋:找不到目標 + data = [10, 20, 30] + target = 40 + index, cmp = linear_search(data, target) + self.assertEqual(index, -1) + self.assertEqual(cmp, 3) + + def test_binary_search_found(self): + # 測試二分搜尋:找到目標 + data = [10, 20, 30, 40, 50, 60, 70] + target = 20 + index, cmp = binary_search(data, target) + self.assertEqual(index, 1) + self.assertGreater(cmp, 0) + self.assertLessEqual(cmp, 3) # log2(7) approx 2.8 + + def test_binary_search_not_found(self): + # 測試二分搜尋:找不到目標 + data = [10, 20, 30, 40, 50] + target = 25 + index, cmp = binary_search(data, target) + self.assertEqual(index, -1) + + def test_binary_search_unsorted_raises_valueerror(self): + # 測試未排序資料拋出 ValueError + data = [30, 10, 20] + target = 20 + with self.assertRaises(ValueError): + binary_search(data, target) + + def test_empty_list(self): + # 測試空 list 邊界 + target = 122 + self.assertEqual(linear_search([], target), (-1, 0)) + self.assertEqual(binary_search([], target), (-1, 0)) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405022/AI_LOG.md b/weeks/week-18/solutions/1114405022/AI_LOG.md new file mode 100644 index 000000000..45aa2530f --- /dev/null +++ b/weeks/week-18/solutions/1114405022/AI_LOG.md @@ -0,0 +1,107 @@ +# AI_LOG — CPE 模擬實戰期末上機考 + +## 第一題:資料清理 (Data Cleaning) + +### 我問 AI 什麼 +1. 告知 AI 要開始第一題「資料清理」,並跟 AI 討論拆解 $\ge 3$ 個 test cases。 +2. 要求 AI 建立測試檔案、執行並確認紅燈、完成 `test:` commit。 +3. 要求 AI 寫實作代碼、執行確認綠燈、完成 `feat:` commit。 +4. 請 AI 用我的學號參數 $D = 4$ 計算 Sample Input 的兩組預期輸出。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:「函式簽名與回傳型別」打算如何定義?無數值符合時回傳空 list 還是 `"NONE"` 字串? + * **我回答**:定義為 `def clean_data(data: list, d: int) -> list:`,無符合者回傳 `"NONE"`。 +* **AI 反問**:回傳 `"NONE"` 字串與簽名中的 `-> list` 衝突,建議將計算與輸出分離,或使用 `list | str` 聯集型別,想選哪種? + * **我回答**:堅持函式直接回傳 `"NONE"` 字串。 +* **AI 反問**:`d = 0` 的例外、空 list 邊界、全被篩掉的 edge case 怎麼處理? + * **我回答**:`d = 0` 拋 `ValueError`,其餘皆回傳 `"NONE"`。 +* **AI 反問**:(提供測試檔後) 這 5 個 test cases 是否足夠,是否需要加其他特殊案例? + * **我回答**:修改我的決定,把原本的 `d = 0` 拋 `ValueError`,改為 `d = 0` 直接回傳 `"NONE"`,並要求更新測試。 + +### AI 給了什麼 +1. 建立了含有 5 個測試案例的 `test_clean_data.py`。 +2. 配合我修改的要求,更新 `test_d_zero_returns_none` 測試。 +3. 實作了 `clean_data.py`(使用 set 進行 O(N) 複雜度保序去重,隨後進行篩選與排序)。 + +### 我改了什麼 +1. **修改了例外處理設計**:原本決定 `d = 0` 拋 `ValueError`,但後來考量程式的魯棒性,決定改成當 `d = 0` 時與找不到資料一樣「回傳 `"NONE"`」。我主動抓出原本測試設計的衝突,並指引 AI 修改 `test_d_zero_raises_valueerror` 為 `test_d_zero_returns_none`。 +2. **型別優化**:主動確認傳入 `list` 而回傳 `list | str` 的動態型別混合處理,確保測試跟實作的斷言完全一致。 + +--- + +## 第二題:凱撒密碼 (Caesar Cipher) + +### 我問 AI 什麼 +1. 告知 AI 開始第二題,並要求拆解 $\ge 3$ 個 test cases。 +2. 要求 AI 依照我的學號位移量 `SHIFT = 3` 進行 TDD 流程(寫測試 $\to$ 紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:函式簽名與回傳型別為何? + * **我回答**:回傳將 `text` 依照 `shift` 位移後的加密字串。參數包括 `text` (str) 與 `shift` (int)。 +* **AI 反問**:`shift` 非整數的例外、空字串與純數字符號的邊界、以及 `SHIFT = 3` 的 `A`、`z`、`"Hello, NPU!"` 該如何加密? + * **我回答**: + 1. 問 AI 關於 `shift` 非整數的處理建議,隨後聽取 AI 的建議選擇「嚴謹做法,明確拋出 `TypeError`」。 + 2. 非英文字元(數字、符號、空白)保持不變。 + 3. `A` 移 3 格變 `D`,小寫 `z` 循環變 `c`。在 AI 協助下,釐清 `"Hello, NPU!"` 會被加密成 `"Khoor, QSX!"`。 +* **AI 反問**:進一步舉例 `"abc XYZ"` 在 `SHIFT = 3` 該加密成什麼? + * **我回答**:理解了字母表末端循環的原理,得出 `"def ABC"` 的正確預期。 + +### AI 給了什麼 +1. 建立了 `test_caesar.py`,包含 4 個測試(基本、末端循環、非字母與空字串、TypeError 測試)。 +2. 實作了 `caesar.py`(利用 `ord()` 和 `chr()` 計算大小寫字母位移,並透過 `% 26` 完美實現循環與負數處理)。 + +### 我改了什麼 +1. **決策例外處理策略**:在 AI 給予多種分析後(Implicit casting vs TypeError),我主動選擇了最符合軟體工程 Fail-Fast 原則的 `TypeError` 當作例外阻擋條件。 +2. **補充驗證案例**:在討論階段,我主動提出對 `"abc XYZ"` 這種跨越字母表末端的連續大寫/小寫組合進行確認,補足了 AI 原始構想中對多重邊界的信心。 + +--- + +## 第三題:任意進位的數字根 (Digital Root in Any Base) + +### 我問 AI 什麼 +1. 告知 AI 開始第三題,並要求拆解 $\ge 3$ 個 test cases。 +2. 請求 AI 提供函式簽名的建議。 +3. 要求 AI 依照 TDD 流程(寫測試 $\to$ 確認紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:針對 $x=0$ 的邊界、`base` 不合法 (如 $\le 1$) 的例外行為、以及驗收標準(如何計算過程)的看法。 + * **我回答**:$x=0$ 時輸出 $0$;`base=0` 時也輸出 $0$。 +* **AI 反問**:實作方式傾向方案 A(模擬法:反覆轉換進位並相加)還是方案 B(數學法:利用 $\pmod{base-1}$ 性質)? + * **我回答**:選擇方案 A(模擬法)。 +* **AI 反問**:關於極大數值(如 $10^{100}$)的處理,認為是否會導致效能問題? + * **我回答**:認為跑得動,只是需要較多時間。 + +### AI 給了什麼 +1. 建議了簽名 `def digital_root(x: int, base: int) -> int:`。 +2. 建立了 `test_digital_root.py`,包含 5 個測試(基本 base=16、大數循環、x=0、base=0、不同基底)。 +3. 實作了 `digital_root.py`(利用 while 迴圈與模除運算 `% base` 實現模擬法數位根計算)。 + +### 我改了什麼 +1. **自定義基底異常行為**:在 AI 建議拋出 `ValueError` 處理非法基底時,我決定將 `base=0` 的行為定義為直接回傳 $0$,使函式在異常輸入下仍能保持穩定回傳。 +2. **確認模擬法可行性**:針對極大數值的運算效能進行討論,確認模擬法在數位根的對數級縮減特性下足以應對大數輸入,決定不採取過於複雜的數學優化,以維持程式碼的可讀性與符合題目「模擬過程」的意圖。 + +--- + +## 第四題:二分搜尋效能 (Binary Search Performance) + +### 我問 AI 什麼 +1. 告知 AI 開始第四題,並要求拆解 $\ge 3$ 個 test cases。 +2. 要求 AI 依照 TDD 流程(寫測試 $\to$ 確認紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:函式簽名與回傳型別建議(Tuple 回傳 `(index, cmp)` vs 屬性記錄)。 + * **我回答**:選擇建議 A,回傳 `(index, cmp_count)` 之 tuple。 +* **AI 反問**:空 list (n=0) 時的預期結果。 + * **我回答**:回傳 `(-1, 0)`。 +* **AI 反問**:`binary_search` 收到未排序資料的處理方式。 + * **我回答**:選擇「做法 B」,即在函式內部檢查排序,若未排序則拋出 `ValueError`。 +* **AI 反問**:如何驗證比較次數 (cmp) 的正確性。 + * **我回答**:同意線性搜尋驗證 `cmp == index + 1`,二分搜尋驗證符合 $\log_2 n$ 量級。 + +### AI 給了什麼 +1. 建立了 `test_search_performance.py`,包含 6 個測試(線性找/沒找、二分找/沒找、未排序拋錯、空 list 邊界)。 +2. 實作了 `search_performance.py`(包含 `linear_search` 與 `binary_search`,並在二分搜尋前加入排序檢查)。 + +### 我改了什麼 +1. **明確化回傳介面**:在 AI 建議多種方式後,決定採用 Tuple 回傳,以確保每個呼叫都能獲取獨立的比較次數,避免在多執行緒或複雜呼叫中產生狀態污染。 +2. **強化安全性檢查**:堅持在 `binary_search` 內部加入排序檢查機制(raise ValueError),而非僅依賴外部輸入正確,確保實作符合嚴謹的工程標準。 diff --git a/weeks/week-18/solutions/1114405022/Q1/AI_LOG.md b/weeks/week-18/solutions/1114405022/Q1/AI_LOG.md new file mode 100644 index 000000000..e70b0edd4 --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q1/AI_LOG.md @@ -0,0 +1,28 @@ +# AI_LOG — 第一題:資料清理 (Data Cleaning) + +## 第一題:資料清理 (Data Cleaning) + +### 我問 AI 什麼 +1. 告知 AI 要開始第一題「資料清理」,並跟 AI 討論拆解 $\ge 3$ 個 test cases。 +2. 要求 AI 建立測試檔案、執行並確認紅燈、完成 `test:` commit。 +3. 要求 AI 寫實作代碼、執行確認綠燈、完成 `feat:` commit。 +4. 請 AI 用我的學號參數 $D = 4$ 計算 Sample Input 的兩組預期輸出。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:「函式簽名與回傳型別」打算如何定義?無數值符合時回傳空 list 還是 `"NONE"` 字串? + * **我回答**:定義為 `def clean_data(data: list, d: int) -> list:`,無符合者回傳 `"NONE"`。 +* **AI 反問**:回傳 `"NONE"` 字串與簽名中的 `-> list` 衝突,建議將計算與輸出分離,或使用 `list | str` 聯集型別,想選哪種? + * **我回答**:堅持函式直接回傳 `"NONE"` 字串。 +* **AI 反問**:`d = 0` 的例外、空 list 邊界、全被篩掉的 edge case 怎麼處理? + * **我回答**:`d = 0` 拋 `ValueError`,其餘皆回傳 `"NONE"`。 +* **AI 反問**:(提供測試檔後) 這 5 個 test cases 是否足夠,是否需要加其他特殊案例? + * **我回答**:修改我的決定,把原本的 `d = 0` 拋 `ValueError`,改為 `d = 0` 直接回傳 `"NONE"`,並要求更新測試。 + +### AI 給了什麼 +1. 建立了含有 5 個測試案例的 `test_clean_data.py`。 +2. 配合我修改的要求,更新 `test_d_zero_returns_none` 測試。 +3. 實作了 `clean_data.py`(使用 set 進行 O(N) 複雜度保序去重,隨後進行篩選與排序)。 + +### 我改了什麼 +1. **修改了例外處理設計**:原本決定 `d = 0` 拋 `ValueError`,但後來考量程式的魯棒性,決定改成當 `d = 0` 時與找不到資料一樣「回傳 `"NONE"`」。我主動抓出原本測試設計的衝突,並指引 AI 修改 `test_d_zero_raises_valueerror` 為 `test_d_zero_returns_none`。 +2. **型別優化**:主動確認傳入 `list` 而回傳 `list | str` 的動態型別混合處理,確保測試跟實作的斷言完全一致。 diff --git a/weeks/week-18/solutions/1114405022/Q1/clean_data.py b/weeks/week-18/solutions/1114405022/Q1/clean_data.py new file mode 100644 index 000000000..f8180f71b --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q1/clean_data.py @@ -0,0 +1,20 @@ +def clean_data(data: list, d: int) -> list | str: + if d == 0: + return "NONE" + + # 1. 去重且保序 (Preserve first occurrence) + seen = set() + deduped = [] + for x in data: + if x not in seen: + deduped.append(x) + seen.add(x) + + # 2. 篩選 (Divisible by d) + filtered = [x for x in deduped if x % d == 0] + + # 3. 排序 (Ascending) + filtered.sort() + + # 回傳結果 + return filtered if filtered else "NONE" diff --git a/weeks/week-18/solutions/1114405022/Q1/test_clean_data.py b/weeks/week-18/solutions/1114405022/Q1/test_clean_data.py new file mode 100644 index 000000000..f1a909ee2 --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q1/test_clean_data.py @@ -0,0 +1,30 @@ +import unittest +from clean_data import clean_data + + +class TestCleanData(unittest.TestCase): + + def test_basic_functionality(self): + # 測試基本流程:去重 -> 篩選(D=4) -> 排序 + self.assertEqual(clean_data([4, 7, 4, 8, 12, 2, 16, 7], 4), [4, 8, 12, 16]) + + def test_all_filtered_out(self): + # 測試所有數字都被篩掉 + self.assertEqual(clean_data([1, 2, 3], 4), "NONE") + + def test_empty_list(self): + # 測試空 list + self.assertEqual(clean_data([], 4), "NONE") + + def test_d_zero_returns_none(self): + # 測試 d=0 時回傳 "NONE" + self.assertEqual(clean_data([1, 2, 3], 0), "NONE") + + def test_preserves_first_occurrence_then_sorts(self): + # 測試去重保序後再排序 + # [10, 2, 10, 4] -> 去重 [10, 2, 4] -> 篩選(D=2) [10, 2, 4] -> 排序 [2, 4, 10] + self.assertEqual(clean_data([10, 2, 10, 4], 2), [2, 4, 10]) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405022/Q2/AI_LOG.md b/weeks/week-18/solutions/1114405022/Q2/AI_LOG.md new file mode 100644 index 000000000..341d8fbff --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q2/AI_LOG.md @@ -0,0 +1,26 @@ +# AI_LOG — 第二題:凱撒密碼 (Caesar Cipher) + +## 第二題:凱撒密碼 (Caesar Cipher) + +### 我問 AI 什麼 +1. 告知 AI 開始第二題,並要求拆解 $\ge 3$ 個 test cases。 +2. 要求 AI 依照我的學號位移量 `SHIFT = 3` 進行 TDD 流程(寫測試 $\to$ 紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:函式簽名與回傳型別為何? + * **我回答**:回傳將 `text` 依照 `shift` 位移後的加密字串。參數包括 `text` (str) 與 `shift` (int)。 +* **AI 反問**:`shift` 非整數的例外、空字串與純數字符號的邊界、以及 `SHIFT = 3` 的 `A`、`z`、`"Hello, NPU!"` 該如何加密? + * **我回答**: + 1. 問 AI 關於 `shift` 非整數的處理建議,隨後聽取 AI 的建議選擇「嚴謹做法,明確拋出 `TypeError`」。 + 2. 非英文字元(數字、符號、空白)保持不變。 + 3. `A` 移 3 格變 `D`,小寫 `z` 循環變 `c`。在 AI 協助下,釐清 `"Hello, NPU!"` 會被加密成 `"Khoor, QSX!"`。 +* **AI 反問**:進一步舉例 `"abc XYZ"` 在 `SHIFT = 3` 該加密成什麼? + * **我回答**:理解了字母表末端循環的原理,得出 `"def ABC"` 的正確預期。 + +### AI 給了什麼 +1. 建立了 `test_caesar.py`,包含 4 個測試(基本、末端循環、非字母與空字串、TypeError 測試)。 +2. 實作了 `caesar.py`(利用 `ord()` 和 `chr()` 計算大小寫字母位移,並透過 `% 26` 完美實現循環與負數處理)。 + +### 我改了什麼 +1. **決策例外處理策略**:在 AI 給予多種分析後(Implicit casting vs TypeError),我主動選擇了最符合軟體工程 Fail-Fast 原則的 `TypeError` 當作例外阻擋條件。 +2. **補充驗證案例**:在討論階段,我主動提出對 `"abc XYZ"` 這種跨越字母表末端的連續大寫/小寫組合進行確認,補足了 AI 原始構想中對多重邊界的信心。 diff --git a/weeks/week-18/solutions/1114405022/Q2/caesar.py b/weeks/week-18/solutions/1114405022/Q2/caesar.py new file mode 100644 index 000000000..2cd43a649 --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q2/caesar.py @@ -0,0 +1,19 @@ +def encrypt_caesar(text: str, shift: int) -> str: + if not isinstance(shift, int): + raise TypeError("shift must be an integer") + + result = [] + for char in text: + if 'A' <= char <= 'Z': + # 大寫字母位移 + new_char = chr((ord(char) - ord('A') + shift) % 26 + ord('A')) + result.append(new_char) + elif 'a' <= char <= 'z': + # 小寫字母位移 + new_char = chr((ord(char) - ord('a') + shift) % 26 + ord('a')) + result.append(new_char) + else: + # 非字母字元保持不變 + result.append(char) + + return "".join(result) diff --git a/weeks/week-18/solutions/1114405022/Q2/test_caesar.py b/weeks/week-18/solutions/1114405022/Q2/test_caesar.py new file mode 100644 index 000000000..92ccecdf3 --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q2/test_caesar.py @@ -0,0 +1,27 @@ +import unittest +from caesar import encrypt_caesar + + +class TestCaesarCipher(unittest.TestCase): + + def test_basic_encryption(self): + # 測試一般英文字母加密(大小寫混合、包含標點) + self.assertEqual(encrypt_caesar("Hello, NPU!", 3), "Khoor, QSX!") + + def test_wrap_around(self): + # 測試大寫與小寫字母表末端的循環位移 + self.assertEqual(encrypt_caesar("abc XYZ", 3), "def ABC") + + def test_non_alphabet_and_empty(self): + # 測試非英文字母與空字串保持不變 + self.assertEqual(encrypt_caesar("123 #!", 3), "123 #!") + self.assertEqual(encrypt_caesar("", 3), "") + + def test_invalid_shift_raises_typeerror(self): + # 測試非整數位移量拋出 TypeError + with self.assertRaises(TypeError): + encrypt_caesar("abc", "invalid") + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405022/Q3/AI_LOG.md b/weeks/week-18/solutions/1114405022/Q3/AI_LOG.md new file mode 100644 index 000000000..bc2724346 --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q3/AI_LOG.md @@ -0,0 +1,25 @@ +# AI_LOG — 第三題:任意進位的數字根 (Digital Root in Any Base) + +## 第三題:任意進位的數字根 (Digital Root in Any Base) + +### 我問 AI 什麼 +1. 告知 AI 開始第三題,並要求拆解 $\ge 3$ 個 test cases。 +2. 請求 AI 提供函式簽名的建議。 +3. 要求 AI 依照 TDD 流程(寫測試 $\to$ 確認紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:針對 $x=0$ 的邊界、`base` 不合法 (如 $\le 1$) 的例外行為、以及驗收標準(如何計算過程)的看法。 + * **我回答**:$x=0$ 時輸出 $0$;`base=0` 時也輸出 $0$。 +* **AI 反問**:實作方式傾向方案 A(模擬法:反覆轉換進位並相加)還是方案 B(數學法:利用 $\pmod{base-1}$ 性質)? + * **我回答**:選擇方案 A(模擬法)。 +* **AI 反問**:關於極大數值(如 $10^{100}$)的處理,認為是否會導致效能問題? + * **我回答**:認為跑得動,只是需要較多時間。 + +### AI 給了什麼 +1. 建議了簽名 `def digital_root(x: int, base: int) -> int:`。 +2. 建立了 `test_digital_root.py`,包含 5 個測試(基本 base=16、大數循環、x=0、base=0、不同基底)。 +3. 實作了 `digital_root.py`(利用 while 迴圈與模除運算 `% base` 實現模擬法數位根計算)。 + +### 我改了什麼 +1. **自定義基底異常行為**:在 AI 建議拋出 `ValueError` 處理非法基底時,我決定將 `base=0` 的行為定義為直接回傳 $0$,使函式在異常輸入下仍能保持穩定回傳。 +2. **確認模擬法可行性**:針對極大數值的運算效能進行討論,確認模擬法在數位根的對數級縮減特性下足以應對大數輸入,決定不採取過於複雜的數學優化,以維持程式碼的可讀性與符合題目「模擬過程」的意圖。 diff --git a/weeks/week-18/solutions/1114405022/Q3/digital_root.py b/weeks/week-18/solutions/1114405022/Q3/digital_root.py new file mode 100644 index 000000000..8b9ab4846 --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q3/digital_root.py @@ -0,0 +1,15 @@ +def digital_root(x: int, base: int) -> int: + if base == 0: + return 0 + if x == 0: + return 0 + + while x >= base: + digits_sum = 0 + temp_x = x + while temp_x > 0: + digits_sum += temp_x % base + temp_x //= base + x = digits_sum + + return x diff --git a/weeks/week-18/solutions/1114405022/Q3/test_digital_root.py b/weeks/week-18/solutions/1114405022/Q3/test_digital_root.py new file mode 100644 index 000000000..cca3df1bc --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q3/test_digital_root.py @@ -0,0 +1,29 @@ +import unittest +from digital_root import digital_root + + +class TestDigitalRoot(unittest.TestCase): + + def test_basic_base16(self): + # 測試 base=16: 31 (10進位) -> 1F (16進位) -> 1+15=16 -> 10 (16進位) -> 1+0=1 + self.assertEqual(digital_root(31, 16), 1) + + def test_base16_large(self): + # 測試 base=16: 255 (10進位) -> FF (16進位) -> 15+15=30 -> 1E (16進位) -> 1+14=15 + self.assertEqual(digital_root(255, 16), 15) + + def test_zero_input(self): + # 測試 x=0 + self.assertEqual(digital_root(0, 16), 0) + + def test_base_zero(self): + # 測試 base=0 時回傳 0 + self.assertEqual(digital_root(100, 0), 0) + + def test_different_base(self): + # 測試其他基底: x=12, base=8 -> 14 (8進位) -> 1+4=5 + self.assertEqual(digital_root(12, 8), 5) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405022/Q4/AI_LOG.md b/weeks/week-18/solutions/1114405022/Q4/AI_LOG.md new file mode 100644 index 000000000..aee8616e8 --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q4/AI_LOG.md @@ -0,0 +1,25 @@ +# AI_LOG — 第四題:二分搜尋效能 (Binary Search Performance) + +## 第四題:二分搜尋效能 (Binary Search Performance) + +### 我問 AI 什麼 +1. 告知 AI 開始第四題,並要求拆解 $\ge 3$ 個 test cases。 +2. 要求 AI 依照 TDD 流程(寫測試 $\to$ 確認紅燈 $\to$ commit $\to$ 實作 $\to$ 綠燈 $\to$ commit)。 + +### AI 反問我什麼 / 我怎麼回答 +* **AI 反問**:函式簽名與回傳型別建議(Tuple 回傳 `(index, cmp)` vs 屬性記錄)。 + * **我回答**:選擇建議 A,回傳 `(index, cmp_count)` 之 tuple。 +* **AI 反問**:空 list (n=0) 時的預期結果。 + * **我回答**:回傳 `(-1, 0)`。 +* **AI 反問**:`binary_search` 收到未排序資料的處理方式。 + * **我回答**:選擇「做法 B」,即在函式內部檢查排序,若未排序則拋出 `ValueError`。 +* **AI 反問**:如何驗證比較次數 (cmp) 的正確性。 + * **我回答**:同意線性搜尋驗證 `cmp == index + 1`,二分搜尋驗證符合 $\log_2 n$ 量級。 + +### AI 給了什麼 +1. 建立了 `test_search_performance.py`,包含 6 個測試(線性找/沒找、二分找/沒找、未排序拋錯、空 list 邊界)。 +2. 實作了 `search_performance.py`(包含 `linear_search` 與 `binary_search`,並在二分搜尋前加入排序檢查)。 + +### 我改了什麼 +1. **明確化回傳介面**:在 AI 建議多種方式後,決定採用 Tuple 回傳,以確保每個呼叫都能獲取獨立的比較次數,避免在多執行緒或複雜呼叫中產生狀態污染。 +2. **強化安全性檢查**:堅持在 `binary_search` 內部加入排序檢查機制(raise ValueError),而非僅依賴外部輸入正確,確保實作符合嚴謹的工程標準。 diff --git a/weeks/week-18/solutions/1114405022/Q4/assets/radar.png b/weeks/week-18/solutions/1114405022/Q4/assets/radar.png new file mode 100644 index 000000000..58235d24a Binary files /dev/null and b/weeks/week-18/solutions/1114405022/Q4/assets/radar.png differ diff --git a/weeks/week-18/solutions/1114405022/Q4/plot.py b/weeks/week-18/solutions/1114405022/Q4/plot.py new file mode 100644 index 000000000..8ff4d432a --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q4/plot.py @@ -0,0 +1,58 @@ +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +import json +import os + +# 必須加上此行,否則在沒有 GUI 的環境會報錯 +matplotlib.use("Agg") +# 設定中文字體 (Windows 系統常用微軟正黑體) +plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei'] +plt.rcParams['axes.unicode_minus'] = False + +def draw_radar_chart(): + # 1. 定義維度 (中文) + categories = ['執行時間', '比較次數', '空間開銷', '準備成本'] + N = len(categories) + + # 2. 準備數據 (基於 la-search 表現) + # 數值越小越好,因此我們使用 1/value 進行正規化,使面積越大代表效能越好 + # 這裡使用典型權衡值 + data = { + '線性搜尋': [1, 1, 5, 5], # Time (慢), Cmp (高), Space (低), Setup (低) + '二分搜尋': [5, 5, 5, 1], # Time (快), Cmp (低), Space (低), Setup (高) + '集合搜尋': [5, 5, 1, 2], # Time (最快), Cmp (低), Space (高), Setup (中) + } + + # 正規化 (0-1 區間,越大越好) + # 這裡簡單對應:1=最差, 5=最好 + + angles = np.linspace(0, 2 * np.pi, N, endpoint=False).tolist() + angles += angles[:1] # 閉合圓環 + + fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True)) + + for label, values in data.items(): + vals = values + values[:1] + ax.plot(angles, vals, linewidth=2, label=label) + ax.fill(angles, vals, alpha=0.25) + + ax.set_theta_offset(np.pi / 2) + ax.set_theta_direction(-1) + ax.set_thetagrids(np.degrees(angles[:-1]), categories) + + # 1. 設定徑向範圍,讓最高分 (5) 離邊緣有一段距離,避免標籤重疊 + ax.set_ylim(0, 6) + + # 2. 增加標籤與圓環的間距 + ax.tick_params(axis='x', pad=25) + + plt.title("搜尋演算法多維權衡 (面積越大效能越好)", size=18, y=1.15) + plt.legend(loc='upper right', bbox_to_anchor=(1.5, 1.1)) + + plt.savefig("C:/Users/user/Desktop/2026 python/weeks/week-18/solutions/1114405022/Q4/assets/radar.png", bbox_inches='tight') + plt.close() + +if __name__ == "__main__": + draw_radar_chart() + diff --git a/weeks/week-18/solutions/1114405022/Q4/search_performance.py b/weeks/week-18/solutions/1114405022/Q4/search_performance.py new file mode 100644 index 000000000..0818ec412 --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q4/search_performance.py @@ -0,0 +1,29 @@ +def linear_search(data: list, target) -> tuple[int, int]: + cmp_count = 0 + for i, val in enumerate(data): + cmp_count += 1 + if val == target: + return (i, cmp_count) + return (-1, cmp_count) + + +def binary_search(data: list, target) -> tuple[int, int]: + # 檢查是否已排序 + if any(data[i] > data[i + 1] for i in range(len(data) - 1)): + raise ValueError("Data must be sorted for binary search") + + low = 0 + high = len(data) - 1 + cmp_count = 0 + + while low <= high: + mid = (low + high) // 2 + cmp_count += 1 + if data[mid] == target: + return (mid, cmp_count) + elif data[mid] < target: + low = mid + 1 + else: + high = mid - 1 + + return (-1, cmp_count) diff --git a/weeks/week-18/solutions/1114405022/Q4/test_plot.py b/weeks/week-18/solutions/1114405022/Q4/test_plot.py new file mode 100644 index 000000000..8952861ca --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q4/test_plot.py @@ -0,0 +1,21 @@ +import unittest +import os +from plot import draw_radar_chart + + +class TestPlot(unittest.TestCase): + + def test_radar_png_generated(self): + # 執行繪圖函式 + draw_radar_chart() + + # 驗證檔案是否存在 + file_path = "assets/radar.png" + self.assertTrue(os.path.exists(file_path), f"PNG file {file_path} was not generated") + + # 驗證檔案大小不為 0 + self.assertGreater(os.path.getsize(file_path), 0, f"PNG file {file_path} is empty") + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405022/Q4/test_search_performance.py b/weeks/week-18/solutions/1114405022/Q4/test_search_performance.py new file mode 100644 index 000000000..170c4e002 --- /dev/null +++ b/weeks/week-18/solutions/1114405022/Q4/test_search_performance.py @@ -0,0 +1,54 @@ +import unittest +from search_performance import linear_search, binary_search + + +class TestSearchPerformance(unittest.TestCase): + + def test_linear_search_found(self): + # 測試線性搜尋:找到目標 + data = [10, 20, 30, 40, 50] + target = 30 + index, cmp = linear_search(data, target) + self.assertEqual(index, 2) + self.assertEqual(cmp, 3) + + def test_linear_search_not_found(self): + # 測試線性搜尋:找不到目標 + data = [10, 20, 30] + target = 40 + index, cmp = linear_search(data, target) + self.assertEqual(index, -1) + self.assertEqual(cmp, 3) + + def test_binary_search_found(self): + # 測試二分搜尋:找到目標 + data = [10, 20, 30, 40, 50, 60, 70] + target = 20 + index, cmp = binary_search(data, target) + self.assertEqual(index, 1) + self.assertGreater(cmp, 0) + self.assertLessEqual(cmp, 3) # log2(7) approx 2.8 + + def test_binary_search_not_found(self): + # 測試二分搜尋:找不到目標 + data = [10, 20, 30, 40, 50] + target = 25 + index, cmp = binary_search(data, target) + self.assertEqual(index, -1) + + def test_binary_search_unsorted_raises_valueerror(self): + # 測試未排序資料拋出 ValueError + data = [30, 10, 20] + target = 20 + with self.assertRaises(ValueError): + binary_search(data, target) + + def test_empty_list(self): + # 測試空 list 邊界 + target = 122 + self.assertEqual(linear_search([], target), (-1, 0)) + self.assertEqual(binary_search([], target), (-1, 0)) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405022/README.md b/weeks/week-18/solutions/1114405022/README.md new file mode 100644 index 000000000..400b84c6e --- /dev/null +++ b/weeks/week-18/solutions/1114405022/README.md @@ -0,0 +1,36 @@ +# 搜尋效能分析報告 (Radar Chart) + +## 1. 維度定義 (Dimensions) +本雷達圖從以下四個維度評估線性搜尋、二分搜尋與集合搜尋的權衡 (Trade-off): + +* **執行時間 (Time)**:使用 `timeit` 量測單次搜尋的平均耗時。 +* **比較次數 (Cmp)**:紀錄在搜尋目標 $K$ 過程中,程式執行比較運算的總次數。 +* **空間開銷 (Space)**:評估演算法所需的額外空間複雜度。 + * 線性/二分搜尋:$O(1)$ (極低) + * 集合搜尋:$O(n)$ (需建立 Hash Set) +* **準備成本 (Setup)**:評估執行搜尋前的預處理成本。 + * 線性搜尋:$0$ (無需準備) + * 二分搜尋:$O(n \log n)$ (需先將資料排序) + * 集合搜尋:$O(n)$ (需將資料轉換為 Set) + +## 2. 正規化說明 (Normalization) +為了在同一個雷達圖中呈現不同單位的數據,我們將所有維度正規化為 **1 $\to$ 5 分** 的評分制: + +* **評分準則**:$\text{效能越好} \to \text{分數越高}$。 +* **5 分 (Best)**:該維度表現最優(例如:耗時最短、空間最省)。 +* **1 分 (Worst)**:該維度表現最差。 + +**正規化對照表:** + +| 維度 | 5 分 (最優) | 1 分 (最差) | +| :--- | :--- | :--- | +| **執行時間** | 耗時最短 (如 $\approx 0.0001s$) | 耗時最長 (如 $\approx 0.01s$) | +| **比較次數** | 比較次數最少 (如 $\log n$) | 比較次數最多 (如 $n$) | +| **空間開銷** | 空間複雜度 $O(1)$ | 空間複雜度 $O(n)$ | +| **準備成本** | 無需準備 (Setup Time $= 0$) | 排序成本最高 (Setup Time $\approx O(n \log n)$) | + +## 3. 結論 +在雷達圖中,**覆蓋面積越大**的演算法代表其綜合效能越均衡且優異。 +- **二分搜尋** 在時間與比較次數上極強,但準備成本最高。 +- **集合搜尋** 在時間上最快,但空間開銷最大。 +- **線性搜尋** 在準備成本與空間上最優,但在大數據量時時間與比較次數表現最差。