Trong lĩnh vực tính toán, phép toán chia lấy dư dùng để xác định phần dư sau khi chia hai số (thường được gọi là modulus).
Khi có hai số, (số bị chia) a và (số chia) n, a chia lấy dư n (viết tắt là a mod n) là phần dư của phép chia có dư Euclid giữa a và n. Ví dụ, biểu thức '5 mod 2' cho kết quả là 1 vì 5 chia cho 2 có thương là 2 và phần dư là 1, trong khi '9 mod 3' cho kết quả là 0 do 9 chia cho 3 có thương là 3 và phần dư là 0; không còn gì sau khi trừ 9 với 3 nhân 3. (Lưu ý rằng phép chia trên máy tính có thể không cho kết quả giống như phép toán này; thương có thể được biểu diễn dưới dạng số thập phân.)
Mặc dù phép toán này thường áp dụng cho các số nguyên, một số hệ thống tính toán cũng cho phép sử dụng các kiểu số khác. Phần dư của một phép chia modulo nguyên với n thường nằm trong khoảng từ 0 đến n − 1. (a mod 1 luôn cho kết quả là 0; a mod 0 không xác định và có thể gây lỗi chia cho 0 trong nhiều ngôn ngữ lập trình.) Xem số học mô-đun để tìm hiểu thêm về các quy ước và lý thuyết số liên quan.
Khi một trong hai số a hoặc n là số âm, định nghĩa cơ bản có thể bị ảnh hưởng và các ngôn ngữ lập trình có thể xử lý các kết quả này theo cách khác nhau.
Tính toán phần dư trong phép toán chia lấy dư
Trong toán học, phép toán chia lấy dư cho kết quả là phần dư của phép chia có dư. Tuy nhiên, các quy ước khác cũng tồn tại. Các máy tính và máy vi tính có cách lưu trữ và đại diện số khác nhau, vì vậy định nghĩa về phép toán chia lấy dư có thể phụ thuộc vào ngôn ngữ lập trình hoặc phần cứng của máy tính.
Trong hầu hết các hệ thống máy tính, thương số q và phần dư r của phép chia a cho n phải thỏa mãn
(1)
Tuy nhiên, vẫn có sự không nhất quán về dấu khi phần dư không bằng 0: có thể có hai lựa chọn cho phần dư, một âm và một dương, và hai lựa chọn cho thương số. Trong lý thuyết số, thường chọn phần dư dương, nhưng trong các ngôn ngữ lập trình, kết quả có thể khác tùy thuộc vào ngôn ngữ và dấu của a hoặc n. Ví dụ, Pascal và ALGOL 68 tiêu chuẩn chọn phần dư dương (hoặc 0) ngay cả khi số chia âm, trong khi một số ngôn ngữ như C90 có thể thay đổi kết quả dựa trên cài đặt khi n hoặc a là số âm. Xem bảng để biết chi tiết. Phép toán a modulo 0 thường không xác định trong nhiều hệ thống, mặc dù một số định nghĩa là a.
- Nhiều cài đặt áp dụng phép chia rút gọn, trong đó thương số được xác định bởi hàm rút gọn q = trunc(a/n), vì vậy theo phương trình (1) phần dư sẽ có cùng dấu với số bị chia. Thương số được làm tròn về không: đến số nguyên đầu tiên có phần hướng về không của thương số hữu tỉ.
- Donald Knuth mô tả phép chia sàn, trong đó thương số được xác định bằng hàm floor q = ⌊a/n⌋, vì vậy theo phương trình (1) phần dư sẽ có cùng dấu với số chia. Do hàm floor, thương số luôn được làm tròn xuống ngay cả khi số âm.
- Raymond T. Boute mô tả định nghĩa phép chia có dư trong đó phần dư luôn không âm, 0 ≤ r, phù hợp với thuật toán phép chia có dư. Trong đó,
hoặc tương đương
với sgn là hàm dấu, do đó
- Ngôn ngữ Common Lisp định nghĩa phép chia làm tròn và phép chia sàn, trong đó thương số được xác định bởi q = round(a/n) và q = ⌈a/n⌉ tương ứng.
- Tiêu chuẩn IEEE 754 định nghĩa hàm số dư, trong đó thương số là a/n được làm tròn theo quy ước gần nhất. Do đó, dấu của phần dư được chọn là gần với số 0 nhất.
Theo quan điểm của Leijen,
Boute cho rằng phép chia Euclidean vượt trội hơn các phép chia khác về tính đều đặn và các thuộc tính toán học hữu ích, mặc dù phép chia sàn, được Knuth ủng hộ, cũng là một định nghĩa tốt. Dù được sử dụng rộng rãi, phép chia rút gọn lại chứng tỏ là kém hơn các định nghĩa khác.
— Daan Leijen, Division and Modulus for Computer Scientists
Tuy nhiên, Boute tập trung vào các đặc điểm của phép toán modulo mà không đánh giá việc phép chia rút gọn (truncated division) thể hiện sự đối xứng của (-a) div n = -(a div n) và a div (-n) = -(a div n), giống như phép chia thông thường. Do cả phép chia sàn và phép chia có dư đều thiếu tính đối xứng này, đánh giá của Boute ít nhất là không toàn diện.
Các lỗi thường gặp
Nếu kết quả phép chia modulo có dấu tương tự như số bị chia, điều này có thể dẫn đến những kết quả không mong muốn.
Chẳng hạn, để xác định xem một số nguyên có phải là số lẻ hay không, ta thường kiểm tra phần dư khi chia cho 2 xem có bằng 1 không:
bool is_odd(int n) { return n % 2 == 1; }
Khi ngôn ngữ lập trình sử dụng phép chia với phần dư có dấu của số bị chia, kiểm tra sẽ không chính xác. Nếu n (số bị chia) là số âm lẻ, n mod 2 sẽ trả về −1, dẫn đến hàm kiểm tra trả về false.
Để khắc phục lỗi này, ta có thể kiểm tra xem phần dư có khác 0 hay không (bởi vì phần dư 0 được coi là giống nhau bất kể dấu):
bool is_odd(int n) { return n % 2 != 0; }
Hoặc, bạn có thể nhớ rằng phần dư của phép chia với số lẻ có thể là 1 hoặc −1:
bool is_odd(int n){ return n % 2 == 1 || n % 2 == -1; }
Biểu tượng
Nhiều máy tính cầm tay có nút chức năng mod(), và nhiều ngôn ngữ lập trình cũng cung cấp hàm tương tự để tính toán mod(a, n). Một số ngôn ngữ hỗ trợ các ký hiệu như '%', 'mod', hoặc 'Mod' để thực hiện phép toán modulo hoặc lấy số dư, chẳng hạn như
a % n
hoặc
a mod n
hoặc có thể dùng trong các môi trường không có hàm mod() (lưu ý rằng kiểu 'int' thường sinh ra giá trị rút gọn như a/n)
a - (n * int(a/n))
Vấn đề hiệu suất
Phép toán modulo có thể được thực hiện sao cho mỗi phép chia với số dư được tính toán chính xác. Đối với những yêu cầu đặc biệt, một số phần cứng có thể sử dụng các phép toán tương tự nhưng nhanh hơn. Ví dụ, phép toán modulo với lũy thừa của 2 có thể được thay thế bằng phép toán AND bitwise:
x % 2 == x & (2 - 1)
Ví dụ (giả sử x là một số nguyên dương):
x % 2 == x & 1
x % 4 == x & 3
x % 8 == x & 7
Trong các thiết bị và phần mềm mà phép toán bitwise thực hiện nhanh hơn phép toán modulo, việc sử dụng các phương pháp thay thế này có thể giúp cải thiện hiệu suất.
Các trình biên dịch tối ưu hóa có khả năng nhận diện các biểu thức dạng expression % constant
với constant
là lũy thừa của 2 và tự động chuyển đổi chúng thành expression & (constant-1)
. Điều này giúp mã nguồn trở nên rõ ràng hơn mà không làm giảm hiệu suất. Tuy nhiên, tối ưu hóa này không áp dụng cho các ngôn ngữ mà kết quả của phép toán modulo phải có cùng dấu với số bị chia (như trong C), trừ khi số bị chia là số nguyên không dấu. Nếu số bị chia là số âm, phép modulo sẽ cho kết quả âm, trong khi expression & (constant-1)
luôn cho kết quả dương.
Tính tương đương
Một số phép toán modulo có thể được mở rộng tương tự sang các phép toán toán học khác, điều này rất hữu ích trong các chứng minh mật mã học, chẳng hạn như trao đổi khóa Diffie-Hellman.
- Phần tử đơn vị:
- (a mod n) mod n = a mod n.
- n mod n = 0 với mọi số nguyên dương x.
- Nếu p là số nguyên tố không chia hết cho b, thì ab mod p = a mod p, theo định lý nhỏ Fermat.
- Phần tử đảo:
- [(−a mod n) + (a mod n)] mod n = 0.
- b mod n kí hiệu phần tử đảo modular, được định nghĩa khi và chỉ khi b và n là các số nguyên tố cùng nhau, khi vế trái xác định: [(b mod n)(b mod n)] mod n = 1.
- Tính phân phối:
- (a + b) mod n = [(a mod n) + (b mod n)] mod n.
- ab mod n = [(a mod n)(b mod n)] mod n.
- Phép chia (định nghĩa): a/b mod n = [(a mod n)(b mod n)] mod n, khi vế phải xác định (tức là khi b và n là các số nguyên tố cùng nhau). Các trường hợp khác là không xác định.
- Phép nhân nghịch đảo: [(ab mod n)(b mod n)] mod n = a mod n.
Dấu hiệu Mod trong các ngôn ngữ lập trình
Ngôn ngữ | Toán tử | Kết quả có cùng dấu với |
---|---|---|
ABAP | MOD
|
Luôn không âm |
ActionScript | %
|
Số bị chia |
Ada | mod
|
Số chia |
rem
|
Số bị chia | |
ALGOL 68 | ÷×, mod
|
Luôn không âm |
AMPL | mod
|
Số bị chia |
APL | |
|
Số chia |
AppleScript | mod
|
Số bị chia |
AutoLISP | (rem d n)
|
Số dư |
AWK | %
|
Số bị chia |
BASIC | Mod
|
Không xác định |
bash | %
|
Số bị chia |
bc | %
|
Số bị chia |
C (ISO 1990) | %
|
Định nghĩa tùy thuộc cài đặt |
div
|
ngôn ngữ lập trình | |
C++ (ISO 1998) | %
|
Định nghĩa tùy thuộc cài đặt |
div
|
Số bị chia | |
C (ISO 1999) | % , div
|
Số bị chia |
C++ (ISO 2011) | % , div
|
Số bị chia |
C# | %
|
Số bị chia |
Clarion | %
|
Số bị chia |
Clojure | mod
|
Số chia |
rem
|
Số bị chia | |
COBOL | FUNCTION MOD
|
Số chia |
CoffeeScript | %
|
Số bị chia |
%%
|
Số chia | |
ColdFusion | %, MOD
|
Số bị chia |
Common Lisp | mod
|
Số chia |
rem
|
Số bị chia | |
Construct 2 | % | |
D | %
|
Số bị chia |
Dart | %
|
Luôn không âm |
remainder() | Số bị chia | |
Eiffel | \\
|
|
Erlang | rem
|
Số bị chia |
Euphoria | mod
|
Số chia |
remainder
|
Số bị chia | |
F# | %
|
Số bị chia |
FileMaker | Mod
|
Số chia |
Forth | mod
|
tùy thuộc vào cài đặt |
Fortran | mod
|
Số bị chia |
modulo
|
Số chia | |
Frink | mod
|
Số chia |
GameMaker: Studio (GML) | mod , %
|
Số bị chia |
GDScript | %
|
Số bị chia |
Go | %
|
Số bị chia |
Haskell | mod
|
Số chia |
rem
|
Số bị chia | |
Haxe | %
|
Số bị chia |
Kotlin | %
|
Số bị chia |
J | |
|
Số chia |
Java | %
|
Số bị chia |
Math.floorMod
|
Số chia | |
JavaScript | %
|
Số bị chia |
Julia | mod
|
Số chia |
rem
|
Số bị chia | |
LabVIEW | mod
|
Số bị chia |
LibreOffice | =MOD()
|
Số chia |
Lua 5 | %
|
Số chia |
Lua 4 | mod(x,y)
|
Số chia |
Liberty BASIC | MOD
|
Số bị chia |
Mathcad | mod(x,y)
|
Số chia |
Maple | e mod m
|
Luôn không âm |
Mathematica | Mod[a, b]
|
Số chia |
MATLAB | mod
|
Số chia |
rem
|
Số bị chia | |
Maxima | mod
|
Số chia |
remainder
|
Số bị chia | |
Maya Embedded Language | %
|
Số bị chia |
Microsoft Excel | =MOD()
|
Số chia |
Minitab | MOD
|
Số chia |
mksh | %
|
Số bị chia |
Modula-2 | MOD
|
Số chia |
REM
|
Số bị chia | |
MUMPS | #
|
Số chia |
Netwide Assembler (NASM, NASMX) | %
|
toán tử modulo không dấu |
%%
|
toán tử modulo có dấu | |
Oberon | MOD
|
Số chia |
Object Pascal, Delphi | mod
|
Số bị chia |
OCaml | mod
|
Số bị chia |
Occam | \
|
Số bị chia |
Pascal (ISO-7185 and -10206) | mod
|
Luôn không âm |
Perl | %
|
Số chia |
PHP | %
|
Số bị chia |
PIC BASIC Pro | \\
|
Số bị chia |
PL/I | mod
|
Số chia (ANSI PL/I) |
PowerShell | %
|
Số bị chia |
Progress | modulo
|
Số bị chia |
Prolog (ISO 1995) | mod
|
Số chia |
rem
|
Số bị chia | |
PureBasic | %,Mod(x,y)
|
Số bị chia |
Python | %
|
Số chia |
math.fmod
|
Số bị chia | |
Racket | remainder
|
Số bị chia |
RealBasic | MOD
|
Số bị chia |
R | %%
|
Số chia |
Rexx | //
|
Số bị chia |
RPG | %REM
|
Số bị chia |
Ruby | %, modulo()
|
Số chia |
remainder()
|
Số bị chia | |
Rust | %
|
Số bị chia |
Scala | %
|
Số bị chia |
Scheme | modulo
|
Số chia |
remainder
|
Số bị chia | |
Scheme RRS | mod
|
Luôn không âm |
mod0
|
Nearest to zero | |
Seed7 | mod
|
Số chia |
rem
|
Số bị chia | |
SenseTalk | modulo
|
Số chia |
rem
|
Số bị chia | |
Smalltalk | \\
|
Số chia |
rem:
|
Số bị chia | |
Spin | //
|
Số chia |
SQL (SQL:1999) | mod(x,y)
|
Số bị chia |
SQL (SQL:2012) | %
|
Số bị chia |
Standard ML | mod
|
Số chia |
Int.rem
|
Số bị chia | |
Stata | mod(x,y)
|
Luôn không âm |
Swift | %
|
Số bị chia |
Tcl | %
|
Số chia |
Torque | %
|
Số bị chia |
Turing | mod
|
Số chia |
Verilog (2001) | %
|
Số bị chia |
VHDL | mod
|
Số chia |
rem
|
Số bị chia | |
VimL | %
|
Số bị chia |
Visual Basic | Mod
|
Số bị chia |
x86 assembly | IDIV
|
Số bị chia |
XBase++ | %
|
Số bị chia |
Mod()
|
Số chia | |
Z3 theorem prover | div , mod
|
Luôn không âm |
Ngôn ngữ | Toán tử | Kết quả có cùng dấu với |
---|---|---|
ABAP | MOD
|
Luôn không âm |
C (ISO 1990) | fmod
|
Số bị chia |
C (ISO 1999) | fmod
|
Số bị chia |
remainder
|
Gần với số 0 | |
C++ (ISO 1998) | std::fmod
|
Số bị chia |
C++ (ISO 2011) | std::fmod
|
Số bị chia |
std::remainder
|
Gần với số 0 | |
C# | %
|
Số bị chia |
Common Lisp | mod
|
Số chia |
rem
|
Số bị chia | |
D | %
|
Số bị chia |
Dart | %
|
Luôn không âm |
remainder() | Số bị chia | |
F# | %
|
Số bị chia |
Fortran | mod
|
Số bị chia |
modulo
|
Số chia | |
Go | math.Mod
|
Số bị chia |
Haskell (GHC) | Data.Fixed.mod'
|
Số chia |
Java | %
|
Số bị chia |
JavaScript | %
|
Số bị chia |
LabVIEW | mod
|
Số bị chia |
Microsoft Excel | =MOD()
|
Số chia |
OCaml | mod_float
|
Số bị chia |
Perl | POSIX::fmod
|
Số bị chia |
Perl6 | %
|
Số chia |
PHP | fmod
|
Số bị chia |
Python | %
|
Số chia |
math.fmod
|
Số bị chia | |
Rexx | //
|
Số bị chia |
Ruby | %, modulo()
|
Số chia |
remainder()
|
Số bị chia | |
Scheme RRS | flmod
|
Luôn không âm |
flmod0
|
Gần với số 0 | |
Standard ML | Real.rem
|
Số bị chia |
Swift | truncatingRemainder(dividingBy:)
|
Số bị chia |
XBase++ | %
|
Số bị chia |
Mod()
|
Số chia |
- Thuật ngữ 'Modulo' và 'modulo' – từ 'modulo' được sử dụng theo nhiều cách khác nhau, tất cả đều bắt nguồn từ cuốn sách của Carl F. Gauss năm 1801 về số học mô đun (tựa Anh: 'introduction of modular arithmetic').
- Lũy thừa Modular
Chú thích
- ^ Perl sử dụng toán tử modulo số học không phụ thuộc vào phần cứng. Xem tài liệu Perl để biết thêm ví dụ và các trường hợp đặc biệt.
- ^ Về mặt toán học, đây là một trong nhiều cách lựa chọn có sẵn theo [[remainder#The inequality satisfied by the remainder|bất đẳng thức thỏa mãn với số dư]]
- ^ Số chia phải là số dương, nếu không sẽ không xác định được.
- ^ Như được định nghĩa trong ACUCOBOL, Micro Focus COBOL, và một số ngôn ngữ khác.
- ^ ^ Thứ tự tham số ngược, ví dụ,
α|ω
tính toán số dư củaω
khi chia choα
.