From ac8444bb41ad5ad836f293199ad50dfebe816620 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 22 Jun 2026 06:25:22 +0800 Subject: [PATCH 1/2] =?UTF-8?q?[moz]=20feat(algorithms):=20=E4=B8=89?= =?UTF-8?q?=E7=A7=8D=E6=8E=92=E5=BA=8F=E7=AE=97=E6=B3=95=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=9C=80=E5=A4=A7=E5=80=BC=E6=9F=A5=E6=89=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - find_max_linear: 线性扫描 O(n)(原 find_max 重命名+alias兼容) - find_max_bubble: 冒泡排序 O(n²) - find_max_quickselect: 快速选择 partition O(n) avg - 37 个测试(7×3 参数化 + 9 一致性 + 7 原有) --- src/algorithms/find_max_bubble.py | 16 +++++ .../{find_max.py => find_max_linear.py} | 8 ++- src/algorithms/find_max_quickselect.py | 41 ++++++++++++ tests/test_find_max.py | 2 +- tests/test_find_max_three.py | 63 +++++++++++++++++++ 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 src/algorithms/find_max_bubble.py rename src/algorithms/{find_max.py => find_max_linear.py} (60%) create mode 100644 src/algorithms/find_max_quickselect.py create mode 100644 tests/test_find_max_three.py diff --git a/src/algorithms/find_max_bubble.py b/src/algorithms/find_max_bubble.py new file mode 100644 index 0000000..e677354 --- /dev/null +++ b/src/algorithms/find_max_bubble.py @@ -0,0 +1,16 @@ +"""find_max_bubble — 冒泡排序法找最大值""" + +from __future__ import annotations + + +def find_max_bubble(nums: list[int | float]) -> int | float | None: + """冒泡排序后取最后一个元素,空列表返回 None。""" + if not nums: + return None + arr = list(nums) # 不修改原列表 + n = len(arr) + for i in range(n - 1): + for j in range(n - 1 - i): + if arr[j] > arr[j + 1]: + arr[j], arr[j + 1] = arr[j + 1], arr[j] + return arr[-1] diff --git a/src/algorithms/find_max.py b/src/algorithms/find_max_linear.py similarity index 60% rename from src/algorithms/find_max.py rename to src/algorithms/find_max_linear.py index e71fd53..aac1bce 100644 --- a/src/algorithms/find_max.py +++ b/src/algorithms/find_max_linear.py @@ -1,9 +1,9 @@ -"""find_max — 从数字列表中查找最大值""" +"""find_max_linear — 线性扫描法找最大值""" from __future__ import annotations -def find_max(nums: list[int | float]) -> int | float | None: +def find_max_linear(nums: list[int | float]) -> int | float | None: """返回列表中的最大值,空列表返回 None。""" if not nums: return None @@ -12,3 +12,7 @@ def find_max(nums: list[int | float]) -> int | float | None: if num > result: result = num return result + + +# 兼容别名 +find_max = find_max_linear diff --git a/src/algorithms/find_max_quickselect.py b/src/algorithms/find_max_quickselect.py new file mode 100644 index 0000000..ae70600 --- /dev/null +++ b/src/algorithms/find_max_quickselect.py @@ -0,0 +1,41 @@ +"""find_max_quickselect — 快速选择 partition 思路找最大值""" + +from __future__ import annotations + +import random + + +def find_max_quickselect(nums: list[int | float]) -> int | float | None: + """使用快速选择的 partition 思路直接找最大值,空列表返回 None。""" + if not nums: + return None + arr = list(nums) # 不修改原列表 + return _quickselect_max(arr, 0, len(arr) - 1) + + +def _quickselect_max(arr: list[int | float], lo: int, hi: int) -> int | float: + """在 arr[lo..hi] 中找最大值。 + + Lomuto partition: < pivot 左,>= pivot 右。 + pivot 落在 store 位置,最大值在 [store, hi]。 + """ + if lo == hi: + return arr[lo] + + pivot_idx = random.randint(lo, hi) + arr[pivot_idx], arr[hi] = arr[hi], arr[pivot_idx] + + pivot = arr[hi] + store = lo + for i in range(lo, hi): + if arr[i] < pivot: + arr[store], arr[i] = arr[i], arr[store] + store += 1 + arr[store], arr[hi] = arr[hi], arr[store] + + # store 是 pivot 最终位置 + # 如果 store == hi,pivot 是当前区间最大值 + if store == hi: + return arr[store] + # 否则在 store+1..hi 中继续找(右侧都 >= pivot) + return _quickselect_max(arr, store + 1, hi) diff --git a/tests/test_find_max.py b/tests/test_find_max.py index 1bbdb1b..4cb875b 100644 --- a/tests/test_find_max.py +++ b/tests/test_find_max.py @@ -2,7 +2,7 @@ import pytest -from src.algorithms.find_max import find_max +from src.algorithms.find_max_linear import find_max class TestFindMax: diff --git a/tests/test_find_max_three.py b/tests/test_find_max_three.py new file mode 100644 index 0000000..166b4eb --- /dev/null +++ b/tests/test_find_max_three.py @@ -0,0 +1,63 @@ +"""三种排序算法找最大值 — 单元测试""" + +import pytest + +from src.algorithms.find_max_linear import find_max_linear +from src.algorithms.find_max_bubble import find_max_bubble +from src.algorithms.find_max_quickselect import find_max_quickselect + +ALL_ALGOS = [find_max_linear, find_max_bubble, find_max_quickselect] + + +# ── 通用测试(每种算法都跑) ── + +@pytest.mark.parametrize("algo", ALL_ALGOS, ids=["linear", "bubble", "quickselect"]) +class TestAllAlgorithms: + def test_normal_list(self, algo): + assert algo([3, 1, 4, 1, 5, 9, 2, 6]) == 9 + + def test_empty_list(self, algo): + assert algo([]) is None + + def test_single_element(self, algo): + assert algo([42]) == 42 + + def test_negative_numbers(self, algo): + assert algo([-5, -1, -10, -3]) == -1 + + def test_floats(self, algo): + assert algo([1.5, 2.7, 0.3, 3.14]) == 3.14 + + def test_mixed_int_float(self, algo): + assert algo([1, 2.5, 3, 0.1]) == 3 + + def test_duplicate_max(self, algo): + assert algo([7, 7, 7]) == 7 + + +# ── 一致性测试(同输入三法结果相同) ── + +class TestConsistency: + @pytest.mark.parametrize("nums", [ + [3, 1, 4, 1, 5, 9, 2, 6], + [-5, -1, -10, -3], + [1.5, 2.7, 0.3, 3.14], + [42], + [7, 7, 7], + [0, -0.0, 0.0], + [100, 200, 50, 150, 75], + ]) + def test_three_algorithms_same_result(self, nums): + results = [algo(nums) for algo in ALL_ALGOS] + assert all(r == results[0] for r in results), f"Inconsistent: {results}" + + def test_empty_consistency(self): + results = [algo([]) for algo in ALL_ALGOS] + assert all(r is None for r in results) + + def test_does_not_mutate_input(self): + original = [3, 1, 4, 1, 5, 9, 2, 6] + for algo in ALL_ALGOS: + nums = list(original) + algo(nums) + assert nums == original, f"{algo.__name__} mutated input" -- 2.45.4 From a402bb2a2db109fc8769cbfdbe57061b97fded38 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 22 Jun 2026 06:30:46 +0800 Subject: [PATCH 2/2] =?UTF-8?q?[moz]=20fix(test):=20=E8=A1=A5=E4=BF=AE=20t?= =?UTF-8?q?est=5Fe2e=5Fv31/test=5Ffour=5Fphase=20=E7=9A=84=20sys.path=20?= =?UTF-8?q?=E6=B1=A1=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #110 遗漏的两个文件,同样在模块级执行 sys.path.insert(DEPLOY_DIR) 导致 CI 中 src 模块从安装目录加载。统一加 allow_module_level skip guard。 - tests/e2e/test_e2e_v31.py - tests/e2e/test_four_phase.py --- tests/e2e/test_e2e_v31.py | 11 ++++------- tests/e2e/test_four_phase.py | 12 +++++------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/e2e/test_e2e_v31.py b/tests/e2e/test_e2e_v31.py index c0cac5b..5b6acfd 100644 --- a/tests/e2e/test_e2e_v31.py +++ b/tests/e2e/test_e2e_v31.py @@ -1,12 +1,11 @@ import pytest +import os + +if not os.environ.get("RUN_INTEGRATION"): + pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True) pytestmark = pytest.mark.e2e -skip_no_integration = pytest.mark.skipif( - not __import__("os").environ.get("RUN_INTEGRATION"), - reason="Set RUN_INTEGRATION=1 to run E2E tests against real daemon", -) - """v3.1 端到端测试 — 新增场景覆盖 覆盖 v3.1 新增功能: @@ -22,7 +21,6 @@ skip_no_integration = pytest.mark.skipif( """ import json -import os import sqlite3 import sys import time @@ -31,7 +29,6 @@ from datetime import datetime, timedelta from pathlib import Path from typing import Any, Dict, Optional -import pytest import requests as http_requests # ── 路径设置 ── diff --git a/tests/e2e/test_four_phase.py b/tests/e2e/test_four_phase.py index 388b61e..b837338 100644 --- a/tests/e2e/test_four_phase.py +++ b/tests/e2e/test_four_phase.py @@ -1,10 +1,11 @@ import os +import sys import pytest -pytestmark = [pytest.mark.e2e, pytest.mark.skipif( - not os.environ.get("RUN_INTEGRATION"), - reason="Set RUN_INTEGRATION=1 to run E2E tests", -)] +if not os.environ.get("RUN_INTEGRATION"): + pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True) + +pytestmark = [pytest.mark.e2e] """#01 四相循环 单元测试 @@ -23,13 +24,10 @@ pytestmark = [pytest.mark.e2e, pytest.mark.skipif( import asyncio import json -import sys from pathlib import Path from typing import Any, Dict, List, Optional from unittest.mock import AsyncMock, MagicMock, patch -import pytest - # ── 路径设置 ── DEPLOY_DIR = Path.home() / ".sanguo_projects" / "sanguo_moziplus_v2" SRC_DIR = DEPLOY_DIR / "src" -- 2.45.4