[#0165]feat(leetcode): Init project, add C# solution, unit test and README

This commit is contained in:
2025-09-23 10:59:18 +08:00
parent c4e23d5be3
commit 72aa011de9
6 changed files with 342 additions and 0 deletions

View File

@@ -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<char>` 抓取下一段,去除前導 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<char>` 可以在 C# 中避免額外配置
- 卡住與修正:原本 console app 移除 `Main` 造成 `dotnet test` 無法編譯,後來補回精簡入口
- 待優化:若要支援超長版本段,可考慮使用 `BigInteger` 或自訂比較邏輯(目前已以字元比較處理)
---
**總結**:核心在於逐段處理並正確處理前導 0 與缺段情況,適合練習字串雙指標與記憶體優化技巧。

View File

@@ -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<char> NextSegment(string version, ref int index)
{
if (index >= version.Length)
{
return ReadOnlySpan<char>.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<char> left, ReadOnlySpan<char> 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<char> TrimLeadingZeros(ReadOnlySpan<char> segment)
{
var index = 0;
while (index < segment.Length && segment[index] == '0')
{
index++;
}
return index == segment.Length ? ReadOnlySpan<char>.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);
}
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -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);
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../csharp/csharp.csproj" />
</ItemGroup>
</Project>

View File

@@ -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 的版本字串,在最後一段差異才分勝負