Tag Archives: lập trình cấu trúc

Chương 8: Chuỗi kí tự

8.1  Kí tự

Trong Java cũng như các ngôn ngữ hướng đối tượng khác thì đối tượng là tập hợp những dữ liệu có liên quan, cùng với một tập các phương thức. Những phương thức nafyhoajt động trên đối tượng kể trên, thực hiện tính toán và đôi lúc thay đổi dữ liệu trong đối tượng đó.

String (chuỗi kí tự) là các đối tượng, bởi vậy bạn có thể hỏi “Có dữ liệu nào được chứa trong một đối tượng String?” và “Có những phương thức nào mà ta có thể kích hoạt được từ đối tượng String?” Những thành phần trong một đối tượng String là các chữ cái, hay tổng quát hơn, là những kí tự. Không phải mọi kí tự đều là chữ cái; còn những kí tự là chữ số, kí hiệu, và các thứ khác. Để đơn giản tôi sẽ gọi chúng đều là các chữ cái. Có nhiều phương thức khác nhau, nhưng trong sách này chỉ dùng một số ít. Các phương thức còn lại được chỉ dẫn ở http://download.oracle.com/javase/6/docs/api/java/lang/String.html.

Phương thức đầu tiên mà ta xét đến là charAt; phương thức này cho phép bạn kết xuất những chữ cái từ một Stringchar là kiểu biến dùng được để lưu trữ từng kí tự riêng lẻ (trái ngược lại với một chuỗi các kí tự).

char cũng hoạt động như các kiểu dữ liệu khác ta đã gặp:

    char ltr = 'c'; 
    if (ltr == 'c') { 
      System.out.println(ltr); 
    }

Những giá trị của kí tự đều xuất hiện trong cặp dấu nháy đơn, như ’c’. Khác với giá trị của chuỗi (xuất hiện giữa cặp dấu nháy kép), các giá trị kí tự chỉ có thể chứa một chữ cái hoặc một kí hiệu.

Sau đây là cách dùng phương thức charAt:

    String fruit = "banana"; 
    char letter = fruit.charAt(1); 
    System.out.println(letter);

fruit.charAt() có nghĩa rằng tôi đang kích hoạt phương thức charAt lên đối tượng có tên fruit. Tôi đang truyền đối số 1 vào phương thức này, tức là tôi đang muốn biết chữ cái đầu tiên của chuỗi là gì. Kết quả là một kí tự, và được lưu vào trong một char có tên letter. Khi tôi in ra giá trị của letter, tôi bị bất ngờ:

a

a không phải là chữ cái đầu tiên của "banana". Trừ khi bạn nghiên cứu khoa học máy tính. Vì những lí do kĩ thuật mà giới khoa học máy tính đều đếm từ số không. Chữ cái thứ 0 của "banana" là chữ b. Chữ cái thứ 1 là a và thứ 2 là n.

Nếu bạn muốn biết chữ cái thứ 0 của một chuỗi, bạn phải truyền tham số là 0:

    char letter = fruit.charAt(0);

8.2  Length

Phương thức tiếp theo đối với String mà ta xét đến là length, vốn trả lại số kí tự có trong chuỗi. Chẳng hạn:

    int length = fruit.length();

length không nhận đối số truyền vào, và trả lại một số nguyên, trong trường hợp này bằng 6. Lưu ý rằng việc có một biến trùng tên với phương thức là hoàn toàn hợp lệ (mặc dù điều này có thể gây nhầm lẫn đối với người đọc mã lệnh).

Để tìm chữ cái cuối cùng trong chuỗi, bạn có thể bị xui khiến để thử theo cách làm sau:

    int length = fruit.length(); 
    char last = fruit.charAt(length); // SAI!!

Cách này không có tác dụng. Lý do là không có chữ cái thứ 6 nào trong "banana". Vì ta đã bắt đầu đếm từ 0, nên sáu chữ cái trong chuỗi được đếm từ 0 tới 5. Để lấy chữ cái cuối cùng, ta phải trừ length đi một.

    int length = fruit.length(); 
    char last = fruit.charAt(length-1);

8.3  Duyệt chuỗi

Một công việc thường làm với một chuỗi là bắt đầu từ điểm đầu của chuỗi, lần lượt chọn từng kí tự, thực hiện một số thao tác đối với chữ cái đó, và công việc được tiếp diễn cho các chữ cái còn lại đến hết chuỗi. Kiểu xử lý như thế này được gọi là duyệt. Một cách tự nhiên để thực hiện việc duyệt là dùng vòng lặp while:

    int index = 0; 
    while (index < fruit.length()) { 
      char letter = fruit.charAt(index); 
      System.out.println(letter); 
      index = index + 1; 
    }

Vòng lặp này để duyệt chuỗi và hiển thị từng chữ cái trên một dòng riêng. Lưu ý điều kiện lặp là index < fruit.length(), nghĩa là khi index bằng với chiều dài của chuỗi, thì điều kiện bị vi phạm, và phần thân của vòng lặp không được thực hiện. Kí tự cuối cùng được truy cập đến sẽ tương ứng với chỉ số fruit.length()-1.

Tên của biến vòng lặp là index (có nghĩa là “chỉ số”). Một chỉ số là một biến hay giá trị được dùng để chỉ định một thành viên của một tập hợp được xếp thứ tự, trong trường hợp này là chuỗi các kí tự. Chỉ số có nhiệm vụ chỉ định thành viên nào bạn cần biết (vì vậy mà nó có tên “chỉ số”).

8.4  Lỗi thực thi

Trở về Mục 1.3.2 tôi đã nói tới các lỗi thực thi, những lỗi không xuất hiện đến tận khi chương trình bắt đầu chạn. Trong Java, những lỗi thực thi được gọi là các biệt lệ.

Có thể bạn không thấy nhiều lỗi thực thi, song vì ta chưa thực hiện nhiều thao tác có khả năng gây nên những lỗi loại này. Và bây giờ ta sẽ gây lỗi. Nếu bạn dùng phương thức charAt rồi cung cấp một chỉ số là số âm hoặc lớn hơn length-1, Java sẽ phát ra một biệt lệ. Bạn có thể hình dung việc “phát” biệt lệ cũng như phát ra một cơn giận dữ.

Khi điều này xảy đến, Java in ra một thông báo lỗi có ghi kiểu biệt lệ và một lần vết ngăn xếp, trong đó có biểu thị những phương thức đang hoạt động khi có biệt lệ xảy ra. Sau đây là một ví dụ:

public class BadString { 
  public static void main(String[] args) { 
    processWord("banana"); 
  } 
  public static void processWord(String s) { 
    char c = getLastLetter(s); 
    System.out.println(c); 
  } 
  public static char getLastLetter(String s) { 
    int index = s.length();    // SAI! 
    char c = s.charAt(index); 
    return c; 
  } 
}

Lưu ý rằng lỗi nằm trong getLastLetter: chỉ số của kí tự cuối cùng đáng ra phải là s.length()-1. Sau đây là kết quả bạn thu được:

Exception in thread "main" java.lang.StringIndexOutOfBoundsException:
String index out of range: 6
        at java.lang.String.charAt(String.java:694)
        at BadString.getLastLetter(BadString.java:24)
        at BadString.processWord(BadString.java:18)
        at BadString.main(BadString.java:14)

Sau đó chương trình kết thúc. Lần vết ngăn xếp này có thể khó đọc, song nó chứa đựng rất nhiều thông tin.

8.5  Đọc tài liệu

Nếu bạn truy cập đến http://download.oracle.com/javase/6/docs/api/java/lang/String.html và kích chuột vào charAt, bạn sẽ xem được tài liệu sau đây (hoặc với nội dung tương tự):

public char charAt(int index)

Returns the char value at the specified index. An index ranges
from 0 to length() - 1. The first char value of the sequence is
at index 0, the next at index 1, and so on, as for array indexing.

Parameters: index - the index of the char value.

Returns: the char value at the specified index of this string.
  The first char value is at index 0.

Throws: IndexOutOfBoundsException - if the index argument is
  negative or not less than the length of this string.

Dòng đầu tiên là nguyên mẫu của phương thức, có nhiệm vụ quy định tên của phương thức, kiểu dữ liệu của các tham số cũng như kiểu trả lại.

Dòng tiếp theo miêu tả những công việc mà phương thức thực hiện. Các dòng sau đó giải thích các tham số và giá trị trả lại. Trong trường hợp này, việc giải thích là quá thừa, nhưng tài liệu luôn được thiết kế để phù hợp một dạng mẫu tiêu chuẩn. Còn dòng cuối cùng mô tả các biệt lệ mà phương thức này có thể phát ra.

Có lẽ bạn sẽ mất chút thời gian để làm quen với kiểu tài liệu thế này, nhưng thời gian công sức bỏ ra cũng đáng.

8.6  Phương thức indexOf

indexOf là phép nghịch đảo của charAtcharAt nhận vào một chỉ số rồi trả lại kí tự ở vị trí chỉ số đó; indexOf nhận một kí tự rồi tìm chỉ số mà kí tự đó xuất hiện.

charAt thất bại nếu chỉ số nằm ngoài phạm vi chuỗi, khi đó phương thức này sẽ phát biệt lệ. indexOf thất bại nếu kí tự không có mặt trong chuỗi, và trả lại giá trị -1.

    String fruit = "banana"; 
    int index = fruit.indexOf('a');

Đoạn mã lệnh này tìm chỉ số của chữ cái ’a’ trong chuỗi. Với trường hợp này, chữ cái nêu trên xuất hiện ba lần, nên ta chưa thấy ngay rằng indexOf nên làm gì. Nhưng theo tài liệu, thì phương thức này sẽ trả lại chỉ số của lần xuất hiện đầu tiên.

Để tìm các lần xuất hiện tiếp theo, còn có một dạng khác của indexOf. Nó nhận vào một đối số thứ hai quy định xem cần bắt đầu tìm kiếm từ vị trí nào trong chuỗi. Đây là một dạng quá tải toán tử, để biết thêm chi tiết, bạn hãy xem Mục 6.4.

Nếu ta kích hoạt:

   int index = fruit.indexOf('a', 2);

nó sẽ bắt đầu ở chữ cái số hai (chữ n đầu tiên) rồi tìm chữ a thứ hai, vốn có chỉ số là 3. Nếu tình cờ chữ cái đó xuất hiện ngay ở chỉ số khởi đầu, thì câu trả lời chính là chỉ số đầu này. Bởi vậy

    int index = fruit.indexOf('a', 5);

sẽ trả lại 5.

8.7  Lặp quay vòng và đếm

Chương trình dưới đây đếm số lần xuất hiện của chữ ’a’ trong một chuỗi:

    String fruit = "banana"; 
    int length = fruit.length(); 
    int count = 0; 
    int index = 0; 
    while (index < length) { 
      if (fruit.charAt(index) == 'a') { 
        count = count + 1; 
      } 
      index = index + 1; 
    } 
    System.out.println(count);

Chương trình này cho thấy một cách viết quen tay thông dụng, đó là một biến đếm. Biến count được khởi tạo bằng không và sau đó tăng thêm một ứng với mỗi lần ta tìm thấy một chữ ’a’. Việc tăng ở đây là chỉ tăng thêm một đơn vị; nó ngược lại với thao tác giảm. Khi ta thoát khỏi vòng lặp, count sẽ chứa kết quả, đó là tổng số các chữ a.

8.8  Các toán tử tăng và giảm

Tăng và giảm là những thao tác thông dụng đến nỗi Java có những toán tử riêng cho chúng. Toán tử ++ cộng thêm một vào giá trị hiện thời của một int hay char-- thì trừ đi một. Hai toán tử trên đều không có tác dụng đối với doubleboolean hay String.

Về khía cạnh kĩ thuật, sẽ hoàn toàn hợp lệ nếu ta tăng một biến rồi đồng thời sử dụng nó trong một biểu thức. Chẳng hạn, bạn có thể thấy lệnh kiểu như sau:

    System.out.println(i++);

Nhìn vào câu lệnh này, thật không rõ là liệu việc tăng sẽ tiến hành trước hay sau khi giá trị được in ra. Bởi vì những biểu thức thế này có xu hướng gây nhầm lẫn, tôi khuyên bạn nên hạn chế sử dụng chúng. Thậm chí, để hạn chế hơn nữa, tôi sẽ không nói cho bạn biết kết quả bằng bao nhiêu. Nếu thực sự muốn biết, bạn có thể thử xem.

Bằng cách dùng toán tử tăng, ta có thể viết lại mã lệnh đếm chữ:\

    int index = 0; 
    while (index < length) { 
      if (fruit.charAt(index) == 'a') { 
        count++; 
      } 
      index++; 
    }

Một lỗi sai thường gặp là viết lệnh kiểu như sau:

    index = index++; // SAI!!

Tuy nhiên, cách này lại hợp lệ về mặt cú pháp, nên trình biên dịch sẽ không cảnh báo bạn. Hiệu ứng của lệnh này là giữ nguyên giá trị của index. Đây thường là một lỗi khó tìm ra.

Hãy nhớ, bạn có thể viết index = index+1, hay index++, nhưng đừng trộn lẫn hai cách viết này.

8.9  String có tính không đổi

Như đã đọc tài liệu về các phương thức của String, có thể bạn phát hiện ra hai phương thức toUpperCase và toLowerCase. Hai phương thức này thường gây nhầm lẫn, vì chúng có tên gọi nghe như thể chúng có tác dụng thay đổi chuỗi hiện có. Song thực ra, chẳng có phương thức nào nói chung và hai phương thức này nói riêng, có thể thay đổi được chuỗi, vì chuỗi có tính không đổi.

Khi bạn kích hoạt toUpperCase đối với một String, bạn sẽ thu được một String mới làm kết quả trả lại. Chẳng hạn:

    String name = "Alan Turing"; 
    String upperName = name.toUpperCase();

Sau khi dòng lệnh thứ hai được thực thi, upperName sẽ chứa giá trị "ALAN TURING", còn name vẫn chứa "Alan Turing".

8.10  String có tính không so sánh được

Ta thường cần so sánh hai chuỗi để xem chúng có giống nhau không, hay chuỗi nào sẽ xếp trước theo thứ tự bảng chữ cái. Thật tuyệt nếu ta sử dụng được các toán tử so sánh như == và >, song ta không thể làm vậy.

Để so sánh các String, ta phải dùng các phương thức equals và compareTo. Chẳng hạn:

    String name1 = "Alan Turing"; 
    String name2 = "Ada Lovelace"; 
    if (name1.equals (name2)) { 
      System.out.println("hai tên này là một."); 
    } 
    int flag = name1.compareTo (name2); 
    if (flag == 0) { 
      System.out.println("Hai tên gọi này là một."); 
    } else if (flag < 0) { 
      System.out.println("tên 1 xếp trước tên 2."); 
    } else if (flag > 0) { 
      System.out.println("tên 2 xếp trước tên 1."); 
    }

Cú pháp ở đây hơi kì quặc. Để so sánh hai String, bạn phải kích hoạt một phương thức lên một chuỗi rồi truyền chuỗi còn lại làm tham số.

Giá trị trả về từ equals thật dễ hiểu; true nếu hai chuỗi có chứa cùng các kí tự, và false trong trường hợp còn lại.

Giá trị trả về từ compareTo lại kì quặc. Đó là khoảng cách giữa hai chữ cái đầu tiên có sự khác biệt ở hai chuỗi. Nếu hai chuỗi bằng nhau thì khoảng cách này bằng 0. Nếu chuỗi thứ nhất (chuỗi mà ta kích hoạt phương thức lên) đứng trước theo thứ tự bảng chữ cái, thì khoảng cách này có giá trị âm. Ngược lại, khoảng cách có giá trị dương. Trong trường hợp này, giá trị trả lại bằng 8, vì chữ cái thứ hai của “Ada” đi trước chữ cái thứ hai của “Alan” là 8 vị trí.

Để trọn vẹn, tôi cũng nói thật rằng việc dùng toán tử == đối với các Strings là hợp lệ nhưng ít khi đúng đắn. Tôi sẽ giải thích lí do trong Mục 13.4; song bây giờ thì chưa.

8.11  Thuật ngữ

đối tượng:
Một tập hợp các dữ liệu có liên quan cùng với một tập các phương thức hoạt động với nó. Các đối tượng mà ta dùng cho đến giờ gồm có String, Bug, Rock, và những đối tượng khác trong GridWorld.
chỉ số:
Một biến hay giá trị được dùng để chọn một trong các thành viên (phần tử) của một tập hợp được xếp thứ tự, như chọn kí tự từ một chuỗi.
biệt lệ:
Một lỗi khi thực thi chương trình.
phát:
Gây nên một biệt lệ.
lần vết ngăn xếp:
Một bản báo cáo cho thấy trạng thái chương trình khi có biệt lệ xảy ra.occurs.
nguyên mẫu:
Dòng đầu tiên của một phương thức, trong đó quy định tên, các tham số và kiểu trả lại.
duyệt:
Việc lặp qua tất cả mọi phần tử của một tập hợp nhằm thực hiện một công việc tương tự đối với từng phần tử.
biến đếm:
Một biến dùng để đếm thứ gì đó; biến này thường được khởi tạo bằng không sau đó tăng thêm.
tăng:
Việc tăng giá trị của biến thêm một đơn vị. Toán tử tăng trong Java là ++.
giảm:
Việc giảm giá trị của biến thêm đi đơn vị. Toán tử giảm trong Java là --.

8.12  Bài tập

Bài tập 1   Hãy viết một phương thức nhận vào một String làm đối số rồi in tất cả các chữ cái theo chiều ngược lại trên cùng một dòng.
Bài tập 2  Hãy đọc nội dung lần vết ngăn xếp ở Mục 8.4 rồi trả lời những câu hỏi sau:

  • Những loại biệt lệ nào đã xảy ra, và những biệt lệ này được định nghĩa trong các gói (package) nào?
  • Giá trị nào của chỉ số gây nên biệt lệ?
  • Phương thức nào phát ra biệt lệ, và phương thức đó được định nghĩa ở đâu?
  • Phương thức nào kích hoạt charAt?
  • Trong BadString.java, charAt được kích hoạt tại dòng số mấy?
Bài tập 3  Hãy bao bọc đoạn mã ở Mục 8.7 vào một phương thức có tên countLetters, sau đó khái quát hoá sao cho nó chấp nhận các đối số là chuỗi và chữ cái cần đếm. Tiếp theo, viết lại phương thức sao cho nó sử dụng indexOf để định vị các chữ a, thay vì kiểm tra từng chữ cái một.
Bài tập 4   Mục đích của bài tập này là ôn lại phép bao bọc và khái quát hoá.

  1. Hãy bao bọc đoạn mã lệnh sau, chuyển đổi nó thành một phương thức nhận vào đối số là một String rồi trả lại giá trị cuối cùng của count.
  2. Mô tả ngắn gọn công dụng của phương thức vừa lập nên (mà không đi vào chi tiết các bước thực hiện như thế nào).
  3. Bây giờ khi bạn đã khái quát hoá để mã lệnh hoạt động được với chuỗi bất kì rồi, bạn còn có thể khái quát hoá theo cách nào nữa?
    String s = "((3 + 7) * 2)"; 
    int len = s.length(); 
    int i = 0; 
    int count = 0; 
    while (i < len) { 
      char c = s.charAt(i); 
      if (c == '(') { 
        count = count + 1; 
      } else if (c == ')') { 
        count = count - 1; 
      } 
      i = i + 1; 
    } 
    System.out.println(count);
Bài tập 5   Mục đích của bài tập này là khám phá những kiểu dữ liệu trong Java và điền vào một số thông tin chi tiết chưa được đề cập đến trong chương này.

  1. Hãy tạo nên một chương trình mới có tên Test.java rồi viết một phương thức main có chứa những biểu thức có kết hợp nhiều kiểu dữ liệu bằng toán tử +. Chẳng hạn, điều gì sẽ xảy ra nếu bạn “cộng” một String và một char? Liệu nó có thực hiện tính tổng hay kết nối? Kiểu của kết quả sẽ là gì? (Bạn xác định được kiểu của kết quả như thế nào?)
  2. Hãy sao chép lại và mở rộng bảng dưới đây rồi điền vào nó. Trong từng ô giao cắt giữa hai kiểu dữ liệu, bạn cần phải xác định xem liệu có hợp lệ nếu dùng toán tử + với những kiểu này không, phép toán nào được thực hiện (cộng hay kết nối), và kiểu kết quả sẽ là gì.
    boolean char int String
    boolean
    char
    int
    String
  3. Hãy tưởng tượng xem các nhà thiết kế nên ngôn ngữ Java đã lựa chọn thế nào khi họ điền vào bảng trên. Trong số các ô điền, có bao nhiêu ô dường như là lựa chọn chắc chắn? Có bao nhiêu ô dường như là lựa chọn tuỳ ý mà có vài phương án tốt như nhau? Có bao nhiêu ô có vẻ còn chứa đựng vấn đề?
  4. Sau đây là một câu đố: thông thường, câu lệnh x++ đúng bằng x = x + 1. Nhưng nếu x là một char, thì nó sẽ không còn đúng! Trong trường hợp này, x++ là hợp lệ, nhưng x = x + 1 sẽ gây ra lỗi. Hãy thử lại và xem thông báo lỗi là gì, và sau đó xem liệu bạn có thể hình dung được điều gì đang diễn ra không.
Bài tập 6  Kết quả của chương trình dưới đây là gì? Bằng một câu, hãy mô tả xem mystery làm gì (chứ không phải các bước thực hiện ra sao).

public class Mystery { 
  public static String mystery(String s) { 
    int i = s.length() - 1; 
    String total = ""; 
    while (i >= 0 ) { 
      char ch = s.charAt(i); 
      System.out.println(i + " " + ch); 
      total = total + ch; 
      i--; 
    } 
    return total; 
  } 
  public static void main(String[] args) { 
    System.out.println(mystery("Allen")); 
  } 
}
Bài tập 7   Một người bạn cho bạn xem phương thức sau đây và diễn giải rằng nếu number là số có hai chữ số bất kì, thì chương trình sẽ in các chữ số theo chiều ngược lại. Người ấy khẳng định rằng nếu number là 17, thì phương thức sẽ cho ra kết quả bằng 71. Liệu người đó có đúng không? Nếu không, hãy giải thích chương trình thực sự làm gì và sửa chữa để nó cho kết quả đúng.

    int number = 17; 
    int lastDigit = number%10; 
    int firstDigit = number/10; 
    System.out.println(lastDigit + firstDigit);
Bài tập 8   Kết quả của chương trình sau là gì?

public class Enigma { 
  public static void enigma(int x) { 
    if (x == 0) { 
      return; 
    } else { 
      enigma(x/2); 
    } 
    System.out.print(x%2); 
  } 
  public static void main(String[] args) { 
    enigma(5); 
    System.out.println(""); 
  } 
}

Hãy giải thích ngắn gọn bằng 4-5 từ xem phương thức enigma thực sự làm điều gì.

Bài tập 9  

  1. Hãy lập một chương trình mới có tên Palindrome.java.
  2. Viết một phương thức có tên first nhận vào một String rồi trả lại chữ cái đầu tiên, và một phương thức last để trả lại chữ cái cuối cùng.
  3. Viết một phương thức có tên  middle nhận vào một String rồi trả lại một chuỗi con có chứa mọi thứ trừ hai chữ cái đầu và cuối. Gợi ý: hãy đọc tài liệu về phương thức substring trong lớp String. Hãy chạy một vài phép thử để chắc rằng bạn hiểu rõ cách hoạt động của substring trước khi thử viết middle. Điều gì sẽ xảy ra nếu bạn kích hoạt middle lên một chuỗi chỉ có hai chứ cái? Một chữ cái? Không có chữ cái nào?
  4. Cách định nghĩa thông thường của một palindrome là một từ mà đọc xuôi ngược đều giống nhau, chẳng hạn “otto” và “palindromeemordnilap.” Một cách khác để định nghĩa một thuộc tính như thế baft là quy định một cách kiểm tra thuộc tính đó. Chẳng hạn, ta có thể nói “một chữ cái là một palindrome, và một từ hai chữ là một palindrome nếu hai chữ cái của nó giống nhau, và một từ bất kì khác là một palindrome nếu chữ cái đầu giống chữ cái cuối và khúc giữa cũng là một palindrome.” Hãy viết một phương thức đệ quy có tên isPalindrome nhận vào một String và trả lại một boolean cho biết từ đó có phải là palindrome hay không.
  5. Một khi bạn đã có đoạn mã để kiểm tra palindrome, hãy tìm cách đơn giản hoá nó bằng cách giảm số điều kiện trong phép kiểm tra. Gợi ý: việc lấy định nghĩa chuỗi rỗng cũng là palindrome có thể giúp ích.
  6. Hãy viết ra trên giấy một chiến lược có tính lặp để kiểm tra palindrome. Có một số phương án khả dĩ, bởi vậy bạn hãy đảm bảo chắc chắn một kế hoạch rõ ràng trước khi bắt đầu viết mã lệnh.
  7. Hãy tạo lập chiến lược bạn chọn thành một phương thức có tên isPalindromeIter.
  8. Câu hỏi phụ: Phụ lục B có mã lệnh để đọc một danh sách các từ vựng từ một file. Hãy đọc một danh sách các từ rồi in ra những palindrome.
Bài tập 10  Một từ được gọi là “abecedarian” nếu các chữ cái trong từ đó xuất hiện theo thứ tự bảng chữ cái. Chẳng hạn, sau đây là tất cả những từ abecedarian gồm 6 chữ cái trong tiếng Anh.

abdest, acknow, acorsy, adempt, adipsy, agnosy, befist, behint, beknow, bijoux, biopsy, cestuy, chintz, deflux, dehors, dehort, deinos, diluvy, dimpsy

  1. Hãy miêu tả một quy trình kiểm tra xem một từ (String) cho trước là abecedarian hay không, nếu coi rằng từ đó chỉ gồm các chữ cái thường. Quy trình này có thể mang tính lặp hay đệ quy.
  2. Tạo dựng quy trình trên thành một phương thức mang tên isAbecedarian.
Bài tập 11   Một dupledrome là một từ chỉ chứa các chữ cái ghép đôi, chẳng hạn như “llaammaa” hay “ssaabb”. Tôi đề ra giả thiết ràng trong tiếng Anh thông dụng không hề có dupledrome nào. Để kiểm chứng giả thiết đó, tôi muốn có chương trình đọc lần lượt các từ vựng từ một cuốn từ điển rồi kiểm tra xem từ đó có phải là dupledrome hay không. Hãy viết một phương thức mang tên isDupledrome nhận vào một String rồi trả lại một boolean để cho biết từ đó có phải là dupledrome không.
Bài tập 12

  1. Vòng giải mã Captain Crunch hoạt động bằng cách lấy mỗi chữ cái trong một chuỗi rồi cộng 13 vào nó. Chẳng hạn, ’a’ trở thành ’n’ và ’b’ trở thành ’o’. Đến cuối, các chữ cái “quay vòng lại”, bởi vậy ’z’ trở thành ’m’. Hãy viết một phương thức nhận vào một String rồi trả lại một String mới có chứa chuỗi sau mã hoá. Bạn cần coi ràng String ban đầu chỉ chứa các chữ in, chữ thường, dấu cách, mà không có dấu chấm phẩy gì khác. Các chữ thường thì được mã hoá thành chữ thường, chữ in thành chữ in. Bạn không được mã hoá  các dấu cách.
  2. Hãy khái quát hoá phương thức Captain Crunch sao cho thay vì cộng 13 vào từng chữ cái, nó có thể cộng thêm bất kì số nào. Bây giờ bạn có thể mã hoá bằn cách cộng 13 rồi giải mã bằng cách cộng -13. Hãy thử làm điều này.
Bài tập 13    Nếu bạn đã giải các bài tập GridWorld trong Chương 5, có thể bạn sẽ thích bài tập này. Mục đích là dùng toán lượng giác để khiến các con bọ (Bug) đuổi bắt lẫn nhau. Hãy sao chép file BugRunner.java thành ChaseRunner.java rồi nhập nó vào môi trường phát triển của bạn. Trước khi thay đổi bất cứ điều gì, hãy kiểm tra đảm bảo rằng bạn biên dịch và chạy được chương trình.

  • Tạo nên hai Bug, một con màu đỏ và một màu xanh lam.
  • Viết một phương thức mang tên distance nhận vào hai Bug rồi tính khoảng cách giữa chúng. Hãy nhớ rằng bạn có thể lấy được toạ độ x của một Bug như sau:
    int x = bug.getLocation().getCol();
  • Viết một phương thức mang tên turnToward nhận vào hai Bug rồi quay mặt một con hướng đến con kia. GỢI Ý: dùng Math.atan2, nhưng hãy nhớ rằng kết quả theo đơn vị radian, bởi vậy bạn phải chuyển sang độ. Ngoài ra, đối với Bug, 0 độ là hướng Bắc chứ không phải hướng Đông.
  • Viết một phương thức mang tên moveToward nhận vào hai Bug, quay mặt con thứ nhất về phía con thứ hai, rồi di chuyển con thứ nhất, nếu có thể.
  • Viết một phương thức mang tên moveBugs nhận hai Bug và một số nguyên n, rồi di chuyển một con Bug về phía con kia n lần. Bạn có thể viết phương thức này theo cách đệ quy, hoặc dùng một vòng lặp while.
  • Kiểm tra từng phương thức vừa viết ở trên ngay khi bạn phát triển chúng. Khi chúng đều hoạt động được, hãy tìm mọi cơ hội cải thiện. Chẳng hạn, nếu bạn có mã lệnh dư thừa trong distance và turnToward, thì bạn có thể bao bọc đoạn mã lệnh lặp lại vào trong một phương thức.
Advertisements

5 phản hồi

Filed under Think Java

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 phản hồi

Filed under Think Java

Chương 6: Phương thức trả lại giá trị

Trở về Mục lục cuốn sách

6.1  Những giá trị được trả lại

Một số phương thức mà ta đã dùng, như các hàm toán học, có trả lại kết quả. Nghĩa là, hiệu ứng từ việc kích hoạt phương thức là tạo ra một giá trị mới mà ta thường gán nó cho một biến hoặc dùng như một phần hợp nên một biểu thức lớn hơn. Chẳng hạn:

    double e = Math.exp(1.0); 
    double height = radius * Math.sin(angle);

Tiếp tục đọc

13 phản hồi

Filed under Think Java

Chương 4: Câu lệnh điều kiện và đệ quy Java

Trở về Mục lục cuốn sách

4.1  Toán tử chia dư

Toán tử chia dư tính với các số nguyên (cùng các biểu thức số nguyên) và cho kết quả là phần dư của phép chia số thứ nhất cho số thứ hai. Trong Java, toán tử chia dư có kí hiệu là dấu phần trăm, %. Cú pháp cũng giống như các toán tử khác:

    int quotient = 7 / 3; 
    int remainder = 7 % 3;

Với toán tử thứ nhất, chia nguyên, kết quả là 2. Với toán tử thứ hai ta được kết quả bằng 1. Như vậy 7 chia cho 3 bằng 2 dư 1. Tiếp tục đọc

2 phản hồi

Filed under Think Java

Chương 3: Phương thức rỗng

Trở về Mục lục cuốn sách

3.1  Dấu phẩy động

Ở chương trước ta đã gặp trục trặc khi tính toán những số không nguyên. Ta đã sửa một cách tạm bợ bằng việc tính số phần trăm thay vì số thập phân, nhưng một giải pháp tổng quát hơn sẽ là dùng số có dấu phẩy động, để biểu diễn được cả số nguyên lẫn số có phần thập phân. Trong Java, kiểu dấu phẩy động có tên double, là chữ gọi tắt của “double-precision” (độ chuẩn xác kép).

Bạn có thể tạo nên các biến phẩy động rồi gán giá trị cho chúng theo cú pháp giống như ta đã làm với những kiểu dữ liệu khác. Chẳng hạn:

    double pi;
    pi = 3.14159;

Tiếp tục đọc

8 phản hồi

Filed under Think Java

Chương 6: Các hàm trả lại kết quả

Trở về Mục lục cuốn sách

6.1 Các giá trị được trả về

Một số hàm dựng sẵn mà ta đã dùng, như các hàm toán học, đều trả lại kết quả. Việc gọi hàm sẽ tạo ra một giá trị, mà chúng ta thường gán vào một biến hoặc sử dụng như một phần của một biểu thức.

e = math.exp(1.0)
height = radius * math.sin(radians)

Tất cả các hàm chúng ta đã viết đều là hàm rỗng; chúng chỉ in ra thông tin hoặc di chuyển con rùa, nhưng kết quả mà chúng trả về là None.

Tiếp tục đọc

2 phản hồi

Filed under Think Python