[#0165]feat(leetcode): Init project, add C# solution, unit test and README
This commit is contained in:
85
problems/0165-compare-version-numbers/README.md
Normal file
85
problems/0165-compare-version-numbers/README.md
Normal 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 與缺段情況,適合練習字串雙指標與記憶體優化技巧。
|
144
problems/0165-compare-version-numbers/csharp/Program.cs
Normal file
144
problems/0165-compare-version-numbers/csharp/Program.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
||||||
|
|
56
problems/0165-compare-version-numbers/test/SolutionTests.cs
Normal file
56
problems/0165-compare-version-numbers/test/SolutionTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
||||||
|
|
29
problems/0165-compare-version-numbers/test/edge_cases.md
Normal file
29
problems/0165-compare-version-numbers/test/edge_cases.md
Normal 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 的版本字串,在最後一段差異才分勝負
|
Reference in New Issue
Block a user