Rational numbers are essential in both mathematics and computing, allowing precise representation of quantities like fractions and decimals within programs and algorithms.
What are rational numbers?
Rational numbers are defined as numbers that can be expressed as the ratio of two integers. This means that any number that can be written in the form a / b, where a and b are both integers and b is not equal to zero, is considered rational.
For example:
3/4 is a rational number because it is the result of dividing the integer 3 by the integer 4.
-1 is also a rational number since it can be written as -1/1.
2.5 is rational because it equals 5/2, both of which are integers.
This definition includes:
Positive fractions (e.g. 7/10)
Negative fractions (e.g. -2/3)
Whole numbers (e.g. 4 = 4/1)
Decimals that can be exactly expressed as a fraction (e.g. 0.5 = 1/2)
The set of all rational numbers is denoted by the symbol ℚ, which comes from the word quotient, indicating the result of a division.
Formal notation for rational numbers
In mathematics, the set of rational numbers is written using set notation as:
ℚ = { a / b | a ∈ ℤ, b ∈ ℤ, and b ≠ 0 }
Practice Questions
FAQ
Although all rational numbers can be represented exactly as fractions, not all of them can be represented exactly in binary floating-point form. This is because binary uses powers of 2 in its base, and only fractions whose denominators are made up of the prime factors 2 (e.g. 1/2, 3/8, 5/16) will have exact binary equivalents. If a rational number’s denominator includes other prime factors (like 3 or 5), its binary representation becomes a repeating binary fraction, which can only be approximated by a limited number of bits. For example, 1/10 in decimal equals 0.1, but in binary it is a repeating fraction: 0.0001100110011... and so on. Since memory is finite, the binary representation is cut off after a certain point, leading to rounding errors. This limitation causes seemingly simple expressions (like 0.1 + 0.2) to produce unexpected results (like 0.30000000000000004), and it's why precise arithmetic with certain rational numbers often requires alternative representations, such as fraction classes or arbitrary-precision types.
Fixed-point arithmetic stores numbers using a fixed number of digits before and after the decimal (or binary) point, making it ideal for applications needing predictable precision, such as embedded systems or currency calculations. Rational numbers in fixed-point are limited to specific fractions based on the resolution (e.g. thousandths or hundredths). This means only certain rational numbers can be represented exactly, and others must be rounded or truncated, which can cause loss of accuracy in calculations.
Floating-point arithmetic, on the other hand, uses a format that stores numbers in scientific notation (significand and exponent), allowing for a much wider range of values but less predictable precision. Rational numbers like 1/3 or 1/10 cannot be stored exactly in binary floating-point and are approximated, potentially introducing small errors, especially in iterative calculations or equality checks.
The key difference is that fixed-point offers predictable and limited precision, while floating-point offers greater range but less predictable accuracy, particularly for non-terminating binary fractions. Developers choose between the two based on the application’s need for range versus exactness.
When two rational numbers are added in a program using floating-point types (e.g. float, double), each number is first converted to its binary floating-point representation. If either number cannot be exactly represented (which is common for non-binary fractions), it will be approximated, often with a small rounding error. During the addition, these tiny inaccuracies can accumulate, leading to a result that is close but not mathematically exact.
For example:
python
x = 0.1
y = 0.2
z = x + yMathematically, z should equal 0.3. But due to floating-point representation limitations, z may be stored as 0.30000000000000004. This is not an error in the calculation itself, but a side effect of binary approximation. When such numbers are later used in conditional statements (like if z == 0.3), unexpected results may occur because the floating-point version of 0.3 is not stored exactly either.
To handle this, programmers often compare floats using a tolerance or use data types like fractions or decimal that offer higher precision and more accurate representations of rational numbers when exactness is critical.
While rational numbers can technically be used in hashing, indexing, or key generation, it is not common or recommended due to the way they are represented in memory. Most systems use floating-point representations for rational numbers, which can suffer from rounding errors and inconsistencies. Two mathematically equal rational values might have slightly different binary representations, causing unequal hash values or inconsistent results in keys.
Moreover, floating-point values are not well-suited for data structures like hash tables or dictionaries as keys because their exact bitwise representations can vary across platforms or computations. For example, 1/3 might be approximated as 0.33333333... in one computation and slightly differently in another, even if logically they are the same.
If rational numbers are to be used as keys or in indexing, it’s better to use reduced numerator and denominator pairs, stored as tuples or strings. This ensures consistent, reproducible hashing and avoids the pitfalls of floating-point inaccuracies. In practice, however, integers or strings are much more commonly used in such operations for their stability and predictability.
Simplifying rational numbers in computation ensures they are stored and compared in their canonical form, which avoids redundancy and helps in recognising equivalence. For instance, 2/4 and 1/2 are mathematically equal, but if stored unsimplified, a program might treat them as different values—leading to logic errors in comparisons, lookups, or caching mechanisms.
Simplification involves dividing both the numerator and the denominator by their greatest common divisor (GCD). For example, the rational number 6/9 can be simplified by dividing both parts by 3, giving 2/3.
In code, simplification is often handled automatically by libraries that implement rational number types. For instance, Python’s fractions.Fraction class always stores rational numbers in simplified form by computing the GCD internally on creation. This not only reduces memory but ensures accurate equality checks.
Simplification also helps in symbolic computation, algebraic manipulation, and expression simplification in software tools, making results cleaner, more interpretable, and reducing downstream errors caused by unnecessary complexity in fractional values.
