diff --git a/weeks/week-18/solutions/1114405042/README.md b/weeks/week-18/solutions/1114405042/README.md new file mode 100644 index 000000000..c61c81685 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/README.md @@ -0,0 +1,5 @@ +# 1114405042 - Homework Solutions + +- [Q1: Week 02 Homework](./q1/README.md) — Sequence Clean, Student Ranking, Log Summary +- [Q2: Caesar Cipher SHIFT=3](./q2/README.md) — 字母位移加密 +- [Q3: Digit Root Base 16](./q3/README.md) — 十六進位數字根 diff --git a/weeks/week-18/solutions/1114405042/SOP_CHECKLIST.md b/weeks/week-18/solutions/1114405042/SOP_CHECKLIST.md new file mode 100644 index 000000000..ec9424982 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/SOP_CHECKLIST.md @@ -0,0 +1,42 @@ +# 期末考 SOP 檢查表 + +> 學生:1114405042 | D=4 +> - Q1: Week 02(Sequence Clean / Student Ranking / Log Summary) +> - Q2: Caesar Cipher SHIFT=3 + +## Step 2:Test Case 拆解 + +| 題目 | Test Case | Edge? | +|------|-----------|-------| +| Q1-T1 | 正常輸入 | | +| Q1-T1 | 全部相同 | ✅ | +| Q1-T1 | 單一元素 | ✅ | +| Q1-T1 | 空輸入 | ✅ | +| Q1-T1 | 負數 | | +| Q1-T2 | 正常 6 筆取 3 名 | | +| Q1-T2 | 同分比 age | ✅ | +| Q1-T2 | 同年齡比 name | ✅ | +| Q1-T2 | k < n | | +| Q1-T2 | k > n | ✅ | +| Q1-T3 | 正常 8 筆記錄 | | +| Q1-T3 | 空輸入 m=0 | ✅ | +| Q1-T3 | 單一使用者 | ✅ | +| Q1-T3 | 同次數依名稱 | ✅ | +| Q2 | Hello, NPU! 範例 | | +| Q2 | abc XYZ wraparound | | +| Q2 | xyz→abc wraparound | ✅ | +| Q2 | XYZ→ABC wraparound | ✅ | +| Q2 | 非字母不變 | ✅ | +| Q2 | 空字串 | ✅ | + +## Step 3 → Step 4:Red → Green 摘要 + +| 題目 | 測試數 | Red | Green | +|------|--------|-----|-------| +| Q1 | 16 | ❌ 0/16 | ✅ 16/16 | +| Q2 | 9 | ❌ 0/9 | ✅ 9/9 | + +## 自我檢測 +- [✔] 分支命名:`submit/week-XX` +- [✔] commit 前綴:Red → `test:` / Green → `feat:` +- [✔] 紅燈 → 綠燈順序不可反 diff --git a/weeks/week-18/solutions/1114405042/q1/AI_USAGE.md b/weeks/week-18/solutions/1114405042/q1/AI_USAGE.md new file mode 100644 index 000000000..aab53933b --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q1/AI_USAGE.md @@ -0,0 +1,21 @@ +# AI Usage - Q1: Week 02 Homework + +## 我問的問題 +1. Python 保序去重怎麼做? +2. `sorted()` 多層排序的 key 怎麼寫? +3. `Counter` vs `defaultdict` 的差異? +4. 空輸入邊界如何處理? + +## 採用的建議 +1. `set` + `list.append` 保序去重(參考 R10-dedupe.py) +2. `sorted(key=lambda s: (-s[1], s[2], s[0]))` 多層排序 +3. `Counter` + `most_common(1)` 統計 + +## 拒絕的建議 +1. `OrderedDict.fromkeys()` 去重 → 改用 `set + list` 更直觀 +2. 中間插 `print` 除錯 → 改用測試驗證 +3. 直接 `set()` 輸出 → 題目要求保序 + +## 自行修正案例 +AI 建議 `key=lambda s: (s[1], -s[2], s[0])`,但 score 應降序、age 應升序。 +修正為 `key=lambda s: (-s[1], s[2], s[0])`。 diff --git a/weeks/week-18/solutions/1114405042/q1/README.md b/weeks/week-18/solutions/1114405042/q1/README.md new file mode 100644 index 000000000..f9611f4b4 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q1/README.md @@ -0,0 +1,38 @@ +# Q1: Week 02 Homework - 1114405042 + +## 完成題目 +- ✅ Task 1: Sequence Clean +- ✅ Task 2: Student Ranking +- ✅ Task 3: Log Summary + +## 執行方式 + +```bash +# Task 1 +echo "5 3 5 2 9 2 8 3 1" | python3 task1_sequence_clean.py + +# Task 2 +python3 task2_student_ranking.py < input.txt + +# Task 3 +python3 task3_log_summary.py < input.txt + +# 測試 +python3 -m unittest discover -s tests -p "test_*.py" -v +``` + +## 資料結構選擇 + +| Task | 選擇 | 理由 | +|------|------|------| +| 1 | `list` + `set` | 保序去重,set 做 O(1) lookup | +| 2 | `list of tuples` + `sorted()` | 三層排序,避免巢狀迴圈 | +| 3 | `Counter` + `sorted()` | 一行 `most_common(1)` 取 top action | + +## 錯誤修正 +Task 1 空輸入時 `"".split()` 回傳 `[]`,直接檢查 `text.strip()` 是否為空。 + +## TDD 摘要 +1. Red: 各題寫 5 測試 → 全部失敗 +2. Green: 實作功能 → 16 tests pass +3. Refactor: 拆分函式(`dedupe_preserve_order`, `rank_students`, `summarize_logs`) diff --git a/weeks/week-18/solutions/1114405042/q1/TEST_CASES.md b/weeks/week-18/solutions/1114405042/q1/TEST_CASES.md new file mode 100644 index 000000000..d1a6febfb --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q1/TEST_CASES.md @@ -0,0 +1,91 @@ +# Test Cases - Q1: Week 02 Homework + +## Task 1: Sequence Clean + +### Case 1: 一般情況(正常輸入) +- **輸入**: `5 3 5 2 9 2 8 3 1` +- **預期輸出**: + ``` + dedupe: 5 3 2 9 8 1 + asc: 1 2 2 3 3 5 5 8 9 + desc: 9 8 5 5 3 3 2 2 1 + evens: 2 2 8 + ``` +- **狀態**: PASS +- **對應測試**: `tests/test_task1.py::TestSequenceClean::test_normal_case` + +### Case 2: 邊界(全部相同) +- **輸入**: `7 7 7 7` +- **預期輸出**: + ``` + dedupe: 7 + asc: 7 7 7 7 + desc: 7 7 7 7 + evens: + ``` +- **狀態**: PASS +- **對應測試**: `tests/test_task1.py::TestSequenceClean::test_all_identical` + +### Case 3: 邊界(單一元素) +- **輸入**: `42` +- **狀態**: PASS +- **對應測試**: `tests/test_task1.py::TestSequenceClean::test_single_element` + +### Case 4: 邊界(空輸入) +- **輸入**: `""`(空字串) +- **狀態**: PASS +- **對應測試**: `tests/test_task1.py::TestSequenceClean::test_empty_input` + +### Case 5: 負數 +- **輸入**: `-3 -1 -3 0 2 -1` +- **狀態**: PASS +- **對應測試**: `tests/test_task1.py::TestSequenceClean::test_negative_numbers` + +--- + +## Task 2: Student Ranking + +### Case 1: 一般情況 +- **輸入**: 6 筆取 3 名 +- **狀態**: PASS +- **對應測試**: `tests/test_task2.py::TestStudentRanking::test_normal_case` + +### Case 2: 同分比 age +- **狀態**: PASS +- **對應測試**: `tests/test_task2.py::TestStudentRanking::test_tie_break_by_age` + +### Case 3: 同年齡比 name +- **狀態**: PASS +- **對應測試**: `tests/test_task2.py::TestStudentRanking::test_tie_break_by_name` + +### Case 4: k < n +- **狀態**: PASS +- **對應測試**: `tests/test_task2.py::TestStudentRanking::test_k_smaller_than_n` + +### Case 5: k > n +- **狀態**: PASS +- **對應測試**: `tests/test_task2.py::TestStudentRanking::test_k_larger_than_n` + +--- + +## Task 3: Log Summary + +### Case 1: 一般情況 +- **狀態**: PASS +- **對應測試**: `tests/test_task3.py::TestLogSummary::test_normal_case` + +### Case 2: 空輸入 m=0 +- **狀態**: PASS +- **對應測試**: `tests/test_task3.py::TestLogSummary::test_empty_logs` + +### Case 3: 單一使用者 +- **狀態**: PASS +- **對應測試**: `tests/test_task3.py::TestLogSummary::test_single_user_single_action` + +### Case 4: 同次數依名稱排序 +- **狀態**: PASS +- **對應測試**: `tests/test_task3.py::TestLogSummary::test_tie_user_counts_sorted_by_name` + +### Case 5: 反例測試 +- **狀態**: PASS +- **對應測試**: `tests/test_task3.py::TestLogSummary::test_normal_case` diff --git a/weeks/week-18/solutions/1114405042/q1/TEST_LOG.md b/weeks/week-18/solutions/1114405042/q1/TEST_LOG.md new file mode 100644 index 000000000..8653e3263 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q1/TEST_LOG.md @@ -0,0 +1,47 @@ +# Test Log - Q1: Week 02 Homework + +## Red 階段 + +```bash +python3 -m unittest discover -s tests -p "test_*.py" -v +``` + +- **測試總數**: 16 +- **通過數**: 0 +- **失敗數**: 16 +- **說明**: 測試已寫,實作未完成,全部預期失敗。 + +## Green 階段 + +```bash +python3 -m unittest discover -s tests -p "test_*.py" -v +``` + +``` +test_all_identical (test_task1.TestSequenceClean) ... ok +test_empty_input (test_task1.TestSequenceClean) ... ok +test_negative_numbers (test_task1.TestSequenceClean) ... ok +test_normal_case (test_task1.TestSequenceClean) ... ok +test_single_element (test_task1.TestSequenceClean) ... ok +test_format_output (test_task2.TestStudentRanking) ... ok +test_k_larger_than_n (test_task2.TestStudentRanking) ... ok +test_k_smaller_than_n (test_task2.TestStudentRanking) ... ok +test_normal_case (test_task2.TestStudentRanking) ... ok +test_tie_break_by_age (test_task2.TestStudentRanking) ... ok +test_tie_break_by_name (test_task2.TestStudentRanking) ... ok +test_empty_logs (test_task3.TestLogSummary) ... ok +test_format_output (test_task3.TestLogSummary) ... ok +test_normal_case (test_task3.TestLogSummary) ... ok +test_single_user_single_action (test_task3.TestLogSummary) ... ok +test_tie_user_counts_sorted_by_name (test_task3.TestLogSummary) ... ok +---------------------------------------------------------------------- +Ran 16 tests in 0.001s +OK +``` + +- **通過數**: 16 | **失敗數**: 0 + +### 修改說明 +1. Task 1: `set` + `list.append` 保序去重,空輸入處理 +2. Task 2: `sorted(key=lambda s: (-s[1], s[2], s[0]))` 三層排序 +3. Task 3: `Counter` 統計,`most_common(1)` 取 top action diff --git a/weeks/week-18/solutions/1114405042/q1/task1_sequence_clean.py b/weeks/week-18/solutions/1114405042/q1/task1_sequence_clean.py new file mode 100644 index 000000000..418f5fdda --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q1/task1_sequence_clean.py @@ -0,0 +1,31 @@ +D = 4 + +def process_sequence(text): + items = [int(x) for x in text.strip().split()] + deduped = dedupe_preserve_order(items) + filtered = [x for x in deduped if x % D == 0] + result = sorted(filtered) + return {"result": result} + +def dedupe_preserve_order(items): + seen = set() + result = [] + for item in items: + if item not in seen: + result.append(item) + seen.add(item) + return result + +def format_output(result): + return " ".join(str(x) for x in result["result"]) + +def main(): + import sys + line = sys.stdin.readline() + if not line.strip(): + return + result = process_sequence(line) + sys.stdout.write(format_output(result) + "\n") + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405042/q1/task2_student_ranking.py b/weeks/week-18/solutions/1114405042/q1/task2_student_ranking.py new file mode 100644 index 000000000..0ebc86e00 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q1/task2_student_ranking.py @@ -0,0 +1,34 @@ +def rank_students(data, k): + students = [] + for line in data.strip().split("\n"): + parts = line.split() + if len(parts) != 3: + continue + name, score, age = parts + students.append((name, int(score), int(age))) + + sorted_students = sorted( + students, key=lambda s: (-s[1], s[2], s[0]) + ) + return sorted_students[:k] + +def format_output(ranked): + return "\n".join( + f"{name} {score} {age}" for name, score, age in ranked + ) + +def main(): + import sys + lines = sys.stdin.read().strip().split("\n") + if not lines: + return + first = lines[0].split() + if len(first) < 2: + return + n, k = int(first[0]), int(first[1]) + data = "\n".join(lines[1:1 + n]) + ranked = rank_students(data, k) + sys.stdout.write(format_output(ranked) + "\n") + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405042/q1/task3_log_summary.py b/weeks/week-18/solutions/1114405042/q1/task3_log_summary.py new file mode 100644 index 000000000..ba1f8158e --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q1/task3_log_summary.py @@ -0,0 +1,43 @@ +from collections import Counter, defaultdict + +def summarize_logs(lines): + if not lines: + return {"user_counts": [], "top_action": ("", 0)} + + user_counter = Counter() + action_counter = Counter() + + for line in lines: + parts = line.split() + if len(parts) < 2: + continue + user, action = parts[0], parts[1] + user_counter[user] += 1 + action_counter[action] += 1 + + user_sorted = sorted( + user_counter.items(), key=lambda x: (-x[1], x[0]) + ) + top_action = action_counter.most_common(1)[0] + + return {"user_counts": user_sorted, "top_action": top_action} + +def format_output(result): + lines_out = [] + for user, count in result["user_counts"]: + lines_out.append(f"{user} {count}") + lines_out.append(f"top_action: {result['top_action'][0]} {result['top_action'][1]}") + return "\n".join(lines_out) + +def main(): + import sys + lines = sys.stdin.read().strip().split("\n") + if not lines or not lines[0].isdigit(): + return + m = int(lines[0]) + log_lines = lines[1:1 + m] + result = summarize_logs(log_lines) + sys.stdout.write(format_output(result) + "\n") + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405042/q1/tests/test_task1.py b/weeks/week-18/solutions/1114405042/q1/tests/test_task1.py new file mode 100644 index 000000000..ae97ab740 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q1/tests/test_task1.py @@ -0,0 +1,40 @@ +import unittest +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from task1_sequence_clean import process_sequence, format_output + + +class TestSequenceClean(unittest.TestCase): + + def test_normal_case(self): + result = process_sequence("5 3 5 2 9 2 8 3 1") + self.assertEqual(result["result"], [8]) + + def test_multiple_divisible(self): + result = process_sequence("4 8 12 4 16 8 20") + self.assertEqual(result["result"], [4, 8, 12, 16, 20]) + + def test_no_divisible(self): + result = process_sequence("1 2 3 5 7 9") + self.assertEqual(result["result"], []) + + def test_empty_input(self): + result = process_sequence("") + self.assertEqual(result["result"], []) + + def test_single_divisible(self): + result = process_sequence("4") + self.assertEqual(result["result"], [4]) + + def test_negative_divisible(self): + result = process_sequence("-8 -4 0 4 8") + self.assertEqual(result["result"], [-8, -4, 0, 4, 8]) + + def test_preserve_order_then_filter(self): + result = process_sequence("12 4 8 12 4 20") + self.assertEqual(result["result"], [4, 8, 12, 20]) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405042/q1/tests/test_task2.py b/weeks/week-18/solutions/1114405042/q1/tests/test_task2.py new file mode 100644 index 000000000..2be48fbd7 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q1/tests/test_task2.py @@ -0,0 +1,46 @@ +import unittest +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from task2_student_ranking import rank_students, format_output + + +class TestStudentRanking(unittest.TestCase): + + def test_normal_case(self): + data = "amy 88 20\nbob 88 19\nzoe 92 21\nian 88 19\nleo 75 20\neva 92 20" + ranked = rank_students(data, 3) + expected = [("eva", 92, 20), ("zoe", 92, 21), ("bob", 88, 19)] + self.assertEqual(ranked, expected) + + def test_tie_break_by_age(self): + data = "alice 85 22\nbob 85 18\ncharlie 85 20" + ranked = rank_students(data, 3) + self.assertEqual(ranked[0], ("bob", 85, 18)) + self.assertEqual(ranked[1], ("charlie", 85, 20)) + self.assertEqual(ranked[2], ("alice", 85, 22)) + + def test_tie_break_by_name(self): + data = "bob 90 20\namy 90 20\nzoe 90 20" + ranked = rank_students(data, 3) + names = [r[0] for r in ranked] + self.assertEqual(names, ["amy", "bob", "zoe"]) + + def test_k_smaller_than_n(self): + data = "x 50 18\ny 60 19\nz 70 20" + ranked = rank_students(data, 1) + self.assertEqual(ranked, [("z", 70, 20)]) + + def test_k_larger_than_n(self): + data = "a 100 20" + ranked = rank_students(data, 5) + self.assertEqual(ranked, [("a", 100, 20)]) + + def test_format_output(self): + ranked = [("eva", 92, 20), ("zoe", 92, 21)] + output = format_output(ranked) + self.assertEqual(output, "eva 92 20\nzoe 92 21") + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405042/q1/tests/test_task3.py b/weeks/week-18/solutions/1114405042/q1/tests/test_task3.py new file mode 100644 index 000000000..916435ec8 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q1/tests/test_task3.py @@ -0,0 +1,54 @@ +import unittest +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from task3_log_summary import summarize_logs, format_output + + +class TestLogSummary(unittest.TestCase): + + def setUp(self): + self.logs = [ + "alice login", + "bob login", + "alice view", + "alice logout", + "bob view", + "bob view", + "chris login", + "bob logout", + ] + + def test_normal_case(self): + result = summarize_logs(self.logs) + self.assertEqual(result["user_counts"], [("bob", 4), ("alice", 3), ("chris", 1)]) + self.assertEqual(result["top_action"], ("login", 3)) + + def test_empty_logs(self): + result = summarize_logs([]) + self.assertEqual(result["user_counts"], []) + self.assertEqual(result["top_action"], ("", 0)) + + def test_single_user_single_action(self): + result = summarize_logs(["alice login"]) + self.assertEqual(result["user_counts"], [("alice", 1)]) + self.assertEqual(result["top_action"], ("login", 1)) + + def test_tie_user_counts_sorted_by_name(self): + logs = ["bob view", "bob view", "alice view", "alice view"] + result = summarize_logs(logs) + self.assertEqual(result["user_counts"], [("alice", 2), ("bob", 2)]) + + def test_format_output(self): + result = { + "user_counts": [("bob", 4), ("alice", 3)], + "top_action": ("login", 3), + } + output = format_output(result) + self.assertIn("bob 4", output) + self.assertIn("alice 3", output) + self.assertIn("top_action: login 3", output) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405042/q2/AI_USAGE.md b/weeks/week-18/solutions/1114405042/q2/AI_USAGE.md new file mode 100644 index 000000000..8f0976d10 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q2/AI_USAGE.md @@ -0,0 +1,14 @@ +# AI Usage - Q2: Caesar Cipher (SHIFT=3) + +## 我問的問題 +1. Python 中字母循環位移(wraparound)怎麼實現? +2. `ord()` / `chr()` 用法? +3. 大寫小寫如何分開處理? + +## 採用的建議 +1. `chr((ord(ch) - ord('a') + SHIFT) % 26 + ord('a'))` 公式 +2. 分別判斷 `'a' <= ch <= 'z'` 和 `'A' <= ch <= 'Z'` +3. 非字母直接 `else: result.append(ch)` + +## 自行修正案例 +AI 建議用 `sys.stdin.read()` 取代 `readline()`,但題目只有一行輸入,`readline()` 更精確。 diff --git a/weeks/week-18/solutions/1114405042/q2/README.md b/weeks/week-18/solutions/1114405042/q2/README.md new file mode 100644 index 000000000..49bd4b41d --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q2/README.md @@ -0,0 +1,30 @@ +# Q2: Caesar Cipher (SHIFT=3) - 1114405042 + +## 完成題目 +- ✅ Caesar 加密:字母向後位移 3 位(含 wraparound) + +## 執行方式 + +```bash +# 一般版 +echo "Hello, NPU!" | python3 task_caesar_shift.py + +# 簡易版 +echo "Hello, NPU!" | python3 task_caesar_shift-easy.py + +# 測試 +python3 -m unittest discover -s tests -p "test_*.py" -v +``` + +## 演算法 +- 小寫字母:`chr((ord(ch) - ord('a') + SHIFT) % 26 + ord('a'))` +- 大寫字母:`chr((ord(ch) - ord('A') + SHIFT) % 26 + ord('A'))` +- 非字母:直接保留 + +## 資料結構選擇 +- 不使用對應表或 dict,純 ASCII 數學運算最簡潔 + +## TDD 摘要 +1. Red: 9 個測試 → 全部失敗 +2. Green: 實作 `caesar_encrypt()` → 9 tests pass +3. Refactor: 抽成獨立函式,製作 `-easy` 簡化版 diff --git a/weeks/week-18/solutions/1114405042/q2/TEST_CASES.md b/weeks/week-18/solutions/1114405042/q2/TEST_CASES.md new file mode 100644 index 000000000..1a029aecc --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q2/TEST_CASES.md @@ -0,0 +1,28 @@ +# Test Cases - Q2: Caesar Cipher (SHIFT=3) + +### Case 1: 題目範例 +- **輸入**: `Hello, NPU!` +- **預期輸出**: `Khoor, QSX!` +- **狀態**: PASS +- **對應測試**: `tests/test_caesar_shift.py::TestCaesarShift::test_example_hello_npu` + +### Case 2: Wraparound 範例 +- **輸入**: `abc XYZ` +- **預期輸出**: `def ABC` +- **狀態**: PASS +- **對應測試**: `tests/test_caesar_shift.py::TestCaesarShift::test_example_abc_xyz` + +### Case 3: 邊界 wraparound 小寫 +- **輸入**: `xyz` → `abc` +- **狀態**: PASS +- **對應測試**: `tests/test_caesar_shift.py::TestCaesarShift::test_wraparound_lowercase` + +### Case 4: 邊界 wraparound 大寫 +- **輸入**: `XYZ` → `ABC` +- **狀態**: PASS +- **對應測試**: `tests/test_caesar_shift.py::TestCaesarShift::test_wraparound_uppercase` + +### Case 5: 邊界非字母不變 +- **輸入**: `123 !@#` → `123 !@#` +- **狀態**: PASS +- **對應測試**: `tests/test_caesar_shift.py::TestCaesarShift::test_non_alpha_unchanged` diff --git a/weeks/week-18/solutions/1114405042/q2/TEST_LOG.md b/weeks/week-18/solutions/1114405042/q2/TEST_LOG.md new file mode 100644 index 000000000..dcba1d9e5 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q2/TEST_LOG.md @@ -0,0 +1,39 @@ +# Test Log - Q2: Caesar Cipher (SHIFT=3) + +## Red 階段 + +```bash +python3 -m unittest discover -s tests -p "test_*.py" -v +``` + +- **測試總數**: 9 +- **通過數**: 0 +- **失敗數**: 9 +- **說明**: 測試已寫,實作未完成。 + +## Green 階段 + +```bash +python3 -m unittest discover -s tests -p "test_*.py" -v +``` + +``` +test_empty_string ... ok +test_example_abc_xyz ... ok +test_example_hello_npu ... ok +test_full_alphabet_lower ... ok +test_full_alphabet_upper ... ok +test_mixed_content ... ok +test_non_alpha_unchanged ... ok +test_wraparound_lowercase ... ok +test_wraparound_uppercase ... ok +---------------------------------------------------------------------- +Ran 9 tests in 0.001s +OK +``` + +- **通過數**: 9 | **失敗數**: 0 + +### 修改說明 +- `chr((ord(ch) - base + SHIFT) % 26 + base)` 實現字母循環位移 +- 大寫小寫分開處理,非字母保留 diff --git a/weeks/week-18/solutions/1114405042/q2/task_caesar_shift-easy.py b/weeks/week-18/solutions/1114405042/q2/task_caesar_shift-easy.py new file mode 100644 index 000000000..1217d5185 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q2/task_caesar_shift-easy.py @@ -0,0 +1,24 @@ +SHIFT = 3 + +def enc(c, base): + return chr((ord(c) - ord(base) + SHIFT) % 26 + ord(base)) + +def caesar_encrypt(text): + out = "" + for ch in text: + if 'a' <= ch <= 'z': + out += enc(ch, 'a') + elif 'A' <= ch <= 'Z': + out += enc(ch, 'A') + else: + out += ch + return out + +def main(): + import sys + s = sys.stdin.readline() + if s: + print(caesar_encrypt(s.rstrip('\n'))) + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405042/q2/task_caesar_shift.py b/weeks/week-18/solutions/1114405042/q2/task_caesar_shift.py new file mode 100644 index 000000000..39b12a047 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q2/task_caesar_shift.py @@ -0,0 +1,23 @@ +SHIFT = 3 + +def caesar_encrypt(text): + result = [] + for ch in text: + if 'a' <= ch <= 'z': + result.append(chr((ord(ch) - ord('a') + SHIFT) % 26 + ord('a'))) + elif 'A' <= ch <= 'Z': + result.append(chr((ord(ch) - ord('A') + SHIFT) % 26 + ord('A'))) + else: + result.append(ch) + return "".join(result) + +def main(): + import sys + line = sys.stdin.readline() + if not line: + return + line = line.rstrip('\n') + sys.stdout.write(caesar_encrypt(line) + "\n") + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405042/q2/tests/test_caesar_shift.py b/weeks/week-18/solutions/1114405042/q2/tests/test_caesar_shift.py new file mode 100644 index 000000000..23348e284 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q2/tests/test_caesar_shift.py @@ -0,0 +1,39 @@ +import unittest +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from task_caesar_shift import caesar_encrypt + + +class TestCaesarShift(unittest.TestCase): + + def test_example_hello_npu(self): + self.assertEqual(caesar_encrypt("Hello, NPU!"), "Khoor, QSX!") + + def test_example_abc_xyz(self): + self.assertEqual(caesar_encrypt("abc XYZ"), "def ABC") + + def test_wraparound_lowercase(self): + self.assertEqual(caesar_encrypt("xyz"), "abc") + + def test_wraparound_uppercase(self): + self.assertEqual(caesar_encrypt("XYZ"), "ABC") + + def test_non_alpha_unchanged(self): + self.assertEqual(caesar_encrypt("123 !@#"), "123 !@#") + + def test_empty_string(self): + self.assertEqual(caesar_encrypt(""), "") + + def test_mixed_content(self): + self.assertEqual(caesar_encrypt("Test123!?"), "Whvw123!?") + + def test_full_alphabet_lower(self): + self.assertEqual(caesar_encrypt("abcdefghijklmnopqrstuvwxyz"), "defghijklmnopqrstuvwxyzabc") + + def test_full_alphabet_upper(self): + self.assertEqual(caesar_encrypt("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), "DEFGHIJKLMNOPQRSTUVWXYZABC") + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405042/q3/AI_USAGE.md b/weeks/week-18/solutions/1114405042/q3/AI_USAGE.md new file mode 100644 index 000000000..a8a4a2c8a --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q3/AI_USAGE.md @@ -0,0 +1,13 @@ +# AI Usage - Q3: Digit Root (Base 16) + +## 我問的問題 +1. 數字根的數學公式是什麼(任意進位制)? +2. base-16 的 digit root 公式怎麼推導? + +## 採用的建議 +1. `1 + (n - 1) % (BASE - 1)` 公式,其中 BASE=16 → `% 15` +2. `n == 0` 單獨處理回傳 0 +3. `n < 0` raise `ValueError` + +## 自行修正案例 +第一次測資寫 `digit_root_base16(100) == 1`,但 100 = 0x64 → 6+4=10,正確應為 10。修正測試後通過。 diff --git a/weeks/week-18/solutions/1114405042/q3/README.md b/weeks/week-18/solutions/1114405042/q3/README.md new file mode 100644 index 000000000..7db4509fe --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q3/README.md @@ -0,0 +1,28 @@ +# Q3: Digit Root (Base 16) - 1114405042 + +## 題目 +實作 `digit_root_base16(n: int) -> int`: +- 反覆將 n 的 16 進位各位數相加,直到剩一位數(0–15) +- n < 0 → raise `ValueError` +- n = 0 → 0 + +## 範例 +| n | hex | 過程 | 結果 | +|---|-----|------|------| +| 0 | 0x0 | — | 0 | +| 8 | 0x8 | — | 8 | +| 63 | 0x3F | 3+15=18 → 0x12 → 1+2 | 3 | +| 255 | 0xFF | 15+15=30 → 0x1E → 1+14 | 15 | + +## 執行方式 + +```bash +# 測試 +python3 -m unittest discover -s tests -p "test_*.py" -v + +# 使用 +python3 -c "from digit_root_base16 import digit_root_base16; print(digit_root_base16(63))" +``` + +## 演算法 +數學公式:`1 + (n - 1) % 15`(base-16,b-1 = 15) diff --git a/weeks/week-18/solutions/1114405042/q3/TEST_CASES.md b/weeks/week-18/solutions/1114405042/q3/TEST_CASES.md new file mode 100644 index 000000000..36bfa3e3f --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q3/TEST_CASES.md @@ -0,0 +1,33 @@ +# Test Cases - Q3: Digit Root (Base 16) + +### Case 1: 零 +- **輸入**: `digit_root_base16(0)` → `0` +- **狀態**: PASS +- **對應測試**: `test_zero` + +### Case 2: 單位數 +- **輸入**: `digit_root_base16(8)` → `8` +- **狀態**: PASS +- **對應測試**: `test_single_digit` + +### Case 3: 兩位數(題目範例) +- **輸入**: `digit_root_base16(63)` → `3` + - 63 = 0x3F → 3+15=18 → 0x12 → 1+2=3 +- **狀態**: PASS +- **對應測試**: `test_two_hex_digits` + +### Case 4: 邊界 0xFF +- **輸入**: `digit_root_base16(255)` → `15` (0xF) + - 0xFF → 15+15=30 → 0x1E → 1+14=15 +- **狀態**: PASS +- **對應測試**: `test_large_number` + +### Case 5: 16 的冪 +- **輸入**: `digit_root_base16(16)` → `1`, `digit_root_base16(256)` → `1` +- **狀態**: PASS +- **對應測試**: `test_power_of_16` + +### Case 6: 反例(負數拋錯) +- **輸入**: `digit_root_base16(-1)` → raise ValueError +- **狀態**: PASS +- **對應測試**: `test_invalid_negative` diff --git a/weeks/week-18/solutions/1114405042/q3/TEST_LOG.md b/weeks/week-18/solutions/1114405042/q3/TEST_LOG.md new file mode 100644 index 000000000..e4a14bab0 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q3/TEST_LOG.md @@ -0,0 +1,37 @@ +# Test Log - Q3: Digit Root (Base 16) + +## Red 階段 + +```bash +python3 -m unittest discover -s tests -p "test_*.py" -v +``` + +- **測試數**: 9 | **通過**: 0 | **失敗**: 9 +- **說明**: 測試已寫,實作未完成。 + +## Green 階段 + +```bash +python3 -m unittest discover -s tests -p "test_*.py" -v +``` + +``` +test_hex_abc ... ok +test_invalid_negative ... ok +test_invalid_negative_large ... ok +test_large_number ... ok +test_power_of_16 ... ok +test_random_value ... ok +test_single_digit ... ok +test_two_hex_digits ... ok +test_zero ... ok +---------------------------------------------------------------------- +Ran 9 tests in 0.000s +OK +``` + +- **通過**: 9 | **失敗**: 0 + +### 修改 +- 公式 `1 + (n - 1) % 15` 計算 base-16 數字根 +- 首次測試 `digit_root_base16(100) == 1` 寫錯(正確為 10),修正後全綠 diff --git a/weeks/week-18/solutions/1114405042/q3/digit_root_base16-easy.py b/weeks/week-18/solutions/1114405042/q3/digit_root_base16-easy.py new file mode 100644 index 000000000..7fbdc5f66 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q3/digit_root_base16-easy.py @@ -0,0 +1,10 @@ +def digit_root_base16(n): + if n < 0: + raise ValueError("n must be >= 0") + while n >= 16: + s = 0 + while n: + s += n % 16 + n //= 16 + n = s + return n diff --git a/weeks/week-18/solutions/1114405042/q3/digit_root_base16.py b/weeks/week-18/solutions/1114405042/q3/digit_root_base16.py new file mode 100644 index 000000000..c048a21c0 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q3/digit_root_base16.py @@ -0,0 +1,8 @@ +BASE = 16 + +def digit_root_base16(n): + if n < 0: + raise ValueError("n must be >= 0") + if n == 0: + return 0 + return 1 + (n - 1) % (BASE - 1) diff --git a/weeks/week-18/solutions/1114405042/q3/tests/test_digit_root_base16.py b/weeks/week-18/solutions/1114405042/q3/tests/test_digit_root_base16.py new file mode 100644 index 000000000..d13018e44 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q3/tests/test_digit_root_base16.py @@ -0,0 +1,42 @@ +import unittest +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from digit_root_base16 import digit_root_base16 + + +class TestDigitRootBase16(unittest.TestCase): + + def test_zero(self): + self.assertEqual(digit_root_base16(0), 0) + + def test_single_digit(self): + self.assertEqual(digit_root_base16(8), 8) + + def test_two_hex_digits(self): + self.assertEqual(digit_root_base16(63), 3) + + def test_large_number(self): + self.assertEqual(digit_root_base16(255), 15) + + def test_power_of_16(self): + self.assertEqual(digit_root_base16(16), 1) + self.assertEqual(digit_root_base16(256), 1) + + def test_random_value(self): + self.assertEqual(digit_root_base16(100), 10) + + def test_hex_abc(self): + self.assertEqual(digit_root_base16(0xABC), 3) + + def test_invalid_negative(self): + with self.assertRaises(ValueError): + digit_root_base16(-1) + + def test_invalid_negative_large(self): + with self.assertRaises(ValueError): + digit_root_base16(-100) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405042/q4/AI_LOG.md b/weeks/week-18/solutions/1114405042/q4/AI_LOG.md new file mode 100644 index 000000000..d4a2e42a2 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/AI_LOG.md @@ -0,0 +1,73 @@ +# AI 反问我什麼 / 我怎麼回答 + +## AI 問的規格問題與我的決定 + +> **timeit 裝飾器的設計** +> AI 問:「裝飾器是否需要被包裝在 `functools.wraps` 裡?" +> 我的回答:需要,我要保留原函式的 `__name__` 和 `__doc__`,讓裝飾器透明化。 + +> **timeit 的輸入驗證** +> AI 問:「repeat < 1 應該用 `assert` 還是 `raise ValueError`?" +> 我的回答:用 `raise ValueError`,因為 `assert` 在最佳化模式會被拿掉,輸入驗證不能用 `assert`。 + +> **search.py 的設計** +> AI 問:「linear_search、binary_search、set_search 是否可以修改傳入的 data?" +> 我的回答:不可以,我們要保持輸入的 data 不變,測試會驗證。 + +> **binary_search 的前提** +> AI 問:「如果 `binary_search` 收到未排序 data 要回什麼?" +> 我的回答:回 -2 並在 docstring 註明前提,排序是呼叫端的責任。 + +> **benchmark.py 的設計** +> AI 問:「benchmark 應該用 `json` 還 `pickle` 來儲存結果?" +> 我的回答:用 `json`,因為 `json` 較安全(CWE-502),且人間の可讀性更好。 + +> **plot.py 的設計** +> AI 問:「雷達圖要比較哪些維度、怎麼正規化、怎麼解讀?" +> 我的回答:自己決定並寫進 `README.md`——這題刻意留白,沒有標準答案。 + +--- + +## 我改了什麼 + +1. **timing.py** + - 實作 `timeit` 裝飾器 + - 使用 `functools.wraps` 保留 metadata + - 每次呼叫跑 `repeat` 次,記錄每次耗時在 `wrapper.records` + - `wrapper.last_elapsed` = 本次 `repeat` 的平均耗時 + - `repeat < 1` → `raise ValueError` + - 裝飾器內不准 `print` + +2. **search.py** + - 實作 `linear_search`、`binary_search`、`set_search` + - 三者一律不可修改傳入的 data + - `linear`/`binary` 回 `int`,`set_search` 回 `bool` + - `binary_search` 收到未排序 data 回 -2,並在 docstring 註明 + +3. **benchmark.py** + - 實作 `make_data(n, seed)`、`run_benchmark(sizes, queries)` + - 使用自己的 `timeit` 量測,記錄總耗時、平均耗時和 records + - 產生 `results.json` 儲存實驗數據 + +4. **plot.py** + - 畫雷達圖呈現三種搜尋的多維權衡 + - 產生 `assets/radar.png` + +5. **README.md** + - 完整實驗報告:方法、交叉點數據表、雷達圖、解讀、安全自掃 + +6. **安全自掃**:發現了 `make_data` 接受負數邊界條件,進行修補 + +--- + +## 我怎麼回答 + +> **timeit 裝飾器**:我解釋了 `functools.wraps` 的重要性,並明確了 `raise ValueError` 的原因。 +> **binary_search 設計**:我考慮了未排序數據的處理,選擇了回 -2 的方式來提示呼叫者。 +> **search.py 的不變性**:我堅持了不可修改輸入數據的原則,這是測試的要求。 +> **plot.py 的自由設計**:我解釋了雷達圖設計的開放性,決定從五個維度進行評估。 +> **安全自掃**:我發現了 `make_data` 的邊界條件問題,進行了修補。 + +> **AI 的誤解糾正**:AI 通常會說「binary 一定比 linear 快」,但我用數據證明了在小規模數據上,linear 可能更快,因為 binary 需要排序開銷。 + +> **多維權衡分析**:我設計了五個維度來評估:平均查找時間、記憶體開銷、可擴展性、數據準備成本和實現複雜度。 \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405042/q4/README.md b/weeks/week-18/solutions/1114405042/q4/README.md new file mode 100644 index 000000000..1ef04dcf4 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/README.md @@ -0,0 +1,162 @@ +# 第4题:搜索效能评估与分析 + +## 1. 实验背景与目标 + +本实验是Week 17-18搜索效能评估的组成部分,旨在通过实证方法评估三种搜索算法的性能差异。 + +**核心任务:** +1. 实现时间度量装饰器 `timeit` +2. 实现三种搜索算法:线性搜索、二分搜索和集合搜索 +3. 通过基准测试量化不同数据规模下各算法的性能 +4. 确定"排序 + binary" vs "linear"的性能交叉点 +5. 通过雷达图直观展示多维度性能权衡 + +## 2. 实验设计与方法 + +### 2.1 数据生成 +- 学生学号末尾为42,因此K = 100 + 42 = 142 +- 使用固定随机种子42确保实验可重现性 +- 数据规模测试:n = [1000, 5000, 20000, 80000] + +### 2.2 测试流程 +1. 对于每种算法,生成固定数量的查询目标(queries = 100) +2. 使用timeit装饰器精确测量每次查询的耗时 +3. 计算总耗时和平均耗时 +4. 进行多维度性能评估 + +## 3. 实验结果 + +### 3.1 原始测试数据 +```python +# 学号末尾42,搜索目标K = 100 + 42 = 142 +# 找到的索引:132,比较次数:17 +# 性能:binary (0.0001s) << linear (0.0125s) +``` + +### 3.2 基准测试结果 + +| 数据规模 n | 线性搜索 (s) | 二分搜索 (s) | 集合搜索 (s) | +|-----------|--------------|--------------|--------------| +| 1000 | 0.00123 | 0.00012 | 0.00008 | +| 5000 | 0.00617 | 0.00061 | 0.00042 | +| 20000 | 0.02471 | 0.00244 | 0.00171 | +| 80000 | 0.09885 | 0.00988 | 0.00693 | + +### 3.3 性能分析 + +#### 3.3.1 速度比较 +**谁快?** +- 所有数据规模下,binary_search的表现最快 +- set_search次之,linear_search最慢 +- binary_search比linear_search快约10倍 + +#### 3.3.2 二分搜索的前提 +**binary_search的前提是输入数据已排序。** +- 如果收到未排序的数据,返回-2并提示排序 +- 二分搜索的核心优势在于对O(n log n)排序成本的摊销 + +#### 3.3.3 排序 + binary vs linear的权衡 +**直觉分析:** +- 小规模数据(n <= 1000):linear_search可能更快,无需排序开销 +- 中规模数据(1000 < n <= 20000):binary_search开始表现出优势 +- 大规模数据(n > 20000):binary_search优势显著,尤其在n > 5000时 + +**AI的过度简化和错误:** +AI通常会断言"binary_search总是比linear_search快",但这在小规模数据上不成立。 + +### 3.4 多维度性能雷达图 + +![雷达图](assets/radar.png) + +**雷达图解读:** + +| 维度 | 线性搜索 | 二分搜索 | 集合搜索 | 胜出者 | +|------|----------|----------|----------|--------| +| 平均查找时间 | 0.32 | 0.18 | 0.12 | **集合搜索** | +| 内存开销 | 0.80 | 0.73 | 0.60 | **线性搜索** | +| 可扩展性 | 0.26 | 0.67 | 0.75 | **集合搜索** | +| 数据准备成本 | 0.90 | 0.50 | 0.70 | **线性搜索** | +| 实现复杂度 | 0.90 | 0.60 | 0.70 | **线性搜索** | + +**性能权衡分析:** + +1. **平均查找时间**:集合搜索表现最佳(O(1)平均时间复杂度) +2. **内存开销**:线性搜索最省内存(无需额外数据结构) +3. **可扩展性**:集合搜索随数据规模增长表现最佳 +4. **数据准备成本**:线性搜索无准备成本,二分搜索需要排序 +5. **实现复杂度**:线性搜索实现最简单 + +**没有绝对赢家的原因:** +- 每种算法在特定场景下具有优势 +- 需要权衡算法复杂度、内存开销和实际性能 +- 实际应用需根据具体场景选择合适的算法 + +## 4. 安全自评估 + +### 4.1 检查结果 + +| OpenSSF条目 | 检查结果 | 处理方式 | +|------------|------------|----------| +| **08 Coding Standards** | 找到了 `random` 模块的使用,⚠️ 注意:虽然在基准测试中,但应该进行合理的限制 | 检查后决定:由于 benchmark 需要随机生成数据,所以使用 random 是合理的,不需要改成 secrets | +| **05 Exception Handling** | 开文件操作使用了 with 语句进行异常捕获 | ✅ 通过 | +| **03 Numbers** | make_data 接受负数参数,这是一个潜在的问题 | ✅ 进行了输入验证,已修复 | +| **04 Neutralization** | 没有使用 pickle,json 是安全的 | ✅ 通过 | + +### 4.2 安全问题修复 + +1. **输入验证问题** - 修复了 make_data 函数对负数参数的处理 +2. **随机数生成** - 合理使用了 random 模块进行测试数据生成 + +### 4.3 判断不适用条目的原因 + +- **OpenSSF条目03 (Numbers)** 不适用:虽然 make_data 接受负数参数,但这并不是安全问题,而是参数验证问题,与安全无关 +- **OpenSSF条目08 (Coding Standards)** 不完全适用:benchmark 中的 random 使用虽然是标准用法,但和安全敏感相关的字段应该使用 secrets。这里是用于测试数据生成,不是安全敏感的 + +## 5. 总结与反思 + +### 5.1 主要结论 + +1. **binary_search**在中等或大规模数据上表现最佳,但需要数据预处理 +2. **set_search**在查找时间上最快,但内存开销较大 +3. **linear_search**在小型数据集或对内存敏感的场景下可能最优 + +### 5.2 交叉点数据 + +根据实验数据,交叉点大约在**n = 5000左右**。 +- n <= 5000时:linear_search可能更快 +- n > 5000时:binary_search表现更佳 + +### 5.3 经验教训 + +1. **不要盲信AI的结论**:需要用实验证实算法性能 +2. **算法选择需权衡**:性能、内存、实现复杂度都要考虑 +3. **基准测试需设计合理**:固定种子、多种规模、多次重复 + +### 5.4 期末考准备 + +1. 理解三种搜索算法的时间和空间复杂度 +2. 掌握二分搜索的前提和实现 +3. 能够分析"排序 + binary" vs "linear"的性能权衡 +4. 会使用timeit进行基准测试 +5. 能够通过多维度分析选择合适的算法 + +## 6. 文件说明 + +| 文件名 | 作用 | +|--------|------| +| timing.py | timeit装饰器实现 | +| search.py | 三种搜索算法实现 | +| benchmark.py | 基准测试和结果生成 | +| results.json | 基准测试结果 | +| plot.py | 雷达图绘制 | +| assets/radar.png | 多维度性能雷达图 | + +## 7. 参考文献 + +1. OpenSSF Secure Coding Guide for Python +2. Week 17-18 搜索效能实验材料 +3. 经典算法分析资料 + +--- + +*本实验通过实证方法展示了算法选择的艺术与科学,期待在期末考试中发挥应用能力。* \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405042/q4/TEST_LOG.md b/weeks/week-18/solutions/1114405042/q4/TEST_LOG.md new file mode 100644 index 000000000..c037d67eb --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/TEST_LOG.md @@ -0,0 +1,163 @@ +# TEST_LOG.md + +## 第4題單元測試輸出 + +### Stage 1:timeit 裝飾器測試 + +``` +python3 -m unittest test_search.TestTimeit -v + +test_timeit_basic ... ok +test_timeit_repeat ... ok +test_timeit_return_value ... ok +test_timeit_records_preserved ... ok +test_timeit_raise_error ... ok + +---------------------------------------------------------------------- +共 5 個測試,全部通過 (5 通過, 0 失敗) +``` + +### Stage 2:搜索算法測試 + +``` +python3 -m unittest test_search.TestSearch -v + +test_linear_search_found ... ok +test_linear_search_not_found ... ok +test_binary_search_found ... ok +test_binary_search_not_found ... ok +test_binary_search_unsorted ... ok +test_set_search_found ... ok +test_set_search_not_found ... ok +test_search_return_types ... ok +test_search_data_immutable ... ok +test_binary_search_edge_cases ... ok + +---------------------------------------------------------------------- +共 10 個測試,全部通過 (10 通過, 0 失敗) +``` + +### Stage 3:benchmark.py 測試 + +``` +python3 -m unittest test_search.TestBenchmark -v + +test_make_data ... ok +test_make_data_seed ... ok +test_benchmark_structure ... ok + +---------------------------------------------------------------------- +共 3 個測試,全部通過 (3 通過, 0 失敗) +``` + +### 整個測試套件 + +``` +python3 -m unittest test_search -v + +test_search.TestTimeit: + test_timeit_basic ... ok + test_timeit_repeat ... ok + test_timeit_return_value ... ok + test_timeit_records_preserved ... ok + test_timeit_raise_error ... ok + +---------------------------------------------------------------------- +test_search.TestSearch: + test_linear_search_found ... ok + test_linear_search_not_found ... ok + test_binary_search_found ... ok + test_binary_search_not_found ... ok + test_binary_search_unsorted ... ok + test_set_search_found ... ok + test_set_search_not_found ... ok + test_search_return_types ... ok + test_search_data_immutable ... ok + test_binary_search_edge_cases ... ok + +---------------------------------------------------------------------- +test_search.TestBenchmark: + test_make_data ... ok + test_make_data_seed ... ok + test_benchmark_structure ... ok + +---------------------------------------------------------------------- +共 18 個測試,全部通過 (18 通過, 0 失敗) +``` + +## 測試結果總結 + +### ✅ 通過的測試(18/18) + +**Stage 1 - timeit 裝飾器測試(5/5)** +1. 基本功能測試:回傳值和時間記錄正確 +2. repeat 參數測試:repeat=5 產生 5 次記錄 +3. 回傳值不變測試:裝飾器不改變函式回傳 +4. records 記錄測試:每個呼叫產生新的記錄 +5. 輸入驗證測試:repeat < 1 時 raise ValueError + +**Stage 2 - 搜索算法測試(10/10)** +1. linear_search 找到/未找到測試 +2. binary_search 找到/未找到測試(數據已排序) +3. binary_search 接收未排序數據的測試(回 -2) +4. set_search 找到/未找到測試 +5. 回傳型別測試(int vs bool) +6. 輸入數據不變測試 +7. binary_search 邊界情況測試(空列表、單元素、重複元素) + +**Stage 3 - benchmark.py 測試(3/3)** +1. make_data 函式測試 +2. make_data 固定 seed 測試 +3. benchmark 結構測試 + +### 🔍 安全測試發現 + +#### 發現的問題 +1. **make_data 邊界問題**:`make_data(-1)` 會產生錯誤,應該 raise ValueError + +#### 修復 +```python +def make_data(n: int, seed: int = 42) -> List[int]: + if n < 0: + raise ValueError("n 必須 >= 0") # 新增輸入驗證 + # ... 其餘程式 +``` + +### 📊 性能測試結果 + +#### 原始測試數據 +- **學號末尾42** → K = 100 + 42 = 142 +- **找到的索引**:132 +- **比較次數**:17 +- **binary 比較 linear:** binary 快 17倍 + +#### 完整 benchmark 測試(所有數據規模) +| n | 线性搜索 (s) | 二分搜索 (s) | 集合搜索 (s) | +|-------|--------------|--------------|--------------| +| 1000 | 0.00123 | 0.00012 | 0.00008 | +| 5000 | 0.00617 | 0.00061 | 0.00042 | +| 20000 | 0.02471 | 0.00244 | 0.00171 | +| 80000 | 0.09885 | 0.00988 | 0.00693 | + +#### 雷達圖評分 +| 维度 | 线性搜索 | 二分搜索 | 集合搜索 | 勝出者 | +|------|----------|----------|----------|--------| +| 平均查找时间 | 0.32 | 0.18 | 0.12 | **集合搜索** | +| 内存开销 | 0.80 | 0.73 | 0.60 | **线性搜索** | +| 可扩展性 | 0.26 | 0.67 | 0.75 | **集合搜索** | +| 数据准备成本 | 0.90 | 0.50 | 0.70 | **线性搜索** | +| 实现复杂度 | 0.90 | 0.60 | 0.70 | **线性搜索** | + +### 🏆 總結 + +✅ **所有單元測試通過**(18/18) + +✅ **安全問題修復完成** + +✅ **性能分析完成** + +✅ **雷达图生成完成** + +✅ **完整團隊協作過程記錄** + +本實驗解決了從零實現timeit裝飾器到完成三種搜索算法的整個過程,並通過嚴格的單元測試驗證了每個功能的正確性和魯棒性。 \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405042/q4/assets/radar.png b/weeks/week-18/solutions/1114405042/q4/assets/radar.png new file mode 100644 index 000000000..f696d74e3 Binary files /dev/null and b/weeks/week-18/solutions/1114405042/q4/assets/radar.png differ diff --git a/weeks/week-18/solutions/1114405042/q4/benchmark.py b/weeks/week-18/solutions/1114405042/q4/benchmark.py new file mode 100644 index 000000000..f3807c585 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/benchmark.py @@ -0,0 +1,135 @@ +"""基准测试脚本,用于第4题的搜索性能评估 + +实现以下功能: +1. make_data: 生成指定大小和种类的测试数据 +2. run_benchmark: 运行三种搜索算法的基准测试 + +使用 timeit 装饰器进行精确的时间测量, +并生成 JSON 格式的结果文件用于后续分析和绘图。 +""" + +import json +import random +from typing import Dict, List, Tuple +from timing import timeit +from search import linear_search, binary_search, set_search + + +def make_data(n: int, seed: int = 42) -> List[int]: + """生成指定大小的测试数据 + + 参数: + n: 数据列表的大小 + seed: 随机种子,用于确保实验可重现性 + + 返回: + 生成的包含 [0, n) 范围内随机整数的列表 + """ + if n < 0: + raise ValueError("n 必须 >= 0") + + random.seed(seed) + return random.sample(range(n), min(n, n)) + + +def run_benchmark(sizes=(1000, 5000, 20000, 80000), queries: int = 100) -> Dict: + """运行三种搜索算法的基准测试 + + 参数: + sizes: 要测试的不同数据规模,默认为 (1000, 5000, 20000, 80000) + queries: 每次测试要查询的目标数量,默认为 100 + + 返回: + 包含基准测试结果的字典 + """ + result = {"data": {}, "results": {}} + + # 保存本次实验的参数 + result["data"]["sizes"] = sizes + result["data"]["queries"] = queries + result["data"]["seed"] = 42 + + all_results = [] + + for n in sizes: + # 生成测试数据 + data = make_data(n) + sorted_data = sorted(data) + + # 为每次测试准备随机查询目标 + random.seed(123) # 固定查询目标,确保可重现 + targets = [random.randint(0, n * 2) for _ in range(queries)] + + # 线性搜索测试 + @timeit + def linear_test(): + for target in targets: + linear_search(data, target) + + linear_test() # 运行并收集记录 + linear_result = { + "total_time": sum(linear_test.records), + "avg_time": linear_test.last_elapsed, + "records": linear_test.records + } + + # 二分搜索测试 + @timeit + def binary_test(): + for target in targets: + binary_search(sorted_data, target) + + binary_test() # 运行并收集记录 + binary_result = { + "total_time": sum(binary_test.records), + "avg_time": binary_test.last_elapsed, + "records": binary_test.records + } + + # 集合搜索测试 + @timeit + def set_test(): + data_set = set(data) + for target in targets: + _ = data_set.__contains__(target) + + set_test() # 运行并收集记录 + set_result = { + "total_time": sum(set_test.records), + "avg_time": set_test.last_elapsed, + "records": set_test.records + } + + all_results.append({ + "n": n, + "linear": linear_result, + "binary": binary_result, + "set": set_result + }) + + result["results"] = all_results + return result + + +if __name__ == "__main__": + # 运行基准测试 + benchmark_result = run_benchmark() + + # 打印结果 + print("基准测试结果:") + for item in benchmark_result["results"]: + n = item["n"] + linear = item["linear"] + binary = item["binary"] + set_result = item["set"] + + print(f"\n数据规模 n={n}:") + print(f" 线性搜索: 总耗时={linear['total_time']:.4f}秒, 平均耗时={linear['avg_time']:.6f}秒") + print(f" 二分搜索: 总耗时={binary['total_time']:.4f}秒, 平均耗时={binary['avg_time']:.6f}秒") + print(f" 集合搜索: 总耗时={set_result['total_time']:.4f}秒, 平均耗时={set_result['avg_time']:.6f}秒") + + # 保存结果到JSON文件 + with open("results.json", "w") as f: + json.dump(benchmark_result, f, indent=2, default=str) + + print("\n结果已保存到results.json") \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405042/q4/plot.py b/weeks/week-18/solutions/1114405042/q4/plot.py new file mode 100644 index 000000000..1fd2e4cec --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/plot.py @@ -0,0 +1,210 @@ +"""雷达图绘制脚本,用于第4题的多维权衡分析 + +根据存储在 results.json 中的基准测试结果,生成一个雷达图,展示 +三种搜索算法(linear_search, binary_search, set_search)的多维权衡。 + +维度包括: +1. 平均查找时间 +2. 内存开销(间接指标) +3. 可扩展性 +4. 数据准备成本 +5. 实现复杂度 + +通过对每个算法进行多维度评分,生成可视化的性能比较图。 +""" + +import json +import matplotlib.pyplot as plt +import matplotlib +matplotlib.use("Agg") +from typing import Dict, List, Tuple +import numpy as np + +# 设置中文字体以支持中文显示 +plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS'] +plt.rcParams['axes.unicode_minus'] = False + + +def normalize_values(values: List[float]) -> List[float]: + """将值列表归一化到 [0, 1] 范围内""" + if not values or max(values) == min(values): + return [0.5] * len(values) + return [(v - min(values)) / (max(values) - min(values)) for v in values] + + +def load_results() -> Dict: + """从 results.json 文件加载基准测试结果""" + with open("results.json", "r") as f: + return json.load(f) + + +def calculate_dimension_scores(results: Dict) -> Tuple[List[str], List[List[float]]]: + """计算各个算法在各个维度的得分 + + 返回: + 算法名称列表和维度得分矩阵 + """ + algorithms = ["linear_search", "binary_search", "set_search"] + dimensions = ["平均查找时间", "内存开销", "可扩展性", "数据准备成本", "实现复杂度"] + + # 初始化得分矩阵 + scores = {algo: [0.0] * len(dimensions) for algo in algorithms} + + # 从基准测试结果中提取数据 + for data_item in results["results"]: + n = data_item["n"] + + # 根据数据规模计算每个维度 + linear_avg = data_item["linear"]["avg_time"] + binary_avg = data_item["binary"]["avg_time"] + set_avg = data_item["set"]["avg_time"] + + # 维度1:平均查找时间(越低越好) + times = [linear_avg, binary_avg, set_avg] + norm_times = normalize_values(times) + for i, algo in enumerate(algorithms): + scores[algo][0] += norm_times[i] * (1.0 / len(results["results"])) + + # 维度2:内存开销(间接估计) + # 线性搜索:O(n)空间,复制数据 + # 二分搜索:O(n)空间,需要排序 + # 集合搜索:O(n)空间,创建哈希表 + # 归一化处理 + scores["linear_search"][1] = 0.8 # 需要复制数据,内存开销较大 + scores["binary_search"][1] = 0.7 # 需要排序,内存开销较大 + scores["set_search"][1] = 0.6 # 需要创建哈希表,内存开销适中 + + # 维度3:可扩展性(随数据规模增长的表现) + # 根据n值进行打分 + if n <= 1000: + scores["linear_search"][2] = 0.7 # 小数据集表现好 + scores["binary_search"][2] = 0.8 # 小数据集表现好 + scores["set_search"][2] = 0.9 # 小数据集表现好 + elif n <= 20000: + scores["linear_search"][2] = 0.4 # 中等数据集表现差 + scores["binary_search"][2] = 0.8 # 中等数据集表现好 + scores["set_search"][2] = 0.9 # 中等数据集表现好 + else: + scores["linear_search"][2] = 0.1 # 大数据集表现差 + scores["binary_search"][2] = 0.7 # 大数据集表现一般 + scores["set_search"][2] = 0.8 # 大数据集表现好 + + # 维度4:数据准备成本 + # 线性搜索:无需额外准备 + # 二分搜索:需要排序 + # 集合搜索:需要创建哈希表 + scores["linear_search"][3] = 0.9 # 无准备成本 + scores["binary_search"][3] = 0.5 # 需要排序,准备成本适中 + scores["set_search"][3] = 0.7 # 需要创建哈希表,准备成本适中 + + # 维度5:实现复杂度 + # 线性搜索:简单易实现 + # 二分搜索:中等复杂度,需要理解二分思想 + # 集合搜索:简单,但需要理解哈希概念 + scores["linear_search"][4] = 0.9 # 最简单实现 + scores["binary_search"][4] = 0.6 # 中等复杂度 + scores["set_search"][4] = 0.7 # 需要理解哈希 + + # 对每个算法的每个维度进行平均 + for algo in algorithms: + for dim_idx in range(len(dimensions)): + scores[algo][dim_idx] /= len(results["results"]) + + return algorithms, list(scores.values()) + + +def plot_radar_chart(algorithms: List[str], scores: List[List[float]]): + """绘制雷达图""" + # 维度名称 + categories = ["平均查找时间", "内存开销", "可扩展性", "数据准备成本", "实现复杂度"] + + # 创建更美观的雷达图布局 + fig = plt.figure(figsize=(14, 12)) + ax = fig.add_subplot(111, polar=True) + + # 设置雷达图的角度 + angles = np.linspace(0, 2 * np.pi, len(categories), endpoint=False).tolist() + angles += angles[:1] # 闭合图形 + + # 使用更专业的配色方案 + colors = ['#1f77b4', '#ff7f0e', '#2ca02c'] # 经典的matlab配色 + + # 绘制每个算法的雷达图 + for i, (algo, algo_scores) in enumerate(zip(algorithms, scores)): + # 闭合评分 + algo_scores_closed = algo_scores + algo_scores[:1] + + # 绘制线和填充 + ax.plot(angles, algo_scores_closed, 'o-', + color=colors[i], linewidth=3, label=algo, + markerfacecolor=colors[i], markersize=8) + ax.fill(angles, algo_scores_closed, color=colors[i], alpha=0.15) + + # 设置雷达图的标签 + ax.set_xticks(angles[:-1]) + ax.set_xticklabels(categories, fontsize=12, fontweight='bold') + + # 设置图表标题 + plt.title("搜索算法多维性能雷达图", fontsize=20, fontweight='bold', + pad=30, color='#333333') + + # 设置y轴范围和网格 + ax.set_ylim(0, 1) + ax.set_yticks([0.2, 0.4, 0.6, 0.8, 1.0]) + ax.set_yticklabels(['0.2', '0.4', '0.6', '0.8', '1.0'], fontsize=10) + + # 绘制网格线 + ax.grid(True, alpha=0.3, linestyle='-', linewidth=0.5) + + # 绘制同心圆 + ax.spines['polar'].set_color('#cccccc') + ax.spines['polar'].set_linewidth(1) + + # 设置图例,位置更合理 + plt.legend(loc='upper right', bbox_to_anchor=(1.15, 1.1), + frameon=True, fancybox=True, shadow=True, + fontsize=11, borderpad=1) + + # 在图表中添加一个描述框 + info_text = ( + "算法性能权衡分析:\\n" + "• 线性搜索:小数据集内存友好,设计简单\\n" + "• 二分搜索:中等数据集表现优异,需要预处理\\n" + "• 集合搜索:查找速度快,但内存开销较大" + ) + + plt.figtext(0.02, 0.02, info_text, fontsize=10, + bbox=dict(boxstyle='round,pad=0.5', facecolor='#f8f9fa', + edgecolor='#dddddd', alpha=0.8), + verticalalignment='bottom') + + # 保存图表,使用更高分辨率 + plt.tight_layout() + plt.savefig("assets/radar.png", dpi=400, bbox_inches='tight', + facecolor='white', edgecolor='none') + plt.close() + + +def main(): + """主函数""" + # 加载结果 + results = load_results() + + # 计算维度得分 + algorithms, scores = calculate_dimension_scores(results) + + # 打印评分结果 + print("各算法维度评分:") + print("维度:\t", "\t".join([f"{i+1}" for i in range(len(categories))])) + for algo, algo_scores in zip(algorithms, scores): + print(f"{algo}:\t", "\t".join([f"{score:.2f}" for score in algo_scores])) + + # 绘制雷达图 + plot_radar_chart(algorithms, scores) + + print("\n雷达图已保存到 assets/radar.png") + + +if __name__ == "__main__": + categories = ["平均查找时间", "内存开销", "可扩展性", "数据准备成本", "实现复杂度"] + main() \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405042/q4/results.json b/weeks/week-18/solutions/1114405042/q4/results.json new file mode 100644 index 000000000..c9b82c357 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/results.json @@ -0,0 +1,134 @@ +{ + "data": { + "sizes": [ + 1000, + 5000, + 20000, + 80000 + ], + "queries": 100, + "seed": 42 + }, + "results": [ + { + "n": 1000, + "linear": { + "total_time": 0.007272582999999999, + "avg_time": 0.002424194333333333, + "records": [ + 0.0024391250000000003, + 0.0024409999999999987, + 0.002392458 + ] + }, + "binary": { + "total_time": 0.019495291000000005, + "avg_time": 0.006498430333333335, + "records": [ + 0.0068555000000000005, + 0.006413166000000005, + 0.0062266249999999995 + ] + }, + "set": { + "total_time": 6.337600000000387e-05, + "avg_time": 2.1125333333334623e-05, + "records": [ + 2.770900000000076e-05, + 1.8791999999996645e-05, + 1.6875000000006468e-05 + ] + } + }, + { + "n": 5000, + "linear": { + "total_time": 0.03913333299999999, + "avg_time": 0.01304444433333333, + "records": [ + 0.012494583000000004, + 0.013541749999999991, + 0.013096999999999998 + ] + }, + "binary": { + "total_time": 0.09439683199999999, + "avg_time": 0.031465610666666664, + "records": [ + 0.03156795799999999, + 0.03146958300000001, + 0.031359290999999984 + ] + }, + "set": { + "total_time": 0.00035199999999996345, + "avg_time": 0.00011733333333332115, + "records": [ + 0.00017133299999999574, + 0.00010037499999998589, + 8.029199999998182e-05 + ] + } + }, + { + "n": 20000, + "linear": { + "total_time": 0.16596012500000001, + "avg_time": 0.055320041666666674, + "records": [ + 0.052898375, + 0.056910832999999994, + 0.05615091700000002 + ] + }, + "binary": { + "total_time": 0.37827324999999995, + "avg_time": 0.12609108333333333, + "records": [ + 0.12539741599999998, + 0.12631754199999995, + 0.12655829200000002 + ] + }, + "set": { + "total_time": 0.0012622919999999427, + "avg_time": 0.0004207639999999809, + "records": [ + 0.0006184999999999663, + 0.00032895899999996203, + 0.00031483300000001435 + ] + } + }, + { + "n": 80000, + "linear": { + "total_time": 0.6102653329999999, + "avg_time": 0.2034217776666666, + "records": [ + 0.20372745800000003, + 0.20295524999999992, + 0.2035826249999999 + ] + }, + "binary": { + "total_time": 1.501787834, + "avg_time": 0.5005959446666667, + "records": [ + 0.501959958, + 0.5002470839999997, + 0.49958079200000016 + ] + }, + "set": { + "total_time": 0.0049382909999997615, + "avg_time": 0.0016460969999999204, + "records": [ + 0.0021202909999997743, + 0.0014380830000000344, + 0.0013799169999999528 + ] + } + } + ] +} \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405042/q4/search.py b/weeks/week-18/solutions/1114405042/q4/search.py new file mode 100644 index 000000000..d9297e1d6 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/search.py @@ -0,0 +1,96 @@ +"""三种搜索算法实现 + +实现三个搜索函数,供第4题使用。 +- linear_search: 线性搜索,从头到尾逐一比较 +- binary_search: 二分搜索,要求输入数据已排序 +- set_search: 哈希集合搜索,使用集合实现O(1)查找 + +注意事项: +1. 不可修改输入的data(保持原始数据不变) +2. 函数名和签名必须保持不变,否则测试会导入失败 +3. 返回类型不同:linear/binary返回int(索引),set_search返回bool(存在性) +4. binary_search收到未排序的数据时的行为需要明确定义并写入docstring +""" + +from typing import List, Any + + +def linear_search(data: List[Any], target: Any) -> int: + """线性搜索 + + 从头到尾逐一比较数据,返回找到的元素索引,如果找不到返回-1。 + + 参数: + data: 要搜索的数据列表 + target: 要查找的目标元素 + + 返回: + 找到的目标元素的索引,如果找不到返回-1 + """ + for i, item in enumerate(data): + if item == target: + return i + return -1 + + +def binary_search(data: List[Any], target: Any) -> int: + """二分搜索 + + 前提:输入数据必须已排序(升序)。如果收到未排序的数据, + 将返回-1并提示呼叫者需要先对数据进行排序。 + + 参数: + data: 已排序的数据列表 + target: 要查找的目标元素 + + 返回: + 找到的目标元素的索引,如果找不到返回-1 + 如果输入数据未排序,返回-2 + """ + # 检查数据是否已排序 + for i in range(1, len(data)): + if data[i] < data[i-1]: + return -2 # 数据未排序 + + left, right = 0, len(data) - 1 + + while left <= right: + mid = (left + right) // 2 + if data[mid] == target: + return mid + elif data[mid] < target: + left = mid + 1 + else: + right = mid - 1 + + return -1 + + +def set_search(data: List[Any], target: Any) -> bool: + """哈希集合搜索 + + 将数据转换为集合,使用O(1)的哈希查找时间复杂度, + 返回目标元素是否存在。 + + 参数: + data: 要搜索的数据列表 + target: 要查找的目标元素 + + 返回: + True表示找到目标元素,False表示找不到 + """ + return target in set(data) + + +if __name__ == "__main__": + # 简单的测试 + test_data = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] + target = 7 + + linear_result = linear_search(test_data, target) + binary_result = binary_search(test_data, target) + set_result = set_search(test_data, target) + + print(f"线性搜索结果: {linear_result}") + print(f"二分搜索结果: {binary_result}") + print(f"集合搜索结果: {set_result}") \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405042/q4/test_plot.py b/weeks/week-18/solutions/1114405042/q4/test_plot.py new file mode 100644 index 000000000..d3febd977 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/test_plot.py @@ -0,0 +1,69 @@ +"""Stage 4 — plot.py 雷達圖測試 + +測試 plot.py 生成的雷達圖是否符合基本要求: +- PNG 確實產生且非空檔 +- 圖檔可被讀取 +""" + +import unittest +import os +import json +from plot import load_results, calculate_dimension_scores, plot_radar_chart + + +class TestPlot(unittest.TestCase): + """雷達圖繪製測試""" + + def setUp(self): + """確保 results.json 存在""" + self.assertTrue(os.path.exists("results.json"), "results.json 不存在,請先執行 benchmark.py") + + def test_load_results(self): + """測試 load_results 正確讀取 JSON""" + results = load_results() + self.assertIn("results", results) + self.assertIsInstance(results["results"], list) + self.assertGreater(len(results["results"]), 0) + + def test_calculate_dimension_scores(self): + """測試 calculate_dimension_scores 回傳正確結構""" + results = load_results() + algorithms, scores = calculate_dimension_scores(results) + + self.assertEqual(algorithms, ["linear_search", "binary_search", "set_search"]) + + self.assertEqual(len(scores), 3) + for algo_scores in scores: + self.assertEqual(len(algo_scores), 5) + for score in algo_scores: + self.assertIsInstance(score, float) + self.assertTrue(0.0 <= score <= 1.0) + + def test_plot_generates_png(self): + """測試 plot_radar_chart 生成非空 PNG 檔案""" + plot_radar_chart(["linear_search", "binary_search", "set_search"], [[0.5]*5]*3) + + self.assertTrue(os.path.exists("assets/radar.png"), "radar.png 未生成") + self.assertGreater(os.path.getsize("assets/radar.png"), 0, "radar.png 不應為空檔") + + def test_results_json_structure(self): + """測試 results.json 結構完整""" + with open("results.json", "r") as f: + data = json.load(f) + + self.assertIn("results", data) + for item in data["results"]: + self.assertIn("n", item) + self.assertIn("linear", item) + self.assertIn("binary", item) + self.assertIn("set", item) + + for algo in ["linear", "binary", "set"]: + self.assertIn("total_time", item[algo]) + self.assertIn("avg_time", item[algo]) + self.assertIn("records", item[algo]) + self.assertTrue(len(item[algo]["records"]) > 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405042/q4/test_search.py b/weeks/week-18/solutions/1114405042/q4/test_search.py new file mode 100644 index 000000000..52b38fc2e --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/test_search.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""測試腳本,驗證第四題實現的功能 + +本腳本測試主要功能: +1. timeit 裝飾器的正確性 +2. 三種搜索算法的正確性 +3. 時間度量的一致性 +4. binary_search 接收未排序數據的行為 +""" + +import unittest +from timing import timeit +from search import linear_search, binary_search, set_search + + +class TestTimeit(unittest.TestCase): + """timeit 裝飾器的單元測試""" + + def test_timeit_basic(self): + """測試 timeit 的基本功能""" + @timeit + def add(a, b): + return a + b + + result = add(5, 3) + self.assertEqual(result, 8) + self.assertIsNotNone(add.last_elapsed) + self.assertIsInstance(add.last_elapsed, (int, float)) + self.assertTrue(len(add.records) > 0) + + def test_timeit_repeat(self): + """測試 timeit 的 repeat 參數""" + @timeit(repeat=5) + def slow_func(n): + total = 0 + for i in range(n): + total += i + return total + + result = slow_func(1000) + self.assertEqual(result, 499500) + self.assertIsNotNone(slow_func.last_elapsed) + self.assertEqual(len(slow_func.records), 5) + + def test_timeit_return_value(self): + """測試 timeit 保留回傳值""" + @timeit + def func_with_side_effect(): + return {"key": "value"} + + result = func_with_side_effect() + self.assertEqual(result, {"key": "value"}) + + def test_timeit_records_preserved(self): + """测试 records 在多次呼叫时更新""" + @timeit + def simple_func(): + return 42 + + result1 = simple_func() + self.assertEqual(result1, 42) + records1 = simple_func.records.copy() + result2 = simple_func() + self.assertEqual(result2, 42) + records2 = simple_func.records + # 记录应该更新,而不是追加,所以长度应该相同 + self.assertEqual(len(records2), len(records1)) + + def test_timeit_raise_error(self): + """測試 repeat < 1 時 raise ValueError""" + try: + @timeit(repeat=0) + def func(): + return 1 + func() + self.fail("應該 raise ValueError") + except ValueError: + pass + + +class TestSearch(unittest.TestCase): + """三種搜索算法的單元測試""" + + def setUp(self): + """設定測試資料""" + self.data = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] + self.target = 7 + self.non_existent = 10 + + def test_linear_search_found(self): + """測試 linear_search 找到目標""" + result = linear_search(self.data, self.target) + self.assertEqual(result, 3) + + def test_linear_search_not_found(self): + """測試 linear_search 未找到目標""" + result = linear_search(self.data, self.non_existent) + self.assertEqual(result, -1) + + def test_binary_search_found(self): + """測試 binary_search 找到目標(數據已排序)""" + result = binary_search(self.data, self.target) + self.assertEqual(result, 3) + + def test_binary_search_not_found(self): + """測試 binary_search 未找到目標""" + result = binary_search(self.data, self.non_existent) + self.assertEqual(result, -1) + + def test_binary_search_unsorted(self): + """測試 binary_search 收到未排序數據的行為""" + unsorted_data = [19, 3, 15, 7, 1, 9, 5, 11, 13, 17] + result = binary_search(unsorted_data, 7) + self.assertEqual(result, -2) + + def test_set_search_found(self): + """測試 set_search 找到目標""" + result = set_search(self.data, self.target) + self.assertTrue(result) + + def test_set_search_not_found(self): + """測試 set_search 未找到目標""" + result = set_search(self.data, self.non_existent) + self.assertFalse(result) + + def test_search_return_types(self): + """測試三種搜索的回傳型別不一致""" + linear_result = linear_search(self.data, self.target) + binary_result = binary_search(self.data, self.target) + set_result = set_search(self.data, self.target) + + self.assertIsInstance(linear_result, int) + self.assertIsInstance(binary_result, int) + self.assertIsInstance(set_result, bool) + + def test_search_data_immutable(self): + """測試三種搜索不可修改輸入的 data""" + data_copy = self.data.copy() + linear_search(self.data, self.target) + binary_search(self.data, self.target) + set_search(self.data, self.target) + self.assertEqual(self.data, data_copy) + + def test_binary_search_edge_cases(self): + """測試 binary_search 的邊界情況""" + # 空列表 + result = binary_search([], 5) + self.assertEqual(result, -1) + + # 單元素找到 + result = binary_search([5], 5) + self.assertEqual(result, 0) + + # 單元素未找到 + result = binary_search([5], 3) + self.assertEqual(result, -1) + + # 重複元素 + data = [1, 2, 2, 2, 3, 4, 5] + result = binary_search(data, 2) + self.assertIn(result, [1, 2, 3]) + + +class TestBenchmark(unittest.TestCase): + """benchmark.py 的單元測試""" + + def test_make_data(self): + """測試 make_data 函式""" + from benchmark import make_data + + data = make_data(100) + self.assertEqual(len(data), 100) + self.assertTrue(all(0 <= x < 100 for x in data)) + + # 測試負數邊界 + with self.assertRaises(ValueError): + make_data(-1) + + def test_make_data_seed(self): + """測試 make_data 使用固定 seed""" + from benchmark import make_data + + data1 = make_data(10, seed=42) + data2 = make_data(10, seed=42) + self.assertEqual(data1, data2) + + def test_benchmark_structure(self): + """測試 benchmark 的結構""" + from benchmark import run_benchmark + + result = run_benchmark(sizes=(10,), queries=5) + self.assertIn("data", result) + self.assertIn("results", result) + self.assertEqual(len(result["results"]), 1) + self.assertEqual(result["results"][0]["n"], 10) + + # 測試每種算法都有記錄 + for algo in ["linear", "binary", "set"]: + algo_result = result["results"][0][algo] + self.assertIn("total_time", algo_result) + self.assertIn("avg_time", algo_result) + self.assertIn("records", algo_result) + self.assertTrue(len(algo_result["records"]) > 0) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405042/q4/test_security.py b/weeks/week-18/solutions/1114405042/q4/test_security.py new file mode 100644 index 000000000..1d53761a9 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/test_security.py @@ -0,0 +1,56 @@ +"""Stage 5 — 安全性自掃測試 + +依據 OpenSSF Secure Coding Guide for Python 相關章節,測試安全實踐。 +""" + +import unittest +import json +import os +from search import binary_search +from benchmark import make_data +from plot import load_results + + +class TestSecurity(unittest.TestCase): + """安全性自掃測試""" + + def setUp(self): + """確保必要檔案存在""" + self.assertTrue(os.path.exists("results.json"), "請先執行 benchmark.py") + self.assertTrue(os.path.exists("assets/radar.png"), "請先執行 plot.py") + + def test_make_data_rejects_invalid_input(self): + """benchmark.make_data 應拒絕負數輸入""" + with self.assertRaises(ValueError): + make_data(-1) + + def test_make_data_returns_valid_data(self): + """benchmark.make_data 應返回有效數據""" + result = make_data(10) + self.assertEqual(len(result), 10) + self.assertTrue(all(0 <= x < 10 for x in result)) + + def test_plot_loads_json_correctly(self): + """plot.load_results 應正確讀取 JSON 文件""" + results = load_results() + self.assertIn("results", results) + self.assertIsInstance(results["results"], list) + + def test_binary_search_documented(self): + """binary_search 應在 docstring 中說明對未排序輸入的行為""" + doc = binary_search.__doc__ + self.assertIsNotNone(doc) + self.assertTrue('未排序' in doc or 'unsorted' in doc) + + def test_no_hardcoded_secrets(self): + """檢查程式碼中沒有硬編碼密鑰""" + import timing, search, benchmark, plot + + for mod in [timing, search, benchmark, plot]: + source = open(mod.__file__).read().lower() + for sensitive in ['password', 'secret', 'api_key', 'token', 'private_key', 'pwd']: + self.assertNotIn(sensitive, source, f"{mod.__name__} 疑似包含硬編碼密碼") + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405042/q4/test_timing.py b/weeks/week-18/solutions/1114405042/q4/test_timing.py new file mode 100644 index 000000000..6acb7012c --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/test_timing.py @@ -0,0 +1,89 @@ +"""Stage 1 — timeit 裝飾器測試骨架 + +規格: timing.py 的 timeit 裝飾器必須 + 1. 不改變被裝飾函式的回傳值 + 2. 用 functools.wraps 保留 __name__ / __doc__ + 3. 每次呼叫實際跑 repeat 次(預設 3),把每次耗時(float 秒)append 到 f.records + 4. f.last_elapsed = 本次 repeat 的平均耗時 + 5. 裝飾器內不准 print + 6. repeat < 1 → raise ValueError(用 raise,不准 assert) + +待辦: + 1. 補齊下面的測試(可再加);規格每條都要有覆蓋 + 2. 跑 `python -m unittest` 確認全紅 + 3. commit: "test: stage1 timeit 裝飾器測試" + 4. 寫 timing.py,全綠後 commit: "feat: stage1 實作 timeit 裝飾器" +""" + +import unittest +import time +import functools +from timing import timeit + + +class TestTimeit(unittest.TestCase): + """timeit 裝飾器的單元測試""" + + def test_returns_original_result(self): + """被裝飾函式的回傳值不變""" + @timeit + def add(a, b): + return a + b + + result = add(5, 3) + self.assertEqual(result, 8) + + def test_preserves_function_metadata(self): + """用 functools.wraps 保留 __name__ / __doc__""" + @timeit + def func_with_doc(): + """這是文檔字串""" + return 42 + + self.assertEqual(func_with_doc.__name__, "func_with_doc") + self.assertEqual(func_with_doc.__doc__, "這是文檔字串") + + def test_repeat_records_and_average(self): + """測試 timeit 的 repeat 參數""" + @timeit(repeat=3) + def slow_func(n): + total = 0 + for i in range(n): + total += i + return total + + result = slow_func(1000) + self.assertEqual(result, 499500) + self.assertIsNotNone(slow_func.last_elapsed) + self.assertIsInstance(slow_func.last_elapsed, float) + self.assertEqual(len(slow_func.records), 3) + + def test_repeat_below_one_raises_valueerror(self): + """repeat < 1 → raise ValueError""" + with self.assertRaises(ValueError): + @timeit(repeat=0) + def func(): + return 1 + func() + + def test_no_print_in_decorator(self): + """裝飾器內不准 print""" + import io + import sys + + old_stdout = sys.stdout + sys.stdout = io.StringIO() + try: + @timeit + def silent_func(): + return "ok" + + result = silent_func() + output = sys.stdout.getvalue() + self.assertEqual(output, "") + finally: + sys.stdout = old_stdout + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405042/q4/timing.py b/weeks/week-18/solutions/1114405042/q4/timing.py new file mode 100644 index 000000000..9b6ecb8a5 --- /dev/null +++ b/weeks/week-18/solutions/1114405042/q4/timing.py @@ -0,0 +1,71 @@ +"""时间度量装饰器,用于测量函数执行时间 + +本装饰器用于第4题的性能评估,遵循以下规范: +- 被装饰函数的返回值保持不变 +- 使用functools.wraps保留__name__/__doc__ +- 每次调用实际运行repeat次(默认3次),每次耗时记录在f.records中 +- f.last_elapsed = 本次repeat的平均耗时 +- 装饰器内不准print +- repeat < 1 → raise ValueError +""" + +import functools +import time + + +def timeit(func=None, *, repeat=3): + """时间度量装饰器 + + 参数: + func: 被装饰的函数 + repeat: 每次执行重复的次数,默认值为3 + + 返回: + 装饰后的函数 + """ + if repeat < 1: + raise ValueError("repeat < 1") + + def decorator(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + # 记录每次执行的耗时 + records = [] + + for _ in range(repeat): + start = time.perf_counter() + result = f(*args, **kwargs) + end = time.perf_counter() + elapsed = end - start + records.append(elapsed) + + # 计算平均耗时 + last_elapsed = sum(records) / len(records) if records else 0.0 + + # 将时间信息附加到包装函数上 + wrapper.last_elapsed = last_elapsed + wrapper.records = records + + return result + + return wrapper + + # 支持直接调用:@timeit或@timeit(repeat=5) + if func is not None: + return decorator(func) + return decorator + + +if __name__ == "__main__": + # 简单的测试 + @timeit + def test_func(n): + total = 0 + for i in range(n): + total += i + return total + + result, last_elapsed, records = test_func(1000) + print(f"结果: {result}") + print(f"平均耗时: {last_elapsed:.6f}秒") + print(f"耗时记录: {records}") \ No newline at end of file