Làm thế nào để chọn được thuật toán tốt nhất, thông thường căn cứ theo các tiêu chuẩn sau: 1. Giải thuật đúng đắn 2. Giải thuật đơn giản 3. Giải thuật thực hiện nhanh Áp dụng những kiến thức về vòng lặp, mảng ... chúng em đã xây dựng được thuật toán giải quyết bài toán Biểu diễn số nguyên lớn. Bài toán được thực hiện bằng ngôn ngữ C sharp để thiết lập thêm giao diện nhằm thuận lợi cho việc sử dụng.
Trang 1LỜI NÓI ĐẦU
Phân tích và thiết kế giải thuật là một môn học cơ sở ngành rất quan trọng đối với sinh viên công nghệ thông tin Có thể xem đây là nền tảng của lập trình Đối với một bài toán nào đó, chúng
ta có thể có nhiều giải thuật khác nhau Vấn đề đặt ra là làm thế nào để chọn được thuật toán tốt nhất, thông thường căn cứ theo các tiêu chuẩn sau:
1 Giải thuật đúng đắn
2 Giải thuật đơn giản
3 Giải thuật thực hiện nhanh
Làm thế nào để xây dựng một thuật toán tối ưu nhất, cách tổ chức cơ sở dữ liệu hợp lí và cung cấp kiến thức cho sinh viên về tác động của giải thuật với dữ liệu Đó chính là mục đích của môn học này
Với đồ án môn học này, mục đích của chúng em là củng cố thêm kiến thức của mình về việc thiết kế thuật toán sao cho hợp lí, đánh giá độ phức tạp của thuật toán một cách chính xác và hơn hết là có thể xây dựng một chương trình theo yêu cầu sao cho tối ưu nhất
Áp dụng những kiến thức về vòng lặp, mảng chúng em đã xây dựng được thuật toán giải quyết bài toán Biểu diễn số nguyên lớn Bài toán được thực hiện bằng ngôn ngữ C sharp để thiết lập thêm giao diện nhằm thuận lợi cho việc sử dụng
Dưới sự hướng dẫn nhiệt tình của thầy Lê Quý Lộc, chúng em đã hoàn thành tốt nhất đồ án môn học Phân tích và thiết kế giải thuật Chúng em mong sẽ nhận được sự quan tâm và góp ý từ phía thầy để bài làm của chúng em được hoàn thiện
Trang 2ĐỀ BÀI
Số nguyên lớn là một mảng các chữ số Yêu cầu:
Xây dựng các hàm tiện ích để tính toán trên số lớn: tăng 1 số 1 đơn vị, tổng hai số, tích hai số
Viết chương trình minh họa việc sử dụng các hàm này Các số có không quá n chữ
số Đánh giá độ phức tạp của các hàm theo n
Viết chương trình nhập n và in ra số Fibonacci thứ n (n >1000)
Trang 3MỤC LỤC
LỜI NÓI ĐẦU 1
ĐỀ BÀI 2
MỤC LỤC 3
I GIỚI THIỆU 4
II BIỂU DIỄN DỮ LIỆU VÀO RA 4
III THUẬT TOÁN CỘNG 5
1 Thuật toán cộng 2 số 5
2 Thuật toán cộng 1 số thêm 1 đơn vị 6
3 Cài đặt 6
4 Đánh giá 7
IV THUẬT TOÁN TRỪ 7
1 Thuật toán trừ 2 số 7
2 Thuật toán trừ 1 số đi 1 đơn vị 8
3 Cài đặt 8
4 Đánh giá 9
V THUẬT TOÁN NHÂN 10
1 Thuật toán nhân 10
2 Cài đặt 10
3 Đánh giá 11
VI THUẬT TOÁN CHIA 12
1 Thuật toán chia 12
2 Cài đặt 12
3 Đánh giá 15
VII THUẬT TOÁN TÍNH SỐ FIBONACCI THỨ N 16
1 Thuật toán 16
2 Cài đặt 17
3 Đánh giá 17
KẾT LUẬN 19
CÁC NGUỒN THAM KHẢO 20
Trang 4I GIỚI THIỆU
Trong quá trình lập trình chắc có lẽ chúng ta cũng đã gặp một số trường hợp số xử lí quá lớn
mà các kiểu double hay long double cũng không lưu trữ được
Đa phần các phương pháp đều tập trung vào chia nhỏ số lớn ra để xử lí, sau đó nối các phần lại với nhau theo thứ tự để hoàn thiện Tuy nhiên, có một cách đơn giản hơn và khá hiệu quả, đó
là xử lí số lớn bằng mảng Coi số lớn như một mảng các chữ số, việc tính toán trên số lớn chính
là tính toán trên các phần tử của mảng Sau đó thực hiện cộng các phần tử của mảng tương ứng
để được mảng mới hiện thị ra màn hình
Chẳng hạn, muốn biểu diễn số 1 tỷ thay vì viết 1.000.000.000 thì ta thể viết thành 109 Việc làm như vậy giúp chúng ta dễ đọc hiểu hơn, tránh được tình trạng thiếu sót, thừa chữ số
Ứng dụng trong thiên văn, mã hóa, máy tính (số bits trên đĩa cứng) …
II BIỂU DIỄN DỮ LIỆU VÀO RA
Dữ liệu được nhập vào từ bàn phím thông qua 1 inputbox, chương trình có giao diện như
dưới đây:
Hình 1: nhập dữ liệu vào chương trình
Dữ liệu ra được xuất trên màn hình thông qua 1 textbox bên phải cửa sổ như sau:
Trang 5Hình 2: dữ liệu được hiển thị thông qua textbox
III THUẬT TOÁN CỘNG
1 Thuật toán cộng 2 số
Đầu vào: bi1 và bi2 là 2 số cần tính tổng
Đầu ra: result là kết quả của phép cộng bi1 và bi2
Xử lý:
Bước 1:
o So sánh độ dài của bi1 và bi2 Giả sử bi1 có độ dài lớn hơn bi2
o Khởi tạo result chứa kết quả với độ dài của result bằng độ dài bi1 + 1
Bước 2:
o Khởi tạo biến carry = 0
o Cộng lần lượt các chữ số của bi1, bi2 và carry từ hàng đơn vị lên For i = 0 to result.length - 1 do
result.data[i] = bi1.data[i] + bi2.data[i] + carry
if result.data[i] > 9 then
carry = 1 result.data[i] -= 10
Trang 6else
carry = 0 endif
endfor
Bước 3: Tính toán lại độ dài của result cho phù hợp và kết thúc thuật toán
Ví dụ: 1234567891011121314151617181920 + 98765432123456789 = 1234567891011220079583740638709
2 Thuật toán cộng 1 số thêm 1 đơn vị
Đầu vào: bi là số cần tăng lên 1 đơn vị
Đầu ra: bi là số đã được tăng lên 1 đơn vị
Xử lý:
Bước 1: khởi tạo carry = 1 và tăng chiều dài bi lên 1
Bước 2: cộng carry vào bi
for i = 0 to bi.length – 1 do
bi.data[i] += bi.data[i] + carry
if bi.data[i] > 9 then
carry = 1 bi.data[i] -= 10 else
break endif
endfor
Bước 3: tính toán lại độ dài của bi cho phù hợp và kết thúc thuật toán
Ví dụ: 1234567891011121314151617181920++ = 1234567891011121314151617181921
3 Cài đặt
Thuật toán cộng 2 số:
BigInt result = new BigInt();
BigInt own = bi1 length > bi2 length ? bi1 : bi2 ;
result length = own length ;
if ( result length < result Capacity ) result length ++;
byte carry = 0;
for ( int = 0; i < result length ; i ++)
{
result data [ ] = ( byte )( bi1 data [ ] + bi2 data [ ] + carry );
if ( result data [ ] > 9)
{
carry = 1;
result data [ ] -= 10;
}
else
{
carry = 0;
}
}
if ( carry > 0) throw new OverflowException();
result AdjustLength ();
Trang 7Thuật toán tăng 1 số lên 1 đơn vị:
if ( bi length < bi Capacity ) bi length ++;
byte carry = 1;
for ( int = 0; i < bi length ; i ++)
{
bi data [ ] = ( byte )( bi data [ ] + carry );
if ( bi data [ ] > 9)
{
carry = 1;
bi data [ ] -= 10;
}
else
{
carry = 0;
break ;
}
}
if ( carry > 0) throw new OverflowException();
bi AdjustLength ();
4 Đánh giá
Thuật toán cộng ở trên là thuật toán tối ưu Độ phức tạp của nó là hàm bậc nhất theo n và
phụ thuộc vào chiều dài(số lượng chữ số) lớn nhất của trong 2 số tham gia vào phép cộng
Trong mọi trường hợp số lần lặp luôn là n + 1 với n là chiều dài lớn nhất của 2 số tham gia vào phép cộng vì thế nên độ phức tạp luôn là n
Thuật toán tăng 1 số lên 1 đơn vị cũng tương tự thuật toán cộng với độ phức tạp là hàm
bậc nhất theo n với n là chiều dài của số cần tăng lên 1 đơn vị
Ưu điểm:
Dễ cài đặt
Thực hiện nhanh
Đơn giản
Nhược điểm: vì xử lý trên mảng tĩnh nên bị giới hạn kích thước bộ nhớ, nghĩa là với đầu vào quá lớn và cài đặt tổng kích thước bộ nhớ cho phép của mảng không đáp ứng được thì sẽ
dễ bị tràn dữ liệu
IV THUẬT TOÁN TRỪ
1 Thuật toán trừ 2 số
Đầu vào: bi1 và bi2 lần lược là số bị trừ và số trừ
Đầu ra: result chứa kết quả của phép trừ bi1 và bi2
Xử lý:
Bước 1: cài đặt biến carry = 0, đặt chiều dài của result bằng chiều dài lớn nhất của
1 trong 2 số bi1 hoặc bi2
Trang 8 Bước 2: trừ lần lược các chữ số của bi1, bi2 và carry từ hàng đơn vị lên
for i = 0 to result.length - 1 do
result.data[i] = bi1.data[i] – bi2.data[i] – carry
if result.data[i] < 0 then
carry = 1 result.data[i] += 10 else
carry = 0 endif
endfor
Bước 3: tính toán lại độ dài của result cho phù hợp và kết thúc thuật toán
Ví dụ: 1234567891011121314151617181920 - 98765432123456789 = 1234567891011022548719493725131
2 Thuật toán trừ 1 số đi 1 đơn vị
Đầu vào: số bi là số cần giảm đi 1 đơn vị
Đầu ra: số bi là số đã giảm đi 1 đơn vị
Xử lý:
Bước 1: cài đặt biến carry = 1
Bước 2: trừ số bi cho biến carry
for i = 0 to bi.length - 1 do
bi.data[i] -= carry
if bi.data[i] < 0 then
carry = 1 bi.data[i] -= 10;
else
break endif
endfor
Bước 3: tính toán lại độ dài của số bi cho phù hợp và kết thúc thuật toán
Ví dụ: 1234567891011121314151617181920 = 1234567891011121314151617181919
3 Cài đặt
Thuật toán trừ 2 số:
BigInt result = new BigInt();
byte [] temp1 , temp2 ;
int comp = bi1 CompareTo ( bi2 , true );
if ( comp == 0)
{
result data [0] = 0;
result length = 1;
return result ;
}
else if ( comp == 1)
{
temp1 = bi1 data ;
Trang 9temp2 = bi2 data ;
result length = bi1 length ;
}
else
{
temp1 = bi2 data ;
temp2 = bi1 data ;
result length = bi2 length ;
}
sbyte temp , carry = 0;
for ( int = 0; i < result length ; i ++)
{
temp = ( sbyte )( temp1 [ ] - temp2 [ ] - carry );
if ( temp < 0)
{
carry = 1;
result data [ ] = ( byte )( temp + 10);
}
else
{
carry = 0;
result data [ ] = ( byte ) temp ;
}
}
result AdjustLength ();
Thuật toán trừ 1 số đi 1 đơn vị:
byte carry = 1;
sbyte temp ;
for ( int = 0; i < bi length ; i ++)
{
temp = ( sbyte )( bi data [ ] - carry );
if ( temp < 0)
{
carry = 1;
bi data [ ] = ( byte )( temp + 10);
}
else
{
bi data [ ] = ( byte ) temp ;
break ;
}
}
bi AdjustLength ();
4 Đánh giá
Thuật toán trừ ở trên là thuật toán tối ưu Độ phức tạp của nó là hàm bậc nhất theo n và
phụ thuộc vào chiều dài (số lượng chữ số) lớn nhất của trong 2 số tham gia vào phép trừ Trong
mọi trường hợp số lần lặp luôn là n với n là chiều dài lớn nhất của 2 số tham gia vào phép trừ
vì thế nên độ phức tạp luôn là n
Trang 10Thuật toán giảm 1 số đi 1 đơn vị cũng tương tự thuật toán trừ với độ phức tạp là hàm bậc
nhất theo n với n là chiều dài của số cần giảm đi 1 đơn vị
Ưu điểm:
Dễ cài đặt
Thực hiện nhanh
Đơn giản
V THUẬT TOÁN NHÂN
1 Thuật toán nhân
Đầu vào: số bi1 và số bi2
Đầu ra: result là kết quả phép nhân giữa bi1 và bi2
Xử lý:
Bước 1: kiểm tra xem nếu 1 trong 2 số bằng 0 thì gán result bằng 0 và kết thúc thuật toán
Bước 2: khởi tạo 2 biến temp1 và temp2, với temp1 tham chiếu đến số có độ dài lớn hơn, temp2 tham chiếu đến số có độ dài nhỏ hơn trong 2 số bi1 và bi2 Khởi tạo result để lưu kết quả phép nhân Khởi tạo biến k = 0
Bước 3: thực hiện nhân lần lượt từng chữ số của temp2 cho temp1
for i = 0 to temp2.length – 1 do
if temp2.data[i] = 0 then
continue endif
carry = 0
k = i for j = 0 to temp1.length – 1 do
temp = temp1.data[j] * temp2.data[i] + result.data[k] + carry carry = temp / 10
result.data[k] = temp % 10
k += 1 endfor
if carry > 0 then
result.data[i + temp1.length] = carry endif
endfor
Bước 4: tính toán lại độ dài của result cho phù hợp và kết thúc thuật toán
Ví dụ: 1234567891011121314151617181920 * 98765432123456789
= 121932631241458101023327185205570305877072054880
2 Cài đặt
Thuật toán nhân 2 số:
Trang 11BigInt result = new BigInt();
if ( bi1 length == 1
&& bi1 data [0] == 0 || bi2 length == 1 && bi2 data [0] == 0) {
result length = 1;
result data [0] = 0;
return result ;
}
BigInt temp1 , temp2;
byte temp , carry ;
if ( bi1 length > bi2 length )
{
temp1 = bi1 ;
temp2 = bi2 ;
}
else
{
temp1 = bi2 ;
temp2 = bi1 ;
}
for ( int = 0; i < temp2 length ; i ++)
{
if ( temp2 data [ ] == 0) continue ;
carry = 0;
for ( int = 0, k = i ; j < temp1 length ; j ++, k ++)
{
temp = ( byte )( temp1 data [ ] * temp2 data [ ] + result data [ ] + carry ); carry = ( byte )( temp / 10);
result data [ ] = ( byte )( temp % 10);
}
if ( carry > 0)
{
if ( i + temp1 length >= result Capacity ) throw new OverflowException(); result data [ + temp1 length ] = carry ;
}
}
result length = bi1 length + bi2 length ;
result AdjustLength ();
3 Đánh giá
Thuật toán nhân trên sẽ tiến hành nhân lần lượt từng chữ số của số có ít chữ số cho số có nhiều chữ số do đó sẽ cần đến 2 vòng lặp lồng nhau để thực hiện Trong trường hợp xấu nhất, tất cả các số của số ít chữ số hơn đều khác 0 thì độ phức tạp của thuật toán là hàm bậc 2 theo
độ dài của số nhân Trong trường hợp tốt nhất, một trong 2 số bằng 0 thì độ phức tạp chỉ bằng
1
Ưu điểm:
Đơn giản để cài đặt
Hiệu quả và thực hiện nhanh
Trang 12Nhược điểm: giống như phép cộng, do kích thước mảng bị cố định nên với những dữ liệu quá lớn và vượt quá khả năng lưu trữ của mảng sẽ gây tràn dữ liệu
VI THUẬT TOÁN CHIA
1 Thuật toán chia
Đầu vào: 2 số bi1 và bi2 lần lược là số bị chia và số chia
Đầu ra: result là kết quả phép chia bi1 cho bi2
Xử lý:
Thuật toán 1: thuật toán chia đơn giản, ý tưởng của thuật toán này là lấy số bị chia trừ dần cho số chia và sau mỗi lần trừ ta tăng biến đếm lên 1 cho đến khi số bị chia nhỏ hơn số chia thì dừng lại Kết quả của biến đếm sẽ là thương số cần tìm
Bước 1: kiếm tra các trường hợp đặt biệt và các trường hợp lỗi
o Nếu bi2 = 0 thì thông báo lỗi chia cho 0
o Nếu bi1 = 0 thì gán result bằng 0 và kết thúc thuật toán
o Nếu bi1 = bi2 thì gán result bằng 1 và kết thúc thuật toán
o Nếu bi2 = 1 thì gán result bằng bi1 và kết thúc thuật toán
Bước 2: khởi tạo biến temp và sao chép dữ liệu của bi1 vào temp
Bước 3: temp = temp – bi2
Bước 4: nếu temp còn lớn hơn hoặc bằng bi2 thì tăng biến đếm lên 1 và quay lại bước 3
Bước 5: gán biến đếm cho result và kết thúc thuật toán
Thuật toán 2: thuật toán chia phân đoạn, ý tưởng của thuật toán này là ta sẽ chia
số bị chia thành các phần (part) có độ dài bằng số chia và tiến hành chia nó cho số
chia bằng thuật toán 1
Bước 1: kiếm tra các trường hợp đặc biệt và các trường hợp lỗi
o Nếu bi2 = 0 thì thông báo lỗi chia cho 0
o Nếu bi1 = 0 thì gán result bằng 0 và kết thúc thuật toán
o Nếu bi1 = bi2 thì gán result bằng 1 và kết thúc thuật toán
o Nếu bi2 = 1 thì gán result bằng bi1 và kết thúc thuật toán
Bước 2: lấy 1 phần của số bị chia (part) có độ dài bằng độ dài số chia và
lấy từ bên trái cùng sang Thực hiện phép chia part cho bi2 bằng thuật
toán 1 Phần dư sẽ được lưu trở lại part, kết quả phép chia sẽ có giá trị từ 0
đến 9 và được lưu vào result
Bước 3: kiếm tra nếu part không phải là bên phải cùng thì dịch part sang phải 1 chữ số và quay lại bước 2
Bước 4: tính toán lại độ dài của result và kết thúc thuật toán
Ví dụ: 1234567891011121314151617181920 / 98765432123456789 = 12499999893362
2 Cài đặt
Các hàm hỗ trợ cho 2 thuật toán:
private static BigInt check_div (BigInt bi1 , BigInt bi2 )