Cách triển khai big int trong C ++


80

Tôi muốn triển khai một lớp int lớn trong C ++ như một bài tập lập trình — một lớp có thể xử lý các số lớn hơn một int dài. Tôi biết rằng đã có một số triển khai mã nguồn mở, nhưng tôi muốn viết riêng. Tôi đang cố gắng tìm hiểu cách tiếp cận đúng là gì.

Tôi hiểu rằng chiến lược chung là lấy số dưới dạng một chuỗi, sau đó chia nó thành các số nhỏ hơn (ví dụ: các chữ số đơn) và đặt chúng trong một mảng. Tại thời điểm này, nó sẽ tương đối đơn giản để thực hiện các toán tử so sánh khác nhau. Mối quan tâm chính của tôi là làm thế nào tôi sẽ thực hiện những thứ như cộng và nhân.

Tôi đang tìm kiếm một cách tiếp cận và lời khuyên chung thay vì mã làm việc thực tế.


4
Điều đầu tiên - chuỗi chữ số cũng được, nhưng hãy nghĩ về cơ số 2 ^ 32 (4 tỷ chữ số phân biệt lẻ). Thậm chí có thể là cơ số 2 ^ 64 những ngày này. Thứ hai, luôn làm việc với các "chữ số" số nguyên không dấu. Bạn có thể tự mình thực hiện phần bổ sung twos cho các số nguyên lớn có dấu, nhưng nếu bạn cố gắng thực hiện xử lý tràn, v.v. với các số nguyên có dấu, bạn sẽ gặp phải các vấn đề bahaviour không xác định theo tiêu chuẩn.
Steve314,

3
Đối với các thuật toán - đối với một thư viện cơ bản, những gì bạn đã học ở trường là đúng.
Steve314,

1
Nếu bạn muốn tự mình thực hiện phép toán đa độ chính xác, thì tôi khuyên bạn nên xem qua Nghệ thuật lập trình máy tính của Donald Knuth . Tôi tin rằng Tập II, Thuật toán bán số, Chương 4, Số học chính xác nhiều, là thứ bạn quan tâm. Ngoài ra, hãy xem Cách thêm 2 số nguyên có kích thước tùy ý trong C ++? , cung cấp mã cho một số thư viện C ++ và OpenSSL.
jww

Câu trả lời:


37

Những điều cần xem xét đối với một lớp int lớn:

  1. Các toán tử toán học: +, -, /, *,% Đừng quên rằng lớp của bạn có thể nằm ở hai bên của toán tử, các toán tử có thể được xâu chuỗi, một trong các toán hạng có thể là int, float, double, v.v. .

  2. Toán tử I / O: >>, << Đây là nơi bạn tìm ra cách tạo đúng cách lớp của mình từ đầu vào của người dùng và cách định dạng nó cho đầu ra.

  3. Chuyển đổi / Truyền: Tìm ra loại / lớp nào mà lớp int lớn của bạn có thể chuyển đổi thành và cách xử lý chuyển đổi đúng cách. Một danh sách nhanh sẽ bao gồm double và float, và có thể bao gồm int (với kiểm tra giới hạn thích hợp) và phức tạp (giả sử nó có thể xử lý phạm vi).


1
Xem tại đây để biết các cách thành ngữ để thực hiện các toán tử.
Vịt rên rỉ vào

5
Đối với số nguyên, toán tử << và >> là các phép toán dịch chuyển bit. Việc hiểu chúng là I / O sẽ là một thiết kế tồi.
Dave

3
@Dave: Ngoại trừ việc đó là C ++ tiêu chuẩn để sử dụng operator<<operator>>với iostreams cho I / O.

9
@ Dave Bạn vẫn có thể xác định << và >> cho các hoạt động bit thay đổi cùng với I / O cho suối ...
miguel.martin

46

Một thử thách thú vị. :)

Tôi giả sử rằng bạn muốn các số nguyên có độ dài tùy ý. Tôi đề xuất cách tiếp cận sau:

Xem xét bản chất nhị phân của kiểu dữ liệu "int". Hãy nghĩ đến việc sử dụng các phép toán nhị phân đơn giản để mô phỏng những gì các mạch trong CPU của bạn thực hiện khi chúng thêm mọi thứ. Trong trường hợp bạn quan tâm sâu hơn, hãy xem xét đọc bài viết wikipedia này về bộ cộng nửa và bộ cộng đầy đủ . Bạn sẽ làm điều gì đó tương tự như vậy, nhưng bạn có thể đi xuống mức thấp như vậy - nhưng do lười biếng, tôi nghĩ rằng tôi sẽ bỏ qua và tìm một giải pháp đơn giản hơn.

Nhưng trước khi đi vào bất kỳ chi tiết thuật toán nào về cộng, trừ, nhân, chúng ta hãy tìm một số cấu trúc dữ liệu. Tất nhiên, một cách đơn giản là lưu trữ mọi thứ trong một vector std ::.

template< class BaseType >
class BigInt
{
typedef typename BaseType BT;
protected: std::vector< BaseType > value_;
};

Bạn có thể muốn cân nhắc xem bạn có muốn tạo vectơ có kích thước cố định hay không và có định phân bổ trước nó hay không. Lý do là đối với các phép toán đa dạng, bạn sẽ phải đi qua từng phần tử của vectơ - O (n). Bạn có thể muốn biết rõ một phép toán sẽ phức tạp như thế nào và số n cố định thực hiện điều đó.

Nhưng bây giờ đến một số thuật toán về hoạt động trên các con số. Bạn có thể làm điều đó ở cấp độ logic, nhưng chúng tôi sẽ sử dụng sức mạnh kỳ diệu của CPU để tính toán kết quả. Nhưng những gì chúng ta sẽ tiếp thu từ minh họa logic của Half- và FullAdders là cách nó xử lý. Ví dụ, hãy xem xét cách bạn triển khai toán tử + = . Đối với mỗi số trong BigInt <> :: value_, bạn sẽ thêm các số đó và xem kết quả có tạo ra một số hình thức thực hiện hay không. Chúng tôi sẽ không làm điều đó một cách khôn ngoan, nhưng dựa vào bản chất của BaseType của chúng tôi (có thể là dài hoặc int hoặc ngắn hoặc bất cứ điều gì): nó tràn.

Chắc chắn nếu bạn cộng hai số thì kết quả phải lớn hơn một số lớn hơn đúng không? Nếu không, thì kết quả bị tràn.

template< class BaseType >
BigInt< BaseType >& BigInt< BaseType >::operator += (BigInt< BaseType > const& operand)
{
  BT count, carry = 0;
  for (count = 0; count < std::max(value_.size(), operand.value_.size(); count++)
  {
    BT op0 = count < value_.size() ? value_.at(count) : 0, 
       op1 = count < operand.value_.size() ? operand.value_.at(count) : 0;
    BT digits_result = op0 + op1 + carry;
    if (digits_result-carry < std::max(op0, op1)
    {
      BT carry_old = carry;
      carry = digits_result;
      digits_result = (op0 + op1 + carry) >> sizeof(BT)*8; // NOTE [1]
    }
    else carry = 0;
  }

  return *this;
}
// NOTE 1: I did not test this code. And I am not sure if this will work; if it does
//         not, then you must restrict BaseType to be the second biggest type 
//         available, i.e. a 32-bit int when you have a 64-bit long. Then use
//         a temporary or a cast to the mightier type and retrieve the upper bits. 
//         Or you do it bitwise. ;-)

Phép toán số học khác tương tự. Heck, bạn thậm chí có thể sử dụng stl-functors std :: plus và std :: hidden, std :: times và std :: divides, ..., nhưng hãy nhớ mang theo. :) Bạn cũng có thể thực hiện phép nhân và chia bằng cách sử dụng các toán tử cộng và trừ, nhưng điều đó rất chậm, bởi vì điều đó sẽ tính toán lại kết quả bạn đã tính trong các lần gọi trước đến cộng và trừ trong mỗi lần lặp. Có rất nhiều thuật toán tốt cho công việc đơn giản này, hãy sử dụng wikipedia hoặc web.

Và tất nhiên, bạn nên triển khai các toán tử tiêu chuẩn như operator<<(chỉ cần chuyển từng giá trị trong value_ sang trái cho n bit, bắt đầu từ value_.size()-1... oh và nhớ mang :), operator<- bạn thậm chí có thể tối ưu hóa một chút ở đây, kiểm tra số thô của chữ số với size()đầu tiên. Và như thế. Sau đó, hãy biến lớp học của bạn trở nên hữu ích, bằng cách befriendig std :: ostream operator<<.

Hy vọng cách tiếp cận này là hữu ích!


6
"int" (như đã ký) là một ý tưởng tồi. Hành vi không xác định của các tiêu chuẩn đối với các lỗi tràn gây khó khăn (nếu không muốn nói là không thể) để hiểu đúng logic, ít nhất là có thể di chuyển được. Tuy nhiên, khá dễ dàng để làm việc trong phần bổ sung twos với các số nguyên không dấu, trong đó hành vi tràn được xác định nghiêm ngặt là đưa ra kết quả modulo 2 ^ n.
Steve314

29

Có một phần hoàn chỉnh về điều này: [Nghệ thuật lập trình máy tính, tập 2: Thuật toán bán số, phần 4.3 Số học chính xác đa điểm, trang 265-318 (ed.3)]. Bạn có thể tìm thấy các tài liệu thú vị khác trong Chương 4, Số học.

Nếu bạn thực sự không muốn nhìn vào một triển khai khác, bạn đã xem xét nó là gì để học chưa? Có vô số sai lầm được thực hiện và việc phát hiện ra những sai lầm đó là chỉ dẫn và cũng nguy hiểm. Ngoài ra còn có những thách thức trong việc xác định các nền kinh tế tính toán quan trọng và có cấu trúc lưu trữ thích hợp để tránh các vấn đề nghiêm trọng về hiệu suất.

Câu hỏi Thách thức dành cho bạn: Bạn định kiểm tra việc triển khai của mình như thế nào và bạn đề xuất như thế nào để chứng minh rằng số học là chính xác?

Bạn có thể muốn một triển khai khác để kiểm tra (mà không cần xem nó hoạt động như thế nào), nhưng sẽ mất nhiều hơn thế để có thể khái quát hóa mà không mong đợi mức độ kiểm tra chi tiết. Đừng quên xem xét các chế độ lỗi (sự cố hết bộ nhớ, hết ngăn xếp, chạy quá lâu, v.v.).

Chúc vui vẻ!


2
So sánh với một số triển khai tham chiếu sẽ không giúp bạn hiểu thêm, vì sau đó bạn gặp một vấn đề khác: làm thế nào để kiểm tra xem triển khai tham chiếu cũng chính xác? Vấn đề tương tự là với việc kiểm tra kiến ​​thức nói chung: Nếu một người phải kiểm tra người khác, ai sẽ kiểm tra người trước? Không có cách nào để giải quyết vấn đề này ngoại trừ một cách được phát minh từ rất lâu trước đây: chứng minh từ tiên đề. Nếu tập hợp các tiên đề được coi là đúng (không có mâu thuẫn) và việc chứng minh được suy ra đúng theo các quy tắc logic, thì nó không thể sai, ngay cả đối với vô số trường hợp không ai có thể kiểm tra được.
SasQ


5

Khi bạn đã có các chữ số của số trong một mảng, bạn có thể thực hiện phép cộng và phép nhân chính xác như cách bạn làm với chúng.


4

Đừng quên rằng bạn không cần giới hạn bản thân ở 0-9 dưới dạng chữ số, tức là sử dụng byte làm chữ số (0-255) và bạn vẫn có thể thực hiện số học tay dài giống như cách bạn làm với chữ số thập phân. Bạn thậm chí có thể sử dụng một mảng dài.


Nếu bạn muốn biểu diễn các số ở dạng thập phân (tức là đối với người bình thường), thì thuật toán 0-9 per nibble dễ dàng hơn. Chỉ cần từ bỏ bộ nhớ.
dmckee --- ex-moderator kitten

Bạn nghĩ rằng việc thực hiện các thuật toán BCD dễ dàng hơn so với các thuật toán nhị phân thông thường của chúng?
Nhật thực

2
AFAIK cơ sở 10 thường được sử dụng vì chuyển đổi số lượng lớn tại cơ sở 255 (hoặc bất cứ điều gì không phải là một sức mạnh của 10) từ / đến cơ sở 10 là đắt tiền, và đầu vào và đầu ra các chương trình của bạn thường sẽ nằm trong cơ sở 10
Tobi

@Tobi: Tôi có thể khuyên bạn nên giữ cơ sở 10000 unsigned, là IO nhanh và dễ thực hiện phép nhân, nhược điểm là nó lãng phí 59% không gian lưu trữ. Tôi khuyên bạn nên căn cứ (2 ^ 32) cho việc học tập tiên tiến hơn, đó là nhiều hơn thế nhanh hơn so với cơ sở 10/10000 cho tất cả mọi thứ trừ IO, nhưng khó khăn hơn nhiều để thực hiện phép nhân / chia.
Vịt kêu vào

3

Tôi không tin rằng sử dụng một chuỗi là cách đúng đắn để đi - mặc dù tôi chưa bao giờ tự viết mã, tôi nghĩ rằng sử dụng một mảng của kiểu số cơ sở có thể là một giải pháp tốt hơn. Ý tưởng là bạn chỉ cần mở rộng những gì bạn đã có giống như cách CPU mở rộng một bit thành một số nguyên.

Ví dụ, nếu bạn có cấu trúc

typedef struct {
    int high, low;
} BiggerInt;

Sau đó, bạn có thể thực hiện thủ công các thao tác gốc trên từng "chữ số" (cao và thấp, trong trường hợp này), lưu ý đến các điều kiện tràn:

BiggerInt add( const BiggerInt *lhs, const BiggerInt *rhs ) {
    BiggerInt ret;

    /* Ideally, you'd want a better way to check for overflow conditions */
    if ( rhs->high < INT_MAX - lhs->high ) {
        /* With a variable-length (a real) BigInt, you'd allocate some more room here */
    }

    ret.high = lhs->high + rhs->high;

    if ( rhs->low < INT_MAX - lhs->low ) {
        /* No overflow */
        ret.low = lhs->low + rhs->low;
    }
    else {
        /* Overflow */
        ret.high += 1;
        ret.low = lhs->low - ( INT_MAX - rhs->low ); /* Right? */
    }

    return ret;
}

Đó là một ví dụ đơn giản, nhưng sẽ khá rõ ràng là làm thế nào để mở rộng đến một cấu trúc có một số lượng thay đổi của bất kỳ lớp số cơ sở nào bạn đang sử dụng.


Theo chuỗi OP có nghĩa là lấy một chuỗi chứa số bạn muốn trong biểu diễn số của nó (dưới bất kỳ cơ sở nào) và khởi tạo BigInt với giá trị.
KTC

STLPLUS sử dụng chuỗi để chứa số nguyên lớn.
lsalamon

2

Sử dụng các thuật toán bạn đã học ở lớp 1 đến lớp 4.
Bắt đầu với cột một, sau đó đến hàng chục, v.v.


2

Giống như những người khác đã nói, hãy làm theo cách cũ, nhưng tránh làm tất cả điều này trong cơ sở 10. Tôi khuyên bạn nên làm tất cả trong cơ sở 65536 và lưu trữ mọi thứ trong một mảng dài.


1

Nếu kiến ​​trúc mục tiêu của bạn hỗ trợ biểu diễn BCD (số thập phân được mã hóa nhị phân), bạn có thể nhận được một số hỗ trợ phần cứng cho phép nhân / phép cộng tốc độ mà bạn cần thực hiện. Bắt trình biên dịch phát ra lệnh BCD là thứ bạn sẽ phải đọc ...

Các chip Motorola 68K series có điều này. Không phải tôi cay đắng hay gì cả.


0

Khởi đầu của tôi là có một mảng số nguyên có kích thước tùy ý, sử dụng 31 bit và 32n'd là tràn.

Op khởi động sẽ là THÊM, và sau đó, MAKE-NEGATIVE, sử dụng phần bổ sung của 2. Sau đó, phép trừ trôi đi một cách nhỏ giọt, và khi bạn có thêm / phụ, mọi thứ khác đều có thể thực hiện được.

Có lẽ có nhiều cách tiếp cận phức tạp hơn. Nhưng đây sẽ là cách tiếp cận ngây thơ từ logic kỹ thuật số.


0

Có thể thử triển khai một cái gì đó như thế này:

http://www.docjar.org/html/api/java/math/BigInteger.java.html

Bạn chỉ cần 4 bit cho một chữ số duy nhất 0 - 9

Vì vậy, một Giá trị Int sẽ cho phép tối đa 8 chữ số mỗi giá trị. Tôi quyết định gắn bó với một mảng ký tự để tôi sử dụng gấp đôi bộ nhớ nhưng đối với tôi, nó chỉ được sử dụng 1 lần.

Ngoài ra, khi lưu trữ tất cả các chữ số trong một int duy nhất, nó làm phức tạp quá mức và nếu có bất cứ điều gì, nó thậm chí có thể làm chậm nó.

Tôi không có bất kỳ bài kiểm tra tốc độ nào nhưng nhìn vào phiên bản java của BigInteger, có vẻ như nó đang làm rất nhiều việc.

Đối với tôi tôi làm như dưới đây

//Number = 100,000.00, Number Digits = 32, Decimal Digits = 2.
BigDecimal *decimal = new BigDecimal("100000.00", 32, 2);
decimal += "1000.99";
cout << decimal->GetValue(0x1 | 0x2) << endl; //Format and show decimals.
//Prints: 101,000.99

OP chưa bao giờ nói anh ấy muốn tập trung vào các chữ số thập phân.
einpoklum

-1

trừ 48 từ chuỗi số nguyên của bạn và in ra để có số chữ số lớn. sau đó thực hiện các phép toán cơ bản. nếu không tôi sẽ cung cấp giải pháp hoàn chỉnh.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.