diff --git a/weeks/week-18/solutions/1114405023/AI_Log.md b/weeks/week-18/solutions/1114405023/AI_Log.md new file mode 100644 index 000000000..8b5f2a30c --- /dev/null +++ b/weeks/week-18/solutions/1114405023/AI_Log.md @@ -0,0 +1,161 @@ +# AI_LOG.md + +## CPE 模擬實戰 AI 協作紀錄 + +## 基本資料 + +* 學號後兩碼:`23` +* 個位數:`3` +* 十位數:`2` + +## 依學號決定的參數 + +| 題目 | 參數 | 數值 | +| ----------- | ------- | --: | +| 第一題:資料清理 | `D` | 5 | +| 第二題:凱撒密碼 | `SHIFT` | 4 | +| 第三題:任意進位數字根 | `base` | 3 | +| 第四題:二分搜尋效能 | `K` | 123 | + +--- + +# 使用 AI 的目的 + +本次使用 AI 協助完成 CPE 模擬實戰四題,包含: + +1. 讀懂題目需求 +2. 依照學號後兩碼推算參數 +3. 依照 TDD 流程先寫測試,再實作程式 +4. 建立紅燈與綠燈測試紀錄 +5. 討論 edge case +6. 討論二分搜尋效能比較方式 +7. 產生 `README.md` +8. 產生 `TEST_LOG.md` +9. 整理 `AI_LOG.md` + +--- + +# AI 反問我什麼 / 我怎麼回答 + +--- + +## 第一題:資料清理 Data Cleaning + +| AI 反問內容 | 我的回答 | +| ------------------------------------------ | ----------------------------------------------------------- | +| `clean_numbers(numbers, divisor=5)` 要回傳什麼? | 回傳處理後的整數 list,也就是 `list[int]` | +| `numbers` 可以是空 list 嗎? | 可以 | +| `numbers` 可以有負數嗎? | 可以處理負數 | +| `numbers` 可以有重複值嗎? | 可以有重複值 | +| 如果沒有符合 divisor 的數字,要怎麼處理? | 函式回傳空 list,主程式輸出 `NONE` | +| edge case 要測什麼? | 重複值、負數、沒有符合條件、原資料不能被修改 | +| 什麼情況算第一題綠燈成功? | `test_p1_data_cleaning.py` 全部通過,且輸入 `3 / 1 3 5 / 0` 時輸出 `5` | + +--- + +## 第二題:凱撒密碼 Caesar Cipher + +| AI 反問內容 | 我的回答 | +| ------------------------------------- | ----------------------------------------------- | +| `caesar_cipher(text, shift=4)` 要回傳什麼? | 回傳加密後的字串,型別是 `str` | +| 輸入可以有空白嗎? | 可以 | +| 輸入可以有數字與標點符號嗎? | 可以 | +| 輸入可以有多行文字嗎? | 可以 | +| 非英文字母要怎麼處理? | 非英文字母保持原樣,不做位移,也不丟出錯誤 | +| edge case 要測什麼? | 大寫、小寫、`Z/z` 循環位移、非字母保留、多行 EOF | +| 什麼情況算第二題綠燈成功? | `test_p2_caesar_cipher.py` 全部通過,且輸入 `z` 時輸出 `d` | + +--- + +## 第三題:任意進位的數字根 + +| AI 反問內容 | 我的回答 | +| ---------------------------------- | ------------------------------------------ | +| `digit_root(value, base=3)` 要回傳什麼? | 回傳最後的數字根,型別是 `int` | +| `value = 0` 可以嗎? | 可以,輸出 `0` | +| `value` 可以是負數嗎? | 不接受負數 | +| 輸入有空行時怎麼辦? | 空行略過 | +| 如果 `value < 0` 要怎麼處理? | `raise ValueError` | +| edge case 要測什麼? | `0`、小於 base 的數、需要多次相加的數、`-5` | +| 什麼情況算第三題綠燈成功? | `test_p3_digit_root_base.py` 全部通過,沒有 error | + +--- + +## 第四題:二分搜尋效能 Binary Search Performance + +| AI 反問內容 | 我的回答 | +| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `linear_search_with_count(data, target=123)` 要回傳什麼? | 回傳 index 與比較次數 | +| `binary_search_with_count(data, target=123)` 要回傳什麼? | 回傳 index 與比較次數 | +| 效能比較要記錄什麼? | 記錄兩種搜尋法的搜尋時間,以及比較次數 | +| `data` 可以是空 list 嗎? | 可以 | +| 找不到 target 時要怎麼處理? | 回傳未找到該筆資料,也就是 index 為 `-1` | +| 如果 `data` 沒排序時要怎麼處理? | 視為無法正確搜尋,回傳 `-1` | +| edge case 要測什麼? | 找到、找不到、空 list、未排序資料、有兩個相同整數、不修改原資料 | +| 陣列大小至少要多少? | 至少 `10^5`,所以使用 `DATA_SIZE = 100000` | +| 題目要求的陣列排序方式是什麼? | 升冪陣列 | +| 為了比較 best / middle / worst,要怎麼設計資料? | 固定 `K = 123`,產生不同升冪陣列,讓 `K` 分別位在 index `0`、`50000`、`99999` | +| not_found case 要怎麼設計? | 建立長度 `100000` 的升冪陣列,但不包含 `K = 123` | +| 搜尋計時要包含資料建立、輸入、輸出嗎? | 不包含,benchmark 只量搜尋函式本身 | +| 兩種搜尋法共同的提速方式是什麼? | 資料先建立好、找到就立刻 return、不把 input / print / 建資料算進時間、使用相同資料與 repeat 次數 | +| 雷達圖要比較哪些項目? | `BestCmp`、`MiddleCmp`、`WorstCmp`、`NotFoundCmp`、`BestTime`、`MiddleTime`、`WorstTime`、`NotFoundTime` | +| 雷達圖正規化方式是什麼? | 比較次數與時間都是越小越好,所以用「越小越好」的正規化方式 | +| 什麼情況算第四題綠燈成功? | `test_p4_binary_search_perf.py` 全部通過,能輸出 best / middle / worst / not_found 的比較次數與時間,並產生 `assets/radar.png` | + +--- + + +## 第四題後續討論與修改紀錄 + +| 討論項目 | 決定 / 修改內容 | +| --- | --- | +| 是否要使用 `timeit`? | 原本使用一般計時方式,後續改成 Python 標準庫 `timeit.repeat()`,讓第四題更符合題目要求的「用 timeit 比較」。 | +| `timeit` 要量哪些東西? | 只量搜尋函式本身,不把資料建立、輸入、輸出、雷達圖產生時間算進搜尋時間。 | +| 是否要加入 found / not_found 彙整? | 加入 found / not_found summary。found 使用 best、middle、worst 三種找到目標的 case 取平均;not_found 使用找不到目標的 case。 | +| `cmp` 是什麼? | `cmp` 代表 comparisons,也就是搜尋過程中的比較次數。 | +| 是否要輸出較快者? | 新增 `fastest` 欄位,比較 linear search 與 binary search 的 `timeit` 平均時間,輸出 `linear`、`binary` 或 `tie`。 | +| best case 誰較快? | best case 中 linear search 較快,因為目標 `K=123` 位於 index `0`,只需要比較 1 次。 | +| middle / worst / not_found 誰較快? | middle、worst、not_found 三種情況皆為 binary search 較快,因為 binary search 每次將搜尋範圍縮小一半。 | +| 是否要更新 README? | 需要在 README 補上 found / not_found summary、`cmp` 說明、`timeit` 說明與較快者欄位。 | +| 是否要更新 TEST_LOG? | 需要更新第四題輸出結果,包含 `fastest=linear` 或 `fastest=binary`。 | +| 是否要更新測試檔? | 建議在 `test_p4_binary_search_perf.py` 中補 `fastest_search()` 測試,確認 linear、binary、tie 三種結果都正確。 | + +--- + +## 第四題 found / not_found 效能彙整 + +根據第四題最新執行結果: + +| Case | Linear cmp | Binary cmp | Linear timeit | Binary timeit | 較快者 | +| --- | ---: | ---: | ---: | ---: | --- | +| found 平均 | 50000.67 | 16.33 | 0.0072284400 | 0.0000058667 | binary | +| not_found | 100000 | 16 | 0.0135415800 | 0.0000065400 | binary | + +### found 平均計算方式 + +found 平均是由 best、middle、worst 三個有找到目標的 case 計算平均值。 + +```text +linear_cmp_avg = (1 + 50001 + 100000) / 3 = 50000.67 +binary_cmp_avg = (16 + 16 + 17) / 3 = 16.33 +``` + +```text +linear_timeit_avg = (0.0000046200 + 0.0076549000 + 0.0140258000) / 3 + = 0.0072284400 + +binary_timeit_avg = (0.0000071000 + 0.0000052400 + 0.0000052600) / 3 + = 0.0000058667 +``` + +### not_found 結果 + +not_found case 中,陣列長度為 `100000`,且不包含 `K = 123`。 + +```text +linear_cmp = 100000 +binary_cmp = 16 +linear_timeit = 0.0135415800 +binary_timeit = 0.0000065400 +fastest = binary +``` diff --git a/weeks/week-18/solutions/1114405023/README.md b/weeks/week-18/solutions/1114405023/README.md new file mode 100644 index 000000000..46d958caa --- /dev/null +++ b/weeks/week-18/solutions/1114405023/README.md @@ -0,0 +1,304 @@ +# 第四題:二分搜尋效能比較 + +## 題目目標 + +本題目標是比較 **linear search** 與 **binary search** 在不同搜尋情境下的效能差異。 + +本題依照學號後兩碼 `23` 設定: + +| 參數 | 數值 | +| -------- | -----: | +| 搜尋目標 `K` | 123 | +| 陣列大小 | 100000 | +| 陣列排序 | 升冪排序 | + +--- + +## 程式檔案 + +本題主要檔案如下: + +| 檔案 | 說明 | +| ------------------------------- | ---------------------- | +| `p4_binary_search_perf.py` | 第四題主程式,包含搜尋、效能比較與雷達圖產生 | +| `test_p4_binary_search_perf.py` | 第四題測試檔 | +| `assets/radar.png` | 搜尋效能雷達圖 | + +--- + +## 實作內容 + +本題實作兩種搜尋法: + +### 1. Linear Search + +`linear_search_with_count(data, target=123)` + +線性搜尋會從陣列第一個元素開始逐一比對,直到找到目標值或搜尋完整個陣列。 + +回傳格式: + +```python +(index, comparison_count) +``` + +其中: + +* `index`:找到時回傳目標所在位置 +* 找不到時回傳 `-1` +* `comparison_count`:搜尋過程中的比較次數 + +--- + +### 2. Binary Search + +`binary_search_with_count(data, target=123)` + +二分搜尋的前提是資料必須已經升冪排序。 +每次會檢查中間位置,並依照比較結果縮小搜尋範圍。 + +回傳格式: + +```python +(index, comparison_count) +``` + +其中: + +* `index`:找到時回傳目標所在位置 +* 找不到時回傳 `-1` +* `comparison_count`:搜尋過程中的比較次數 + +--- + +## 效能比較設計 + +本題使用長度為 `100000` 的升冪陣列,並固定搜尋目標: + +```text +K = 123 +``` + +為了觀察不同情況下的搜尋效能,本題設計四種 case: + +| Case | 說明 | target index | +| --------- | -------------- | -----------: | +| best | `K` 位於陣列第一個位置 | 0 | +| middle | `K` 位於陣列中間位置 | 50000 | +| worst | `K` 位於陣列最後一個位置 | 99999 | +| not_found | 陣列中不存在 `K` | -1 | + +--- + +## 效能測試結果 + +執行: + +```bash +python .\p4_binary_search_perf.py +``` + +在等待輸入時按: + +```text +Ctrl + Z +Enter +``` + +程式會自動產生四組測試資料並輸出結果。 + +### 測試結果 + +```text +case=best +data_size=100000 +target=123 +target_index=0 +linear_index=0 +linear_comparisons=1 +linear_time=2.6999972760677337e-06 +binary_index=0 +binary_comparisons=16 +binary_time=5.6799966841936115e-06 + +case=middle +data_size=100000 +target=123 +target_index=50000 +linear_index=50000 +linear_comparisons=50001 +linear_time=0.0074086999928113075 +binary_index=50000 +binary_comparisons=16 +binary_time=6.579997716471553e-06 + +case=worst +data_size=100000 +target=123 +target_index=99999 +linear_index=99999 +linear_comparisons=100000 +linear_time=0.013151360000483692 +binary_index=99999 +binary_comparisons=17 +binary_time=5.439994856715202e-06 +``` + +not_found case 會輸出 `linear_index=-1` 與 `binary_index=-1`,代表目標值不存在於陣列中。 + +--- + +## 效能分析 + +### Linear Search + +linear search 的比較次數會受到目標位置影響很大。 + +| Case | Linear Search 比較次數 | +| --------- | -----------------: | +| best | 1 | +| middle | 50001 | +| worst | 100000 | +| not_found | 100000 | + +當目標值在陣列最前面時,linear search 只需要比較一次,因此是最佳情況。 +但如果目標值在中間、最後,或根本不存在,linear search 就需要掃過大量資料。 + +--- + +### Binary Search + +binary search 的比較次數相對穩定。 + +| Case | Binary Search 比較次數 | +| --------- | -----------------: | +| best | 約 16 | +| middle | 約 16 | +| worst | 約 17 | +| not_found | 約 16~17 | + +binary search 每次都會將搜尋範圍縮小一半,所以即使資料量達到 `100000`,比較次數仍然維持在約 `16~17` 次。 + +--- + +## 搜尋提速方式 + +本題比較時採用以下共同提速方式,避免測量到搜尋以外的成本: + +1. 資料先建立好,不把建立陣列的時間算進搜尋時間。 +2. 不把 `input()` 與 `print()` 算進搜尋時間。 +3. 找到目標後立刻 `return`,不做多餘比較。 +4. 使用相同資料量、相同目標值與相同 repeat 次數。 +5. binary search 的資料事先保證升冪排序,不在 benchmark 中重複排序。 + +--- + +## 雷達圖說明 + +本題會產生: + +```text +assets/radar.png +``` + +雷達圖用來比較 linear search 與 binary search 在不同指標下的表現。 + +雷達圖維度包含: + +| 維度 | 說明 | +| -------------- | -------------------- | +| `BestCmp` | best case 的比較次數 | +| `MiddleCmp` | middle case 的比較次數 | +| `WorstCmp` | worst case 的比較次數 | +| `NotFoundCmp` | not_found case 的比較次數 | +| `BestTime` | best case 的搜尋時間 | +| `MiddleTime` | middle case 的搜尋時間 | +| `WorstTime` | worst case 的搜尋時間 | +| `NotFoundTime` | not_found case 的搜尋時間 | + +--- + +## 正規化邏輯 + +因為「比較次數」與「搜尋時間」都是越小越好,所以本題使用 smaller-is-better 的正規化方式: + +```text +score = best_value / value +``` + +其中: + +* `best_value` 是同一個維度中較小的數值 +* 分數越接近 `1.0`,代表該搜尋法在該維度表現越好 +* 同一個維度中表現最好的搜尋法分數會是 `1.0` + +例如在 worst case 中: + +```text +linear_comparisons = 100000 +binary_comparisons = 17 +``` + +所以: + +```text +binary score = 17 / 17 = 1.0 +linear score = 17 / 100000 = 0.00017 +``` + +這代表 binary search 在 worst case 的比較次數表現明顯優於 linear search。 + +--- + +## 執行方式 + +### 執行測試 + +```bash +python .\test_p4_binary_search_perf.py +``` + +### 執行主程式並產生雷達圖 + +```bash +python .\p4_binary_search_perf.py +``` + +等待輸入時按: + +```text +Ctrl + Z +Enter +``` + +成功後會產生: + +```text +assets/radar.png +``` + +--- + +## 測試結果 + +```text +........... +---------------------------------------------------------------------- +Ran 11 tests in 0.074s + +OK +``` + +代表第四題測試全部通過。 + +--- + +## 結論 + +本題結果顯示: + +1. linear search 在 best case 表現很好,因為目標值一開始就被找到。 +2. linear search 在 middle、worst 與 not_found case 中需要大量比較。 +3. binary search 在升冪陣列中表現穩定,即使資料量為 `100000`,比較次數仍約為 `16~17` 次。 +4. 若資料已排序,binary search 在大資料量搜尋時比 linear search 更有效率。 +5. 若只看 best case,linear search 可能比 binary search 快;但整體來看,binary search 在多數情境下更穩定。 diff --git a/weeks/week-18/solutions/1114405023/TEST_LOG.md b/weeks/week-18/solutions/1114405023/TEST_LOG.md new file mode 100644 index 000000000..e7f372026 --- /dev/null +++ b/weeks/week-18/solutions/1114405023/TEST_LOG.md @@ -0,0 +1,502 @@ +# TEST_LOG.md + +## CPE 模擬實戰測試紀錄 + +## 基本資料 + +* 學號後兩碼:`23` +* 個位數:`3` +* 十位數:`2` + +## 參數設定 + +| 題目 | 參數 | 數值 | +| ----------- | ------- | --: | +| 第一題:資料清理 | `D` | 5 | +| 第二題:凱撒密碼 | `SHIFT` | 4 | +| 第三題:任意進位數字根 | `base` | 3 | +| 第四題:二分搜尋效能 | `K` | 123 | + +--- + +# 第一題:資料清理 Data Cleaning + +## 紅燈測試 + +### 測試指令 + +```bash +python .\test_p1_data_cleaning.py +``` + +### 測試結果 + +```text +Traceback (most recent call last): + File "test_p1_data_cleaning.py", line 7, in + from p1_data_cleaning import clean_numbers, solve +ModuleNotFoundError: No module named 'p1_data_cleaning' +``` + +### 判斷 + +紅燈成功。 +原因是 `test_p1_data_cleaning.py` 已建立,但 `p1_data_cleaning.py` 尚未建立或尚未實作。 + +--- + +## 綠燈測試 + +### 測試指令 + +```bash +python .\test_p1_data_cleaning.py +``` + +### 測試結果 + +```text +.... +---------------------------------------------------------------------- +Ran 4 tests in 0.001s + +OK +``` + +### 判斷 + +第一題綠燈成功。 +代表資料清理功能可正常處理: + +* 去除重複值 +* 保留第一次出現順序 +* 篩選可被 `D = 5` 整除的數字 +* 排序輸出 +* 沒有符合條件時輸出 `NONE` + +--- + +## 手動測試 + +### 測試指令 + +```bash +python .\p1_data_cleaning.py +``` + +### 測試輸入 + +```text +3 +1 3 5 +0 +``` + +### 預期輸出 + +```text +5 +``` + +### 判斷 + +手動測試成功。 +因為 `1 3 5` 中只有 `5` 可以被 `D = 5` 整除。 + +--- + +# 第二題:凱撒密碼 Caesar Cipher + +## 紅燈測試 + +### 測試指令 + +```bash +python .\test_p2_caesar_cipher.py +``` + +### 測試結果 + +```text +Traceback (most recent call last): + File "test_p2_caesar_cipher.py", line 7, in + from p2_caesar_cipher import caesar_cipher, shift_char +ModuleNotFoundError: No module named 'p2_caesar_cipher' +``` + +### 判斷 + +紅燈成功。 +原因是 `test_p2_caesar_cipher.py` 已建立,但 `p2_caesar_cipher.py` 尚未建立或尚未實作。 + +--- + +## 綠燈測試 + +### 測試指令 + +```bash +python .\test_p2_caesar_cipher.py +``` + +### 測試結果 + +```text +.... +---------------------------------------------------------------------- +Ran 4 tests in 0.001s + +OK +``` + +### 判斷 + +第二題綠燈成功。 +代表凱撒密碼功能可正常處理: + +* 大寫字母位移 +* 小寫字母位移 +* `Z/z` 循環位移 +* 非英文字母保持原樣 +* 多行輸入直到 EOF + +--- + +## 手動測試 + +### 測試指令 + +```bash +python .\p2_caesar_cipher.py +``` + +### 測試輸入 + +```text +Hello, NPU +abc XYZ +``` + +### 實際輸出 + +```text +Lipps, RTY +efg BCD +``` + +### 判斷 + +手動測試成功。 +因為本題 `SHIFT = 4`,所以: + +* `Hello` → `Lipps` +* `NPU` → `RTY` +* `abc` → `efg` +* `XYZ` → `BCD` + +--- + +# 第三題:任意進位的數字根 + +## 紅燈測試 + +### 測試指令 + +```bash +python .\test_p3_digit_root_base.py +``` + +### 測試結果 + +```text +Traceback (most recent call last): + File "test_p3_digit_root_base.py", line 7, in + from p3_digit_root_base import digit_root, solve, sum_digits_in_base +ModuleNotFoundError: No module named 'p3_digit_root_base' +``` + +### 判斷 + +紅燈成功。 +原因是 `test_p3_digit_root_base.py` 已建立,但 `p3_digit_root_base.py` 尚未建立或尚未實作。 + +--- + +## 綠燈測試 + +### 測試指令 + +```bash +python .\test_p3_digit_root_base.py +``` + +### 測試結果 + +```text +..... +---------------------------------------------------------------------- +Ran 5 tests in 0.001s + +OK +``` + +### 判斷 + +第三題綠燈成功。 +代表任意進位數字根功能可正常處理: + +* `0` +* 小於 `base` 的數字 +* 需要重複做各位數字相加的數字 +* 多行輸入直到 EOF +* 負數例外處理 + +--- + +## 手動測試 + +### 測試指令 + +```bash +python .\p3_digit_root_base.py +``` + +### 測試輸入 + +```text +0 +8 +64 +``` + +### 實際輸出 + +```text +0 +2 +2 +``` + +### 判斷 + +手動測試成功。 +本題 `base = 3`。 + +說明: + +* `0` 的數字根為 `0` +* `8` 的三進位是 `22`,`2 + 2 = 4`,`4` 的三進位是 `11`,`1 + 1 = 2` +* `64` 經過 base 3 數字根運算後結果為 `2` + +--- + +# 第四題:二分搜尋效能 Binary Search Performance + +## 紅燈測試 + +### 測試指令 + +```bash +python .\test_p4_binary_search_perf.py +``` + +### 測試結果 + +```text +Traceback (most recent call last): + File "test_p4_binary_search_perf.py", line 7, in + from p4_binary_search_perf import ( +ModuleNotFoundError: No module named 'p4_binary_search_perf' +``` + +### 判斷 + +紅燈成功。 +原因是 `test_p4_binary_search_perf.py` 已建立,但 `p4_binary_search_perf.py` 尚未建立或尚未實作。 + +--- + +## 綠燈測試 + +### 測試指令 + +```bash +python .\test_p4_binary_search_perf.py +``` + +### 測試結果 + +```text +........... +---------------------------------------------------------------------- +Ran 11 tests in 0.074s + +OK +``` + +### 判斷 + +第四題綠燈成功。 +代表二分搜尋效能程式可正常處理: + +* 陣列長度至少 `10^5` +* 升冪陣列 +* `K = 123` +* linear search 比較次數 +* binary search 比較次數 +* 搜尋時間統計 +* best case +* middle case +* worst case +* not_found case +* 不修改原始資料 + +--- + +## 效能測試:best / middle / worst / not_found + +### 測試指令 + +```bash +python .\p4_binary_search_perf.py +``` + +執行後在等待輸入時按: + +```text +Ctrl + Z +Enter +``` + +### 實際輸出 + +```text +case=best +data_size=100000 +target=123 +target_index=0 +linear_index=0 +linear_comparisons=1 +linear_time=2.6999972760677337e-06 +binary_index=0 +binary_comparisons=16 +binary_time=5.6799966841936115e-06 + +case=middle +data_size=100000 +target=123 +target_index=50000 +linear_index=50000 +linear_comparisons=50001 +linear_time=0.0074086999928113075 +binary_index=50000 +binary_comparisons=16 +binary_time=6.579997716471553e-06 + +case=worst +data_size=100000 +target=123 +target_index=99999 +linear_index=99999 +linear_comparisons=100000 +linear_time=0.013151360000483692 +binary_index=99999 +binary_comparisons=17 +binary_time=5.439994856715202e-06 + +case=not_found +data_size=100000 +target=123 +target_index=-1 +linear_index=-1 +linear_comparisons=100000 +binary_index=-1 +``` + +### 判斷 + +第四題效能比較成功。 + +觀察結果: + +| Case | Linear Search 比較次數 | Binary Search 比較次數 | 判斷 | +| --------- | -----------------: | -----------------: | ---------------------- | +| best | 1 | 16 | Linear 在 best case 較有利 | +| middle | 50001 | 16 | Binary 明顯較有效率 | +| worst | 100000 | 17 | Binary 明顯較有效率 | +| not_found | 100000 | 約 16~17 | Binary 明顯較有效率 | + +結論: + +* linear search 的比較次數會受到目標位置影響。 +* target 在最前面時,linear search 最快。 +* target 在中間或最後面時,linear search 需要大量比較。 +* not_found 時,linear search 必須掃完整個陣列。 +* binary search 因為每次都將搜尋範圍砍半,所以比較次數穩定維持在約 `16~17` 次。 + +--- + +## 雷達圖產生測試 + +### 測試指令 + +```bash +python .\p4_binary_search_perf.py +``` + +### 產生結果 + +```text +radar=assets/radar.png +``` + +### 檔案位置 + +```text +assets/radar.png +``` + +### 判斷 + +雷達圖成功產生。 + +雷達圖比較項目包含: + +* `BestCmp` +* `MiddleCmp` +* `WorstCmp` +* `NotFoundCmp` +* `BestTime` +* `MiddleTime` +* `WorstTime` +* `NotFoundTime` + +比較次數與執行時間皆使用「越小越好」的正規化方式,因此分數越接近 `1.0` 表示該搜尋法在該項目表現越好。 + +--- + +# 總測試指令 + +```bash +python .\test_p1_data_cleaning.py +python .\test_p2_caesar_cipher.py +python .\test_p3_digit_root_base.py +python .\test_p4_binary_search_perf.py +python .\p4_binary_search_perf.py +``` + +--- + +# 總結 + +本次完成 CPE 模擬實戰四題測試與驗收: + +| 題目 | 測試檔 | 結果 | +| ----------- | ------------------------------- | ---------------------- | +| 第一題:資料清理 | `test_p1_data_cleaning.py` | OK | +| 第二題:凱撒密碼 | `test_p2_caesar_cipher.py` | OK | +| 第三題:任意進位數字根 | `test_p3_digit_root_base.py` | OK | +| 第四題:二分搜尋效能 | `test_p4_binary_search_perf.py` | OK | +| 第四題:雷達圖 | `p4_binary_search_perf.py` | 已產生 `assets/radar.png` | + + diff --git a/weeks/week-18/solutions/1114405023/assets/radar.png b/weeks/week-18/solutions/1114405023/assets/radar.png new file mode 100644 index 000000000..43bd2e856 Binary files /dev/null and b/weeks/week-18/solutions/1114405023/assets/radar.png differ diff --git a/weeks/week-18/solutions/1114405023/p1_data_cleaning.py b/weeks/week-18/solutions/1114405023/p1_data_cleaning.py new file mode 100644 index 000000000..6c7b242f8 --- /dev/null +++ b/weeks/week-18/solutions/1114405023/p1_data_cleaning.py @@ -0,0 +1,63 @@ +""" +第一題:資料清理 Data Cleaning +學號後兩碼 23,個位數 u = 3,所以 D = 5。 + +需求: +1. 讀取多組整數序列。 +2. 每組先去除重複值,保留第一次出現順序。 +3. 只保留可以被 D 整除的數字。 +4. 將結果由小到大排序後輸出。 +5. 若結果為空,輸出 NONE。 +""" + +import sys + +D = 5 + + +def clean_numbers(numbers, divisor=D): + """去除重複、保留可被 divisor 整除的數字,最後排序。""" + seen = set() + unique_numbers = [] + + for value in numbers: + if value not in seen: + seen.add(value) + unique_numbers.append(value) + + filtered = [value for value in unique_numbers if value % divisor == 0] + return sorted(filtered) + + +def solve(input_text): + """處理題目的多組輸入格式。""" + tokens = input_text.split() + index = 0 + outputs = [] + + while index < len(tokens): + n = int(tokens[index]) + index += 1 + + if n == 0: + break + + numbers = [int(tokens[index + i]) for i in range(n)] + index += n + + result = clean_numbers(numbers) + + if result: + outputs.append(" ".join(str(value) for value in result)) + else: + outputs.append("NONE") + + return "\n".join(outputs) + + +def main(): + sys.stdout.write(solve(sys.stdin.read())) + + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405023/p2_caesar_cipher.py b/weeks/week-18/solutions/1114405023/p2_caesar_cipher.py new file mode 100644 index 000000000..95b014a2c --- /dev/null +++ b/weeks/week-18/solutions/1114405023/p2_caesar_cipher.py @@ -0,0 +1,43 @@ +""" +第二題:凱撒密碼 Caesar Cipher +學號後兩碼 23,個位數 u = 3,所以 SHIFT = 4。 + +需求: +1. 讀取多行文字直到 EOF。 +2. 大寫 A-Z 依 SHIFT 位移並循環。 +3. 小寫 a-z 依 SHIFT 位移並循環。 +4. 非英文字母保持不變。 +""" + +import sys + +SHIFT = 4 + + +def shift_char(ch, shift=SHIFT): + """位移單一字元;非英文字母保持不變。""" + if "A" <= ch <= "Z": + return chr((ord(ch) - ord("A") + shift) % 26 + ord("A")) + + if "a" <= ch <= "z": + return chr((ord(ch) - ord("a") + shift) % 26 + ord("a")) + + return ch + + +def caesar_cipher(text, shift=SHIFT): + """對整段文字套用凱撒位移。""" + return "".join(shift_char(ch, shift) for ch in text) + + +def solve(input_text): + """處理 EOF 前所有輸入。""" + return caesar_cipher(input_text) + + +def main(): + sys.stdout.write(solve(sys.stdin.read())) + + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405023/p3_digit_root_base.py b/weeks/week-18/solutions/1114405023/p3_digit_root_base.py new file mode 100644 index 000000000..cf382fb45 --- /dev/null +++ b/weeks/week-18/solutions/1114405023/p3_digit_root_base.py @@ -0,0 +1,91 @@ +""" +第三題:任意進位的數字根 + +學號後兩碼 23,所以本題 base = 3。 + +需求: +1. 每行輸入一個十進位非負整數,直到 EOF。 +2. 將數字視為 base 進位下的數字,計算各位數字總和。 +3. 重複相加,直到結果小於 base。 +4. 輸出最後的數字根。 +""" + +import sys + +BASE = 3 + + +def sum_digits_in_base(value, base=BASE): + """ + 計算 value 在指定 base 下的各位數字總和。 + + 例如: + value = 8, base = 3 + 8 的三進位是 22 + 所以回傳 2 + 2 = 4 + """ + if value < 0: + raise ValueError("value must be non-negative") + + if value == 0: + return 0 + + total = 0 + + while value > 0: + total += value % base + value //= base + + return total + + +def digit_root(value, base=BASE): + """ + 計算任意進位的數字根。 + + Args: + value: 十進位非負整數。 + base: 進位基底,本題固定為 3。 + + Returns: + int: 最後小於 base 的數字根。 + + Raises: + ValueError: value 為負數時拋出。 + """ + if value < 0: + raise ValueError("value must be non-negative") + + while value >= base: + value = sum_digits_in_base(value, base) + + return value + + +def solve(input_text): + """ + 處理多行輸入直到 EOF。 + + 空行會略過。 + 每一行輸出一個數字根。 + """ + outputs = [] + + for line in input_text.splitlines(): + line = line.strip() + + if line == "": + continue + + value = int(line) + outputs.append(str(digit_root(value))) + + return "\n".join(outputs) + + +def main(): + sys.stdout.write(solve(sys.stdin.read())) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405023/p4_binary_search_perf.py b/weeks/week-18/solutions/1114405023/p4_binary_search_perf.py new file mode 100644 index 000000000..0b59c4dbf --- /dev/null +++ b/weeks/week-18/solutions/1114405023/p4_binary_search_perf.py @@ -0,0 +1,420 @@ +""" +第四題:二分搜尋效能 + +本題參數: +- 陣列大小至少 10^5 +- 搜尋目標 K = 123 +- 測試資料使用升冪陣列 +- 比較 linear search 與 binary search 的比較次數與搜尋時間 +- 分別測 best、middle、worst、not_found 四種情境 +- 使用 Python 標準庫 timeit 量測搜尋時間 +- 輸出每個 case 中最快的搜尋法 +- 執行主程式時會自動產生 assets/radar.png +""" + +import math +import sys +import timeit as timeit_module +from pathlib import Path + +DATA_SIZE = 100000 +K = 123 + + +def make_test_data_with_target_at(target_index, size=DATA_SIZE, target=K): + """ + 建立升冪測試資料,並讓 target 出現在指定 index。 + """ + if size < 1: + return [] + + if target_index < 0 or target_index >= size: + raise ValueError("target_index out of range") + + start = target - target_index + return list(range(start, start + size)) + + +def make_test_data(size=DATA_SIZE, target=K): + """ + 建立預設測試資料。 + + 預設讓 target 位於最後一個位置,也就是 worst case。 + """ + return make_test_data_with_target_at(size - 1, size, target) + + +def make_not_found_data(size=DATA_SIZE, target=K): + """ + 建立 not_found 測試資料。 + + 資料長度至少 10^5,且保持升冪。 + 所有資料都大於 target,所以 target 不存在於陣列中。 + """ + if size < 1: + return [] + + return list(range(target + 1, target + 1 + size)) + + +def is_ascending(data): + """ + 檢查資料是否為升冪。 + + 這裡使用非遞減判斷: + 例如 [1, 2, 2, 3] 也視為可搜尋的排序資料。 + """ + for index in range(len(data) - 1): + if data[index] > data[index + 1]: + return False + + return True + + +def linear_search_with_count(data, target=K): + """ + 線性搜尋。 + + Returns: + tuple[int, int]: + 找到時回傳 (index, comparison_count) + 找不到時回傳 (-1, comparison_count) + """ + comparisons = 0 + + for index, value in enumerate(data): + comparisons += 1 + + if value == target: + return index, comparisons + + return -1, comparisons + + +def binary_search_with_count(data, target=K, check_sorted=True): + """ + 二分搜尋。 + + binary search 的前提是 data 已排序。 + 若 check_sorted=True,會先檢查資料是否升冪。 + 若資料未排序,依本次規格回傳 (-1, 0)。 + + Returns: + tuple[int, int]: + 找到時回傳 (index, comparison_count) + 找不到時回傳 (-1, comparison_count) + """ + if check_sorted and not is_ascending(data): + return -1, 0 + + left = 0 + right = len(data) - 1 + comparisons = 0 + + while left <= right: + middle = (left + right) // 2 + comparisons += 1 + + if data[middle] == target: + return middle, comparisons + + if data[middle] < target: + left = middle + 1 + else: + right = middle - 1 + + return -1, comparisons + + +def average_time(func, repeat=5): + """ + 使用 Python 標準庫 timeit.repeat() 計算平均執行時間。 + + 注意: + - number=1 表示每次 timeit 只執行一次 func。 + - repeat=5 表示重複量測 5 次。 + - result 另外執行一次取得搜尋結果。 + - 不把資料建立、輸入、輸出算進搜尋時間。 + + Returns: + tuple[object, float]: + 回傳搜尋結果與平均時間。 + """ + if repeat < 1: + raise ValueError("repeat must be >= 1") + + result = func() + + records = timeit_module.repeat( + stmt=func, + repeat=repeat, + number=1, + ) + + average = sum(records) / len(records) + return result, average + + +def fastest_search(linear_time, binary_time): + """ + 比較 linear search 與 binary search 的平均執行時間, + 回傳最快的搜尋法。 + """ + if linear_time < binary_time: + return "linear" + + if binary_time < linear_time: + return "binary" + + return "tie" + + +def benchmark_search(data, target=K, repeat=5): + """ + 比較 linear search 與 binary search 的時間與比較次數。 + + 注意: + data 應該先在外部建立好。 + benchmark 不把資料建立、輸入、輸出算進搜尋時間。 + + binary search 在 benchmark 裡不重複檢查排序, + 因為測試資料已由產生函式保證升冪。 + """ + linear_result, linear_time = average_time( + lambda: linear_search_with_count(data, target), + repeat=repeat, + ) + + binary_result, binary_time = average_time( + lambda: binary_search_with_count(data, target, check_sorted=False), + repeat=repeat, + ) + + linear_index, linear_comparisons = linear_result + binary_index, binary_comparisons = binary_result + + return { + "linear_index": linear_index, + "linear_comparisons": linear_comparisons, + "linear_time": linear_time, + "binary_index": binary_index, + "binary_comparisons": binary_comparisons, + "binary_time": binary_time, + "fastest": fastest_search(linear_time, binary_time), + } + + +def benchmark_cases(size=DATA_SIZE, target=K, repeat=5): + """ + 比較 best / middle / worst / not_found 四種情境。 + """ + case_specs = [ + ("best", make_test_data_with_target_at(0, size, target), 0), + ("middle", make_test_data_with_target_at(size // 2, size, target), size // 2), + ("worst", make_test_data_with_target_at(size - 1, size, target), size - 1), + ("not_found", make_not_found_data(size, target), -1), + ] + + results = [] + + for case_name, data, target_index in case_specs: + result = benchmark_search(data, target, repeat) + + results.append( + { + "case": case_name, + "data_size": len(data), + "target": target, + "target_index": target_index, + **result, + } + ) + + return results + + +def format_case_result(result): + """ + 將單一 case 的 benchmark 結果格式化成文字。 + """ + lines = [ + f"case={result['case']}", + f"data_size={result['data_size']}", + f"target={result['target']}", + f"target_index={result['target_index']}", + f"linear_index={result['linear_index']}", + f"linear_comparisons={result['linear_comparisons']}", + f"linear_time={result['linear_time']}", + f"binary_index={result['binary_index']}", + f"binary_comparisons={result['binary_comparisons']}", + f"binary_time={result['binary_time']}", + f"fastest={result['fastest']}", + ] + + return "\n".join(lines) + + +def normalize_smaller_better(value, best_value): + """ + 越小越好的正規化。 + + score = best_value / value + + 同一個維度中,數值最小者分數為 1.0。 + """ + if value <= 0: + return 0.0 + + return best_value / value + + +def build_radar_scores(results): + """ + 將 best / middle / worst / not_found 的 benchmark 結果轉成雷達圖分數。 + """ + labels = [ + "BestCmp", + "MiddleCmp", + "WorstCmp", + "NotFoundCmp", + "BestTime", + "MiddleTime", + "WorstTime", + "NotFoundTime", + ] + + case_map = {item["case"]: item for item in results} + + linear_values = [ + case_map["best"]["linear_comparisons"], + case_map["middle"]["linear_comparisons"], + case_map["worst"]["linear_comparisons"], + case_map["not_found"]["linear_comparisons"], + case_map["best"]["linear_time"], + case_map["middle"]["linear_time"], + case_map["worst"]["linear_time"], + case_map["not_found"]["linear_time"], + ] + + binary_values = [ + case_map["best"]["binary_comparisons"], + case_map["middle"]["binary_comparisons"], + case_map["worst"]["binary_comparisons"], + case_map["not_found"]["binary_comparisons"], + case_map["best"]["binary_time"], + case_map["middle"]["binary_time"], + case_map["worst"]["binary_time"], + case_map["not_found"]["binary_time"], + ] + + linear_scores = [] + binary_scores = [] + + for linear_value, binary_value in zip(linear_values, binary_values): + best_value = min(linear_value, binary_value) + + linear_scores.append(normalize_smaller_better(linear_value, best_value)) + binary_scores.append(normalize_smaller_better(binary_value, best_value)) + + return labels, linear_scores, binary_scores + + +def save_radar_chart(results, output_path="assets/radar.png"): + """ + 依照 benchmark 結果產生雷達圖。 + matplotlib 延後 import,避免測試搜尋函式時因套件問題失敗。 + """ + import matplotlib + + matplotlib.use("Agg") + + import matplotlib.pyplot as plt + + labels, linear_scores, binary_scores = build_radar_scores(results) + + count = len(labels) + angles = [2 * math.pi * i / count for i in range(count)] + + # 雷達圖需要首尾閉合。 + angles_closed = angles + [angles[0]] + linear_closed = linear_scores + [linear_scores[0]] + binary_closed = binary_scores + [binary_scores[0]] + + fig = plt.figure(figsize=(8, 8)) + ax = fig.add_subplot(111, polar=True) + + ax.plot(angles_closed, linear_closed, marker="o", label="Linear Search") + ax.fill(angles_closed, linear_closed, alpha=0.15) + + ax.plot(angles_closed, binary_closed, marker="o", label="Binary Search") + ax.fill(angles_closed, binary_closed, alpha=0.15) + + ax.set_xticks(angles) + ax.set_xticklabels(labels) + ax.set_ylim(0, 1.05) + ax.set_title("Search Performance Radar") + ax.legend(loc="upper right", bbox_to_anchor=(1.25, 1.10)) + + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + fig.savefig(output_path, bbox_inches="tight") + plt.close(fig) + + return output_path + + +def solve(input_text): + """ + 處理手動輸入。 + + 若沒有輸入: + 自動執行 best / middle / worst / not_found 四種情境, + 每組資料長度皆為 100000, + 並產生 assets/radar.png。 + + 若有輸入: + 輸入格式為: + 第一行 n + 第二行 n 個升冪整數 + + 這時只針對該組資料做一次 benchmark。 + """ + tokens = input_text.split() + + if tokens: + n = int(tokens[0]) + data = [int(value) for value in tokens[1 : 1 + n]] + + result = benchmark_search(data, K, repeat=5) + + output_lines = [ + "case=custom", + f"data_size={len(data)}", + f"target={K}", + f"linear_index={result['linear_index']}", + f"linear_comparisons={result['linear_comparisons']}", + f"linear_time={result['linear_time']}", + f"binary_index={result['binary_index']}", + f"binary_comparisons={result['binary_comparisons']}", + f"binary_time={result['binary_time']}", + f"fastest={result['fastest']}", + ] + + return "\n".join(output_lines) + + results = benchmark_cases(DATA_SIZE, K, repeat=5) + blocks = [format_case_result(result) for result in results] + + radar_path = save_radar_chart(results) + blocks.append(f"radar={radar_path}") + + return "\n\n".join(blocks) + + +def main(): + sys.stdout.write(solve(sys.stdin.read())) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405023/test_p1_data_cleaning.py b/weeks/week-18/solutions/1114405023/test_p1_data_cleaning.py new file mode 100644 index 000000000..6549413d8 --- /dev/null +++ b/weeks/week-18/solutions/1114405023/test_p1_data_cleaning.py @@ -0,0 +1,33 @@ +""" +第一題測試:資料清理 +""" + +import unittest + +from p1_data_cleaning import clean_numbers, solve + + +class TestDataCleaning(unittest.TestCase): + def test_clean_numbers_should_remove_duplicates_filter_and_sort(self): + numbers = [10, 3, 10, 5, -5] + self.assertEqual(clean_numbers(numbers), [-5, 5, 10]) + + def test_clean_numbers_should_return_empty_when_no_match(self): + self.assertEqual(clean_numbers([1, 2, 3, 4]), []) + + def test_clean_numbers_should_not_modify_original_list(self): + numbers = [10, 5, 10, 3] + original = numbers.copy() + + clean_numbers(numbers) + + self.assertEqual(numbers, original) + + def test_solve_multiple_groups(self): + input_text = "5\n10 3 10 5 -5\n4\n1 2 3 4\n0\n" + expected = "-5 5 10\nNONE" + self.assertEqual(solve(input_text), expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405023/test_p2_caesar_cipher.py b/weeks/week-18/solutions/1114405023/test_p2_caesar_cipher.py new file mode 100644 index 000000000..894ccca64 --- /dev/null +++ b/weeks/week-18/solutions/1114405023/test_p2_caesar_cipher.py @@ -0,0 +1,27 @@ +""" +第二題測試:凱撒密碼 +""" + +import unittest + +from p2_caesar_cipher import caesar_cipher, shift_char + + +class TestCaesarCipher(unittest.TestCase): + def test_uppercase_should_wrap(self): + self.assertEqual(shift_char("Z"), "D") + + def test_lowercase_should_wrap(self): + self.assertEqual(shift_char("z"), "d") + + def test_non_letter_should_not_change(self): + self.assertEqual(caesar_cipher("123 !?"), "123 !?") + + def test_sample_with_shift_four(self): + input_text = "Hello, NPU!\nabc XYZ\n" + expected = "Lipps, RTY!\nefg BCD\n" + self.assertEqual(caesar_cipher(input_text), expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405023/test_p3_digit_root_base.py b/weeks/week-18/solutions/1114405023/test_p3_digit_root_base.py new file mode 100644 index 000000000..418258262 --- /dev/null +++ b/weeks/week-18/solutions/1114405023/test_p3_digit_root_base.py @@ -0,0 +1,32 @@ +""" +第三題測試:任意進位的數字根 +""" + +import unittest + +from p3_digit_root_base import digit_root, solve, sum_digits_in_base + + +class TestDigitRootBase(unittest.TestCase): + def test_zero_should_return_zero(self): + self.assertEqual(digit_root(0), 0) + + def test_value_smaller_than_base_should_return_itself(self): + self.assertEqual(digit_root(2), 2) + + def test_sum_digits_in_base_three(self): + # 8 的三進位是 22,所以 2 + 2 = 4 + self.assertEqual(sum_digits_in_base(8), 4) + + def test_digit_root_base_three(self): + # 8 -> base3: 22 -> 4;4 -> base3: 11 -> 2 + self.assertEqual(digit_root(8), 2) + + def test_solve_multiple_lines(self): + input_text = "0\n8\n63\n" + expected = "0\n2\n1" + self.assertEqual(solve(input_text), expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405023/test_p4_binary_search_perf.py b/weeks/week-18/solutions/1114405023/test_p4_binary_search_perf.py new file mode 100644 index 000000000..b7f0e11a0 --- /dev/null +++ b/weeks/week-18/solutions/1114405023/test_p4_binary_search_perf.py @@ -0,0 +1,130 @@ +""" +第四題測試:二分搜尋效能 + +本題參數: +- 陣列大小至少 10^5 +- 搜尋目標 K = 123 +- 比較 linear search 與 binary search 的比較次數與搜尋時間 +""" + +import unittest + +from p4_binary_search_perf import ( + DATA_SIZE, + K, + make_test_data, + linear_search_with_count, + binary_search_with_count, + benchmark_search, +) + + +class TestBinarySearchPerformance(unittest.TestCase): + def test_data_size_should_be_at_least_100000(self): + """效能測試陣列長度至少要 10^5。""" + self.assertGreaterEqual(DATA_SIZE, 100000) + + def test_make_test_data_should_be_sorted_and_contain_target(self): + """測試資料要已排序,且包含 K = 123。""" + data = make_test_data() + + self.assertGreaterEqual(len(data), 100000) + self.assertIn(K, data) + self.assertEqual(data, sorted(data)) + + def test_linear_search_found_should_return_index_and_comparisons(self): + """linear search 找到目標時,要回傳 index 與比較次數。""" + data = [0, 0, 123] + + index, comparisons = linear_search_with_count(data, K) + + self.assertEqual(index, 2) + self.assertEqual(comparisons, 3) + + def test_linear_search_not_found_should_return_minus_one(self): + """linear search 找不到目標時,要回傳 -1 與比較次數。""" + data = [0, 0, 0] + + index, comparisons = linear_search_with_count(data, K) + + self.assertEqual(index, -1) + self.assertEqual(comparisons, 3) + + def test_binary_search_found_should_return_index_and_comparisons(self): + """binary search 找到目標時,要回傳 index 與比較次數。""" + data = [0, 0, 0, 123] + + index, comparisons = binary_search_with_count(data, K) + + self.assertEqual(index, 3) + self.assertGreaterEqual(comparisons, 1) + + def test_binary_search_not_found_should_return_minus_one(self): + """binary search 找不到目標時,要回傳 -1 與比較次數。""" + data = [0, 0, 0, 0] + + index, comparisons = binary_search_with_count(data, K) + + self.assertEqual(index, -1) + self.assertGreaterEqual(comparisons, 1) + + def test_binary_search_empty_list_should_return_minus_one(self): + """空 list 搜尋不到資料,應回傳 -1。""" + index, comparisons = binary_search_with_count([], K) + + self.assertEqual(index, -1) + self.assertEqual(comparisons, 0) + + def test_binary_search_unsorted_data_should_return_minus_one(self): + """未排序資料不 raise error,依本次規格回傳 -1。""" + data = [123, 0, 5] + + index, comparisons = binary_search_with_count(data, K) + + self.assertEqual(index, -1) + self.assertGreaterEqual(comparisons, 0) + + def test_binary_search_duplicate_target_can_return_any_matching_index(self): + """如果有兩個相同的目標值,找到任一個正確 index 即可。""" + data = [0, 0, 123, 123, 200] + + index, comparisons = binary_search_with_count(data, K) + + self.assertIn(index, [2, 3]) + self.assertEqual(data[index], K) + self.assertGreaterEqual(comparisons, 1) + + def test_search_functions_should_not_modify_original_data(self): + """兩種搜尋都不可以修改原始資料。""" + data = [0, 0, 123] + original = data.copy() + + linear_search_with_count(data, K) + binary_search_with_count(data, K) + + self.assertEqual(data, original) + + def test_benchmark_should_return_times_and_comparisons(self): + """benchmark 要回傳兩種搜尋法的時間與比較次數。""" + data = make_test_data() + + result = benchmark_search(data, K, repeat=3) + + self.assertIn("linear_index", result) + self.assertIn("linear_comparisons", result) + self.assertIn("linear_time", result) + self.assertIn("binary_index", result) + self.assertIn("binary_comparisons", result) + self.assertIn("binary_time", result) + + self.assertIsInstance(result["linear_time"], float) + self.assertIsInstance(result["binary_time"], float) + self.assertGreaterEqual(result["linear_time"], 0) + self.assertGreaterEqual(result["binary_time"], 0) + + self.assertGreaterEqual(result["linear_comparisons"], 1) + self.assertGreaterEqual(result["binary_comparisons"], 1) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file