2016年12月21日 星期三

[Java/Excel] 用 Eratosthenes Sieve (和 Euler Sieve) 求質數表,以及因式分解

上星期想破頭的數論之後,這星期都在看質數相關的Coding。

雖然最終要求的是:$ x^2+y^2 =N $ 的整數解,過程中也回顧了 Eratosthenes's Sieve 求質數表,和用質數表做因式分解 的方法。所用的語言方面,一個目標是自己想要熟習,而且資源又常見的Java;另一個是如果自己在中學時期想這樣做的話,應該會用到的VBA。

Java的例子不少,但試過才知道即使實現的是同一套算法,效率也可以有很大差別。從前只知道應用數學中會看一套算法的Big O去比較時間複雜度,但原來實踐起來用什麼語言、什麼變數物件,結果用什麼什麼形式去表現⋯⋯都會大大影響速度。就是為了找出實際運行得更快的寫法,就一頭栽到這個質數的做法上。雖然一開始時用LinkedList、Map等寫法真的很方便理解,但之後還是儘量換成基本的陣列。

下面這樣的Java寫的  Eratosthenes Sieve 求 一億($10^8$)之內的質數,大約是10秒。如果不計最後一個for loop用來製作傳回值的LinkedList<Integer>的話,主流程大約4秒。不過要求更大的質數,應該還要考慮該程式對使用整數的上限(int: [-2,147,483,648, 2,147,483,647]),大數字在記憶體的儲存方法,整數表的儲存方法等。很多事情都是這樣吧,開始時只要求做到是容易的,但要追求深究下去就會困難⋯⋯

最後還有兩個方法是Modified Eratosthenes Sieve 和 Euler Sieve,都是理論上應該更有效率的算法,不過實踐起來還是達不到應有的分別,要怪我對如何更好地寫代碼還是不熟識吧。 Eratosthenes Sieve的Modification的想法是一開始就不考慮2和3的倍數,只看6k+1和6k+5的情況。至於 Euler Sieve 是改善Eratosthenes Sieve中一個合成數會重覆被質數篩去而浪費的時間,例如,合成數6 會在篩選質數 2和3 的倍數時重覆考慮。理想中Eratosthenes Sieve的時間複雜度是 O( n*log(log(n)) ),而Euler Sieve的時間複雜度是O(n)。實測中,同樣求一億($10^8$)之內的質數,大約是2-3秒的時間。


JAVA:

Prime.java
package JGT;
import java.util.*;

public class Prime {

    // 用 Eratosthenes Sieve 求質數表
    public static List<Integer> Eratosthenes(int max)
    {    
        boolean[] a = new boolean[max+1];
        LinkedList<Integer> primes = new LinkedList<>();
        
        for (int i=2; i<=max; i++)  a[i] = true; 
        for (int i=2; i<=Math.sqrt(max); i++) {
            if (a[i]) 
                for (int j=i; j*i<=max; j++) 
                    a[j*i] = false;
        }
        
        for (int i = 0; i <= max; i++) {
            if (a[i]) primes.add(i);
        }
        return primes;
    } 
    
    // 選擇求質數表的方法,因為之後還有其他方法
    public static List<Integer> create(int max) {
        return Eratosthenes(max);  // return Euler(max); 
    }
    
    // 因式分解()
    public static List<Integer> factorize(int n) {
        final List<Integer> Primes;
        Primes = Prime.create(n / 2);
        return factorize(n, Primes);
    }

    // 因式分解),提供預先準備的質數表
    public static List<Integer> factorize(int n, List<Integer> primes) {
        List<Integer> factors = new ArrayList<>();
        
        if (primes.size() <= Math.sqrt(n)) {
            primes = Prime.create(n / 2);
        }
        for(int i = 0; primes.get(i) <= Math.sqrt(n);) { 
            if(n % primes.get(i) == 0) { 
                factors.add(primes.get(i));
                n /= primes.get(i); 
            } else { i++; }
        } 
        factors.add(n);      
        return factors;
    }
    
}

測試用的 PrimeFactorize.java,將結果顯示到Console和檔案:
package JGT;
import java.util.*;
import java.io.*;

public class PrimeFactorize {
    public static void main(String[] args) {
        final int N = 1000000;
        List<Integer> primes, factors = new ArrayList<>();
        long startTime, endTime;
       
        // 用 Eratosthenes Sieve 求質數表
        startTime = System.currentTimeMillis();
        primes = Prime.Eratosthenes(N);
        endTime = System.currentTimeMillis();
        System.out.println("Execution time: " + (endTime - startTime) + " ms." );
        try {
            FileWriter fw = new FileWriter("primes.txt");
            for (Object p : primes) fw.write(p +"\n");
            fw.close();
        } catch (IOException e) {}
       
        // 求因式分解(一)
        startTime = System.currentTimeMillis();
        factors = Prime.factorize(1000545);
        endTime = System.currentTimeMillis();
        System.out.println("Execution time: " + (endTime - startTime) + " ms." );
        for(Integer f : factors) { System.out.printf("%d ", f); }
        System.out.println();
       
        // 因式分解),提供預先準備的質數表
        startTime = System.currentTimeMillis();
        factors = Prime.factorize(1000545, primes);
        endTime = System.currentTimeMillis();
        System.out.println("Execution time: " + (endTime - startTime) + " ms." );
        for(Integer f : factors) { System.out.printf("%d ", f); }
        System.out.println();
     }
}

VBA:

' 用 Eratosthenes Sieve 求質數表
 Public Function Eratosthenes(max As Integer) As Variant
    Dim a() As Boolean
    Dim primes() As Integer

    For i = 2 To max
        ReDim Preserve a(1 To i)
        a(i) = True
    Next i
    For i = 2 To Sqr(max)
        For j = i * i To max Step i
            a(j) = False
        Next j
    Next i
    For i = 2 To max
       If a(i) Then
            primeCount = primeCount + 1
            ReDim Preserve primes(1 To primeCount)
            primes(primeCount) = i
       End If
    Next i

    Eratosthenes = primes
End Function

' 求因式分解
Public Function Factorize(n As Integer) As Variant
    Dim factors() As Integer

    primes = Eratosthenes(n / 2)
    factorCount = 0
    For Each i In primes
        If (n Mod i = 0) Then
            Do While (n Mod i = 0)
                factorCount = factorCount + 1
                ReDim Preserve factors(1 To factorCount)
                factors(factorCount) = i
                n = n / i
            Loop
        End If
    Next i

    Factorize = factors
End Function

測試用的 Sub-procedure,將結果顯示到Column A, B:
Sub Testing()
    primes = Eratosthenes(100)
    For i = 1 To (UBound(primes) - LBound(primes) + 1)
        Range("A" & i).Value = primes(i)
    Next i
    
    factors = Factorize(100)
    For i = 1 To (UBound(factors) - LBound(factors) + 1)
        Range("B" & i).Value = factors(i)
    Next i
End Sub

其他:

另外兩套求質數表的做法。
當中Modified Eratosthenes Sieve 是按 這篇 寫的,而Euler Sieve是按 這篇 的pseudo-code寫的。

Euler Sieve中留意 prime[j] < i 的關係、和這句:if (i % primeList[j] == 0) break;  讓合成數只會被它最小的一個質因數篩走。

當 i 可以整除 質數 primeList[j] 的時候,會跳出最內層的 for loop 而不必浪費時間去篩走 i 的倍數,因為它們應該在篩走 質數 primeList[j] 的倍數時已經處理了。如果用藍色表示會被篩走的合成數當時的(i * prime[j]),刪除線表示因為跳出 for-loop 而去略去的組合,大概是這樣?

i : prime[j]
1:
2: 2
3: 2, 3
4: 2, 3
5: 2, 3, 5
6: 2, 3, 5
7: 2, 3, 5, 7
8: 2, 3, 5, 7
9: 2, 3, 5, 7
10: 2, 3, 5, 7
11: 2, 3, 5, 7, 11
...
15: 2, 3, 5, 11, 13 
...

// 用 修改的 Eratosthenes Sieve 求質數表
public static List<Integer> Eratosthenes2(int max) {
    LinkedList<Integer> primes = new LinkedList<>();
    boolean[] a = new boolean[max+1];
    
    a[2]=true; a[3]=true; a[5]=true;
    for(int i=1; i<=(max-5)/6; i++) {
        a[6*i+1] = true; a[6*i+5] = true;
    }
    if( (max-5)/6+2 <= max)  a[(max-5)/6+2]=true;
    for (int i=2; i<=Math.sqrt(max); i++) {
        if (a[i])
            for (int j=i; j*i<=max; j++) a[j*i] = false;
    }
    
    for (int i=0; i<=max; i++) {
        if (a[i]) primes.add(i);
    }
    return primes;
}

// 用 Euler Sieve 求質數表
public static List<Integer> Euler(int max)
{    
    boolean[] a = new boolean[max+1];
    int[] primeList = new int[(int)( max/(Math.log(max)-4) )];
    int primeCount = 0;
    LinkedList<Integer> primes = new LinkedList<>();

    for (int i=2; i<=max; i++)  a[i] = true;         
    for (int i=1; i<=max; i++) {
        if (a[i]) primeList[primeCount++] = i;
        for (int j = 0; j < primeCount; j++) {
            if (i * primeList[j] > max) break;
            a[i * primeList[j] ] = false;
            if (i % primeList[j] == 0) break;
        }
    }
    for (int i=0; i<=max; i++) {
        if (a[i]) primes.add(i);
    }
    return primes;
} 





沒有留言:

張貼留言