Sınırları Aşan Matematik: Kendi BigInt Kütüphanemizi Yazalım

sinirlari-asan-matematik-13

Bir tam sayının milyonlarca basamağı olsaydı onu nasıl toplardınız? Standart veri türleri işlemcinin belirlediği sınırlar içinde yaşarken BigInt yaklaşımı, sayıları küçük parçalara ayırarak bu duvarı aşar. Bu projede veri kaybına uğramadan dev sayıları saklayan, toplayan ve çarpan küçük bir kütüphanenin temelini kuracağız. ``

Standart tam sayılar neden yetmez?

Birçok dilde 64 bit işaretli tam sayıların aralığı şöyledir:

\[-2^{63} \leq n \leq 2^{63}-1\]

Bu sınır yaklaşık 19 ondalık basamağa karşılık gelir. Daha büyük bir değer taşmaya uğrayabilir veya dile bağlı olarak farklı bir türe dönüştürülebilir. Kayan noktalı sayılar geniş bir aralık sunsa da bütün basamakları kesin biçimde korumaz. Örneğin JavaScript’in klasik Number türünde güvenli tam sayı üst sınırı $2^{53}-1$ değeridir.

Yaklaşım Kapasite Kesinlik Temel maliyet
64 bit tamsayı Sabit Tam Çok düşük
Kayan nokta Çok geniş aralık Büyük sayılarda kayıplı Düşük
BigInt Bellekle sınırlı Tam Basamak sayısıyla artar

BigInt’in sırrı aslında şaşırtıcı derecede tanıdıktır: İlkokuldaki sütun işlemleri! Sayıyı tek parça yerine bir dizi içinde saklarız. Her hücre bir basamak taşıyabilir; fakat işlemciyi daha verimli kullanmak için tabanı 10 değil, örneğin $10^9$ seçebiliriz.

\[N = a_0B^0+a_1B^1+\cdots+a_kB^k, \qquad B=10^9\]

Burada $a_0$ en düşük anlamlı parçadır. Böylece 12345678901234567890 sayısı küçükten büyüğe [234567890, 345678901, 12] olarak tutulur.

Python ile çekirdek sınıf

Python zaten sınırsız büyüklükte tamsayı destekler; onu burada algoritmayı rahatça göstermek için kullanıyoruz. Aşağıdaki eğitim sürümü negatif olmayan sayıları destekliyor:

class BigUInt:
    BASE = 10**9

    def __init__(self, value='0'):
        value = value.lstrip('0') or '0'
        self.parts = []
        for end in range(len(value), 0, -9):
            start = max(0, end - 9)
            self.parts.append(int(value[start:end]))
        self._trim()

    def _trim(self):
        while len(self.parts) > 1 and self.parts[-1] == 0:
            self.parts.pop()

    def __str__(self):
        head = str(self.parts[-1])
        tail = ''.join(f'{x:09d}' for x in reversed(self.parts[:-1]))
        return head + tail

    def __add__(self, other):
        result, carry = [], 0
        size = max(len(self.parts), len(other.parts))
        for i in range(size):
            a = self.parts[i] if i < len(self.parts) else 0
            b = other.parts[i] if i < len(other.parts) else 0
            total = a + b + carry
            result.append(total % self.BASE)
            carry = total // self.BASE
        if carry:
            result.append(carry)
        answer = BigUInt()
        answer.parts = result
        return answer

carry, yani elde, bir hücrenin tabanı aşan bölümünü sonraki hücreye aktarır. Toplama her parçayı yalnızca bir kez ziyaret ettiği için zaman karmaşıklığı $O(n)$ olur.

Uzun çarpma

Çarpma işleminde her parçayı karşı tarafın tüm parçalarıyla eşleştiririz:

def multiply(a, b):
    out = [0] * (len(a.parts) + len(b.parts))
    for i, x in enumerate(a.parts):
        carry = 0
        for j, y in enumerate(b.parts):
            total = out[i + j] + x * y + carry
            out[i + j] = total % a.BASE
            carry = total // a.BASE
        out[i + len(b.parts)] += carry

    result = BigUInt()
    result.parts = out
    result._trim()
    return result

Bu klasik algoritma $O(nm)$ karmaşıklığındadır. Binlerce parçada Karatsuba ile yaklaşık $O(n^{1.585})$, daha büyük girdilerde FFT tabanlı yöntemlerle daha iyi sonuç alınabilir.

Sıradaki geliştirmeler

Gerçek bir kütüphane için işaret yönetimi, çıkarma, karşılaştırma, bölme ve sıfıra bölme denetimi eklenmelidir. Rastgele üretilen sayılarla sonuçları dilin yerleşik BigInt özelliğine karşı sınamak da güçlü bir test stratejisidir. Böylece dev sayıların sihir değil; doğru temsil, elde aktarımı ve dikkatli algoritma seçiminden ibaret olduğunu görebilirsiniz.

Yorumlar