2025年9月23日 星期二

字串分割

今日的 165. Compare Version Numbers Solved 使用 Python3 五分鐘就可以解掉了,主要的思考套路是設法弄出兩個等長的整數陣列來比較就可以了。

class Solution:
    def compareVersion(self, version1: str, version2: str) -> int:
        ver1 = [int(x) for x in version1.split(".")]
        ver2 = [int(x) for x in version2.split(".")]
        for x, y in zip_longest(ver1, ver2, fillvalue=0):
            if x < y:
                return -1
            elif x > y:
                return 1
        return 0

如果要用 C++ 就會有點囉唆,要使用 stringstream 搭配 getline 才行,不是那麼直覺,之後再用 resize 調整成一樣的長度來比較。

class Solution {
    vector&lt;int&gt; parse(string version) {
    	string num;
        stringstream ss(version);
        vector&lt;int&gt; ver;
        while (getline(ss, num, '.'))
            ver.emplace_back(stoi(num));
        return ver;
    }

public:
    int compareVersion(string version1, string version2) {
        auto ver1 = parse(version1);
        auto ver2 = parse(version2);
        int len = max(ver1.size(), ver2.size());
        ver1.resize(len, 0);
        ver2.resize(len, 0);

        for (int i = 0; i &lt; len; ++i) {
        	if (ver1[i] &lt; ver2[i])
            	return -1;
            else if (ver1[i] &lt; ver2[i])
            	return 1;
        }

        return 0;
    }
};

如果是 Go 要熟悉一下內建的 strings.Split 以及 strconv.Atoi

func compareVersion(version1 string, version2 string) int {
	str1 := strings.Split(version1, ".")
	str2 := strings.Split(version2, ".")
	length := max(len(str1), len(str2))
	ver1 := make([]int, length)
	ver2 := make([]int, length)
	for i, s := range str1 {
		num, _ := strconv.Atoi(s)
		ver1[i] = num
	}
	for i, s := range str2 {
		num, _ := strconv.Atoi(s)
		ver2[i] = num
	}
	for i := range length {
		if ver1[i] &lt; ver2[i] {
        	return -1
		} else if ver1[i] &gt; ver2[i] {
        	return 1
		}
	}
	return 0
}

如果是 Rust 要懂得使用 split, filter_map, collect 的串接組合技,跟 trim, parse, ok 來將字串轉換成整數,以及用 resize 將陣列調整成一樣長度,再用跟 Python 一樣的 zip 結合起來一個一個比較。

impl Solution {
    pub fn compare_version(version1: String, version2: String) -> i32 {
        let mut ver1: Vec&lt;i32&gt; = version1.split('.').filter_map(|s| s.trim().parse::&lt;i32&gt;().ok()).collect();
        let mut ver2: Vec&lt;i32&gt; = version2.split('.').filter_map(|s| s.trim().parse::&lt;i32&gt;().ok()).collect();
        let len = ver1.len().max(ver2.len());
        ver1.resize(len, 0);
        ver2.resize(len, 0);
        for (x, y) in ver1.into_iter().zip(ver2) {
            if x &lt; y {
            	return -1;
            } else if x &gt; y {
            	return 1;
            }
        }
        0
    }
}

2025年9月17日 星期三

C++ 自製的 gcd 比標準函式庫提供的跑得還要快?

今天在用 C++ 重複練習 2197. Replace Non-Coprime Numbers in Array 一遍,試著寫出跑得更快的版本,發現自己寫出來的 gcd 在 LeetCode 平台上反而跑得比較快,目前還不曉得背後到底是怎麼一回事,先記錄一下,目前想到的做法是盡量避免呼叫 gcd 以及盡量使用已使用的記憶體空間來加速,最好的時候可以跑到 Runtime 2ms Beats 98.62% 的成績。

class Solution {
    int gcd(int a, int b) {
        if (b > a)
            swap(a, b);
        while (b != 0) {
            a %= b;
            swap(a, b);
        }
        return a;
    }

public:
    vector&lt;int&gt; replaceNonCoprimes(vector&lt;int&gt;& nums) {
        int idx = 0;
        for (int i = 1; i < nums.size(); ++i) {
            int num = gcd(nums[idx], nums[i]);
            if (num == 1)
                nums[++idx] = nums[i];
            else
                nums[idx] = nums[idx] / num * nums[i];
            for (int j = idx; j > 0; --j) {
                int num = gcd(nums[j], nums[j - 1]);
                if (num == 1)
                    break;
                nums[j - 1] *= nums[j] / num;
                --idx;
            }
        }
        nums.resize(idx + 1);
        return nums;
    }
};

2025年9月16日 星期二

Rust 沒官方標準支援 gcd 跟 lcm 的 traits 蠻令人意外的

今天在練習 2197. Replace Non-Coprime Numbers in Array 時,需要使用到 GCD 跟 LCM,在 C++/Python 都有官方支援,Rust 跟 Go 都沒有支援,有點意外。

impl Solution {
    pub fn replace_non_coprimes(nums: Vec&lt;i32>) -> Vec&lt;i32> {
        let mut arr = Vec::with_capacity(nums.len());
        let mut curr = nums[0];
        let gcd = |mut a: i32, mut b: i32| -> i32 {
            if a < b {
                (a, b) = (b, a);
            }
            while b != 0 {
                (a, b) = (b, a % b);
            }
            a
        };
        let lcm = |a: i32, b: i32| -> i32 {
            a / gcd(a, b) * b
        };
        for &num in &nums[1..] {
            if gcd(num, curr) > 1 {
                curr = lcm(num, curr);
                while let Some(&last) = arr.last() && gcd(curr, last) > 1 {
                    curr = lcm(last, curr);
                    arr.pop();
                }
            } else {
                arr.push(curr);
                curr = num;
            }
        }
        arr.push(curr);
        arr
    }
}

這裡還剛好可以使用到 Rust 1.88 版本才開始有支援的 Let chainsLet chains 讓程式碼的表示更直白,真是太棒了。

if let Channel::Stable(v) = release_info()
    && let Semver { major, minor, .. } = v
    && major == 1
    && minor == 88
{
    println!("`let_chains` was stabilized in this version");
}

2025年9月6日 星期六

基於四底對數的整數級數求和

***此篇文章由 Gemini AI 產生*** ### **問題定義** 我們的目標是計算一個特定總和 `S`,這個總和是由一個函數 `f(i) = ⌊log₄(i)⌋ + 1` 對所有從 1 到 `4^p - 1` 的整數 `i` 的值累加而成。其數學表達式為: $$S_p = \sum_{i=1}^{4^p-1} \left( \lfloor \log_4(i) \rfloor + 1 \right)$$ 這裡的 `p` 是一個正整數。 --- ### **公式推導** 直接計算這個總和很困難,關鍵在於觀察函數 `f(i)` 的特性。`f(i)` 的值在一系列連續的整數區間內是恆定的,這些區間由 4 的次方所界定。 #### **步驟一:級數的轉換** 1. **分析函數值**: * 對於任何落在區間 $[4^k, 4^{k+1} - 1]$ 內的整數 `i`,`log₄(i)` 的值會介於 `k` 與 `k+1` 之間 (即 `k ≤ log₄(i) < k+1`)。 * 因此,`⌊log₄(i)⌋` 的值恆為 `k`。 * 所以,對於這個區間內的所有 `i`,函數 `f(i)` 的值恆為 `k+1`。 2. **計算區間內的整數數量**: * 在區間 $[4^k, 4^{k+1} - 1]$ 中,整數的數量為: $$(4^{k+1} - 1) - 4^k + 1 = 4^{k+1} - 4^k = 4^k(4-1) = 3 \cdot 4^k$$ 3. **重寫總和表達式**: * 我們的總和 $S_p$ 是加總到 $4^p - 1$,這恰好涵蓋了從 `k=0` 到 `k=p-1` 的所有完整區間。 * 因此,我們可以將原本對 `i` 的求和,轉化為對 `k` 的分組求和: $$S_p = \sum_{k=0}^{p-1} (\text{區間內函數的值}) \times (\text{區間內的整數數量})$$ $$S_p = \sum_{k=0}^{p-1} (k+1) \cdot (3 \cdot 4^k)$$ #### **步驟二:求解等比等差數列** 我們可以將常數 3 提出,得到一個**等比等差數列**的和: $$S_p = 3 \cdot \sum_{k=0}^{p-1} (k+1) \cdot 4^k$$ 令核心部分為 `A`: $$A = \sum_{k=0}^{p-1} (k+1) \cdot 4^k = 1 \cdot 4^0 + 2 \cdot 4^1 + 3 \cdot 4^2 + \dots + p \cdot 4^{p-1}$$ 我們使用**錯位相減法**來求解 `A`。首先將 `A` 乘以級數的公比 4: $$4A = 1 \cdot 4^1 + 2 \cdot 4^2 + \dots + (p-1) \cdot 4^{p-1} + p \cdot 4^p$$ 接著,將 `A` 減去 `4A`: $$-3A = (1 \cdot 4^0 + 2 \cdot 4^1 + \dots + p \cdot 4^{p-1}) - (1 \cdot 4^1 + 2 \cdot 4^2 + \dots + p \cdot 4^p)$$ 合併同類項後,算式變為: $$-3A = 1 \cdot 4^0 + (2-1) \cdot 4^1 + (3-2) \cdot 4^2 + \dots + (p - (p-1)) \cdot 4^{p-1} - p \cdot 4^p$$ 這可以簡化為一個等比級數與一個單項: $$-3A = (4^0 + 4^1 + 4^2 + \dots + 4^{p-1}) - p \cdot 4^p$$ 括號中的部分是一個首項為 1,公比為 4,共有 `p` 項的等比數列。其和為 $\frac{4^p - 1}{4 - 1} = \frac{4^p - 1}{3}$。代入上式: $$-3A = \frac{4^p - 1}{3} - p \cdot 4^p$$ 為了求解 `A`,我們繼續整理。兩邊同乘以 -1: $$3A = p \cdot 4^p - \frac{4^p - 1}{3}$$ 通分整理後: $$3A = \frac{3p \cdot 4^p - (4^p - 1)}{3}$$ 最終得到: $$3A = \frac{(3p - 1) \cdot 4^p + 1}{3}$$ --- ### **最終公式** 我們最初的目標是 $S_p = 3 \cdot A$。將 `A` 的結果代入,即可得到該總和的封閉形式解(closed-form solution): $$S_p = \frac{(3p - 1) \cdot 4^p + 1}{3}$$ 這個公式允許我們直接代入 `p` 來獲得最終結果,而無需進行迭代求和。 --- ### **範例驗證 (p=2)** 現在我們設定 `p = 2` 來驗證公式的準確性。我們要計算從 `i = 1` 到 `4² - 1 = 15` 的總和: $$S_2 = \sum_{i=1}^{15} \left( \lfloor \log_4(i) \rfloor + 1 \right)$$ #### **方法一:直接套用公式** 將 `p = 2` 代入我們推導出的公式,逐步計算: $$S_2 = \frac{(3 \cdot 2 - 1) \cdot 4^2 + 1}{3}$$ $$S_2 = \frac{(6 - 1) \cdot 16 + 1}{3}$$ $$S_2 = \frac{5 \cdot 16 + 1}{3}$$ $$S_2 = \frac{80 + 1}{3}$$ $$S_2 = \frac{81}{3} = 27$$ 公式計算出的結果是 **27**。 #### **方法二:分組逐項計算** 我們也可以將 1 到 15 的數字分組來計算實際總和: 1. **第一組 (k=0):** 當 `i` 在區間 `[1, 3]` * 包含的數字:1, 2, 3 (共 3 個) * 對於這些數字,`⌊log₄(i)⌋ + 1` 的值皆為 `0 + 1 = 1`。 * 此組的總和為: 3 個數字 × 1 = 3 2. **第二組 (k=1):** 當 `i` 在區間 `[4, 15]` * 包含的數字:4, 5, ..., 15 (共 `15 - 4 + 1 = 12` 個) * 對於這些數字,`⌊log₄(i)⌋ + 1` 的值皆為 `1 + 1 = 2`。 * 此組的總和為: 12 個數字 × 2 = 24 **總和** = 第一組總和 + 第二組總和 = `3 + 24 = 27`。 兩種方法得到的結果 **27** 完全相同。

2025年9月5日 星期五

想算數字裡有幾個 1?C++, Python3, Go, Rust 大亂鬥!

***此篇文章由 Gemini AI 產生*** 哈囉,各位寫 code 的夥伴們,大家好! 今天我們來聊一個面試或刷題時偶爾會碰到,但其實也蠻有趣的小問題:「怎麼計算一個數字在二進位下有幾個 1?」 聽起來好像很學術,但別擔心,其實觀念很簡單。比方說,數字 5,寫成二進位是 `101`,那它就有兩個 1。數字 7,二進位是 `111`,就有三個 1。這在某些演算法或需要操作底層位元(bit)的場景,其實還蠻實用的。 今天我們就來看看用四種現在很夯的語言:C++, Python3, Go, 和 Rust,要怎麼解決這個問題。順便比較一下它們的寫法和「個性」有什麼不一樣。 #### C++:硬底子真功夫,效能我最強 C++ 就是那個班上最認真、對所有細節都一清二楚的學霸。要解決這個問題,C++ 提供了一個超級方便的內建函式(built-in function),不過在比較舊的 C++ 版本可能需要自己手刻。 在比較新的 C++ 標準 (C++20) 中,你可以直接用 `` 這個函式庫: ```cpp #include #include int main() { int num = 77; // 77 的二進位是 1001101 // std::popcount 會直接幫你數好有幾個 1 int count = std::popcount(static_cast<unsigned int>(num)); std::cout << "數字 " << num << " 裡有 " << count << " 個 1!" << std::endl; // 輸出: 數字 77 裡有 4 個 1! return 0; } ``` **重點分析:** * **`std::popcount`**: 這名字超直白,「pop」在這裡指的是 "population"(總數),所以 `popcount` 就是「計算總數」。這是編譯器等級的優化,速度快到飛起來。 * **`static_cast<unsigned int>`**: 這是在做「型別轉換」。因為 `popcount` 通常是針對無號整數 (unsigned) 操作的,所以我們先把 `num` 轉成無號整數,避免一些奇怪的問題。 C++ 的寫法就是這麼樸實無華且枯燥,但效能絕對是頂尖的! #### Python3:人生苦短,我用 Python Python 就是那個朋友群裡最會聊天的萬人迷,語法親切又好懂。解決這個問題,方法多到你可以自己選。 最直覺的方法,就是直接把它變成二進位字串,然後數裡面有幾個 '1'。 ```python num = 77 # 77 的二進位是 1001101 # bin(num) 會回傳 '0b1001101' # 我們用 .count('1') 來數 '1' 的數量 count = bin(num).count('1') print(f"數字 {num} 裡有 {count} 個 1!") # 輸出: 數字 77 裡有 4 個 1! ``` 如果你是 Python 3.10 以上的版本,還可以用一個更潮的寫法: ```python num = 77 count = num.bit_count() # 直接呼叫! print(f"數字 {num} 裡有 {count} 個 1!") # 輸出: 數字 77 裡有 4 個 1! ``` **重點分析:** * **`bin()`**: Python 的內建函式,可以馬上把數字轉成 `0b` 開頭的二進位字串,超方便。 * **`.count('1')`**: 字串的 method,用來計算某個字元出現的次數。 * **`.bit_count()`**: 在新版 Python 中,整數自己就有這個 method,寫起來更乾淨,而且底層的效能也很好。 Python 的寫法就是這麼優雅,幾行就搞定,可讀性超高。 #### Go:簡單、可靠,Google 親兒子 Go 語言給人的感覺,就像是個務實的工程師,不喜歡花俏的東西,但該有的都有,而且跑起來穩定又快速。 Go 在標準函式庫 `math/bits` 裡面,也直接提供了需要的功能。 ```go package main import ( "fmt" "math/bits" ) func main() { var num uint = 77 // 77 的二進位是 1001101 // Go 推薦使用無號整數 (uint) 來做位元運算 count := bits.OnesCount(num) fmt.Printf("數字 %d 裡有 %d 個 1!\n", num, count) // 輸出: 數字 77 裡有 4 個 1! } ``` **重點分析:** * **`import "math/bits"`**: Go 把跟位元操作相關的功能都整理在這個 package 裡,分工很清楚。 * **`bits.OnesCount()`**: 函式名稱也是簡單明瞭,就是「計算 1 的數量」。 * **`var num uint = 77`**: Go 是一個強型別語言,它會建議你,在做位元運算時,最好一開始就宣告成無號整數 (`uint`),這樣語意更明確。 Go 的風格就是這樣,清楚、直接,而且效能也很棒。 #### Rust:安全、並行,程式界的超級英雄 Rust 是這幾年超級紅的語言,主打的就是「安全」跟「極致效能」。它給人的感覺有點像 C++ 的進化版,嚴格但能讓你寫出非常可靠的程式碼。 Rust 在整數型別上,也直接內建了計算 1 數量的方法。 ```rust fn main() { let num = 77_i32; // 77 的二進位是 1001101 // _i32 是告訴編譯器,這是一個 32 位元的整數 // 直接在數字後面呼叫 .count_ones() let count = num.count_ones(); println!("數字 {} 裡有 {} 個 1!", num, count); // 輸出: 數字 77 裡有 4 個 1! } ``` **重點分析:** * **`_i32`**: 這是 Rust 的語法糖,用來標明數字的型別,`i32` 代表 32 位元有號整數。Rust 對型別非常嚴格,所以寫清楚是好習慣。 * **`.count_ones()`**: Rust 把這個功能直接做成整數型別的一個 method,只要是數字,後面加上 `.count_ones()` 就能用,非常直覺。 Rust 的寫法兼具了 C++ 的效能和 Python 的簡潔,而且還有編譯器這個超級保母在後面幫你檢查,讓你寫 code 超有安全感。 ----- ### 總結一下 | 語言 | 主要寫法 | 風格特色 | | --- | --- | --- | | **C++** | `std::popcount(num)` | 效能至上,語法較嚴謹 | | **Python3** | `bin(num).count('1')` 或 `num.bit_count()` | 語法甜美,可讀性高,快速開發 | | **Go** | `bits.OnesCount(num)` | 務實可靠,標準庫功能齊全 | | **Rust** | `num.count_ones()` | 安全第一,語法現代且效能強悍 | 今天這個小問題,其實四種語言都有非常簡單、高效的解法。從這些小地方,我們也可以稍微窺探出不同程式語言在設計哲學上的差異。 沒有最好的語言,只有最適合的工具。希望這篇簡單的比較,能幫助大家對這幾種語言有更具體的認識。下次不管你用哪種語言,碰到要數 1 的時候,就知道該怎麼做了吧! Happy coding!

2025年9月2日 星期二

C++/Python3/Go/Rust 關於 list/array/vector 的 sort 在設計上的不同差異

***此篇文章由 Gemini AI 產生*** 在軟體開發中,排序幾乎可以說是最常見的需求之一。然而,不同的程式語言在排序功能的設計上卻有著一些有趣的差異。這篇文章將帶您一探究竟,比較 C++、Python、Go 和 Rust 中 `list`、`array` 或 `vector` 等資料結構的排序(sort)功能,並透過程式碼範例,讓您更深入了解它們在設計上的不同之處。 ### C++:回傳布林值的比較函式 C++ 的 `std::ranges::sort`(以及舊版的 `std::sort`)在自訂排序行為時,需要傳入一個比較函式(Comparison Function),而這個函式必須回傳布林值(`bool`)。 這個比較函式通常接收兩個參數,並在這兩個參數之間進行比較。如果第一個參數應該排在第二個參數之前,則回傳 `true`,否則回傳 `false`。 ```cpp #include #include #include #include struct Person { std::string name; int age; }; int main() { std::vector<Person> people = { {"Alice", 30}, {"Bob", 25}, {"Charlie", 35} }; // 使用 lambda 運算式定義比較函式,按年齡升序排序 std::ranges::sort(people, [](const Person& a, const Person& b) { return a.age < b.age; }); for (const auto& person : people) { std::cout << person.name << " (" << person.age << ")" << std::endl; } return 0; } ``` **輸出結果:** ``` Bob (25) Alice (30) Charlie (35) ``` ### Python:提供 Key Function 提取比較值 Python 的 `list.sort()` 方法則提供了另一種更為簡潔的自訂排序方式。您可以傳入一個名為 `key` 的參數,這個 `key` 參數會接收一個函式,用來從每個元素中提取一個用於比較的「鍵」(key)。 這種設計的好處是,您不需要撰寫一個完整的比較函式,只需要提供一個簡單的函式來告訴 `sort()` 方法要用哪個值來進行排序即可。 ```python class Person: def __init__(self, name, age): self.name = name self.age = age people = [ Person("Alice", 30), Person("Bob", 25), Person("Charlie", 35) ] # 使用 lambda 函式作為 key,按年齡升序排序 people.sort(key=lambda person: person.age) for person in people: print(f"{person.name} ({person.age})") ``` **輸出結果:** ``` Bob (25) Alice (30) Charlie (35) ``` ### Go:回傳 -1, 0, 1 的比較函式 Go 語言的 `slices.SortFunc` 則採用了另一種常見的比較函式設計。您需要傳入一個比較函式,這個函式會回傳一個整數,用來表示兩個元素的相對順序: * **-1**:如果第一個元素應該排在第二個元素之前。 * **0**:如果兩個元素相等。 * **1**:如果第一個元素應該排在第二個元素之後。 ```go package main import ( "fmt" "slices" ) type Person struct { Name string Age int } func main() { people := []Person{ {"Alice", 30}, {"Bob", 25}, {"Charlie", 35}, } // 按年齡升序排序 slices.SortFunc(people, func(a, b Person) int { if a.Age < b.Age { return -1 } if a.Age > b.Age { return 1 } return 0 }) fmt.Println(people) } ``` **輸出結果:** ``` [{Bob 25} {Alice 30} {Charlie 35}] ``` ### Rust:兩種方式,任君挑選 Rust 在排序功能的設計上,可以說是集各家之大成,提供了兩種不同的自訂排序方式: 1. **`sort_unstable_by_key`**:類似於 Python 的 `key` 參數,您可以傳入一個函式來提取用於比較的鍵。 2. **`sort_unstable_by`**:類似於 Go 的比較函式,您可以傳入一個回傳 `Ordering`(一個包含 `Less`、`Equal` 和 `Greater` 三個值的枚舉)的函式。 ```rust #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] struct Person { name: String, age: u32, } fn main() { let mut people = vec![ Person { name: "Alice".to_string(), age: 30 }, Person { name: "Bob".to_string(), age: 25 }, Person { name: "Charlie".to_string(), age: 35 }, ]; // 1. 使用 sort_unstable_by_key 按年齡升序排序 people.sort_unstable_by_key(|p| p.age); println!("{:?}", people); // 2. 使用 sort_unstable_by 按年齡降序排序 people.sort_unstable_by(|a, b| b.age.cmp(&a.age)); println!("{:?}", people); } ``` **輸出結果:** ``` [Person { name: "Bob", age: 25 }, Person { name: "Alice", age: 30 }, Person { name: "Charlie", age: 35 }] [Person { name: "Charlie", age: 35 }, Person { name: "Alice", age: 30 }, Person { name: "Bob", age: 25 }] ``` ### 總結 | 語言 | 方法 | 回傳值 | | --- | --- | --- | | C++ | 比較函式 | `bool` | | Python | Key Function | 任何可比較的值 | | Go | 比較函式 | `-1`, `0`, `1` | | Rust | Key Function 或比較函式 | 任何可比較的值或 `Ordering` | 從以上比較可以看出,雖然排序是一個基本的功能,但不同的程式語言在 API 設計上卻有著不同的哲學和取捨。C++ 提供了最底層的控制,但也相對繁瑣;Python 則追求簡潔與易用性;Go 則採用了傳統的「三向比較」;而 Rust 則提供了最靈活的選擇,讓開發者可以根據自己的需求選擇最適合的方式。 希望這篇文章能幫助您更深入地了解不同程式語言在排序功能設計上的差異,並在未來的開發工作中,選擇最適合您需求的排序方式。