From 72aa011de909dc9fdbcb1a2b74ea44ffed27231f Mon Sep 17 00:00:00 2001 From: MH Hung Date: Tue, 23 Sep 2025 10:59:18 +0800 Subject: [PATCH] [#0165]feat(leetcode): Init project, add C# solution, unit test and README --- .../0165-compare-version-numbers/README.md | 85 +++++++++++ .../csharp/Program.cs | 144 ++++++++++++++++++ .../csharp/csharp.csproj | 9 ++ .../test/SolutionTests.cs | 56 +++++++ .../test/TestProject.csproj | 19 +++ .../test/edge_cases.md | 29 ++++ 6 files changed, 342 insertions(+) create mode 100644 problems/0165-compare-version-numbers/README.md create mode 100644 problems/0165-compare-version-numbers/csharp/Program.cs create mode 100644 problems/0165-compare-version-numbers/csharp/csharp.csproj create mode 100644 problems/0165-compare-version-numbers/test/SolutionTests.cs create mode 100644 problems/0165-compare-version-numbers/test/TestProject.csproj create mode 100644 problems/0165-compare-version-numbers/test/edge_cases.md diff --git a/problems/0165-compare-version-numbers/README.md b/problems/0165-compare-version-numbers/README.md new file mode 100644 index 0000000..ef07b2f --- /dev/null +++ b/problems/0165-compare-version-numbers/README.md @@ -0,0 +1,85 @@ +# [165] Compare Version Numbers + +## 題目資訊 +- **難度**: Medium +- **標籤**: Two Pointer, String +- **題目連結**: https://leetcode.com/problems/compare-version-numbers/ +- **練習日期**: 2025-09-23 +- **目標複雜度**: 時間 O(n)、空間 O(1) + +## 題目描述 +Given two **version strings**, `version1` and `version2`, compare them. A version string consists of **revisions** separated by dots `'.'`. The **value of the revision** is its **integer conversion** ignoring leading zeros. + +To compare version strings, compare their revision values in **left-to-right order**. If one of the version strings has fewer revisions, treat the missing revision values as `0`. + +Return the following: + +If `version1 < version2`, return -1. +If `version1 > version2`, return 1. +Otherwise, return 0. + +## 先備條件與限制 +- 1 <= `version1.length, version2.length` <= 500 +- version1 and version2 only contain digits and '.' +- version1 and version2 are valid version numbers. +- All the given revisions in `version1` and `version2` can be stored in a **32-bit integer** + +## 解題思路 + +### 初步分析 +- 類型:字串雙指標掃描 +- 關鍵觀察:版本號可以逐段比較,缺段視為 0,且修訂號只包含數字 +- 複雜度目標理由:只需線性掃過兩個字串一次即可完成比較 + +### 解法比較 +1. 解法A(基準/暴力): + - 名稱:`MyCompareVersion` + - 思路:以 `Split('.')` 將版本字串拆成陣列,逐段轉成整數後比較;若另一側段數不足以 0 補齊 + - 正確性:LeetCode 限制每段可裝進 32-bit 整數,直接使用 `int.TryParse` 安全可靠 + - 複雜度:時間 O(n),空間 O(k)(k 為段數,需配置字串陣列與子字串) +2. 解法B(優化): + - 名稱:`CompareVersion` + - 思路:雙指標同步掃描兩個版本字串,藉由 `ReadOnlySpan` 抓取下一段,去除前導 0 後用字元比較避免溢位與額外配置 + - 正確性:段長先比較、再逐字比較,完全符合題意;缺段會回傳空 span 視為 0 + - 複雜度:時間 O(n),空間 O(1) + +## 實作細節 + +### 常見陷阱 +- 前導 0:需在比較前移除,否則 `"01"` 與 `"1"` 會被視為不同 +- 段數不一致:右側缺少的段要視為 0 +- 空字串或末尾點:`""`、`"1."` 都可能出現,需要妥善處理 +- 非數字字元:防守性處理(當前實作視為 0),但依題意實際資料不會出現 + +## 測試案例 + +### 範例輸入輸出 +``` +Input: version1 = "1.2", version2 = "1.10" +Output: -1 +Explanation: +version1's second revision is "2" and version2's second revision is "10": 2 < 10, so version1 < version2. +``` + +### 邊界清單 +- [x] 空字串 / 僅有 0 +- [x] 單一段 / 全相同 +- [x] 含 0 / 大數 / 前導 0 +- [ ] 去重(與此題無關) +- [x] 大資料壓力(長度 200 的版本字串) + +## 複雜度分析 +- 最壞:時間 O(n)、空間 O(1) +- 備註:保留的 `MyCompareVersion` 雖然同為 O(n),但空間為 O(k) + +## 相關題目 / Follow-up +- 179. Largest Number:同樣涉及字串排序與比較 +- 415. Add Strings:字串逐位操作 + +## 學習筆記 +- 今天學到:兩指標搭配 `ReadOnlySpan` 可以在 C# 中避免額外配置 +- 卡住與修正:原本 console app 移除 `Main` 造成 `dotnet test` 無法編譯,後來補回精簡入口 +- 待優化:若要支援超長版本段,可考慮使用 `BigInteger` 或自訂比較邏輯(目前已以字元比較處理) + +--- +**總結**:核心在於逐段處理並正確處理前導 0 與缺段情況,適合練習字串雙指標與記憶體優化技巧。 diff --git a/problems/0165-compare-version-numbers/csharp/Program.cs b/problems/0165-compare-version-numbers/csharp/Program.cs new file mode 100644 index 0000000..ecc5c59 --- /dev/null +++ b/problems/0165-compare-version-numbers/csharp/Program.cs @@ -0,0 +1,144 @@ +// LeetCode 165: Compare Version Numbers +// 難度: Medium +// 日期: 2025-09-23 + +using System; + +public class Solution +{ + // Two-pointer parser that compares segments without allocating intermediate arrays. + public int CompareVersion(string version1, string version2) + { + var i = 0; + var j = 0; + + while (i < version1.Length || j < version2.Length) + { + var segment1 = NextSegment(version1, ref i); + var segment2 = NextSegment(version2, ref j); + + var comparison = CompareSegments(segment1, segment2); + if (comparison != 0) + { + return comparison; + } + } + + return 0; + } + + // Reads the next numeric segment (between dots) as a span and advances the current index. + private static ReadOnlySpan NextSegment(string version, ref int index) + { + if (index >= version.Length) + { + return ReadOnlySpan.Empty; + } + + var start = index; + while (index < version.Length && version[index] != '.') + { + index++; + } + + var segment = version.AsSpan(start, index - start); + + if (index < version.Length && version[index] == '.') + { + index++; + } + + return segment; + } + + // Compares two trimmed segments lexicographically to avoid integer overflow. + private static int CompareSegments(ReadOnlySpan left, ReadOnlySpan right) + { + left = TrimLeadingZeros(left); + right = TrimLeadingZeros(right); + + if (left.Length > right.Length) + { + return 1; + } + + if (left.Length < right.Length) + { + return -1; + } + + for (var i = 0; i < left.Length; i++) + { + if (left[i] > right[i]) + { + return 1; + } + + if (left[i] < right[i]) + { + return -1; + } + } + + return 0; + } + + private static ReadOnlySpan TrimLeadingZeros(ReadOnlySpan segment) + { + var index = 0; + while (index < segment.Length && segment[index] == '0') + { + index++; + } + + return index == segment.Length ? ReadOnlySpan.Empty : segment[index..]; + } + + public int MyCompareVersion(string version1, string version2) + { + var v1 = version1.Split('.'); + var v2 = version2.Split('.'); + + if (v1.Length >= v2.Length) + { + return CompareString(v1, v2); + } + + return CompareString(v2, v1) * -1; + } + + private static int CompareString(string[] longer, string[] shorter) + { + for (var i = 0; i < longer.Length; i++) + { + _ = int.TryParse(longer[i], out var num1); + var num2 = i < shorter.Length && int.TryParse(shorter[i], out var parsed) ? parsed : 0; + + if (num1 > num2) + { + return 1; + } + + if (num1 < num2) + { + return -1; + } + } + + return 0; + } +} + +public class Program +{ + public static void Main(string[] args) + { + // optional: keep console app functional for manual verification + if (args.Length == 2) + { + var solution = new Solution(); + var result = solution.CompareVersion(args[0], args[1]); + Console.WriteLine(result); + } + } +} diff --git a/problems/0165-compare-version-numbers/csharp/csharp.csproj b/problems/0165-compare-version-numbers/csharp/csharp.csproj new file mode 100644 index 0000000..9dbab49 --- /dev/null +++ b/problems/0165-compare-version-numbers/csharp/csharp.csproj @@ -0,0 +1,9 @@ + + + Exe + net8.0 + enable + enable + + + diff --git a/problems/0165-compare-version-numbers/test/SolutionTests.cs b/problems/0165-compare-version-numbers/test/SolutionTests.cs new file mode 100644 index 0000000..e49f83b --- /dev/null +++ b/problems/0165-compare-version-numbers/test/SolutionTests.cs @@ -0,0 +1,56 @@ +// LeetCode 165: Compare Version Numbers 單元測試(xUnit) + +using System.Linq; +using Xunit; + +public class SolutionTests { + private readonly Solution _s = new Solution(); + + [Theory] + [InlineData("1.01", "1.001", 0)] + [InlineData("1.0", "1.0.0", 0)] + [InlineData("1.0.0.0", "1", 0)] + [InlineData("0.1", "1.1", -1)] + [InlineData("1.0.1", "1", 1)] + [InlineData("7.5.2.4", "7.5.3", -1)] + public void CompareVersion_ReturnsExpectedResult(string version1, string version2, int expected) { + var actual = _s.CompareVersion(version1, version2); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("1.0.1", "1")] + [InlineData("3.0.0", "2.9.9.9")] + [InlineData("10.4", "10.3.9")] + public void CompareVersion_IsAntiSymmetric(string left, string right) { + var forward = _s.CompareVersion(left, right); + var backward = _s.CompareVersion(right, left); + + Assert.Equal(1, forward); + Assert.Equal(-1, backward); + } + + [Theory] + [InlineData("", "", 0)] + [InlineData("", "0", 0)] + [InlineData("0.0.0", "", 0)] + [InlineData("000", "0", 0)] + [InlineData("2147483647", "2147483646", 1)] + [InlineData("2147483646", "2147483647", -1)] + public void CompareVersion_HandlesBoundaryInputs(string version1, string version2, int expected) { + var actual = _s.CompareVersion(version1, version2); + + Assert.Equal(expected, actual); + } + + [Fact] + public void CompareVersion_LongSequencesDifferAtEnd() { + var left = string.Join('.', Enumerable.Repeat("0", 199).Append("1")); + var right = string.Join('.', Enumerable.Repeat("0", 200)); + + var result = _s.CompareVersion(left, right); + + Assert.Equal(1, result); + } +} diff --git a/problems/0165-compare-version-numbers/test/TestProject.csproj b/problems/0165-compare-version-numbers/test/TestProject.csproj new file mode 100644 index 0000000..e2d8b41 --- /dev/null +++ b/problems/0165-compare-version-numbers/test/TestProject.csproj @@ -0,0 +1,19 @@ + + + net8.0 + enable + false + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/problems/0165-compare-version-numbers/test/edge_cases.md b/problems/0165-compare-version-numbers/test/edge_cases.md new file mode 100644 index 0000000..f8bf338 --- /dev/null +++ b/problems/0165-compare-version-numbers/test/edge_cases.md @@ -0,0 +1,29 @@ +# 邊界情況清單(165 Compare Version Numbers) + +## 需要測試的邊界 +- [x] 空輸入 / 單一元素:`""`、`"0"` +- [x] 重複元素 / 全相同:`"1.0.0"` vs `"1"` +- [x] 極值(最小/最大):`"2147483647"` vs `"2147483646"` +- [x] 含負數 / 0 / 大數:題目無負數,已覆蓋多零與大修訂號 +- [x] 大資料量(接近上限):200 段版本字串 + +## 額外案例 +### 案例 1 +- 輸入:`version1 = "", version2 = "0"` +- 預期:`0` +- 說明:空字串缺少所有段,視為全 0 + +### 案例 2 +- 輸入:`version1 = "2147483647", version2 = "2147483646"` +- 預期:`1` +- 說明:驗證最大 32-bit 整數段處理 + +### 案例 3 +- 輸入:`version1 = "0.0.0", version2 = "0"` +- 預期:`0` +- 說明:多段全 0 與單段 0 視為相同 + +### 案例 4 +- 輸入:`version1 = string.Join(".", Enumerable.Repeat("0", 199)) + ".1"`, `version2 = string.Join(".", Enumerable.Repeat("0", 200))` +- 預期:`1` +- 說明:長度 200 的版本字串,在最後一段差異才分勝負