+4

Hệ cơ số

I. Giới thiệu chung

Trong Toán học, hệ cơ số (hay hệ đếm) là một hệ thống các kí hiệu toán học và quy tắc sử dụng tập kí hiệu đó để biểu diễn số đếm. Các kí hiệu toán học có thể là chữ số hoặc các kí tự chữ cái. Cần phân biệt giữa Hệ cơ sốCơ số (số lượng kí hiệu sử dụng trong một hệ cơ số).

Có rất nhiều hệ cơ số khác nhau, mỗi hệ cơ số có những quy tắc biểu diễn số khác nhau. Những dãy kí hiệu giống nhau có thể sẽ ứng với những số khác nhau trong các hệ đếm khác nhau. Ví dụ trong hệ thập phân, 1111 thể hiện số "mười một", tuy nhiên trong hệ nhị phân, nó lại thể hiện số "ba",... Số đếm mà dãy kí hiệu thể hiện được gọi là giá trị của nó.

Có hai loại hệ cơ số là hệ cơ số phụ thuộc vào vị trí và hệ cơ số không phụ thuộc vào vị trí. Chẳng hạn, hệ đếm La Mã là một hệ cơ số không phụ thuộc vào vị trí. Hệ đếm này gồm các kí hiệu chữ cái: I,V,X,L,C,D,M;I, V, X, L, C, D, M; mỗi kí hiệu có giá trị cụ thể:

I=1,V=5,X=10,L=50,C=100,D=500,M=1000.I=1, V=5, X=10, L=50, C=100, D=500, M=1000.

Trong hệ đếm này, giá trị của các kí hiệu không phụ thuộc vào vị trí của nó. Ví dụ, trong hai biểu diễn IX (9)IX \text{ }(9)XI (11)XI\text{ } (11) thì XX đều có giá trị là 1010.

Các hệ đếm thường dùng là các hệ đếm phụ thuộc vị trí. Mọi số nguyên basebase bất kỳ có giá trị lớn hơn 11 đều có thể được chọn làm cơ số cho một hệ đếm. Trong các hệ đếm loại này, số lượng kí hiệu sử dụng sẽ chính bằng cơ số của hệ đếm đó, và giá trị tương ứng của các kí hiệu là: 0,1,2,...,base10, 1, 2,..., base - 1. Để thể hiện một biểu diễn XX là biểu diễn của số ở hệ cơ số basebase, ta kí hiệu là XbaseX_{base}.

</div>

II. Biểu diễn số trong các hệ đếm

1. Giá trị của một số trong hệ cơ số bất kỳ

Trong một hệ cơ số bb, giả sử số NN có biểu diễn:

dndn1dn2...d0,d1d2...dmd_nd_{n-1}d_{n-2}...d_0,d_{-1}d_{-2}...d_{-m}

trong đó có n+1n + 1 chữ số bên trái dấu phẩy, mm chữ số bên phải dấu phẩy thể hiện cho phần nguyên và phần phân của NN, và 0di<b0 \le d_i < b. Khi đó giá trị của số N được tính theo công thức:

N=dnbn+dn1bn1+...+ d0b0+d1b1+d2b2+...+ dmbmN=d_nb^n+d_{n-1}b^{n-1}+...+\text{ }d_0b^0+d_{-1}b^{-1}+d_{-2}b^{-2}+...+\text{ }d_{-m}b^{-m}

Giá trị của một số trong hệ cơ số bb cũng chính là biểu diễn tương ứng của nó ở hệ cơ số 1010.

Cài đặt chương trình tính giá trị một số thực NN trong hệ cơ số bb:

void enter()
{
    getline(cin, N); // Nhập N ở dạng xâu.
    cin >> B;
}

// Tính giá trị của biểu diễn N trong hệ cơ số B.
double get_value(string N, int B) 
{
    int pos = N.find('.'); // Tìm vị trí dấu '.' của N.
    // Lấy phần nguyên và phần phân của N.
    string left_path = N.substr(0, pos), right_path = N.substr(pos + 1, N.size() - pos);

    // Tính giá trị từng phần, lưu ý ép kiểu số thực.
    double value = 0, power = 1;
    for (int i = left_path.size() - 1; i >= 0; --i)
    {
        value += (double)(left_path[i] - '0') * power;
        power = power * (double)B;
    }

    power = 1.0 / (double)B;
    for (int i = 0; i < right_path.size(); ++i)
    {
        value += (double)(right_path[i] - '0') * power;
        power /= (double)B;
    }

    return value;
}

2. Các hệ cơ số thông dụng trong Tin học

Trong Tin học, ngoài hệ cơ số 1010, người ta còn sử dụng hai loại hệ đếm sau:

  • Hệ cơ số 22 (Hệ nhị phân): Chỉ sử dụng hai kí hiệu 0011. Lấy ví dụ, 10112=1×20+1×21+0×22+1×23=11101011_2=1\times 2^0 + 1\times 2^1 + 0\times 2^2 + 1\times 2^3=11_{10}.
  • Hệ cơ số 1616 (Hệ thập lục phân hay hệ Hexa): Sử dụng các chữ số từ 00 tới 9966 chữ cái A,B,C,D,E,F;A, B, C, D, E, F; trong đó A,B,C,D,E,FA, B, C, D, E, F có giá trị lần lượt là 10,11,12,13,14,1510, 11, 12, 13, 14, 15. Lấy ví dụ, 16A=10×160+6×161+1×162=3621016A = 10\times 16^0+6\times 16^1+1\times 16^2=362_{10}.

3. Biểu diễn số nguyên và số thực

3.1. Biểu diễn số nguyên

Trong Tin học, các số nguyên có thể được biểu diễn dưới dạng có dấu hoặc không dấu. Để biểu diễn số nguyên, ta có thể chọn kích thước số nguyên là 11 byte, 22 byte, 44 byte$,...,2^N$ byte, mỗi cách chọn sẽ tương ứng với một khoảng giá trị có thể biểu diễn.

Đối với số nguyên không âm, kích thước 2N2^N byte sẽ lưu trữ được các số trong phạm vi từ 00 tới (28×2N1),(2^{8 \times 2^N} - 1), bởi 11 byte gồm 88 bit và toàn bộ các bit đều được sử dụng để biểu diễn giá trị số.

Đối với số nguyên có dấu, bit bên phải nhất của số nguyên sẽ được dùng để thể hiện dấu của số đó, quy ước 11 là dấu âm, 00 là dấu dương, các bit còn lại sẽ dùng để biểu diễn giá trị số. Theo đó, số nguyên kích thước 2N2^N sẽ biểu diễn được các giá trị trong phạm vi (28×2N1+1)(-2^{8 \times 2^N - 1} + 1) đến (28×2N11)(2^{8 \times 2^N - 1} - 1). Vấn đề này sẽ liên quan tới việc lựa chọn kiểu dữ liệu và kiểm soát bộ nhớ chương trình khi lập trình.

<center> </center>

3.2. Biểu diễn số thực

Khác với cách viết trong Toán học, khi mà ta dùng dấu (,)(,) để ngăn cách giữa phần nguyên và phần phân; trong Tin học ta thay dấu (,)(,) bằng dấu (.)(.), và các nhóm ba chữ số cạnh nhau sẽ viết liền. Ví dụ, trong toán ta viết $123 \ 456,$$789;$ thì trong Tin học sẽ viết là 123456.789123456.789.

Một cách biểu diễn mà máy tính sử dụng để lưu trữ số thực là dạng dấu chấm động, khi mọi số thực sẽ được biểu diễn ở dạng ±M×10±K\pm M \times 10^{\pm K}. Trong đó, 0,1M<1,0,1 \le M < 1, MM được gọi là phần định trị, và KK là một số nguyên không âm được gọi là phần bậc. Ví dụ, số 123456,789123 456,789 sẽ được biểu diễn dưới dạng 0.123456789×1060.123456789 \times 10^6.

4. Phân tách các chữ số của một số nguyên

Giả sử số nguyên NNnn chữ số được biểu diễn ở hệ thập phân dưới dạng:

N=dndn1dn2...d1N=d_nd_{n-1}d_{n-2}...d_1

Phân tích cấu tạo số của N,N, ta có: N=dn×10n1+dn1×10n2+...+d1×100N=d_n \times 10^{n - 1} + d_{n-1}\times10^{n-2}+...+d_1\times 10^0 log(dn×10n1)log(N)=log(dn×10n1+dn1×10n2+...+d1×100)log(10n)\Rightarrow log(d_n\times 10^{n-1}) \le log(N)=log(d_n \times 10^{n - 1} + d_{n-1}\times10^{n-2}+...+d_1\times 10^0) \le log(10^n) (n1)log(N)n\Leftrightarrow (n-1)\le log(N) \le n. log(N)nlog(N)+1\Leftrightarrow log(N) \le n \le log(N)+1.

Giữa hai số log(N)log(N)log(N)+1log(N)+1 chỉ có duy nhất một số là [log(N)]+1[log(N)]+1. Vậy n=[log(N)]+1n=[log(N)]+1. Ta có thể cài đặt chương trình tính số chữ số của số nguyên NN như sau:

int cnt_digits(int N)
{
    int digits = 0;
    while (N != 0)
    {
        ++digits;
        N /= 10;
    }

    return digits;
}

Hoặc sử dụng trực tiếp hàm log10\text{log}10 của thư viện <cmath> trong C++:

int cnt_digits(int N)
{
    return (int)log10(N) + 1;
}

Dựa vào biểu diễn trên của số nguyên N,N, ta nhận thấy, chữ số hàng đơn vị của N chính bằng N % 10,N \ \% \ 10, chữ số hàng chục bằng N % 100,...,N \ \% \ 100,..., chữ số ở hàng thứ KK tính từ phải qua trái chính bằng N % 10KN \ \% \ 10^K. Đối với bất kỳ hệ cơ số nào ta cũng có thể coi như đang ở hệ cơ số 1010 để tìm các chữ số từ phải qua trái theo cách này.

Cài đặt:

int find_k_digits(int N, int K)
{
    int power = (int)pow(10, K);

    return N % power;
}

III. Chuyển đổi giữa các hệ cơ số

1. Chuyển đổi từ hệ cơ số 1010 sang các hệ cơ số khác

Xét số thực NN ở hệ cơ số 1010. Để tìm biểu diễn của NN trong hệ cơ số bb, ta sẽ lần lượt chuyển đổi phần nguyên và phần phân, sau đó ghép chúng lại với nhau.

1.1. Chuyển đổi phần nguyên

Xét phần nguyên của NNKK. Giả sử trong hệ đếm b, KK có giá trị là:

K=dnbn+dn1bn1+...+ d1b+d0K=d_nb^n + d_{n-1}b^{n-1} + ... + \text{ }d_1b + d_0

Do 0d0<b0 \le d_0 < b nên khi chia KK cho bb thì phần dư của phép chia là d0d_0, còn thương là:

K1=dnbn+dn1bn1+...+ d1K_1=d_nb^n + d_{n-1}b^{n-1} +...+ \text{ }d_1

Tương tự, d1d_1 chính là phần dư của phép chia K1K_1 cho b,b, và sẽ thu được K2K_2 là thương của phép chia đó. Lặp lại quá trình chia như trên tới khi thu được thương bằng 00, khi đó biểu diễn của KK ở hệ cơ số bbdn...d0d_n...d_0. Nói cách khác, ta lấy KK chia cho bb, thu nhận thương và số dư ở mỗi lần chia cho tới khi thương bằng 00, khi đó viết các số dư theo thứ tự ngược từ dưới lên trên thì sẽ thu được biểu diễn của KK ở hệ cơ số bb.

Ví dụ, với K=410K=4_{10}b=2b=2, quá trình chuyển đổi sẽ diễn ra như sau:

  • Chia 44 cho 22, thu được K1=2,d0=0K_1=2, d_0 = 0.
  • Chia 22 cho 22, thu được K2=1,d1=0K_2=1, d_1=0.
  • Chia 11 cho 22, thu được K3=0,d2=1K_3=0, d_2=1.
  • Tới đây quá trình dừng lại, thu được kết quả 410=10024_{10}=100_2.

Cài đặt:

// Chuyển phần nguyên K sang hệ đếm b, lưu vào string.
string change_integer_path(int K, int b) 
{
    string res;
    while (K != 0)
    {
        res = (char)(K % b + 48) + res;
        K /= b;
    }

    return res;
}

1.2. Chuyển đổi phần phân

Xét phần phân của số thực NNKK'. Giả sử trong hệ đếm bb, KK' có giá trị là:

K=d1b1+d2b2+...+ dmbm (1)K'=d_{-1}b^{-1}+d_{-2}b^{-2}+...+\text{ }d_{-m}b^{-m}\text{ }(1)

Nhân cả 22 vế của (1)(1) với bb, ta có:

K1=d1+d2b1+...+ dmb(m1)K'_1=d_{-1}+d_{-2}b^{-1}+...+\text{ }d_{-m}b^{-(m-1)}

Ta thấy, d1d_{-1} chính là phần nguyên của kết quả phép nhân KK' với bb, còn phần phân của kết quả là:

K2=d2b1+...+ dmb(m1) (2)K'_2=d_{-2}b^{-1}+...+\text{ }d_{-m}b^{-(m-1)}\text{ }(2)

Tiếp tục lặp lại phép nhân như trên với (2), ta sẽ thu được d2d_{-2} là phần nguyên. Làm liên tục theo cách này, cuối cùng thu được dãy d1d2d3...,d_{-1}d_{-2}d_{-3}..., trong đó 0di<b0 \le d_i < b. Nói cách khác, lấy phần phân KK' nhân liên tục với bb, ở mỗi bước thu nhận chữ số ở phần nguyên của kết quả và lặp lại quá trình nhân với phần phân của kết quả cho tới khi thu được số lượng chữ số như ý muốn.

Ví dụ, với K=0.12310,b=2K'=0.123_{10}, b=2 và cần lấy tới 55 chữ số phần phân ở kết quả, ta làm như sau:

  • 0.123×2=0.246d1=00.123 \times 2 = 0.246 \rightarrow d_{-1}=0.
  • 0.246×2=0.492d2=00.246 \times 2 = 0.492 \rightarrow d_{-2}=0.
  • 0.492×2=0.984d3=00.492 \times 2 = 0.984 \rightarrow d_{-3}=0.
  • 0.984×2=1.968d4=10.984 \times 2 = 1.968 \rightarrow d_{-4}=1.
  • 0.968×2=1.936d5=10.968 \times 2 = 1.936 \rightarrow d_{-5}=1. \cdots

Tới đây thu được kết quả 0.12310=0.00011...20.123_{10}=0.00011..._2

Cài đặt:

// Chuyển phần phân K sang hệ đếm B, lấy cnt_digits chữ số phần phân.
string change_double_path(double K, int B, int cnt_digits)
{
    string ans;
    while (ans.size() < cnt_digits)
    {
        double next_K = K * (double)B;
        int digit = (int)next_K;

        ans = ans + (char)(digit + 48);
        K = next_K - (int)next_K;
    }

    return ans;
}

2. Chuyển đổi hệ cơ số XX sang hệ cơ số YY

Để chuyển một số từ hệ cơ số XX sang hệ cơ số Y,Y, ta làm theo các bước sau:

  • Bước 11: Tính giá trị của XX, nói cách khác là chuyển XX sang hệ cơ số 1010.
  • Bước 22: Chuyển kết quả vừa tìm được sang hệ cơ só YY theo phương pháp chuyển một số ở hệ 1010 sang hệ YY ở phần 11.

Cài đặt:

// Chuyển số thực N từ hệ đếm X sang hệ đếm Y, lấy D chữ số sau dấu phẩy.
string change_X_to_Y(double N, int X, int Y, int D)
{
    string NN = to_string(N);
    double value_X = get_value(NN, X);

    int integer_path = (int)value_X;
    double double_path = value_X - integer_path;

    string res = change_integer_path(integer_path, Y) + '.' + change_double_path(double_path, Y, D);
    return res;
}

3. Chuyển đổi giữa hệ cơ số 22 (hệ nhị phân) và hệ cơ số 1616 (hệ Hexa)

Do 1616 là lũy thừa của 22 (16=24)(16=2^4), nên việc chuyển đổi giữa hệ nhị phân và hệ hexa có thể được thực hiện dễ dàng theo quy tắc sau:

  • Bước 11: Tính từ vị trí phân cách phần nguyên và phần phân, ta gộp các chữ số thành từng nhóm 44 chữ số về hai phía trái phải, nếu thiếu chữ số sẽ thay bằng các chữ số 00.
  • Bước 22:Tính giá trị của từng nhóm chữ số, sau đó thay kết quả bằng một kí hiệu tương ứng ở hệ Hexa. Ví dụ 22 tương ứng với 22, 1010 tương ứng với AA,...
  • Bước 33: Đặt các kí hiệu sau khi đã chuyển đổi vào đúng thứ tự của từng nhóm, ta thu được kết quả chuyển đổi.

Cài đặt:

char hexa[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

string binary_to_hexa(double N)
{
    string NN = to_string(N);
    int pos = NN.find('.');
    string left_path = NN.substr(0, pos), right_path = NN.substr(pos + 1, NN.size() - pos);

    // Bổ sung đủ chữ số 0 để tạo thành các nhóm 4.
    while (left_path.size() % 4 != 0)
        left_path = '0' + left_path;
    while (right_path.size() % 4 != 0)
        right_path = right_path + '0';

    string ans_left, ans_right;
    for (int i = 0; i < left_path.size() - 3; i += 4)
    {
        // Gộp cụm 4 kí tự liên tiếp.
        string group = left_path.substr(i, 4);

        // Tính giá trị cụm 4 kí tự.
        int power = 1, value = 0;
        for (int j = 3; j >= 0; --j)
        {
            value += (group[j] - '0') * power;
            power *= 2;
        }

        // Lấy kí tự hexa mang giá trị tương ứng.
        ans_left = ans_left + hexa[value];
    }
    for (int i = 0; i < right_path.size() - 3; ++i)
    {
        string group = left_path.substr(i, 4);

        int power = 1, value = 0;
        for (int j = 3; j >= 0; --j)
        {
            value += (group[j] - '0') * power;
            power *= 2;
        }

        ans_right = ans_right + hexa[value];
    }

    return (ans_left + '.' + ans_right);
}

Ở chiều hướng ngược lại, khi chuyển từ hệ nhị phân sang hệ hexa, chúng ta chỉ cần đổi từng kí tự hexa sang cụm bốn kí tự nhị phân có giá trị tương ứng, công việc này có thể thực hiện dễ dàng như sau:

void hexa_to_binary(double N)
{
    string NN = to_string(N);
    for (int i = 0; i < NN.size(); ++i)
        switch (NN[i])
        {
            case '0':
                cout << "0000";
                break;
            case '1':
                cout << "0001";
                break;
            case '2':
                cout << "0010";
                break;
            case '3':
                cout << "0011";
                break;
            case '4':
                cout << "0100";
                break;
            case '5':
                cout << "0101";
                break;
            case '6':
                cout << "0110";
                break;
            case '7':
                cout << "0111";
                break;
            case '8':
                cout << "1000";
                break;
            case '9':
                cout << "1001";
                break;
            case 'A':
                cout << "1010";
                break;
            case 'B':
                cout << "1011";
                break;
            case 'C':
                cout << "1100";
                break;
            case 'D':
                cout << "1101";
                break;
            case 'E':
                cout << "1110";
                break;
            case 'F':
                cout << "1111";
                break;
            default: // Trường hợp dấu '.' ngăn cách phần nguyên và phần phân.
                continue;
                break;
        }
}

IV. Tài liệu tham khảo


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.