Chương 7: Phép lặp và vòng lặp

7.1  Phép gán nhiều lần

Bạn có thể khiến cho nhiều lệnh gán chỉ tới cùng một biến; mà hiệu quả của nó là nhằm thay thế giá trị cũ bằng giá trị mới.

    int liz = 5; 
    System.out.print(liz); 
    liz = 7; 
    System.out.println(liz);

Kết quả của chương trình này bằng 57, vì lần đầu tiên khi in liz biến này có giá trị bằng 5, còn lần thứ hai thì biến có giá trị bằng 7.

Hình thức gán nhiều lần như thế này là lí do mà tôi mô tả các biến như là hộp chứa giá trị. Khi bạn gán một giá trị vào cho biến, bạn thay đổi nội dung của hộp chứa, như ở hình vẽ sau:

Khi có nhiều phép gán đối với cùng một biến, thì rất chú trọng việc phân biệt giữa câu lệnh gán và đẳng thức. Vì Java dùng dấu = cho lệnh gán nên ta bị lôi cuốn vào việc diễn giải một câu lệnh như a = b là câu lệnh đẳng thức. Thật ra không phải vậy!

Trước hết, đẳng thức thì có tính giao hoán, còn lệnh gán thì không. Chẳng hạn trong toán học, nếu a = 7 thì 7 = a. Nhưng trong Java a = 7; lại là một lệnh gán hợp lệ, còn 7 = a; thì không.

Hơn nữa, trong toán học, một đẳng thức thì luôn đúng. Nếu bây giờ a = b, thì a sẽ luôn bằng b. Trong Java, một lệnh gán có thể làm cho hai biến bằng nhau, nhưng không có gì bắt buộc chúng bằng nhau mãi!

    int a = 5; 
    int b = a; // bây giờ thì a bằng b  
    a = 3; // a không còn bằng b nữa

Dòng lệnh thứ ba đã thay đổi giá trị của a mà không làm thay đổi giá trị của b, vì vậy chúng không còn bằng nhau. Một số ngôn ngữ lập trình có dùng kí hiệu khác cho phép gán, như <- hoặc :=, để tránh sự nhầm lẫn này.

Mặc dù phép gán nhiều lần thường có ích, so bạn nên cẩn thận khi dùng. Nếu giá trị của các biến thay đổi thường xuyên thì có có thể khiến cho mã lệnh khó đọc và gỡ lỗi.

7.2  Câu lệnh while

Máy tính thường được dùng để tự động hóa các thao tác có tính lặp lại. Thực hiện những thao tác lặp lại này mà không phạm lỗi là điều mà máy tính làm tốt còn chúng ta làm rất dở.

Ta đã thấy các phương thức như countdown và factorial trong đó dùng đệ quy để thực hiện lặp. Quá trình này được gọi là phép lặp. Java có những đặc điểm ngôn ngữ giúp cho việc viết các phương thức nêu trên một cách dễ dàng hơn. Ở chương này ta xem xét câu lệnh while. Về sau (ở Mục 12.4) ta xét đến câu lệnh for.

Dùng câu lệnh while, ta có thể viết lại countdown:

  public static void countdown(int n) { 
    while (n > 0) { 
      System.out.println(n); 
      n = n-1; 
    } 
    System.out.println("Bum!"); 
  }

Gần như là bạn có thể đọc được toàn bộ câu lệnh while bằng tiếng Anh. Lệnh này diễn tả là, “Khi n lớn hơn không, hãy in giá trị của n rồi giảm giá trị của n xuống 1. Khi bạn đạt đến không, hãy in ra từ ‘Bum!”’

Theo cách quy củ hơn, luồng thực thi của một lệnh while như sau:

  1. Định giá điều kiện trong cặp ngoặc tròn, cho ra true hoặc false.
  2. Nếu điều kiện là sai, thì thoát khỏi lệnh while rồi tiếp tục thực thi câu lệnh liền sau.
  3. Nếu điều kiện là đúng, thì thực thi những câu lệnh trong phạm vi cặp ngoặc nhọn, rồi trở lại bước 1.

Kiểu luồng thực thi này được gọi là vòng lặp vì bước thứ ba vòng ngược trở lên đầu. Những câu lệnh bên trong vòng lặp được gọi là thân của vòng lặp. Nếu điều kiện là sai ngay lần đầu tiên qua vòng lặp thì những câu lệnh bên trong vòng lặp không bao giờ được thực thi.

Phần thân vòng lặp cần phải thay đổi giá trị của một vài biến sao cho cuối cùng thì điều kiện trở nên sai và vòng lặp chấm dứt. Nếu không, vòng sẽ được lặp lại mãi, và được gọi là vòng lặp vô hạn. Một câu chuyện đùa luôn được nhắc đến trong giới khoa học máy tính là qua việc nhận thấy chỉ dẫn trên gói dầu gội đầu, “Xát, xả nước, rồi lặp lại,” chính là một vòng lặp vô hạn.

Ở trường hợp countdown, ta có thể chứng minh rằng vòng lặp sẽ kết thúc nếu n là số dương. Còn trong những trường hợp khác thì không dễ nói trước:

  public static void sequence(int n) { 
    while (n != 1) { 
      System.out.println(n); 
      if (n%2 == 0) { // n chẵn 
        n = n / 2; 
      } else { // n lẻ 
        n = n*3 + 1; 
      } 
    } 
  }

Điều kiện của vòng lặp này là n != 1, vì vậy vòng lặp sẽ tiếp diễn đến tận khi n bằng 1, và điều này khiến cho điều kiện bị sai đi.

Tại mỗi vòng lặp, chương trình in ra giá trị của n rồi kiểm tra xem liệu số này chẵn hay lẻ. Nếu chẵn, giá trị của n được chia cho 2. Nếu lẻ, giá trị được thay thế bởi 3n+1. Chẳng hạn, nếu giá trị ban đầu (tức đối số được truyền vào sequence) bằng 3, thì kết quả là ta có dãy 3, 10, 5, 16, 8, 4, 2, 1.

Vì đôi khi n tăng và đôi khi giảm, nên sẽ không có cách chứng minh nào dễ thấy ràng cuối cùng n sẽ đạt đến 1, hay chương trình sẽ kết thúc. Với một số giá trị đặc biệt của n, ta có thể chứng minh được sự kết thúc đó. Chẳng hạn, nếu giá trị khởi đầu là một số lũy thừa của hai, thì giá trị của n sẽ luôn chẵn qua mỗi lần lặp, cho đến khi ta thu được 1. Ví dụ trước sẽ kết thúc với một dãy như vậy với giá trị ban đầu bằng 16.

Ngoài những giá trị đặc biệt, thì một câu hỏi thú vị là liệu ta có thể chứng minh được rằng đoạn chương trình trên có kết thúc với tất cả những giá trị của n hay không. Cho đến giờ, chưa ai có thể chứng minh hoặc bác bỏ nó! Bạn hãy tìm thêm thông tin ở http://en.wikipedia.org/wiki/Collatz_conjecture.

7.3  Bảng số liệu

Một trong những công việc thích hợp với dùng vòng lặp, đó là phát sinh ra bảng số liệu. Trước khi máy tính trở nên phổ biến, mọi người đã phải tính tay các phép logarit, sin, cosin, và những hàm toán học khác.

Để đơn giản hóa việc này, sách toán thường in kèm những bảng dài liệt kê giá trị các hàm nói trên. Việc tạo ra các bảng như vậy rất chậm và nhàm chán, và dễ mắc phải nhiều lỗi.

Khi máy tính xuất hiện, đã có những phản ứng ban đầu kiểu như: “Điều này thật tuyệt! Giờ ta có thể dùng máy tính để tạo ra các bảng, vì vậy sẽ không có lỗi.” Điều này trở nên (gần như là) sự thật nhưng vẫn chứa đựng tầm nhìn hạn hẹp. Không lâu sau đó, máy tính và máy bỏ túi đã xuất hiện tràn lan và bảng số trở nên lỗi thời.

Ừ, gần như vậy. Có những phép tính mà máy tính lấy con số từ bảng để có giá trị gần đúng, rồi thực hiện tính toán nhằm cải thiện kết quả gần đúng này. Ở trường hợp khác, có những lỗi nằm ngay ở bảng số, được biết đến nhiều nhất là bảng mà máy Intel Pentium đã dùng để thực hiện phép chia với số có dấu phẩy động.

Mặc dù bảng loga không còn hữu dụng như xưa, song nó vẫn dùng được làm ví dụ về tính lặp. Chương trình sau in ra một dãy các số ở cột bên trái cùng với giá trị logarit của chúng ở cột phải:

    double x = 1.0; 
    while (x < 10.0) { 
      System.out.println(x + " " + Math.log(x)); 
      x = x + 1.0; 
    }

Kết quả của chương trình này là:

1.0   0.0
2.0   0.6931471805599453
3.0   1.0986122886681098
4.0   1.3862943611198906
5.0   1.6094379124341003
6.0   1.791759469228055
7.0   1.9459101490553132
8.0   2.0794415416798357
9.0   2.1972245773362196

Nhìn vào những giá trị này, bạn có thể nói rằng phương thức log này dùng cơ số nào?

Vì các lũy thừa của 2 rất quan trọng trong ngành khoa học máy tính, nên ta thường muốn lấy loga theo cơ số 2. Để tính toán, ta có thể dùng biểu thức:

log2 x = loge x / loge 2

Hãy thay câu lệnh print bằng

    System.out.println(x + " " + Math.log(x) / Math.log(2.0));

để cho ra

1.0   0.0
2.0   1.0
3.0   1.5849625007211563
4.0   2.0
5.0   2.321928094887362
6.0   2.584962500721156
7.0   2.807354922057604
8.0   3.0
9.0   3.1699250014423126

Có thể thấy rằng 1, 2, 4, và 8 là các lũy thừa của 2 vì các giá trị logarit cơ số 2 của chúng đều là những số nguyên. Nếu muốn tìm logarit của những lũy thừa khác của 2, ta có thể sửa chương trình trên thành:

    double x = 1.0; 
    while (x < 100.0) { 
      System.out.println(x + " " + Math.log(x) / Math.log(2.0)); 
      x = x * 2.0; 
    }

Bây giờ thay vì cộng thêm một số với x trong mỗi vòng lặp (điều này cho ra dãy cấp số cộng), ta đem nhân một giá trị với x (thu được cấp số nhân). Kết quả là:

1.0   0.0
2.0   1.0
4.0   2.0
8.0   3.0
16.0   4.0
32.0   5.0
64.0   6.0

Bảng logarit có thể không còn có ích nữa, nhưng với nhà khoa học máy tính, việc nhớ được các lũy thừa của hai nhất thiết có ích! Khi nào rảnh rỗi, bạn hãy ghi nhớ các lũy thừa của hai đến tận 65536 (tức là 216).

7.4  Bảng hai chiều

Trong một bảng hai chiều, bạn đọc giá trị ở điểm giao cắt giữa một hàng với một cột. Bảng cửu chương là một ví dụ điển hình. Giả sử bạn muốn in ra một bảng tính nhân với các giá trị từ 1 đến 6.

Một cách bắt đầu ổn thỏa là viết một vòng lặp để in ra các bội số của 2 trên cùng một dòng.

    int i = 1; 
    while (i <= 6) { 
      System.out.print(2*i + " "); 
      i = i + 1; 
    } 
    System.out.println("");

Dòng đầu tiên khởi tạo một biến có tên là i; nó đóng vai trò một biến đếm hoặc biến vòng lặp. Khi vòng lặp được thực thi, giá trị của i tăng từ 1 lên 6. Khi i bằng 7, vòng lặp kết thúc. Mỗi lần lặp, chương trình sẽ in ra giá trị của 2*i, theo sau là ba dấu cách.

Một lần nữa, dấu phẩy trong câu lệnh print ngăn không cho xuống dòng. Sau khi vòng lặp kết thúc, lệnh print thứ hai bắt đầu một dòng mới. Vì ta dùng System.out.print, nên toàn bộ kết quả được ghi trên một dòng. 

Có những môi trường mà kết quả từ print được lưu lại mà chưa hiển thị đến khi kích hoạt println. Nếu chương trình kết thúc, mà bạn quên kích hoạt println, có thể bạn sẽ không bao giờ thấy được kết quả được lưu lại này.

Kết quả của chương trình là:

2   4   6   8   10   12

Mọi việc đến giờ tiến triển tốt. Bước tiếp theo là bao bọc và khái quát hóa.

7.5  Bao bọc và khái quát hóa

Bao bọc là quá trình đặt một đoạn mã lệnh vào trong một phương thức; việc này cho phép ta tận dụng được những ưu điểm của phương thức. Ta đã thấy hai ví dụ về bao bọc, khi ta viết printParity ở Mục 4.3 và isSingleDigit ở Mục 6.7.

Khái quát hóa nghĩa là chọn lấy một điều cụ thể, như công việc in ra các bội số của 2, rồi làm cho nó trở thành khái quát hơn, chẳng hạn như in ra các bội số của một số nguyên bất kì.

Phương thức sau đây bao bọc đoạn mã lệnh nói trên rồi khái quát hóa nó để in ra các bội số của n.

  public static void printMultiples(int n) { 
    int i = 1; 
    while (i <= 6) { 
      System.out.print(n*i + " "); 
      i = i + 1; 
    } 
    System.out.println(""); 
  }

Để bao bọc, ta chỉ cần viết thêm dòng thứ nhất, tức là khai báo tên, tham số, và kiểu trả lại. Để khái quát hóa, ta chỉ cần thay thế giá trị 2 bởi tham số n.

Nếu ta kích hoạt phương thức này với đối số bằng 2, ta sẽ nhận được kết quả giống như trước. Với đối số bằng 3, kết quả sẽ là:

3   6   9   12   15   18

Với đối số bằng 4, kết quả là:

4   8   12   16   20   24

Bây giờ có thể bạn đã đoán được cách in một bảng tính nhân     bằng cách kích hoạt printMultiples lặp lại với những đối số khác nhau. Thực ra, ta có thể dùng một vòng khác để lặp qua các hàng trong bảng:

    int i = 1; 
    while (i <= 6) { 
      printMultiples(i); 
      i = i + 1; 
    }

Trước hết, hãy lưu ý sự giống nhau của vòng lặp này với vòng lặp bên trong printMultiples. Tất cả những gì ta đã làm chỉ là việc thay lệnh print bằng một lời kích hoạt phương thức.

Kết quả của chương trình này là

1   2   3   4   5   6
2   4   6   8   10   12
3   6   9   12   15   18
4   8   12   16   20   24
5   10   15   20   25   30
6   12   18   24   30   36

vốn là một bảng tính nhân (hơi lôi thôi). Nếu bạn không thích lôi thôi, thì Java sẵn có những phương thức giúp bạn kiểm soát chặt chẽ hơi định dạng của kết quả; song bây giờ ta không đề cập đến điều này.

7.6  Phương thức và bao bọc

Ở Mục 3.5 tôi đã liệt kê vài lý do mà phương thức trở nên có ích. Sau đây còn thêm một số lý do khác:

  • Bằng cách đặt tên cho một dãy các câu lệnh, bạn có thể làm cho chương trình mình viết trở nên dễ đọc và gỡ lỗi hơn.
  • Việc chia một chương trình dài thành nhiều phương thức cho phép bạn phân chia các phần của chương trình, tiến hành gỡ lỗi chúng một cách độc lập, rồi ghép lại thành tổng thể.
  • Phương thức cho phép cả đệ quy lẫn lặp lại.
  • Các phương thức được thiết kế tốt thì thường hữu ích cho nhiều chương trình khác nhau. Một khi đã viết ra và gỡ lỗi xong một phương thức, bạn có thể tái sử dụng nó.

Để biểu diễn tiếp kĩ thuật bao bọc, ta hãy lấy đoạn mã lệnh ở cuối mục trước rồi bọc nó vào trong một phương thức:

  public static void printMultTable() { 
    int i = 1; 
    while (i <= 6) { 
      printMultiples(i); i = i + 1; 
    } 
  }

Quá trình mà tôi hiện đang giới thiệu được gọi là bao bọc và khái quát hóa. Ta phát triển mã lệnh bằng cách viết riêng những dòng lệnh vào main hoặc vào phương thức khác. Khi mã lệnh này thực hiện được, ta lấy lại nó rồi bọc vào một phương thức. Rồi bạn khái quát hóa phương thức bằng cách bổ sung các tham số.

Lúc mới lập trình, đôi khi bạn không biết cách chia chương trình thành các phương thức. Quy trình trên giúp bạn thiết kế trong khi lập trình.

7.7  Các biến địa phương

Có thể bạn tự hỏi bằng cách nào mà ta dùng được cùng một biến, i, cả trong printMultiples lẫn printMultTable. Chẳng phải nó sẽ gây rắc rối khi một trong hai phương thức thay đổi giá trị của biến sao?

Lời giải đáp cho cả hai câu hỏi trên đều là không, vì i trong printMultiples và i trong printMultTable không phải cùng một biến. Chúng có cùng tên gọi, nhưng không tham chiếu đến cùng vị trí lưu trữ, và việc thay đổi một biến này sẽ không ảnh hưởng gì tới biến kia.

Những biến được tạo ra bên trong phần định nghĩa phương thức được gọi là biến địa phương, vì chúng chỉ tồn tại bên trong phương thức đó. Bạn không thể truy cập biến địa phương từ ngoài phương thức “chủ” của nó, và bạn có thể tùy ý đặt nhiều biến cùng tên, miễn là chúng không phải trong cùng một hàm.

Mặc dù điều này có thể gây nhầm lẫn, song có những lí do thích đáng để sử dụng lại các tên gọi. Chẳng hạn, các tên ij và k thường được dùng làm biến lặp. Nếu bạn tránh dùng chúng trong một phương thức chỉ vì bạn đã dùng chúng ở nơi khác, thì chương trình viết ra sẽ khó đọc hơn.

7.8  Nói thêm về khái quát hóa

Xét một ví dụ khác về khái quát hóa. Hãy hình dung rằng bạn  muốn có một chương trình để in ra bảng tính nhân với kích thước bất kì, chứ không chỉ 6 × 6. Bạn có thể thêm một tham số vào printMultTable:

  public static void printMultTable(int high) { 
    int i = 1; 
    while (i <= high) { 
      printMultiples(i); 
      i = i + 1; 
    } 
  }

Tôi đã thay giá trị 6 bởi tham số high. Nếu tôi kích hoạt printMultTable với đối số 7, tôi sẽ được:

1   2   3   4   5   6
2   4   6   8   10   12
3   6   9   12   15   18
4   8   12   16   20   24
5   10   15   20   25   30
6   12   18   24   30   36
7   14   21   28   35   42

Thế này tạm được, nhưng có lẽ ta muốn nhận được một bảng hình vuông hơn (số cột và số hàng phải bằng nhau). Để làm điều này, ta thêm một tham số nữa vào printMultiples để cụ thể hóa xem bảng có bao nhiêu cột.

Ta gọi tham số này là high, nhằm cho thấy các phương thức khác nhau hoàn toàn có thể chứa những tham biến có cùng tên (cũng như các biến địa phương):

  public static void printMultiples(int n, int high) { 
    int i = 1; 
    while (i <= high) { 
      System.out.print(n*i + " "); 
      i = i + 1; 
    } 
    System.out.println(""); 
  } 
  public static void printMultTable(int high) { 
    int i = 1; 
    while (i <= high) { 
      printMultiples(i, high); 
      i = i + 1; 
    } 
  }

Lưu ý rằng khi thêm một tham số mới, ta phải sửa lại dòng đầu tiên, đồng thời ta cũng phải sửa chỗ phương thức được kích hoạt trong  printMultTable. Đúng như dự kiến, chương trình này phát sinh ra bảng vuông 7 × 7:

1   2   3   4   5   6   7
2   4   6   8   10   12   14
3   6   9   12   15   18   21
4   8   12   16   20   24   28
5   10   15   20   25   30   35
6   12   18   24   30   36   42
7   14   21   28   35   42   49

Khi bạn khái quát quá một phương thức theo cách thích hợp, thường bạn sẽ thu được chương trình với những tính năng mà bạn chưa lường trước. Chẳng hạn, có thể bạn nhận thấy rằng bảng nhân có tính đối xứng, vì ab = ba, nên tất cả những con số trong bảng đều xuất hiện lặp hai lần. Lẽ ra bạn có thể tiết kiệm mực bằng cách chỉ  in ra nửa bảng thôi. Để làm điều này, chỉ cần thay đổi một dòng lệnh trong printMultTable. Hãy sửa lệnh

      printMultiples(i, high);

thành

      printMultiples(i, i);

và thu được

1
2   4
3   6   9
4   8   12   16
5   10   15   20   25
6   12   18   24   30   36
7   14   21   28   35   42   49

Tôi sẽ để bạn tự hình dung cơ chế của cách máy tính đã xử lí trong trường hợp này.

7.9  Thuật ngữ

vòng lặp:
Một câu lệnh được lặp đi lặp lại nhiều lần khi một điều kiện nào đó được thỏa mãn.
vòng lặp vô hạn:
Một vòng lặp có điều kiện luôn luôn đúng.
phần thân:
Những câu lệnh bên trong vòng lặp.
lặp:
Một lượt chạy (thực thi) qua phần thân vòng lặp, bao gồm cả việc định giá điều kiện.
bao bọc:
Việc phân chia một chương trình lớn, phức tạp thành nhiều thành phần (như phương thức) rồi cô lập riêng các thành phần (chẳng hạn, bằng cách dùng các biến địa phương).
biến địa phương:
Một biến được khai báo bên trong một phương thức; biến này chỉ tồn tại trong phương thức đó. Những biến địa phương đều không truy cập được từ ngoài phương thức của nó, và không can thiệp tới bất kì phương thức nào khác.
khái quát hóa:
Việc thay thế những thứ cụ thể một cách không cần thiết (như một giá trị không đổi) bằng những thứ có tính khái quát thích hợp (nhưng một biến hoặc một tham số). Việc khái quát hóa khiến cho mã lệnh linh hoạt hơn, dễ sử dụng lại hơn, và đôi khi dễ viết hơn.
phát triển chương trình:
Một quá trình để viết nên những chương trình máy tính. Cho đến bây giờ ta đã gặp “phát triển tăng dần” và “bao bọc và khái quát hóa”.

7.10  Bài tập

Bài tập 1  Xét đoạn mã lệnh sau:

  public static void main(String[] args) { 
    loop(10); 
  } 
  public static void loop(int n) { 
    int i = n; 
    while (i > 0) { 
      System.out.println(i); 
      if (i%2 == 0) { 
        i = i/2; 
      } else { 
        i = i+1; 
      } 
    } 
  }
  1. Hãy kẻ một bảng để chỉ ra giá trị của các biến i và n trong quá trình thực thi loop. Bảng chỉ được phép chứa một cột cho mỗi biến và một hàng cho mỗi vòng lặp.
  2. Kết quả của chương trình này là gì?
Bài tập 2   Giả sử bạn có một số, a, và bạn muốn tính căn bậc hai của nó. Một cách làm điều này là khởi đầu bằng một phỏng đoán sơ lược về đáp số, x0, và rồi cải thiện phỏng đoán này theo công thức sau:

x1 =(x0 + a/x0) / 2

Chẳng hạn, nếu ta muốn tìm căn bậc hai của 9, và bắt đầu với x0 = 6, thì x1 =(6 + 9/6) /2 = 15/4 = 3.75, giá trị này đã sát hơn. Ta có thể lặp lại quy trình này, dùng x1 để tính ra x2, và cứ như vậy. Trong trường hợp này, x2 = 3.075 và x3 = 3.00091. Như vậy nó hội tụ rất nhanh về đáp số đúng (vốn bằng 3).

Hãy viết một phương thức có tên squareRoot nhận vào tham số là một double và trả lại một giá trị xấp xỉ cho căn bậc hai của tham số đó, theo kĩ thuật tính nêu trên. Bạn không được phép dùng Math.sqrt.

Với giá trị ban đầu, bạn nên lấy a/2. Phương thức bạn viết cần phải lặp lại đến khi nó đạt được hai giá trị ước tính liên tiếp chỉ sai khác nhau chưa đến 0.0001; nói cách khác, là đến khi giá trị tuyệt đối của xn − xn−1 nhỏ hơn 0.0001. Bạn có thể dùng Math.abs để tính giá trị tuyệt đối này.

Bài tập 3   Ở Bài tập 9 ta đã viết một dạng đệ quy của power, trong đó nhận một biến double có tên x cùng một biến nguyên n and rồi trả lại xn. Bây giờ hãy viết một phương thức lặp để thực hiện tính toán như vậy.
Bài tập 4   Mục 6.8 có trình bày một phương thức đệ quy để tính hàm giai thừa. Hãy viết một dạng tính lặp cho factorial.
Bài tập 5   Một cách để tính ex là dùng khai triển chuỗi vô hạn

ex = 1 + x + x2 / 2! + x3 / 3! + x4 / 4! + …

Nếu biến vòng lặp có tên i, thì số hạng thứ i sẽ là xi / i!.

  1. Hãy viết một phương thức có tên myexp để tính tổng của n số hạng đầu tiên trong dãy này. Bạn có thể dùng phương thức factorial ở Mục 6.8 hoặc dùng phiên bản tính lặp như ở bài tập trước.
  2. Bạn có thể khiến phương thức này hiệu quả hơn nhiều nếu nhận thấy rằng ở mỗi lần lặp, tử số của số hạng thì đúng bằng tử số của số hạng liền trước đó nhân với x còn mẫu số thì đúng bằng mẫu của số hạng trước đó nhân với i. Hãy tận dụng kết quả của quan sát này để tránh dùng cả Math.pow lẫn factorial, rồi kiểm tra rằng bạn vẫn có thể đạt được kết quả y hệt.
  3. Hãy viết một phương thức có tên check nhận vào một tham số, x, để in ra giá trị của xMath.exp(x) và myexp(x) cho các giá trị x khác nhau. Kết quả phải có dạng như sau:
    1.0     2.708333333333333       2.718281828459045

    GỢI Ý: bạn có thể dùng String "\t" để in ra một dấu tab giữa các cột trong bảng.

  4. Hãy thay đổi số các số hạng trong chuỗi (chính là đối số thứ hai mà check gửi đến myexp) rồi xem sự ảnh hưởng đến độ chính xác của kết quả. Điều chỉnh giá trị này đến khi giá trị ước tính phù hợp với đáp số “đúng” khi x bằng 1.
  5. Hãy viết một vòng lặp trong main để kích hoạt check với những giá trị 0.1, 1.0, 10.0, và 100.0. Độ chính xác của kết quả sẽ thay đổi thế nào khi x biến đổi? So sánh số chữ số giống nhau thay vief hiệu số giữa các giá trị đúng và giá trị ước tính được.
  6. Thêm vào một vòng lặp trong main nhằm kiểm tra myexp với các giá trị -0.1, -1.0, -10.0, và -100.0. Hãy nhận xét về độ chính xác.
Bài tập 6   Một cách để tính exp(−x2) là dùng khai triển chuỗi vô hạn

exp(−x2) = 1 − x2 + x4/2 − x6/6 + …

Nói cách khác, ta cần phải cộng các số hạng lại, trong đó số hạng thứ i bằng (−1)i x2i /i!. Hãy viết một phương thức có tên gauss nhận vào các đối số x và n rồi trả lại tổng của n số hạng đầu tiên trong chuỗi này. Bạn không được dùng cả factorial lẫn pow.

5 bình luận

Filed under Think Java

5 responses to “Chương 7: Phép lặp và vòng lặp

  1. Pingback: Think Java: Cách suy nghĩ như nhà khoa học máy tính | Blog của Chiến

  2. chủ thớt ơi không có đáp án mấy bài tập ạ , nghĩ mãi mà không ra.someone helps meeee.

Bình luận về bài viết này