Tất cả đều là vấn đề của việc lưu trữ đầy đủ và các thuật toán để coi các con số như những phần nhỏ hơn. Giả sử bạn có một trình biên dịch trong đó an int
chỉ có thể từ 0 đến 99 và bạn muốn xử lý các số lên đến 999999 (chúng tôi sẽ chỉ lo lắng về các số dương ở đây để giữ cho nó đơn giản).
Bạn thực hiện điều đó bằng cách cho mỗi số ba int
s và sử dụng các quy tắc tương tự mà bạn (lẽ ra) đã học ở trường tiểu học để cộng, trừ và các phép toán cơ bản khác.
Trong một thư viện độ chính xác tùy ý, không có giới hạn cố định về số lượng kiểu cơ sở được sử dụng để biểu thị các số của chúng ta, chỉ cần bất kỳ bộ nhớ nào có thể chứa.
Bổ sung cho ví dụ 123456 + 78
::
12 34 56
78
-- -- --
12 35 34
Làm việc từ đầu cuối ít quan trọng nhất:
- mang ban đầu = 0.
- 56 + 78 + 0 carry = 134 = 34 với 1 carry
- 34 + 00 + 1 mang = 35 = 35 với 0 mang
- 12 + 00 + 0 mang = 12 = 12 với 0 mang
Trên thực tế, đây là cách bổ sung thường hoạt động ở mức bit bên trong CPU của bạn.
Phép trừ tương tự (sử dụng phép trừ kiểu cơ số và mượn thay vì mang theo), phép nhân có thể được thực hiện với các phép cộng lặp lại (rất chậm) hoặc tích chéo (nhanh hơn) và phép chia phức tạp hơn nhưng có thể được thực hiện bằng cách chuyển và trừ các số liên quan (sự phân chia dài mà bạn đã học khi còn nhỏ).
Tôi thực sự đã viết các thư viện để thực hiện loại công việc này bằng cách sử dụng lũy thừa tối đa của mười có thể vừa với một số nguyên khi được bình phương (để ngăn tràn khi nhân hai int
s với nhau, chẳng hạn như 16 bit int
được giới hạn từ 0 đến 99 để tạo ra 9,801 (<32,768) khi bình phương hoặc 32-bit int
sử dụng từ 0 đến 9,999 để tạo ra 99,980,001 (<2,147,483,648)), điều này đã làm giảm đáng kể các thuật toán.
Một số thủ thuật cần chú ý.
1 / Khi cộng hoặc nhân các số, hãy phân bổ trước khoảng trống tối đa cần thiết rồi giảm sau nếu bạn thấy quá nhiều. Ví dụ, thêm hai số 100- "chữ số" (trong đó chữ số là một int
) sẽ không bao giờ cung cấp cho bạn nhiều hơn 101 chữ số. Nhân một số có 12 chữ số với một số có 3 chữ số sẽ không bao giờ tạo ra nhiều hơn 15 chữ số (cộng các chữ số).
2 / Để tăng tốc độ, hãy bình thường hóa (giảm dung lượng lưu trữ cần thiết cho) các số chỉ khi thực sự cần thiết - thư viện của tôi có điều này như một lệnh gọi riêng để người dùng có thể quyết định giữa các mối quan tâm về tốc độ và lưu trữ.
3 / Phép cộng một số dương là phép trừ, và trừ một số âm cũng giống như phép cộng với số dương tương đương. Bạn có thể tiết kiệm khá nhiều mã bằng cách để các phương pháp cộng và trừ gọi nhau sau khi điều chỉnh các dấu hiệu.
4 / Tránh trừ các số lớn với số nhỏ vì bạn luôn kết thúc bằng các số như:
10
11-
-- -- -- --
99 99 99 99 (and you still have a borrow).
Thay vào đó, hãy trừ 10 cho 11, sau đó phủ định nó:
11
10-
--
1 (then negate to get -1).
Đây là những bình luận (được chuyển thành văn bản) từ một trong những thư viện mà tôi phải làm việc này. Rất tiếc, bản thân mã đã có bản quyền, nhưng bạn có thể chọn đủ thông tin để xử lý bốn thao tác cơ bản. Giả sử trong điều sau đây -a
và -b
đại diện cho các số âm và a
và b
là số 0 hoặc số dương.
Đối Ngoài ra , nếu có dấu hiệu là khác nhau, sử dụng phép trừ của phủ định:
-a + b becomes b - a
a + -b becomes a - b
Đối với phép trừ , nếu các dấu hiệu khác nhau, hãy sử dụng phép cộng phủ định:
a - -b becomes a + b
-a - b becomes -(a + b)
Ngoài ra, xử lý đặc biệt để đảm bảo chúng tôi đang trừ số nhỏ cho số lớn:
small - big becomes -(big - small)
Phép nhân sử dụng toán học cấp đầu vào như sau:
475(a) x 32(b) = 475 x (30 + 2)
= 475 x 30 + 475 x 2
= 4750 x 3 + 475 x 2
= 4750 + 4750 + 4750 + 475 + 475
Cách thức đạt được điều này bao gồm việc trích xuất từng chữ số trong số 32 một (ngược) sau đó sử dụng phép cộng để tính giá trị được thêm vào kết quả (ban đầu là số 0).
ShiftLeft
và các ShiftRight
phép toán được sử dụng để nhanh chóng nhân hoặc chia a LongInt
với giá trị bọc (10 cho phép toán "thực"). Trong ví dụ trên, chúng ta cộng 475 với 0 2 lần (chữ số cuối cùng của 32) để được 950 (kết quả = 0 + 950 = 950).
Sau đó, chúng ta chuyển sang trái 475 để được 4750 và chuyển sang phải 32 để được 3. Cộng 4750 với 0 3 lần để được 14250 sau đó thêm vào kết quả của 950 để được 15200.
Dịch trái 4750 để có 47500, dịch phải 3 để nhận 0. Vì dịch sang phải 32 bây giờ bằng 0, chúng ta đã hoàn thành và trên thực tế 475 x 32 bằng 15200.
Phép chia cũng phức tạp nhưng dựa trên số học ban đầu (phương pháp "gazinta" cho "đi vào"). Hãy xem xét phép chia dài sau đây cho 12345 / 27
:
457
+-------
27 | 12345 27 is larger than 1 or 12 so we first use 123.
108 27 goes into 123 4 times, 4 x 27 = 108, 123 - 108 = 15.
---
154 Bring down 4.
135 27 goes into 154 5 times, 5 x 27 = 135, 154 - 135 = 19.
---
195 Bring down 5.
189 27 goes into 195 7 times, 7 x 27 = 189, 195 - 189 = 6.
---
6 Nothing more to bring down, so stop.
Do đó 12345 / 27
là 457
với phần dư 6
. Kiểm chứng:
457 x 27 + 6
= 12339 + 6
= 12345
Điều này được thực hiện bằng cách sử dụng một biến kéo xuống (ban đầu là 0) để giảm từng phân đoạn của 12345 cho đến khi nó lớn hơn hoặc bằng 27.
Sau đó, chúng tôi chỉ cần trừ 27 từ đó cho đến khi chúng tôi nhận được dưới 27 - số lượng trừ là phân đoạn được thêm vào dòng trên cùng.
Khi không còn phân đoạn nào để gỡ xuống, chúng ta đã có kết quả của mình.
Hãy nhớ rằng đây là những thuật toán khá cơ bản. Có nhiều cách tốt hơn để thực hiện số học phức tạp nếu số của bạn đặc biệt lớn. Bạn có thể xem xét một thứ gì đó như Thư viện số học chính xác GNU - nó tốt hơn và nhanh hơn đáng kể so với các thư viện của riêng tôi.
Nó có một điểm sai khá đáng tiếc ở chỗ nó sẽ đơn giản thoát ra nếu hết bộ nhớ (theo ý kiến của tôi là một lỗ hổng khá nghiêm trọng đối với một thư viện mục đích chung) nhưng, nếu bạn có thể xem qua, nó khá tốt về những gì nó làm.
Nếu bạn không thể sử dụng nó vì lý do cấp phép (hoặc vì bạn không muốn ứng dụng của mình thoát ra mà không có lý do rõ ràng), ít nhất bạn có thể lấy các thuật toán từ đó để tích hợp vào mã của riêng bạn.
Tôi cũng nhận thấy rằng những người tham gia MPIR (một nhánh của GMP) dễ dàng hơn trong việc thảo luận về những thay đổi tiềm năng - họ có vẻ là một nhóm thân thiện với nhà phát triển hơn.