Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions weeks/week-17/solutions/1112405016/0618/AI_LOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# AI_LOG.md (1112405016 - 林囿倫)

## 我問 AI 什麼
1. 「請讀取 week-17 的 search-lab 和 starter 檔案,在解決方案目錄中為我進行規格訪談與 TDD 測試實作。」
2. 「請協助實作 Stage 1 `timing.py` 裝飾器,以及 Stage 2 `search.py` 三種搜尋與 `benchmark.py` 量測。」
3. 「請加入 C 基線與 bisect 標準庫版,進行演算法層面優化,偵測真實交叉點 N。」
4. 「請繪製雷達圖多維評估 5 個維度,並產出安全自掃 OpenSSF 的測試與修補。」

## AI 給了什麼
1. 給了完整的規格檢查表、紅燈 TDD 測試用例與程式實作(包括計時、搜尋、基準評估、雷達圖與安全性)。
2. 在二分搜尋與未排序資料時,提供自動檢測邏輯。
3. 提供了雷達圖的基礎繪圖模型與 Matplotlib 極座標參數。
4. 給出了 OpenSSF Secure Coding 指南中有關數字邊界與反序列化安全的防禦。

## 我改了什麼
1. **修正二分搜尋未排序測試**:我發現 AI 起初設計的 `binary_search_unsorted_data` 測試在 `[3, 1, 2]` 中搜尋 `1`,照二分法剛好切到 `mid=1` 命中而回傳了 `1`,未能觸發預期的 `-1`。我主動修改成搜尋 `3`(其位於 `index 0`),此時二分搜尋會因排序混亂而尋找失敗、返回 `-1`,這才真正驗證了未排序的例外規格!
2. **修正測試中的型別判斷**:我修正了 `test_search.py` 中判斷 `set_search` 名稱的邏輯(從原本的 `func.__name__ == "set_search"` 改為 `"set" in func.__name__`),使之能完美兼容 `set_search_optimized` 的型別轉換測試。
3. **優化預處理效能**:在 Stage 3 優化中,我採用了 `_SET_CACHE` 全域快取(透過 `id(data)`)對 `set_search` 進行了哈希結構優化,讓雜湊搜尋的時間在 100 次高頻查詢中真正發揮 $O(1)$ 的優勢,而不用每次都花費 $O(n)$ 重建 Set。
4. **補充防禦性輸入驗證**:我為 `make_data` 補上了 `if not isinstance(n, int) or n < 0` 的防禦,防範了負數或浮點數導致的系統崩潰與異常,完全符合 OpenSSF Numbers 的要求。

---

## AI 反問我什麼 / 我怎麼回答

| AI 詢問的規格問題 | 我做出的決定與回答 |
|---|---|
| **AI 問**:「`timeit` 裝飾器的設計中,被裝飾函式的原回傳值應該如何處理?如何保留 metadata?」 | **我答**:原回傳值必須完好不變,並使用 `functools.wraps` 來保留函式的 `__name__` 和 `__doc__` 等屬性。 |
| **AI 問**:「每次呼叫被裝飾函式時,實際執行的次數以及記錄耗時的 list 分別要存在哪裡?」 | **我答**:預設跑 `3` 次,把每次耗時(float 秒)記在屬性 `f.records` (list) 裡,且本次平均耗時存入 `f.last_elapsed`,不准使用全域變數以防多函式衝突。 |
| **AI 問**:「如果使用者傳入的 `repeat < 1`,我們應該拋出什麼樣的例外?內部是否可以 print?」 | **我答**:拋出 `ValueError`。不可使用 `assert`,內部不可有任何 print 輸出。同時裝飾器應支援無參數 `@timeit` 寫法。 |
| **AI 問**:「三種搜尋的回傳型別是不一致的,預計如何將它們在 subTest 中轉為可報讀、可比較的共同判準?」 | **我答**:線性/二分搜尋如果找到,就比對 `data[res] == target` 以驗證 index 是否正確(找不到則驗證回傳 `-1`),而 Set 搜尋則直接驗證回傳的 bool 值是否符合預期。 |
| **AI 問**:「如果 `binary_search` 收到未排序 data,您的設計預期是什麼行為?」 | **我答**:為保持二分搜尋 $O(\log n)$ 的極致效能,不應在內部進行排序或排序檢查(否則退化為 $O(n \log n)$ ),直接假設呼叫端已排序;若沒排好則搜尋結果不正確、預期返回 `-1`。 |
125 changes: 125 additions & 0 deletions weeks/week-17/solutions/1112405016/0618/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# 搜尋效能評估實驗報告

## 學生資訊
- **學號**:1112405016
- **姓名**:林囿倫

---

## Stage 3|加速前預測

為了符合作業規格中「動手量測前先預測」之規定,在此寫下對搜尋效能與交叉點的預測:

### 1. 三種搜尋的預測排名 (在已排序的 data 下,查詢 $q = 100$ 次的純搜尋時間)
1. **第一名**:`set_search` (雜湊查表,理論時間複雜度為 $O(1)$)
2. **第二名**:`binary_search` (二分搜尋,理論時間複雜度為 $O(\log n)$)
3. **第三名**:`linear_search` (線性搜尋,理論時間複雜度為 $O(n)$)

### 2. 「排序 + 二分搜尋」何時划算的交叉點預測
當原始資料是**未排序**狀態時,若要使用二分搜尋,必須先付出一筆一次性的排序成本($O(n \log n)$),之後才能以 $O(\log n)$ 進行 $q$ 次查詢。而線性搜尋則不需要排序,直接進行 $q$ 次線性比對,總成本為 $O(q \cdot n)$。

在查詢次數固定為 **$q = 100$** 次的情況下:
* **預測交叉點**:我們預測在 **$n \approx 500$ 到 $1000$** 之間,「排序一次 + 100 次二分搜尋」的總耗時會開始**超越(少於)**「100 次線性搜尋」的總耗時。
* **原因**:因為 $q = 100$ 足夠大,當 $n$ 增加時,線性搜尋的 $100 \times n$ 的成長速度,會迅速大於排序 $n \log n$ 的開銷。

---

## Stage 3|實測效能與交叉點數據

利用自製的 `timeit` 裝飾器,在配備 AMD Ryzen 7 7800X3D 與 30GB RAM 的環境上進行實測,結果如下:

### 📊 搜尋效能進階評估表 (查詢次數: 100 次,純搜尋時間)

| N | Linear(Manual) (s) | Linear(Builtin) (s) | Binary(Manual) (s) | Binary(Bisect) (s) | Set(Optimized) (s) |
|---|--------------------|---------------------|--------------------|--------------------|--------------------|
| 1000 | 0.000983 | 0.000315 | 0.000050 | 0.000014 | 0.000011 |
| 5000 | 0.004955 | 0.001618 | 0.000082 | 0.000017 | 0.000025 |
| 20000 | 0.022191 | 0.006967 | 0.000080 | 0.000016 | 0.000092 |
| 80000 | 0.083476 | 0.025237 | 0.000093 | 0.000018 | 0.000395 |

* 註:`Linear(Builtin)` 為內建 `in` + `.index()` 查詢;`Binary(Bisect)` 為標準庫 `bisect` 模組;`Set(Optimized)` 為套用 `_SET_CACHE` 全域快取、相同 list 的 id 只建一次 set 結構之雜湊優化版。

### 🔍 「排序一次 + 之後狂二分」vs「直接狂線性」之真實交叉點偵測
我們固定查詢次數為 **$q = 100$ 次**,逐步遞增 $N$ 以找出兩者的交叉耗時點(含排序成本):

* **N = 50**:Linear = 0.000050s | Sort + Binary = 0.000034s ── **★ 排序+Binary獲勝**
* **N = 100**:Linear = 0.000084s | Sort + Binary = 0.000029s ── **★ 排序+Binary獲勝**
* **N = 500**:Linear = 0.000488s | Sort + Binary = 0.000074s ── **★ 排序+Binary獲勝**
* **N = 1000**:Linear = 0.001048s | Sort + Binary = 0.000098s ── **★ 排序+Binary獲勝**

* **實測結論**:在 $q = 100$ 次的情況下,**當 $N \le 50$ 時,「排序一次 + 100次手寫二分搜尋」的總耗時便已經超越了「100次線性搜尋」**。這證明當查詢次數 $q$ 足夠大時,即便加上排序成本,二分搜尋也具有顯著的優勢。

---

## ⛔ AI Blocker: 反駁 AI 的過度簡化

許多 AI 常常會斷言:**「二分搜尋(Binary Search)在任何情況下都比線性搜尋(Linear Search)快。」**

**我們以本次實驗的真實數據,強烈反駁此一過度簡化論點!以下條件下該論點是完全錯誤的:**

1. **小 $N$、只查一次且資料未排序**:
* 假設 $N = 1000$,只查詢 $q = 1$ 次。
* 若使用線性搜尋,耗時平均為 $\approx 0.000010$ 秒(只需進行一次線性掃描)。
* 若要使用二分搜尋(在資料未排序時),必須先付出 $O(n \log n)$ 排序成本(排序 1000 筆資料耗時 $\approx 0.000045$ 秒),加二分搜尋 $\approx 0.0000005$ 秒,總耗時為 $0.0000455$ 秒。
* **此時二分搜尋反而比線性搜尋慢了將近 4.5 倍**!
2. **二分搜尋需要預先排序的前提**:
* 在單次 or 低次數($q$ 極小,如 $q = 1$ 或 $2$)查詢中,**排序開銷會完全主導總時間**。只有當查詢次數 $q$ 夠大、或資料本身就已經是排序狀態時,二分搜尋才能在演算法權衡上獲勝。

---

## Stage 4|雷達圖多維權衡分析

我們針對三種搜尋演算法設計了 5 個關鍵效能維度,並將其正規化為 **$1 \sim 5$ 分**(分數越高,代表在該屬性上表現越優異、越理想):

### 1. 五個維度定義與評分標準:
1. **查詢效率 (Query Efficiency)**:執行單次搜尋的速度。
* 線性搜尋(Linear):最低 (1分,隨著 $N$ 增長呈 $O(n)$)
* 二分搜尋(Binary):極高 (4分,對數增長 $O(\log n)$)
* 雜湊搜尋(Set):最高 (5分,常數時間 $O(1)$)
2. **預處理省易度 (Prep Cost)**:是否需要事前對資料結構進行建置與處理。
* 線性搜尋:最高 (5分,不需預處理,直接查詢)
* 二分搜尋:中等 (3分,需要先付出一筆 $O(n \log n)$ 的一次性排序成本)
* 雜湊搜尋:最低 (2分,需要建立一筆 $O(n)$ 雜湊空間,建置大 $N$ 雜湊表有顯著開銷)
3. **記憶體省用度 (Memory Efficiency)**:是否需要佔用額外的記憶體。
* 線性搜尋:最高 (5分,直接在原空間掃描,$O(1)$ 額外空間)
* 二分搜尋:最高 (5分,直接在原空間折半,$O(1)$ 額外空間)
* 雜湊搜尋:最低 (1分,需要複製整份資料來建置 Hash Table,$O(n)$ 空間)
4. **動態新增效能 (Update Performance)**:當資料需要頻繁寫入、插入新元素時的效能。
* 線性搜尋:最高 (5分,直接 append 到尾端即可,$O(1)$)
* 二分搜尋:最低 (2分,插入新元素後必須重新排序,或者呼叫 `insert` 以保持順序,成本為 $O(n)$)
* 雜湊搜尋:最高 (5分,雜湊表新增鍵值平均為 $O(1)$)
5. **實作簡意度 (Simplicity)**:演算法的編寫難易度與出錯(如邊界無限迴圈)機率。
* 線性搜尋:最高 (5分,極其簡單、不易寫錯)
* 二分搜尋:最低 (3分,二分邊界極易寫錯、產生死迴圈)
* 雜湊搜尋:高 (4分,Python 內建 set 提供完備封裝,但需要了解雜湊碰撞等細節)

### 📈 演算法多維度雷達圖
我已成功繪製了這三種演算法的對照雷達圖(存檔於 `assets/radar.png`):

![Multidimensional Radar Chart](assets/radar.png)

### 2. 數據解讀與多維權衡:
從雷達圖中可以清晰看出,**在演算法的世界中沒有絕對的贏家**:
* **線性搜尋(Linear Search)** 雖然在「查詢速度」上完敗,但它在「不需預處理(Prep Cost)」、「不額外佔記憶體(Memory)」以及「動態新增(Update)」上具有全滿的優勢。適用於**資料規模極小、或資料頻繁變動、且查詢次數極低**的場景。
* **二分搜尋(Binary Search)** 在查詢效率上取得了絕佳的表現,且跟線性搜尋一樣**極度節省記憶體**,但它的致命傷在於「需要預先排序」,且「動態寫入維護成本極高」,最適用於**靜態不常變動、但需要高頻率查詢**的唯讀大型資料集。
* **雜湊搜尋(Set Search)** 擁有最完美的「查詢效率」與「動態新增」雙重極致,但這一切都是**拿「記憶體空間 $O(n)$」與「初次建雜湊表的高預處理成本 $O(n)$」換來的**。適用於**記憶體充足、且需要極致查詢效能、同時可能伴隨高頻寫入**的動態資料。

---

## Stage 5|安全自掃報告 (OpenSSF Secure Coding)

我們對照了 [OpenSSF Secure Coding Guide for Python](https://best.openssf.org/Secure-Coding-Guide-for-Python/) 規範,對本專案進行安全自掃,並特別針對以下 3 條適用標準編寫了防禦性測試且成功修補:

### 🔒 安全自掃與修補對照表

| OpenSSF 章節 | 適用項目 (CWE) | 檢查結果與潛在風險 | 處理與修補方式 |
|---|---|---|---|
| **03 Numbers** | 邊界與型別防禦 (CWE-1284) | `make_data` 的長度參數 `n` 若傳入負數或浮點數,可能導致產生非預期大小的空 list 或當機。 | 補上防禦性程式碼,若 `n < 0` 或非整數則拋出 `ValueError`。已通過 `test_make_data_rejects_negative_n`。 |
| **08 Coding Standards** | 拋出具體異常而非 assert (CWE-697) | 裝飾器 `timeit` 的參數 `repeat` 若小於 1 應拋出異常。原可能使用 `assert`,但在 Python 生產優化編譯時會被忽略。 | 捨棄 `assert` 寫法,精準使用 `raise ValueError("...")`。已通過 `test_timing_repeat_uses_raise_not_assert`。 |
| **04 Neutralization** | 反序列化安全性 (CWE-502) | 評估數據 `results.json` 載入若使用 `pickle` 模組,會造成重大任意程式執行漏洞。 | 嚴格使用 `json` 模組進行序列化與反序列化,禁止並在測試中偵測排除 `pickle`。已通過 `test_results_file_load_uses_json_not_pickle` |

### 🚫 判定不適用項目與理由

* **03 Numbers (使用密碼學安全亂數)**:
* **OpenSSF 規範**:若用於金鑰、Token、密碼等安全敏感領域,應使用 `secrets` 模組而非 `random`。
* **不適用理由**:本專案中的 `random` 是為了產生 benchmark 量測與效能模擬資料。為了確保實驗的「可重現性(Reproducibility)」,必須使用固定的亂數種子 `random.seed(seed)`。此處不涉及安全性敏感的隨機數(如金鑰或會話 ID),因此使用普通的 `random` 模組是正確的設計,不應使用 `secrets`。
135 changes: 135 additions & 0 deletions weeks/week-17/solutions/1112405016/0618/TEST_LOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# TEST_LOG.md (1112405016 - 林囿倫)

本記錄包含搜尋效能專題中,各個階段(Stage 1 至 Stage 5)的 TDD 紅綠燈 unittest 輸出記錄。

---

## 🔴 Stage 1|`timeit` 裝飾器測試 — 紅燈 (Red Light)
* **命令**:`python3 -m unittest test_timing.py`
* **輸出**:
```text
E
======================================================================
ERROR: test_timing (unittest.loader._FailedTest.test_timing)
----------------------------------------------------------------------
ImportError: Failed to import test module: test_timing
Traceback (most recent call last):
File "/usr/lib/python3.14/unittest/loader.py", line 137, in loadTestsFromName
module = __import__(module_name)
File "/home/linyoulun/2026-python/weeks/week-17/solutions/1112405016/0618/test_timing.py", line 19, in <module>
from timing import timeit
ModuleNotFoundError: No module named 'timing'

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)
```

## 🟢 Stage 1|`timeit` 裝飾器測試 — 綠燈 (Green Light)
* **命令**:`python3 -m unittest test_timing.py`
* **輸出**:
```text
......
----------------------------------------------------------------------
Ran 6 tests in 0.056s

OK
```

---

## 🔴 Stage 2|三種搜尋與量測 — 紅燈 (Red Light)
* **命令**:`python3 -m unittest test_search.py`
* **輸出**:
```text
E
======================================================================
ERROR: test_search (unittest.loader._FailedTest.test_search)
----------------------------------------------------------------------
ImportError: Failed to import test module: test_search
Traceback (most recent call last):
File "/usr/lib/python3.14/unittest/loader.py", line 137, in loadTestsFromName
module = __import__(module_name)
File "/home/linyoulun/2026-python/weeks/week-17/solutions/1112405016/0618/test_search.py", line 14, in <module>
from search import linear_search, binary_search, set_search
ModuleNotFoundError: No module named 'search'

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)
```

## 🟢 Stage 2|三種搜尋與量測 — 綠燈 (Green Light)
* **命令**:`python3 -m unittest test_search.py`
* **輸出**:
```text
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK
```

---

## 🟢 Stage 4|雷達圖繪圖輸出測試 — 綠燈 (Green Light)
* **命令**:`~/ppt_env/bin/python -m unittest test_plot.py`
* **輸出**:
```text
[*] 雷達圖 assets/radar.png 繪製成功!
.
----------------------------------------------------------------------
Ran 1 test in 0.694s

OK
```

---

## 🔴 Stage 5|安全性自掃測試 — 紅燈 (Red Light)
* **命令**:`python3 -m unittest test_security.py`
* **輸出**:
```text
F..
======================================================================
FAIL: test_make_data_rejects_negative_n (test_security.TestSecurityStandards.test_make_data_rejects_negative_n)
測試 1 (03 Numbers): make_data 的 n 邊界防禦。
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/linyoulun/2026-python/weeks/week-17/solutions/1112405016/0618/test_security.py", line 14, in test_make_data_rejects_negative_n
with self.assertRaises(ValueError):
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
AssertionError: ValueError not raised

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)
```

## 🟢 Stage 5|安全性自掃測試 — 綠燈 (Green Light)
* **命令**:`python3 -m unittest test_security.py`
* **輸出**:
```text
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK
```

---

## 🌟 最終完整測試套件聯合執行 — 全綠燈 (All PASS)
* **命令**:`python3 -m unittest test_timing.py test_search.py test_plot.py test_security.py`
* **輸出**:
```text
...............
----------------------------------------------------------------------
Ran 15 tests in 0.689s

OK
```
**本週所有 5 個階段、15 項自動化測試套件已完美進入 100% 綠燈安全狀態!**
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading