From 4612be1dbc04101264e488d5306c8224a77982fa Mon Sep 17 00:00:00 2001 From: 1114405019 Date: Mon, 22 Jun 2026 19:57:46 +0800 Subject: [PATCH 1/2] =?UTF-8?q?0622-1114405019-=E6=B4=AA=E7=BF=8A=E6=BB=95?= =?UTF-8?q?=20Test-4:=20=E6=96=B0=E5=A2=9E=E4=BA=8C=E5=88=86=E6=90=9C?= =?UTF-8?q?=E5=B0=8B=E6=95=88=E8=83=BD=E6=B8=AC=E8=A9=A6=EF=BC=88=E7=B4=85?= =?UTF-8?q?=E7=87=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 先寫 test_search_perf.py 涵蓋 FOUND/NOT FOUND、邊界值、單元素、 cmp 上限與 linear/binary 交叉驗證,此時尚無 search_perf.py 實作, 測試應為紅燈(ModuleNotFoundError)。 --- .../solutions/1114405019/test_search_perf.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 weeks/week-18/solutions/1114405019/test_search_perf.py diff --git a/weeks/week-18/solutions/1114405019/test_search_perf.py b/weeks/week-18/solutions/1114405019/test_search_perf.py new file mode 100644 index 000000000..29314e650 --- /dev/null +++ b/weeks/week-18/solutions/1114405019/test_search_perf.py @@ -0,0 +1,112 @@ +""" +針對 search_perf.py 的單元測試。 + +測重點:linear_search / binary_search 的正確性(FOUND / NOT FOUND / idx / cmp), +以及 generate_sorted_array 對「K 在不在陣列裡」的控制能力。 +timeit 量測與畫圖(plot_radar)不在此檔測試,因為效能數字會隨機器浮動、 +畫圖也沒有「正確答案」可斷言。 +""" + +import math + +from search_perf import binary_search, generate_sorted_array, linear_search + + +def test_binary_found_idx_matches_value(): + """找到時,回報的 idx 必須真的指向 target,而不是隨便回一個數字。""" + arr = [2, 4, 6, 8, 10, 12, 14] + idx, cmp = binary_search(arr, 10) + assert idx is not None + assert arr[idx] == 10 + assert cmp >= 1 + + +def test_binary_not_found_in_gap_between_elements(): + """target 落在兩個元素中間,最容易因為 off-by-one 誤判成鄰居元素。""" + arr = [1, 3, 5, 7, 9] + idx, cmp = binary_search(arr, 4) + assert idx is None + assert cmp >= 1 + + +def test_binary_not_found_below_minimum(): + """target 比陣列最小值還小,迴圈必須正確收斂到 NOT FOUND,不能誤判邊界。""" + arr = [10, 20, 30] + idx, _ = binary_search(arr, 1) + assert idx is None + + +def test_binary_not_found_above_maximum(): + """target 比陣列最大值還大,同樣是邊界收斂測試。""" + arr = [10, 20, 30] + idx, _ = binary_search(arr, 99) + assert idx is None + + +def test_single_element_found(): + """陣列只有 1 個元素,且該元素恰為 target:最小規模的 FOUND 案例。""" + idx, cmp = binary_search([5], 5) + assert idx == 0 + assert cmp == 1 + + +def test_single_element_not_found(): + """陣列只有 1 個元素,且該元素不是 target:最小規模的 NOT FOUND 案例。""" + idx, cmp = binary_search([5], 7) + assert idx is None + assert cmp == 1 + + +def test_first_element_boundary(): + """target 剛好是陣列第一個元素,確認迴圈條件沒有漏掉左邊界。""" + arr = [2, 4, 6, 8, 10] + idx, _ = binary_search(arr, 2) + assert idx == 0 + + +def test_last_element_boundary(): + """target 剛好是陣列最後一個元素,確認迴圈條件沒有漏掉右邊界。""" + arr = [2, 4, 6, 8, 10] + idx, _ = binary_search(arr, 10) + assert idx == len(arr) - 1 + + +def test_binary_cmp_upper_bound(): + """ + 二分搜尋的比較次數理論上限約為 ceil(log2(m)) + 1。 + 如果超出這個量級,代表搜尋邏輯(例如沒有正確折半)有問題。 + """ + arr = list(range(0, 2000, 2)) # m = 1000 個唯一升冪整數 + upper_bound = math.ceil(math.log2(len(arr))) + 1 + for target in (-5, 0, 1, 998, 1998, 50000): + _, cmp = binary_search(arr, target) + assert cmp <= upper_bound + + +def test_linear_binary_consistency_found_and_not_found(): + """ + 交叉驗證:同一陣列、同一 target,linear 與 binary 的 FOUND/NOT FOUND + 結果必須一致;陣列為唯一值時,FOUND 的 idx 也必須一致。 + 若兩者結果不一致,代表至少一個演算法寫錯了。 + """ + arr = generate_sorted_array(m=500, target=119, present=True, seed=1) + for target in (119, -999, arr[0], arr[-1], arr[len(arr) // 2] + 1): + l_idx, _ = linear_search(arr, target) + b_idx, _ = binary_search(arr, target) + assert (l_idx is None) == (b_idx is None) + if l_idx is not None: + assert l_idx == b_idx + + +def test_generate_sorted_array_present_guarantees_target(): + """present=True 時,target 必須恰好出現一次,且陣列升冪排列。""" + arr = generate_sorted_array(m=300, target=119, present=True, seed=42) + assert arr.count(119) == 1 + assert arr == sorted(arr) + + +def test_generate_sorted_array_absent_guarantees_no_target(): + """present=False 時,target 絕對不能出現在陣列中。""" + arr = generate_sorted_array(m=300, target=119, present=False, seed=42) + assert 119 not in arr + assert arr == sorted(arr) From 3dede58f49fefeab6fd3ca3d5ea3f9f1f242b723 Mon Sep 17 00:00:00 2001 From: 1114405019 Date: Mon, 22 Jun 2026 19:58:19 +0800 Subject: [PATCH 2/2] =?UTF-8?q?0622-1114405019-=E6=B4=AA=E7=BF=8A=E6=BB=95?= =?UTF-8?q?=20Test-4:=20=E5=AE=8C=E6=88=90=E4=BA=8C=E5=88=86=E6=90=9C?= =?UTF-8?q?=E5=B0=8B=E6=95=88=E8=83=BD=E5=AF=A6=E4=BD=9C=EF=BC=88=E7=B6=A0?= =?UTF-8?q?=E7=87=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 search_perf.py(linear_search/binary_search/陣列生成/timeit/雷達圖串接)、 search_perf-easy.py(AI 教的簡化版,含中文註解)、plot.py、requirements.txt、 README.md、AI_LOG.md 與 test_log.txt,test_search_perf.py 12 個測試全數通過。 --- weeks/week-18/solutions/1114405019/AI_LOG.md | 15 ++ weeks/week-18/solutions/1114405019/README.md | 86 ++++++++++ .../solutions/1114405019/assets/radar.png | Bin 0 -> 58792 bytes weeks/week-18/solutions/1114405019/plot.py | 63 ++++++++ .../solutions/1114405019/requirements.txt | 2 + .../solutions/1114405019/search_perf-easy.py | 44 ++++++ .../solutions/1114405019/search_perf.py | 147 ++++++++++++++++++ .../week-18/solutions/1114405019/test_log.txt | 21 +++ 8 files changed, 378 insertions(+) create mode 100644 weeks/week-18/solutions/1114405019/AI_LOG.md create mode 100644 weeks/week-18/solutions/1114405019/README.md create mode 100644 weeks/week-18/solutions/1114405019/assets/radar.png create mode 100644 weeks/week-18/solutions/1114405019/plot.py create mode 100644 weeks/week-18/solutions/1114405019/requirements.txt create mode 100644 weeks/week-18/solutions/1114405019/search_perf-easy.py create mode 100644 weeks/week-18/solutions/1114405019/search_perf.py create mode 100644 weeks/week-18/solutions/1114405019/test_log.txt diff --git a/weeks/week-18/solutions/1114405019/AI_LOG.md b/weeks/week-18/solutions/1114405019/AI_LOG.md new file mode 100644 index 000000000..d1921188b --- /dev/null +++ b/weeks/week-18/solutions/1114405019/AI_LOG.md @@ -0,0 +1,15 @@ +# AI_LOG + +## 我問 AI 什麼 + +請 AI 針對「二分搜尋效能比較」(K = 100 + 學號末兩碼 19 = 119)設計測試與實作:要求先寫測試(紅燈)、我確認後才寫實作,並要求 AI 在動手前先提出函式簽名、陣列產生策略(如何控制 K 在不在陣列中是可控的)、以及邊界測試規劃。 + +## AI 給了什麼 + +AI 提出 `linear_search` / `binary_search` 回傳 `(idx, cmp)`、用獨立的 `generate_sorted_array(m, target, present, seed)` 控制 K 是否在陣列中、以及 12 個涵蓋邊界值(單元素、第一/最後元素、間隙誤判、cmp 上限、linear/binary 交叉驗證)的測試案例。雷達圖第一版用中文標籤,執行時 matplotlib 出現 `Glyph ... missing from font(s) DejaVu Sans` 警告(中文字會變成方框)。 + +## 我改了什麼 + +1. 對 AI 提出的兩個開放決策做了選擇(而不是讓 AI 自己猜):main() 的輸入方式採「優先讀 stdin,沒輸入才自動生成」,雷達圖的 small_n/large_n 採 200 / 200,000——後者特地選擇遠超過題目要求的 10^5 門檻,確保能看出 binary 在大資料量下的優勢。 +2. 確認測試案例前,檢查了 12 個測試是否真的涵蓋題目要求的所有 edge case(K 存在/不存在、單元素、邊界元素、off-by-one、cmp 上限、交叉驗證),確認無誤後才放行讓 AI 寫實作。 +3. 發現雷達圖出現中文缺字警告後,要求把圖上文字(軸標籤、標題)改成英文,避免輸出圖片裡出現方框亂碼;中文解讀則保留在 README.md 文字說明中,不受字型限制影響。 diff --git a/weeks/week-18/solutions/1114405019/README.md b/weeks/week-18/solutions/1114405019/README.md new file mode 100644 index 000000000..70db112eb --- /dev/null +++ b/weeks/week-18/solutions/1114405019/README.md @@ -0,0 +1,86 @@ +# 第四題:二分搜尋效能(學號 1114405019) + +K = 100 + 19 = **119**(學號末兩碼 19) + +## 檔案 + +- `search_perf-easy.py`:**AI 教的簡單版本**,只留 `linear_search` / `binary_search` 兩個核心函式,附詳細中文註解,給 CPE 練習時手打背誦用 +- `search_perf.py`:**手打的完整版本**,加上 `generate_sorted_array` / `time_searches` / `collect_radar_metrics` / `main`,串接輸入輸出、timeit、畫圖 +- `plot.py`:雷達圖(`matplotlib.use("Agg")`,無視窗環境可執行) +- `test_search_perf.py`:pytest 單元測試(只測搜尋正確性,不測 timeit / 畫圖) +- `assets/radar.png`:雷達圖輸出 +- `test_log.txt`:測試執行紀錄(針對手打版本 `search_perf.py` 跑 `test_search_perf.py` 的結果) +- `requirements.txt`:依賴套件(`matplotlib`、`pytest`) +- `AI_LOG.md`:AI 使用紀錄 + +## 輸入/輸出 + +- 優先從 stdin 讀「第1行 m;第2行 m 個升冪整數」。 +- 沒有 stdin 輸入時,自動用 `generate_sorted_array(m=200_000, target=119, present=True)` 產生陣列。 +- 輸出:`FOUND idx cmp=次數` 或 `NOT FOUND cmp=次數`,接著印兩種搜尋的 timeit 結果與較快者。 + +## 陣列產生規則(如何控制 K 在不在) + +`generate_sorted_array(m, target, present, seed)`:在排除 `target` 的整數範圍內取樣 `m`(或 `m-1`)個唯一整數、排序, +若 `present=True` 才用 `bisect.insort` 把 `target` 插入。因此: + +- `present=True` → `target` **保證**恰好出現一次 → 結果必為 `FOUND`。 +- `present=False` → `target` **保證**不出現 → 結果必為 `NOT FOUND`。 + +`main()` 自動生成時固定用 `present=True`,所以執行 `python search_perf.py`(無 stdin)一定會印出 `FOUND`; +`NOT FOUND` 與其他邊界情況(單元素陣列、K 為第一個/最後一個元素、off-by-one 邊界)改用單元測試裡手刻的固定陣列驗證, +因為這些情況需要精確控制元素位置,用隨機產生器反而不可控。 + +## 雷達圖:維度與正規化 + +選了 4 個維度(`plot.py` 的 `DIMENSIONS`): + +1. **small-n speed**:m=200 時的 timeit 耗時 +2. **large-n speed**:m=200,000 時的 timeit 耗時 +3. **fewer comparisons**:在 large-n 規模下的 cmp 次數 +4. **no presort needed**:binary 需要事先排序好的陣列才能用,linear 不需要——這是類別變數(0/1),不是連續量 + +選這 4 個的原因:前 3 個是這題明確要求量測的指標(小 n 速度、大 n 速度、cmp),第 4 個補上一個 binary 的隱藏成本—— +如果原始資料沒排序,要先付出排序成本,這在前 3 個維度裡看不出來,但是 binary search 真實使用情境裡很關鍵的權衡。 + +正規化方式(讓「越外凸代表越好」): + +- 連續且越小越好的維度(時間、cmp):`score = min(linear, binary) / 該方法的值`。贏家分數恰好是 1, + 輸家是兩者的比例(沒有直接除以「全部方法的最大值」,因為時間/次數的量級差很多——例如 binary 的 cmp + 可能是個位數、linear 是六位數——直接除最大值會讓贏家的分數被輸家的尺度拉到幾乎貼著 0,反而看不出差異; + 用「最小值 / 自己」可以保證贏家永遠頂滿,輸家的分數則反映「贏家比我快幾倍」這個有意義的比例)。 +- `needs_presort`(已經是 0/1):直接 `score = 1 - value`,因為「不需要排序」才是優點,分數方向要跟其他維度一致(外凸=好)。 + +## 解讀(2-3 句) + +雷達圖中 binary 在 **large-n speed**、**small-n speed**、**fewer comparisons** 三個維度都明顯外凸, +顯示資料量越大時 binary search 的優勢越明顯(這也對應到 O(log n) vs O(n) 的理論複雜度)。 +但 linear 在 **no presort needed** 維度滿分、binary 是 0 分——binary search 要求資料先排序好, +若資料本身是無序的且只搜尋一次,排序成本可能蓋過 binary 省下來的搜尋時間,所以沒有絕對贏家, +要看「資料量大小」與「資料是否已經排序/會被重複查詢多次」來決定用哪種演算法。 + +## 測試 + +``` +pytest test_search_perf.py -v +``` + +12 個測試,涵蓋:找到時 idx 正確、NOT FOUND 邊界(落在元素間隙、小於最小值、大於最大值)、 +單元素陣列(FOUND / NOT FOUND)、K 為第一個/最後一個元素、cmp 次數理論上限、 +linear 與 binary 在多組 target 上的 FOUND/NOT FOUND 與 idx 交叉一致性、 +以及 `generate_sorted_array` 對 K 在不在陣列裡的保證。全數通過,紀錄於 `test_log.txt`。 + +## 執行 + +``` +python search_perf.py +``` + +範例輸出(陣列由程式隨機生成,idx/cmp/timeit 數字每次執行會不同): + +``` +FOUND 100323 cmp=18 +linear : 0.0426 s +binary : 0.0000 s +=> binary faster +``` diff --git a/weeks/week-18/solutions/1114405019/assets/radar.png b/weeks/week-18/solutions/1114405019/assets/radar.png new file mode 100644 index 0000000000000000000000000000000000000000..c84542c8c2edd2b21813b469f8ceb93355f79525 GIT binary patch literal 58792 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 literal 0 HcmV?d00001 diff --git a/weeks/week-18/solutions/1114405019/plot.py b/weeks/week-18/solutions/1114405019/plot.py new file mode 100644 index 000000000..a0c7de73f --- /dev/null +++ b/weeks/week-18/solutions/1114405019/plot.py @@ -0,0 +1,63 @@ +""" +畫 linear vs binary 的多維權衡雷達圖。 +無視窗環境(CI / 沒有 DISPLAY)需要在 import pyplot 之前指定 Agg backend。 +正規化與維度選擇的理由寫在 README.md。 +""" + +import math +import os + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +DIMENSIONS = ["small_n_time", "large_n_time", "cmp", "needs_presort"] +LABELS = ["small-n speed", "large-n speed", "fewer comparisons", "no presort needed"] + + +def _normalize(metrics): + """ + 把每個維度轉成「越大越好、範圍落在 (0, 1]」的分數: + - 連續且越小越好的維度(時間、cmp):score = min_value / value, + 贏家恰好等於 1,輸家是 min/value 的比例(避免兩個方法數值差太多時, + 輸家被壓成肉眼看不出來的 0)。 + - 已經是 0/1 類別變數的維度(needs_presort):直接 score = 1 - value, + 因為「不需要先排序」才是優點。 + """ + scores = {method: [] for method in metrics} + for dim in DIMENSIONS: + values = {method: metrics[method][dim] for method in metrics} + if dim == "needs_presort": + for method in metrics: + scores[method].append(1 - values[method]) + continue + min_v = min(v for v in values.values() if v > 0) if any(values.values()) else 1 + for method in metrics: + v = values[method] + scores[method].append(min_v / v if v > 0 else 1.0) + return scores + + +def plot_radar(metrics, output_path="assets/radar.png"): + """metrics: collect_radar_metrics() 回傳的 dict('linear' / 'binary' 兩個 method)。""" + scores = _normalize(metrics) + n = len(DIMENSIONS) + angles = [i / n * 2 * math.pi for i in range(n)] + angles += angles[:1] + + fig, ax = plt.subplots(figsize=(6, 6), subplot_kw={"projection": "polar"}) + for method, color in (("linear", "tab:blue"), ("binary", "tab:orange")): + values = scores[method] + scores[method][:1] + ax.plot(angles, values, label=method, color=color) + ax.fill(angles, values, alpha=0.15, color=color) + + ax.set_xticks(angles[:-1]) + ax.set_xticklabels(LABELS) + ax.set_ylim(0, 1) + ax.set_title("Linear vs Binary Search Trade-offs") + ax.legend(loc="upper right", bbox_to_anchor=(1.2, 1.1)) + + os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) + fig.savefig(output_path, bbox_inches="tight") + plt.close(fig) diff --git a/weeks/week-18/solutions/1114405019/requirements.txt b/weeks/week-18/solutions/1114405019/requirements.txt new file mode 100644 index 000000000..132e920b8 --- /dev/null +++ b/weeks/week-18/solutions/1114405019/requirements.txt @@ -0,0 +1,2 @@ +matplotlib>=3.10 +pytest>=8.0 diff --git a/weeks/week-18/solutions/1114405019/search_perf-easy.py b/weeks/week-18/solutions/1114405019/search_perf-easy.py new file mode 100644 index 000000000..f0862cf8f --- /dev/null +++ b/weeks/week-18/solutions/1114405019/search_perf-easy.py @@ -0,0 +1,44 @@ +""" +簡化/好記版本:只留下 CPE 當場手打時真正需要記住的兩個函式。 +不含 timeit、雷達圖、檔案輸入這些「裝飾」,方便練習時專心背邏輯。 +""" + + +def linear_search(arr, target): + """ + 線性搜尋:從第一個元素開始,一個一個比對到底。 + + 口訣:「從頭找到尾,找到就回頭」 + - 不要求 arr 已排序,所以隨機亂序的陣列也能用。 + - cmp 用來計算總共比了幾次,每比一次 +1。 + """ + cmp = 0 + for i in range(len(arr)): + cmp += 1 + if arr[i] == target: + return i, cmp # 找到了,立刻回頭,不要多比 + return None, cmp # 比到最後都沒有,才算 NOT FOUND + + +def binary_search(arr, target): + """ + 二分搜尋:每次都看中間那個,比較完直接砍掉一半。 + + 口訣:「先看中間,比大砍右,比小砍左」 + - 前提:arr 一定要先排好(升冪),否則砍半的邏輯會錯。 + - lo / hi 是目前還沒被排除的範圍兩端(含 lo 和 hi 本身)。 + - 迴圈條件用 lo <= hi(不是 <),因為當 lo == hi 時, + 代表還剩 1 個元素沒檢查,這就是最容易漏掉邊界的地方。 + """ + cmp = 0 + lo, hi = 0, len(arr) - 1 + while lo <= hi: + mid = (lo + hi) // 2 # 中間位置,整數除法直接捨去小數 + cmp += 1 + if arr[mid] == target: + return mid, cmp # 中間就是答案,回頭 + elif arr[mid] < target: + lo = mid + 1 # target 比中間大 -> 答案在右半邊,砍掉左邊(含 mid) + else: + hi = mid - 1 # target 比中間小 -> 答案在左半邊,砍掉右邊(含 mid) + return None, cmp # lo > hi 代表範圍已經砍到空,確定找不到 diff --git a/weeks/week-18/solutions/1114405019/search_perf.py b/weeks/week-18/solutions/1114405019/search_perf.py new file mode 100644 index 000000000..0b30f69fa --- /dev/null +++ b/weeks/week-18/solutions/1114405019/search_perf.py @@ -0,0 +1,147 @@ +""" +第四題:二分搜尋效能比較。 + +學號末兩碼 = 19,搜尋目標 K = 100 + 19 = 119。 +""" + +import bisect +import math +import random +import sys +import timeit + +K = 119 + + +def linear_search(arr, target): + """從頭逐一比較,找到回傳 (idx, cmp);找不到回傳 (None, cmp)。""" + cmp = 0 + for i, v in enumerate(arr): + cmp += 1 + if v == target: + return i, cmp + return None, cmp + + +def binary_search(arr, target): + """ + 前提:arr 已升冪排序。 + 每次迴圈只做一次 == / < / > 的「決定方向」比較,cmp 計次以此為準, + 這樣才能跟 ceil(log2(m)) + 1 的理論上限對齊。 + """ + cmp = 0 + lo, hi = 0, len(arr) - 1 + while lo <= hi: + mid = (lo + hi) // 2 + cmp += 1 + if arr[mid] == target: + return mid, cmp + elif arr[mid] < target: + lo = mid + 1 + else: + hi = mid - 1 + return None, cmp + + +def generate_sorted_array(m, target, present, seed=None): + """ + 產生長度為 m 的升冪唯一整數陣列。 + present=True:target 保證恰好出現一次。 + present=False:target 保證不出現。 + 做法:在排除 target 的範圍內取樣 m(或 m-1)個唯一值, + present=True 時再用 bisect.insort 把 target 插入正確位置。 + """ + rng = random.Random(seed) + span = max(m * 5, 1000) + pool_low, pool_high = target - span, target + span + candidates = [v for v in range(pool_low, pool_high) if v != target] + + count = m - 1 if present else m + sample = rng.sample(candidates, count) + sample.sort() + + if present: + bisect.insort(sample, target) + return sample + + +def time_searches(arr, target, number=5): + """用 timeit 量測 linear_search 與 binary_search 各跑 number 次的總秒數。""" + linear_time = timeit.timeit(lambda: linear_search(arr, target), number=number) + binary_time = timeit.timeit(lambda: binary_search(arr, target), number=number) + return {"linear": linear_time, "binary": binary_time} + + +def collect_radar_metrics(small_n, large_n, target, seed=None): + """ + 為雷達圖準備原始數據(未正規化): + - small_n / large_n 兩種規模下,linear 與 binary 各自的耗時 + - 在 large_n 規模下兩者的比較次數 cmp + - 是否需要先排序(categorical:linear=0 不需要,binary=1 需要) + """ + small_arr = generate_sorted_array(small_n, target, present=True, seed=seed) + large_arr = generate_sorted_array(large_n, target, present=True, seed=seed) + + small_time = time_searches(small_arr, target) + large_time = time_searches(large_arr, target) + + _, linear_cmp = linear_search(large_arr, target) + _, binary_cmp = binary_search(large_arr, target) + + return { + "linear": { + "small_n_time": small_time["linear"], + "large_n_time": large_time["linear"], + "cmp": linear_cmp, + "needs_presort": 0, + }, + "binary": { + "small_n_time": small_time["binary"], + "large_n_time": large_time["binary"], + "cmp": binary_cmp, + "needs_presort": 1, + }, + } + + +def _read_array_from_stdin(): + """嘗試從 stdin 讀取「第1行 m;第2行 m 個升冪整數」,讀不到就回傳 None。""" + data = sys.stdin.read().strip() + if not data: + return None + lines = data.splitlines() + if len(lines) < 2: + return None + m = int(lines[0]) + arr = [int(x) for x in lines[1].split()] + if len(arr) != m: + raise ValueError(f"輸入的整數個數 ({len(arr)}) 與宣告的 m ({m}) 不一致") + return arr + + +def main(): + arr = _read_array_from_stdin() + if arr is None: + # 沒有 stdin 輸入:自動生成一個 >= 10^5 的大陣列,並保證 K 在裡面。 + arr = generate_sorted_array(m=200_000, target=K, present=True, seed=0) + + idx, cmp = binary_search(arr, K) + if idx is not None: + print(f"FOUND {idx} cmp={cmp}") + else: + print(f"NOT FOUND cmp={cmp}") + + times = time_searches(arr, K) + print(f"linear : {times['linear']:.4f} s") + print(f"binary : {times['binary']:.4f} s") + faster = "binary" if times["binary"] < times["linear"] else "linear" + print(f"=> {faster} faster") + + from plot import plot_radar + + metrics = collect_radar_metrics(small_n=200, large_n=200_000, target=K, seed=0) + plot_radar(metrics, "assets/radar.png") + + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405019/test_log.txt b/weeks/week-18/solutions/1114405019/test_log.txt new file mode 100644 index 000000000..9a893052f --- /dev/null +++ b/weeks/week-18/solutions/1114405019/test_log.txt @@ -0,0 +1,21 @@ +============================= test session starts ============================= +platform win32 -- Python 3.11.9, pytest-9.0.2, pluggy-1.6.0 -- C:\Users\yiteng\AppData\Local\Programs\Python\Python311\python.exe +cachedir: .pytest_cache +rootdir: C:\Users\yiteng\Downloads\0622-4\2026-python\weeks\week-18\solutions\1114405019 +plugins: anyio-4.13.0 +collecting ... collected 12 items + +test_search_perf.py::test_binary_found_idx_matches_value PASSED [ 8%] +test_search_perf.py::test_binary_not_found_in_gap_between_elements PASSED [ 16%] +test_search_perf.py::test_binary_not_found_below_minimum PASSED [ 25%] +test_search_perf.py::test_binary_not_found_above_maximum PASSED [ 33%] +test_search_perf.py::test_single_element_found PASSED [ 41%] +test_search_perf.py::test_single_element_not_found PASSED [ 50%] +test_search_perf.py::test_first_element_boundary PASSED [ 58%] +test_search_perf.py::test_last_element_boundary PASSED [ 66%] +test_search_perf.py::test_binary_cmp_upper_bound PASSED [ 75%] +test_search_perf.py::test_linear_binary_consistency_found_and_not_found PASSED [ 83%] +test_search_perf.py::test_generate_sorted_array_present_guarantees_target PASSED [ 91%] +test_search_perf.py::test_generate_sorted_array_absent_guarantees_no_target PASSED [100%] + +============================= 12 passed in 0.02s ==============================