Nội dung bài viết

Đề bài: viết hàm tính tổng các chữ số của một số tự nhiên cho trước. Ví dụ với 232, kết quả là 2 + 3 + 2 = 7.

Đây là bài tập rất hay gặp khi mới học lập trình. Nó đơn giản nhưng dạy được cách tách một số thành từng chữ số, kỹ thuật dùng lại trong nhiều bài toán khác.

Cách 1: dùng phép chia lấy dư

<?php
function sumDigit( int $number ): int {
    $sum = 0;
    while ( $number > 0 ) {
        $digit  = $number % 10;
        $sum   += $digit;
        $number = intdiv( $number, 10 );
    }
    return $sum;
}

echo sumDigit( 232 ); // 7

Giải thích

Ý tưởng cốt lõi nằm ở hai phép toán:

  • $number % 10 cho ra chữ số cuối cùng. Với 232, phép này trả về 2.
  • intdiv( $number, 10 ) cắt bỏ chữ số cuối. Với 232, kết quả là 23.

Lặp lại hai bước đó cho tới khi số về 0 là ta duyệt qua toàn bộ chữ số.

Theo dõi từng vòng lặp với đầu vào 232:

Vòng$number đầu vòng$digit$sum$number cuối vòng
12322223
223352
32270

Đến đây $number bằng 0, điều kiện $number > 0 không còn đúng, vòng lặp dừng và hàm trả về 7.

Một chi tiết nhỏ nhưng quan trọng

Bản viết đầu tiên của tôi dùng công thức này để cắt chữ số cuối:

$number = ( $number - $digit ) / 10;

Nó cho ra kết quả đúng, nhưng trong PHP phép / luôn trả về float chứ không phải int. Với số nhỏ thì không thấy vấn đề gì, nhưng khi làm việc với số lớn, float chỉ giữ được khoảng 15 tới 17 chữ số có nghĩa và bắt đầu sai lệch.

intdiv() chia lấy phần nguyên và luôn trả về int, nên vừa đúng ý đồ vừa an toàn hơn. Nếu dùng PHP cũ hơn phiên bản 7, thay bằng (int) ( $number / 10 ).

Cách 2: xử lý như chuỗi

<?php
function sumDigit( int $number ): int {
    return array_sum( str_split( (string) $number ) );
}

Chuyển số thành chuỗi, tách từng ký tự rồi cộng lại. Ngắn gọn và dễ đọc, PHP tự ép kiểu ký tự sang số khi cộng.

Nhược điểm là phải cấp phát thêm một mảng và một chuỗi, chậm hơn cách 1 một chút. Với bài toán thực tế thì khác biệt này không đáng kể.

Cách 3: đệ quy

<?php
function sumDigit( int $number ): int {
    if ( $number < 10 ) {
        return $number;
    }
    return ( $number % 10 ) + sumDigit( intdiv( $number, 10 ) );
}

Cùng ý tưởng với cách 1 nhưng diễn đạt bằng đệ quy. Trường hợp dừng là khi số chỉ còn một chữ số.

Xử lý số âm và số 0

Cả ba cách trên đều trả về 0 nếu đầu vào là 0, điều này đúng. Nhưng với số âm thì cách 1 và cách 3 trả về 0 vì vòng lặp không chạy lần nào, còn cách 2 sẽ cố cộng cả dấu trừ vào.

Nếu muốn hàm chấp nhận cả số âm, chuẩn hóa đầu vào ngay từ đầu:

function sumDigit( int $number ): int {
    $number = abs( $number );
    $sum    = 0;
    while ( $number > 0 ) {
        $sum   += $number % 10;
        $number = intdiv( $number, 10 );
    }
    return $sum;
}

Vì sao nên học cách 1

Cách 2 ngắn hơn và trong công việc thật tôi sẽ chọn nó. Nhưng cách 1 mới là cách đáng bỏ công hiểu, vì cặp % 10/ 10 xuất hiện ở rất nhiều bài toán khác: kiểm tra số đối xứng, đảo ngược một số, tính chữ số kiểm tra của số thẻ theo thuật toán Luhn, chuyển đổi giữa các hệ cơ số. Nắm được nó là mở khóa được cả một nhóm bài toán.