用歐拉計劃學Rust語言(第17~21題)

最近想學習Libra數字貨幣的MOVE語言,發現它是用Rust編寫的,因此先補一下Rust的基礎知識。學習了一段時間,發現Rust的學習曲線很是陡峭,不過仍有快速入門的辦法。git

學習任何一項技能最怕沒有反饋,尤爲是學英語、學編程的時候,必定要「用」,學習編程時有一個很是有用的網站,它就是「歐拉計劃」,網址: https://projecteuler.net編程

這個網站提供了幾百道由易到難的數學問題,你能夠用任何辦法去解決它,固然主要還得靠編程,編程語言不限,論壇裏已經有Java、C#、Python、Lisp、Haskell等各類解法,固然若是你直接用google搜索答案就沒任何樂趣了。數組

學習Rust最好先把基本的語法和特性看過一遍,而後就能夠動手解題了,解題的過程就是學習、試錯、再學習、掌握和鞏固的過程,學習進度會大大加快。性能優化

第17題

問題描述:編程語言

1到1000用英文單詞寫下來,求總字符個數(空格和連字符不算),例如:342,英文單詞是:three hundred and forty-two。函數式編程

問題分解:函數

  • 數字轉換成英文單詞
    • 1到19的拼寫
    • 20到99的拼寫
    • 100到999的拼寫
    • 1000的拼寫
  • 單詞中去掉空格和連字符
  • 取字符總數

1到19的拼寫比較特殊,須要分別對待,而超過20的數,能夠利用遞歸調用。這裏能夠學到String的語法知識點。Rust中的字符串有點煩人,list[n].to_string()、"one thousand".to_string()的這種寫法讓人很是不適應。除了String以外,還有字符串切片(slice)、字符常量,須要看基礎教程慢慢理解。性能

fn english_number(n: usize) -> String {
    let list0_9 = vec![
        "zero", "one", "two", "three", "four", 
        "five", "six", "seven", "eight", "nine",
    ];
    if n <= 9 {
        return list0_9[n].to_string();
    }
    if n <= 19 {
        let list = vec![
            "ten", "eleven", "twelve", "thirteen", "fourteen",
            "fifteen", "sixteen", "seventeen", "eighteen", "nineteen",
        ];
        return list[n - 10].to_string();
    }
    if n <= 99 {
        let a: usize = n / 10; // 十位
        let b: usize = n % 10;
        let list = vec![
            "", "", "twenty", "thirty", "forty", 
            "fifty", "sixty", "seventy", "eighty", "ninety"
        ];
        let str = list[a].to_string();
        if b > 0 {
            return str + "-" + &english_number(b);
        }
        return str;
    }
    if n <= 999 {
        let a: usize = n / 100; // 百位
        let b: usize = n % 100;
        let str = list0_9[a].to_string() + " hundred";
        if b > 0 {
            return str + " and " + &english_number(b);
        }
        return str;
    }
    if n == 1000 {
        return "one thousand".to_string();
    }
    return "unknown".to_string();
}

從字符串裏移除特定的字符,要利用函數式編程,還有filter()和collect()函數,一鼓作氣。filter()函數中的*c又是讓人寫錯的地方。學習

fn remove_space(s: &str) -> String {
    s.chars().filter(|c| *c != ' ' && *c != '-').collect()
}

主程序就比較容易了,求和便可。優化

let mut sum = 0;
for n in 1..=1000 {
    let s = remove_space(&english_number(n));
    sum += s.len();
    // println!("{}: {} {}", n, english_number(n), s.len());
}
println!("{}", sum);

第18題

問題描述:

從堆成三角的數字中,找到一條路徑,使其和最大,求和。一個節點的下一個點只能是下一層的左、右節點。

image

爲了節省內存空間,用一維數組表示這些數,須要準確地計算出各個索引位置的行號,我爲了方便,最上一層的行號爲1。

let w = [
                                75, // row 1
                              95, 64,  // row 2
                            17, 47, 82,  // row 3
                          18, 35, 87, 10,
                        20, 04, 82, 47, 65,
                      19, 01, 23, 75, 03, 34,
                    88, 02, 77, 73, 07, 63, 67,
                  99, 65, 04, 28, 06, 16, 70, 92,
                41, 41, 26, 56, 83, 40, 80, 70, 33,
              41, 48, 72, 33, 47, 32, 37, 16, 94, 29,
            53, 71, 44, 65, 25, 43, 91, 52, 97, 51, 14,
          70, 11, 33, 28, 77, 73, 17, 78, 39, 68, 17, 57,
        91, 71, 52, 38, 17, 14, 91, 43, 58, 50, 27, 29, 48,
      63, 66, 04, 68, 89, 53, 67, 30, 73, 16, 69, 87, 40, 31,
    04, 62, 98, 27, 23, 09, 70, 98, 73, 93, 38, 53, 60, 04, 23,
];

在數組中的第n個數,行號是多少?利用了第r行必定有r個數的性質。

fn row(n: usize) -> usize {
    let mut s = 0;
    for r in 1.. {
        s += r;
        if s > n {return r;}
    }
    return 0;
}

根據上面行號的性質,能夠得出節點n的下一層的左節點的編號是 n + r,右節點是 n + r + 1。

求路徑的和的時候能夠利用遞歸,終止條件是遇到最底一層的時候,因爲原題只讓求路徑長度,這裏沒有記下來所走路徑的編號。

fn max_path_weight(w: &[u32], n: usize) -> u32 {
    if n >= w.len() {return 0;} //越界判斷
    let r = row(n);
    let bottom_row = row(w.len() - 1);
    if r == bottom_row { // 遞歸的退出條件
        return w[n];
    }
    let left = max_path_weight(w, n + r);
    let right = max_path_weight(w, n + r + 1);
    let max = if left > right {left} else {right};
    return w[n] + max;
}

主程序調用只須要一行,數組的總層數很少,複雜度不高,沒再作進一步的性能優化。

println!("{}", max_path_weight(&w, 0));

第19題

問題描述:

求20世紀(1901年1月1日到2000年12月31日)有多少個月的一號是星期日?

本題固然能夠利用閏年的性質,只用數學公式就能算出來,這裏用編程辦法,熟悉一下日期時間的函數庫。

關於日期的庫用chrono,網上有些資料比較老,建議直接參考官網上的幫助,寫得很是詳細,少走一些彎路。

在https://docs.rs 網站上搜索chrono便可。

use chrono::prelude::*;
use time::Duration;

代碼簡單粗暴,每次加一天,用到Duration;判斷星期幾用到weekday(),判斷幾號用了day(),邏輯很簡單。

let mut count = 0;
let mut d = Utc.ymd(1901, 1, 1);
while d <= Utc.ymd(2000, 12, 31) {
    if d.weekday() == Weekday::Sun && d.day() == 1 {
        println!("{}", d);
        count += 1;
    }
    d = d + Duration::days(1);
}
println!("{}", count);

第20題

問題描述:

求100的階乘中全部數字之和。

本題與第16題很是類似,稍微修改就出來,不解釋。

let mut prod = BigUint::from(1 as u64);
for i in 1..=100 {
    prod *= BigUint::from(i as u64);
}

let full_str = prod.to_str_radix(10);
let s = full_str
    .chars()
    .map(|c| c.to_digit(10).unwrap())
    .sum::<u32>();
println!("{}", s);

第21題

問題描述:

10000以內的全部親和數之和。所謂親和數,是指兩個正整數中,彼此的所有約數之和(自己除外)與另外一方相等。好比,220的因子有1, 2, 4, 5, 10, 11, 20, 22, 44, 55 和 110,因子之和是 284,而284的全部因子是 1, 2, 4, 71 和 142,因子之和是220。

問題分解:

  • 求全部因子
  • 因子求和
  • 找出親和數
  • 親和數再求和

在第12題裏已經求出了一半的因子,函數是:

fn half_factors(num: u32) -> Vec<u32> {
    let s = (num as f32).sqrt() as u32;
    (1..=s).filter(|x| num % x == 0).collect::<Vec<u32>>()
}

很容易補上後面的一半因子。

fn proper_divisors(num: u32) -> Vec<u32> {
    let mut v = half_factors(num);
    for i in (1..v.len()).rev() { //不要num自身,因此從1開始
        v.push(num / v[i]);
    }
    v
}

全部因子求和:

fn proper_divisors_sum(num: u32) -> u32 {
    let divs = proper_divisors(num);
    divs.iter().sum::<u32>()
}

主程序暴力循環便可,10000以內只有5對親和數:

let mut sum = 0;
for a in 1u32..10000 {
    let b = proper_divisors_sum(a);
    if a != b && proper_divisors_sum(b) == a {
        sum += a;
        println!("{} {}", a, b);
    }
}
println!("{}", sum);

由於親和數是成對出現的,還能夠優化性能,引入一個數組,這裏再也不展開了。


在projecteuler中註冊一個帳號,能夠添加好友,一塊兒討論學習,個人Key是:

1539870_KBNiIXymh4SnmDEDZmUTg7tu1MTBVlLj

近期文章:

相關文章
相關標籤/搜索