You are on page 1of 244

Machine Translated by Google

CHƯƠNG

25
CÂY TÌM KIẾM BINARY

Mục tiêu
■ Để thiết kế và triển khai cây tìm kiếm nhị phân (§25.2).

■ Để biểu diễn cây nhị phân sử dụng cấu trúc dữ liệu được liên kết (§25.2.1).

■ Để tìm kiếm một phần tử trong cây tìm kiếm nhị phân (§25.2.2).

■ Để chèn một phần tử vào cây tìm kiếm nhị phân (§25.2.3).

■ Để duyệt các phần tử trong cây nhị phân (§25.2.4).

■ Để thiết kế và triển khai giao diện Cây , lớp AbstractTree và lớp


BST (§25.2.5).

■ Để xóa các phần tử khỏi cây tìm kiếm nhị phân (§25.3).

■ Để hiển thị cây nhị phân bằng đồ thị (§25.4).

■ Để tạo các trình vòng lặp để duyệt cây nhị phân (§25.5).

■ Để triển khai mã hóa Huffman để nén dữ liệu bằng hệ nhị phân

cây (§25.6).
Machine Translated by Google

930, chương 25, cây tìm kiếm nhị phân

25.1 Giới thiệu


Cây là một cấu trúc dữ liệu cổ điển với nhiều ứng dụng quan trọng.
Chìa khóa

Điểm
Cây cung cấp một tổ chức phân cấp, trong đó dữ liệu được lưu trữ trong các nút. Chương này giới
thiệu cây tìm kiếm nhị phân. Bạn sẽ học cách xây dựng cây tìm kiếm nhị phân, cách tìm kiếm phần tử,
chèn phần tử, xóa phần tử và duyệt các phần tử trong cây tìm kiếm nhị phân. Bạn cũng sẽ học cách
xác định và triển khai cấu trúc dữ liệu tùy chỉnh cho cây tìm kiếm nhị phân.

25.2 Cây tìm kiếm nhị phân


Cây tìm kiếm nhị phân có thể được thực hiện bằng cách sử dụng cấu trúc được liên kết.
Chìa khóa

Điểm
Nhớ lại rằng danh sách, ngăn xếp và hàng đợi là cấu trúc tuyến tính bao gồm một chuỗi các phần tử.
Cây nhị phân Cây nhị phân là một cấu trúc phân cấp. Nó trống hoặc bao gồm một phần tử, được gọi là gốc và hai
nguồn gốc
cây nhị phân riêng biệt, được gọi là cây con bên trái và cây con bên phải, một trong hai hoặc cả
cây con bên trái hai cây có thể trống. Ví dụ về cây nhị phân được thể hiện trong Hình 25.1.
cây con bên phải

60 G

55 100 F R

45 57 67 107 MỘT M T

(Một) (b)

HÌNH 25.1 Mỗi nút trong cây nhị phân có 0, một hoặc hai cây con.

chiều dài Chiều dài của đường dẫn là số cạnh của đường dẫn. Độ sâu của một nút là độ dài của đường dẫn
chiều sâu từ gốc đến nút. Tập hợp tất cả các nút ở một độ sâu nhất định đôi khi được gọi là một mức của cây.
cấp độ Anh chị em là các nút chia sẻ cùng một nút cha. Gốc của cây con trái (phải) của một nút được gọi
anh em ruột là con trái (phải) của nút.
Lá cây Một nút không có con được gọi là một lá. Chiều cao của cây nompty là chiều dài của đường đi từ nút
Chiều cao gốc đến lá xa nhất của nó. Chiều cao của cây chứa một nút duy nhất là 0. Thông thường, chiều cao
của cây trống là —1. Hãy xem xét cây trong hình 25.1a. Chiều dài của đường đi từ nút 60 đến nút 45
là 2. Chiều sâu của nút 60 là 0, độ sâu của nút 55 là 1 và độ sâu của nút 45 là 2. Chiều cao của
cây là 2.
Các nút 45 và 57 là anh em ruột. Các nút 45, 57, 67 và 107 ở cùng cấp độ.
cây tìm kiếm nhị phân Một loại cây nhị phân đặc biệt được gọi là cây tìm kiếm nhị phân (BST) thường hữu ích. Một BST
(không có phần tử trùng lặp) có thuộc tính là đối với mọi nút trong cây, giá trị của bất kỳ nút nào
trong cây con bên trái của nó nhỏ hơn giá trị của nút đó và giá trị của bất kỳ nút nào trong cây
con bên phải của nó lớn hơn giá trị của nút. Các cây nhị phân trong Hình 25.1 đều là BST.

Lưu ý sư phạm
Hoạt ảnh BST trên Companion Để có bản trình diễn GUI tương tác để xem cách hoạt động của BST, hãy truy cập www.cs.armstrong.edu/liang/

Trang mạng animation / web / BST.html, như trong Hình 25.2.


Machine Translated by Google

25.2 Cây tìm kiếm nhị phân 931

HÌNH 25.2 Công cụ hoạt ảnh cho phép bạn chèn, xóa và tìm kiếm các phần tử.

25.2.1 Biểu diễn cây tìm kiếm nhị phân


Một cây nhị phân có thể được biểu diễn bằng cách sử dụng một tập hợp các nút được liên kết. Mỗi nút chứa một giá trị và

hai liên kết có tên trái và phải tham chiếu đến nút con trái và nút con phải, như thể hiện trong Hình 25.3.

nguồn gốc 60

55 100

45 57 67 107

HÌNH 25.3 Cây nhị phân có thể được biểu diễn bằng cách sử dụng một tập hợp các nút được liên kết.

Một nút có thể được định nghĩa là một lớp, như sau:

Class TreeNode <E> {


phần tử E được bảo vệ ;
Đã bảo vệ TreeNode <E> trái;
TreeNode được bảo vệ <E> đúng;

public TreeNode (E e)
{element = e;
}
}
Machine Translated by Google

932 chương 25 cây tìm kiếm nhị phân

Gốc biến đề cập đến nút gốc của cây. Nếu cây trống, gốc là rỗng. Đoạn mã sau tạo ba nút đầu tiên của
cây trong Hình 25.3.

// Tạo nút gốc


TreeNode <Integer> root = new TreeNode <> (60);

// Tạo nút con bên trái root.left = new


TreeNode <> (55);

// Tạo nút con bên phải root.right = new


TreeNode <> (100);

25.2.2 Tìm kiếm phần tử


Để tìm kiếm một phần tử trong BST, bạn bắt đầu từ gốc và quét từ đó xuống cho đến khi tìm thấy một
phần tử phù hợp hoặc bạn đến một cây con trống. Thuật toán được mô tả trong Liệt kê 25.1.
Để hiện tại trỏ đến gốc (dòng 2). Lặp lại các bước sau cho đến khi hiện tại là null (dòng 4) hoặc
phần tử khớp với current.element (dòng 12).

■ Nếu phần tử nhỏ hơn current.element, hãy gán current.left cho hiện tại
(dòng 6).

■ Nếu phần tử lớn hơn current.element, hãy gán current.right cho hiện tại (dòng 9).

■ Nếu phần tử bằng current.element, trả về true (dòng 12).

Nếu hiện tại là null, cây con trống và phần tử không có trong cây (dòng 14).

DANH SÁCH 25.1 Tìm kiếm phần tử trong BST


1 tìm kiếm boolean công khai (phần tử E) {
bắt đầu từ gốc 2 TreeNode <E> current = root; // Bắt đầu từ gốc
3
4 while (current! = null)
5 if (element <current.element) {
cây con bên trái 6 current = current.left; // Quẹo trái
7 }
8 else if (element> current.element) {
cây con bên phải 9 current = current.right; // Đi sang phải
10 }
11 else // Phần tử khớp với current.element
thành lập 12 trả về true; // Phần tử được tìm thấy
13
không tìm thấy 14 trả về sai; // Phần tử không có trong cây
15}

25.2.3 Chèn một phần tử vào BST


Để chèn một phần tử vào BST, bạn cần xác định vị trí để chèn phần tử đó vào cây. Ý tưởng chính là
xác định vị trí cha cho nút mới. Liệt kê 25.2 đưa ra thuật toán.

LISTING 25.2 Chèn phần tử vào BST


1 boolean insert (E e) {
nếu (cây trống) 2
tạo một nút mới 3 // Tạo nút cho e làm gốc;
4 khác {
5 // Định vị nút cha
6 cha mẹ = hiện tại = gốc;
xác định vị trí cha mẹ
7 while (current! = null)
Machine Translated by Google

25.2 Cây tìm kiếm nhị phân 933

if (e <giá trị trong current.element) {


cha mẹ = hiện tại; // Giữ lại trang gốc
8 current = current.left; // Quẹo trái đứa trẻ còn lại

9 }
10 else if (e> giá trị trong current.element) {
11 cha mẹ = hiện tại; // Giữ lại trang gốc
12 current = current.right; // Đi sang phải đúng đứa trẻ
13 }
14 khác
15 trả về sai; // Nút trùng lặp không được chèn
16 17 18

19 // Tạo một nút mới cho e và đính kèm nó vào cha


20
21 trả về true; // Phần tử được chèn
22 }
23}

Nếu cây trống, hãy tạo một nút gốc với phần tử mới (dòng 2–3). Nếu không, hãy xác định vị trí nút cha

cho nút phần tử mới (dòng 6–17). Tạo một nút mới cho phần tử và liên kết nút này với nút cha của nó. Nếu

phần tử mới nhỏ hơn phần tử cha, nút của phần tử mới sẽ là nút con bên trái của phần tử cha. Nếu phần

tử mới lớn hơn phần tử cha, thì nút của phần tử mới sẽ là nút con bên phải của phần tử cha.

Ví dụ, để chèn 101 vào cây trong Hình 25.3, sau khi vòng lặp while kết thúc trong thuật toán, cha trỏ

đến nút cho 107, như thể hiện trong Hình 25.4a. Nút mới cho 101 trở thành nút con bên trái của nút cha.

Để chèn 59 vào cây, sau khi vòng lặp while kết thúc trong thuật toán, nút cha trỏ tới nút 57, như thể

hiện trong Hình 25.4b. Nút mới cho 59 trở thành nút con bên phải của nút cha.

nguồn gốc 60 nguồn gốc 60

55 100 55 100
cha mẹ cha mẹ

45 57 67 107 45 57 67 107

101 59 101

(a) Chèn 101 (b) Chèn 59

HÌNH 25.4 Hai phần tử mới được chèn vào cây.

25.2.4 Traversal cây


Duyệt qua cây là quá trình truy cập mỗi nút trong cây chính xác một lần. Có một số cách để đi qua một đi ngang qua cây

cái cây. Phần này trình bày các đường đi ngang qua inorder, postorder, preorder, deep-first, và wide-
first.

Với tính năng truyền tải không đơn giản, cây con bên trái của nút hiện tại được truy cập đệ quy đầu inorder traversal

tiên, sau đó đến nút hiện tại và cuối cùng là cây con bên phải của nút hiện tại một cách đệ quy. Trình

duyệt inorder hiển thị tất cả các nút trong BST theo thứ tự tăng dần.

Với tính năng duyệt theo thứ tự sau, cây con bên trái của nút hiện tại được truy cập đệ quy đầu truyền qua đơn đặt hàng

tiên, sau đó đệ quy cây con bên phải của nút hiện tại và cuối cùng là chính nút hiện tại. Một ứng dụng

của postorder là tìm kích thước của thư mục trong hệ thống tệp. Như được hiển thị trong
Machine Translated by Google

934, chương 25, cây tìm kiếm nhị phân

Hình 25.5, mỗi thư mục là một nút bên trong và một tập tin là một nút lá. Bạn có thể áp dụng postorder
để lấy kích thước của từng tệp và thư mục con trước khi tìm kích thước của thư mục gốc.

đặt hàng trước truyền tải Với duyệt đặt hàng trước, nút hiện tại được truy cập đầu tiên, sau đó đệ quy cây con bên trái của

truyền tải chiều sâu đầu tiên nút hiện tại và cuối cùng là cây con bên phải của nút hiện tại một cách đệ quy. Truyền tải đầu tiên
theo độ sâu giống như truyền tải đặt hàng trước. Một ứng dụng của đặt hàng trước là in tài liệu có
cấu trúc. Như trong Hình 25.6, bạn có thể in mục lục của một cuốn sách bằng cách sử dụng tính năng
duyệt đặt trước.

danh mục

f1 f2 fm
d1 d2 . . . dn

f11 . . . f1m
d11 d12
f21 fn1 . . . fnk

HÌNH 25.5 Một thư mục chứa các tệp và thư mục con.

sách

Chương 1 chương 2 . . . Chương n

Phần 1 Phần 2 . . .

HÌNH 25.6 Cây có thể được sử dụng để biểu thị một tài liệu có cấu trúc như sách và các chương,
phần của nó.

Ghi chú

Bạn có thể tạo lại cây tìm kiếm nhị phân bằng cách chèn các phần tử vào thứ tự trước của chúng.

Cây được tái tạo bảo toàn mối quan hệ cha và con cho các nút trong cây tìm kiếm nhị phân ban

đầu.

theo chiều rộng-đầu tiên Với truyền qua chiều rộng-thứ nhất, các nút được truy cập theo cấp độ. Đầu tiên là thăm gốc rễ,
sau đó tất cả các con của gốc từ trái sang phải, sau đó đến các cháu của gốc từ trái sang phải, và cứ
tiếp tục như vậy.
Ví dụ, trong cái cây trong Hình 25.4b, inorder là

45 55 57 59 60 67 100 101 107

Đơn đặt hàng là

45 59 57 55 67 101 107 100 60

Đơn đặt hàng trước là

60 55 45 57 59 100 67 107 101

Chiều rộng-đầu tiên là

60 55 100 45 57 67 107 59 101


Machine Translated by Google

25.2 Cây tìm kiếm nhị phân 935

Bạn có thể sử dụng cây sau để ghi nhớ inorder, postorder và preorder.

1 2

Đơn đặt hàng trước là 1 + 2, đơn đặt hàng sau là 1 2 + và đơn đặt hàng trước là + 1 2.

25.2.5 Lớp BST


Theo mô hình thiết kế của Java Collections Framework API, chúng tôi sử dụng một giao diện có
tên là Tree để xác định tất cả các hoạt động chung cho cây và cung cấp một lớp trừu tượng có
tên là AbstractTree để triển khai một phần Tree, như thể hiện trong Hình 25.7. Một BST cụ thể
lớp có thể được định nghĩa để mở rộng AbstractTree, như trong Hình 25.8.

«Giao diện»
java.lang.Iterable <E>

+ iterator (): Trình lặp <E> Trả về một trình lặp để duyệt qua các phần tử trong bộ sưu tập này

«Giao diện»
Cây <E>

+ search (e: E): boolean Trả về true nếu phần tử được chỉ định nằm trong cây.

+ insert (e: E): boolean Trả về true nếu phần tử được thêm thành công.

+ delete (e: E): boolean Trả về true nếu phần tử được xóa khỏi cây thành công.

+ inorder (): vô hiệu In các nút trong trình duyệt nhỏ hơn.

+ preorder (): void In các nút trong trình duyệt đặt hàng trước.

+ postorder (): void In các nút trong truyền qua thứ tự sau.

+ getSize (): int Trả về số phần tử trong cây.

+ isEmpty (): boolean Trả về true nếu cây trống.

+ clear (): void Loại bỏ tất cả các phần tử khỏi cây.

AbstractTree <E>

HÌNH 25.7 Giao diện Tree xác định các hoạt động chung cho cây và lớp AbstractTree triển khai một phần Tree.

Danh sách 25.3, 25.4 và 25.5 cung cấp các triển khai cho Tree, AbstractTree và BST.

LISTING 25.3 Tree.java


1 giao diện công cộng Cây <E> mở rộng Lặp lại <E> { giao diện

/ ** Trả về true nếu phần tử nằm trong cây * /


2 3 tìm kiếm boolean công khai (E e); Tìm kiếm
4
5 / ** Chèn phần tử e vào cây tìm kiếm nhị phân.
6 * Trả về true nếu phần tử được chèn thành công. * /
7 public boolean insert (E e); chèn
8
Machine Translated by Google

936, chương 25: Cây tìm kiếm nhị phân

AbstractTree <E>

m 0
TreeNode <E> BST <E mở rộng có thể so sánh được <E>>

#element: E #root: TreeNode <E> Gốc của cây.

#left: TreeNode <E> #size: int Số lượng nút trong cây.

#right: TreeNode <E>


+ BST () Tạo một BST mặc định.
1
+ BST (đối tượng: E []) Tạo một BST từ một loạt các phần tử.
Liên kết

+ đường dẫn (e: E): Trả về đường dẫn của các nút từ gốc dẫn đến
java.util.List <TreeNode <E>> nút cho phần tử được chỉ định. Phần tử
có thể không ở trên cây.

HÌNH 25.8 Lớp BST xác định một BST cụ thể.

/ ** Xóa phần tử được chỉ định khỏi cây.


9 * Trả về true nếu phần tử được xóa thành công. * /
xóa bỏ 10 11 xóa boolean công khai (E e);
12
13 / ** Truyền tải Inorder từ gốc * /
inorder 14 public void inorder ();
15
16 / ** Truyền qua thứ tự đăng từ gốc * /
đơn đặt hàng 17 public void postorder ();
18
19 / ** Đặt hàng trước truyền tải từ gốc * /
đặt hàng trước 20 public void preorder ();
21
22 / ** Lấy số lượng nút trong cây * /
getSize 23 public int getSize ();
24

/ ** Trả về true nếu cây trống * /


isEmpty 25 public boolean isEmpty ();
26 27}

DANH SÁCH 25.4 AbstractTree.java


lớp trừu tượng 1 lớp trừu tượng công khai AbstractTree <E>
2 thực hiện Tree <E> {
3 @Override / ** Truyền tải Inorder từ gốc * /
inorder mặc định 4 public void inorder () {
thực hiện 5 }
6

7 @Override / ** Truyền qua thứ tự đăng từ gốc * /


đơn đặt hàng mặc định public void postorder () {
thực hiện }
8 9 10

11 @Override / ** Đặt hàng trước truyền tải từ gốc * /


đặt hàng trước mặc định 12 public void preorder () {
thực hiện 13 }
14
15 @Override / ** Trả về true nếu cây trống * /
Machine Translated by Google

25.2 Cây tìm kiếm nhị phân 937

16 public boolean isEmpty () { triển khai isEmpty


17 trả về getSize () == 0;
18 }
19}

DANH SÁCH 25,5 BST.java


1 lớp công khai BST <E mở rộng có thể so sánh được <E>> Lớp BST
2 mở rộng AbstractTree <E> {
root TreeNode <E> được bảo vệ ; nguồn gốc

3 4 bảo vệ int size = 0; kích cỡ

5
/ ** Tạo cây tìm kiếm nhị phân mặc định * /
6 7 BST công cộng () { phương thức khởi tạo no-arg

}
8 9

10 / ** Tạo cây tìm kiếm nhị phân từ một mảng các đối tượng * /
11 BST công khai (E [] đối tượng) { constructor
12 for (int i = 0; i <objects.length; i ++)
13 insert (các đối tượng [i]);
14 }
15
16 @Override / ** Trả về true nếu phần tử nằm trong cây * /
17 tìm kiếm boolean công khai (E e) { Tìm kiếm
18 TreeNode <E> current = root; // Bắt đầu từ gốc
19
20 while (current! = null) {
21 if (e.compareTo (current.element) < 0) { so sánh các đối tượng
22 current = current.left;
23 }
24 else if (e.compareTo (current.element)> 0) {
25 current = current.right;
26 }
27 else // phần tử khớp với current.element
28 trả về true; // Phần tử được tìm thấy
29 }
30
31 trả về sai;
32 }
33
34 @Override / ** Chèn phần tử e vào cây tìm kiếm nhị phân.
35 * Trả về true nếu phần tử được chèn thành công. * /
36 public boolean insert (E e) { chèn
37 if (root == null)
38 root = createNewNode (e); // Tạo một thư mục gốc mới gốc mới

39 khác {
40 // Định vị nút cha
41 TreeNode <E> parent = null;
42 TreeNode <E> current = root;
43 while (current! = null)
44 if (e.compareTo (current.element) < 0) { so sánh các đối tượng
45 cha mẹ = hiện tại;
46 current = current.left;
47 }
48 else if (e.compareTo (current.element)> 0) {
49 cha mẹ = hiện tại;
50 current = current.right;
51 }
52 khác
53 trả về sai; // Nút trùng lặp không được chèn
Machine Translated by Google

938, chương 25, cây tìm kiếm nhị phân

54
55 // Tạo nút mới và gắn nó vào nút cha
liên kết với cha mẹ 56 if (e.compareTo (parent.element) < 0)
57 parent.left = createNewNode (e);
58 khác
59 parent.right = createNewNode (e);
60 }
61
tăng kích thước 62 kích
63 thước ++; trả về true; // Phần tử đã được chèn thành công
64 }
65
tạo nút mới 66 Bảo vệ TreeNode <E> createNewNode (E e) {
67 trả về TreeNode mới <> (e);
68 }
69
70 @Override / ** Truyền tải Inorder từ gốc * /
inorder 71 public void inorder () {
72 inorder (gốc);
73 }
74
75 / ** Truyền ngang Inorder từ cây con * /
phương pháp trợ giúp đệ quy 76 void inorder được bảo vệ (TreeNode <E> root) {
77 if (root == null) return;
78 inorder (root.left);
79 System.out.print (root.element + " ");
80 inorder (root.right);
81 }
82
83 @Override / ** Truyền qua thứ tự đăng từ gốc * /
đơn đặt hàng 84 public void postorder () {
85 postorder (gốc);
86 }
87
88 / ** Truyền qua thứ tự bưu điện từ một cây con * /
phương pháp trợ giúp đệ quy 89 Đặt hàng trước được bảo vệ void (TreeNode <E> root) {
90 if (root == null) return;
91 postorder (root.left);
92 postorder (root.right);
93 System.out.print (root.element + " ");
94 }
95
96 @Override / ** Đặt hàng trước truyền tải từ gốc * /
đặt hàng trước 97 public void preorder () {
98 preorder (root);
99 }
100
101 / ** Đặt hàng trước truyền tải từ một cây con * /
phương pháp trợ giúp đệ quy 102 Đã bảo vệ void postorder (TreeNode <E> root) {
103 if (root == null) return;
104 System.out.print (root.element + " ");
105 đặt hàng trước (root.left);
106 đặt hàng trước (root.right);
107 }
108
109 / ** Lớp bên trong này là tĩnh, vì nó không truy cập bất kỳ thành viên cá
110 thể nào được xác định trong lớp bên ngoài của nó * /
lớp bên trong 111 public static class TreeNode <E expand Có thể so sánh được <E>> {
112 phần tử E được bảo vệ ;
113 Đã bảo vệ TreeNode <E> left;
Machine Translated by Google

25.2 Cây tìm kiếm nhị phân 939

114 được bảo vệ TreeNode <E> quyền;


115
116 Public TreeNode (E e) {
117 phần tử = e;
118 }
119 }
120
121 @Override / ** Lấy số lượng nút trong cây * /
122 public int getSize () { getSize
123 trả về kích thước;
124 }
125
126 / ** Trả về gốc của cây * /
127 public TreeNode <E> getRoot () { getRoot
128 trả về gốc;
129 }
130
131 / ** Trả về một đường dẫn từ gốc dẫn đến phần tử được chỉ định * /
132 public java.util.ArrayList <TreeNode <E>> path (E e) { đường dẫn

133 java.util.ArrayList <TreeNode <E>> list =


134 new java.util.ArrayList <> ();
135 TreeNode <E> current = root; // Bắt đầu từ gốc
136
137 while (current! = null) {
138 list.add (hiện tại); // Thêm nút vào danh sách
139 if (e.compareTo (current.element) < 0) {
140 current = current.left;
141 }
142 else if (e.compareTo (current.element)> 0) {
143 current = current.right;
144 }
145 khác
146 nghỉ;
147 }
148
149 danh sách trả lại ; // Trả về danh sách mảng các nút
150 }
151
152 @Override / ** Xóa một phần tử khỏi cây tìm kiếm nhị phân.
153 * Trả về true nếu phần tử được xóa thành công.
154 * Trả về false nếu phần tử không có trong cây. * /
155 xóa boolean công khai (E e) { xóa bỏ
156 // Định vị nút sẽ bị xóa và cũng xác định vị trí nút cha của nó
157 TreeNode <E> parent = null; xác định vị trí cha mẹ

158 TreeNode <E> current = root; while xác định vị trí hiện tại

159 (current! = null) {


160 if (e.compareTo (current.element) < 0) {
161 cha mẹ = hiện tại;
162 current = current.left;
163 }
164 else if (e.compareTo (current.element)> 0) {
165 cha mẹ = hiện tại;
166 current = current.right;
167 }
168 khác
169 nghỉ; // Phần tử nằm trong cây được trỏ đến bởi hiện tại hiện tại được tìm thấy

170 }
171
172 if (hiện tại == null) không tìm thấy

173 trả về sai; // Phần tử không có trong cây


Machine Translated by Google

940, chương 25, cây tìm kiếm nhị phân

174
175 // Trường hợp 1: hiện tại không có con bên trái
Trường hợp 1 176 if (current.left == null) {
177 // Kết nối nút cha với nút con bên phải của nút hiện tại
178 if (cha == null) {
179 root = current.right;
180 }
khác {
if (e.compareTo (parent.element) < 0)
kết nối lại phụ huynh parent.left = current.right; khác

kết nối lại phụ huynh parent.right = current.right;


}
}
khác {
Trường hợp 2 181182 183 184 185 186 187hợp
// Trường 1882:189
Nút hiện tại có nút con bên trái.
190 // Định vị nút ngoài cùng bên phải trong cây con bên trái của
191 // nút hiện tại và cũng là nút cha của nó.
xác định vị trí parentOfRightMost 192 TreeNode <E> parentOfRightMost = hiện tại; TreeNode
xác định đúng vị trí 193 <E> rightMost = current.left;
194
195 while (rightMost.right! = null) {
196 parentOfRightMost = rightMost;
197 rightMost = rightMost.right; // Tiếp tục sang phải
198 }
199
200 // Thay thế phần tử hiện tại bằng phần tử trong rightMost
thay thế hiện tại 201 current.element = rightMost.element;
202
203 // Loại bỏ nút ngoài cùng bên phải
204 if (parentOfRightMost.right == rightMost)
205 parentOfRightMost.right = rightMost.left;
kết nối lại 206 khác
parentOfRightMost 207 // Trường hợp đặc biệt: parentOfRightMost == current
208 parentOfRightMost.left = rightMost.left;
209 }
210
giảm kích cỡ 211 kích cỡ--;
xóa thành công 212 trả về true; // Phần tử đã được xóa thành công
213 }
214
215 @Override / ** Lấy một trình lặp. Sử dụng inorder. * /
người lặp lại 216 public java.util.Iterator <E> iterator () {
217 trả về InorderIterator mới ();
218 }
219
220 // Lớp bên trong InorderIterator
lớp trình lặp 221 lớp riêng InorderIterator triển khai java.util.Iterator <E> {
222 // Lưu trữ các phần tử trong một danh sách
danh sách nội bộ 223 private java.util.ArrayList <E> list =
224 new java.util.ArrayList <> ();
vị trí hiện tại 225 int hiện tại riêng tư = 0; // Trỏ đến phần tử hiện tại trong danh sách
226
227 public InorderIterator () {
228 inorder (); // Traverse cây nhị phân và lưu trữ các phần tử trong danh sách
229 }
230
231 / ** Truyền tải Inorder từ gốc * /
232 private void inorder () {
lấy danh sách inorder 233 inorder (gốc);
Machine Translated by Google

25.2 Cây tìm kiếm nhị phân 941

234 }
235
236 / ** Truyền ngang Inorder từ cây con * /
237 private void inorder (TreeNode <E> root) {
238 if (root == null) return;
239 inorder (root.left);
240 list.add (root.element);
241 inorder (root.right);
242 }
243
244 @Override / ** Các yếu tố khác để đi ngang? * /
245 public boolean hasNext () { hasNext trong trình lặp?

246 if (hiện tại <list.size ())


247 trả về true;
248
249 trả về sai;
250 }
251
252 @Override / ** Lấy phần tử hiện tại và chuyển sang phần tiếp theo * /
253 công khai E tiếp theo () { lấy phần tử tiếp theo
254 return list.get (hiện tại ++);
255 }
256
257 @Override / ** Xóa phần tử hiện tại * /
258 public void remove () { loại bỏ hiện tại
259 xóa (list.get (hiện tại)); // Xóa phần tử hiện tại
260 list.clear (); // Xóa danh sách
261 inorder (); // Xây dựng lại danh sách làm mới danh sách

262 }
263 }
264
265 / ** Xóa tất cả các phần tử khỏi cây * /
266 public void clear () {
thông thoáng

267 root = null;


268 kích thước = 0;
269 }
270}

Phương thức insert (E e) (dòng 36–64) tạo một nút cho phần tử e và chèn nó vào cây.
Nếu cây trống, nút sẽ trở thành gốc. Nếu không, phương thức này sẽ tìm thấy một nút cha
đã ăn được thích hợp cho nút để duy trì thứ tự của cây. Nếu phần tử đã có trong cây,
phương thức trả về false; nếu không thì nó trả về true.
Phương thức inorder () (dòng 71–81) gọi inorder (gốc) để đi qua toàn bộ cây. Phương
thức inorder (TreeNode root) đi qua cây với gốc được chỉ định. Đây là một phương pháp
đệ quy. Nó duyệt đệ quy qua cây con bên trái, sau đó đến gốc và cuối cùng là cây con bên
phải. Quá trình truyền tải kết thúc khi cây trống.
Phương thức postorder ( ) (dòng 84–94) và phương thức preorder () (dòng 97–107) là
được thực hiện tương tự bằng cách sử dụng đệ quy.

Phương thức path (E e) (dòng 132–150) trả về một đường dẫn của các nút dưới dạng
danh sách mảng. Đường dẫn bắt đầu từ gốc dẫn đến phần tử. Phần tử có thể không có trong
cây. Ví dụ, trong Hình 25.4a, đường dẫn (45) chứa các nút cho các phần tử 60, 55 và 45
và đường dẫn (58) chứa các nút cho các phần tử 60, 55 và 57.
Việc triển khai delete () và iterator () (dòng 155–269) sẽ được thảo luận trong
Mục 25.3 và 25.5.

Liệt kê 25.6 đưa ra một ví dụ tạo cây tìm kiếm nhị phân bằng BST (dòng 4). Chương
trình thêm các chuỗi vào cây (dòng 5–11), duyệt qua cây trong inorder, postorder và
preorder (dòng 14–20), tìm kiếm một phần tử (dòng 24) và lấy một đường dẫn từ nút chứa
Peter đến gốc (dòng 28–31).
Machine Translated by Google

942, chương 25, cây tìm kiếm nhị phân

DANH SÁCH 25.6 TestBST.java


1 lớp công khai TestBST {
2 public static void main (String [] args) {
3 // Tạo một BST
tạo cây 4 BST <String> tree = new BST <> ();
chèn 5 tree.insert ("George");
6 tree.insert ("Michael");
7 tree.insert ("Tom");
số 8
tree.insert ("Adam");
tree.insert ("Jones");
9 tree.insert ("Peter");
10 tree.insert ("Daniel");
11 12

13 // Traverse cây
14 System.out.print ("Inorder (đã sắp xếp): ");
inorder 15 tree.inorder ();
16 System.out.print ("\ nPostorder: ");
đơn đặt hàng 17 tree.postorder ();
18 System.out.print ("\ nĐặt hàng: ");
đặt hàng trước
19 tree.preorder ();
20 System.out.print ("\ nSố nút là" + tree.getSize ());
getSize
21
22 // Tìm kiếm một phần tử
"
23 System.out.print ("\ n Có phải Peter ở trên cây không? +
Tìm kiếm 24 tree.search ("Peter"));
25
26 // Lấy đường dẫn từ gốc đến Peter
27 System.out.print ("\ nMột đường dẫn từ gốc đến Peter là: ");
28 java.util.ArrayList <BST.TreeNode <Chuỗi>> path = tree.path
29 ("Peter");
30 for (int i = 0; path! = null && i <path.size (); i ++)
31 System.out.print (path.get (i) .element + " ");
32

Số nguyên [] số = {2, 4, 3, 1, 8, 5, 6, 7};


33 BST <Integer> intTree = new BST <> (số);
34 System.out.print ("\ nInorder (đã sắp xếp): ");
35 intTree.inorder ();
36 }
37 38}

Inorder (đã sắp xếp): Adam Daniel George Jones Michael Peter Tom Postorder:
Daniel Adam Jones Peter Tom Michael George Preorder: George Adam Daniel Michael
Jones Tom Peter Số lượng nút là 7

Peter có ở trên cây không? đúng vậy


Đường đi từ gốc đến Peter là: George Michael Tom Peter Inorder (đã sắp xếp):
1 2 3 4 5 6 7 8

Chương trình kiểm tra đường dẫn! = Null ở dòng 30 để đảm bảo rằng đường dẫn không rỗng trước
khi gọi path.get (i). Đây là một ví dụ về lập trình phòng thủ để tránh các lỗi thời gian chạy tiềm
ẩn.

Chương trình tạo một cây khác để lưu trữ các giá trị int (dòng 34). Sau khi tất cả các yếu tố là
được chèn vào các cây, các cây sẽ xuất hiện như trong Hình 25.9.
Nếu các phần tử được chèn theo một thứ tự khác (ví dụ: Daniel, Adam, Jones, Peter, Tom, Michael,
George), cây sẽ trông khác. Tuy nhiên, trình duyệt inorder in các phần tử theo cùng một thứ tự miễn
là tập hợp các phần tử giống nhau. Trình duyệt inorder hiển thị một danh sách đã được sắp xếp.
Machine Translated by Google

25.3 Xóa các phần tử khỏi BST 943

nguồn gốc 2

1 4

nguồn gốc
George
3 số 8

Adam Michael 5

Daniel Jones Tom 6

Peter 7

(Một) (b)

HÌNH 25.9 Các BST trong Liệt kê 25.6 được minh họa ở đây sau khi chúng được tạo.

25.1 Hiển thị kết quả của việc chèn 44 vào Hình 25.4b.

25.2 Hiển thị thứ tự sắp xếp, thứ tự trước và thứ tự sau của việc duyệt qua các phần tử trong hệ nhị phân

cây trong hình 25.1b.

25.3 Nếu một tập hợp các phần tử được chèn vào một BST theo hai thứ tự khác nhau, liệu hai BST phản hồi

có giống nhau không? Liệu đường đi ngang qua inorder có giống nhau không? Quá trình truyền qua

đơn đặt hàng có giống nhau không? Quá trình duyệt đặt hàng trước có giống nhau không?

25.4 Độ phức tạp về thời gian của việc chèn một phần tử vào BST là bao nhiêu?

25.5 Thực hiện phương thức tìm kiếm (phần tử) sử dụng đệ quy.

25.3 Xóa các phần tử khỏi BST


Để xóa một phần tử khỏi BST, trước tiên hãy xác định vị trí nó trong cây và sau đó
xem xét hai trường hợp — liệu nút có nút con bên trái hay không — trước khi xóa
phần tử và kết nối lại cây.

Phương thức chèn (phần tử) đã được trình bày trong Phần 25.2.3. Thường thì bạn cần xóa một phần
tử khỏi cây tìm kiếm nhị phân. Làm như vậy phức tạp hơn nhiều so với việc thêm một phần tử vào
cây tìm kiếm nhị phân.
Để xóa một phần tử khỏi cây tìm kiếm nhị phân, trước tiên bạn cần xác định vị trí nút chứa
phần tử và cũng là nút cha của nó. Cho phép hiện tại trỏ đến nút có chứa phần tử trong cây tìm
kiếm nhị phân và cha trỏ tới nút cha của nút hiện tại . Nút hiện tại có thể là nút con bên trái
hoặc nút con bên phải của nút cha . Có hai trường hợp để xem xét.

Trường hợp 1: Nút hiện tại không có nút con bên trái, như hình 25.10a. Trong trường hợp này,
chỉ cần kết nối nút cha với nút con bên phải của nút hiện tại, như trong Hình 25.10b.

Ví dụ, để xóa nút 10 trong Hình 25.11a, bạn sẽ kết nối nút cha của nút 10
với nút con bên phải của nút 10, như trong Hình 25.11b.
Machine Translated by Google

944, chương 25, cây tìm kiếm nhị phân

cha mẹ cha mẹ

dòng điện có thể là bên trái hoặc


Cây con có thể là bên trái hoặc
con phải của cha mẹ cây con bên phải của cha mẹ
hiện hành
hiện tại điểm nút
bị xóa

Không có con trái

Cây con Cây con

(Một)
(b)

HÌNH 25.10 Trường hợp 1: Nút hiện tại không có nút con bên trái.

nguồn gốc 20 nguồn gốc 20

10 40 40

16 30 80 16 30 80

27 50 27 50

(Một) (b)

HÌNH 25.11 Trường hợp 1: Xóa nút 10 khỏi (a) dẫn đến (b).

Ghi chú

Nếu nút hiện tại là một lá, nó rơi vào Trường hợp 1. Ví dụ, để xóa phần tử 16 trong Hình

25.11a, hãy nối phần tử con bên phải của nó (trong trường hợp này là null) với phần tử cha của nút 16.

Trường hợp 2: Nút hiện tại có nút con bên trái. Để rightMost trỏ đến nút có chứa phần tử lớn nhất

trong cây con bên trái của nút hiện tại và parentOfRightMost trỏ đến nút cha của nút rightMost , như thể

hiện trong Hình 25.12a. Lưu ý rằng nút rightMost không thể có nút con bên phải nhưng có thể có nút con

bên trái. Thay thế giá trị phần tử trong hiện tại
với nút trong nút RightMost , kết nối nút parentOfRightMost với nút con bên trái của nút rightMost và xóa

nút RightMost , như thể hiện trong Hình 25.12b.

Ví dụ, hãy xem xét việc xóa nút 20 trong Hình 25.13a. Nút rightMost có giá trị phần tử là 16. Thay

thế giá trị phần tử 20 bằng 16 trong nút hiện tại và đặt nút 10 làm nút cha cho nút 14, như thể hiện

trong Hình 25.13b.

Ghi chú

Nếu con bên trái của dòng điện không có con bên phải, current.left trỏ đến phần tử lớn trong cây

con bên trái của dòng điện. Trong trường hợp này, rightMost là hiện tại.

left và parentOfRightMost là hiện tại. Bạn phải quan tâm đến trường hợp đặc biệt này để kết

nối lại đúng đứa con của rightMost với parentOfRightMost.


Machine Translated by Google

25.3 Xóa các phần tử khỏi BST 945

cha mẹ cha mẹ

hiện tại có thể là con


Nội dung của hiện tại
bên trái hoặc bên phải
hiện hành hiện hành
nút được thay thế bằng nội dung
của dòng điện mẹ trỏ đến của nút rightMost. Nút rightMost bị xóa.
nút bị xóa

Cây con bên phải Cây con bên phải

. .
. .
. .

parentOfRightMost parentOfRightMost

đúng Nội dung được sao


chép thành hiện tại và
vô giá trị
nút bị xóa

leftChildOfRightMost leftChildOfRightMost

(Một) (b)

HÌNH 25.12 Trường hợp 2: Nút hiện tại có nút con bên trái.

nguồn gốc 20 nguồn gốc 16

10 40 10 40

đúng

16 30 80 30 80

14 27 50 14 27 50

(Một) (b)

HÌNH 25.13 Trường hợp 2: Xóa nút 20 khỏi (a) dẫn đến kết quả là (b).

Thuật toán xóa một phần tử khỏi cây tìm kiếm nhị phân có thể được mô tả trong Liệt kê 25.7. phương pháp xóa

LISTING 25.7 Xóa một phần tử khỏi BST


1 xóa boolean (E e) {2
Xác định vị trí của phần tử e trong cây; 3 không ở trên cây

nếu phần tử e không được tìm thấy


Machine Translated by Google

946, chương 25, cây tìm kiếm nhị phân

4 trả về true;
5
xác định vị trí hiện tại 6 Đặt hiện tại là nút chứa e và cha là nút cha của hiện tại;
xác định vị trí cha mẹ
7

Trường hợp 1 if (hiện tại không có con bên trái) // Trường hợp 1
9 Kết nối đúng con của
10 11 hiện tại với cha mẹ; hiện tại không được tham chiếu, vì vậy
12 nó bị loại bỏ; else //
Trường hợp 2 13 Trường hợp 2
14 Xác định vị trí nút ngoài cùng bên phải trong cây con bên trái của dòng điện.
15 Sao chép giá trị phần tử ở nút ngoài cùng bên phải thành hiện tại.
16 Nối nút cha của nút ngoài cùng bên phải với nút con bên trái của nút ngoài
17 cùng bên phải;
18
19 trả về true; // Phần tử đã bị xóa
20}

Việc triển khai đầy đủ phương thức xóa được đưa ra trong các dòng 155–213 trong Liệt kê
25.5. Phương thức định vị nút (có tên hiện tại) sẽ bị xóa và cũng định vị nút cha (được
đặt tên là cha) của nó trong các dòng 157–170. Nếu hiện tại là null, phần tử không có trong
cây. Ở đó, phương thức trả về false (dòng 173). Xin lưu ý rằng nếu hiện tại là gốc, thì
giá trị gốc là null. Nếu cây trống, cả cây hiện tại và cây mẹ đều rỗng.
Trường hợp 1 của thuật toán được đề cập trong các dòng 176–187. Trong trường hợp này, nút hiện
tại không có nút con bên trái (ví dụ: current.left == null). Nếu cấp độ gốc là null, hãy gán current.right
tới gốc (dòng 178–180). Nếu không, hãy gán current.right cho parent.left hoặc parent.right, tùy thuộc
vào việc current là con bên trái hay bên phải của cha mẹ (182–185).
Trường hợp 2 của thuật toán được đề cập trong các dòng 188–209. Trong trường hợp này, dòng điện có một con trái.

Thuật toán xác định vị trí nút ngoài cùng bên phải (có tên là rightMost) trong cây con bên trái của
nút cur thuê và cũng là nút cha của nó (có tên là parentOfRightMost) (dòng 195–198). Thay phần tử trong

dòng điện bằng phần tử trong rightMost (dòng 201); gán rightMost.left
thành parentOfRightMost.right hoặc parentOfRightMost.left (dòng 204–208), tùy thuộc vào việc rightMost

là con phải hay trái của parentOfRightMost.


Liệt kê 25.8 đưa ra một chương trình thử nghiệm xóa các phần tử khỏi cây tìm kiếm nhị phân.

DANH SÁCH 25.8 TestBSTDelete.java


1 lớp công khai TestBSTDelete {
public static void main (String [] args) {
2 BST <String> tree = new BST <> ();
3 tree.insert ("George");
tree.insert ("Michael");
4 tree.insert ("Tom");
5 tree.insert ("Adam");
6 tree.insert ("Jones");
7 tree.insert ("Peter");
8 9 tree.insert ("Daniel");
10 11 printTree (cây);
12
13 System.out.println ("\ nSau khi xóa George:");
xóa một phần tử 14 tree.delete ("George");
15 printTree (cây);
16
17 System.out.println ("\ nSau khi xóa Adam:");
xóa một phần tử 18 tree.delete ("Adam");
19 printTree (cây);
20
21 System.out.println ("\ nSau khi xóa Michael:");
xóa một phần tử 22 tree.delete ("Michael");
Machine Translated by Google

25.3 Xóa các phần tử khỏi BST 947

23 printTree (cây);
24 }
25
26 public static void printTree (BST tree) {
27 // Traverse cây
28 System.out.print ("Inorder (đã sắp xếp): ");
29 tree.inorder ();
30 System.out.print ("\ nPostorder: ");
31 tree.postorder ();
32 System.out.print ("\ nĐặt hàng: ");
33 tree.preorder ();
34 System.out.print ("\ nSố nút là" + tree.getSize ());
35 System.out.println ();
36 }
37}

Inorder (đã sắp xếp): Adam Daniel George Jones Michael Peter Tom
Người đăng bài: Daniel Adam Jones Peter Tom Michael George
Đặt hàng trước: George Adam Daniel Michael Jones Tom Peter
Số nút là 7

Sau khi xóa George:


Inorder (đã sắp xếp): Adam Daniel Jones Michael Peter Tom
Người đăng bài: Adam Jones Peter Tom Michael Daniel
Đặt hàng trước: Daniel Adam Michael Jones Tom Peter
Số nút là 6

Sau khi xóa Adam:


Inorder (đã sắp xếp): Daniel Jones Michael Peter Tom
Người đăng bài: Jones Peter Tom Michael Daniel
Đặt hàng trước: Daniel Michael Jones Tom Peter
Số nút là 5

Sau khi xóa Michael:


Inorder (đã sắp xếp): Daniel Jones Peter Tom
Người đăng bài: Peter Tom Jones Daniel
Đặt hàng trước: Daniel Jones Tom Peter
Số nút là 4

Hình 25.14–25.16 cho thấy cây phát triển như thế nào khi các phần tử bị xóa khỏi nó.

Xóa cái này George Daniel


nút

Adam Michael Adam Michael

Daniel Jones Tom Jones Tom

Peter Peter

(a) Xóa George (b) Sau khi George bị xóa

HÌNH 25.14 Xóa George rơi vào Trường hợp 2.


Machine Translated by Google

948 chương 25 cây tìm kiếm nhị phân

Daniel Daniel
Xóa nút này

Adam Michael Michael

Jones Tom Jones Tom

Peter Peter

(a) Xóa Adam (b) Sau khi Adam bị xóa

HÌNH 25.15 Việc xóa Adam rơi vào Trường hợp 1.

Daniel Daniel

Xóa nút này Michael Jones

Jones Tom Tom

Peter Peter

(a) Xóa Michael (b) Sau khi Michael bị xóa

HÌNH 25.16 Xóa Michael rơi vào Trường hợp 2.

Lưu ý

Rõ ràng là độ phức tạp về thời gian cho người đặt hàng trước, đặt hàng trước và đặt hàng

sau là O (n), vì mỗi nút chỉ được duyệt một lần. Độ phức tạp về thời gian cho việc tìm

kiếm, chèn và xóa là chiều cao của cây. Trong trường hợp xấu nhất, chiều cao của cây là O

(n). Nếu một cây cân đối, chiều cao sẽ là O (logn). Chúng tôi sẽ giới thiệu cây nhị phân

cân bằng tốt trong Chương 26 và các Chương 40 và 41 bổ sung.

25.6 Hiển thị kết quả xóa 55 khỏi cây trong Hình 25.4b.

25.7 Hiển thị kết quả xóa 60 khỏi cây trong Hình 25.4b.

25.8 Độ phức tạp về thời gian của việc xóa một phần tử khỏi BST là bao nhiêu?

25,9 Thuật toán có đúng không nếu các dòng 204–208 trong Liệt kê 25,5 trong Trường hợp 2 của xóa ()

phương pháp được thay thế bởi mã sau đây?

parentOfRightMost.right = rightMost.left;
Machine Translated by Google

25.4 Trực quan hóa cây và MVC 949

25.4 Trực quan hóa cây và MVC


Bạn có thể sử dụng đệ quy để hiển thị một cây nhị phân.
Chìa khóa

Điểm

Lưu ý sư phạm
Một thách thức mà khóa học cấu trúc dữ liệu phải đối mặt là tạo động lực cho sinh viên. Việc

hiển thị cây nhị phân bằng đồ thị sẽ không chỉ giúp bạn hiểu hoạt động của cây nhị phân mà còn

có thể kích thích sự quan tâm của bạn đối với lập trình. Phần này giới thiệu các kỹ thuật để

hình dung cây nhị phân. Bạn cũng có thể áp dụng các kỹ thuật trực quan hóa cho các dự án khác.

Làm thế nào để bạn hiển thị một cây nhị phân? Nó là một cấu trúc đệ quy, vì vậy bạn có thể hiển thị một cây

nhị phân bằng cách sử dụng đệ quy. Bạn có thể chỉ cần hiển thị gốc, sau đó hiển thị hai cây con một cách đệ quy.

Các kỹ thuật hiển thị tam giác Sierpinski (Liệt kê 18.9, SierpinskiTriangle.java)

có thể được áp dụng để hiển thị một cây nhị phân. Để đơn giản, chúng tôi giả sử các khóa là số nguyên dương

nhỏ hơn 100. Danh sách 25.9 và 25.10 cung cấp cho chương trình và Hình 25.17 cho thấy một số lần chạy mẫu

của chương trình.

HÌNH 25.17 Cây nhị phân được hiển thị bằng đồ thị.

DANH SÁCH 25,9 BSTAnimation.java


1 nhập javafx.application.Application;
2 nhập javafx.geometry.Pos;
3 nhập javafx.stage.Stage;
4 nhập javafx.scene.Scene;
5 nhập javafx.scene.control.Button;
6 nhập javafx.scene.control.Label;
7 nhập javafx.scene.control.TextField;
8 nhập javafx.scene.layout.BorderPane;
9 nhập javafx.scene.layout.HBox;
10
11 lớp công khai BSTAnimation mở rộng Ứng dụng {
12 @Override // Ghi đè phương thức bắt đầu trong lớp Ứng dụng
13 public void start (Giai đoạn chínhStage) {
14 BST <Integer> tree = new BST <> (); // Tạo cây tạo một cái cây

15
16 Ngăn BorderPane = new BorderPane ();
17 BTView view = new BTView (cây); // Tạo một BTView xem cho cây
18 pane.setCenter (xem); nơi xem cây
19
20 TextField tfKey = new TextField ();
21 tfKey.setPrefColumnCount (3);
22 tfKey.setAlignment (Pos.BASELINE_RIGHT);
23 Nút btInsert = new Nút ("Chèn");
24 Nút btDelete = new Nút ("Xóa");
25 HBox hBox = mới HBox (5);
26 hBox.getChildren (). addAll (new Label ("Nhập khóa: "),
Machine Translated by Google

950 chương 25 cây tìm kiếm nhị phân

27 tfKey, btInsert, btDelete);


28 hBox.setAlignment (Pos.CENTER);
đặt hBox 29 pane.setBottom (hBox);
30
xử lý chèn 31 btInsert.setOnAction (e -> {
32 int key = Integer.parseInt (tfKey.getText ());
33 if (tree.search (key)) { // key đã có trong cây rồi
34 view.displayTree ();
35 view.setStatus (key + "đã có trong cây");
36 } khác {
đút chìa khóa vào
37 tree.insert (khóa); // Chèn khóa mới
trưng bày cây 38 view.displayTree ();
39 view.setStatus (phím + "được chèn trong cây");
40 }
41 });
42

xử lý xóa 43 btDelete.setOnAction (e -> {


44 int key = Integer.parseInt (tfKey.getText ());
45 if (! tree.search (key)) { // key không có trong cây
46 view.displayTree ();
47 view.setStatus (phím +} "không ở trên cây");
48 else {
phím xoá 49 tree.delete (khóa); // Xóa khóa
trưng bày cây 50 view.displayTree ();
51 view.setStatus (phím + "bị xóa khỏi cây");
52 }
53 });
54

// Tạo một khung cảnh và đặt khung vào vùng hiển thị
55 Cảnh cảnh = Cảnh mới (ngăn, 450, 250);
56 primaryStage.setTitle ("BSTAnimation"); // Đặt tiêu đề sân khấu
57 primaryStage.setScene (cảnh); // Đặt cảnh vào sân khấu
58 primaryStage.show (); // Hiển thị sân khấu
59 }
60 61}

NGHE 25.10 BTView.java


1 nhập javafx.scene.layout.Pane;
2 nhập javafx.scene.paint.Color;
3 nhập javafx.scene.shape.Circle;
4 nhập javafx.scene.shape.Line;
5 nhập javafx.scene.text.Text;
6
7 lớp công khai BTView mở rộng Ngăn {
cây để trưng bày BST riêng <Integer> tree = new BST <> ();
bán kính đôi riêng = 15; // Bán kính nút cây
8 9 10 riêng đôi vGap = 50; // Khoảng cách giữa hai cấp độ trong một cây
11
12 BTView (BST <Integer> cây) {
đặt một cái cây 13 this.tree = cây;
14 setStatus ("Cây trống");
15 }
16
17 public void setStatus (String msg) {
18 getChildren (). add (new Text (20, 20, msg));
19 }
20
Machine Translated by Google

25.4 Trực quan hóa cây và MVC 951

21 public void displayTree () {


22 this.getChildren (). clear (); // Xóa ngăn xóa màn hình
23 if (tree.getRoot ()! = null) {
24 // Hiển thị cây một cách đệ quy
25 displayTree (tree.getRoot (), getWidth () / 2, vGap, cây hiển thị một cách đệ quy
26 getWidth () / 4);
27 }
28 }
29

30 / ** Hiển thị một cây con bắt nguồn từ vị trí (x, y) * /


31 private void displayTree (gốc BST.TreeNode <Integer>,
32 nhân đôi x, nhân đôi y, nhân đôi hGap) {
33 if (root.left! = null) {
34 // Vẽ một đường tới nút bên trái
35 getChildren (). add (new Line (x - hGap, y + vGap, x, y)); // Vẽ đệ kết nối hai nút
36 quy cây con bên trái
37 displayTree (root.left, x - hGap, y + vGap, hGap / 2); vẽ cây con bên trái

38 }
39
40 if (root.right! = null) {
41 // Vẽ một đường tới nút bên phải
42 getChildren (). add (new Line (x + hGap, y + vGap, x, y)); // Vẽ đệ kết nối hai nút
43 quy cây con bên phải
44 displayTree (root.right, x + hGap, y + vGap, hGap / 2); vẽ cây con bên phải
45 }
46
47 // Hiển thị một nút
48 Circle circle = new Circle (x, y, radius);
49 circle.setFill (Màu.WHITE);
50 circle.setStroke (Color.BLACK);
51 getChildren (). addAll (vòng tròn, hiển thị một nút
52 new Text (x - 4, y + 4, root.element + ""));
53 }
54}

Trong Liệt kê 25.9, BSTAnimation.java, một cây được tạo (dòng 14) và chế độ xem dạng cây được đặt
trong ngăn (dòng 18). Sau khi một khóa mới được đưa vào cây (dòng 37), cây sẽ được sơn lại (dòng
38) để phản ánh sự thay đổi. Sau khi khóa bị xóa (dòng 49), cây sẽ được sơn lại (dòng 50) để phản
ánh sự thay đổi.
Trong Liệt kê 25.10, BTView.java, nút được hiển thị dưới dạng hình tròn có bán kính 15 (dòng 48).
Khoảng cách giữa hai cấp trong cây được xác định trong vGap 50 (dòng 25). hGap (dòng 32) xác định

khoảng cách giữa hai nút theo chiều ngang. Giá trị này giảm đi một nửa (hGap / 2)
ở cấp độ tiếp theo khi phương thức displayTree được gọi đệ quy (dòng 44, 51). Lưu ý rằng vGap không
được thay đổi trong cây.
Phương thức displayTree được gọi đệ quy để hiển thị cây con bên trái (dòng 33–38) và cây con
bên phải (dòng 40–45) nếu cây con không trống. Một dòng được thêm vào ngăn để nối hai nút (dòng 35,
42). Lưu ý rằng phương pháp này trước tiên sẽ thêm các dòng vào ô và sau đó thêm vòng tròn vào ô
(dòng 52) để các vòng tròn sẽ được sơn trên đầu các dòng để đạt được hiệu ứng hình ảnh mong muốn.

Chương trình giả định rằng các khóa là số nguyên. Bạn có thể dễ dàng sửa đổi chương trình với
một kiểu chung để hiển thị các khóa ký tự hoặc chuỗi ngắn.
Trực quan hóa dạng cây là một ví dụ của phần mềm architec mô hình-view-controller (MVC). Đây là
một kiến trúc quan trọng để phát triển phần mềm. Mô hình này là để lưu trữ và xử lý dữ liệu. Chế độ
xem là để trình bày dữ liệu một cách trực quan. Bộ điều khiển xử lý tương tác của người dùng với mô
hình và điều khiển chế độ xem, như trong Hình 25.18.
Machine Translated by Google

952, chương 25, cây tìm kiếm nhị phân

Bộ điều khiển

BSTAnimation

Quang cảnh Mô hình

BTView BST

HÌNH 25.18 Bộ điều khiển lấy dữ liệu và lưu trữ trong một mô hình. Chế độ xem hiển thị dữ liệu
được lưu trữ trong mô hình.

Kiến trúc MVC tách biệt việc lưu trữ và xử lý dữ liệu khỏi phần trình bày trực quan
của dữ liệu. Nó có hai lợi ích chính:

■ Nó làm cho nhiều chế độ xem có thể được chia sẻ dữ liệu thông qua cùng một mô hình.
Ví dụ, bạn có thể tạo một dạng xem mới hiển thị cây có gốc ở bên trái và cây mọc theo chiều
ngang ở bên phải (xem Bài tập lập trình 25.11).

■ Nó đơn giản hóa công việc viết các ứng dụng phức tạp và làm cho các thành phần có thể mở rộng và dễ

bảo trì. Có thể thực hiện các thay đổi đối với chế độ xem mà không ảnh hưởng đến mô hình và ngược

lại.

25.10 Phương thức displayTree sẽ được gọi bao nhiêu lần nếu cây trống? Phương thức displayTree sẽ
Kiểm tra điểm
được gọi bao nhiêu lần nếu cây có 100 nút?

25.11 Các nút trong cây được phương thức displayTree truy cập theo thứ tự nào: inorder,
đặt hàng trước, hoặc đặt hàng sau?

25.12 Điều gì sẽ xảy ra nếu mã ở dòng 47–52 trong BTView.java được chuyển sang dòng 33?

25.13 MVC là gì? Những lợi ích của MVC là gì?

25.5 Trình lặp lại


Chìa khóa
BST có thể lặp lại vì nó được định nghĩa là một kiểu con của giao diện java.lang.Iterable .
Điểm

Các phương thức inorder (), preorder () và postorder () hiển thị các phần tử trong inorder, preorder và

postorder trong một cây nhị phân. Các phương pháp này được giới hạn trong việc hiển thị các mốc cao trong

một cái cây. Nếu bạn muốn xử lý các phần tử trong cây nhị phân thay vì hiển thị chúng, thì không thể sử dụng
người lặp lại

các phương pháp này. Nhớ lại rằng một trình lặp được cung cấp để duyệt qua các phần tử trong một tập hợp

hoặc danh sách. Bạn có thể áp dụng phương pháp tương tự trong cây nhị phân để cung cấp một cách thống nhất

trong việc duyệt các phần tử trong cây nhị phân.

Giao diện java.lang.Iterable định nghĩa phương thức trình vòng lặp , phương thức này trả về một
thể hiện của giao diện java.util.Iterator . Giao diện java.util.Iterator (xem Hình 25.19) xác định

các tính năng chung của trình vòng lặp.

«Giao diện»
java.util.Iterator <E>

+ hasNext (): boolean Trả về true nếu trình lặp có nhiều phần tử hơn.

+ next (): E Trả về phần tử tiếp theo trong trình vòng lặp.

+ remove (): void Loại bỏ khỏi vùng chứa bên dưới phần tử cuối cùng được
trả về bởi trình vòng lặp (thao tác tùy chọn).

HÌNH 25.19 Giao diện Iterator xác định một cách thống nhất để duyệt qua các phần tử trong một vùng
chứa.
Machine Translated by Google

25.5 Trình lặp 953

Giao diện Tree mở rộng java.lang.Iterable. Vì BST là một lớp con của Cây thực thi
AbstractTree và AbstractTree , nên BST là một kiểu con của Iterable.
Giao diện Iterable chứa phương thức iterator () trả về một thể hiện của java.util.Iterator.

Bạn có thể duyệt cây nhị phân trong inorder, preorder hoặc postorder. Vì inorder được sử dụng thường xuyên,

chúng tôi sẽ sử dụng inorder để duyệt các phần tử trong cây nhị phân. Chúng tôi định nghĩa một lớp trình lặp có

tên InorderIterator để triển khai mặt liên java.util.Iterator trong Liệt kê 25.5 (dòng 221–263). Phương thức trình cách tạo một trình lặp

lặp chỉ đơn giản trả về một thể hiện của InorderIterator (dòng 217).

Phương thức khởi tạo InorderIterator gọi phương thức inorder (dòng 228). Phương thức inorder (root) (dòng

237–242) lưu trữ tất cả các phần tử từ cây trong danh sách. Các phần tử được duyệt trong inorder.

Khi một đối tượng Iterator được tạo, giá trị hiện tại của nó được khởi tạo thành 0 (dòng 225), trỏ đến phần

tử đầu tiên trong danh sách. Gọi phương thức next () trả về phần tử hiện tại và di chuyển hiện tại để trỏ đến

phần tử tiếp theo trong danh sách (dòng 253).

Phương thức hasNext () kiểm tra xem hiện tại có còn trong phạm vi danh sách hay không (dòng 246).

Phương thức remove () loại bỏ phần tử hiện tại khỏi cây (dòng 259). Sau đó, một

danh sách mới được tạo (dòng 260–261). Lưu ý rằng dòng điện không cần thay đổi.

Liệt kê 25.11 đưa ra một chương trình thử nghiệm lưu trữ các chuỗi trong một BST và hiển thị tất cả các

chuỗi ở dạng chữ hoa.

LISTING 25.11 TestBSTWithIterator.java


1 lớp công khai TestBSTWithIterator {
2 public static void main (String [] args) {
3 BST <String> tree = new BST <> ();
4
tree.insert ("George");
5 tree.insert ("Michael");
6 tree.insert ("Tom");
7 tree.insert ("Adam");
8 tree.insert ("Jones");
9 tree.insert ("Peter");
10 tree.insert ("Daniel");
11
cho (Chuỗi s: cây) sử dụng một trình lặp

System.out.print (s.toUpperCase () + " "); lấy chữ hoa


12 }
13 14 15}

ADAM DANIEL GEORGE JONES MICHAEL PETER TOM

Vòng lặp foreach (dòng 12–13) sử dụng một trình vòng lặp để duyệt qua tất cả các phần tử trong cây.

Hướng dẫn thiết kế


Iterator là một mẫu thiết kế phần mềm quan trọng. Nó cung cấp một cách thống nhất để đi qua mô hình trình lặp

các phần tử trong một thùng chứa, đồng thời ẩn các chi tiết cấu trúc của thùng chứa. Bằng ưu điểm của trình vòng lặp

cách đề cập đến cùng một giao diện java.util.Iterator, bạn có thể viết một chương trình

duyệt các phần tử của tất cả các vùng chứa theo cùng một cách.

Ghi chú

java.util.Iterator định nghĩa một trình vòng lặp chuyển tiếp, trình này sẽ duyệt các
phần tử trong trình vòng lặp theo hướng về phía trước và mỗi phần tử chỉ có thể được các biến thể của trình lặp

duyệt qua một lần. Java API cũng cung cấp java.util.ListIterator, hỗ trợ duyệt theo cả
hướng tiến và lùi. Nếu cấu trúc dữ liệu của bạn đảm bảo khả năng di chuyển linh hoạt,
bạn có thể xác định các lớp trình vòng lặp là một kiểu con của java.util.ListIterator.
Machine Translated by Google

954, chương 25, cây tìm kiếm nhị phân

Việc triển khai trình lặp không hiệu quả. Mỗi khi bạn xóa một phần tử thông qua trình lặp, toàn bộ
danh sách sẽ được xây dựng lại (dòng 261 trong Liệt kê 25.5 BST.java). Máy khách phải luôn sử dụng
phương thức xóa trong lớp BinraryTree để xóa một phần tử. Để ngăn người dùng sử dụng phương thức
remove trong trình lặp, hãy triển khai trình lặp như sau:

public void remove () {


ném UnsupportedOperationException mới
("Loại bỏ một phần tử khỏi trình lặp không được hỗ trợ");
}

Sau khi làm cho phương thức remove không được hỗ trợ bởi lớp trình lặp, bạn có thể triển khai trình
lặp hiệu quả hơn mà không cần phải duy trì danh sách cho các phần tử trong cây. Bạn có thể sử dụng
một ngăn xếp để lưu trữ các nút và nút trên cùng ngăn xếp chứa phần tử sẽ được trả về từ phương
thức next () . Nếu cây được cân bằng tốt, kích thước ngăn xếp tối đa sẽ là O (logn).

25.14 Trình lặp là gì?

Kiểm tra điểm 25.15 Phương thức nào được định nghĩa trong giao diện java.lang.Iterable <E> ?

25.16 Giả sử bạn xóa phần mở rộng Iterable <E> khỏi dòng 1 trong Liệt kê 25.3, Tree.java.
Liệu Liệt kê 25.11 có còn được biên dịch không?

25.17 Lợi ích của việc trở thành một kiểu con của Iterable <E> là gì?

25.6 Nghiên cứu điển hình: Nén dữ liệu


Mã hóa Huffman nén dữ liệu bằng cách sử dụng ít bit hơn để mã hóa các ký tự xảy ra thường
Chìa khóa

Điểm xuyên hơn. Các mã cho các ký tự được xây dựng dựa trên sự xuất hiện lại của các ký tự trong
văn bản bằng cách sử dụng cây nhị phân, được gọi là cây mã hóa Huffman.

Nén dữ liệu là một nhiệm vụ phổ biến. Có rất nhiều tiện ích có sẵn để nén tệp.
Phần này giới thiệu về mã hóa Huffman, được phát minh bởi David Huffman vào năm 1952.
Trong ASCII, mọi ký tự được mã hóa thành 8 bit. Nếu một văn bản bao gồm 100 ký tự, thì sẽ cần 800
bit để biểu diễn văn bản. Ý tưởng về mã hóa Huffman là sử dụng ít bit hơn để mã hóa các ký tự được
sử dụng thường xuyên trong văn bản và nhiều bit hơn để mã hóa các ký tự ít được sử dụng hơn nhằm

Mã hóa Huffman giảm kích thước tổng thể của tệp. Trong mã hóa Huffman, mã của các ký tự được xây dựng dựa trên sự
xuất hiện của các ký tự trong văn bản bằng cách sử dụng cây nhị phân, được gọi là cây mã hóa Huffman.
Giả sử văn bản là Mississippi. Cây Huffman của nó có thể được thể hiện như trong Hình 25.20a. Các
cạnh bên trái và bên phải của nút lần lượt được gán giá trị 0 và 1. Mỗi nhân vật là một chiếc lá
trên cây. Mã cho ký tự bao gồm các giá trị cạnh trong đường dẫn từ gốc đến lá, như trong Hình
25.20b. Vì i và s xuất hiện nhiều hơn M và p trong văn bản, chúng được gán mã ngắn hơn.
Dựa trên sơ đồ mã hóa trong Hình 25.20,
được mã hóa thành được giải mã thành

Mississippi = = = = = = = 7 000101011010110010011 = = = = = = = 7 Mississippi

0 1

Tính cách Mã số Tính thường xuyên

0 1 tôi
M 000 1
P 001 2
S 01 4
S 1 4
0 1 tôi

M p

(a) Cây mã hóa Huffman (b) Bảng mã ký tự

HÌNH 25.20 Các mã cho các ký tự được xây dựng dựa trên sự xuất hiện của các ký tự trong văn bản

bằng cách sử dụng một cây mã hóa.


Machine Translated by Google

25.6 Nghiên cứu điển hình: Nén dữ liệu 955

Cây mã hóa cũng được sử dụng để giải mã một chuỗi các bit thành các ký tự. Để làm như vậy, hãy bắt giải mã
đầu với bit đầu tiên trong chuỗi và xác định xem nên đi đến nhánh bên trái hay bên phải của gốc cây dựa

trên giá trị bit. Xem xét bit tiếp theo và tiếp tục đi xuống nhánh trái hoặc phải dựa trên giá trị bit.

Khi bạn đến một lá, bạn đã tìm thấy một nhân vật. Bit tiếp theo trong luồng là bit đầu tiên của ký tự

tiếp theo. Ví dụ: luồng 011001 được giải mã để nhấm nháp, với 01 s khớp , 1 khớp i và 001 khớp p.

Để xây dựng cây mã hóa Huffman, hãy sử dụng một thuật toán như sau: xây dựng cây mã hóa

1. Bắt đầu với một rừng cây. Mỗi cây chứa một nút cho một ký tự. Trọng lượng của

nút là tần số của ký tự trong văn bản.

2. Lặp lại hành động sau để kết hợp các cây cho đến khi chỉ còn một cây: Chọn hai cây có trọng lượng

nhỏ nhất và tạo một nút mới làm nút cha của chúng. Trọng lượng của cây mới là tổng trọng lượng

của các cây con.

3. Đối với mỗi nút bên trong, gán cho cạnh trái của nó một giá trị 0 và cạnh phải một giá trị 1. Tất cả các

nút lá đại diện cho các ký tự trong văn bản.

Đây là một ví dụ về việc xây dựng một cây mã hóa cho văn bản Mississippi. Bảng tần số cho các ký tự

được thể hiện trong Hình 25.20b. Ban đầu khu rừng chứa các cây một nút, như trong Hình 25.21a. Các cây

được kết hợp nhiều lần để tạo thành cây lớn cho đến khi chỉ còn lại một cây, như trong Hình 25.21b – d.

trọng lượng: 3 trọng lượng: 4 trọng lượng: 4

'S' 'tôi'

trọng lượng: 1 trọng lượng: 4 trọng lượng: 4 trọng lượng: 2 trọng lượng: 1 trọng lượng: 2

'M' 'S' 'tôi'


'P' 'M' 'P'

(Một) (b)

trọng lượng: 11

0 1

trọng lượng: 7 trọng lượng: 4 trọng lượng: 7 trọng lượng: 4

'tôi' 'tôi'

0 1

trọng lượng: 3 trọng lượng: 4 trọng lượng: 3 trọng lượng: 4

'S' 'S'

0 1

trọng lượng: 1 trọng lượng: 2 trọng lượng: 1 trọng lượng: 2

'M' 'P' 'M' 'P'

(C) (d)

HÌNH 25.21 Cây mã hóa được xây dựng bằng cách kết hợp nhiều lần hai giá trị có trọng số nhỏ nhất
cây.

Cần lưu ý rằng không có mã nào là tiền tố của mã khác. Thuộc tính này đảm bảo rằng thuộc tính tiền tố

các luồng có thể được giải mã một cách rõ ràng.

Lưu ý sư phạm
Để có bản trình diễn GUI tương tác để xem cách mã hóa Huffman hoạt động, hãy truy cập www.cs.armstrong Hoạt ảnh mã hóa Huffman đang bật

.edu / liang / animation / HuffmanCodingAnimation.html, như trong Hình 25.22. Trang web đồng hành
Machine Translated by Google

956, chương 25, cây tìm kiếm nhị phân

HÌNH 25.22 Công cụ hoạt ảnh cho phép bạn tạo và xem cây Huffman, đồng thời nó thực hiện mã hóa và giải mã bằng cách sử dụng
cây.

thuật toán tham lam Thuật toán được sử dụng ở đây là một ví dụ về thuật toán tham lam. Một thuật toán tham lam
thường được sử dụng trong việc giải quyết các vấn đề tối ưu hóa. Thuật toán đưa ra lựa chọn
tối ưu cục bộ với hy vọng rằng lựa chọn này sẽ dẫn đến giải pháp tối ưu toàn cục. Trong trường
hợp này, thuật toán luôn chọn hai cây có trọng số nhỏ nhất và tạo một nút mới làm nút cha của chúng.
Giải pháp cục bộ tối ưu trực quan này thực sự dẫn đến giải pháp tối ưu cuối cùng để xây dựng
cây Huffman. Một ví dụ khác, hãy xem xét đổi tiền thành ít đồng tiền nhất có thể.
Một thuật toán tham lam sẽ lấy đồng tiền lớn nhất có thể đầu tiên. Ví dụ: với 98 ¢, bạn sẽ sử
dụng ba phần tư để tạo ra 75 ¢, thêm hai dime để tạo ra 95 ¢ và thêm ba xu để tạo ra 98 ¢. Thuật
toán tham lam tìm ra một giải pháp tối ưu cho vấn đề này.
Tuy nhiên, một thuật toán tham lam không phải lúc nào cũng tìm ra kết quả tối ưu; xem vấn đề
nhập gói bin trong Bài tập lập trình 25.22.
Liệt kê 25.12 đưa ra một chương trình nhắc người dùng nhập một chuỗi, hiển thị tần suất
bảng các ký tự trong chuỗi và hiển thị mã Huffman cho mỗi ký tự.

DANH SÁCH 25.12 HuffmanCode.java


1 nhập java.util.Scanner;
2
3 lớp công khai HuffmanCode {
4 public static void main (String [] args) {
5 Đầu vào máy quét = Máy quét mới (System.in);
6 System.out.print ("Nhập văn bản: ");
7 String text = input.nextLine ();
số 8

tần số đếm int [] counts = getCharacterFrequency (văn bản); // Đếm tần số


9 10
11 System.out.printf ("% - 15s% -15s% -15s% -15s \ n",
12 "Mã ASCII", "Ký tự", "Tần số", "Mã");
13

lấy cây Huffman 14 Tree tree = getHuffmanTree (số lượng); // Tạo cây Huffman
mã cho mỗi ký tự 15 Chuỗi [] mã = getCode (tree.root); // Nhận mã
16
17 for (int i = 0; i <Code.length; i ++)
18 if (counts [i]! = 0) // (char) i không có trong văn bản nếu counts [i] là 0
19 System.out.printf ("% - 15d% -15s% -15d% -15s \ n",
Machine Translated by Google

25.6 Nghiên cứu điển hình: Nén dữ liệu 957

20 i, (char) i + "", đếm [i], mã [i]);


21 }
22
23 / ** Lấy mã Huffman cho các ký tự * Phương thức này
24 được gọi một lần sau khi cây Huffman được xây dựng
25 * /
26 public static String [] getCode (gốc Tree.Node) { nhận được mã

27 if (root == null) return null;


28 Chuỗi [] mã = new Chuỗi [2 * 128];
29 gánCode (gốc, mã);
30 trả lại mã;
31 }
32
33 / * Nhận mã một cách đệ quy đến nút lá * /
34 private static void mateCode (Tree.Node root, String [] các mã) { gán mã
35 if (root.left! = null) {
36 root.left.code = root.code + "0";
37 gánCode (root.left, mã);
38
39 root.right.code = root.code + "1";
40 gánCode (root.right, các mã);
41 }
42 khác {
43 mã [(int) root.element] = root.code;
44 }
45 }
46
47 / ** Nhận cây Huffman từ các mã * /
48 public static Tree getHuffmanTree (int [] counts) { getHuffmanTree
49 // Tạo một đống để chứa cây
50 Heap <Tree> heap = new Heap <> (); // Được định nghĩa trong Liệt kê 23.9
51 for (int i = 0; i <counts.length; i ++) {
52 nếu (đếm [i]> 0)
53 heap.add (new Tree (counts [i], (char) i)); // Một cây nút lá
54 }
55

56 trong khi (heap.getSize ()> 1) {


57 Cây t1 = heap.remove (); // Loại bỏ cây có trọng số nhỏ nhất
58 Cây t2 = heap.remove (); // Loại bỏ heap.add nhỏ nhất tiếp theo
59 (new Tree (t1, t2)); // Kết hợp hai cây
60 }
61
62 return heap.remove (); // Cây cuối cùng
63 }
64
65 / ** Nhận tần số của các ký tự * /
66 public static int [] getCharacterFrequency (Chuỗi văn bản) { getCharacterFrequency
67 int [] counts = new int [256]; // 256 ký tự ASCII
68
69 for (int i = 0; i <text.length (); i ++)
70 counts [(int) text.charAt (i)] ++; // Đếm các ký tự trong văn bản
71
72 số lần trả lại ;
73 }
74
75 / ** Xác định cây mã hóa Huffman * /
76 public static class Tree triển khai <Tree> {có thể so sánh được Cây huffman
77 Gốc nút; // Gốc cây
78
79 / ** Tạo một cây với hai cây con * /
Machine Translated by Google

958, chương 25, cây tìm kiếm nhị phân

80 Cây công cộng (Cây t1, Cây t2) {


81 root = new Node ();
82 root.left = t1.root;
83 root.right = t2.root;
84 root.weight = t1.root.weight + t2.root.weight;
85 }
86
87 / ** Tạo một cây chứa một nút lá * /
88 cây công cộng (int weight, phần tử char ) {
89 root = new Node (trọng số, phần tử);
90 }
91
92 @Override / ** So sánh các cây dựa trên trọng lượng của chúng * /
93 public int so sánhTo (Cây t) {
94 if (root.weight <t.root.weight) // Đảo ngược thứ tự một cách có chủ đích
95 trả về 1;
96 else if (root.weight == t.root.weight)
97 trả về 0;
98 khác
99 trả về -1;
100 }
101
nút cây 102 Node lớp công khai {
103 phần tử char ; // Lưu ký tự cho một nút lá
104 trọng lượng int ; // trọng số của cây con bắt nguồn từ nút này
105 Nút trái; // Tham chiếu đến cây con bên trái
106 Nút bên phải; // Tham chiếu đến cây con bên phải
107 Mã chuỗi = ""; // Mã của nút này từ gốc
108
109 / ** Tạo một nút trống * /
110 nút công khai () {
111 }
112
113 / ** Tạo một nút với trọng lượng và ký tự được chỉ định * /
114 public Node (int weight, phần tử char ) {
115 this.weight = trọng lượng;
116 this.element = phần tử;
117 }
118 }
119 }
120}

Nhập văn bản: Chào mừng


Mã ASCII Nhân vật 87 Tần suất 1 1 Mã số
2 1 1 1 110
W 99 101 108 109 111 C 111
10
el 011
m 010
o 00

Chương trình nhắc người dùng nhập một chuỗi văn bản (dòng 5–7) và đếm tần suất của các
getCharacterFrequency ký tự trong văn bản (dòng 9). Phương thức getCharacterFrequency (dòng 66–73) tạo một số
mảng để đếm số lần xuất hiện của mỗi trong số 256 bộ truyền ký tự ASCII trong văn bản.
Nếu một ký tự xuất hiện trong văn bản, số lượng tương ứng của nó sẽ tăng lên 1 (dòng
70).
Machine Translated by Google

Đố 959

Chương trình thu được một cây mã hóa Huffman dựa trên số đếm (dòng 14). Cây bao gồm các nút
được liên kết. Lớp Node được định nghĩa trong các dòng 102–118. Mỗi nút bao gồm phần tử thuộc tính Lớp nút

(lưu trữ ký tự), trọng số (lưu trữ trọng số của cây con dưới nút này), bên trái (liên kết đến cây
con bên trái), bên phải (liên kết đến cây con bên phải) và mã
(lưu trữ mã Huffman cho nhân vật). Lớp Cây (dòng 76–119) chứa thuộc tính gốc. Từ gốc, bạn có thể Lớp cây

truy cập tất cả các nút trong cây. Lớp Cây có nghĩa là có thể so sánh được. Các cây có thể so sánh
dựa trên trọng lượng của chúng. Thứ tự so sánh được đảo ngược có chủ ý (dòng 93–100) để cây có
trọng lượng nhỏ nhất được loại bỏ trước tiên khỏi đống cây.

Phương thức getHuffmanTree trả về một cây mã hóa Huffman. Ban đầu, các cây nút đơn được tạo và getHuffmanTree

thêm vào đống (dòng 50–54). Trong mỗi lần lặp của vòng lặp while (dòng 56–60), hai cây có trọng
lượng nhỏ nhất được lấy ra khỏi đống và được kết hợp để tạo thành một cây lớn, sau đó cây mới được
thêm vào đống. Quá trình này tiếp tục cho đến khi đống chỉ chứa một cây, đó là cây Huffman cuối cùng
của chúng ta cho văn bản.
Phương thức AssignCode chỉ định mã cho mỗi nút trong cây (dòng 34–45). Phương thức getCode lấy gán mã

mã cho mỗi ký tự trong nút lá (dòng 26–31). Các mã phần tử [i] chứa mã cho ký tự (char) i, trong đó nhận được mã

i từ 0 đến 255. Lưu ý rằng mã [i] là null nếu (char) i không có trong văn bản.

25.18 Mỗi nút bên trong cây Huffman có hai nút con. Nó có đúng không?
Kiểm tra điểm

25.19 Thuật toán tham lam là gì? Cho một ví dụ.

25.20 Nếu lớp Heap ở dòng 50 trong Liệt kê 25.10 được thay thế bằng
java.util.PutorQueue, chương trình sẽ vẫn hoạt động chứ?

ĐIỀU KHOẢN CHÍNH

cây tìm kiếm nhị phân 930 Mã hóa Huffman 954


cây nhị phân 930 inorder traversal 933
theo chiều rộng-thứ nhất 934 thiết bị bưu điện qua 933
đường đi ngang đầu tiên 934 đặt hàng trước qua 934
thuật toán tham lam 956 xuyên qua cây 933

TÓM TẮT CHƯƠNG

1. Cây tìm kiếm nhị phân (BST) là một cấu trúc dữ liệu phân cấp. Bạn đã học cách xác định
và triển khai một lớp BST, cách chèn và xóa các phần tử vào / khỏi BST cũng như cách
duyệt qua BST bằng cách sử dụng tìm kiếm inorder, postorder, preorder, depth-first và
width-first.

2. Trình lặp là một đối tượng cung cấp một cách thống nhất để duyệt qua các phần tử trong một trình điều

khiển, chẳng hạn như một tập hợp, một danh sách hoặc một cây nhị phân. Bạn đã học cách xác định và triển

khai các lớp trình vòng lặp để duyệt qua các phần tử trong cây nhị phân.

3. Mã hóa Huffman là một sơ đồ nén dữ liệu bằng cách sử dụng ít bit hơn để mã hóa các bộ
truyền động char xảy ra thường xuyên hơn. Mã cho các ký tự được xây dựng dựa trên sự xuất
hiện của các ký tự trong văn bản bằng cách sử dụng cây nhị phân, được gọi là cây mã hóa Huffman.

ĐỐ

Trả lời câu hỏi cho chương này trực tuyến tại www.cs.armstrong.edu/liang/intro10e/quiz.html.
Machine Translated by Google

960 Chương 25 Cây tìm kiếm nhị phân

BÀI TẬP LẬP TRÌNH

Phần 25,2–25,6

* 25.1 (Thêm phương thức mới trong BST) Thêm các phương thức mới sau vào BST.

/ ** Hiển thị các nút trong truyền qua chiều rộng-ưu tiên * /
public void breadthFirstTraversal ()

/ ** Trả về chiều cao của cây nhị phân này * /


public int height ()

* 25.2 (Kiểm tra cây nhị phân đầy đủ) Cây nhị phân đầy đủ là cây nhị phân với các lá trên
Cùng trình độ. Thêm một phương thức trong lớp BST để trả về true nếu cây là một

Cây nhị phân. (Gợi ý: Số nút trong cây nhị phân đầy đủ là 2depth - 1.)

/ ** Trả về true nếu cây là cây nhị phân đầy đủ * /


boolean isFullBST ()

** 25.3 (Triển khai trình duyệt inorder mà không sử dụng đệ quy) Triển khai trình duyệt inorder
trong BST sử dụng ngăn xếp thay vì đệ quy. Viết chương trình thử nghiệm nhắc
người dùng nhập 10 số nguyên, lưu trữ chúng trong BST và gọi phương thức inorder
để hiển thị các phần tử.

** 25.4 (Triển khai đặt hàng trước truyền tải mà không sử dụng đệ quy) Triển khai phương thức
đặt hàng trước trong BST bằng cách sử dụng ngăn xếp thay vì đệ quy. Viết một
chương trình thử nghiệm nhắc người dùng nhập 10 số nguyên, lưu trữ chúng trong
BST và gọi phương thức đặt hàng trước để hiển thị các phần tử.

** 25.5 (Triển khai duyệt thứ tự sau mà không sử dụng đệ quy) Triển khai phương thức sắp xếp
sau trong BST bằng cách sử dụng ngăn xếp thay vì đệ quy. Viết một chương trình
thử nghiệm nhắc người dùng nhập 10 số nguyên, lưu trữ chúng trong BST và gọi
phương thức postorder để hiển thị các phần tử.

** 25.6 (Tìm lá) Thêm một phương thức trong lớp BST để trả về số
lá như sau:

/ ** Trả về số lượng nút lá * /


public int getNumberOfLeaves ()

** 25.7 (Tìm các vi sóng) Thêm một phương thức trong lớp BST để trả về số lượng các vi tần
như sau:

/ ** Trả về số lượng các nút không phải ở lá * /


public int getNumberofNonLeaves ()

*** 25.8 (Triển khai trình lặp hai chiều) Giao diện java.util.Iterator định nghĩa một trình lặp
chuyển tiếp. API Java cũng cung cấp java.util.ListIterator
giao diện xác định một trình lặp hai chiều. Nghiên cứu ListIterator và xác định một
trình lặp hai chiều cho lớp BST .

** 25.9 (Nhân bản cây và bằng) Thực hiện các phương thức nhân bản và bằng trong lớp BST . Hai
cây BST là bằng nhau nếu chúng chứa các phần tử giống nhau. Phương thức clone trả
về một bản sao giống hệt của BST.
Machine Translated by Google

Bài tập lập trình 961

25.10 (Trình vòng lặp đặt hàng trước) Thêm phương thức sau trong lớp BST trả về một trình vòng
lặp để duyệt qua các phần tử trong BST ở chế độ đặt hàng trước.

/ ** Trả về một trình lặp để duyệt qua các phần tử trong đơn đặt hàng trước * /
java.util.Iterator <E> preorderIterator ()

25.11 (Cây hiển thị) Viết một lớp dạng xem mới hiển thị cây theo chiều ngang
với gốc ở bên trái như trong Hình 25.23.

HÌNH 25.23 Cây nhị phân được hiển thị theo chiều ngang.

** 25.12 ( BST thử nghiệm) Thiết kế và viết một chương trình thử nghiệm hoàn chỉnh để kiểm tra xem lớp BST trong Liệt

kê 25.5 có đáp ứng tất cả các yêu cầu hay không.

** 25.13 (Thêm các nút mới trong BSTAnimation) Sửa đổi Liệt kê 25.9, BSTAnimation.java, để thêm ba
nút mới — Show Inorder, Show Preorder và Show Postorder—

để hiển thị kết quả trong một nhãn, như trong Hình 25.24. Bạn cũng cần sửa đổi BST.java
để triển khai các phương thức inorderList (), preorderList () và postorderList () để
mỗi phương thức này trả về Danh sách các phần tử nút trong inorder, preorder và

postorder, như sau:

public java.util.List <E> inorderList ();


public java.util.List <E> preorderList ();
public java.util.List <E> postorderList ();

HÌNH 25.24 Khi bạn nhấp vào nút Show Inorder, Show Preorder, hoặc Show Postorder, các phần tử được
hiển thị trong một inorder, preorder hoặc postorder trong một nhãn.
Machine Translated by Google

962, chương 25, cây tìm kiếm nhị phân

* 25.14 ( BST chung sử dụng Bộ so sánh) Sửa đổi BST trong Liệt kê 25.5, sử dụng tham số chung và Bộ

so sánh để so sánh các đối tượng. Xác định một phương thức khởi tạo mới với một Bộ so

sánh như đối số của nó như sau:

BST (Comparator <? Super E> comparator)

* 25.15 (Tham chiếu dành cho phụ huynh cho BST) Xác định lại Mã cây bằng cách thêm tham chiếu vào một

cha của nút, như được hiển thị bên dưới:

BST.TreeNode <E>

#element: E
#left: TreeNode <E>
#right: TreeNode <E>
#parent: TreeNode <E>

Thực hiện lại các phương thức chèn và xóa trong lớp BST để cập nhật nút cha cho mỗi

nút trong cây. Thêm phương thức mới sau vào BST:

/ ** Trả về nút cho phần tử được chỉ định.


* Trả về null nếu phần tử không có trong cây. * /
private TreeNode <E> getNode (phần tử E)

/ ** Trả về true nếu nút của phần tử là một lá * /


boolean riêng isLeaf (phần tử E)

/ ** Trả về đường dẫn của các phần tử từ phần tử đã chỉ định


* ở gốc trong danh sách mảng. * /
public ArrayList <E> getPath (E e)

Viết chương trình thử nghiệm nhắc người dùng nhập 10 số nguyên, thêm chúng vào cây,

xóa số nguyên đầu tiên khỏi cây và hiển thị đường dẫn cho tất cả các nút lá. Đây là một

cuộc chạy mẫu:

Nhập 10 số nguyên: 45 54 67 56 50 45 23 59 23 67
[50, 54, 23]
[59, 56, 67, 54, 23]

*** 25.16 (Nén dữ liệu: Mã hóa Huffman) Viết chương trình nhắc người dùng nhập
tên tệp, sau đó hiển thị bảng tần số của các ký tự trong tệp và hiển
thị mã Huffman cho từng ký tự.

*** 25.17 (Nén dữ liệu: Hoạt ảnh mã hóa Huffman) Viết chương trình cho phép người
dùng nhập văn bản và hiển thị cây mã hóa Huffman dựa trên văn bản,
như trong Hình 25.25a. Hiển thị trọng số của cây con bên trong vòng
tròn gốc của cây con. Hiển thị ký tự của mỗi nút lá. Hiển thị các bit
được mã hóa cho văn bản trong nhãn. Khi người dùng nhấp vào Văn bản Giải mã
, một chuỗi bit được giải mã thành văn bản hiển thị trong nhãn, như trong Hình 25.25b.
Machine Translated by Google

Bài tập lập trình 963

(Một)

(b)

HÌNH 25.25 (a) Hoạt ảnh hiển thị cây mã hóa cho một chuỗi văn bản nhất định và các bit được
mã hóa cho văn bản được hiển thị trong nhãn; (b) Bạn có thể nhập một chuỗi bit để hiển thị văn
bản của nó trong nhãn.

*** 25.18 (Nén tệp) Viết chương trình nén tệp nguồn thành tệp đích bằng phương pháp mã hóa
Huffman. Đầu tiên sử dụng ObjectOutputStream để xuất các mã Huffman vào tệp
đích, sau đó sử dụng BitOutputStream trong Bài tập lập trình 17.17 để xuất nội
dung nhị phân được mã hóa sang tệp tar get. Chuyển các tệp từ dòng lệnh bằng
lệnh sau:

java Practice25_18 sourcefile targetfile

*** 25.19 (Giải nén tệp) Bài tập trước nén tệp. Tệp nén chứa mã Huffman và nội dung được
nén. Viết chương trình giải nén tệp nguồn thành tệp đích bằng lệnh sau:

java Practice25_19 sourcefile targetfile

25.20 (Đóng gói thùng sử dụng khớp đầu tiên) Viết chương trình đóng gói các đối tượng
có trọng lượng khác nhau vào thùng chứa. Mỗi thùng có thể chứa tối đa 10
pound. Chương trình sử dụng một thuật toán tham lam đặt một đối tượng vào
thùng đầu tiên mà nó sẽ phù hợp. Chương trình của bạn sẽ nhắc người dùng nhập tổng số
Machine Translated by Google

964, chương 25, cây tìm kiếm nhị phân

vật và khối lượng của mỗi vật. Chương trình hiển thị tổng số thùng chứa cần thiết để đóng gói

các đối tượng và nội dung của mỗi thùng chứa. Đây là bản chạy mẫu của chương trình:

Nhập số đối tượng: 6


Nhập khối lượng của các vật: 7 5 2 3 5 8
Thùng 1 chứa các vật có khối lượng 7 2
Thùng 2 chứa các vật có khối lượng 5 3
Thùng 3 chứa các vật có khối lượng 5
Thùng 4 chứa vật có khối lượng 8

Chương trình này có đưa ra một giải pháp tối ưu, đó là tìm số lượng thùng chứa tối thiểu để

đóng gói các đối tượng không?

25.21 (Đóng gói thùng với đối tượng nhỏ nhất trước) Viết lại chương trình trước đó sử dụng
thuật toán tham lam mới để đặt một đối tượng có trọng lượng nhỏ nhất vào thùng
đầu tiên mà nó sẽ phù hợp. Chương trình của bạn sẽ nhắc người dùng nhập tổng số
đối tượng và trọng lượng của từng đối tượng. Chương trình hiển thị tổng số
thùng chứa cần thiết để đóng gói các đối tượng và nội dung của mỗi thùng chứa. Đây
là bản chạy mẫu của chương trình:

Nhập số đối tượng: 6


Nhập khối lượng của các vật: 7 5 2 3 5 8
Thùng 1 chứa các vật có khối lượng 2 3 5
Thùng 2 chứa các vật có khối lượng 5
Thùng 3 chứa vật có khối lượng 7
Thùng 4 chứa vật có khối lượng 8

Chương trình này có đưa ra một giải pháp tối ưu, đó là tìm số lượng thùng chứa tối thiểu để

đóng gói các đối tượng không?

25.22 (Đóng thùng với đối tượng lớn nhất trước) Viết lại chương trình trước đó để đặt một
đối tượng có trọng lượng lớn nhất vào thùng đầu tiên mà nó sẽ vừa. Cho một ví dụ
để chứng tỏ rằng chương trình này không tạo ra một giải pháp tối ưu.

25.23 (Đóng gói thùng tối ưu) Viết lại chương trình trước đó để nó tìm ra giải pháp tối
ưu đóng gói tất cả các đối tượng bằng cách sử dụng số lượng thùng nhỏ nhất. Đây
là bản chạy mẫu của chương trình:

Nhập số đối tượng: 6


Nhập khối lượng của các vật: 7 5 2 3 5 8
Thùng 1 chứa các vật có khối lượng 7 3
Thùng 2 chứa các vật có khối lượng 5 5
Thùng 3 chứa vật có khối lượng 2 8
Số thùng tối ưu là 3

Độ phức tạp về thời gian của chương trình của bạn là gì?
Machine Translated by Google

CHƯƠNG

26
CÂY AVL

Mục tiêu
■ Để biết cây AVL là gì (§26.1).

■ Để hiểu cách cân bằng lại cây bằng cách sử dụng phép quay LL,
phép quay LR, phép quay RR và phép quay RL (§26.2).

■ Để thiết kế lớp AVLTree bằng cách mở rộng lớp BST (§26.3).

■ Để chèn các phần tử vào cây AVL (§26.4).

■ Để thực hiện tái cân bằng cây (§26.5).

■ Để xóa các phần tử khỏi cây AVL (§26.6).

■ Để triển khai lớp AVLTree (§26.7).

■ Để kiểm tra lớp AVLTree (§26.8).

■ Để phân tích độ phức tạp của các hoạt động tìm kiếm, chèn và xóa trong
cây AVL (§26.9).
Machine Translated by Google

966, chương 26, cây AVL

26.1 Giới thiệu


Cây AVL là một cây tìm kiếm nhị phân cân bằng.
Chìa khóa

Điểm
Chương 25 đã giới thiệu cây tìm kiếm nhị phân. Thời gian tìm kiếm, chèn và xóa đối với cây nhị
phân phụ thuộc vào chiều cao của cây. Trong trường hợp xấu nhất, chiều cao là O (n). Nếu một cây
cây cân bằng hoàn hảo hoàn toàn cân bằng - tức là một cây nhị phân hoàn chỉnh - thì chiều cao của nó là log n. Chúng ta
có thể duy trì một cây cân bằng hoàn hảo không? Có, nhưng làm như vậy sẽ rất tốn kém. Sự thỏa
cây cân đối hiệp là duy trì một cây cân bằng - nghĩa là, chiều cao của mỗi cây con của hai nút là như nhau.
Chương này giới thiệu cây AVL. Web Chương 40 và 41 giới thiệu cây 2-4 và cây đỏ-đen.
Cây AVL Cây AVL rất cân đối. Cây AVL được phát minh vào năm 1962 bởi hai nhà khoa học máy tính người
Nga, GM Adelson-Velsky và EM Landis (do đó có tên là AVL). Trong cây AVL, hiệu số giữa chiều cao
của hai cây con của mọi nút là 0 hoặc 1. Có thể chỉ ra rằng chiều cao tối đa của cây AVL là O (log
O (log n) n).
Quá trình chèn hoặc xóa một phần tử trong cây AVL giống như trong cây tìm kiếm nhị phân thông
thường, ngoại trừ việc bạn có thể phải cân bằng lại cây sau khi thực hiện thao tác chèn hoặc xóa.
yếu tố cân bằng Hệ số cân bằng của một nút là chiều cao của cây con bên phải trừ đi chiều cao của cây con bên
cân bằng trái. Một nút được cho là cân bằng nếu hệ số cân bằng của nó là -1, 0 hoặc 1. Một nút được coi

trái nặng là nặng bên trái nếu hệ số cân bằng của nó là -1 và nặng bên phải nếu hệ số cân bằng của nó là +1.
nặng nề
Lưu ý sư phạm
Để có bản trình diễn GUI tương tác để xem cách cây AVL hoạt động, hãy truy cập www.cs.armstrong.edu/

liang / animation / web / AVLTree.html, như trong Hình 26.1.

Hình ảnh động cây AVL đang bật

Trang web đồng hành

HÌNH 26.1 Công cụ hoạt ảnh cho phép bạn chèn, xóa và tìm kiếm các phần tử.

26.2 Cây tái cân bằng


Sau khi chèn hoặc xóa một phần tử khỏi cây AVL, nếu cây mất cân bằng, hãy thực
Chìa khóa

Điểm hiện thao tác xoay để cân bằng lại cây.

Nếu một nút không cân bằng sau một thao tác chèn hoặc xóa, bạn cần phải cân bằng lại nó. Quá trình
Vòng xoay tái cân bằng một nút được gọi là quay. Có bốn cách xoay có thể xảy ra: LL, RR, LR và RL.
Machine Translated by Google

26.2 Cây tái cân bằng 967

Vòng quay LL: Sự mất cân bằng LL xảy ra tại một nút A, sao cho A có hệ số cân bằng là -2 Vòng quay LL

và con trái B với hệ số cân bằng -1 hoặc 0, như trong Hình 26.2a. Loại mất cân bằng này có thể được LL mất cân bằng

khắc phục bằng cách thực hiện một chuyển động quay sang phải tại A, như trong Hình 26.2b.
Vòng quay RR: Sự mất cân bằng RR xảy ra tại một nút A, sao cho A có hệ số cân bằng là +2 và nút con Xoay vòng RR

bên phải B có hệ số cân bằng +1 hoặc 0, như trong Hình 26.3a. Loại mất cân bằng này có thể được khắc Mất cân bằng RR

phục bằng cách thực hiện một động tác quay sang trái tại A, như trong Hình 26.3b.

A 2 0 hoặc 1 B

1 hoặc 0 B A 0 hoặc 1

T3 h

h 1 T1

h T2 h T2 T3 h

h 1 T1
Chiều cao của T2 là h Chiều cao của T2 là h
hoặc h 1 hoặc h 1

(Một) (b)

HÌNH 26.2 Vòng quay LL khắc phục sự mất cân bằng LL.

A 2 B 0 hoặc 1

B 1 hoặc 0 0 hoặc 1 A
h T3

T1 h 1

h h T3 h T2
T2

T1 giờ 1
Chiều cao của T2 là Chiều cao của T2 là
h hoặc h 1 h hoặc h 1

(Một) (b)

HÌNH 26.3 Một vòng quay RR khắc phục sự mất cân bằng RR.

Vòng quay LR: Sự mất cân bằng LR xảy ra tại một nút A, sao cho A có hệ số cân bằng là -2 Vòng quay LR

và con trái B với hệ số cân bằng +1, như trong Hình 26.4a. Giả sử con bên phải của B là C. Loại mất Mất cân bằng LR

cân bằng này có thể được khắc phục bằng cách thực hiện một phép quay kép (đầu tiên là một phép quay
trái đơn tại B và sau đó là một phép quay đơn sang phải tại A), như trong Hình 26.4b.
Vòng quay RL: Sự mất cân bằng RL xảy ra tại một nút A, sao cho A có hệ số cân bằng là +2 Xoay vòng RL

và con phải B với hệ số cân bằng -1, như trong Hình 26.5a. Giả sử con trái của B là C. Loại mất cân RL mất cân bằng

bằng này có thể được khắc phục bằng cách thực hiện quay kép (đầu tiên là quay phải đơn tại B và sau
đó quay trái đơn tại A), như trong Hình 26.5b.

26.1 Cây AVL là gì? Mô tả các thuật ngữ sau: hệ số cân bằng, cân trái và
phải-nặng. Kiểm tra điểm

26.2 Chỉ ra hệ số cân bằng của mỗi nút trong các cây trong Hình 26.6.

26.3 Mô tả phép quay LL, phép quay RR, phép quay LR và phép quay RL cho cây AVL.
Machine Translated by Google

968, chương 26, cây AVL

A 2 C 0

1 B 0 hoặc 1 B MỘT 0 hoặc 1

T4 h

C 1, 0 hoặc 1
h h T1 h T2 h T3 T4 h
T1

T2 và T3 có thể có
chiều cao khác nhau, nhưng
h T2 h T3 ít nhất một người có

chiều cao của h.

(Một) (b)

HÌNH 26.4 Một vòng quay LR khắc phục sự mất cân bằng LR.

A 2 C 0

1 B 0 hoặc 1 MỘT B 0 hoặc 1

h T1

0, 1 hoặc 1 C
h h T1 h T2 h T3 T4 h
T4

T2 và T3 có thể có
h T2 h T3 chiều cao khác nhau, nhưng
ít nhất một

có chiều cao là h.

(Một) (b)

HÌNH 26.5 Vòng quay RL khắc phục sự mất cân bằng RL.

60 60

55 100 55 100

45 67 107 45 67 107

87 87 105 187

(Một) (b)

HÌNH 26.6 Hệ số cân bằng xác định xem một nút có cân bằng hay không.
Machine Translated by Google

26.3 Thiết kế lớp cho cây AVL 969

26.3 Thiết kế lớp cho cây AVL


Vì cây AVL là cây tìm kiếm nhị phân, nên AVLTree được thiết kế như một lớp con của BST.

Cây AVL là một cây nhị phân, vì vậy bạn có thể định nghĩa lớp AVLTree để mở rộng lớp BST , như
thể hiện trong Hình 26.7. Các lớp BST và TreeNode đã được định nghĩa trong Phần 25.2.5.

TreeNode <E> BST <E mở rộng có thể so sánh được <E>>

m 0
AVLTreeNode <E> AVLTree <E mở rộng có thể so sánh được <E>>

#height: int + AVLTree () Tạo một cây AVL trống.

+ AVLTree (đối tượng: E []) Tạo cây AVL từ một mảng đối tượng.

#createNewNode (): AVLTreeNode <E> Ghi đè phương pháp này để tạo AVLTreeNode.

+ insert (e: E): boolean Trả về true nếu phần tử được thêm thành công.
1
Liên kết + delete (e: E): boolean Trả về true nếu phần tử bị xóa khỏi

cây thành công.

-updateHeight (nút: Đặt lại chiều cao của nút được chỉ định.
AVLTreeNode <E>): void

-balancePath (e: E): void Cân bằng các nút trong đường dẫn từ nút cho
phần tử gốc nếu cần.

-balanceFactor (nút: Trả về hệ số cân bằng của nút.

AVLTreeNode <E>): int

-balanceLL (A: TreeNode, Thực hiện cân bằng LL.

parentOfA: TreeNode <E>): void

-balanceLR (A: TreeNode <E>, Thực hiện cân bằng LR.

parentOfA: TreeNode <E>): void

-balanceRR (A: TreeNode <E>, Thực hiện cân bằng RR.

parentOfA: TreeNode <E>): void

-balanceRL (A: TreeNode <E>, Thực hiện cân bằng RL.

parentOfA: TreeNode <E>): void

HÌNH 26.7 Lớp AVLTree mở rộng BST với các triển khai mới cho các phương thức chèn và xóa .

Để cân bằng cây, bạn cần biết chiều cao của mỗi nút. Để thuận tiện, hãy lưu trữ chiều
cao của mỗi nút trong AVLTreeNode và xác định AVLTreeNode là một lớp con của BST.TreeNode. AVLTreeNode
Lưu ý rằng TreeNode được định nghĩa là một lớp bên trong tĩnh trong BST. AVLTreeNode
sẽ được định nghĩa là một lớp bên trong tĩnh trong AVLTree. TreeNode chứa phần tử
trường dữ liệu , trái và phải, được kế thừa bởi AVLTreeNode. Do đó, AVLTreeNode chứa
bốn trường dữ liệu, như trong Hình 26.8.

nút: AVLTreeNode <E>

#element: E

#height: int

#left: TreeNode <E>

#right: TreeNode <E>

HÌNH 26.8 Một AVLTreeNode chứa phần tử trường dữ liệu được bảo vệ , chiều cao,
trái và phải.
Machine Translated by Google

970, chương 26, cây AVL

Trong lớp BST , phương thức createNewNode () tạo một đối tượng TreeNode . Phương thức này được ghi

đè trong lớp AVLTree để tạo một AVLTreeNode. Lưu ý rằng kiểu trả về của phương thức createNewNode ()

createNewNode () trong lớp BST là TreeNode, nhưng kiểu trả về của phương thức createNewNode () trong lớp AVLTree là
AVLTreeNode. Điều này là tốt, vì AVLTreeNode là một lớp con của TreeNode.

Tìm kiếm phần tử trong AVLTree giống như tìm kiếm trong cây nhị phân thông thường,
vì vậy phương thức tìm kiếm được định nghĩa trong lớp BST cũng hoạt động đối với AVLTree.

Các phương thức chèn và xóa được ghi đè để chèn và xóa một phần tử và mỗi

hình thành các hoạt động tái cân bằng nếu cần thiết để đảm bảo rằng cây được cân bằng.

26.4 Các trường dữ liệu trong lớp AVLTreeNode là gì?

Kiểm tra điểm 26.5 Đúng hay sai: AVLTreeNode là một lớp con của TreeNode?

26.6 Đúng hay sai: AVLTree là một lớp con của BST.

26.4 Ghi đè phương thức chèn


Chèn một phần tử vào cây AVL cũng giống như chèn nó vào BST, ngoại trừ việc cây có thể cần được
Chìa khóa

Điểm cân bằng lại.

Một phần tử mới luôn được chèn vào dưới dạng nút lá. Kết quả của việc thêm một nút mới, chiều cao của tổ

tiên của nút lá mới có thể tăng lên. Sau khi chèn một nút mới, hãy kiểm tra các nút dọc theo đường dẫn từ

nút lá mới lên đến gốc. Nếu tìm thấy một nút không cân bằng, hãy thực hiện một phép quay thích hợp bằng

cách sử dụng thuật toán trong Liệt kê 26.1.

DANH SÁCH 26.1 Các nút cân bằng trên một đường dẫn

1 balancePath (E e) {
lấy con đường 2 Lấy đường dẫn từ nút chứa phần tử e đến gốc, như minh họa trong Hình 26.9;
3
4 cho mỗi nút A trong đường dẫn đến gốc {
cập nhật chiều cao nút 5 Cập nhật chiều cao của A; Hãy
lấy nút cha 6 để parentOfA biểu thị cha mẹ của A,
7 là nút tiếp theo trong đường dẫn, hoặc null nếu A là nút gốc;
8

là cân bằng? switch (balanceFactor (A)) {


9 case -2: if balanceFactor (A.left) == -1 hoặc 0
Vòng quay LL 10 11 Thực hiện luân chuyển LL; // Xem Hình 26.2
12 khác
Vòng quay LR 13 Thực hiện xoay LR; // Xem Hình 26.4
14 nghỉ;
15 case +2: if balanceFactor (A.right) == +1 hoặc 0
Xoay vòng RR 16 Thực hiện quay RR; // Xem Hình 26.3
17 khác
Xoay vòng RL 18 Thực hiện quay RL; // Xem Hình 26.5
19 } // Kết thúc công tắc
20 } // Kết thúc cho
21} // Kết thúc phương thức

Thuật toán xem xét từng nút trong đường dẫn từ nút lá mới đến nút gốc. Cập nhật chiều cao của nút trên

đường dẫn. Nếu một nút được cân bằng, không cần thực hiện hành động nào. Nếu một nút không cân bằng, hãy

thực hiện một phép quay thích hợp.

26.7 Đối với cây AVL trong Hình 26.6a, hiển thị cây AVL mới sau khi thêm phần tử 40.

Kiểm tra điểm


Bạn thực hiện thao tác xoay nào để cân bằng lại cây? Nút nào không cân bằng?
Machine Translated by Google

26.5 Thực hiện các vòng quay 971

nguồn gốc

parentOfA

MỘT

Nút mới chứa phần tử e

HÌNH 26.9 Các nút dọc theo đường dẫn từ nút lá mới có thể bị mất cân bằng.

26.8 Đối với cây AVL trong Hình 26.6a, hiển thị cây AVL mới sau khi thêm phần tử 50. Bạn thực
hiện thao tác xoay nào để cân bằng lại cây? Nút nào không cân bằng?

26.9 Đối với cây AVL trong Hình 26.6a, hãy hiển thị cây AVL mới sau khi thêm phần tử 80. Bạn thực
hiện thao tác xoay nào để cân bằng lại cây? Nút nào không cân bằng?

26.10 Đối với cây AVL trong Hình 26.6a, hãy hiển thị cây AVL mới sau khi thêm phần tử 89. Bạn thực
hiện phép quay nào để cân bằng lại cây? Nút nào không cân bằng?

26.5 Thực hiện luân phiên


Một cây không cân bằng trở nên cân bằng bằng cách thực hiện một hoạt động quay thích hợp.
Chìa khóa

Điểm
Phần 26.2, Cây Cân bằng lại, minh họa cách thực hiện các phép quay tại một nút. Liệt kê 26.2 đưa
ra thuật toán cho phép quay LL, như được minh họa trong Hình 26.2.

LISTING 26.2 Thuật toán xoay LL


1 balanceLL (TreeNode A, TreeNode parentOfA) {
2 Gọi B là con trái của A. 3 con trái của A

4 nếu (A là gốc) kết nối lại phụ huynh của B


Gọi B là gốc mới
5 6 khác {
7 if (A là con bên trái của parentOfA)
Cho B là con bên trái của parentOfA;
khác
8 Cho B là con bên phải của parentOfA;
9 }
10 11 12

13 Đặt T2 trở thành cây con bên trái của A bằng cách gán B.right cho A.left; 14 Biến A di chuyển cây con

thành con của B bằng cách gán A cho B.right;


Kết thúc Cập
phương
nhật chiều
thức cao của nút A và nút B; 15 16} // điều chỉnh chiều cao

Lưu ý rằng chiều cao của các nút A và B có thể được thay đổi, nhưng chiều cao của các nút khác
trong cây không được thay đổi. Bạn có thể thực hiện các phép quay RR, LR và RL theo cách tương tự.
Machine Translated by Google

972, chương 26 cây AVL

26.6 Thực hiện phương pháp xóa


Xóa một phần tử khỏi cây AVL cũng giống như xóa nó khỏi BST, ngoại trừ việc cây có thể cần
Chìa khóa

Điểm được cân bằng lại.

Như đã thảo luận trong Phần 25.3, Xóa phần tử khỏi BST, để xóa một phần tử khỏi cây nhị phân,
trước tiên, thuật toán xác định vị trí nút có chứa phần tử. Cho phép hiện tại trỏ đến nút có chứa
phần tử trong cây nhị phân và cha trỏ tới nút cha của nút hiện tại . Nút hiện tại có thể là nút
con bên trái hoặc nút con bên phải của nút cha .
Hai trường hợp phát sinh khi xóa một phần tử.
Trường hợp 1: Nút hiện tại không có nút con bên trái, như hình 25.10a. Để xóa nút hiện tại ,
chỉ cần kết nối nút mẹ với nút con bên phải của hiện tại
như trong Hình 25.10b.
Chiều cao của các nút dọc theo đường dẫn từ nút mẹ đến nút gốc có thể đã giảm. Để đảm bảo rằng
cây được cân bằng, hãy gọi

balancePath (parent.element); // Được định nghĩa trong Liệt kê 26.1

Trường hợp 2: Nút hiện tại có nút con bên trái. Để rightMost trỏ đến nút có chứa phần tử lớn
nhất trong cây con bên trái của nút hiện tại và parentOfRightMost trỏ đến nút cha của nút
rightMost , như thể hiện trong Hình 25.12a. Nút rightMost không thể có nút con bên phải nhưng nó
có thể có nút con bên trái. Thay thế giá trị phần tử trong hiện tại
với nút trong nút RightMost , kết nối nút parentOfRightMost với nút con bên trái của nút rightMost ,
và xóa nút RightMost , như thể hiện trong Hình 25.12b.
Chiều cao của các nút dọc theo đường dẫn từ parentOfRightMost đến gốc có thể đã giảm. Để đảm
bảo rằng cây được cân bằng, hãy gọi

balancePath (parentOfRightMost); // Được định nghĩa trong Liệt kê 26.1

26.11 Đối với cây AVL trong Hình 26.6a, hãy hiển thị cây AVL mới sau khi xóa phần tử 107. Bạn thực

Kiểm tra điểm hiện thao tác xoay nào để cân bằng lại cây? Nút nào không cân bằng?

26.12 Đối với cây AVL trong Hình 26.6a, hãy hiển thị cây AVL mới sau khi xóa phần tử 60. Bạn thực
hiện thao tác xoay nào để cân bằng lại cây? Nút nào không cân bằng?

26.13 Đối với cây AVL trong Hình 26.6a, hãy hiển thị cây AVL mới sau khi xóa phần tử 55. Bạn đã
thực hiện thao tác xoay nào để cân bằng lại cây? Nút nào không cân bằng?

26.14 Đối với cây AVL trong Hình 26.6b, hiển thị cây AVL mới sau khi xóa các phần tử 67
và 87. Bạn đã thực hiện thao tác xoay nào để cân bằng lại cây? Nút nào không cân bằng?

26.7 Lớp AVLTree


Lớp AVLTree mở rộng lớp BST để ghi đè chèn và xóa
Chìa khóa

Điểm các phương pháp tái tạo cân bằng cho cây nếu cần thiết.

Liệt kê 26.3 cung cấp mã nguồn hoàn chỉnh cho lớp AVLTree .

LISTING 26.3 AVLTree.java


1 lớp công khai AVLTree <E mở rộng có thể so sánh được <E>> mở rộng BST <E> {
2 / ** Tạo một cây AVL trống * /
phương thức khởi tạo no-arg 3 công cộng AVLTree () {
Machine Translated by Google

26.7 Lớp AVLTree 973

4 }
5
6 / ** Tạo cây AVL từ một mảng các đối tượng * /
công cộng AVLTree (E [] đối tượng) { constructor
7 siêu (vật thể);
8 }
9 10

11 @Override / ** Ghi đè createNewNode để tạo AVLTreeNode * /


12 AVLTreeNode được bảo vệ <E> createNewNode (E e) { tạo nút cây AVL
13 trả về AVLTreeNode mới <E> (e);
14 }
15
16 @Override / ** Chèn một phần tử và cân bằng lại nếu cần * /
17 public boolean insert (E e) { ghi đè chèn
18 boolean thành công = super.insert (e);
19 nếu (! thành công)
20 trả về sai; // e đã ở trên cây rồi
21 khác {
22 balancePath (e); // Cân bằng từ e sang gốc nếu cần cây thăng bằng

23 }
24
25 trả về true; // e được chèn
26 }
27
28 / ** Cập nhật chiều cao của một nút được chỉ định * /
29 private void updateHeight (nút AVLTreeNode <E>) { cập nhật chiều cao nút
30 if (node.left == null && node.right == null) // nút là một lá
31 node.height = 0;
32 else if (node.left == null) // nút không có cây con bên trái
33 node.height = 1 + ((AVLTreeNode <E>) (node.right)). height;
34 else if (node.right == null) // nút không có cây con bên phải
35 node.height = 1 + ((AVLTreeNode <E>) (node.left)). height;
36 khác
37 node.height = 1 +
38 Math.max (((AVLTreeNode <E>) (node.right)). Height,
39 ((AVLTreeNode <E>) (node.left)). Height);
40 }
41

42 / ** Cân bằng các nút trong đường dẫn từ chỉ định


43 * nút gốc nếu cần thiết
44 * /
45 private void balancePath (E e) { nút cân bằng

46 java.util.ArrayList <TreeNode <E>> path = path (e); for (int i có được con đường

47 = path.size () - 1; i> = 0; i—–) {


48 AVLTreeNode <E> A = (AVLTreeNode <E>) (path.get (i)); updateHeight xem xét một nút
49 (A); AVLTreeNode <E> parentOfA = (A == root)? null : (AVLTreeNode cập nhật chiều cao

50 <E>) (path.get (i - 1)); có được chiều cao

51
52
53 switch (balanceFactor (A)) {
54 trường hợp -2: trái nặng
55 if (balanceFactor ((AVLTreeNode <E>) A.left) <= 0) {
56 balanceLL (A, parentOfA); // Thực hiện xoay LL Vòng quay LL
57 }
58 khác {
59 balanceLR (A, parentOfA); // Thực hiện xoay LR Vòng quay LR
60 }
61 nghỉ;
62 trường hợp +2: nặng nề
Machine Translated by Google

974, chương 26, cây AVL

63 if (balanceFactor ((AVLTreeNode <E>) A.right)> = 0) {


Xoay vòng RR 64 balanceRR (A, parentOfA); // Thực hiện xoay RR
65 }
66 khác {
Xoay vòng RL 67 balanceRL (A, parentOfA); // Thực hiện xoay RL
68 }
69 }
70 }
71 }
72
73 / ** Trả về hệ số cân bằng của nút * /
lấy hệ số cân bằng 74 private int balanceFactor (nút AVLTreeNode <E>) {
75 if (node.right == null) // nút không có cây con bên phải
76 return -node.height;
77 else if (node.left == null) // nút không có cây con bên trái
78 return + node.height;
79 khác
80 return ((AVLTreeNode <E>) node.right) .height -
81 ((AVLTreeNode <E>) node.left) .height;
82 }
83
84 / ** Số dư LL (xem Hình 26.2) * /
Vòng quay LL 85 private void balanceLL (TreeNode <E> A, TreeNode <E> parentOfA) {
86 TreeNode <E> B = A.left; // A là trái nặng và B là trái nặng
87
88 if (A == root) {
89 gốc = B;
90 }
91 khác {
92 if (parentOfA.left == A) {
93 parentOfA.left = B;
94 }
95 khác {
96 parentOfA.right = B;
97 }
98 }
99
100 A.left = B.right; // Đặt T2 trở thành cây con bên trái của A
101 B.phải = A; // Đặt A là con bên trái của B
cập nhật chiều cao 102 updateHeight ((AVLTreeNode <E>) A);
103 updateHeight ((AVLTreeNode <E>) B);
104 }
105
106 / ** Cân bằng LR (xem Hình 26.4) * /
Vòng quay LR 107 private void balanceLR (TreeNode <E> A, TreeNode <E> parentOfA) {
108 TreeNode <E> B = A.left; // A là trái nặng
109 TreeNode <E> C = B.right; // B là dấu nặng bên phải
110
111 if (A == root) {
112 gốc = C;
113 }
114 khác {
115 if (parentOfA.left == A) {
116 parentOfA.left = C;
117 }
118 khác {
119 parentOfA.right = C;
120 }
Machine Translated by Google

26.7 AVLTree Lớp 975

121 }
122
123 A.left = C.right; // Biến T3 thành cây con bên trái của A
124 B.right = C.left; // Biến T2 thành cây con bên phải của B
125 C.left = B;
126 C.right = A;
127
128 // Điều chỉnh độ cao
129 updateHeight ((AVLTreeNode <E>) A); cập nhật chiều cao

130 updateHeight ((AVLTreeNode <E>) B);


131 updateHeight ((AVLTreeNode <E>) C);
132 }
133
134 / ** Cân bằng RR (xem Hình 26.3) * /
135 private void balanceRR (TreeNode <E> A, TreeNode <E> parentOfA) { Xoay vòng RR
136 TreeNode <E> B = A.right; // A là dòng bên phải và B là dòng bên phải
137
138 if (A == root) {
139 gốc = B;
140 }
141 khác {
142 if (parentOfA.left == A) {
143 parentOfA.left = B;
144 }
145 khác {
146 parentOfA.right = B;
147 }
148 }
149
150 A.right = B.left; // Biến T2 thành cây con bên phải của A
151 B.left = A;
152 updateHeight ((AVLTreeNode <E>) A); cập nhật chiều cao

153 updateHeight ((AVLTreeNode <E>) B);


154 }
155
156 / ** Cân bằng RL (xem Hình 26.5) * /
157 private void balanceRL (TreeNode <E> A, TreeNode <E> parentOfA) { Xoay vòng RL
158 TreeNode <E> B = A.right; // A là dấu nặng bên phải
159 TreeNode <E> C = B.left; // B là trái nặng
160
161 if (A == root) {
162 gốc = C;
163 }
164 khác {
165 if (parentOfA.left == A) {
166 parentOfA.left = C;
167 }
168 khác {
169 parentOfA.right = C;
170 }
171 }
172
173 A.right = C.left; // Biến T2 thành cây con bên phải của A
174 B.left = C.right; // Đặt T3 thành cây con bên trái của B
175 C.left = A;
176 C.right = B;
177
178 // Điều chỉnh độ cao
Machine Translated by Google

976, chương 26, cây AVL

cập nhật chiều cao 179 updateHeight ((AVLTreeNode <E>) A);


180 updateHeight ((AVLTreeNode <E>) B);
181 updateHeight ((AVLTreeNode <E>) C);
182 }
183
184 @Override / ** Xóa một phần tử khỏi cây AVL.
185 * Trả về true nếu phần tử được xóa thành công
186 * Trả về false nếu phần tử không có trong cây * /
ghi đè xóa 187 xóa boolean công khai (phần tử E) {
188 if (root == null)
189 trả về sai; // Phần tử không có trong cây
190
191 // Định vị nút sẽ bị xóa và cũng xác định vị trí nút cha của nó
192 TreeNode <E> parent = null;
193 TreeNode <E> current = root;
194 while (current! = null) {
195 if (element.compareTo (current.element) < 0) {
196 cha mẹ = hiện tại;
197 current = current.left;
198 }
199 else if (element.compareTo (current.element)> 0) {
200 cha mẹ = hiện tại;
201 current = current.right;
202 }
203 khác
204 nghỉ; // Phần tử nằm trong cây được trỏ bởi hiện tại
205 }
206
207 if (hiện tại == null)
208 trả về sai; // Phần tử không có trong cây
209
210 // Trường hợp 1: dòng điện không có con bên trái (Xem hình 25.10)
211 if (current.left == null) {
212 // Kết nối nút cha với nút con bên phải của nút hiện tại
213 if (cha == null) {
214 root = current.right;
215 }
216 khác {
217 if (element.compareTo (parent.element) < 0)
218 parent.left = current.right;
219 khác
220 parent.right = current.right;
221
222 // Cân bằng cây nếu cần
nút cân bằng 223 balancePath (parent.element);
224 }
225 }
226 khác {
227 // Trường hợp 2: Nút hiện tại có nút con bên trái
228 // Định vị nút ngoài cùng bên phải trong cây con bên trái của
229 // nút hiện tại và cũng là nút cha của nó
230 TreeNode <E> parentOfRightMost = hiện tại;
231 TreeNode <E> rightMost = current.left;
232
233 while (rightMost.right! = null) {
234 parentOfRightMost = rightMost;
235 rightMost = rightMost.right; // Tiếp tục sang phải
236 }
237
Machine Translated by Google

26.7 Lớp AVLTree 977

238 // Thay thế phần tử hiện tại bằng phần tử trong rightMost
239 current.element = rightMost.element;
240
// Loại bỏ nút ngoài cùng bên phải
if (parentOfRightMost.right == rightMost)
parentOfRightMost.right = rightMost.left;
khác
// Trường hợp đặc biệt: parentOfRightMost là hiện tại
parentOfRightMost.left = rightMost.left;
241242 243 244 245 246 247
248 // Cân bằng cây nếu cần
249 balancePath (parentOfRightMost.element); nút cân bằng

250 }
251
252 kích cỡ--;
253 trả về true; // Phần tử được chèn
254 }
255
256 / ** AVLTreeNode là TreeNode cộng với chiều cao * /
257 lớp tĩnh được bảo vệ AVLTreeNode <E mở rộng có thể so sánh được <E>> lớp AVLTreeNode bên trong
258 mở rộng BST.TreeNode <E> {
259 bảo vệ int height = 0; // Trường dữ liệu mới chiều cao nút
260
261 AVLTreeNode công cộng (E e) {
262 siêu (e);
263 }
264 }
265}

Lớp AVLTree mở rộng BST. Giống như lớp BST , lớp AVLTree có một phương thức khởi tạo no-arg tạo
người xây dựng
ra một AVLTree trống (dòng 3–4) và một phương thức khởi tạo tạo một AVLTree ban đầu từ một mảng các
phần tử (dòng 7–9).
Phương thức createNewNode () được định nghĩa trong lớp BST tạo ra một TreeNode. Phương thức

này được ghi đè để trả về một AVLTreeNode (dòng 12–14).


Phương thức chèn trong AVLTree được ghi đè trong các dòng 17–26. Đầu tiên, phương thức này gọi chèn
phương thức chèn trong BST, sau đó gọi đường dẫn cân bằng (e) (dòng 22) để đảm bảo rằng cây được
cân bằng.

Đầu tiên, phương thức balancePath lấy các nút trên đường dẫn từ nút chứa phần tử e đến gốc balancePath
(dòng 46). Đối với mỗi nút trong đường dẫn, hãy cập nhật chiều cao của nó (dòng 49), kiểm tra hệ số
cân bằng của nó (dòng 53) và thực hiện các phép quay thích hợp nếu cần (dòng 53–69).
Bốn phương pháp để thực hiện các phép quay được định nghĩa trong các dòng 85–182. Mỗi phương sự quay

thức được gọi với hai đối số TreeNode — A và parentOfA — để thực hiện một vòng quay thích hợp tại
nút A. Cách mỗi vòng quay được thực hiện được minh họa trong Hình 26.2–26.5. Sau khi xoay, độ cao
của các nút A, B và C được cập nhật (dòng 102, 129, 152, 179).
Phương thức xóa trong AVLTree được ghi đè trong các dòng 187–264. Phương pháp giống nhau xóa bỏ
như cái được triển khai trong lớp BST , ngoại trừ việc bạn phải cân bằng lại các nút sau khi xóa
trong hai trường hợp (dòng 224, 249).

26.15 Tại sao phương thức createNewNode được định nghĩa được bảo vệ?

26.16 Phương thức updateHeight được gọi khi nào? Khi nào phương thức balanceFactor được gọi? Khi Kiểm tra điểm

nào phương thức balancePath được gọi?

26.17 Các trường dữ liệu trong lớp AVLTree là gì?

26.18 Trong phương pháp chèn và xóa , khi bạn đã thực hiện xoay để cân bằng
một nút trong cây, có thể vẫn còn các nút không cân bằng?
Machine Translated by Google

978, chương 26 cây AVL

26.8 Kiểm tra Lớp AVLTree


Phần này đưa ra một ví dụ về việc sử dụng lớp AVLTree .
Chìa khóa

Điểm
Liệt kê 26.4 đưa ra một chương trình thử nghiệm. Chương trình tạo một AVLTree được khởi tạo bằng một

mảng các số nguyên 25, 20 và 5 (dòng 4–5), chèn các phần tử trong các dòng 9–18 và xóa các phần tử trong
các dòng 22–28. Vì AVLTree là một lớp con của BST và các phần tử trong BST có thể lặp lại, chương trình

sử dụng một vòng lặp foreach để duyệt qua tất cả các phần tử trong dòng 33–35.

DANH SÁCH 26.4 TestAVLTree.java


1 lớp công khai TestAVLTree {
2 public static void main (String [] args) {
3 // Tạo cây AVL
tạo một AVLTree 4
AVLTree <Integer> tree = new AVLTree <> (new Integer [] {25,
5 20, 5});
6 System.out.print ("Sau khi chèn 25, 20, 5:");
7 printTree (cây);
8

chèn 34 cây.insert (34);


chèn 50 9 cây.insert (50);
10 11 System.out.print ("\ nSau khi chèn 34, 50:");
12 printTree (cây);
13
chèn 30 14 cây.insert (30);
15 System.out.print ("\ nSau khi chèn 30");
16 printTree (cây);
17
chèn 10 18 cây.insert (10);
19 System.out.print ("\ nSau khi chèn 10");
20 printTree (cây);
21
xóa 34 22 cây.delete (34);
xóa 30 23 cây. xóa (30);
xóa 50 24 cây. xóa (50);
25 System.out.print ("\ nSau khi xóa 34, 30, 50:");
26 printTree (cây);
27

xóa 5 28 cây.delete (5);


29 System.out.print ("\ nSau khi xóa 5:");
30 printTree (cây);
31
32 System.out.print ("\ nSo sánh các phần tử trong cây: ");
cho mỗi vòng lặp 33 for (int e: tree) {
34 System.out.print (e + " ");
35 }
36 }
37
38 public static void printTree (BST tree) {
39 // Traverse cây
40 System.out.print ("\ nInorder (đã sắp xếp): ");
41 tree.inorder ();
41 System.out.print ("\ nPostorder: ");
43 tree.postorder ();
44 System.out.print ("\ nĐặt hàng: ");
45 tree.preorder ();
46 System.out.print ("\ nSố nút là" + tree.getSize ());
Machine Translated by Google

26.8 Kiểm tra AVLTree Class 979

47 System.out.println ();
}
48 49}

Sau khi chèn 25, 20, 5:


Inorder (đã sắp xếp): 5 20 25
Đặt hàng sau: 5 25 20
Đặt hàng trước: 20 5 25
Số nút là 3

Sau khi chèn 34, 50:


Inorder (đã sắp xếp): 5 20 25 34 50
Đặt hàng sau: 5 25 50 34 20
Đặt hàng trước: 20 5 34 25 50
Số nút là 5

Sau khi chèn 30


Inorder (đã sắp xếp): 5 20 25 30 34 50
Đặt hàng sau: 5 20 30 50 34 25
Đặt hàng trước: 25 20 5 34 30 50
Số nút là 6

Sau khi chèn 10


Inorder (đã sắp xếp): 5 10 20 25 30 34 50
Đặt hàng sau: 5 20 10 30 50 34 25
Đặt hàng trước: 25 10 5 20 34 30 50
Số nút là 7

Sau khi loại bỏ 34, 30, 50:


Inorder (đã sắp xếp): 5 10 20 25
Đặt hàng sau: 5 20 25 10
Đặt hàng trước: 10 5 25 20
Số nút là 4

Sau khi loại bỏ 5:


Inorder (đã sắp xếp): 10 20 25
Đặt hàng sau: 10 25 20
Đặt hàng trước: 20 10 25
Số nút là 3
Duyệt các phần tử trong cây: 10 20 25

Hình 26.10 cho thấy cây phát triển như thế nào khi các phần tử được thêm vào cây. Sau 25 và 20
được thêm vào, cây như trong Hình 26.10a. 5 được chèn vào như một con bên trái của 20, như
trong Hình 26.10b. Cây không cân đối. Nó là trái nặng tại nút 25. Thực hiện một phép quay LL để
tạo ra một cây AVL, như thể hiện trong Hình 26.10c.
Sau khi chèn 34, cây được hiển thị trong hình 26.10d. Sau khi chèn 50, cây như hình 26.10e.
Cây không cân đối. Nó nặng phải ở nút 25. Thực hiện một vòng quay RR để tạo ra một cây AVL, như
thể hiện trong Hình 26.10f.
Sau khi chèn 30, cây như hình 26.10g. Cây không cân đối. Biểu diễn
một vòng quay RL để tạo ra một cây AVL, như thể hiện trong Hình 26.10h.
Sau khi chèn 10, cây như hình 26.10i. Cây không cân đối. Biểu diễn
một phép quay LR để tạo ra một cây AVL, như trong Hình 26.10j.
Machine Translated by Google

980, chương 26, cây AVL

25 Cần xoay LL 25 20 20
tại nút 25

20 20 5 25 5 25

5 34

(a) Chèn 25, 20 (b) Chèn 5 (c) Cân bằng (d) Chèn 34

20 20 20
Cần xoay vòng RR Xoay vòng RL lúc
tại nút 25 nút 20

5 25 5 34 5 34

34 25 50 25 50

50 30

(e) Chèn 50 (f) Cân bằng (g) Chèn 30

25 25 25
LR xoay lúc
nút 20

20 34 20 34 10 34

5 30 50 5 30 50 5 20 30 50

10

(h) Cân bằng (i) Chèn 10 (j) Cân bằng

HÌNH 26.10 Cây phát triển khi các phần tử mới được chèn vào.

Hình 26.11 cho thấy cây phát triển như thế nào khi các phần tử bị xóa. Sau khi xóa 34, 30 và 50, cây như

hình 26.11b. Cây không cân đối. Thực hiện một phép quay LL để tạo ra một cây AVL, như trong Hình 26.11c.

Sau khi xóa 5, cây như hình 26.11d. Cây không cân đối. Biểu diễn

một vòng quay RL để tạo ra một cây AVL, như thể hiện trong Hình 26.11e.

26.19 Hiển thị sự thay đổi của cây AVL khi chèn 1, 2, 3, 4, 10, 9, 7, 5, 8, 6 vào
Kiểm tra điểm
cây, theo thứ tự này.

26.20 Đối với cây được xây dựng ở câu hỏi trước, hãy chỉ ra sự thay đổi của nó sau 1, 2, 3, 4, 10, 9, 7,
5, 8, 6 bị xóa khỏi cây theo thứ tự này.

26.21 Bạn có thể duyệt qua các phần tử trong cây AVL bằng vòng lặp foreach không?
Machine Translated by Google

26,9 Phân tích độ phức tạp thời gian cây AVL 981

25 25 10

Vòng quay LL

10 34 10 tại nút 25 5 25

5 20 30 50 5 20 20

(a) Xóa 34, 30, 50 (b) Sau 34, 30, 50 bị xóa (c) Cân bằng

10 20

Vòng quay RL ở 10
25 10 25

20

(d) Sau khi 5 bị xóa (e) Cân bằng

HÌNH 26.11 Cây phát triển khi các phần tử bị xóa khỏi cây.

26.9 Phân tích độ phức tạp thời gian cây AVL


Vì chiều cao của cây AVL là O (log n), độ phức tạp về thời gian của các phương thức tìm

kiếm, chèn và xóa trong AVLTree là O (log n).

Độ phức tạp về thời gian của các phương pháp tìm kiếm, chèn và xóa trong AVLTree phụ thuộc vào chiều cao

của cây. Ta có thể chứng minh rằng chiều cao của cây là O (log n).

Gọi G (h) biểu thị số lượng nút tối thiểu trong cây AVL có chiều cao h. Obvi ously, G (1) là 1 và G

(2) là 2. Số nút tối thiểu trong cây AVL có chiều cao h Ú 3 phải có hai cây con nhỏ nhất: một cây có chiều

cao h - 1 và cây còn lại có chiều cao h - 2. Như vậy,

G (h) = G (h - 1) + G (h - 2) + 1

Nhớ lại rằng số Fibonacci ở chỉ số i có thể được mô tả bằng quan hệ lặp lại F (i) = F (i - 1) + F (i -

2). Do đó, hàm G (h) về cơ bản giống với F (i). Có thể chứng minh rằng

h 6 1.4405 log (n + 2) - 1.3277

với n là số nút trong cây. Do đó, chiều cao của cây AVL là O (log n).

Các phương pháp tìm kiếm, chèn và xóa chỉ liên quan đến các nút dọc theo một đường dẫn trong cây.
Các phương thức updateHeight và balanceFactor được thực thi trong một thời gian không đổi cho mỗi nút

trong đường dẫn. Phương thức balancePath được thực thi trong một khoảng thời gian không đổi cho một nút

trong đường dẫn. Do đó, độ phức tạp về thời gian cho các phương pháp tìm kiếm, chèn và xóa là O (log n).

26.22 Chiều cao tối đa / tối thiểu cho cây AVL gồm 3 nút, 5 nút và 7 là bao nhiêu
điểm giao?

26.23 Nếu cây AVL có chiều cao là 3 thì cây có thể có số nút tối đa là bao nhiêu?
Cây có thể có số nút tối thiểu là bao nhiêu?

26.24 Nếu cây AVL có chiều cao là 4 thì cây có thể có số nút tối đa là bao nhiêu?
Cây có thể có số nút tối thiểu là bao nhiêu?
Machine Translated by Google

982, chương 26, cây AVL

ĐIỀU KHOẢN CHÍNH

Cây AVL 966 nặng bên phải 966


hệ số cân bằng 966 Vòng quay RL 967

nặng bên trái 966 vòng quay 966


LL xoay 967 Vòng quay RR 967
Vòng quay LR 967 cây cân đối 966

cây cân bằng hoàn hảo 966

TÓM TẮT CHƯƠNG

1. Cây AVL là một cây nhị phân cân bằng tốt. Trong cây AVL, sự khác biệt giữa
chiều cao của hai cây con cho mọi nút là 0 hoặc 1.

2. Quá trình chèn hoặc xóa một phần tử trong cây AVL giống như trong cây tìm kiếm
nhị phân regu lar. Sự khác biệt là bạn có thể phải cân bằng lại cây sau khi thực
hiện thao tác chèn hoặc xóa.

3. Sự mất cân bằng trong cây gây ra bởi sự chèn và xóa được cân bằng lại thông qua phép
quay cây con tại nút của sự mất cân bằng.

4. Quá trình tái cân bằng một nút được gọi là quá trình quay. Có thể có bốn cách
xoay: xoay LL, xoay LR, xoay RR và xoay RL.

5. Chiều cao của cây AVL là O (log n). Do đó, sự phức tạp về thời gian cho việc tìm kiếm,

phương thức chèn và xóa là O (log n).

ĐỐ

Trả lời câu hỏi cho chương này trực tuyến tại www.cs.armstrong.edu/liang/intro10e/quiz.html.

BÀI TẬP LẬP TRÌNH

* 26.1 (Hiển thị cây AVL bằng đồ thị) Viết chương trình hiển thị cây AVL dọc theo
với hệ số cân bằng của nó cho mỗi nút.

26.2 (So sánh hiệu suất) Viết chương trình thử nghiệm tạo ngẫu nhiên 500.000 số và chèn chúng
vào một BST, cải tổ lại 500.000 số và mỗi lần tạo thành một tìm kiếm, sắp xếp lại các
số trước khi xóa chúng khỏi cây. Viết một chương trình thử nghiệm khác làm điều
tương tự cho AVLTree.
So sánh thời gian thực hiện của hai chương trình này.

*** 26.3 (Hoạt ảnh cây AVL) Viết chương trình tạo hoạt ảnh cho các phương pháp chèn, xóa và tìm
kiếm trên cây AVL , như thể hiện trong Hình 26.1.

** 26.4 (Tham chiếu chính cho BST) Giả sử rằng lớp TreeNode được định nghĩa trong BST có tham
chiếu đến cha của nút, như được hiển thị trong Bài tập lập trình 25.15.
Triển khai lớp AVLTree để hỗ trợ thay đổi này. Viết chương trình kiểm tra cộng
các số 1, 2 ,. . . , 100 đến cây và hiển thị các đường dẫn cho tất cả các nút lá.

** 26.5 (Phần tử nhỏ nhất thứ k) Bạn có thể tìm phần tử nhỏ nhất thứ k trong BST trong thời gian O

(n) từ một trình lặp nhỏ hơn. Đối với cây AVL, bạn có thể tìm thấy nó trong O (log n)
thời gian. Để đạt được điều này, hãy thêm một trường dữ liệu mới có tên là kích thước trong
AVLTreeNode để lưu trữ số lượng nút trong cây con bắt nguồn từ nút này. Lưu ý rằng kích thước của
Machine Translated by Google

Bài tập lập trình 983

một nút v lớn hơn tổng kích thước của hai nút con của nó. Hình 26.12 cho thấy một cây AVL và giá
trị kích thước cho mỗi nút trong cây.

25 kích thước: 6

20 kích thước: 2 34 kích thước: 3

5 kích thước: 1 30 kích thước: 1 50 kích thước: 1

HÌNH 26.12 Trường dữ liệu kích thước trong AVLTreeNode lưu trữ số lượng nút trong cây con bắt nguồn từ

nút.

Trong lớp AVLTree , hãy thêm phương thức sau để trả về độ cao nhỏ nhất thứ k trong cây.

public E find (int k)

Phương thức trả về null nếu k <1 hoặc k> kích thước của cây. Phương thức này có thể được thực

hiện bằng cách sử dụng phương thức đệ quy find (k, root), trả về phần tử nhỏ nhất thứ k trong

cây với gốc được chỉ định. Gọi A và B lần lượt là con trái và con phải của căn. Giả sử rằng cây

không trống và k… root.size, find (k, root) có thể được định nghĩa đệ quy như sau:

root.element, nếu A là null và k là 1;

B.element, nếu A là rỗng và k là 2;

find (k, root) = find (k, A), nếu k 6 = A.size;

root.element, nếu k = A.size + 1;

find (k - A.size - 1, B), nếu k 7 A.size + 1;

Sửa đổi phương thức chèn và xóa trong AVLTree để đặt giá trị chính xác cho thuộc tính kích thước

trong mỗi nút. Phương thức chèn và xóa sẽ vẫn trong thời gian O (log n). Phương thức find (k)

có thể được thực hiện trong thời gian O (log n).

Do đó, bạn có thể tìm phần tử nhỏ nhất thứ k trong cây AVL trong thời gian O (log n).

Sử dụng phương pháp chính sau để kiểm tra chương trình của bạn:

nhập java.util.Scanner;

lớp công cộng Bài tập26_05 {


public static void main (String [] args) {
AVLTree <Đôi> cây = mới AVLTree <> ();
Đầu vào máy quét = Máy quét mới (System.in);
System.out.print ("Nhập 15 số: ");
for (int i = 0; i < 15; i ++) {
tree.insert (input.nextDouble ());
}

System.out.print ("Nhập k: ");


System.out.println ("Số nhỏ nhất thứ " + k + "là" tree.find (k)); +

}
}

** 26.6 (Thử nghiệm AVLTree) Thiết kế và viết một chương trình thử nghiệm hoàn chỉnh để kiểm tra xem AVLTree

lớp trong Liệt kê 26.4 đáp ứng tất cả các yêu cầu.
Machine Translated by Google

Trang này cố ý để trống


Machine Translated by Google

CHƯƠNG

27
GIẶT

Mục tiêu
■ Để hiểu băm là gì và băm được sử dụng để làm gì (§27.2).

■ Để lấy mã băm cho một đối tượng và thiết kế hàm băm để ánh xạ khóa tới
một chỉ mục (§27.3).

■ Để xử lý va chạm bằng cách sử dụng địa chỉ mở (§27.4).

■ Để biết sự khác biệt giữa thăm dò tuyến tính, thăm dò bậc hai và
băm kép (§27.4).

■ Để xử lý va chạm bằng cách sử dụng dây xích riêng biệt (§27.5).

■ Để hiểu hệ số tải và nhu cầu băm lại (§27.6).

■ Để triển khai MyHashMap bằng cách sử dụng băm (§27.7).

■ Để triển khai MyHashSet bằng cách sử dụng hàm băm (§27.8).


Machine Translated by Google

986 chương 27 băm

27.1 Giới thiệu


Băm là siêu hiệu quả. Phải mất O (1) thời gian để tìm kiếm, chèn và xóa một phần tử bằng cách sử
Chìa khóa

Điểm dụng hàm băm.

Chương trước đã giới thiệu cây tìm kiếm nhị phân. Một phần tử có thể được tìm thấy trong O (log n)

thời gian trong một cây tìm kiếm được cân bằng tốt. Có cách nào hiệu quả hơn để tìm kiếm một phần tử trong

tại sao băm? vùng chứa không? Chương này giới thiệu một kỹ thuật gọi là băm. Bạn có thể sử dụng hàm băm để đưa vào một

bản đồ hoặc một tập hợp để tìm kiếm, chèn và xóa một phần tử trong thời gian O (1).

27.2 Băm là gì?


Hashing sử dụng một hàm băm để ánh xạ một khóa đến một chỉ mục.
Chìa khóa

Điểm
Trước khi giới thiệu hàm băm, chúng ta hãy xem lại bản đồ, đây là một cấu trúc dữ liệu được triển khai

bản đồ bằng cách sử dụng hàm băm. Nhớ lại rằng một bản đồ (được giới thiệu trong Phần 21.5) là một đối tượng chứa

Chìa khóa
để lưu trữ các mục nhập. Mỗi mục nhập chứa hai phần: khóa và giá trị. Khóa, còn được gọi là khóa tìm kiếm,

giá trị được sử dụng để tìm kiếm giá trị tương ứng. Ví dụ, một từ điển có thể được lưu trữ trong một bản đồ,

trong đó các từ là các khóa và các định nghĩa của các từ là các giá trị.

Ghi chú

từ điển Bản đồ còn được gọi là từ điển, bảng băm hoặc mảng kết hợp.
bảng băm
Khung tập hợp Java xác định giao diện java.util.Map để lập mô hình bản đồ.
mảng kết hợp
Ba cách triển khai cụ thể là java.util.HashMap, java.util.LinkedHashMap và java.util.TreeMap.
java.util.HashMap được thực hiện bằng cách sử dụng băm, java.
use.LinkedHashMap sử dụng LinkedList và java.util.TreeMap sử dụng cây đỏ-đen. (Phần thưởng Chương 41 giới

thiệu các cây đỏ-đen.) Bạn sẽ tìm hiểu khái niệm băm và sử dụng nó để thực hiện một bản đồ băm trong chương

này.

Nếu bạn biết chỉ số của một phần tử trong mảng, bạn có thể truy xuất phần tử đó bằng cách sử dụng chỉ

mục trong thời gian O (1). Vậy điều đó có nghĩa là chúng ta có thể lưu trữ các giá trị trong một mảng và sử

dụng khóa làm chỉ số để tìm giá trị? Câu trả lời là có - nếu bạn có thể ánh xạ một khóa đến một chỉ mục.

bảng băm Mảng lưu trữ các giá trị được gọi là bảng băm. Hàm ánh xạ một khóa đến một chỉ mục trong bảng băm được

hàm băm gọi là hàm băm. Như trong Hình 27.1, một hàm băm lấy một chỉ mục từ một khóa và sử dụng chỉ mục để lấy giá

băm trị cho khóa. Hashing là một kỹ thuật lấy giá trị bằng cách sử dụng chỉ mục thu được từ khóa mà không cần

thực hiện tìm kiếm.

.
i = hash (key) Một mục nhập
tôi
giá trị cốt lõi

.
.
N - 1 .

Hàm băm

HÌNH 27.1 Một hàm băm ánh xạ một khóa đến một chỉ mục trong bảng băm.

Làm cách nào để bạn thiết kế một hàm băm tạo chỉ mục từ một khóa? Tốt nhất, chúng tôi muốn thiết kế một

hàm ánh xạ mỗi khóa tìm kiếm đến một chỉ mục khác nhau trong bảng băm. Một hàm như vậy được gọi là một hàm

hàm băm hoàn hảo băm hoàn hảo. Tuy nhiên, rất khó để tìm được một hàm băm hoàn hảo
Machine Translated by Google

27.3 Hàm băm và mã băm 987


chức năng. Khi hai hoặc nhiều khóa được ánh xạ đến cùng một giá trị băm, chúng tôi nói rằng một xung đột va chạm

đã xảy ra. Mặc dù có những cách để đối phó với va chạm, sẽ được thảo luận ở phần sau của chương này, nhưng tốt

hơn hết là bạn nên tránh va chạm ngay từ đầu. Vì vậy, bạn nên thiết kế một hàm băm nhanh và dễ tính toán để giảm

thiểu va chạm.

27.1 Hàm băm là gì? Hàm băm hoàn hảo là gì? Va chạm là gì?
Kiểm tra điểm

27.3 Hàm băm và mã băm


Một hàm băm điển hình trước tiên chuyển khóa tìm kiếm thành một giá trị nguyên được gọi là mã băm, sau
Chìa khóa

đó nén mã băm thành một chỉ mục vào bảng băm. Điểm

Đối tượng lớp gốc của Java có phương thức hashCode , phương thức này trả về một mã băm số nguyên. Theo mặc định, Mã Băm
phương thức trả về địa chỉ bộ nhớ cho đối tượng. Hợp đồng chung cho phương thức hashCode như sau: Mã Băm()

1. Bạn nên ghi đè phương thức hashCode bất cứ khi nào phương thức bằng bị ghi đè

để đảm bảo rằng hai đối tượng bằng nhau trả về cùng một mã băm.

2. Trong quá trình thực thi một chương trình, gọi phương thức hashCode nhiều lần

trả về cùng một số nguyên, miễn là dữ liệu của đối tượng không bị thay đổi.

3. Hai đối tượng không bằng nhau có thể có cùng mã băm, nhưng bạn nên triển khai

phương pháp hashCode để tránh quá nhiều trường hợp như vậy.

27.3.1 Mã băm cho các loại nguyên thủy


Đối với các khóa tìm kiếm kiểu byte, short, int và char, chỉ cần chuyển chúng thành int. Do đó, byte, short, int, char
hai khóa tìm kiếm khác nhau thuộc bất kỳ loại nào trong số này sẽ có mã băm khác nhau.
Đối với khóa tìm kiếm thuộc loại float, hãy sử dụng Float.floatToIntBits (key) làm mã băm. Lưu ý rằng trôi nổi

floatToIntBits (float f) trả về một giá trị int có biểu diễn bit giống như biểu diễn bit cho số thực f. Do đó, hai

khóa tìm kiếm khác nhau của kiểu float sẽ có mã băm khác nhau.

Đối với một khóa tìm kiếm thuộc loại dài, chỉ cần chuyển nó thành int sẽ không phải là một lựa chọn tốt, bởi vì Dài
tất cả các khóa chỉ khác nhau ở 32 bit đầu tiên sẽ có cùng một mã băm. Để xem xét 32 bit đầu tiên, hãy chia 64 bit

thành hai nửa và thực hiện phép toán loại trừ hoặc kết hợp hai nửa. Quá trình này được gọi là gấp. Mã băm cho một

khóa dài là gấp

int hashCode = (int) (key ^ (key >> 32));

Lưu ý rằng >> là toán tử dịch phải dịch chuyển các bit 32 vị trí sang phải. Đối với bài kiểm tra, 1010110 >> 2 cho

kết quả là 0010101. ^ là toán tử-hoặc độc quyền bit. Nó hoạt động trên hai bit tương ứng của các toán hạng nhị

phân. Ví dụ: 1010110 ^ 0110111 cho kết quả là 1100001. Để biết thêm về hoạt động theo chiều bit, hãy xem Phụ lục G,

Hoạt động theo chiều bit.

Đối với khóa tìm kiếm thuộc loại double, trước tiên hãy chuyển đổi nó thành giá trị dài bằng cách sử dụng gấp đôi

Phương thức Double.doubleToLongBits , sau đó thực hiện gấp như sau: gấp

bit dài = Double.doubleToLongBits (khóa);


int hashCode = (int) (bits ^ (bits >> 32));

27.3.2 Mã băm cho chuỗi


Các phím tìm kiếm thường là chuỗi, vì vậy điều quan trọng là phải thiết kế một hàm băm tốt cho chuỗi. Một cách tiếp

cận trực quan là tính tổng Unicode của tất cả các ký tự làm mã băm cho chuỗi. Cách tiếp cận này có thể hoạt động nếu

hai phím tìm kiếm trong một ứng dụng không chứa các chữ cái giống nhau, nhưng
Machine Translated by Google

988 chương 27 băm

nó sẽ tạo ra nhiều va chạm nếu các phím tìm kiếm chứa các chữ cái giống nhau, chẳng hạn như tod
và dot.

Một cách tiếp cận tốt hơn là tạo một mã băm có vị trí của các ký tự thành con
cạnh bên. Cụ thể, hãy để mã băm là

s0 * b (n - 1) + s1 * b (n - 2) + C
+ sn-1

trong đó si là s.charAt (i). Biểu thức này là một đa thức đối với một số b dương, vì vậy nó
mã băm đa thức được gọi là mã băm đa thức. Sử dụng quy tắc Horner để đánh giá đa thức (xem Phần 6.7), mã
băm có thể được tính toán một cách hiệu quả như sau:

(c ((s0 * b + s1 ) b + s2 ) b + C
+ sn-2 ) b + sn-1

Việc tính toán này có thể gây ra tràn cho các chuỗi dài, nhưng tràn số học bị bỏ qua
trong Java. Bạn nên chọn một giá trị b thích hợp để giảm thiểu va chạm. Các thử nghiệm
cho thấy rằng các lựa chọn tốt cho b là 31, 33, 37, 39 và 41. Trong lớp Chuỗi , Mã băm
được sử dụng nhiều hơn khi sử dụng mã băm đa thức với b là 31.

27.3.3 Nén mã băm


Mã băm cho một khóa có thể là một số nguyên lớn nằm ngoài phạm vi của chỉ mục bảng băm, vì
vậy bạn cần phải thu nhỏ lại để vừa với phạm vi của chỉ mục. Giả sử chỉ số cho bảng băm nằm
trong khoảng từ 0 đến N-1. Cách phổ biến nhất để chia tỷ lệ số nguyên thành từ 0 đến N-1 là sử dụng

h (hashCode) = hashCode% N

Để đảm bảo rằng các chỉ số trải đều, chọn N là số nguyên tố lớn hơn 2.
Tốt nhất là bạn nên chọn số nguyên tố N. Tuy nhiên để tìm được số nguyên tố lớn thì
mất nhiều thời gian. Trong triển khai Java API cho java.util.HashMap, N được đặt thành
giá trị lũy thừa là 2. Có một lý do chính đáng cho sự lựa chọn này. Khi N là giá trị lũy
thừa của 2,

h (hashCode) = hashCode% N

giống như

h (hashCode) = hashCode & (N - 1)

Ký hiệu và &, là toán tử AND theo chiều bit (xem Phụ lục G, Các phép toán theo chiều
bit). AND của hai bit tương ứng mang lại giá trị 1 nếu cả hai bit đều là 1. Ví dụ, giả
sử N = 4 và hashCode = 11, 11% 4 = 3, giống như 01011 & 00011 = 11. Có thể thực hiện
toán tử & nhanh hơn nhiều so với toán tử % .
Để đảm bảo rằng hàm băm được phân phối đồng đều, một hàm băm bổ sung cũng được sử
dụng cùng với hàm băm chính trong việc triển khai java.util.HashMap. Chức năng này được
định nghĩa là:

private static int SupplementalHash (int h) {


h ^ = (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

^ và >>> là các phép toán dịch sang phải loại trừ hoặc không dấu theo bitwise (cũng được
giới thiệu trong Phụ lục G). Các phép toán bit nhanh hơn nhiều so với các phép toán
nhân, chia và dư. Bạn nên thay thế các hoạt động này bằng các hoạt động bitwise khi có
thể.
Hàm băm hoàn chỉnh được định nghĩa là:

h (Mã băm) = Bổ sungHash (Mã băm)% N


Machine Translated by Google

27.4 Xử lý xung đột bằng cách sử dụng địa chỉ mở 989

Điều này cũng giống như

h (hashCode) = SupplementalHash (hashCode) & (N - 1)

vì N là một giá trị của lũy thừa của 2.

27.2 Mã băm là gì? Mã băm cho Byte, Short, Integer và


Tính cách? Kiểm tra điểm

27.3 Mã băm cho một đối tượng Float được tính như thế nào?

27.4 Mã băm cho một đối tượng Long được tính như thế nào?

27.5 Mã băm cho đối tượng Double được tính như thế nào?

27.6 Mã băm cho đối tượng Chuỗi được tính như thế nào?

27.7 Mã băm được nén thành một số nguyên đại diện cho chỉ mục trong bảng băm như thế nào?

27.8 Nếu N là giá trị của lũy thừa 2 thì N / 2 có giống với N >> 1 không?

27,9 Nếu N là giá trị của lũy thừa 2 thì m% N có giống với m & (N - 1) với m nguyên nào không?

27.4 Xử lý xung đột bằng cách sử dụng địa chỉ mở


Xung đột xảy ra khi hai khóa được ánh xạ tới cùng một chỉ mục trong bảng băm.
Chìa khóa

Nói chung, có hai cách để xử lý xung đột: định địa chỉ mở và chuỗi riêng biệt. Điểm

Định địa chỉ mở là quá trình tìm kiếm một vị trí mở trong bảng băm trong trường hợp xảy ra va chạm. mở địa chỉ
Định địa chỉ mở có một số biến thể: thăm dò tuyến tính, thăm dò bậc hai và băm kép.

27.4.1 Thăm dò tuyến tính Khi xung

đột xảy ra trong quá trình chèn một mục nhập vào bảng băm, thăm dò tuyến tính sẽ tuần tự tìm vị trí thêm đầu vào

khả dụng tiếp theo. Ví dụ: nếu xung đột xảy ra tại hashTable [k% N], hãy kiểm tra xem hashTable [(k thăm dò tuyến tính

+ 1)% N] có khả dụng hay không. Nếu không, hãy kiểm tra hashTable [(k + 2)

% N] , v.v., cho đến khi tìm thấy một ô khả dụng, như trong Hình 27.2.

Lưu
ý Khi đầu dò đến cuối bảng, nó sẽ quay trở lại đầu bảng.
Do đó, bảng băm được coi như thể nó là hình tròn. bảng băm tròn

0
phím: 44
Phần tử mới có khóa Để đơn giản, chỉ các phím được hiển
1 thị và các giá trị không được hiển
26 sẽ được chèn
thị. Ở đây N là 11 và index = key% N.
2

4 phím: 4

Thăm dò 3 lần trước khi 5 phím: 16


tìm thấy ô trống
6 phím: 28

số 8

10 phím: 21

HÌNH 27.2 Khảo sát tuyến tính tuần tự tìm vị trí khả dụng tiếp theo.
Machine Translated by Google

990 chương 27 băm

mục tìm kiếm Để tìm kiếm một mục nhập trong bảng băm, hãy lấy chỉ mục, chẳng hạn như k, từ hàm băm cho khóa. Kiểm

tra xem hashTable [k% N] có chứa mục nhập hay không. Nếu không, hãy kiểm tra xem hashTable [(k + 1)% N]

có chứa mục nhập hay không, v.v., cho đến khi tìm thấy mục nhập đó hoặc đạt đến ô trống.

Xoá mục nhập Để xóa mục nhập khỏi bảng băm, hãy tìm kiếm mục nhập khớp với khóa. Nếu mục nhập được tìm thấy, hãy

đặt một điểm đánh dấu đặc biệt để biểu thị rằng mục nhập có sẵn. Mỗi ô trong bảng băm có ba trạng thái

có thể có: bị chiếm, được đánh dấu hoặc trống. Lưu ý rằng một ô được đánh dấu cũng có sẵn để chèn.

Việc thăm dò tuyến tính có xu hướng khiến các nhóm ô liên tiếp trong bảng băm bị chiếm dụng.
cụm Mỗi nhóm được gọi là một cụm. Mỗi cụm thực sự là một chuỗi thăm dò mà bạn phải tìm kiếm khi truy xuất,

thêm hoặc xóa một mục nhập. Khi các cụm phát triển về kích thước, chúng có thể hợp nhất thành các cụm
thậm chí lớn hơn, làm chậm thời gian tìm kiếm hơn nữa. Đây là một nhược điểm lớn của thăm dò tuyến tính.

Lưu ý sư phạm
hoạt ảnh thăm dò tuyến tính đang bật Để có bản trình diễn GUI tương tác để xem cách hoạt động của thăm dò tuyến tính, hãy truy cập www.cs.armstrong.edu/

Trang web đồng hành liang / animation / HashingLinearProbingAnimation.html, như trong Hình 27.3.

27.4.2 Đo lường bậc hai


thăm dò bậc hai Thăm dò bậc hai có thể tránh được vấn đề phân cụm có thể xảy ra trong thăm dò tuyến tính. Thăm dò tuyến

tính xem xét các ô liên tiếp bắt đầu từ chỉ số k. Mặt khác, thăm dò bậc hai xem xét các ô ở chỉ số (k +
j2 )% N, với j Ú 0, nghĩa là, k% N, (k + 1)% N, (k + 4)

% n, (k + 9)% N, v.v., như trong Hình 27.4.

HÌNH 27.3 Công cụ hoạt ảnh cho thấy cách hoạt động của thăm dò tuyến tính.
Machine Translated by Google

27.4 Xử lý xung đột bằng cách sử dụng địa chỉ mở 991

0 phím: 44
Phần tử mới với Vì đơn giản, chỉ có các phím là
1 hiển thị chứ không phải các giá
phím 26 sẽ được chèn
trị. Ở đây N là 11 và index = key% N.
2

4 phím: 4

5 phím: 16

Đầu dò bậc hai 2 6 phím: 28


lần trước khi tìm thấy .

một ô trống 7 .
.
số 8

10 phím: 21

2 vì
HÌNH 27.4 Phép dò bậc hai làm tăng chỉ số tiếp theo trong dãy lên j

j = 1, 2, 3,. . . .

Thăm dò bậc hai hoạt động theo cách tương tự như thăm dò tuyến tính ngoại trừ sự thay đổi trong

trình tự tìm kiếm. Khảo sát bậc hai tránh được vấn đề phân cụm của thăm dò tuyến tính, nhưng nó có vấn

đề phân cụm riêng, được gọi là phân cụm thứ cấp; nghĩa là, các mục nhập va chạm với một mục nhập bị phân cụm thứ cấp

chiếm dụng sử dụng cùng một trình tự thăm dò.

Thăm dò tuyến tính đảm bảo rằng có thể tìm thấy ô có sẵn để chèn miễn là

bảng không đầy đủ. Tuy nhiên, không có gì đảm bảo như vậy cho việc thăm dò bậc hai.

LƯU Ý sư phạm
Để có bản trình diễn GUI tương tác để xem cách hoạt động của thăm dò bậc hai, hãy truy cập www.cs.armstrong. hoạt ảnh thăm dò bậc hai trên Trang

edu / liang / animation / HashingQuadraticProbingAnimation.html, như trong Hình 27.5. web Đồng hành

27.4.3 Băm kép


Một lược đồ địa chỉ mở khác để tránh vấn đề phân cụm được gọi là double hash ing. Bắt đầu từ chỉ số băm đôi
ban đầu k, cả thăm dò tuyến tính và thăm dò bậc hai đều thêm một gia số vào k để xác định một chuỗi tìm
2
kiếm. Gia số là 1 đối với thăm dò tuyến tính và j cho bậc hai

để thăm dò. Các bước tăng này độc lập với các phím. Băm kép sử dụng một hàm băm phụ h ′ (phím) trên các

phím để xác định giá trị gia tăng nhằm tránh vấn đề phân cụm. Cụ thể, hàm băm kép xem xét các ô ở chỉ số

(k + j * h ′ (phím))% N, với j Ú 0, nghĩa là, k% N, (k + h ′ (phím))% N, ( k + 2 * h ′ (phím))% N, (k + 3


* h ′ (phím))% N, v.v.

Ví dụ: đặt hàm băm chính h và hàm băm phụ h ' trên một hàm băm
bảng kích thước 11 được xác định như sau:

h (key) = key% 11;


h '(phím) = 7 - phím% 7;

Đối với khóa tìm kiếm là 12, chúng tôi có

h (12) = 12 % 11 = 1;
h '(12) = 7 - 12 % 7 = 2;

Giả sử các phần tử có khóa 45, 58, 4, 28 và 21 đã được đặt trong bảng băm.

Bây giờ chúng ta chèn phần tử có khóa 12. Chuỗi thăm dò cho khóa 12 bắt đầu ở chỉ mục 1. Vì ô ở chỉ mục

1 đã bị chiếm, hãy tìm kiếm ô tiếp theo ở chỉ mục 3 (1 + 1 * 2). Vì ô ở chỉ mục 3 đã bị chiếm dụng, hãy

tìm kiếm ô tiếp theo ở chỉ mục 5 (1 + 2 * 2). Vì ô ở chỉ mục 5 trống, phần tử cho khóa 12 hiện được chèn

vào ô này. Quá trình tìm kiếm được minh họa trong Hình 27.6.

Các chỉ số của chuỗi thăm dò như sau: 1, 3, 5, 7, 9, 0, 2, 4, 6, 8, 10. Chuỗi này đạt đến toàn bộ

bảng. Bạn nên thiết kế các chức năng của mình để tạo ra một trình tự thăm dò
Machine Translated by Google

992 chương 27 băm

HÌNH 27.5 Công cụ hoạt ảnh cho thấy cách hoạt động của thăm dò bậc hai.

0 0 0

1 1 phím: 45 1
h (12) phím: 45 phím: 45

2 2 2

3 h (12) + h '(12) 3 phím: 58 3 phím: 58


phím: 58

4 4 phím: 4 4
phím: 4 phím: 4

5 5 h (12) + 2 * h '(12) 5

6 6 phím: 28 6
phím: 28 phím: 28
. . .
7 . 7 . 7 .

số 8
số 8 số 8

9 9 9

10 10 phím: 21 10
phím: 21 phím: 21

HÌNH 27.6 Hàm băm phụ trong phép băm kép xác định mức tăng của chỉ mục tiếp theo trong đầu dò
sự nối tiếp.

đạt đến toàn bộ bảng. Lưu ý rằng hàm thứ hai không bao giờ được có giá trị bằng 0, vì số 0
không phải là số gia.

27.10 Địa chỉ mở là gì? Thăm dò tuyến tính là gì? Thăm dò bậc hai là gì? Băm kép là gì?
Kiểm tra điểm

27.11 Mô tả vấn đề phân cụm để thăm dò tuyến tính.


Machine Translated by Google

27.6 Hệ số tải và băm lại 993

27.12 Phân cụm thứ cấp là gì?

27.13 Hiển thị bảng băm có kích thước 11 sau khi chèn các mục nhập có các phím 34, 29, 53, 44, 120,

39, 45 và 40, sử dụng thăm dò tuyến tính.

27.14 Hiển thị bảng băm có kích thước 11 sau khi chèn các mục nhập có các phím 34, 29, 53, 44, 120,

39, 45 và 40, sử dụng thăm dò bậc hai.

27.15 Hiển thị bảng băm có kích thước 11 sau khi chèn các mục nhập có các phím 34, 29, 53, 44, 120,

39, 45 và 40, sử dụng hàm băm kép với các chức năng sau:

h (k) = k% 11;
h '(k) = 7 - k% 7;

27.5 Xử lý va chạm bằng cách sử dụng chuỗi riêng


Lược đồ chuỗi riêng biệt đặt tất cả các mục nhập có cùng chỉ mục băm vào cùng một vị
Chìa khóa
trí, thay vì tìm các vị trí mới. Mỗi vị trí trong sơ đồ chuỗi riêng biệt sử dụng một Điểm
nhóm để chứa nhiều mục nhập. chuỗi riêng biệt

Bạn có thể triển khai một nhóm bằng cách sử dụng một mảng, ArrayList hoặc LinkedList. Chúng tôi sẽ sử dụng
xô thực hiện
LinkedList để trình diễn. Bạn có thể xem mỗi ô trong bảng băm dưới dạng tham chiếu đến phần đầu của danh

sách được liên kết và các phần tử trong danh sách được liên kết được xâu chuỗi bắt đầu từ phần đầu, như

thể hiện trong Hình 27.7.

0 Vì đơn giản, chỉ có các phím là


phím: 44
Phần tử mới với được hiển thị chứ không phải các giá
1 trị. Ở đây N là 11 và index = key% N.
phím 26 sẽ được chèn
2

4 phím: 4 phím: 26

5 . phím: 16

6 phím: 28
.

7 .

số 8

10 phím: 21

HÌNH 27.7 Lược đồ chuỗi riêng biệt xâu chuỗi các mục nhập có cùng chỉ số băm trong một
nhóm.

27.16 Hiển thị bảng băm có kích thước 11 sau khi chèn các mục nhập bằng các phím 34, 29, 53, 44,

120, 39, 45 và 40, sử dụng chuỗi riêng biệt. Kiểm tra điểm

27.6 Hệ số tải và băm lại


Hệ số tải đo lường mức độ đầy đủ của một bảng băm. Nếu hệ số tải vượt quá, hãy
Chìa khóa
tăng kích thước bảng băm và tải lại các mục nhập vào một bảng băm mới lớn hơn. Điều Điểm
này được gọi là làm lại.
làm lại
Hệ số tải l (lambda) đo lường mức độ đầy đủ của một bảng băm. Đó là tỷ lệ giữa số phần tử với hệ số tải
n
kích thước của bảng băm, nghĩa là, l = , trong đó n là số phần tử và N là số vị trí
n
trong bảng băm.

Lưu ý rằng l bằng 0 nếu bảng băm trống. Đối với lược đồ địa chỉ mở, l nằm trong khoảng từ
0 đến 1; l là 1 nếu bảng băm đầy. Đối với lược đồ chuỗi riêng, l có thể là bất kỳ giá trị nào.
Machine Translated by Google

994 chương 27 băm

Khi l tăng, xác suất va chạm tăng. Các nghiên cứu cho thấy rằng bạn nên duy trì hệ số tải dưới
0,5 đối với lược đồ địa chỉ mở và dưới 0,9 đối với lược đồ chuỗi riêng.

Giữ hệ số tải dưới một ngưỡng nhất định là điều quan trọng đối với hiệu suất của băm. Khi
triển khai lớp java.util.HashMap trong Java API, ngưỡng 0,75 được sử dụng. Bất cứ khi nào hệ
số tải vượt quá ngưỡng, bạn cần tăng kích thước bảng băm và chuyển tất cả các mục trong bản
đồ vào một bảng băm mới lớn hơn. Lưu ý rằng bạn cần thay đổi các hàm băm, vì kích thước bảng
băm đã được thay đổi. Để giảm khả năng băm lại, vì nó tốn kém, ít nhất bạn nên tăng gấp đôi
kích thước bảng băm. Ngay cả khi băm lại định kỳ, băm vẫn là một cách triển khai hiệu quả cho
bản đồ.

Lưu ý sư phạm
hoạt ảnh chuỗi riêng biệt trên Để có bản trình diễn GUI tương tác để xem cách hoạt động của chuỗi riêng biệt, hãy truy cập www.cs.armstrong

Trang web Đồng hành .edu / liang / animation / HashingUsingSeparateChainingAnimation.html, như trong Hình 27.8.

HÌNH 27.8 Công cụ hoạt ảnh cho thấy cách hoạt động của chuỗi riêng biệt.

27.17 Hệ số tải là gì? Giả sử bảng băm có kích thước ban đầu là 4 và hệ số tải của nó là 0,5;

hiển thị bảng băm sau khi chèn các mục nhập có các khóa 34, 29, 53, 44, 120, 39, 45
và 40, sử dụng thăm dò tuyến tính.

27.18 Giả sử bảng băm có kích thước ban đầu là 4 và hệ số tải của nó là 0,5; hiển thị bảng
băm sau khi chèn các mục nhập có các khóa 34, 29, 53, 44, 120, 39, 45 và 40, sử dụng
thăm dò bậc hai.
Machine Translated by Google

27.7 Triển khai bản đồ bằng cách sử dụng băm 995

27.19 Giả sử bảng băm có kích thước ban đầu là 4 và hệ số tải của nó là 0,5; hiển thị bảng
băm sau khi chèn các mục nhập với các phím 34, 29, 53, 44, 120, 39, 45 và 40, sử
dụng chuỗi riêng biệt.

27.7 Triển khai bản đồ bằng cách sử dụng băm


Một bản đồ có thể được thực hiện bằng cách sử dụng băm.

Bây giờ bạn đã hiểu khái niệm băm. Bạn biết cách thiết kế một hàm băm tốt để ánh xạ khóa đến một
chỉ mục trong bảng băm, cách đo lường hiệu suất bằng hệ số tải cũng như cách tăng kích thước bảng
và rehash để duy trì hiệu suất. Phần này trình bày cách triển khai một bản đồ bằng cách sử dụng
chuỗi riêng biệt.
Chúng tôi thiết kế giao diện Bản đồ tùy chỉnh của mình để nhân bản java.util.Map và đặt tên cho
giao diện là MyMap và một lớp cụ thể là MyHashMap, như trong Hình 27.9.

«Giao diện»

MyMap <K, V>

+ clear (): void + Xóa tất cả các mục khỏi bản đồ này.

containsKey (key: K): boolean Trả về true nếu bản đồ này chứa một mục nhập cho khóa
được chỉ định.

+ containsValue (value: V): boolean Trả về true nếu bản đồ này ánh xạ một hoặc nhiều khóa đến giá
trị được chỉ định.

+ entrySet (): Đặt <Entry <K, V >> + get (key: Trả về một tập hợp bao gồm các mục nhập trong bản đồ này.

K): V + isEmpty (): boolean + keySet (): Set Trả về giá trị cho khóa được chỉ định trong bản đồ này.

<K> + put (key: K, value: V): V + remove (key: Trả về true nếu bản đồ này không chứa ánh xạ.

K): void + size (): int + values (): Đặt <V> Trả về một tập hợp bao gồm các khóa trong bản đồ này.

Đặt một ánh xạ trong bản đồ này.

Xóa các mục nhập cho khóa được chỉ định.

Trả về số lượng ánh xạ trong bản đồ này.

Trả về một tập hợp bao gồm các giá trị trong bản đồ này.

MyHashMap <K, V>

+ MyHashMap () Tạo một bản đồ trống với dung lượng mặc định 4 và ngưỡng
hệ số tải mặc định 0,75f.

+ MyHashMap (dung lượng: int) Tạo bản đồ với dung lượng được chỉ định và
ngưỡng hệ số tải mặc định 0,75f.

+ MyHashMap (dung lượng: int, Tạo bản đồ với dung lượng cụ thể và
loadFactorThreshold: float) ngưỡng hệ số tải.

MyMap.Entry <K, V>

-key: K

-value: V

+ Entry (key: K, value: V) + getkey Tạo một mục nhập với khóa và giá trị được chỉ định.

(): K + getValue (): V Trả lại khóa trong mục nhập.

Trả về giá trị trong mục nhập.

HÌNH 27.9 MyHashMap triển khai giao diện MyMap .


Machine Translated by Google

996 chương 27 băm

Bạn triển khai MyHashMap như thế nào? Nếu bạn sử dụng ArrayList và lưu một mục mới ở cuối
danh sách, thời gian tìm kiếm sẽ là O (n). Nếu bạn triển khai MyHashMap bằng cây nhị phân, thời
gian tìm kiếm sẽ là O (log n) nếu cây được cân bằng tốt. Tuy nhiên, bạn có thể áp dụng MyHashMap
bằng cách sử dụng hàm băm để có được thuật toán tìm kiếm thời gian O (1). Liệt kê 27.1 hiển thị
giao diện MyMap và Liệt kê 27.2 triển khai MyHashMap bằng cách sử dụng chuỗi riêng biệt.

LISTING 27.1 MyMap.java


giao diện MyMap 1 giao diện công khai MyMap <K, V> {
(); / ** Xóa tất cả các mục khỏi bản đồ này * / 2 3 public void clear
thông thoáng

/ ** Trả về true nếu khóa được chỉ định nằm trong bản đồ * /
containsKey 5 6 boolean công cộng chứaKey (khóa K);
7
số 8
/ ** Trả về true nếu bản đồ này chứa giá trị được chỉ định * / public boolean
containsValue 9 containsValue (V value);
10
11 / ** Trả lại một tập hợp các mục nhập trong bản đồ * /
entrySet 12 public java.util.Set <Entry <K, V >> entrySet ();
13
14 / ** Trả về giá trị khớp với khóa được chỉ định * /
đến 15 public V get (khóa K);
16
17 / ** Trả về true nếu bản đồ này không chứa bất kỳ mục nhập nào * /
isEmpty 18 public boolean isEmpty ();
19
20 / ** Trả lại một tập hợp bao gồm các khóa trong bản đồ này * /
bộ chìa khoá 21 public java.util.Set <K> keySet ();
22
23 / ** Thêm mục nhập (khóa, giá trị) vào bản đồ * /
đặt 24 public V put (khóa K, giá trị V);
25
26 / ** Xóa mục nhập cho khóa được chỉ định * /
tẩy 27 public void remove (K key);
28
29 / ** Trả về số lượng ánh xạ trong bản đồ này * /
kích cỡ 30 public int size ();
31
32 / ** Trả về một tập hợp bao gồm các giá trị trong bản đồ này * /
giá trị 33 public java.util.Set <V> giá trị ();
34
35 / ** Xác định một lớp bên trong cho Entry * /
Vào lớp bên trong 36 public static class Entry <K, V> {
37 Phím K;
38 Giá trị V;
39
40 mục nhập công khai (khóa K, giá trị V) {
41 this.key = key;
42 this.value = giá trị;
43 }
44
45 public K getKey () {
46 chìa khóa quay trở lại ;

47 }
48
49 public V getValue () {
50 giá trị trả về ;
51 }
52
Machine Translated by Google

27.7 Triển khai bản đồ bằng cách sử dụng Hashing 997

@Ghi đè
53 public String toString () {
54 return "[" + phím + "," + giá trị + "]";
55 }
56 }
57 58}

LISTING 27.2 MyHashMap.java


1 nhập java.util.LinkedList;
2
3 lớp công khai MyHashMap <K, V> triển khai MyMap <K, V> { lớp MyHashMap
4 // Xác định kích thước bảng băm mặc định. Phải là lũy thừa của 2
private static int DEFAULT_INITIAL_CAPACITY = 4; công suất ban đầu mặc định
5 6

7 // Xác định kích thước bảng băm tối đa. 1 << 30 giống như 2 ^ 30
8 private static int MAXIMUM_CAPACITY = 1 << 30; công suất tối đa
9
10 // Dung lượng bảng băm hiện tại. Công suất là lũy thừa của 2
dung lượng int riêng ; 11 năng lực hiện tại
12
13 // Xác định hệ số tải mặc định
14 float tĩnh riêng DEFAULT_MAX_LOAD_FACTOR = 0,75f; hệ số tải mặc định

15
16 // Chỉ định hệ số tải được sử dụng trong bảng băm
17 float loadFactorThreshold riêng tư; 18 ngưỡng hệ số tải

19 // Số mục nhập trong bản đồ


20 int size = 0; kích cỡ

21
22 // Bảng băm là một mảng với mỗi ô là một danh sách được liên kết
23 Bảng LinkedList <MyMap.Entry <K, V >> []; 24 bảng băm

25 / ** Xây dựng bản đồ với dung lượng và hệ số tải mặc định * /


26 public MyHashMap () { phương thức khởi tạo no-arg

27 cái này (DEFAULT_INITIAL_CAPACITY, DEFAULT_MAX_LOAD_FACTOR);


28 }
29
30 / ** Xây dựng bản đồ với dung lượng ban đầu được chỉ định và
31 * hệ số tải mặc định * /
32 public MyHashMap (int initialCapacity) { constructor
33 this (InitialCapacity, DEFAULT_MAX_LOAD_FACTOR);
34 }
35
36 / ** Xây dựng bản đồ với dung lượng ban đầu được chỉ định * và hệ số tải
37 * /
38 public MyHashMap (int initialCapacity, float loadFactorThreshold) { constructor
39 nếu (Công suất ban đầu> MAXIMUM_CAPACITY)
40 this.capacity = MAXIMUM_CAPACITY;
41 khác
42 this.capacity = trimToPowerOf2 (InitialCapacity);
43
44 this.loadFactorThreshold = loadFactorThreshold;
45 table = new LinkedList [dung lượng];
46 }
47
48 @Override / ** Xóa tất cả các mục khỏi bản đồ này * / public void clear () {
49 thông thoáng

50 kích thước = 0;
51 removeEntries ();
52 }
Machine Translated by Google

998 chương 27 băm

53
54 @Override / ** Trả về true nếu khóa được chỉ định nằm trong bản đồ * /
containsKey 55 boolean công cộng chứaKey (khóa K) {
56 if (get (key)! = null)
57 trả về true;
58 khác
59 trả về sai;
60 }
61

62 @Override / ** Trả về true nếu bản đồ này chứa giá trị * / public boolean
containsValue 63 containsValue (V value) {
64 for (int i = 0; i <dung lượng; i ++) {
65 if (table [i]! = null) {
66 LinkedList <Mục nhập <K, V >> bucket = table [i]; for
67 (Entry <K, V> entry: bucket)
68 if (entry.getValue (). equals (value)) trả
69 về true;
70 }
71 }
72
73 trả về sai;
74 }
75
76 @Override / ** Trả về một tập hợp các mục nhập trong bản đồ * /
entrySet 77 public java.util.Set <MyMap.Entry <K, V >> entrySet () {
78 java.util.Set <MyMap.Entry <K, V >> set = new
79 java.util.HashSet <> ();
80
81 for (int i = 0; i <dung lượng; i ++) {
82 if (table [i]! = null) {
83 LinkedList <Mục nhập <K, V >> bucket = table [i]; for
84 (Entry <K, V> entry: bucket)
85 set.add (mục nhập);
86 }
87 }
88
89 trả lại bộ;
90 }
91
92 @Override / ** Trả về giá trị khớp với khóa được chỉ định * /
đến 93 public V get (khóa K) {
94 int bucketIndex = hash (key.hashCode ());
95 if (table [bucketIndex]! = null) {
96 LinkedList <Mục nhập <K, V >> bucket = table [bucketIndex]; for
97 (Entry <K, V> entry: bucket)
98 if (entry.getKey (). equals (key))
99 return entry.getValue ();
100 }
101
102 trả về null;
103 }
104
105 @Override / ** Trả về true nếu bản đồ này không có mục nào * /
isEmpty 106 public boolean isEmpty () {
107 trả về kích thước == 0;
108 }
109
110 @Override / ** Trả lại một bộ bao gồm các khóa trong bản đồ này * /
bộ chìa khoá 111 public java.util.Set <K> keySet () {
112 java.util.Set <K> set = new java.util.HashSet <K> ();
Machine Translated by Google

27.7 Triển khai bản đồ bằng cách sử dụng băm 999

113
114 for (int i = 0; i <dung lượng; i ++) {
115 if (table [i]! = null) {
116 LinkedList <Mục nhập <K, V >> bucket = table [i]; for
117 (Entry <K, V> entry: bucket)
118 set.add (entry.getKey ());
119 }
120 }
121
122 trả lại bộ;
123 }
124
125 @Override / ** Thêm mục nhập (khóa, giá trị) vào bản đồ * /
126 public V put (khóa K, giá trị V) {
127 if (get (key)! = null) { // Khoá đã có trong bản đồ
128 int bucketIndex = hash (key.hashCode ());
129 LinkedList <Mục nhập <K, V >> bucket = table [bucketIndex]; for
130 (Entry <K, V> entry: bucket)
131 if (entry.getKey (). equals (key)) {
132 V oldValue = entry.getValue ();
133 // Thay thế giá trị cũ bằng giá trị mới
134 entry.value = giá trị; //
135 Trả về giá trị cũ cho khóa
136 trả về oldValue;
137 }
138 }
139
140 // Kiểm tra hệ số tải
141 if (kích thước> = dung lượng * loadFactorThreshold) {
142 nếu (dung lượng == MAXIMUM_CAPACITY)
143 ném mới RuntimeException ("Vượt quá dung lượng tối đa");
144
145 rehash ();
146 }
147
148 int bucketIndex = hash (key.hashCode ());
149
150 // Tạo danh sách liên kết cho nhóm nếu chưa được tạo
151 if (table [bucketIndex] == null) {
152 table [bucketIndex] = new LinkedList <Entry <K, V >> ();
153 }
154
155 // Thêm một mục mới (khóa, giá trị) vào hashTable [index]
156 table [bucketIndex] .add (new MyMap.Entry <K, V> (key, value));
157
158 kích thước ++; // Tăng kích thước
159
160 giá trị trả về ;
161 }
162
163 @Override / ** Xóa các mục nhập cho khóa được chỉ định * /
164 public void remove (K key) {
165 int bucketIndex = hash (key.hashCode ());
166
167 // Xóa mục nhập đầu tiên khớp với khóa khỏi nhóm
168 if (table [bucketIndex]! = null) {
169 LinkedList <Mục nhập <K, V >> bucket = table [bucketIndex]; for
170 (Entry <K, V> entry: bucket)
171 if (entry.getKey (). equals (key)) {
172 bucket.remove (mục nhập);
Machine Translated by Google

1000 chương 27 băm


173 kích cỡ--; // Giảm kích thước
174 nghỉ; // Chỉ xóa một mục nhập khớp với khóa
175 }
176 }
177 }
178
179 @Override / ** Trả lại số mục nhập trong bản đồ này * /
kích cỡ 180 public int size () {
181 trả về kích thước;
182 }
183
184 @Override / ** Trả về một tập hợp bao gồm các giá trị trong bản đồ này * /
giá trị 185 public java.util.Set <V> giá trị () {
186 java.util.Set <V> set = new java.util.HashSet <> ();
187
188 for (int i = 0; i <dung lượng; i ++) {
189 if (table [i]! = null) {
190 LinkedList <Mục nhập <K, V >> bucket = table [i];
191 for (Entry <K, V> entry: bucket)
192 set.add (entry.getValue ());
193 }
194 }
195
196 trả lại bộ;
197 }
198
199 / ** Hàm băm * /
băm 200 private int hash (int hashCode) {
201 trả về SupplementalHash (Mã băm) & (dung lượng - 1);
202 }
203
204 / ** Đảm bảo băm được phân bổ đồng đều * /
bổ sung
205 private static int SupplementalHash (int h) {
206 h ^ = (h >>> 20) ^ (h >>> 12);
207 return h ^ (h >>> 7) ^ (h >>> 4);
208 }
209
210 / ** Trả về lũy thừa 2 cho Công suất ban đầu * /
trimToPowerOf2 211 private int trimToPowerOf2 (int initialCapacity) {
212 int dung lượng = 1;
213 while (dung lượng <ban đầuCapacity) {
214 dung lượng << = 1; // Tương tự như dung lượng * = 2. <= hiệu quả hơn
215 }
216
217 công suất trở lại ;
218 }
219
220 / ** Xóa tất cả các mục nhập khỏi mỗi nhóm * /
removeEntries 221 private void removeEntries () {
222 for (int i = 0; i <dung lượng; i ++) {
223 if (table [i]! = null) {
224 bảng [i] .clear ();
225 }
226 }
227 }
228
229 / ** Rehash bản đồ * /
rehash 230 private void rehash () {
231 java.util.Set <Entry <K, V >> set = entrySet (); // Nhận các mục nhập
232 dung lượng << = 1; // Tương tự như dung lượng * = 2. <= hiệu quả hơn
Machine Translated by Google

27.7 Triển khai bản đồ bằng cách sử dụng băm 1001

233 table = new LinkedList [dung lượng]; // Tạo một bảng băm mới
234 kích thước = 0; // Đặt lại kích thước thành 0
235
236 for (Entry <K, V> entry: set) {
237 put (entry.getKey (), entry.getValue ()); // Lưu trữ vào bảng mới
238 }
239 }
240
241 @Override / ** Trả về biểu diễn chuỗi cho bản đồ này * /
242 public String toString () { toString
243 StringBuilder builder = new StringBuilder ("[");
244
245 for (int i = 0; i <dung lượng; i ++) {
246 if (table [i]! = null && table [i] .size ()> 0)
247 for (Entry <K, V> entry: table [i])
248 builder.append (mục nhập);
249 }
250
251 builder.append ("]");
252 return builder.toString ();
253 }
254}

Lớp MyHashMap triển khai giao diện MyMap bằng cách sử dụng chuỗi riêng biệt. Các tham số xác định kích thước
bảng băm và các hệ số tải được xác định trong lớp. Mặc định tham số bảng băm
công suất ban đầu là 4 (dòng 5) và công suất tối đa là 230 (dòng 8). Công suất bảng băm hiện tại được thiết kế

dưới dạng giá trị lũy thừa của 2 (dòng 11). Ngưỡng hệ số tải mặc định là 0,75f (dòng 14). Bạn có thể chỉ định

ngưỡng hệ số tải tùy chỉnh khi xây dựng bản đồ.

Ngưỡng hệ số tải tùy chỉnh được lưu trữ trong loadFactorThreshold (dòng 17). Kích thước trường dữ liệu biểu thị

số mục nhập trong bản đồ (dòng 20). Bảng băm là một mảng. Mỗi ô trong mảng là một danh sách liên kết (dòng 23).

Ba hàm tạo được cung cấp để xây dựng một bản đồ. Bạn có thể tạo bản đồ mặc định với ngưỡng dung lượng và hệ ba nhà xây dựng

số tải mặc định bằng cách sử dụng hàm tạo no-arg (dòng 26–28), một bản đồ có dung lượng được chỉ định và ngưỡng

hệ số tải mặc định (dòng 32–34) và bản đồ với dung lượng cụ thể và ngưỡng hệ số tải (dòng 38–46).

Phương pháp xóa xóa tất cả các mục nhập khỏi bản đồ (dòng 49–52). Nó gọi removeEntries (), xóa tất cả các mục thông thoáng

trong nhóm (dòng 221–227). Phương thức removeEntries () cần thời gian O (dung lượng) để xóa tất cả các mục trong

bảng.

Phương thức containsKey (key) kiểm tra xem khóa được chỉ định có trong bản đồ hay không bằng cách gọi phương containsKey
thức get (dòng 55–60). Vì phương thức get cần O (1) thời gian, nên phương thức containsKey (key) mất O (1) thời

gian.

Phương thức containsValue (value) kiểm tra xem giá trị có trong bản đồ hay không (dòng 63–74). Phương pháp containsValue

này cần thời gian O (dung lượng + kích thước). Nó thực sự là O (công suất), kể từ kích thước công suất 7.

Phương thức entrySet () trả về một tập hợp chứa tất cả các mục trong bản đồ (dòng 77–90). entrySet
Phương pháp này cần thời gian O (công suất).

Phương thức get (key) trả về giá trị của mục nhập đầu tiên với khóa được chỉ định (dòng đến
93–103). Phương pháp này mất O (1) thời gian.

Phương thức isEmpty () chỉ trả về true nếu bản đồ trống (dòng 106–108). Phương pháp này mất O (1) thời gian. isEmpty

Phương thức keySet () trả về tất cả các khóa trong bản đồ dưới dạng một tập hợp. Phương pháp tìm các khóa từ bộ chìa khoá

mỗi nhóm và thêm chúng vào một tập hợp (dòng 111–123). Phương pháp này cần thời gian O (công suất).

Phương thức put (key, value) thêm một mục mới vào bản đồ. Trước tiên, phương pháp kiểm tra xem khóa đã có đặt
trong bản đồ hay chưa (dòng 127), nếu có, nó định vị mục nhập và thay thế giá trị cũ bằng giá trị mới trong mục

nhập cho khóa (dòng 134) và giá trị cũ được trả về ( dòng 136). Nếu như
Machine Translated by Google

1002 chương 27 băm

chìa khóa mới trong bản đồ, mục nhập mới được tạo trong bản đồ (dòng 156). Trước khi chèn mục nhập mới,

phương pháp kiểm tra xem kích thước có vượt quá ngưỡng hệ số tải hay không (dòng 141). Nếu vậy, chương

trình sẽ gọi rehash () (dòng 145) để tăng dung lượng và lưu trữ các mục nhập vào bảng băm mới lớn hơn.

rehash Phương thức rehash () trước tiên sao chép tất cả các mục nhập trong một tập hợp (dòng 231), tăng gấp đôi

dung lượng (dòng 232), tạo một bảng băm mới (dòng 233) và đặt lại kích thước thành 0 (dòng 234). Sau đó,

phương pháp bắt các mục nhập vào bảng băm mới (dòng 236–238). Phương thức rehash nhận O (dung lượng)

thời gian. Nếu không thực hiện rehash, phương thức put sẽ mất O (1) thời gian để thêm một mục mới.
tẩy Phương thức remove (key) xóa mục nhập có khóa được chỉ định trong bản đồ (đường

164–177). Phương pháp này mất O (1) thời gian.


kích cỡ Phương thức size () chỉ trả về kích thước của bản đồ (dòng 180–182). Phương pháp này mất O (1) thời gian.

giá trị Phương thức giá trị () trả về tất cả các giá trị trong bản đồ. Phương pháp kiểm tra từng mục nhập

từ tất cả các nhóm và thêm nó vào một tập hợp (dòng 185–197). Phương pháp này cần thời gian O (công suất).
băm Phương thức hash () gọi ra hàm bổ sung để đảm bảo rằng hàm băm được phân phối đồng đều để tạo ra một chỉ

mục cho bảng băm (dòng 200–208). Phương pháp này mất O (1) thời gian.

Bảng 27.1 tóm tắt độ phức tạp về thời gian của các phương thức trong MyHashMap.

BẢNG 27.1 Độ phức tạp về thời gian cho các phương thức

trong MyHashMap

Phương pháp Thời gian

thông thoáng() O (công suất)

chứaKey (khóa: Chìa khóa) O (1)

chứaValue (giá trị: V) O (công suất)

entrySet () O (công suất)

get (key: K) O (1)

isEmpty () O (1)

bộ chìa khoá() O (công suất)

đặt (khóa: K, giá trị: V) O (1)

loại bỏ (phím: K) O (1)

kích cỡ() O (1)

giá trị () O (công suất)

rehash () O (công suất)

Vì việc băm lại không thường xuyên xảy ra nên độ phức tạp về thời gian đối với phương pháp đưa là O (1).

Lưu ý rằng sự phức tạp của clear, entrySet, keySet, giá trị và rehash

phương pháp phụ thuộc vào dung lượng, vì vậy để tránh hiệu suất kém cho các phương pháp này, bạn nên chọn

công suất ban đầu cẩn thận.

Liệt kê 27.3 đưa ra một chương trình thử nghiệm sử dụng MyHashMap.

LISTING 27.3 TestMyHashMap.java


1 lớp công khai TestMyHashMap {
2 public static void main (String [] args) {
3 // Tạo bản đồ
tạo một bản đồ 4 MyMap <String, Integer> map = new MyHashMap <> (); map.put
đặt mục ("Smith", 30);
5 map.put ("Anderson", 31);
6 map.put ("Lewis", 29);
7 8 map.put ("Nấu", 29);
Machine Translated by Google

27.7 Triển khai bản đồ bằng cách sử dụng băm 1003

map.put ("Smith", 65);


9 10
"
11 System.out.println ("Các mục trong bản đồ: + bản đồ); mục hiển thị
12
13 System.out.println (" Tuổi của Lewis là" map.get + nhận giá trị
14 ("Lewis"));
15
"
16 System.out.println (" Smith có trong bản đồ không? +
17 Map.containsKey (" Smith ")); là chìa khóa trong bản đồ?
"
18 System.out.println (" Tuổi 33 có trong bản đồ không? +
19 map.containsValue (33)); là giá trị trong bản đồ?

20
21 map.remove ("Smith"); Xoá mục nhập
"
22 System.out.println ("Các mục trong bản đồ: + bản đồ);
23
24 map.clear ();
"
25 System.out.println ("Các mục trong bản đồ: + bản đồ);
26 }
27}

Các mục trong bản đồ: [[Anderson, 31] [Smith, 65] [Lewis, 29] [Cook, 29]]
Tuổi của Lewis là 29
Smith có trong bản đồ không? đúng vậy
Tuổi 33 có trong bản đồ không? sai
Các mục trong bản đồ: [[Anderson, 31] [Lewis, 29] [Cook, 29]]
Các mục trong bản đồ: []

Chương trình tạo bản đồ bằng MyHashMap (dòng 4) và thêm năm mục vào bản đồ (dòng 5-9). Dòng
5 thêm khóa Smith với giá trị 30 và dòng 9 thêm Smith với giá trị 65. Giá trị sau thay thế giá
trị cũ. Bản đồ thực sự chỉ có bốn mục nhập. Chương trình hiển thị các mục nhập trong bản đồ
(dòng 11), nhận giá trị cho một khóa (dòng 14), kiểm tra xem bản đồ có chứa khóa (dòng 17) và một
giá trị (dòng 19) hay không, xóa mục nhập có khóa Smith .
(dòng 21) và hiển thị lại các mục trong bản đồ (dòng 22). Cuối cùng, chương trình sẽ xóa bản đồ
(dòng 24) và hiển thị một bản đồ trống (dòng 25).

27.20 1 << 30 ở dòng 8 trong Liệt kê 27.2 là gì ? Các số nguyên được tạo ra từ 1 << 1,
1 << 2 và 1 << 3? Kiểm tra điểm

27.21 Các số nguyên được tạo ra từ 32 >> 1, 32 >> 2, 32 >> 3 và 32 >> 4 là gì?

27.22 Trong Liệt kê 27.2, chương trình sẽ hoạt động nếu LinkedList được thay thế bằng ArrayList?
Trong Liệt kê 27.2, làm cách nào để thay thế mã trong các dòng 55–59 bằng một dòng mã?

27.23 Mô tả cách phương thức put (key, value) được triển khai trong MyHashMap
lớp học.

27.24 Trong Liệt kê 27.5, phương thức bổ sung được khai báo là static, hàm băm có thể
phương thức được khai báo static?

27.25 Hiển thị đầu ra của đoạn mã sau.

MyMap <String, String> map = new MyHashMap <> ();


map.put ("Texas", "Dallas");
map.put ("Oklahoma", "Norman");
map.put ("Texas", "Austin");
map.put ("Oklahoma", "Tulsa");

System.out.println (map.get ("Texas"));


System.out.println (map.size ());
Machine Translated by Google

1004 chương 27 băm

27.8 Tập hợp triển khai sử dụng băm


Một tập hợp băm có thể được thực hiện bằng cách sử dụng một bản đồ băm.
Chìa khóa

Điểm
Aset (được giới thiệu trong Chương 21) là một cấu trúc dữ liệu lưu trữ các giá trị riêng biệt. Khung tập hợp Java

bộ băm định nghĩa giao diện java.util.Set cho các tập mô hình. Ba tation quan trọng cụ thể làjava.util.HashSet,

bản đồ băm java.util.LinkedHashSet và java.util.TreeSet. java.util.HashSet được triển khai bằng cách sử dụng băm,

bộ java.util.LinkedHashSet sử dụng LinkedList và java.util.TreeSet sử dụng cây đỏ đen.

Bạn có thể triển khai MyHashSet bằng cách sử dụng tương tự như cách triển khai MyHashMap.

Sự khác biệt duy nhất là các cặp khóa / giá trị được lưu trữ trong bản đồ, trong khi các phần tử được lưu trữ trong
tập hợp.

Bộ của tôi Chúng tôi thiết kế giao diện Đặt tùy chỉnh của mình để phản chiếu java.util . Đặt và đặt tên cho giao diện

MyHashSet MySet và một lớp cụ thể MyHashSet, như trong Hình 27.10.

«Giao diện»
java.lang.Iterable <E>

+ iterator (): java.util.Iterator <E>

«Giao diện»
MySet <E>

+ clear (): void Xóa tất cả các phần tử khỏi tập hợp này.

+ chứa (e: E): boolean Trả về true nếu phần tử nằm trong tập hợp.

+ add (e: E): boolean Thêm phần tử vào tập hợp và trả về true nếu phần tử được thêm vào

thành công.

+ remove (e: E): boolean Loại bỏ phần tử khỏi tập hợp và trả về true nếu tập hợp
chứa phần tử.

+ isEmpty (): boolean Trả về true nếu tập hợp này không chứa bất kỳ phần tử nào.

+ size (): int Trả về số phần tử trong tập hợp này.

MyHashSet <E>

+ MyHashSet () Tạo một tập hợp trống với dung lượng mặc định 4 và tải mặc định
ngưỡng hệ số 0,75f.

+ MyHashMap (dung lượng: int) Tạo một tập hợp với công suất được chỉ định và hệ số tải mặc định
ngưỡng 0,75f.

+ MyHashMap (dung lượng: int, Tạo một tập hợp với ngưỡng công suất và hệ số tải được chỉ định.
loadFactorThreshold: float)

HÌNH 27.10 MyHashSet triển khai giao diện MySet .

Liệt kê 27.4 hiển thị giao diện MySet và Liệt kê 27.5 triển khai MyHashSet bằng cách sử dụng chuỗi riêng.

LISTING 27.4 MySet.java


1 giao diện công khai MySet <E> mở rộng java.lang.Iterable <E> {
2 / ** Xóa tất cả các phần tử khỏi tập hợp này * /
thông thoáng
khoảng trống công khai clear ();
3 4

5 / ** Trả về true nếu phần tử nằm trong tập hợp * /


Machine Translated by Google

27.8 Bộ triển khai sử dụng băm 1005

công khai boolean chứa (E e); chứa


6 7

/ ** Thêm một phần tử vào tập hợp * /


public boolean add (E e); cộng
8 9 10

11 / ** Xóa phần tử khỏi tập hợp * /


12 public boolean remove (E e); tẩy

13
14 / ** Trả về true nếu tập hợp không chứa bất kỳ phần tử nào * /
15 public boolean isEmpty (); isEmpty
16
17 / ** Trả về số phần tử trong tập hợp * /
18 public int size (); kích cỡ

19}

LISTING 27,5 MyHashSet.java


1 nhập java.util.LinkedList;
2
3 lớp công khai MyHashSet <E> triển khai MySet <E> { lớp MyHashSet
// Xác định kích thước bảng băm mặc định. Phải là lũy thừa của 2
45 private static int DEFAULT_INITIAL_CAPACITY = 4; công suất ban đầu mặc định
6
7 // Xác định kích thước bảng băm tối đa. 1 << 30 giống như 2 ^ 30
8 private static int MAXIMUM_CAPACITY = 1 << 30; công suất tối đa
9
10 // Dung lượng bảng băm hiện tại. Công suất là lũy thừa của 2
11 dung lượng int riêng ; 12 năng lực hiện tại

13 // Xác định hệ số tải mặc định


14 float tĩnh riêng DEFAULT_MAX_LOAD_FACTOR = 0,75f; hệ số tải tối đa mặc định

15
16 // Chỉ định ngưỡng hệ số tải được sử dụng trong bảng băm
17 float loadFactorThreshold riêng tư; 18 ngưỡng hệ số tải

19 // Số phần tử trong tập hợp


20 private int size = 0; kích cỡ

21
22 // Bảng băm là một mảng với mỗi ô là một danh sách được liên kết
23 bảng LinkedList <E> [] riêng tư ; bảng băm
24
25 / ** Xây dựng một tập hợp với công suất và hệ số tải mặc định * /
26 public MyHashSet () { phương thức khởi tạo no-arg

27 cái này (DEFAULT_INITIAL_CAPACITY, DEFAULT_MAX_LOAD_FACTOR);


28 }
29
30 / ** Xây dựng một tập hợp với công suất ban đầu được chỉ định và * hệ số tải
31 mặc định * /
32 public MyHashSet (int initialCapacity) { constructor
33 this (InitialCapacity, DEFAULT_MAX_LOAD_FACTOR);
34 }
35
36 / ** Xây dựng một tập hợp với công suất ban đầu được chỉ định * và hệ số
37 tải * /
38 public MyHashSet (int initialCapacity, float loadFactorThreshold) { constructor
39 nếu (Công suất ban đầu> MAXIMUM_CAPACITY)
40 this.capacity = MAXIMUM_CAPACITY;
41 khác
42 this.capacity = trimToPowerOf2 (InitialCapacity);
Machine Translated by Google

1006 chương 27 băm

43
44 this.loadFactorThreshold = loadFactorThreshold;
45 table = new LinkedList [dung lượng];
46 }
47
48 @Override / ** Xóa tất cả các phần tử khỏi tập hợp này * / public
thông thoáng 49 void clear () {
50 kích thước = 0;
51 removeElements ();
52 }
53
54 @Override / ** Trả về true nếu phần tử nằm trong tập hợp * /
chứa 55 boolean công cộng chứa (E e) {
56 int bucketIndex = hash (e.hashCode ());
57 if (table [bucketIndex]! = null) {
58 LinkedList <E> bucket = table [bucketIndex]; for (Phần
59 tử E: bucket)
60 if (element.equals (e)) trả
61 về true;
62 }
63
64 trả về sai;
65 }
66
67 @Override / ** Thêm một phần tử vào tập hợp * /
cộng 68 public boolean add (E e) {
69 if (chứa (e)) // Phần tử trùng lặp không được lưu trữ
70 trả về sai;
71
72 if (kích thước + 1 > dung lượng * loadFactorThreshold) {
73 nếu (dung lượng == MAXIMUM_CAPACITY)
74 ném mới RuntimeException ("Vượt quá dung lượng tối đa");
75
76 rehash ();
77 }
78
79 int bucketIndex = hash (e.hashCode ());
80
81 // Tạo danh sách liên kết cho nhóm nếu chưa được tạo
82 if (table [bucketIndex] == null) {
83 table [bucketIndex] = new LinkedList <E> ();
84 }
85
86 // Thêm e vào hashTable [index]
87 bảng [bucketIndex] .add (e);
88
89 kích thước ++; // Tăng kích thước
90
91 trả về true;
92 }
93
94 @Override / ** Xóa phần tử khỏi tập hợp * /
tẩy 95 public boolean remove (E e) {
96 if (! chứa (e))
97 trả về sai;
98
99 int bucketIndex = hash (e.hashCode ());
100
101 // Tạo danh sách liên kết cho nhóm nếu chưa được tạo
102 if (table [bucketIndex]! = null) {
Machine Translated by Google

27.8 Bộ triển khai sử dụng băm 1007

103 LinkedList <E> bucket = table [bucketIndex]; for (Phần


104 tử E: bucket)
105 if (e.equals (phần tử)) {
106 bucket.remove (phần tử);
107 nghỉ;
108 }
109 }
110
111 kích cỡ--; // Giảm kích thước
112
113 trả về true;
114 }
115
116 @Override / ** Trả về true nếu tập hợp không chứa phần tử * /
117 public boolean isEmpty () { isEmpty

118 trả về kích thước == 0;


119 }
120
121 @Override / ** Trả về số phần tử trong tập hợp * /
122 public int size () { kích cỡ

123 trả về kích thước;


124 }
125
126 @Override / ** Trả về một trình lặp cho các phần tử trong tập hợp này * /
127 public java.util.Iterator <E> iterator () {
người lặp lại

128 trả về MyHashSetIterator mới (this);


129 }
130
131 / ** Lớp bên trong cho trình lặp * /
132 lớp riêng MyHashSetIterator triển khai java.util.Iterator <E> { lớp bên trong

133 // Lưu trữ các phần tử trong một danh sách


134 danh sách riêng java.util.ArrayList <E>;
135 int hiện tại riêng tư = 0; // Trỏ đến phần tử hiện tại trong danh sách
136 bộ MyHashSet <E> riêng tư ;
137
138 / ** Tạo danh sách từ tập hợp * /
139 public MyHashSetIterator (MyHashSet <E> set) {
140 this.set = set;
141 list = setToList ();
142 }
143
144 @Override / ** Yếu tố tiếp theo để đi ngang? * /
145 public boolean hasNext () {
146 if (hiện tại <list.size ())
147 trả về true;
148
149 trả về sai;
150 }
151
152 @Override / ** Lấy phần tử hiện tại và di chuyển con trỏ sang phần tiếp theo * /
153 công khai E tiếp theo () {
154 return list.get (hiện tại ++);
155 }
156
157 / ** Xóa phần tử hiện tại và làm mới danh sách * /
158 public void remove () {
159 // Xóa phần tử hiện tại khỏi bộ băm
160 set.remove (list.get (hiện tại));
161 list.remove (hiện tại); // Xóa phần tử hiện tại khỏi danh sách
162 }
Machine Translated by Google

1008 chương 27 băm

163 }
164
165 / ** Hàm băm * /
băm 166 private int hash (int hashCode) {
167 trả về SupplementalHash (Mã băm) & (dung lượng - 1);
168 }
169
170 / ** Đảm bảo băm được phân bổ đồng đều * /
bổ sung 171 private static int SupplementalHash (int h) {
172 h ^ = (h >>> 20) ^ (h >>> 12);
173 return h ^ (h >>> 7) ^ (h >>> 4);
174 }
175
176 / ** Trả về lũy thừa 2 cho Công suất ban đầu * /
trimToPowerOf2 177 private int trimToPowerOf2 (int initialCapacity) {
178 int dung lượng = 1;
179 while (dung lượng <ban đầuCapacity) {
180 dung lượng << = 1; // Tương tự như dung lượng * = 2. <= hiệu quả hơn
181 }
182
183 công suất trở lại ;
184 }
185
186 / ** Xóa tất cả các e khỏi mỗi thùng * /
187 private void removeElements () {
188 for (int i = 0; i <dung lượng; i ++) {
189 if (table [i]! = null) {
190 bảng [i] .clear ();
191 }
192 }
193 }
194
195 / ** Rehash tập hợp * /
rehash 196 private void rehash () {
197 java.util.ArrayList <E> list = setToList (); // Sao chép vào danh sách
198 dung lượng << = 1; // Tương tự như dung lượng * = 2. <= hiệu quả hơn
199 table = new LinkedList [dung lượng]; // Tạo một bảng băm mới
200 kích thước = 0;
201
202 for (Phần tử E: danh sách) {
203 thêm (phần tử); // Thêm từ bảng cũ vào bảng mới
204 }
205 }
206
207 / ** Sao chép các phần tử trong bộ băm vào danh sách mảng * /
setToList 208 private java.util.ArrayList <E> setToList () {
209 java.util.ArrayList <E> list = new java.util.ArrayList <> ();
210
211 for (int i = 0; i <dung lượng; i ++) {
212 if (table [i]! = null) {
213 cho (E e: table [i]) {
214 list.add (e);
215 }
216 }
217 }
218
219 danh sách trả lại ;
220 }
221
222 @Override / ** Trả về biểu diễn chuỗi cho tập hợp này * /
Machine Translated by Google

27.8 Bộ triển khai sử dụng băm 1009

223 public String toString () { toString


224 java.util.ArrayList <E> list = setToList ();
225 StringBuilder builder = new StringBuilder ("[");
226
227 // Thêm các phần tử ngoại trừ phần tử cuối cùng vào trình tạo chuỗi
228 for (int i = 0; i <list.size () - 1; i ++) {
229 builder.append (list.get (i) + ", ");
230 }
231
232 // Thêm phần tử cuối cùng trong danh sách vào trình tạo chuỗi
233 if (list.size () == 0)
234 builder.append ("]");
235 khác
236 builder.append (list.get (list.size () - 1) + "]");
237
238 return builder.toString ();
239 }
240}

Lớp MyHashSet triển khai giao diện MySet bằng cách sử dụng chuỗi riêng biệt. Việc triển khai MyHashSet rất giống

với việc triển khai MyHashMap ngoại trừ những điểm khác biệt sau: MyHashSet so với MyHashMap

1. Các phần tử được lưu trữ trong bảng băm cho MyHashSet, nhưng các mục nhập (khóa / giá trị

các cặp) được lưu trữ trong bảng băm cho MyHashMap.

2. MySet mở rộng java.lang.Iterable và MyHashSet triển khai MySet và over rides iterator (). Vì vậy, các phần tử

trong MyHashSet có thể lặp lại.

Ba hàm tạo được cung cấp để xây dựng một tập hợp. Bạn có thể tạo một tập hợp mặc định với công suất và hệ số tải mặc ba nhà xây dựng

định bằng cách sử dụng hàm tạo no-arg (dòng 26–28), một tập hợp với công suất được chỉ định và hệ số tải mặc định

(dòng 32–34) và một tập hợp với giá trị được chỉ định công suất và hệ số tải (dòng 38–46).

Phương thức rõ ràng loại bỏ tất cả các phần tử khỏi tập hợp (dòng 49–52). Nó gọi removeElements (), xóa tất cả thông thoáng

các ô trong bảng (dòng 190). Mỗi ô trong bảng là một danh sách được liên kết lưu trữ các phần tử có cùng mã băm.

Phương thức removeElements () mất thời gian O (dung lượng).

Phương thức chứa (phần tử) kiểm tra xem phần tử được chỉ định có nằm trong tập hợp hay không bằng cách kiểm tra chứa
xem nhóm được chỉ định có chứa phần tử (dòng 55–65) hay không. Phương pháp này mất O (1) thời gian.

Phương thức add (element) thêm một phần tử mới vào tập hợp. Đầu tiên, phương thức kiểm tra xem phần tử đã có cộng

trong tập hợp chưa (dòng 69). Nếu vậy, phương thức trả về false. Sau đó, phương pháp sẽ kiểm tra xem kích thước

có vượt quá ngưỡng hệ số tải hay không (dòng 72). Nếu vậy, chương trình sẽ gọi rehash () (dòng 76) để tăng dung

lượng và lưu trữ các phần tử vào bảng băm mới lớn hơn.

Phương thức rehash () trước tiên sao chép tất cả các phần tử trong một danh sách (dòng 197), tăng gấp đôi dung rehash
lượng (dòng 198), tạo một bảng băm mới (dòng 199) và đặt lại kích thước thành 0 (dòng 200). Sau đó, phương thức

sao chép các phần tử vào bảng băm mới lớn hơn (dòng 202–204). Phương thức rehash mất thời gian O (dung lượng). Nếu

không thực hiện rehash, phương thức add sẽ mất O (1) thời gian để thêm một phần tử mới.

Phương thức remove (element) loại bỏ phần tử được chỉ định trong tập hợp (dòng 95–114). tẩy

Phương pháp này mất O (1) thời gian.

Phương thức size () chỉ đơn giản trả về số phần tử trong tập hợp (dòng 122–124). Phương pháp này mất O (1) kích cỡ

thời gian.

Phương thức iterator () trả về một thể hiện của java.util.Iterator. Lớp MyHashSetIterator triển khai người lặp lại

java.util.Iterator để tạo một trình vòng lặp chuyển tiếp.

Khi một MyHashSetIterator được xây dựng, nó sẽ sao chép tất cả các phần tử trong tập hợp vào một danh sách
Machine Translated by Google

1010 chương 27 băm

(dòng 141). Biến dòng trỏ đến phần tử trong danh sách. Ban đầu, hiện tại là 0

(dòng 135), trỏ đến phần tử đầu tiên trong danh sách. MyHashSetIterator triển khai các phương thức hasNext (),

next () và remove () trong java.util.Iterator. Gọi hasNext () trả về true nếu hiện tại <list.size (). Gọi next

() trả về phần tử hiện tại và di chuyển hiện tại để trỏ đến phần tử tiếp theo (dòng 153). Gọi loại bỏ ()

loại bỏ phần tử hiện tại trong trình vòng lặp khỏi tập hợp.

băm Phương thức hash () gọi hàm bổ sung để đảm bảo rằng hàm băm được phân phối đồng đều để tạo ra một chỉ mục

cho bảng băm (dòng 166–174). Phương pháp này nhận O (1)
thời gian.

Bảng 27.2 tóm tắt độ phức tạp về thời gian của các phương thức trong MyHashSet.

BẢNG 27.2 Độ phức tạp về thời gian cho các

phương thức trong MyHashSet

Phương pháp Thời gian

thông thoáng() O (công suất)

chứa (e: E) O (1)

thêm (e: E) O (1)

loại bỏ (e: E) O (1)

isEmpty () O (1)

kích cỡ() O (1)

biến lặp () O (công suất)

rehash () O (công suất)

Liệt kê 27.6 đưa ra một chương trình thử nghiệm sử dụng MyHashSet.

LISTING 27.6 TestMyHashSet.java


1 lớp công khai TestMyHashSet {
2 public static void main (String [] args) {
3 // Tạo MyHashSet
tạo một bộ 4 MySet <String> set = new MyHashSet <> ();
thêm các yếu tố 5 set.add ("Smith");
6 set.add ("Anderson");
set.add ("Lewis");
set.add ("Nấu");
set.add ("Smith");
7 8 9 10
"
yếu tố hiển thị 11 System.out.println ("Các phần tử trong tập hợp: + set);
"
đặt kích thước 12 System.out.println (" Số phần tử trong tập hợp: + set.size ());
"
13 System.out.println ("Có Smith trong tập hợp không? + Set.contains (" Smith ")) ;
14
loại bỏ phần tử 15 set.remove ("Smith");
16 System.out.print ("Các tên được đặt bằng chữ hoa là ");
vòng lặp foreach 17 for (String s: set)
18 System.out.print (s.toUpperCase () + " ");
19
tập hợp rõ ràng 20 set.clear ();
"
21 System.out.println ("\ nCác cài đặt trong bộ: + bộ);
22 }
23}
Machine Translated by Google

Tóm tắt chương 1011

Chương trình tạo một tập hợp bằng MyHashSet (dòng 4) và thêm năm phần tử vào tập hợp (dòng 5-9).

Dòng 5 thêm Smith và dòng 9 thêm lại Smith . Vì chỉ các phần tử không trùng lặp được lưu trữ trong tập

hợp, Smith chỉ xuất hiện trong tập hợp một lần. Tập hợp thực sự có bốn phần tử. Chương trình hiển thị

các phần tử (dòng 11), lấy kích thước của nó (dòng 12), kiểm tra xem tập hợp có chứa một phần tử được

chỉ định hay không (dòng 13) và loại bỏ một phần tử (dòng 15). Vì các phần tử trong một tập hợp có thể

lặp lại, nên một vòng lặp foreach được sử dụng để duyệt qua tất cả các phần tử trong tập hợp (dòng 17–

18). Cuối cùng, chương trình xóa tập hợp (dòng 20) và hiển thị tập hợp trống (dòng 21).

27.26 Tại sao bạn có thể sử dụng vòng lặp foreach để duyệt qua các phần tử trong một tập hợp?

27.27 Mô tả cách phương thức add (e) được triển khai trong lớp MyHashSet .

27.28 Trong Liệt kê 27.5, phương thức remove trong trình vòng lặp sẽ xóa phần tử hiện tại
khỏi tập hợp. Nó cũng xóa phần tử hiện tại khỏi danh sách nội bộ (dòng 161):

Điều này có cần thiết không?

27.29 Thay thế mã trong các dòng 146-149 Trong Liệt kê 27.5 bằng một câu lệnh.

ĐIỀU KHOẢN CHÍNH

mảng kết hợp 986 thăm dò tuyến tính 989


cụm 990 hệ số tải 993

từ điển 986 mở địa chỉ 989


băm kép 991 hàm băm hoàn hảo 986
mã băm 987 mã băm đa thức 988
hàm băm 986 thăm dò bậc hai 990
bản đồ băm 1004 làm lại 993
bộ băm 1004 phân cụm thứ cấp 991
bảng băm 986 chuỗi riêng biệt 993

TÓM TẮT CHƯƠNG

1. Bản đồ là một cấu trúc dữ liệu lưu trữ các mục nhập. Mỗi mục nhập chứa hai phần: khóa
và giá trị. Khóa còn được gọi là khóa tìm kiếm, dùng để tìm kiếm giá trị tương ứng.
Bạn có thể triển khai một bản đồ để có được độ phức tạp thời gian O (1) khi tìm kiếm,
truy xuất, chèn và xóa bằng cách sử dụng kỹ thuật băm.

2. Tập hợp là một cấu trúc dữ liệu lưu trữ các phần tử. Bạn có thể sử dụng kỹ thuật băm để đưa vào

một tập hợp nhằm đạt được độ phức tạp về thời gian O (1) khi tìm kiếm, chèn và xóa cho một tập hợp.

3. Hashing là một kỹ thuật lấy giá trị bằng cách sử dụng chỉ mục thu được từ một khóa mà không
cần thực hiện tìm kiếm. Một hàm băm điển hình trước tiên chuyển đổi một khóa tìm kiếm thành
Machine Translated by Google

1012, chương 27 băm

một giá trị số nguyên được gọi là mã băm, sau đó nén mã băm thành một chỉ mục vào bảng băm.

4. Xung đột xảy ra khi hai khóa được ánh xạ tới cùng một chỉ mục trong bảng băm. Đồng
minh của Gener, có hai cách để xử lý va chạm: định địa chỉ mở và chuỗi riêng biệt.

5. Định địa chỉ mở là quá trình tìm kiếm một vị trí mở trong bảng băm trong trường
hợp va chạm. Định địa chỉ mở có một số biến thể: thăm dò tuyến tính, thăm dò
bậc hai và băm kép.

6. Lược đồ chuỗi riêng biệt đặt tất cả các mục nhập có cùng chỉ mục băm vào cùng
một vị trí, thay vì tìm các vị trí mới. Mỗi vị trí trong sơ đồ chuỗi riêng
biệt được gọi là một nhóm. Một thùng là một thùng chứa nhiều mục nhập.

ĐỐ

Trả lời câu hỏi cho chương này trực tuyến tại www.cs.armstrong.edu/liang/intro10e/quiz.html.

BÀI TẬP LẬP TRÌNH

** 27.1 (Triển khai MyMap bằng cách sử dụng địa chỉ mở với thăm dò tuyến tính) Tạo một lớp con
crete mới triển khai MyMap bằng cách sử dụng địa chỉ mở với thăm dò tuyến tính.
Để đơn giản, hãy sử dụng f (key) = key% size làm hàm băm, trong đó size là kích thước
bảng băm. Ban đầu, kích thước bảng băm là 4. Kích thước bảng được tăng gấp đôi bất
cứ khi nào hệ số tải vượt quá ngưỡng (0,5).

** 27.2 (Triển khai MyMap bằng cách sử dụng địa chỉ mở với thăm dò bậc hai) Tạo một lớp cụ thể mới
triển khai MyMap bằng cách sử dụng địa chỉ mở với thăm dò bậc hai. Để đơn giản, hãy
sử dụng f (key) = key% size làm hàm băm, trong đó size là kích thước bảng băm. Ban
đầu, kích thước bảng băm là 4. Kích thước bảng được tăng gấp đôi bất cứ khi nào hệ
số tải vượt quá ngưỡng (0,5).

** 27.3 (Triển khai MyMap bằng cách sử dụng địa chỉ mở với hàm băm kép) Tạo một lớp cụ thể mới
triển khai MyMap bằng cách sử dụng địa chỉ mở với hàm băm kép. Để đơn giản, hãy sử
dụng f (key) = key% size làm hàm băm, trong đó size là kích thước bảng băm. Ban đầu,
kích thước bảng băm là 4. Kích thước bảng được tăng gấp đôi bất cứ khi nào hệ số tải
vượt quá ngưỡng (0,5).

** 27.4 (Sửa đổi MyHashMap với các khóa trùng lặp) Sửa đổi MyHashMap để cho phép các khóa phân
loại trùng lặp cho các mục nhập. Bạn cần phải sửa đổi việc triển khai cho lệnh put (key,
giá trị) phương pháp. Ngoài ra, hãy thêm một phương thức mới có tên getAll (key) trả về một

tập hợp các giá trị khớp với khóa trong bản đồ.

** 27.5 (Triển khai MyHashSet bằng MyHashMap) Triển khai MyHashSet bằng MyHashMap. Lưu ý rằng bạn
có thể tạo các mục nhập bằng (key, key) chứ không phải (key,
giá trị).

** 27.6 (Tạo hoạt ảnh thăm dò tuyến tính) Viết chương trình tạo hoạt ảnh thăm dò tuyến tính, như thể

hiện trong Hình 27.3. Bạn có thể thay đổi kích thước ban đầu của bảng băm trong chương trình.
Giả sử ngưỡng hệ số tải là 0,75.

** 27.7 (Tạo hoạt ảnh chuỗi riêng biệt) Viết chương trình tạo hoạt ảnh cho MyHashMap, như thể
hiện trong Hình 27.8. Bạn có thể thay đổi kích thước ban đầu của bảng. Giả sử ngưỡng
hệ số tải là 0,75.
Machine Translated by Google

Bài tập lập trình 1013

** 27.8 (Tạo hoạt ảnh cho thăm dò bậc hai) Viết chương trình tạo hoạt ảnh cho thăm dò bậc
hai, như thể hiện trong Hình 27.5. Bạn có thể thay đổi kích thước ban đầu của bảng
băm trong pro gram. Giả sử ngưỡng hệ số tải là 0,75.

** 27.9 (Triển khai mã băm cho chuỗi) Viết phương thức trả về mã băm cho chuỗi bằng cách sử dụng

cách tiếp cận được mô tả trong Phần 27.3.2 với giá trị b 31. Tiêu đề func như sau:

public static int hashCodeForString (Chuỗi s)

** 27.10 (So sánh MyHashSet và MyArrayList) MyArrayList được định nghĩa trong Liệt kê 24.3.
Viết chương trình tạo 1000000 giá trị kép ngẫu nhiên giữa 0
và 999999 và lưu trữ chúng trong MyArrayList và trong MyHashSet. Tạo danh sách 1000000
giá trị kép ngẫu nhiên từ 0 đến 1999999. Đối với mỗi num ber trong danh sách, hãy kiểm

tra xem nó có trong danh sách mảng và trong tập băm hay không. Chạy chương trình của
bạn để hiển thị tổng thời gian kiểm tra cho danh sách mảng và cho bộ băm.

** 27.11 (setToList) Viết phương thức sau để trả về ArrayList từ một tập hợp.

public static <E> ArrayList <E> setToList (Set <E> s)


Machine Translated by Google

Trang này cố ý để trống


Machine Translated by Google

CHƯƠNG

28
GRAPHS VÀ
CÁC ỨNG DỤNG

Mục tiêu
■ Để mô hình hóa các vấn đề trong thế giới thực bằng cách sử dụng đồ thị và giải thích Bảy

Các nhịp cầu của bài toán Königsberg (§28.1).

■ Để mô tả các thuật ngữ đồ thị: đỉnh, cạnh, đồ thị đơn giản, đồ thị có trọng số / không

có trọng số và đồ thị có hướng / vô hướng (§28.2).

■ Để biểu diễn các đỉnh và cạnh bằng cách sử dụng danh sách, mảng cạnh, đối tượng cạnh,

ma trận kề và danh sách kề (§28.3).

■ Để lập mô hình biểu đồ bằng giao diện Graph , AbstractGraph

và lớp UnweightedGraph (§28.4).

■ Để hiển thị đồ thị một cách trực quan (§28.5).

■ Để biểu diễn đường đi ngang của đồ thị bằng cách sử dụng lớp AbstractGraph.Tree (§28.6).

■ Để thiết kế và thực hiện tìm kiếm theo chiều sâu (§28.7).

■ Để giải quyết vấn đề vòng kết nối bằng cách sử dụng tìm kiếm theo chiều sâu (§28.8).

■ Để thiết kế và thực hiện tìm kiếm theo chiều rộng-ưu tiên (§28.9).

■ Để giải quyết vấn đề chín đuôi bằng cách sử dụng tìm kiếm đầu tiên theo chiều rộng (§28.10).
Machine Translated by Google

1016 Chương 28 Đồ thị và Ứng dụng

28.1 Giới thiệu


Nhiều vấn đề trong thế giới thực có thể được giải quyết bằng cách sử dụng các thuật toán đồ thị.
Chìa khóa

Điểm
vấn đề Đồ thị rất hữu ích trong việc mô hình hóa và giải quyết các vấn đề trong thế giới thực. Ví dụ, bài toán tìm số

chuyến bay ít nhất giữa hai thành phố có thể được mô hình hóa bằng biểu đồ, trong đó các đỉnh đại diện cho các

thành phố và các cạnh đại diện cho các chuyến bay giữa hai thành phố liền kề, như trong Hình 28.1. Bài toán tìm số

chuyến bay kết nối tối thiểu giữa hai thành phố được rút gọn thành tìm đường đi ngắn nhất giữa hai đỉnh trong biểu

đồ.

Seattle (0)

Boston (6)

Chicago (5)

New York (7)


Denver (3)

San Francisco (1)


Thành phố Kansas (4)

Los Angeles (2)


Atlanta (8)

Dallas (10)

Houston (11)

Miami (9)

HÌNH 28.1 Một biểu đồ có thể được sử dụng để lập mô hình các chuyến bay giữa các thành phố.

lý thuyết đồ thị Việc nghiên cứu các vấn đề về đồ thị được gọi là lý thuyết đồ thị. Lý thuyết đồ thị được Leonhard Euler sáng

Bảy cây cầu của Königsberg lập vào năm 1736, khi ông đưa ra thuật ngữ đồ thị để giải Bảy

Những nhịp cầu của vấn đề Königsberg. Thành phố Königsberg, Phổ (nay là Kaliningrad, Nga), bị chia cắt bởi sông

Pregel. Có hai hòn đảo trên sông. Thành phố và các đảo được nối với nhau bằng bảy cây cầu, như trong Hình 28.2a.

Câu hỏi đặt ra là liệu người ta có thể đi bộ, băng qua mỗi cây cầu đúng một lần và quay trở lại điểm xuất phát

không? Euler đã chứng minh rằng điều đó là không thể.

MỘT

MỘT

C D
Đảo 1
Đảo 2 C D

B B

(a) Bản phác thảo bảy cây cầu (b) Mô hình đồ thị

HÌNH 28.2 Bảy cây cầu nối liền đảo và đất liền.

Để thiết lập một bằng chứng, Euler đầu tiên đã tóm tắt bản đồ thành phố Königsberg bằng cách loại bỏ tất cả các

đường phố, tạo ra bản phác thảo như trong Hình 28.2a. Tiếp theo, ông thay thế từng khối đất bằng
Machine Translated by Google

28.2 Các thuật ngữ đồ thị cơ bản 1017

một chấm, được gọi là đỉnh hoặc nút, và mỗi cầu nối với một đoạn thẳng, được gọi là cạnh, như thể hiện

trong Hình 28.2b. Cấu trúc với các đỉnh và cạnh này được gọi là đồ thị.

Nhìn vào biểu đồ, chúng ta hỏi liệu có một đường đi bắt đầu từ bất kỳ đỉnh nào, đi qua tất cả các cạnh

chính xác một lần và quay trở lại đỉnh bắt đầu hay không. Euler đã chứng minh rằng để tồn tại một đường đi

như vậy, mỗi đỉnh phải có một số cạnh chẵn. Do đó, bài toán Bảy cây cầu của Königsberg không có lời giải.

Các vấn đề về đồ thị thường được giải quyết bằng cách sử dụng các thuật toán. Các thuật toán đồ thị

có nhiều ứng dụng trong các lĩnh vực khác nhau, chẳng hạn như trong khoa học máy tính, toán học, sinh học,

kỹ thuật, kinh tế, di truyền và khoa học xã hội. Chương này trình bày các thuật toán cho tìm kiếm theo

chiều sâu và tìm kiếm theo chiều rộng, và các ứng dụng của chúng. Chương tiếp theo trình bày các nhịp điệu

thuật toán để tìm một cây bao trùm tối thiểu và các đường đi ngắn nhất trong đồ thị có trọng số, và các ứng

dụng của chúng.

28.2 Các thuật ngữ đồ thị cơ bản


Một đồ thị bao gồm các đỉnh và các cạnh nối các đỉnh.
Chìa khóa

Điểm
Chương này không giả định rằng bạn đã có bất kỳ kiến thức nào về lý thuyết đồ thị hoặc toán học rời rạc.

Chúng tôi sử dụng các thuật ngữ thuần túy và đơn giản để xác định đồ thị.

Đồ thị là gì? Đồ thị là một cấu trúc toán học biểu thị mối quan hệ giữa các thực thể trong thế giới đồ thị là gì?
thực. Ví dụ, biểu đồ trong Hình 28.1 biểu thị các chuyến bay giữa các thành phố và biểu đồ trong Hình 28.2b

biểu thị các cây cầu giữa các vùng đất.

Một đồ thị bao gồm một tập hợp các đỉnh khác nhau (còn được gọi là các nút hoặc điểm) và một tập hợp xác định một đồ thị

các cạnh nối các đỉnh. Để thuận tiện, chúng ta định nghĩa một đồ thị là G = (V, E), trong đó V biểu thị một

tập các đỉnh và E biểu thị một tập các cạnh. Ví dụ, V và E cho đồ thị trong Hình 28.1 như sau:

V = {"Seattle", "San Francisco", "Los Angeles",


"Denver", "Kansas City", "Chicago", "Boston", "New York",
"Atlanta", "Miami", "Dallas", "Houston"};

E = {{"Seattle", "San Francisco"}, {"Seattle", "Chicago"},


{"Seattle", "Denver"}, {"San Francisco", "Denver"},
...
};

Một đồ thị có thể có hướng hoặc không có hướng. Trong một đồ thị có hướng, mỗi cạnh có một hướng, điều đồ thị có hướng so với vô hướng

này chỉ ra rằng bạn có thể di chuyển từ đỉnh này sang đỉnh khác thông qua cạnh đó. Bạn có thể mô hình hóa

các mối quan hệ cha / con bằng cách sử dụng một đồ thị có hướng, trong đó một cạnh từ đỉnh A đến B chỉ ra

rằng A là cha của B. Hình 28.3a cho thấy một đồ thị có hướng.

Peter (0) Jane (1) MỘT D MỘT D

B E B E
Cindy (3) Đánh dấu (2)

Wendy (4) C C

(a) Đồ thị có hướng (b) Một đồ thị hoàn chỉnh (c) Một đồ thị con của đồ thị trong (b)

HÌNH 28.3 Đồ thị có thể xuất hiện ở nhiều dạng.

Trong một đồ thị vô hướng, bạn có thể di chuyển theo cả hai hướng giữa các đỉnh. Đồ thị trong Hình 28.1

là vô hướng.
Machine Translated by Google

1018 Chương 28 Đồ thị và Ứng dụng

đồ thị có trọng số so với đồ thị không Các cạnh có thể có trọng số hoặc không có trọng số. Ví dụ: bạn có thể ấn định trọng số cho mỗi cạnh
có trọng số
trong biểu đồ hình 28.1 để chỉ thời gian bay giữa hai thành phố.

Hai đỉnh trong đồ thị được cho là kề nhau nếu chúng được nối với nhau bởi cùng một cạnh.
các đỉnh liền kề Tương tự, hai cạnh được cho là kề nhau nếu chúng được nối với cùng một đỉnh. Một cạnh trong đồ thị
cạnh sự cố nối hai đỉnh được cho là sự cố đối với cả hai đỉnh. Bậc của đỉnh là số cạnh đối với nó.
trình độ

láng giềng Hai đỉnh được gọi là lân cận nếu chúng kề nhau. Tương tự, hai cạnh được gọi là
hàng xóm nếu họ liền kề.
vòng Vòng lặp là một cạnh liên kết một đỉnh với chính nó. Nếu hai đỉnh được nối bởi hai hay nhiều cạnh
cạnh song song thì các cạnh này được gọi là các cạnh song song. Đồ thị đơn giản là đồ thị không có bất kỳ vòng lặp
đồ thị đơn giản hoặc cạnh song song nào. Trong một đồ thị hoàn chỉnh, cứ hai cặp đỉnh được nối với nhau, như trong
đồ thị hoàn chỉnh Hình 28.3b.
đồ thị kết nối Một đồ thị được kết nối nếu tồn tại một đường đi giữa hai đỉnh bất kỳ trong đồ thị. Đồ thị con của
bảng phụ đồ thị G là đồ thị có tập đỉnh là tập con của G và tập cạnh của nó là tập con của G. Ví dụ, đồ thị

trong Hình 28.3c là đồ thị con của đồ thị trong Hình 28.3b .

xe đạp Giả sử rằng đồ thị được kết nối và vô hướng. Chu trình là một đường khép kín bắt đầu từ một đỉnh
cây và kết thúc tại cùng một đỉnh. Một đồ thị liên thông là một cây nếu nó không có chu trình.
cây kéo dài Cây bao trùm của đồ thị G là đồ thị con liên thông của G và đồ thị con là cây chứa tất cả các đỉnh
trong G.

Lưu ý sư phạm
Trước khi chúng tôi giới thiệu các thuật toán và ứng dụng đồ thị, sẽ rất hữu ích nếu bạn làm

quen với đồ thị bằng công cụ tương tác tại www.cs.armstrong.edu/liang/animation/GraphLearningTool

.html, như trong Hình 28.4. Công cụ này cho phép bạn thêm / bớt / di chuyển các đỉnh và
công cụ học đồ thị trên
vẽ các cạnh bằng cử chỉ chuột. Bạn cũng có thể tìm cây tìm kiếm theo chiều sâu (DFS) và
Trang web đồng hành
cây tìm kiếm theo chiều rộng (BFS) và đường đi ngắn nhất giữa hai đỉnh.

HÌNH 28.4 Bạn có thể sử dụng công cụ này để tạo biểu đồ bằng cử chỉ chuột và hiển thị các cây DFS / BFS và các đường đi ngắn nhất.
Machine Translated by Google

28.3 Biểu diễn đồ thị 1019

28.1 Bài toán Bảy cây cầu của Königsberg nổi tiếng là gì?

28.2 Đồ thị là gì? Giải thích các thuật ngữ sau: đồ thị vô hướng, đồ thị có hướng, đồ thị Kiểm tra điểm

có trọng số, tung độ của đỉnh, cạnh song song, đồ thị đơn giản, đồ thị đầy đủ, đồ
thị liên thông, chu trình, đồ thị con, cây và cây bao trùm.

28.3 Có bao nhiêu cạnh trong một đồ thị hoàn chỉnh có 5 đỉnh? Có bao nhiêu cạnh trong một
cây gồm 5 đỉnh?

28.4 Có bao nhiêu cạnh trong một đồ thị hoàn chỉnh với n đỉnh? Có bao nhiêu cạnh trong một
cây gồm n đỉnh?

28.3 Biểu diễn đồ thị


Biểu diễn một đồ thị là lưu trữ các đỉnh và các cạnh của nó trong một chương trình. Cấu trúc
Chìa khóa

dữ liệu để lưu trữ một đồ thị là các mảng hoặc danh sách. Điểm

Để viết một chương trình xử lý và thao tác trên đồ thị, bạn phải lưu trữ hoặc biểu diễn dữ liệu của đồ thị

trong máy tính.

28.3.1 Biểu diễn các đỉnh Các đỉnh có thể được

lưu trữ trong một mảng hoặc một danh sách. Ví dụ: bạn có thể lưu trữ tất cả các tên thành phố trong biểu đồ

ở Hình 28.1 bằng cách sử dụng mảng sau:

String [] vertices = {"Seattle", "San Francisco", "Los Angeles",


"Denver", "Kansas City", "Chicago", "Boston", "New York",
"Atlanta", "Miami", "Dallas", "Houston"};

Lưu ý

Các đỉnh có thể là đối tượng của bất kỳ kiểu nào. Ví dụ: bạn có thể coi các thành phố loại đỉnh
là đối tượng chứa thông tin như tên, dân số và thị trưởng của nó. Do đó, bạn có thể
xác định các đỉnh như sau:

City city0 = new City ("Seattle", 608660, "Mike McGinn");


...
City city11 = new City ("Houston", 2099451, "Annise Parker");
City [] vertices = {city0, city1, ..., city11};

public class City


{ private String cityName;
dân số int tư nhân ; thị
trưởng tư nhân String;

public City (String cityName, int dân, String thị trưởng) { this.cityName
= cityName; this.population = dân số; this.mayor = thị trưởng;

public String getCityName ()


{ return cityName;
}

public int getPopulation () { return


dân số;
}
Machine Translated by Google

1020 Chương 28 Đồ thị và Ứng dụng

public String getMayor () {


thị trưởng trở lại ;
}

public void setMayor (String thị trưởng) {


this.mayor = thị trưởng;
}

public void setPopulation (int dân số) {


this.population = dân số;
}
}

Các đỉnh có thể được gắn nhãn thuận tiện bằng cách sử dụng các số tự nhiên 0, 1, 2, c, n -
1, cho một đồ thị cho n đỉnh. Do đó, các đỉnh [0] đại diện cho "Seattle", các đỉnh [1] đại
diện cho "San Francisco", v.v., như thể hiện trong Hình 28.5.

đỉnh [0] Seattle

đỉnh [1] San Francisco

đỉnh [2] Los Angeles

đỉnh [3] Denver

đỉnh [4] Thành phố Kansas

đỉnh [5] Chicago

đỉnh [6] Boston

đỉnh [7] Newyork

đỉnh [8] Atlanta

đỉnh [9] Miami

đỉnh [10] Dallas

đỉnh [11] Houston

HÌNH 28.5 Một mảng lưu trữ tên đỉnh.

Lưu ý

đỉnh tham chiếu Bạn có thể tham chiếu một đỉnh theo tên của nó hoặc chỉ số của nó, tùy theo điều kiện nào thuận tiện hơn. Thật đáng

tiếc, thật dễ dàng để truy cập một đỉnh thông qua chỉ mục của nó trong một chương trình.

28.3.2 Biểu diễn các cạnh: Mảng cạnh Các cạnh có thể

được biểu diễn bằng cách sử dụng một mảng hai chiều. Ví dụ, bạn có thể lưu trữ tất cả các
cạnh trong biểu đồ trong Hình 28.1 bằng cách sử dụng mảng sau:

int [] [] edge =
{ {0, 1}, {0, 3}, {0, 5},
{1, 0}, {1, 2}, {1, 3}, {2,
1}, {2, 3}, {2, 4}, {2, 10}, {3, 0}, {3,
1}, {3, 2}, {3, 4}, {3, 5}, {4 , 2}, {4, 3}, {4,
5}, {4, 7}, {4, 8}, {4, 10}, {5, 0}, {5, 3}, {5, 4 }, {5,
6}, {5, 7},
Machine Translated by Google

28.3 Biểu diễn đồ thị 1021

{6, 5}, {6, 7},


{7, 4}, {7, 5}, {7, 6}, {7, 8},
{8, 4}, {8, 7}, {8, 9}, {8, 10}, {8, 11},
{9, 8}, {9, 11},
{10, 2}, {10, 4}, {10, 8}, {10, 11},
{11, 8}, {11, 9}, {11, 10}
};

Biểu diễn này được gọi là mảng cạnh. Các đỉnh và cạnh trong Hình 28.3a có thể được biểu diễn như sau: mảng cạnh

String [] names = {"Peter", "Jane", "Mark", "Cindy", "Wendy"};

int [] [] edge = {{0, 2}, {1, 2}, {2, 4}, {3, 4}};

28.3.3 Biểu diễn các cạnh: Đối tượng cạnh


Một cách khác để biểu diễn các cạnh là xác định các cạnh dưới dạng đối tượng và lưu trữ các cạnh trong
java.util.ArrayList. Lớp Edge có thể được định nghĩa như sau:

lớp công khai Edge {


int u;
int v;

public Edge (int u, int v) {


this.u = u;
this.v = v;
}

public boolean bằng (Đối tượng o) {


return u == ((Cạnh) o) .u && v == ((Cạnh) o) .v;
}
}

Ví dụ: bạn có thể lưu trữ tất cả các cạnh trong biểu đồ trong Hình 28.1 bằng cách sử dụng danh sách sau:

java.util.ArrayList <Edge> list = new java.util.ArrayList <> ();


list.add (new Edge (0, 1));
list.add (new Edge (0, 3));
list.add (New Edge (0, 5));
...

Lưu trữ các đối tượng Edge trong ArrayList rất hữu ích nếu bạn không biết trước về các cạnh.

Mặc dù biểu diễn các cạnh bằng mảng cạnh hoặc các đối tượng Cạnh trong Phần 28.3.2 và trước đó

trong phần này có thể trực quan cho đầu vào, nhưng nó không hiệu quả cho quá trình xử lý nội bộ. Hai

phần tiếp theo giới thiệu cách biểu diễn đồ thị bằng ma trận kề và danh sách kề.

Hai cấu trúc dữ liệu này rất hiệu quả để xử lý đồ thị.

28.3.4 Biểu diễn các cạnh: Ma trận kề


Giả sử rằng đồ thị có n đỉnh. Bạn có thể sử dụng ma trận n * n hai chiều, ví dụ: adjacencyMatrix, để

biểu diễn các cạnh. Mỗi phần tử trong mảng là 0 hoặc 1. adjacencyMatrix [i] [j] là 1 nếu có một cạnh từ

đỉnh i đến đỉnh j; ngược lại, adjacencyMatrix [i] [j] bằng 0. Nếu đồ thị là vô hướng, ma trận là đối

xứng, vì adjacencyMatrix [i] [j] giống với adjacencyMatrix [j] [i]. Ví dụ, các cạnh trong biểu đồ trong

Hình 28.1 có thể được biểu diễn bằng ma trận kề như sau:

ma trận kề

int [] [] adjacencyMatrix = {
{0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0}, // Seattle
{1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0}, // San Francisco
Machine Translated by Google

1022 Chương 28 Đồ thị và Ứng dụng

{0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0}, // Los Angeles


{1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0}, // Denver
{0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0}, // Kansas City
{1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0}, // Chicago
{0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0}, // Boston
{0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0}, // New York
{0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1}, // Atlanta
{0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, // Miami
{0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1}, // Dallas
{0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0} // Houston
};

Ghi chú

Vì ma trận là đối xứng cho một đồ thị vô hướng, nên để tiết kiệm dung lượng, bạn có thể sử dụng một

mảng rách nát mảng rách nát.

Ma trận kề cho đồ thị có hướng trong Hình 28.3a có thể được biểu diễn như sau:

int [] [] a = {{0, 0, 1, 0, 0}, // Peter


{0, 0, 1, 0, 0}, // Jane
{0, 0, 0, 0, 1}, // Đánh dấu
{0, 0, 0, 0, 1}, // Cindy
{0, 0, 0, 0, 0} // Wendy
};

28.3.5 Đại diện cho các cạnh: Danh sách gần kề


danh sách đỉnh kề Bạn có thể biểu diễn các cạnh bằng cách sử dụng danh sách đỉnh kề hoặc danh sách cạnh kề.
danh sách cạnh kề Danh sách đỉnh kề cho đỉnh i chứa các đỉnh kề với i và danh sách cạnh kề cho đỉnh i chứa các
cạnh kề với i. Bạn có thể xác định một mảng danh sách. Mảng có n mục nhập và mỗi mục nhập là
một danh sách. Danh sách đỉnh kề của đỉnh i chứa tất cả các đỉnh j sao cho có một cạnh từ đỉnh
i đến đỉnh j. Ví dụ, để biểu diễn các cạnh trong biểu đồ trong Hình 28.1, bạn có thể tạo một
mảng danh sách như sau:

java.util.List <Integer> [] Neighbor = new java.util.List [12];

láng giềng [0] chứa tất cả các đỉnh kề với đỉnh 0 (tức là Seattle), láng giềng [1] chứa tất cả các đỉnh kề với đỉnh 1 (tức là,

San Francisco), v.v., như thể hiện trong Hình 28.6.

Để biểu diễn danh sách cạnh kề cho biểu đồ trong Hình 28.1, bạn có thể tạo một mảng
danh sách như sau:

java.util.List <Edge> [] Neighbor = new java.util.List [12];

láng giềng [0] chứa tất cả các cạnh kề với đỉnh 0 (tức là Seattle), láng giềng [1] chứa tất cả các cạnh kề với đỉnh 1 (tức là,

San Francisco), v.v., như thể hiện trong Hình 28.7.

Ghi chú

ma trận kề so với Bạn có thể biểu diễn một biểu đồ bằng cách sử dụng ma trận kề hoặc danh sách kề. Cái nào tốt hơn?

danh sách kề Nếu đồ thị dày đặc (tức là có rất nhiều cạnh), thì việc sử dụng ma trận kề được ưu tiên hơn. Nếu

đồ thị rất thưa thớt (tức là rất ít cạnh), sử dụng danh sách kề sẽ tốt hơn, vì sử dụng ma trận kề

sẽ lãng phí rất nhiều không gian.

Cả ma trận kề và danh sách kề đều có thể được sử dụng trong một chương trình để làm cho nhịp điệu

thuật toán hiệu quả hơn. Ví dụ: cần thời gian không đổi O (1) để kiểm tra xem hai đỉnh có được nối

với nhau bằng ma trận kề hay không và cần thời gian tuyến tính O (m) để in tất cả các cạnh trong

biểu đồ bằng cách sử dụng danh sách kề, trong đó m là số cạnh .


Machine Translated by Google

28.3 Biểu diễn đồ thị 1023

Seattle hàng xóm [0] 1 3 5

San Francisco hàng xóm [1] 0 2 3

Los Angeles hàng xóm [2] 1 3 4 10

Denver hàng xóm [3] 0 1 2 4 5

Thành phố Kansas hàng xóm [4] 2 3 5 7 số 8 10

Chicago hàng xóm [5] 0 3 4 6 7

Boston hàng xóm [6] 5 7

Newyork hàng xóm [7] 4 5 6 số 8

Atlanta hàng xóm [8] 4 7 9 10 11

Miami hàng xóm [9] số 8 11

Dallas hàng xóm [10] 2 4 số 8 11

Houston hàng xóm [11] số 8 9 10

HÌNH 28.6 Các cạnh trong biểu đồ trong Hình 28.1 được biểu diễn bằng cách sử dụng danh sách đỉnh kề.

Seattle hàng xóm [0] Cạnh (0, 1) Cạnh (0, 3) Cạnh (0, 5)

San Francisco hàng xóm [1] Cạnh (1, 0) Cạnh (1, 2) Cạnh (1, 3)

Los Angeles hàng xóm [2] Cạnh (2, 1) Cạnh (2, 3) Cạnh (2, 4) Cạnh (2, 10)

Denver hàng xóm [3] Cạnh (3, 0) Cạnh (3, 1) Cạnh (3, 2) Cạnh (3, 4) Cạnh (3, 5)

Thành phố Kansas hàng xóm [4] Cạnh (4, 2) Cạnh (4, 3) Cạnh (4, 5) Cạnh (4, 7) Cạnh (4, 8) Cạnh (4, 10)

Chicago hàng xóm [5] Cạnh (5, 0) Cạnh (5, 3) Cạnh (5, 4) Cạnh (5, 6) Cạnh (5, 7)

Boston hàng xóm [6] Cạnh (6, 5) Cạnh (6, 7)

Newyork hàng xóm [7] Cạnh (7, 4) Cạnh (7, 5) Cạnh (7, 6) Cạnh (7, 8)

Atlanta hàng xóm [8] Cạnh (8, 4) Cạnh (8, 7) Cạnh (8, 9) Cạnh (8, 10) Cạnh (8, 11)

Miami hàng xóm [9] Cạnh (9, 8) Cạnh (9, 11)

Dallas hàng xóm [10] Cạnh (10, 2) Cạnh (10, 4) Cạnh (10, 8) Cạnh (10, 11)

Houston hàng xóm [11] Cạnh (11, 8) Cạnh (11, 9) Cạnh (11, 10)

HÌNH 28.7 Các cạnh trong biểu đồ trong Hình 28.1 được biểu diễn bằng cách sử dụng danh sách cạnh kề.

Ghi chú

Danh sách đỉnh kề đơn giản hơn để biểu diễn đồ thị không trọng số. Tuy nhiên, danh sách danh sách đỉnh kề so
cạnh kề linh hoạt hơn cho nhiều ứng dụng đồ thị. Có thể dễ dàng thêm các ràng buộc về màu với danh sách cạnh kề

sắc trên các cạnh bằng cách sử dụng danh sách các cạnh liền kề. Vì lý do này, cuốn sách này

sẽ sử dụng danh sách cạnh kề để biểu diễn đồ thị.

Bạn có thể sử dụng mảng, danh sách mảng hoặc danh sách được liên kết để lưu trữ danh sách kề. Chúng tôi sẽ sử sử dụng ArrayList

dụng danh sách thay vì mảng, vì danh sách có thể dễ dàng mở rộng để cho phép bạn thêm các đỉnh mới.

Hơn nữa, chúng tôi sẽ sử dụng danh sách mảng thay vì danh sách liên kết, bởi vì các thuật toán của chúng tôi chỉ yêu

cầu tìm kiếm các đỉnh liền kề trong danh sách. Sử dụng danh sách mảng hiệu quả hơn cho các thuật toán của chúng tôi.

Sử dụng danh sách mảng, danh sách cạnh kề trong Hình 28.6 có thể được xây dựng như sau:

Danh sách <ArrayList <Edge>> hàng xóm = new ArrayList <> ();
Neighbor.add (new ArrayList <Edge> ());
Neighbor.get (0) .add (new Edge (0, 1));
Machine Translated by Google

1024 Chương 28 Đồ thị và Ứng dụng

Neighbor.get (0) .add (new Edge (0, 3));


Neighbor.get (0) .add (new Edge (0, 5));
Neighbor.add (new ArrayList <Edge> ());
Neighbor.get (1) .add (new Edge (1, 0));
Neighbor.get (1) .add (new Edge (1, 2));
Neighbor.get (1) .add (new Edge (1, 3));
...
...
hàng xóm.get (11) .add ( Edge mới (11, 8));
hàng xóm.get (11) .add ( Edge mới (11, 9));
hàng xóm.get (11) .add ( Edge mới (11, 10));

28.5 Làm thế nào để bạn biểu diễn các đỉnh trong một đồ thị? Làm thế nào để bạn biểu diễn các cạnh bằng cách sử dụng một

mảng cạnh? Làm thế nào để bạn biểu diễn một cạnh bằng cách sử dụng một đối tượng cạnh? Làm thế nào để bạn biểu
Kiểm tra điểm

diễn các cạnh bằng cách sử dụng ma trận kề? Làm thế nào để bạn biểu diễn các cạnh bằng cách sử dụng danh sách kề?

28.6 Biểu diễn đồ thị sau đây bằng cách sử dụng mảng cạnh, danh sách các đối tượng cạnh, ma trận hàm kề, danh sách đỉnh

kề và danh sách cạnh kề, tương ứng.

1 4

0 3

2 5

28.4 Đồ thị mô hình hóa


Giao diện Đồ thị xác định các hoạt động phổ biến cho một đồ thị.
Chìa khóa

Điểm
Java Collections Framework đóng vai trò là một ví dụ điển hình cho việc thiết kế các cấu trúc
cấu trúc dữ liệu phức tạp. Các đặc điểm chung của cấu trúc dữ liệu được định nghĩa trong các
giao diện (ví dụ: Bộ sưu tập, Bộ, Danh sách, Hàng đợi), như trong Hình 20.1. Các lớp trừu
tượng (ví dụ, AbstractCollection, AbstractSet, AbstractList) triển khai một phần các giao diện.
Các lớp cụ thể (ví dụ: HashSet, LinkedHashSet, TreeSet, ArrayList, LinkedList, PriorityQueue)
cung cấp các triển khai cụ thể. Mẫu thiết kế này rất hữu ích cho việc lập mô hình đồ thị. Chúng
ta sẽ định nghĩa một giao diện có tên là Graph chứa tất cả các hoạt động phổ biến của đồ thị và
một lớp trừu tượng tên là AbstractGraph thực hiện một phần giao diện Graph . Nhiều đồ thị cụ
thể có thể được thêm vào thiết kế. Ví dụ, chúng tôi sẽ xác định các đồ thị như vậy có tên là
UnweightedGraph và WeightedGraph. Mối quan hệ của các giao diện và lớp này được minh họa trong Hình 28.8.

UnweightedGraph
Đồ thị AbstractGraph
WeightedGraph

Giao diện Lớp trừu tượng Lớp bê tông

HÌNH 28.8 Các đồ thị có thể được mô hình hóa bằng cách sử dụng các giao diện, các lớp trừu tượng và các lớp cụ thể.

Các phép toán phổ biến cho một đồ thị là gì? Nói chung, bạn cần lấy số đỉnh trong đồ thị, lấy tất cả các đỉnh trong

đồ thị, lấy đối tượng đỉnh với một chỉ số xác định, lấy chỉ số của đỉnh với một tên được chỉ định, lấy các hàng xóm

của một đỉnh, lấy mức độ cho một đỉnh, xóa biểu đồ, thêm một đỉnh mới, thêm một cạnh mới, thực hiện tìm kiếm theo

chiều sâu và
Machine Translated by Google

28.4 Đồ thị mô hình hóa 1025

thực hiện tìm kiếm theo chiều rộng-ưu tiên. Tìm kiếm trước theo chiều sâu và tìm kiếm theo chiều rộng sẽ

được giới thiệu trong phần tiếp theo. Hình 28.9 minh họa các phương pháp này trong biểu đồ UML.

AbstractGraph không giới thiệu bất kỳ phương thức mới nào. Một danh sách các đỉnh và một danh sách

hiệu ứng tính từ cạnh được định nghĩa trong lớp AbstractGraph . Với những trường dữ liệu này, chỉ cần

«Giao diện» Kiểu chung V là kiểu cho các đỉnh.

Đồ thị <V>

+ getSize (): int + Trả về số đỉnh trong biểu đồ.

getVertices (): Danh sách <V> + Trả về các đỉnh trong đồ thị.

getVertex (index: int): V + getIndex Trả về đối tượng đỉnh cho chỉ số đỉnh đã chỉ định.

(v: V): int + getNeighbors (index: Trả về chỉ số cho đỉnh được chỉ định.

int): List <Integer> + getDegree (index: int): int + printEdges Trả về các lân cận của đỉnh với chỉ số được chỉ định.

(): void + clear (): void + addVertex (v, V): boolean Trả về mức độ cho một chỉ số đỉnh được chỉ định.

In các cạnh.

Xóa biểu đồ.

Trả về true nếu v được thêm vào đồ thị. Trả về false nếu v đã
có trong đồ thị.

+ addEdge (u: int, v: int): boolean Thêm một cạnh từ u đến v vào biểu đồ ném
IllegalArgumentException nếu u hoặc v không hợp lệ. Trả về true nếu cạnh
được thêm vào và false nếu (u, v) đã có trong đồ thị.

+ dfs (v: int): AbstractGraph <V> .Tree + bfs (v: int): Lấy cây tìm kiếm theo chiều sâu bắt đầu từ v.

AbstractGraph <V> .Tree Lấy cây tìm kiếm theo chiều rộng bắt đầu từ v.

AbstractGraph <V>

#vertices: Danh sách <V> Các đỉnh trong biểu đồ.


#neighbors: Danh sách <Danh sách <Hàng rào>> Các lân cận cho mỗi đỉnh trong đồ thị.

#AbstractGraph () Xây dựng một đồ thị rỗng.

#AbstractGraph (đỉnh: V [], cạnh: int [] []) Xây dựng một đồ thị với các cạnh và đỉnh được chỉ định được

lưu trữ trong mảng.

#AbstractGraph (đỉnh: Danh sách <V>, cạnh: Xây dựng một đồ thị với các cạnh và đỉnh được chỉ định

Danh sách <Edge>) được lưu trữ trong danh sách.

#AbstractGraph (các cạnh: int [] [], Xây dựng một đồ thị với các cạnh được chỉ định trong một mảng

numberOfVertices: int) và các đỉnh nguyên 1, 2,….

#AbstractGraph (các cạnh: Danh sách <Edge>, Xây dựng một đồ thị với các cạnh được chỉ định trong một danh sách và

numberOfVertices: int) + addEdge (e: Edge): các đỉnh số nguyên 1, 2,….

boolean
Các lớp bên trong Cây được định nghĩa ở đây Thêm một cạnh vào danh sách cạnh kề.

UnweightedGraph <V>

+ UnweightedGraph () Xây dựng một đồ thị rỗng không có trọng số.

+ UnweightedGraph (đỉnh: V [], cạnh: int [] []) Xây dựng một đồ thị với các cạnh và đỉnh được chỉ định

trong mảng.

+ UnweightedGraph (đỉnh: List <V>, Xây dựng một đồ thị với các cạnh và đỉnh được chỉ định

edge: List <Edge>) được lưu trữ trong danh sách.

+ UnweightedGraph (các cạnh: List <Edge>, Xây dựng một đồ thị với các cạnh được chỉ định trong một mảng

numberOfVertices: int) và các đỉnh nguyên 1, 2,….

+ UnweightedGraph (các cạnh: int [] [], Xây dựng một đồ thị với các cạnh được chỉ định trong một danh sách và

numberOfVertices: int) các đỉnh số nguyên 1, 2,….

HÌNH 28.9 Giao diện đồ thị xác định các hoạt động chung cho tất cả các loại đồ thị.
Machine Translated by Google

1026 Chương 28 Đồ thị và Ứng dụng

thực hiện tất cả các phương thức được xác định trong giao diện Đồ thị . Để thuận tiện, chúng ta giả sử đồ

thị là một đồ thị đơn giản, tức là, một đỉnh không có cạnh với chính nó và không có cạnh song song nào từ
đỉnh u đến v.

AbstractGraph triển khai tất cả các phương thức từ Graph và nó không giới thiệu bất kỳ phương thức mới

nào ngoại trừ một phương thức addEdge (cạnh) tiện lợi bổ sung một đối tượng Edge vào danh sách cạnh kề.

UnweightedGraph chỉ cần mở rộng AbstractGraph với năm đoạn mã cấu tạo để tạo các thể hiện Đồ thị cụ thể.

Ghi chú

đỉnh và chỉ số của chúng Bạn có thể tạo một đồ thị với bất kỳ loại đỉnh nào. Mỗi đỉnh được liên kết với một chỉ
số, chỉ số này giống như chỉ số của đỉnh trong danh sách đỉnh. Nếu bạn tạo một biểu
đồ mà không chỉ định các đỉnh, các đỉnh sẽ giống như các chỉ số của chúng.

Ghi chú

tại sao AbstractGraph? Lớp AbstractGraph triển khai tất cả các phương thức trong giao diện Đồ thị . Vậy tại
sao nó lại được định nghĩa là trừu tượng? Trong tương lai, bạn có thể cần thêm các
phương thức mới vào giao diện Đồ thị mà không thể thực hiện được trong AbstractGraph.
Để làm cho các lớp dễ bảo trì, bạn nên định nghĩa lớp AbstractGraph là lớp trừu tượng.

Giả sử rằng tất cả các giao diện và lớp này đều có sẵn. Liệt kê 28.1 đưa ra một chương trình thử nghiệm

tạo đồ thị trong Hình 28.1 và một đồ thị khác cho đồ thị trong Hình 28.3a.

DANH SÁCH 28.1 TestGraph.java


1 lớp công khai TestGraph {
public static void main (String [] args) {
đỉnh 2 3 String [] vertices = {"Seattle", "San Francisco", "Los Angeles",
4 "Denver", "Kansas City", "Chicago", "Boston", "New York",
5 "Atlanta", "Miami", "Dallas", "Houston"};
6
// Mảng cạnh cho đồ thị trong Hình 28.1
các cạnh int [] [] edge = {
{0, 1}, {0, 3}, {0, 5},
7 {1, 0}, {1, 2}, {1, 3},
8 {2, 1}, {2, 3}, {2, 4}, {2, 10},
9 {3, 0}, {3, 1}, {3, 2}, {3, 4}, {3, 5},
10 11 12 13 {4, 2}, {4, 3}, {4, 5}, {4, 7}, {4, 8}, {4, 10},
14 {5, 0}, {5, 3}, {5, 4}, {5, 6}, {5, 7},
15 {6, 5}, {6, 7},
16 {7, 4}, {7, 5}, {7, 6}, {7, 8},
17 {8, 4}, {8, 7}, {8, 9}, {8, 10}, {8, 11},
18 {9, 8}, {9, 11},
19 {10, 2}, {10, 4}, {10, 8}, {10, 11},
20 {11, 8}, {11, 9}, {11, 10}
21 };
22
tạo một đồ thị 23 Graph <String> graph1 = new UnweightedGraph <> (đỉnh, cạnh);
24 System.out.println (" Số đỉnh trong graph1:"
số đỉnh 25 + graph1.getSize ());
26 System.out.println ("Đỉnh có chỉ số 1 là"
lấy đỉnh 27 + graph1.getVertex (1));
28 System.out.println (" Chỉ số cho Miami là" graph1.getIndex+
lấy chỉ mục 29 ("Miami"));
30 System.out.println ("Các cạnh của graph1:");
in cạnh 31 graph1.printEdges ();
32

33 // Danh sách các đối tượng Edge cho đồ thị trong Hình 28.3a
34 String [] names = {"Peter", "Jane", "Mark", "Cindy", "Wendy"};
Machine Translated by Google

28.4 Đồ thị mô hình hóa 1027

35 java.util.ArrayList <AbstractGraph.Edge> edgeList


36 = new java.util.ArrayList <> ();
37 edgeList.add (mới AbstractGraph.Edge (0, 2)); danh sách các đối tượng Edge

38 edgeList.add (mới AbstractGraph.Edge (1, 2));


39 edgeList.add (mới AbstractGraph.Edge (2, 4));
40 edgeList.add (mới AbstractGraph.Edge (3, 4));
41 // Tạo một đồ thị có 5 đỉnh
42 Graph <String> graph2 = new UnweightedGraph <> tạo một đồ thị
43 (java.util.Arrays.asList (tên), edgeList);
44 System.out.println ("\ nSố đỉnh trong graph2:"
45 + graph2.getSize ());
46 System.out.println ("Các cạnh của graph2:");
47 graph2.printEdges (); in cạnh
48 }
49}

Số đỉnh trong đồ thị 1: 12


Đỉnh có chỉ số 1 là San Francisco
Chỉ số cho Miami là 9
Các cạnh cho đồ thị1:
Seattle (0): (0, 1) (0, 3) (0, 5)
San Francisco (1): (1, 0) (1, 2) (1, 3)
Los Angeles (2): (2, 1) (2, 3) (2, 4) (2, 10)
Denver (3): (3, 0) (3, 1) (3, 2) (3, 4) (3, 5)
Thành phố Kansas (4): (4, 2) (4, 3) (4, 5) (4, 7) (4, 8) (4, 10)
Chicago (5): (5, 0) (5, 3) (5, 4) (5, 6) (5, 7)
Boston (6): (6, 5) (6, 7)
New York (7): (7, 4) (7, 5) (7, 6) (7, 8)
Atlanta (8): (8, 4) (8, 7) (8, 9) (8, 10) (8, 11)
Miami (9): (9, 8) (9, 11)
Dallas (10): (10, 2) (10, 4) (10, 8) (10, 11)
Houston (11): (11, 8) (11, 9) (11, 10)

Số đỉnh trong đồ thị2: 5


Các cạnh cho đồ thị2:
Peter (0): (0, 2)
Jane (1): (1, 2)
Dấu (2): (2, 4)
Cindy (3): (3, 4)
Wendy (4):

Chương trình tạo graph1 cho đồ thị trong Hình 28.1 trong các dòng 3–23. Các đỉnh cho đồ thị1
được xác định trong các dòng 3–5. Các cạnh của đồ thị1 được xác định trong 8–21. Các cạnh được
biểu diễn bằng mảng hai chiều. Đối với mỗi hàng i trong mảng, các cạnh [i] [0] và các cạnh [i] [1]
chỉ ra rằng có một cạnh từ cạnh đỉnh [i] [0] đến cạnh đỉnh [i] [1]. Đối với đề thi, hàng đầu
tiên, {0, 1}, đại diện cho cạnh từ đỉnh 0 (các cạnh [0] [0]) đến đỉnh 1
(các cạnh [0] [1]). Hàng {0, 5} đại diện cho cạnh từ đỉnh 0 (các cạnh [2] [0]) đến đỉnh tex 5
(các cạnh [2] [1]). Biểu đồ được tạo ở dòng 23. Dòng 31 gọi printEdges ()
phương thức trên graph1 để hiển thị tất cả các cạnh trong graph1.

Chương trình tạo graph2 cho đồ thị trong Hình 28.3a trong các dòng 34–43. Các cạnh của graph2
được xác định trong các dòng 37–40. graph2 được tạo bằng cách sử dụng danh sách các đối tượng
Edge ở dòng 43. Dòng 47 gọi phương thức printEdges () trên graph2 để hiển thị tất cả các cạnh
trong graph2.
Lưu ý rằng cả graph1 và graph2 đều chứa các đỉnh của chuỗi. Các đỉnh được gán
với các chỉ số 0, 1,. n-1.
. . ,Chỉ
của số
đỉnh
là Miami
vị trílàcủa
9. đỉnh trong các đỉnh. Ví dụ, chỉ số
Machine Translated by Google

1028 Chương 28 Đồ thị và Ứng dụng

Bây giờ chúng ta chuyển sự chú ý sang việc triển khai giao diện và các lớp. Danh sách
28.2, 28.3 và 28.4 cung cấp giao diện Đồ thị , lớp AbstractGraph và UnweightedGraph
lớp, tương ứng.

DANH SÁCH 28.2 Graph.java


1 giao diện công khai Biểu đồ <V> {
2 / ** Trả về số đỉnh trong biểu đồ * /
getSize 3 public int getSize ();
4
/ ** Trả về các đỉnh trong biểu đồ * /
getVertices 5 public java.util.List <V> getVertices ();
6 7

/ ** Trả về đối tượng cho chỉ mục đỉnh được chỉ định * /
getVertex public V getVertex (int index);
8 9 10

11 / ** Trả về chỉ mục cho đối tượng đỉnh được chỉ định * /
getIndex 12 public int getIndex (V v);
13
14 / ** Trả về các lân cận của đỉnh với chỉ số được chỉ định * /
getNeighbors 15 public java.util.List <Integer> getNeighbors (int index);
16
17 / ** Trả về mức độ cho một đỉnh được chỉ định * /
getDegree 18 public int getDegree (int v);
19
20 / ** In các cạnh * /
printEdges 21 public void printEdges ();
22
23 / ** Xóa biểu đồ * /
thông thoáng 24 khoảng trống công khai clear ();
25
26 / ** Thêm một đỉnh vào biểu đồ * /
addVertex 27 public void addVertex (V vertex);
28
29 / ** Thêm một cạnh vào biểu đồ * /
addEdge 30 public void addEdge (int u, int v);
31
32 / ** Lấy cây tìm kiếm theo chiều sâu bắt đầu từ v * /
dfs 33 public AbstractGraph <V> .Tree dfs (int v);
34

/ ** Lấy cây tìm kiếm theo chiều rộng bắt đầu từ v * /


bfs 35 public AbstractGraph <V> .Tree bfs (int v);
36 37}

DANH SÁCH 28.3 AbstractGraph.java


1 nhập java.util. *;
2
3 lớp trừu tượng công khai AbstractGraph <V> triển khai Graph <V> {
4 Danh sách bảo vệ <V> vertices = new ArrayList <> (); // Lưu trữ các đỉnh
Danh sách được bảo vệ <Danh sách <Edge>> hàng xóm
5 = new ArrayList <> (); // Danh sách gần kề
6 7

/ ** Xây dựng một đồ thị rỗng * /


phương thức khởi tạo no-arg được bảo vệ AbstractGraph () {
8 }
9 10 11

12 / ** Xây dựng đồ thị từ các đỉnh và cạnh được lưu trữ trong mảng * /
constructor 13 bảo vệ AbstractGraph (V [] đỉnh, int [] [] cạnh) {
Machine Translated by Google

28.4 Đồ thị mô hình hóa 1029

14 for (int i = 0; i <vertices.length; i ++)


15 addVertex (đỉnh [i]);
16
17 createAdjacencyLists (cạnh, đỉnh.length);
18 }
19
20 / ** Xây dựng một đồ thị từ các đỉnh và các cạnh được lưu trữ trong Danh sách * /
21 bảo vệ AbstractGraph (Danh sách các đỉnh <V>, các cạnh Danh sách <Edge>) { constructor
22 for (int i = 0; i <vertices.size (); i ++)
23 addVertex (vertices.get (i));
24
25 createAdjacencyLists (cạnh, đỉnh.size ());
26 }
27
28 / ** Xây dựng đồ thị cho các đỉnh số nguyên 0, 1, 2 và danh sách cạnh * /
29 AbstractGraph được bảo vệ (Các cạnh của danh sách <Edge>, int numberOfVertices) { constructor
30 for (int i = 0; i <numberOfVertices; i ++) addVertex
31 ((V) (new Integer (i))); // các đỉnh là {0, 1, ...}
32
33 createAdjacencyLists (cạnh, numberOfVertices);
34 }
35
36 / ** Xây dựng một đồ thị từ các đỉnh số nguyên 0, 1 và mảng cạnh * /
37 AbstractGraph được bảo vệ (int [] [] edge, int numberOfVertices) { constructor
38 for (int i = 0; i <numberOfVertices; i ++) addVertex
39 ((V) (new Integer (i))); // các đỉnh là {0, 1, ...}
40
41 createAdjacencyLists (cạnh, numberOfVertices);
42 }
43
44 / ** Tạo danh sách kề cho mỗi đỉnh * /
45 private void createAdjacencyLists (
46 int [] [] edge, int numberOfVertices) {
47 for (int i = 0; i <edge.length; i ++) {
48 addEdge (các cạnh [i] [0], các cạnh [i] [1]);
49 }
50 }
51
52 / ** Tạo danh sách kề cho mỗi đỉnh * /
53 private void createAdjacencyLists (
54 Liệt kê các cạnh <Edge>, int numberOfVertices) {
55 for (Cạnh cạnh: các cạnh) {
56 addEdge (edge.u, edge.v);
57 }
58 }
59
60 @Override / ** Trả về số đỉnh trong biểu đồ * /
61 public int getSize () { getSize
62 return vertices.size ();
63 }
64
65 @Override / ** Trả về các đỉnh trong biểu đồ * /
66 danh sách công khai <V> getVertices () { getVertices
67 trả về đỉnh;
68 }
69
70 @Override / ** Trả về đối tượng cho đỉnh được chỉ định * /
71 public V getVertex (int index) { getVertex
72 return vertices.get (chỉ mục);
73 }
Machine Translated by Google

1030 Chương 28 Đồ thị và Ứng dụng

74
75 @Override / ** Trả về chỉ mục cho đối tượng đỉnh được chỉ định * /
getIndex 76 public int getIndex (V v) {
77 return vertices.indexOf (v);
78 }
79
80 @Override / ** Trả về các vùng lân cận của đỉnh được chỉ định * /
getNeighbors 81 danh sách công khai <Integer> getNeighbors (int index) {
82 List <Integer> result = new ArrayList <> ();
83 for (Edge e: Neighbor.get (index))
84 result.add (ev);
85
86 trả về kết quả;
87 }
88
89 @Override / ** Trả về mức độ cho một đỉnh được chỉ định * /
getDegree 90 public int getDegree (int v) {
91 trả lại hàng xóm.get (v) .size ();
92 }
93
94 @Override / ** In các cạnh * /
printEdges 95 public void printEdges () {
96 for (int u = 0; u <Neighbor.size (); u ++) {
97 System.out.print (getVertex (u) + "(" + u + "): ");
98 for (Edge e: Neighbor.get (u)) {
99 System.out.print ("(" + getVertex (eu) + "," +
100 getVertex (ev) + ") ");
101 }
102 System.out.println ();
103 }
104 }
105
106 @Override / ** Xóa biểu đồ * /
thông thoáng 107 public void clear () {
108 vertices.clear ();
109 hàng xóm.clear ();
110 }
111
112 @Override / ** Thêm một đỉnh vào biểu đồ * /
addVertex 113 public boolean addVertex (V vertex) {
114 if (! vertices.contains (vertex)) {
115 vertices.add (đỉnh);
116 Neighbor.add (new ArrayList <Edge> ());
117 trả về true;
118 }
119 khác {
120 trả về sai;
121 }
122 }
123
124 / ** Thêm một cạnh vào biểu đồ * /
addEdge 125 boolean bảo vệ addEdge (Cạnh e) {
126 if (eu < 0 || eu> getSize () - 1)
127 ném mới IllegalArgumentException ("Không có chỉ mục như vậy:" + eu);
128
129 if (ev < 0 || ev> getSize () - 1)
130 ném mới IllegalArgumentException ("Không có chỉ mục như vậy:" + ev);
131
132 if (! Neighbor.get (eu) .contains (e)) {
133 hàng xóm.get (eu) .add (e);
Machine Translated by Google

28.4 Đồ thị mô hình hóa 1031

134 trả về true;


135 }
136 khác {
137 trả về sai;
138 }
139 }
140
141 @Override / ** Thêm một cạnh vào biểu đồ * /
142 public boolean addEdge (int u, int v) { addEdge bị quá tải
143 return addEdge (new Edge (u, v));
144 }
145
146 / ** Lớp bên trong cạnh bên trong lớp AbstractGraph * /
147 public static class Edge { Cạnh bên trong lớp
148 public int u; // Đỉnh bắt đầu của cạnh
149 công khai int v; // Đỉnh kết thúc của cạnh
150
151 / ** Tạo một cạnh cho (u, v) * /
152 public Edge (int u, int v) {
153 this.u = u;
154 this.v = v;
155 }
156
157 public boolean bằng (Đối tượng o) {
158 return u == ((Cạnh) o) .u && v == ((Cạnh) o) .v;
159 }
160 }
161
162 @Override / ** Lấy cây DFS bắt đầu từ đỉnh v * /
163 / ** Sẽ được thảo luận trong Phần 28.7 * /
164 public Tree dfs (int v) { phương pháp dfs

165 Danh sách <Integer> searchOrder = new ArrayList <> ();


166 int [] parent = new int [vertices.size ()];
167 for (int i = 0; i <parent.length; i ++)
168 cha [i] = -1; // Khởi tạo cha [i] thành -1
169
170 // Đánh dấu các đỉnh đã thăm
171 boolean [] isVisited = new boolean [vertices.size ()];
172
173 // Tìm kiếm đệ quy
174 dfs (v, cha, searchOrder, isVisited);
175
176 // Trả về cây tìm kiếm
177 return new Tree (v, parent, searchOrder);
178 }
179
180 / ** Phương thức đệ quy để tìm kiếm DFS * /
181 private void dfs (int u, int [] parent, List <Integer> searchOrder,
182 boolean [] isVisited) {
183 // Lưu trữ đỉnh đã truy cập
184 searchOrder.add (u);
185 isVisited [u] = true; // Vertex v được truy cập
186
187 for (Edge e: Neighbor.get (u)) {
188 if (! isVisited [ev]) {
189 cha [ev] = u; // Đỉnh mẹ của đỉnh ev là u
190 dfs (ev, parent, searchOrder, isVisited); // Tìm kiếm đệ quy
191 }
192 }
193 }
Machine Translated by Google

1032 Chương 28 Đồ thị và Ứng dụng

194
195 @Override / ** Bắt đầu tìm kiếm bfs từ đỉnh v * /
196 / ** Sẽ được thảo luận trong Phần 28.9 * /
phương pháp bfs 197 bfs cây công cộng (int v ) {
198 Danh sách <Integer> searchOrder = new ArrayList <> ();
199 int [] parent = new int [vertices.size ()];
200 for (int i = 0; i <parent.length; i ++)
201 cha [i] = -1; // Khởi tạo cha [i] thành -1
202
203 java.util.LinkedList <Integer> queue =
204 new java.util.LinkedList <> (); // danh sách được sử dụng như một hàng đợi
205 boolean [] isVisited = new boolean [vertices.size ()];
206 queue.offer (v); // Enqueue v
207 isVisited [v] = true; // Đánh dấu là đã ghé thăm
208
209 while (! queue.isEmpty ()) {
210 int u = queue.poll (); // Dequeue cho u
211 searchOrder.add (u); // bạn đã tìm kiếm
212 for (Edge e: Neighbor.get (u)) {
213 if (! isVisited [ev]) {
214 queue.offer (ev); // Enqueue w
215 cha [ev] = u; // Cha của w là u
216 isVisited [ev] = true; // Đánh dấu là đã ghé thăm
217 }
218 }
219 }
220
221 return new Tree (v, parent, searchOrder);
222 }
223
224 / ** Lớp bên trong cây bên trong lớp AbstractGraph * /
225 / ** Sẽ được thảo luận trong Phần 28.6 * /
Lớp bên trong cây 226 cây lớp công cộng {
227 private int root; // Gốc cây
228 private int [] cha mẹ; // Lưu trữ cha của mỗi đỉnh
229 Danh sách riêng tư <Integer> searchOrder; // Lưu trữ thứ tự tìm kiếm
230
231 / ** Xây dựng một cây với root, cha và searchOrder * /
232 public Tree (int root, int [] parent, List <Integer> searchOrder) {
233 this.root = root;
234 this.parent = cha mẹ;
235 this.searchOrder = searchOrder;
236 }
237
238 / ** Trả lại gốc cây * /
239 public int getRoot () {
240 trả về gốc;
241 }
242
243 / ** Trả về đỉnh cha của đỉnh v * /
244 public int getParent (int v) {
245 return cha [v];
246 }
247
248 / ** Trả về một mảng đại diện cho thứ tự tìm kiếm * /
249 danh sách công khai <Integer> getSearchOrder () {
250 trả về searchOrder;
251 }
252
253 / ** Trả về số đỉnh được tìm thấy * /
254 public int getNumberOfVerticesFound () {
Machine Translated by Google

28.4 Đồ thị mô hình hóa 1033

255 trả về searchOrder.size ();


256 }
257
258 / ** Trả về đường đi của các đỉnh từ một đỉnh đến gốc * /
259 danh sách công khai <V> getPath (int index) {
260 ArrayList <V> path = new ArrayList <> ();
261
262 làm {
263 path.add (vertices.get (index));
264 index = cha [chỉ mục];
265 }
266 while (chỉ số! = -1);
267
268 đường trở lại ;
269 }
270
271 / ** In một đường dẫn từ gốc đến đỉnh v * /
272 public void printPath (int index) {
273 Danh sách <V> path = getPath (index);
" "
274 System.out.print ("Một đường dẫn từ" + vertices.get (root) + đến +
275 vertices.get (index) + ": ");
276 for (int i = path.size () - 1; i> = 0; i--)
277 System.out.print (path.get (i) + " ");
278 }
279
280 / ** In cả cây * /
281 public void printTree () {
282 System.out.println ("Gốc là:" + vertices.get (root));
283 System.out.print ("Các biên: ");
284 for (int i = 0; i <parent.length; i ++) {
285 if (cha [i]! = -1) {
286 // Hiển thị một cạnh
287 System.out.print ("(" + vertices.get (cha [i]) + "," vertices.get +
288 (i) + ") ");
289 }
290 }
291 System.out.println ();
292 }
293 }
294}

LISTING 28.4 UnweightedGraph.java


1 nhập java.util. *;
2
3 lớp công khai UnweightedGraph <V> mở rộng AbstractGraph <V> {
4 / ** Xây dựng một đồ thị rỗng * / phương thức khởi tạo no-arg

5 public UnweightedGraph () {
6 }
7

số 8
/ ** Xây dựng đồ thị từ các đỉnh và cạnh được lưu trữ trong mảng * /
9 public UnweightedGraph (V [] đỉnh, int [] [] edge) { constructor
10 siêu (đỉnh, cạnh);
11 }
12
13 / ** Xây dựng một đồ thị từ các đỉnh và các cạnh được lưu trữ trong Danh sách * /
14 public UnweightedGraph (List <V> đỉnh, List <Edge> cạnh) { constructor
15 siêu (đỉnh, cạnh);
16 }
17
18 / ** Xây dựng đồ thị cho các đỉnh số nguyên 0, 1, 2 và danh sách cạnh * /
Machine Translated by Google

1034 Chương 28 Đồ thị và Ứng dụng

constructor 19 public UnweightedGraph (Danh sách các cạnh <Edge>, int numberOfVertices) {
20 siêu (cạnh, numberOfVertices);
21 }
22

/ ** Xây dựng một đồ thị từ các đỉnh số nguyên 0, 1 và mảng cạnh * /


constructor public UnweightedGraph (int [] [] edge, int numberOfVertices) {
siêu (cạnh, numberOfVertices);
}
23 24 25 26 27}

Mã trong giao diện Đồ thị trong Liệt kê 28.2 và lớp UnweightedGraph trong Liệt kê 28.4 là đơn giản. Hãy để

chúng tôi tìm hiểu mã trong lớp AbstractGraph trong Liệt kê 28.3.

Lớp AbstractGraph xác định các đỉnh của trường dữ liệu (dòng 4) để lưu trữ các đỉnh và hàng xóm (dòng 5) để

lưu trữ các cạnh trong danh sách kề. Neighbor.get (i) lưu trữ tất cả các cạnh tính từ đỉnh đến đỉnh i. Bốn hàm

tạo được nạp chồng được xác định trong các dòng 9–42 để tạo một đồ thị mặc định hoặc một đồ thị từ các mảng

hoặc danh sách các cạnh và đỉnh. Phương thức createAdjacencyLists (int [] [] edge, int numberOfVertices) tạo

danh sách kề từ các cạnh trong một mảng (dòng 45–50). CreateAdjacencyLists ( Danh sách các cạnh <Edge>, int

numberOfVertices)

phương thức tạo danh sách kề từ các cạnh trong danh sách (dòng 53–58).

Phương thức getNeighbors (u) (dòng 81–87) trả về một danh sách các đỉnh kề với đỉnh u. Phương thức clear

() (dòng 106–110) loại bỏ tất cả các đỉnh và cạnh khỏi đồ thị. Phương thức addVertex (u) (dòng 112–122) thêm

một đỉnh mới vào các đỉnh và trả về true. Nó trả về false nếu đỉnh đã nằm trong đồ thị (dòng 120).

Phương thức addEdge (e) (dòng 124–139) thêm một cạnh mới vào danh sách cạnh kề và trả về true. Nó trả về

false nếu cạnh đã có trong đồ thị. Phương thức này có thể ném IllegalArgumentExcepiton nếu cạnh không hợp lệ

(dòng 126–130).

Phương thức printEdges () (dòng 95–104) hiển thị tất cả các đỉnh và cạnh kề với mỗi đỉnh.

Mã trong các dòng 164–293 cung cấp các phương pháp tìm cây tìm kiếm theo chiều sâu và

cây tìm kiếm theo chiều rộng, sẽ được giới thiệu lần lượt trong Phần 28.7 và 28.9.

28.7 Mô tả mối quan hệ giữa Graph, AbstractGraph và UnweightedGraph.


Kiểm tra điểm 28.8 Đối với mã trong Liệt kê 28.1, TestGraph.java, graph1.getIndex ("Seattle") là gì?
Graph1.getDegree (5) là gì ? Graph1.getVertex (4) là gì ?

28.5 Hình ảnh hóa đồ thị


Để hiển thị một biểu đồ một cách trực quan, mỗi đỉnh phải được chỉ định một vị trí.
Chìa khóa

Điểm
Phần trước đã giới thiệu cách lập mô hình biểu đồ bằng giao diện Graph , lớp AbstractGraph và lớp

UnweightedGraph . Phần này thảo luận về cách hiển thị biểu đồ bằng đồ thị. Để hiển thị một đồ thị, bạn cần biết

vị trí của mỗi đỉnh được chơi và tên của mỗi đỉnh. Để đảm bảo một đồ thị có thể được hiển thị, chúng tôi xác

định một mặt liên có tên là Displayable có các phương thức lấy tọa độ x và y và tên của chúng, đồng thời tạo

các thể hiện đỉnh của Displayable, trong Liệt kê 28.5.

LISTING 28.5 Displayable.java


Giao diện có thể hiển thị 1 giao diện công cộng Có thể hiển thị {
2 public int getX (); // Lấy tọa độ x của đỉnh
public int getY (); // Lấy tọa độ y của đỉnh
3 4 public String getName (); // Lấy tên hiển thị của đỉnh
5}

Một biểu đồ với các đỉnh Có thể hiển thị bây giờ có thể được hiển thị trên một ngăn có tên GraphView , như

được hiển thị trong Liệt kê 28.6.


Machine Translated by Google

28.5 Hình ảnh hóa đồ thị 1035

DANH SÁCH 28.6 GraphView.java


1 nhập javafx.scene.layout.Pane;
2 nhập javafx.scene.shape.Circle;
3 nhập javafx.scene.shape.Line;
4 nhập javafx.scene.text.Text;
5
6 lớp công khai GraphView mở rộng ngăn { mở rộng ngăn
7 đồ thị riêng <? mở rộng Đồ thị có thể hiển thị>; Các đỉnh có thể hiển thị
số 8

public GraphView (Graph <? expandable Displayable > graph) {


9 this.graph = graph;
10 11
12 // Vẽ các đỉnh
13 java.util.List <? mở rộng Displayable> vertices =
14 graph.getVertices ();
15 for (int i = 0; i <graph.getSize (); i ++) {
16 int x = vertices.get (i) .getX ();
17 int y = vertices.get (i) .getY ();
18 Tên chuỗi = vertices.get (i) .getName ();
19
20 getChildren (). add (new Circle (x, y, 16)); // Hiển thị một đỉnh hiển thị một đỉnh
21 getChildren (). add (new Text (x - 8, y - 18, name)); hiển thị một văn bản

22 }
23
24 // Vẽ các cạnh cho các cặp đỉnh
25 for (int i = 0; i <graph.getSize (); i ++) {
26 java.util.List <Integer> hàng xóm = graph.getNeighbors (i);
27 int x1 = graph.getVertex (i) .getX ();
28 int y1 = graph.getVertex (i) .getY ();
29 cho (int v: hàng xóm) {
30 int x2 = graph.getVertex (v) .getX ();
31 int y2 = graph.getVertex (v) .getY ();
32

// Vẽ một cạnh cho (i, v)


33 getChildren (). add (new Line (x1, y1, x2, y2)); vẽ một cạnh
34 }
35 }
36 }
37 38}

Để hiển thị một biểu đồ trên một ngăn, chỉ cần tạo một phiên bản của GraphView bằng
cách chuyển biểu đồ làm đối số trong hàm tạo (dòng 9). Lớp cho đỉnh của đồ thị phải bao
hàm giao diện Có thể hiển thị để hiển thị các đỉnh (dòng 13–22) . Đối với mỗi chỉ số đỉnh
i, việc gọi graph.getNeighbors (i) trả về danh sách kề của nó (dòng 26). Từ danh sách
này, bạn có thể tìm thấy tất cả các đỉnh kề với i và vẽ một đường để nối i với đỉnh liền
kề của nó (dòng 27–34).
Liệt kê 28.7 đưa ra một ví dụ về việc hiển thị đồ thị trong Hình 28.1, như trong Hình
28.10.

LISTING 28.7 DisplayUSMap.java


1 nhập javafx.application.Application;
2 nhập javafx.scene.Scene;
3 nhập javafx.stage.Stage;
4
5 lớp công khai DisplayUSMap mở rộng Ứng dụng {
6 @Override // Ghi đè phương thức bắt đầu trong lớp Ứng dụng
7 public void start (Giai đoạn chínhStage) {
số 8
City [] vertices = {new City ("Seattle", 75, 50),
Machine Translated by Google

1036 Chương 28 Đồ thị và Ứng dụng

Thành phố mới ("San Francisco", 50, 210),


9 Thành phố mới ("Los Angeles", 75, 275), Thành phố mới ("Denver", 275, 175),
10 Thành phố mới ("Thành phố Kansas", 400, 245),
11 Thành phố mới ("Chicago", 450, 100), Thành phố mới ("Boston", 700, 80),
12 Thành phố mới ("New York", 675, 120), Thành phố mới ("Atlanta", 575, 295),
13 Thành phố mới ("Miami", 600, 400), Thành phố mới ("Dallas", 408, 325),
14 Thành phố mới ("Houston", 450, 360) };
15 16
17 // Mảng cạnh cho đồ thị trong Hình 28.1
18 int [] [] edge = {
19 {0, 1}, {0, 3}, {0, 5}, {1, 0}, {1, 2}, {1, 3},
20 {2, 1}, {2, 3}, {2, 4}, {2, 10},
21 {3, 0}, {3, 1}, {3, 2}, {3, 4}, {3, 5},
22 {4, 2}, {4, 3}, {4, 5}, {4, 7}, {4, 8}, {4, 10},
23 {5, 0}, {5, 3}, {5, 4}, {5, 6}, {5, 7},
24 {6, 5}, {6, 7}, {7, 4}, {7, 5}, {7, 6}, {7, 8},
25 {8, 4}, {8, 7}, {8, 9}, {8, 10}, {8, 11},
26 {9, 8}, {9, 11}, {10, 2}, {10, 4}, {10, 8}, {10, 11},
27 {11, 8}, {11, 9}, {11, 10}
28 };
29

tạo một đồ thị 30 Graph <City> graph = new UnweightedGraph <> (đỉnh, cạnh);
31
32 // Tạo một cảnh và đặt nó vào vùng hiển thị
tạo một GraphView 33 Cảnh cảnh = Cảnh mới ( GraphView mới (đồ thị), 750, 450);
34 primaryStage.setTitle ("Bản đồ hiển thị"); // Đặt tiêu đề sân khấu
35 primaryStage.setScene (cảnh); // Đặt cảnh vào sân khấu
36 primaryStage.show (); // Hiển thị sân khấu
37 }
38

Đẳng cấp thành phố 39 Thành phố lớp tĩnh triển khai Có thể hiển thị {
40 int riêng x, y;
41 tên chuỗi riêng ;
42
43 Thành phố (Tên chuỗi, int x, int y) {
44 this.name = tên;
45 this.x = x;
46 this.y = y;
47 }
48
49 @Override
50 public int getX () {
51 trả về x;
52 }
53
54 @Override
55 public int getY () {
56 trả lại y;
57 }
58
59 @Override
60 public String getName () {
61 trả lại tên;
62 }
63 }
64}

Thành phố loại được định nghĩa để lập mô hình các đỉnh với tọa độ và tên của chúng (dòng 39–63).
Chương trình tạo một đồ thị với các đỉnh của loại Thành phố (dòng 30). Vì City triển khai
Displayable, một đối tượng GraphView được tạo cho biểu đồ sẽ hiển thị biểu đồ trong ngăn (dòng 33).
Như một bài tập để làm quen với các lớp và giao diện đồ thị, hãy thêm một thành phố (ví dụ:
Savannah) với các cạnh thích hợp vào biểu đồ.
Machine Translated by Google

28,6 Đường truyền đồ thị 1037

HÌNH 28.10 Biểu đồ được hiển thị trong ngăn.

28.9 Liệu Liệt kê 28.7 DisplayUSMap.java có hoạt động không, nếu mã ở dòng 30–34 trong
Liệt kê 28.6 GraphView.java được thay thế bằng mã sau? Kiểm tra điểm

if (i <v) {
int x2 = graph.getVertex (v) .getX ();
int y2 = graph.getVertex (v) .getY ();

// Vẽ một cạnh cho (i, v)


getChildren (). add (new Line (x1, y1, x2, y2));

28.10 Đối với đối tượng graph1 được tạo trong Liệt kê 28.1, TestGraph.java, bạn có thể tạo một đối tượng GraphView

như sau không?

GraphView view = new GraphView (graph1);

28,6 Truyền tải đồ thị


Trước hết theo chiều sâu và theo chiều rộng là hai cách phổ biến để duyệt qua một biểu đồ. Chìa khóa

Điểm

Duyệt đồ thị là quá trình truy cập mỗi đỉnh trong đồ thị chính xác một lần. Có hai cách phổ biến
tìm kiếm theo chiều sâu
để duyệt qua biểu đồ: duyệt theo chiều sâu trước tiên (hoặc tìm kiếm theo chiều sâu) và duyệt
tìm kiếm theo chiều rộng-đầu tiên

theo chiều rộng trước tiên (hoặc tìm kiếm theo chiều rộng). Cả hai đường truyền đều dẫn đến một
cây bao trùm, cây này có thể được mô hình hóa bằng cách sử dụng một lớp, như thể hiện trong Hình
28.11. Lưu ý rằng Tree là một lớp bên trong được định nghĩa trong lớp AbstractGraph . AbstractGraph
<V> .Tree khác với giao diện Cây được định nghĩa trong Phần 25.2.5. AbstractGraph.Tree là một lớp
chuyên biệt được thiết kế để mô tả mối quan hệ cha-con của các nút, trong khi giao diện Cây xác
định các toán hạng phổ biến như tìm kiếm, chèn và xóa trong cây. Vì không cần thực hiện các thao
tác này đối với cây bao trùm, nên AbstractGraph <V> .Tree không được định nghĩa là một kiểu con của Cây.
Lớp Tree được định nghĩa là một lớp bên trong trong lớp AbstractGraph trong các dòng 226–293

trong Liệt kê 28.3. Hàm tạo tạo một cây với gốc, các cạnh và một thứ tự tìm kiếm.

Lớp Tree định nghĩa bảy phương thức. Phương thức getRoot () trả về gốc của cây.
Bạn có thể lấy thứ tự của các đỉnh được tìm kiếm bằng cách gọi getSearchOrder ()
phương pháp. Bạn có thể gọi getParent (v) để tìm đỉnh cha của đỉnh v trong tìm kiếm. Invok ing

getNumberOfVerticesFound () trả về số đỉnh được tìm kiếm. Phương thức getPath (index) trả về một danh sách các

đỉnh từ chỉ mục đỉnh được chỉ định đến gốc. Invok ing printPath (v) hiển thị một đường dẫn từ gốc đến v. Bạn có thể

hiển thị tất cả các cạnh trong cây bằng cách sử dụng phương thức printTree () .
Machine Translated by Google

1038 Chương 28 Đồ thị và Ứng dụng

AbstractGraph <V> .Tree

-root: int Gốc của cây.

-parent: int [] Cha mẹ của các đỉnh.


-searchOrder: List <Integer> Các lệnh đi ngang qua các đỉnh.

+ Tree (root: int, parent: int [], Tạo cây với gốc, gốc, và
searchOrder: List <Integer>) + searchOrder.

getRoot (): int + getSearchOrder (): Trả về gốc của cây.

Trả về thứ tự của các đỉnh được tìm kiếm.


List <Integer> + getParent (index: int):
int + getNumberOfVerticesFound (): int + Trả về giá trị gốc cho chỉ mục đỉnh đã chỉ định.
Trả về số đỉnh được tìm kiếm.
getPath (index: int): Danh sách <V>
Trả về danh sách các đỉnh từ chỉ mục đỉnh được chỉ
định đến gốc.

+ printPath (index: int): void + Hiển thị một đường dẫn từ gốc đến đỉnh được chỉ định.

printTree (): void Hiển thị cây với gốc và tất cả các cạnh.

HÌNH 28.11 Lớp Tree mô tả các nút có quan hệ cha - con.

Phần 28.7 và 28.9 sẽ giới thiệu tìm kiếm theo chiều sâu và tìm kiếm theo chiều rộng, tương ứng. Cả hai

tìm kiếm sẽ dẫn đến một thể hiện của lớp Tree .

28.11 AbstractGraph <V> .ree có triển khai giao diện Cây được định nghĩa trong Liệt kê 25.3 không

Kiểm tra điểm Tree.java?

28.12 Bạn sử dụng phương pháp nào để tìm đỉnh cha của đỉnh trong cây?

28,7 Độ sâu-Tìm kiếm Đầu tiên (DFS)


Tìm kiếm theo chiều sâu của một biểu đồ bắt đầu từ một đỉnh trong biểu đồ và truy cập tất cả
Chìa khóa

Điểm các đỉnh trong biểu đồ càng xa càng tốt trước khi theo dõi ngược lại.

Tìm kiếm theo chiều sâu của một biểu đồ giống như tìm kiếm theo chiều sâu của một cây được thảo luận trong Phần

25.2.4, Traversal cây. Trong trường hợp của một cái cây, việc tìm kiếm bắt đầu từ gốc. Trong biểu đồ, việc tìm kiếm

có thể bắt đầu từ bất kỳ đỉnh nào.

Tìm kiếm theo chiều sâu của cây trước tiên sẽ truy cập vào gốc, sau đó truy cập đệ quy vào các cây con

của gốc. Tương tự, tìm kiếm theo chiều sâu của một biểu đồ trước tiên sẽ truy cập một đỉnh, sau đó nó sẽ

truy cập đệ quy tất cả các đỉnh lân cận với đỉnh đó. Sự khác biệt là đồ thị có thể chứa các chu kỳ, điều này

có thể dẫn đến một đệ quy vô hạn. Để tránh vấn đề này, bạn cần theo dõi các đỉnh đã được thăm.

Tìm kiếm được gọi là tìm kiếm theo chiều sâu vì nó tìm kiếm “sâu hơn” trong biểu đồ nhiều nhất có thể.

Tìm kiếm bắt đầu từ một số đỉnh v. Sau khi truy cập v, nó sẽ đến thăm một người hàng xóm không được mong

đợi của v. Nếu v không có người hàng xóm không được mong đợi, tìm kiếm sẽ lùi lại đỉnh mà nó đạt đến v.

Chúng tôi giả định rằng đồ thị được kết nối và quá trình tìm kiếm bắt đầu từ đỉnh nào có thể đạt tới tất cả

các đỉnh. Nếu không đúng như vậy, hãy xem Bài tập lập trình 28.4 để tìm các thành phần được kết nối trong biểu đồ.

28.7.1 Thuật toán tìm kiếm theo độ sâu


Thuật toán tìm kiếm theo độ sâu được mô tả trong Liệt kê 28.8.

LISTING 28.8 Thuật toán tìm kiếm theo độ sâu-đầu tiên


Đầu vào: G = (V, E) và một đỉnh bắt đầu v
Đầu ra: một cây DFS bắt nguồn từ v

1 Cây dfs (đỉnh v) {2 visit v;


thăm v
Machine Translated by Google

28,7 Độ sâu-Tìm kiếm Đầu tiên (DFS) 1039

cho mỗi người hàng xóm w của v


nếu (w chưa được ghé thăm) { kiểm tra một người hàng xóm

đặt v làm cha cho w trong cây;


dfs (w); tìm kiếm đệ quy

}
3 4 5 6 7 8}

Bạn có thể sử dụng một mảng có tên isVisited để biểu thị liệu một đỉnh đã được thăm hay chưa.

Ban đầu, isVisited [i] là sai đối với mỗi đỉnh i. Khi một đỉnh, chẳng hạn như v, được truy cập,
isVisited [v] được đặt thành true.

Xem xét đồ thị trong hình 28.12a. Giả sử chúng ta bắt đầu tìm kiếm theo chiều sâu từ đỉnh 0. Đầu tiên truy cập

0, sau đó là bất kỳ hàng xóm nào của nó, giả sử 1. Bây giờ 1 được truy cập, như thể hiện trong Hình 28.12b.

Đỉnh 1 có ba láng giềng — 0, 2 và 4. Vì 0 đã được thăm, bạn sẽ đến thăm 2 hoặc 4. Hãy để chúng tôi chọn 2. Bây giờ,

2 được thăm, như trong Hình 28.12c. Đỉnh 2 có ba lân cận: 0, 1 và 3. Vì 0 và 1 đã được thăm, nên chọn 3. 3 hiện

đã được thăm, như thể hiện trong Hình 28.12d. Tại thời điểm này, các đỉnh đã được thăm theo thứ tự sau:

0, 1, 2, 3

Vì tất cả các hàng xóm của 3 đã được thăm, quay ngược về 2. Vì tất cả các đỉnh của 2 đã được thăm, nên quay

ngược của 1. 4 tiếp giáp với 1, nhưng 4 chưa được thăm. Do đó, hãy truy cập 4, như trong Hình 28.12e. Vì tất cả

những người hàng xóm của 4 người đã được đến thăm, hãy quay ngược lại với 1.

Vì tất cả những người hàng xóm của 1 đã được truy cập, lùi về 0. Vì tất cả những người hàng xóm của 0 đã được

đến thăm nên việc tìm kiếm kết thúc.

Vì mỗi cạnh và mỗi đỉnh chỉ được truy cập một lần nên độ phức tạp về thời gian của các dfs Độ phức tạp thời gian DFS

phương thức là O (| E | + | V |), trong đó | E | biểu thị số cạnh và | V | số lượng đỉnh.

0 1 0 1 0 1

2 2 2

3 4 3 4 3 4

(Một) (b) (C)

0 1 0 1

2 2

3 4 3 4

(d) (e)

HÌNH 28.12 Tìm kiếm theo độ sâu lần đầu truy cập một cách đệ quy nút và các nút lân cận của nó.

28.7.2 Triển khai Tìm kiếm theo độ sâu-Đầu tiên


Thuật toán cho DFS trong Liệt kê 28.8 sử dụng đệ quy. Nó là tự nhiên để sử dụng đệ quy để đưa ra nó. Ngoài ra,

bạn có thể sử dụng ngăn xếp (xem Bài tập lập trình 28.3).
Machine Translated by Google

1040 Chương 28 Đồ thị và Ứng dụng

Phương thức dfs (int v) được thực hiện trong các dòng 164–193 trong Liệt kê 28.3. Nó trả về một thể
hiện của lớp Tree với đỉnh v là gốc. Phương thức lưu trữ các đỉnh được tìm kiếm trong

danh sách searchOrder (dòng 165), cha của mỗi đỉnh trong mảng cha (dòng 166) và sử dụng mảng isVisited

để cho biết liệu một đỉnh đã được truy cập hay chưa (dòng 171). Nó gọi phương thức trợ giúp dfs (v,

parent, searchOrder, isVisited) để thực hiện tìm kiếm theo chiều sâu (dòng 174).

Trong phương thức trình trợ giúp đệ quy, tìm kiếm bắt đầu từ đỉnh u. u được thêm vào searchOrder

ở dòng 184 và được đánh dấu là đã thăm (dòng 185). Đối với mỗi hàng xóm không được truy cập của u,

phương thức được gọi đệ quy để thực hiện tìm kiếm theo chiều sâu. Khi một đỉnh ev được truy cập, đỉnh
ev được lưu trữ trong cha [ev] (dòng 189). Phương thức trả về khi tất cả các đỉnh được truy cập cho

một đồ thị được kết nối hoặc trong một thành phần được kết nối.

Liệt kê 28.9 đưa ra một chương trình thử nghiệm hiển thị DFS cho đồ thị trong Hình 28.1
bắt đầu từ Chicago. Hình minh họa đồ họa của DFS bắt đầu từ Chicago được thể hiện trong Hình
28.13. Để có bản demo GUI tương tác của DFS, hãy truy cập www.cs.armstrong.edu/liang/animation/
Tìm kiếm Bản đồ Hoa Kỳ USMapSearch.html.

DANH SÁCH 28.9 TestDFS.java


1 lớp công khai TestDFS {
2 public static void main (String [] args) {
đỉnh 3 String [] vertices = {"Seattle", "San Francisco", "Los Angeles",
4 "Denver", "Kansas City", "Chicago", "Boston", "New York",
5 "Atlanta", "Miami", "Dallas", "Houston"};
6

các cạnh 7 int [] [] edge = {


8 {0, 1}, {0, 3}, {0, 5},
9 {1, 0}, {1, 2}, {1, 3},
10 {2, 1}, {2, 3}, {2, 4}, {2, 10},
11 {3, 0}, {3, 1}, {3, 2}, {3, 4}, {3, 5},
12 {4, 2}, {4, 3}, {4, 5}, {4, 7}, {4, 8}, {4, 10},
13 {5, 0}, {5, 3}, {5, 4}, {5, 6}, {5, 7},
14 {6, 5}, {6, 7},
15 {7, 4}, {7, 5}, {7, 6}, {7, 8},
16 {8, 4}, {8, 7}, {8, 9}, {8, 10}, {8, 11},
17 {9, 8}, {9, 11},
18 {10, 2}, {10, 4}, {10, 8}, {10, 11},
19 {11, 8}, {11, 9}, {11, 10}
20 };
21

tạo một đồ thị 22 Graph <String> graph = new UnweightedGraph <> (đỉnh, cạnh);
23 AbstractGraph <Chuỗi> .Tree dfs =
lấy DFS 24 graph.dfs (graph.getIndex ("Chicago"));
25

nhận lệnh tìm kiếm 26 java.util.List <Integer> searchOrders = dfs.getSearchOrder ();


27 System.out.println (dfs.getNumberOfVerticesFound () +
28 "các đỉnh được tìm kiếm theo thứ tự DFS này:");
29 for (int i = 0; i <searchOrders.size (); i ++)
30 System.out.print (graph.getVertex (searchOrders.get (i)) + " ");
31 System.out.println ();
32
33 for (int i = 0; i <searchOrders.size (); i ++)
34 if (dfs.getParent (i)! = -1)
35 System.out.println ("cha của" + graph.getVertex (i) +
"là" + graph.getVertex (dfs.getParent (i)));
}
36 37 38}
Machine Translated by Google

28,7 Độ sâu-Tìm kiếm Đầu tiên (DFS) 1041

12 đỉnh được tìm kiếm theo thứ tự DFS này:

Chicago Seattle San Francisco Los Angeles Denver


Thành phố Kansas New York Boston Atlanta Miami Houston Dallas
cha mẹ của Seattle là Chicago
cha mẹ của San Francisco là Seattle
cha mẹ của Los Angeles là San Francisco
cha mẹ của Denver là Los Angeles
cha mẹ của Thành phố Kansas là Denver
cha mẹ của Boston là New York
thành phố New York là thành phố Kansas
cha mẹ của Atlanta là New York
cha mẹ của Miami là Atlanta
cha mẹ của Dallas là Houston
cha mẹ của Houston là Miami

HÌNH 28.13 Một tìm kiếm DFS bắt đầu từ Chicago.

28.7.3 Các ứng dụng của DFS


Tìm kiếm theo chiều sâu có thể được sử dụng để giải quyết nhiều vấn đề, chẳng hạn như sau:

■ Phát hiện xem một đồ thị có được kết nối hay không. Tìm kiếm đồ thị bắt đầu từ bất kỳ đỉnh nào.

Nếu số đỉnh được tìm kiếm bằng với số đỉnh trong đồ thị,
Machine Translated by Google

1042 Chương 28 Đồ thị và Ứng dụng

đồ thị được kết nối. Nếu không, đồ thị không được kết nối. (Xem Bài tập lập trình 28.1.)

■ Phát hiện liệu có đường đi giữa hai đỉnh hay không (xem phần Lập trình
Bài tập 28.5).

■ Tìm đường đi giữa hai đỉnh (xem Bài tập lập trình 28.5).

■ Tìm tất cả các thành phần được kết nối. Thành phần được kết nối là một đồ thị con được
kết nối cực đại trong đó mọi cặp đỉnh được nối với nhau bằng một đường (xem Bài tập
lập trình 28.4).

■ Phát hiện xem có chu trình trong đồ thị hay không (xem Bài tập lập trình 28.6).

■ Tìm một chu trình trong đồ thị (xem Bài tập Lập trình 28.7).

■ Tìm đường đi / chu trình Hamilton. Đường đi Hamilton trong đồ thị là đường đi đến mỗi
đỉnh trong đồ thị đúng một lần. Một chu trình Hamilton thăm mỗi đỉnh trong đồ thị đúng
một lần và quay trở lại đỉnh bắt đầu (xem Bài tập lập trình 28.17).

Sáu vấn đề đầu tiên có thể được giải quyết dễ dàng bằng cách sử dụng phương pháp dfs trong Liệt
kê 28.3. Để tìm đường đi / chu trình Hamilton, bạn phải khám phá tất cả các DFS có thể có để tìm DFS
dẫn đến đường đi dài nhất. Đường đi / chu trình Hamilton có nhiều ứng dụng, bao gồm để giải bài toán
nổi tiếng về Chuyến du hành của Hiệp sĩ, được trình bày trong Phần bổ sung VI.E trên Trang web Đồng
hành.

28.13 Tìm kiếm theo chiều sâu là gì?

Kiểm tra điểm


28.14 Vẽ cây DFS cho đồ thị trong Hình 28.3b bắt đầu từ nút A.

28.15 Vẽ cây DFS cho đồ thị trong Hình 28.1 bắt đầu từ đỉnh Atlanta.

28.16 Kiểu trả về khi gọi dfs (v) là gì?

28.17 Thuật toán tìm kiếm theo độ sâu được mô tả trong Liệt kê 28.8 sử dụng đệ quy. Ngoài ra, bạn có
thể sử dụng một ngăn xếp để triển khai nó, như được hiển thị bên dưới. Chỉ ra lỗi trong
thuật toán này và đưa ra một thuật toán đúng.

// Phiên bản sai


Cây dfs (đỉnh v) {
đẩy v vào ngăn xếp;
đánh dấu v đã thăm;

while (ngăn xếp không trống) {


bật một đỉnh, nói u, từ ngăn xếp
thăm bạn;
cho mỗi người hàng xóm của bạn
nếu (w chưa được ghé thăm)
đẩy w vào ngăn xếp;
}
}

28.8 Nghiên cứu điển hình: Vấn đề về các vòng kết nối
Bài toán các vòng tròn được kết nối là xác định xem tất cả các vòng tròn trong một mặt phẳng hai chiều có
Chìa khóa

Điểm được kết nối hay không. Vấn đề này có thể được giải quyết bằng cách sử dụng phương thức theo chiều sâu.

Thuật toán DFS có nhiều ứng dụng. Phần này áp dụng thuật toán DFS để giải quyết vấn đề các vòng kết
nối.
Trong bài toán vòng tròn được kết nối, bạn xác định xem tất cả các vòng tròn trong mặt phẳng hình tam giác hai

dimen có được kết nối hay không. Nếu tất cả các vòng tròn được nối với nhau, chúng sẽ được sơn dưới dạng các vòng

tròn được tô màu, như trong Hình 28.14a. Nếu không, chúng sẽ không được điền, như trong Hình 28.14b.
Machine Translated by Google

28.8 Nghiên cứu điển hình: Vấn đề về các vòng kết nối 1043

(a) Các vòng kết nối được kết nối (b) Các vòng kết nối không được kết nối

HÌNH 28.14 Bạn có thể áp dụng DFS để xác định xem các vòng tròn có được kết nối hay không.

Chúng tôi sẽ viết một chương trình cho phép người dùng tạo một vòng kết nối bằng cách nhấp chuột vào một vùng

trống hiện không được bao phủ bởi một vòng tròn. Khi các vòng kết nối được thêm vào, các vòng kết nối sẽ được tô

màu lại nếu chúng được kết nối hoặc không được tô bằng cách khác.

Chúng tôi sẽ tạo một biểu đồ để mô hình hóa vấn đề. Mỗi đường tròn là một đỉnh trong đồ thị. Hai vòng tròn được

kết nối nếu chúng trùng nhau. Chúng tôi áp dụng DFS trong biểu đồ và nếu tất cả các đỉnh được tìm thấy trong tìm

kiếm theo chiều sâu, thì biểu đồ được kết nối.

Chương trình được đưa ra trong Liệt kê 28.10.

LISTING 28.10 ConnectedCircles.java


1 nhập javafx.application.Application;
2 nhập javafx.geometry.Point2D;
3 nhập javafx.scene.Node;
4 nhập javafx.scene.Scene;
5 nhập javafx.scene.layout.Pane;
6 nhập javafx.scene.paint.Color;
7 nhập javafx.scene.shape.Circle;
8 nhập javafx.stage.Stage;
9
10 lớp công khai ConnectedCircles mở rộng Ứng dụng {
11 @Override // Ghi đè phương thức bắt đầu trong lớp Ứng dụng
12 public void start (Giai đoạn chínhStage) {
13 // Tạo một cảnh và đặt nó vào vùng hiển thị
14 Cảnh cảnh = Cảnh mới ( CirclePane mới (), 450, 350); tạo một ngăn vòng tròn
15 primaryStage.setTitle ("ConnectedCircles"); // Đặt tiêu đề sân khấu
16 primaryStage.setScene (cảnh); // Đặt cảnh vào sân khấu
17 primaryStage.show (); // Hiển thị sân khấu
18 }
19
20 / ** Ngăn để hiển thị các vòng tròn * /
21 class CirclePane mở rộng Pane { ngăn để hiển thị các vòng kết nối
22 public CirclePane () {
23 this.setOnMouseClicked (e -> { xử lý nhấp chuột

24 if (! isInsideACircle (new Point2D (e.getX (), e.getY ()))) { nó nằm bên trong một vòng tròn khác?

25 // Thêm một vòng kết nối mới


26 getChildren (). add (new Circle (e.getX (), e.getY (), 20)); thêm một vòng kết nối mới

27 colorIfConnected (); màu sắc nếu tất cả được kết nối

28 }
29 });
30 }
Machine Translated by Google

1044 Chương 28 Đồ thị và Ứng dụng

31
32 / ** Trả về true nếu điểm nằm trong vòng tròn hiện có * /
33 private boolean isInsideACircle (Point2D p) {
34 for (Vòng tròn nút: this.getChildren ())
chứa điểm? 35 if (circle.contains (p))
36 trả về true;
37

38 trả về sai;
39 }
40
41 / ** Tô màu tất cả các vòng tròn nếu chúng được kết nối * /
42 private void colorIfConnected () {
43 if (getChildren (). size () == 0)
44 trở lại; // Không có vòng kết nối nào trong ngăn
45
46 // Xây dựng các cạnh
tạo các cạnh 47 java.util.List <AbstractGraph.Edge> edge = new
48 java.util.ArrayList <> ();
49 for (int i = 0; i <getChildren (). size (); i ++)
50 for (int j = i + 1; j <getChildren (). size (); j ++)
51 if (trùng lặp ((Circle) (getChildren (). get (i)),
52 (Circle) (getChildren (). get (j)))) {
53 edge.add (new AbstractGraph.Edge (i, j));
54 edge.add (new AbstractGraph.Edge (j, i));
55 }
56
57 // Tạo một đồ thị với các đường tròn là các đỉnh
tạo một đồ thị 58 Graph <Node> graph = new UnweightedGraph <>
59 ((java.util.List <Node>) getChildren (), các cạnh);
lấy một cây tìm kiếm 60 AbstractGraph <Node> .Tree tree = graph.dfs (0); // cây DFS
kết nối? 61 boolean isAllCirclesConnected = getChildren (). size () == cây
62 .getNumberOfVerticesFound ();
63

64 for (Vòng tròn nút: getChildren ()) {


kết nối 65 if (isAllCirclesConnected) { // Tất cả các vòng kết nối đều được kết nối
66 ((Hình tròn) hình tròn) .setFill (Màu.RED);
67

không kết nối 68 } khác {


69 ((Vòng tròn) hình tròn) .setStroke (Màu.BLACK);
70 ((Hình tròn) hình tròn) .setFill (Màu.WHITE);
71 }
72 }
73 }
74 }
75

hai đường tròn trùng nhau? chồng chéo boolean tĩnh công cộng (Vòng tròn circle1, Circle circle2) {
trả về Point2D mới (circle1.getCenterX (), circle1.getCenterY ()).
khoảng cách (circle2.getCenterX (), circle2.getCenterY ())
<= circle1.getRadius () + circle2.getRadius ();
76 }
77 78 79 80 81}

Lớp JavaFX Circle chứa các trường dữ liệu x, y và bán kính, các trường này chỉ định vị trí tâm và bán

kính của vòng tròn. Nó cũng xác định vùng chứa để kiểm tra nếu một điểm nằm trong vòng tròn. Phương pháp

chồng chéo (dòng 76–80) kiểm tra xem hai vòng tròn có chồng lên nhau hay không.

Khi người dùng nhấp chuột ra bên ngoài bất kỳ vòng kết nối hiện có nào, một vòng kết nối mới sẽ được

tạo ra ở điểm chuột và vòng tròn được thêm vào ngăn (dòng 26).

Để phát hiện xem các vòng tròn có được kết nối hay không, chương trình xây dựng một đồ thị (dòng 46–59).

Các đường tròn là các đỉnh của đồ thị. Các cạnh được xây dựng theo dòng 49–55. Hai vòng tròn
Machine Translated by Google

28,9 Breadth-First Search (BFS) 1045

các đỉnh được nối với nhau nếu chúng trùng nhau (dòng 51). DFS của biểu đồ cho kết quả là một cây (dòng 60).

Hàm getNumberOfVerticesFound () của cây trả về số đỉnh được tìm kiếm. Nếu nó bằng số vòng tròn, tất cả các

vòng tròn được nối với nhau (dòng 61–62).

28.18 Biểu đồ được tạo ra như thế nào cho bài toán các vòng kết nối?

28.19 Khi bạn kích chuột vào bên trong một hình tròn, chương trình có tạo một hình tròn mới không? Kiểm tra điểm

28.20 Làm thế nào để chương trình biết được nếu tất cả các vòng tròn được kết nối với nhau?

28,9 Breadth-First Search (BFS)


Tìm kiếm theo chiều rộng-đầu tiên của một biểu đồ sẽ truy cập các đỉnh theo cấp độ. Mức đầu
Chìa khóa

tiên bao gồm đỉnh bắt đầu. Mỗi cấp tiếp theo bao gồm các đỉnh kề với các đỉnh ở cấp trước đó. Điểm

Đường đi ngang theo chiều rộng-đầu tiên của biểu đồ giống như đường đi ngang qua chiều rộng-đầu tiên của
một cây được thảo luận trong Phần 25.2.4, Truyền qua cây. Với truyền tải đầu tiên theo chiều rộng của một

cây, các nút được truy cập theo cấp độ. Đầu tiên là thăm gốc, sau đó đến tất cả các con của rễ, sau đó là

cháu của rễ, v.v. Tương tự, tìm kiếm theo chiều rộng-đầu tiên của một đồ thị trước tiên sẽ truy cập một

đỉnh, sau đó đến tất cả các đỉnh lân cận của nó, sau đó là tất cả các đỉnh kề với các đỉnh đó, v.v. Để đảm

bảo rằng mỗi đỉnh chỉ được truy cập một lần, nó sẽ bỏ qua một đỉnh nếu nó đã được truy cập.

28.9.1 Thuật toán tìm kiếm đầu tiên theo chiều rộng

Thuật toán cho tìm kiếm theo chiều rộng bắt đầu từ đỉnh v trong biểu đồ được mô tả trong Liệt kê 28.11.

LISTING 28.11 Thuật toán tìm kiếm đầu tiên theo chiều rộng
Đầu vào: G = (V, E) và một đỉnh bắt đầu v
Đầu ra: một cây BFS bắt nguồn từ v

1 cây bfs (đỉnh v) {


2 tạo một hàng đợi trống để lưu trữ các đỉnh được truy cập; 3 thêm v vào hàng tạo một hàng đợi
đợi; đánh dấu v đã thăm; enqueue v

4
5
while (hàng đợi không trống) {
dequeue một đỉnh, ví dụ u, từ hàng đợi; dequeue thành bạn

thêm u vào danh sách các đỉnh được duyệt; cho mỗi bạn đã đi ngang qua

người hàng xóm của bạn kiểm tra một người hàng xóm w

6 nếu bạn chưa được đến thăm { có phải tôi đã đến thăm?

7 thêm w vào hàng đợi; đặt u enqueue w


8 làm cha cho w trong cây;
9 đánh dấu w đã thăm;
10 11 12 13 14 }
}
15 16}

Xét đồ thị trong hình 28.15a. Giả sử bạn bắt đầu tìm kiếm theo chiều rộng từ đỉnh 0. Đầu tiên truy cập

0, sau đó truy cập tất cả các lân cận của nó, 1, 2 và 3, như thể hiện trong Hình 28.15b. Đỉnh 1 có ba lân

cận: 0, 2 và 4. Vì 0 và 2 đã được truy cập, bây giờ bạn sẽ chỉ truy cập 4, như thể hiện trong Hình 28.15c.

Đỉnh 2 có ba hàng xóm, 0, 1 và 3, tất cả đều đã được truy cập. Đỉnh 3 có ba hàng xóm, 0, 2 và 4, tất cả đều

đã được truy cập. Đỉnh 4 có hai hàng xóm, 1 và 3, tất cả đều đã được ghé thăm. Do đó, cuộc tìm kiếm kết

thúc.

Vì mỗi cạnh và mỗi đỉnh chỉ được truy cập một lần nên độ phức tạp về thời gian của phương thức bfs là Độ phức tạp thời gian BFS

O (| E | + | V |), trong đó | E | biểu thị số cạnh và | V | số lượng đỉnh.


Machine Translated by Google

1046 Chương 28 Đồ thị và Ứng dụng

0 1 0 1 0 1

2 2 2

3 4 3 4 3 4

(Một) (b) (C)

HÌNH 28.15 Tìm kiếm theo chiều rộng truy cập một nút, sau đó đến các nút lân cận của nó, sau đó là

các nút lân cận của nó, v.v.

28.9.2 Triển khai Tìm kiếm theo chiều rộng-Đầu tiên


Phương thức bfs (int v) được định nghĩa trong giao diện Đồ thị và được triển khai trong lớp
AbstractGraph trong Liệt kê 28.3 (dòng 197–222). Nó trả về một thể hiện của lớp Tree với đỉnh v
là gốc. Phương thức lưu trữ các đỉnh được tìm kiếm trong danh sách Tìm kiếm

(dòng 198), đỉnh cha của mỗi đỉnh trong mảng cha (dòng 199), sử dụng danh sách được liên kết cho một hàng

đợi (dòng 203–204) và sử dụng mảng isVisited để cho biết một đỉnh đã được truy cập hay chưa (dòng 207) .

Việc tìm kiếm bắt đầu từ đỉnh v. V được thêm vào hàng đợi ở dòng 206 và được đánh dấu là đã thăm (dòng

207). Phương thức bây giờ kiểm tra từng đỉnh u trong hàng đợi (dòng 210) và thêm nó vào searchOrder (dòng

211). Phương thức này thêm từng ev hàng xóm không được truy cập của u vào hàng đợi (dòng 214), đặt cha của

nó thành u (dòng 215) và đánh dấu nó là đã thăm (dòng 216).

Liệt kê 28.12 đưa ra một chương trình thử nghiệm hiển thị BFS cho đồ thị trong Hình 28.1
bắt đầu nhập từ Chicago. Hình minh họa đồ họa của BFS bắt đầu từ Chicago được thể hiện trong
Hình 28.16. Để có bản demo GUI tương tác của BFS, hãy truy cập www.cs.armstrong.edu/liang/animation/
USMapSearch.html.

DANH SÁCH 28.12 TestBFS.java


1 lớp công khai TestBFS {
2 public static void main (String [] args) {
đỉnh String [] vertices = {"Seattle", "San Francisco", "Los Angeles",
3 4 "Denver", "Kansas City", "Chicago", "Boston", "New York",
5 "Atlanta", "Miami", "Dallas", "Houston"};
6
các cạnh int [] [] edge = {
{0, 1}, {0, 3}, {0, 5},
{1, 0}, {1, 2}, {1, 3},
7 {2, 1}, {2, 3}, {2, 4}, {2, 10},
8 {3, 0}, {3, 1}, {3, 2}, {3, 4}, {3, 5},
9 {4, 2}, {4, 3}, {4, 5}, {4, 7}, {4, 8}, {4, 10},
10 {5, 0}, {5, 3}, {5, 4}, {5, 6}, {5, 7},
11 {6, 5}, {6, 7},
12 {7, 4}, {7, 5}, {7, 6}, {7, 8},
13 {8, 4}, {8, 7}, {8, 9}, {8, 10}, {8, 11},
14 {9, 8}, {9, 11},
15 {10, 2}, {10, 4}, {10, 8}, {10, 11},
16 {11, 8}, {11, 9}, {11, 10}
17 };
18 19 20 21

tạo một đồ thị 22 Graph <String> graph = new UnweightedGraph <> (đỉnh, cạnh);
23 AbstractGraph <Chuỗi> .Tree bfs =
tạo một cây BFS 24 graph.bfs (graph.getIndex ("Chicago"));
25
nhận lệnh tìm kiếm 26 java.util.List <Integer> searchOrders = bfs.getSearchOrder ();
Machine Translated by Google

28,9 Breadth-First Search (BFS) 1047

27 System.out.println (bfs.getNumberOfVerticesFound () +
28 "các đỉnh được tìm kiếm theo thứ tự sau:");
29 for (int i = 0; i <searchOrders.size (); i ++)
30 System.out.println (graph.getVertex (searchOrders.get (i)));
31

for (int i = 0; i <searchOrders.size (); i ++)


if (bfs.getParent (i)! = -1)
32 System.out.println ("cha của" "là" + graph.getVertex (i) + +
33 graph.getVertex (bfs.getParent (i)));
34 }
35 36 37}

12 đỉnh được tìm kiếm theo thứ tự sau:

Chicago Seattle Denver Thành phố Kansas Boston New York


San Francisco Los Angeles Atlanta Dallas Miami Houston
cha mẹ của Seattle là Chicago
cha mẹ của San Francisco là Seattle
cha mẹ của Los Angeles là Denver
cha mẹ của Denver là Chicago
cha mẹ của Thành phố Kansas là Chicago
cha mẹ của Boston là Chicago
cha mẹ của New York là Chicago
cha mẹ của Atlanta là thành phố Kansas
cha mẹ của Miami là Atlanta
cha mẹ của Dallas là thành phố Kansas
cha mẹ của Houston là Atlanta

28.9.3 Các ứng dụng của BFS


Nhiều vấn đề được giải quyết bởi DFS cũng có thể được giải quyết bằng cách sử dụng BFS. Cụ thể, BFS
có thể được sử dụng để giải quyết các vấn đề sau:

■ Phát hiện xem một đồ thị có được kết nối hay không. Một đồ thị được kết nối nếu có một đường đi giữa hai

đỉnh bất kỳ trong đồ thị.

■ Phát hiện xem có đường đi giữa hai đỉnh hay không.

■ Tìm đường đi ngắn nhất giữa hai đỉnh. Bạn có thể chứng minh rằng đường dẫn giữa gốc và bất kỳ nút nào

trong cây BFS là đường ngắn nhất giữa nút gốc và nút.

(Xem Điểm Kiểm tra Câu hỏi 28.25.)

■ Tìm tất cả các thành phần được kết nối. Một thành phần được kết nối là một thành phần được kết nối tối đa

đồ thị con trong đó mọi cặp đỉnh được nối với nhau bằng một đường dẫn.

■ Phát hiện xem có chu trình trong đồ thị hay không (xem Bài tập lập trình 28.6).

■ Tìm một chu trình trong đồ thị (xem Bài tập Lập trình 28.7).

■ Kiểm tra xem một biểu đồ có phải là lưỡng phân hay không. (Một đồ thị là lưỡng phân nếu các đỉnh của đồ

thị có thể được chia thành hai tập rời rạc sao cho không có cạnh nào tồn tại giữa các đỉnh trong cùng

một tập.) (Xem Bài tập lập trình 28.8.)

28.21 Kiểu trả về khi gọi bfs (v) là gì?


28.22 Tìm kiếm đầu tiên theo chiều rộng là gì? Kiểm tra điểm

28.23 Vẽ cây BFS cho đồ thị trong Hình 28.3b bắt đầu từ nút A.

28.24 Vẽ cây BFS cho đồ thị trong Hình 28.1 bắt đầu từ đỉnh Atlanta.

28.25 Chứng minh rằng đường dẫn giữa nút gốc và bất kỳ nút nào trong cây BFS là đường
ngắn nhất giữa nút gốc và nút.
Machine Translated by Google

1048 Chương 28 Đồ thị và Ứng dụng

HÌNH 28.16 Tìm kiếm BFS bắt đầu từ Chicago.

28.10 Nghiên cứu điển hình: Vấn đề Cửu vĩ


Bài toán chín đuôi có thể được rút gọn thành bài toán đường đi ngắn nhất.

Vấn đề chín đuôi như sau. Chín đồng xu được đặt trong một ma trận ba nhân ba với một số ngửa và một
số úp xuống. Một động tác hợp pháp là lấy một đồng xu ngửa và đảo ngược nó, cùng với các đồng xu liền
kề với nó (điều này không bao gồm các đồng xu liền kề theo đường chéo).
Nhiệm vụ của bạn là tìm ra số lần di chuyển tối thiểu dẫn đến tất cả các đồng xu đều bị úp xuống. Ví
dụ, bắt đầu với chín đồng tiền như trong Hình 28.17a. Sau khi bạn lật đồng xu thứ hai ở hàng cuối
cùng, chín đồng xu bây giờ giống như trong Hình 28.17b. Sau khi bạn lật đồng xu thứ hai ở hàng đầu
tiên, chín đồng xu đều được úp xuống, như trong Hình 28.17c.

H H H H H H T T T
T T T T H T T T T
H H H T T T T T T

(Một) (b) (C)

HÌNH 28.17 Vấn đề được giải quyết khi tất cả các đồng xu đều úp xuống.

Chúng tôi sẽ viết một chương trình nhắc người dùng nhập trạng thái ban đầu của chín đồng tiền và
hiển thị giải pháp, như được hiển thị trong lần chạy mẫu sau.
Machine Translated by Google

28.10 Nghiên cứu điển hình: Vấn đề Cửu Vĩ 1049

Nhập chín đồng ban đầu Hs và Ts: HHHTTTHHH

Các bước để lật đồng xu là


HHH
TTT
HHH

HHH
THT
TTT

TTT
TTT
TTT

Mỗi trạng thái của chín đồng tiền đại diện cho một nút trong biểu đồ. Ví dụ, ba trạng thái trong Hình 28.17

tương ứng với ba nút trong biểu đồ. Để thuận tiện, chúng tôi sử dụng ma trận 3 * 3 để đại diện cho tất cả

các nút và sử dụng 0 cho đầu và 1 cho đuôi. Vì có chín ô và mỗi ô là 0 hoặc 1, nên có tổng cộng 29 (512) nút,

có nhãn 0, 1,. . . , và 511, như trong Hình 28.18.

0 0 0 0 0 0 0 0 0 0 0 0 1 1 1
0 0 0 0 0 0 0 0 0 0 0 0 . . . . . 1 1 1
0 0 0 0 0 1 0 1 0 0 1 1 1 1 1

0 1 2 3 511

HÌNH 28.18 Có tổng số 512 nút được dán nhãn theo thứ tự này: 0, 1, 2 ,. . . , 511.

Chúng ta gán một cạnh từ nút v đến u nếu có một chuyển động hợp pháp từ u sang v. Hình 28.19 cho thấy một

phần đồ thị. Lưu ý rằng có một cạnh từ 511 đến 47, vì bạn có thể lật một ô trong nút 47 để trở thành nút 511.

Nút cuối cùng trong Hình 28.18 đại diện cho trạng thái của 9 đồng xu úp xuống. Cho thuận tiện,

chúng tôi gọi nút cuối cùng này là nút đích. Do đó, nút đích có nhãn 511. Giả sử trạng thái ban đầu của bài

toán chín đuôi tương ứng với nút s. Vấn đề được rút gọn ở việc tìm đường đi ngắn nhất từ nút s đến nút

đích, tương đương với việc tìm đường ngắn nhất từ nút s đến nút đích trong cây BFS bắt nguồn từ nút đích.

Bây giờ nhiệm vụ là xây dựng một đồ thị bao gồm 512 nút có nhãn 0, 1, 2 ,. . . , 511, và các cạnh giữa các

nút. Khi đồ thị được tạo, hãy lấy một cây BFS bắt nguồn từ nút 511. Từ cây BFS, bạn có thể tìm thấy một đường

đi ngắn nhất từ gốc đến bất kỳ đỉnh nào. Chúng ta sẽ tạo một lớp có tên là NineTailModel, lớp này chứa phương

thức để có được đường đi ngắn nhất từ nút đích đến bất kỳ nút nào khác. Biểu đồ UML của lớp được thể hiện

trong Hình 28.20.

Một cách trực quan, một nút được biểu diễn trong ma trận 3 * 3 với các chữ cái H và T. Trong
chương trình của chúng tôi, chúng tôi sử dụng một mảng đơn chiều gồm chín ký tự để đại diện cho
một nút. Ví dụ, nút cho đỉnh 1 trong Hình 28.18 được biểu diễn là {'H', 'H', 'H', 'H', 'H', 'H',
'H', 'H', 'T' } trong mảng.
Phương thức getEdges () trả về một danh sách các đối tượng Edge .

Phương thức getNode (chỉ mục) trả về nút cho chỉ mục được chỉ định. Ví dụ, getNode (0) trả về nút chứa

chín Hs. getNode (511) trả về nút chứa chín Ts. Phương thức getIndex (nút) trả về chỉ mục của nút.

Lưu ý rằng cây trường dữ liệu được định nghĩa là được bảo vệ để nó có thể được truy cập từ lớp con

WeightedNineTail trong chương tiếp theo.

Phương thức getFlippedNode (char [] node, int position) lật nút ở vị trí được chỉ định và các vị trí

liền kề của nó. Phương thức này trả về chỉ mục của nút mới.
Machine Translated by Google

1050 Chương 28 Đồ thị và Ứng dụng

Vị trí là một giá trị từ 0 đến 8, trỏ đến một đồng xu trong nút, như thể hiện trong hình sau.

0 0 0 HH H HHHTTTHHH Một nút là

1 1 1 T T T một mảng gồm


chín ký tự
0 0 0 H H H Vị trí là 2 ở
đây trong một nút

Ví dụ, đối với nút 56 trong Hình 28.19, lật nó ở vị trí 0, và bạn sẽ nhận được nút 51.
Nếu bạn lật nút 56 ở vị trí 1, bạn sẽ nhận được nút 47.
Phương thức flipACell (char [] node, int row, int column) lật một nút tại hàng và cột
được chỉ định. Ví dụ: nếu bạn lật nút 56 ở hàng 0 và cột 0, nút mới là 408. Nếu bạn lật nút
56 ở hàng 2 và cột 0, nút mới là 30.

511

1 1 1
1 1 1
1 1 1

408 488 240 30 47 51

1 1 0 1 1 1 0 1 1 0 0 0 0 0 0 0 0 0
0 1 1 1 0 1 1 1 0 0 1 1 1 0 1 1 1 0
0 0 0 0 0 0 0 0 0 1 1 0 1 1 1 0 1 1

0 0 0
1 1
0 0 1 0

56

HÌNH 28.19 Nếu nút u trở thành nút v sau khi lật các ô, hãy gán một cạnh từ v đến u.

NineTailModel

#tree: AbstractGraph <Integer> .Tree Một cây bắt nguồn từ nút 511.

+ NineTailModel () + Xây dựng một mô hình cho bài toán chín đuôi và lấy được cây.
getShortestPath (nodeIndex: int): Trả về một đường dẫn từ nút đã chỉ định đến nút gốc. Đường dẫn
Danh sách <Integer> được trả về bao gồm các nhãn nút trong danh sách.

-getEdges (): Trả về danh sách các đối tượng Edge cho biểu đồ.
Danh sách <AbstractGraph.Edge>
+ getNode (index: int): char [] + Trả về một nút bao gồm chín ký tự của Hs và Ts.
getIndex (node: char []): int + Trả về chỉ mục của nút được chỉ định.

getFlippedNode (node: char [], Lật nút tại vị trí được chỉ định và các vị trí liền kề
position: int): int + flipACell của nó và trả về chỉ mục của nút đã lật.
(node: char [], row: int, Lật nút tại hàng và cột được chỉ định.
cột: int): void +
printNode (node: char []): void Hiển thị nút trên bảng điều khiển.

HÌNH 28.20 Lớp NineTailModel lập mô hình bài toán chín đuôi bằng cách sử dụng đồ thị.
Machine Translated by Google

28.10 Nghiên cứu điển hình: Vấn đề Cửu Vĩ 1051

Liệt kê 28.13 hiển thị mã nguồn cho NineTailModel.java.

DANH SÁCH 28.13 NineTailModel.java


1 nhập java.util. *;
2
3 lớp công khai NineTailModel {
4 public cuối cùng static int NUMBER_OF_NODES = 512;
5 cây AbstractGraph <Integer> được bảo vệ . // Định nghĩa cây tuyên bố một cái cây

6
7 / ** Xây dựng mô hình * /
public NineTailModel () {
// Tạo các cạnh
8 Danh sách <AbstractGraph.Edge> edge = getEdges (); tạo các cạnh
9 10 11

12 // Tạo đồ thị
13 UnweightedGraph <Integer> graph = new UnweightedGraph <> ( tạo đồ thị
14 cạnh, NUMBER_OF_NODES);
15
16 // Lấy cây BSF bắt nguồn từ nút đích
17 cây = graph.bfs (511); tạo cây

18 }
19
20 / ** Tạo tất cả các cạnh cho biểu đồ * /
21 Danh sách riêng tư <AbstractGraph.Edge> getEdges () { nhận được các cạnh

22 Danh sách <AbstractGraph.Edge> edge =


23 mới ArrayList <> (); // Lưu trữ các cạnh
24
25 for (int u = 0; u <NUMBER_OF_NODES; u ++) {
26 for (int k = 0; k < 9; k ++) {
27 char [] node = getNode (u); // Lấy nút cho đỉnh u
28 if (nút [k] == 'H') {
29 int v = getFlippedNode (nút, k);
30 // Thêm cạnh (v, u) để di chuyển hợp pháp từ nút u sang nút v
31 edge.add (new AbstractGraph.Edge (v, u)); thêm một cạnh
32 }
33 }
34 }
35

36 trả lại các cạnh;


37 }
38
39 public static int getFlippedNode (char [] node, int position) { lật ô
40 int row = position / 3;
41 int cột = vị trí% 3;
42
cột
43 flipACell (nút, hàng, cột);
44 flipACell (nút, hàng - 1, cột);
45 flipACell (nút, hàng + 1, cột);
Lật
46 flipACell (nút, hàng, cột - 1); chèo thuyền

47 flipACell (nút, hàng, cột + 1);


48
49 trả về getIndex (nút);
50 }
51
52 public static void flipACell (char [] node, int row, int column) { lật một ô
53 if (row> = 0 && row <= 2 && column> = 0 && column <= 2) {
54 // Trong ranh giới
55 if (nút [hàng * 3 + cột] == 'H')
56 nút [row * 3 + column] = 'T'; // Lật từ H sang T
Machine Translated by Google

1052 Chương 28 Đồ thị và Ứng dụng

57 khác
58 nút [hàng * 3 + cột] = 'H'; // Lật từ T sang H
59 }
60 }
61

lấy chỉ mục cho một nút 62 public static int getIndex (char [] node) { Ví dụ:
63 int kết quả = 0; chỉ số: 3
64
nút: HHHHHHHHTT
65 for (int i = 0; i < 9; i ++) if
66 (node [i] == 'T') HHH
67 result = kết quả * 2 + 1;
HHH
68 khác
69
HTT
result = kết quả * 2 + 0;
70
71 trả về kết quả;
72 }
73
lấy nút cho một chỉ mục 74 public static char [] getNode (int index) { Ví dụ:
75 char [] result = new char [9]; nút: THHHHHHHTT
76
chỉ số: 259
77 for (int i = 0; i < 9; i ++) {
78 int digit = chỉ số% 2; THH
79 nếu (chữ số == 0) HHH
80 result [8 - i] = 'H'; HTT
81 khác
82 result [8 - i] = 'T';
83 index = chỉ số / 2;
84 }
85
86 trả về kết quả;
87 }
88

con đường ngắn nhất


89 danh sách công khai <Integer> getShortestPath (int nodeIndex) {
90 trả về tree.getPath (nodeIndex); Ví dụ:
91 }
nút: THHHHHHHTT
92

hiển thị một nút 93 public static void printNode (char [] node) { Đầu ra:
94 for (int i = 0; i < 9; i ++) THH
95 nếu (i% 3 ! = 2) HHH
96 System.out.print (nút [i]);
HTT
97 khác
98 System.out.println (nút [i]);
99
100 System.out.println ();
101 }
102}

Hàm tạo (dòng 8–18) tạo một đồ thị với 512 nút và mỗi cạnh tương ứng với việc di
chuyển từ nút này sang nút kia (dòng 10). Từ biểu đồ, cây BFS bắt nguồn từ nút đích 511
sẽ thu được (dòng 17).
Để tạo các cạnh, phương thức getEdges (dòng 21–37) kiểm tra từng nút u để xem liệu nó có
thể được lật sang nút khác hay không v. Nếu có, hãy thêm (v, u) vào danh sách Cạnh (dòng
31). Phương thức getFlippedNode (nút, vị trí) tìm một nút bị lật bằng cách lật một ô H và
các ô lân cận của nó trong một nút (dòng 43–47). Phương thức flipACell (nút, hàng, cột)
thực sự lật một ô H và các ô lân cận của nó trong một nút (dòng 52–60).
Phương thức getIndex (nút) được thực hiện giống như cách chuyển đổi một số nhị
phân thành một số thập phân (dòng 62–72). Phương thức getNode (index) trả về một nút
bao gồm các chữ cái H và T (dòng 74–87).
Machine Translated by Google

Điều khoản chính 1053

Phương thức getShortestpath (nodeIndex) gọi getPath (nodeIndex)


phương pháp để lấy một đỉnh trong một đường đi ngắn nhất từ nút được chỉ định đến nút đích
(dòng 89–91).
Phương thức printNode (nút) hiển thị một nút trên bảng điều khiển (dòng 93–101).
Liệt kê 28.14 đưa ra một chương trình nhắc người dùng nhập một nút ban đầu và hiển thị
các bước để tiếp cận nút mục tiêu.

DANH SÁCH 28.14 NineTail.java


1 nhập java.util.Scanner;
2
3 lớp công khai NineTail {
4 public static void main (String [] args) {
// Nhắc người dùng nhập chín xu 'Hs và Ts
5 System.out.print ("Nhập chín xu ban đầu Hs và Ts: ");
6 Đầu vào máy quét = Máy quét mới (System.in);
7 String s = input.nextLine (); char []
8 initialNode = s.toCharArray (); nút ban đầu
9 10

11 NineTailModel model = new NineTailModel (); tạo mô hình


12 java.util.List <Integer> path =
13 model.getShortestPath (NineTailModel.getIndex (InitialNode)); có được con đường ngắn nhất

14
15 System.out.println ("Các bước để lật đồng xu ");
16 for (int i = 0; i <path.size (); i ++)
17 NineTailModel.printNode (
18 NineTailModel.getNode (path.get (i) .intValue ()));
19 }
20}

Chương trình nhắc người dùng nhập một nút ban đầu có chín chữ cái với sự kết hợp của Hs
và Ts dưới dạng chuỗi ở dòng 8, lấy một mảng ký tự từ chuỗi (dòng 9), tạo mô hình đồ thị để
lấy cây BFS ( dòng 11), lấy một đường đi ngắn nhất từ nút ban đầu đến nút đích (dòng 12–13) và
hiển thị các nút trong đường dẫn (dòng 16–18).

28.26 Các nút được tạo như thế nào cho biểu đồ trong NineTailModel?

28.27 Các cạnh được tạo như thế nào cho biểu đồ trong NineTailModel? Kiểm tra điểm

28.28 Điều gì được trả về sau khi gọi getIndex ("HTHTTTHHH" .toCharArray ()) trong Liệt kê
28.13? Điều gì được trả về sau khi gọi getNode (46) trong Liệt kê 28.13?

28.29 Nếu các dòng 26 và 27 được hoán đổi trong Liệt kê 28.13, NineTailModel.java, thì pro gram có hoạt

động không? Tại sao?

ĐIỀU KHOẢN CHÍNH

danh sách kề 1022 các cạnh sự cố 1018


ma trận kề 1021 cạnh song song 1018
các đỉnh liền kề 1018 Bảy cây cầu của Königsberg 1016
tìm kiếm theo chiều rộng-đầu tiên 1037 đồ thị đơn giản 1018
hoàn thành đồ thị 1018 cây kéo dài 1018
chu kỳ 1018 cây 1018

độ 1018 đồ thị vô hướng 1017


tìm kiếm theo chiều sâu 1037 đồ thị không trọng số 1018
đồ thị có hướng 1017 đồ thị có trọng số 1018
đồ thị 1016
Machine Translated by Google

1054 Chương 28 Đồ thị và Ứng dụng

TÓM TẮT CHƯƠNG

1. Đồ thị là một cấu trúc toán học hữu ích thể hiện mối quan hệ giữa các thực thể trong thế giới
thực. Bạn đã học cách lập mô hình đồ thị bằng cách sử dụng các lớp và giao diện, cách biểu diễn

các đỉnh và cạnh bằng cách sử dụng mảng và danh sách được liên kết cũng như cách triển khai các

toán hạng cho đồ thị.

2. Duyệt đồ thị là quá trình truy cập mỗi đỉnh trong đồ thị chính xác một lần. Bạn đã
học được hai cách phổ biến để duyệt qua biểu đồ: tìm kiếm theo chiều sâu (DFS) và
tìm kiếm theo chiều rộng (BFS).

3. DFS và BFS có thể được sử dụng để giải quyết nhiều vấn đề như phát hiện đồ thị có liên
thông hay không, phát hiện xem có chu trình trong đồ thị hay không, và tìm đường đi
ngắn nhất giữa hai đỉnh.

ĐỐ

Trả lời câu hỏi cho chương này trực tuyến tại www.cs.armstrong.edu/liang/intro10e/quiz.html.

BÀI TẬP LẬP TRÌNH

Phần 28,6–28,10

* 28.1 (Kiểm tra xem đồ thị có được kết nối hay không) Viết chương trình đọc đồ thị từ một
tệp và xác định xem đồ thị có được kết nối hay không. Dòng đầu tiên trong tệp chứa
một số cho biết số đỉnh (n). Các đỉnh được đánh dấu là 0, 1,. . . , n-1. Mỗi dòng
tiếp theo, với định dạng
đưau ra
v1 các
v2 mô
vítả
dụcác cạnhtệp
về hai (u,cho
v1),
đồ(u,
thịv2), v.v.ứng
tương Hình ...,
của28.21
chúng.

Tập tin 0 1 Tập tin

6 6
0 1
0 1 2 0 1 2 3
1 0 3 1 0 3
2 0 3 4 2 3 2 0 3
3 1 2 4 5 3 0 1 2 2 3
4 2 3 5 4 5
5 3 4 4 5 5 4 4 5

(Một) (b)

HÌNH 28.21 Các đỉnh và cạnh của đồ thị có thể được lưu trữ trong một tệp.

Chương trình của bạn sẽ nhắc người dùng nhập tên của tệp, sau đó nó sẽ đọc dữ liệu
từ tệp, tạo một phiên bản g của UnweightedGraph, gọi g.printEdges () để hiển thị tất

cả các cạnh và gọi dfs () để lấy một phiên bản cây của AbstractGraph.Tree. Nếu
tree.getNumberOfVerticesFound ()
giống như số đỉnh của đồ thị, đồ thị được liên thông. Đây là bản chạy mẫu của
chương trình:
Machine Translated by Google

Bài tập lập trình 1055

Nhập tên tệp: c: \ works \ GraphSample1.txt


Số đỉnh là 6

Đỉnh 0: (0, 1) (0, 2)


Đỉnh 1: (1, 0) (1, 3)
Đỉnh 2: (2, 0) (2, 3) (2, 4)
Đỉnh 3: (3, 1) (3, 2) (3, 4) (3, 5)
Đỉnh 4: (4, 2) (4, 3) (4, 5)
Đỉnh 5: (5, 3) (5, 4)
Đồ thị được kết nối

(Gợi ý: Sử dụng UnweightedGraph (list, numberOfVertices) mới để tạo biểu đồ, trong
đó danh sách chứa danh sách các đối tượng AbstractGraph.Edge . Sử dụng
AbstractGraph.Edge (u, v) mới để tạo cạnh. Đọc dòng đầu tiên để lấy số của các
đỉnh. Đọc từng dòng tiếp theo thành một chuỗi s và sử dụng s.split ("[\\ s +]") để
trích xuất các đỉnh từ chuỗi và tạo các cạnh từ các đỉnh.)

* 28.2 (Tạo tệp cho biểu đồ) Sửa đổi Liệt kê 28.1, TestGraph.java, để tạo tệp đại diện cho
graph1. Định dạng tệp được mô tả trong Bài tập lập trình 28.1.
Tạo tệp từ mảng được xác định trong các dòng 8–21 trong Liệt kê 28.1. Số
đỉnh của biểu đồ là 12, sẽ được lưu trữ trong dòng đầu tiên của tệp.
Nội dung của tệp sẽ như sau:

12
0 1 3 5
1 0 2 3
2 1 3 4 10
3 0 1 2 4 5
4 2 3 5 7 8 10
5 0 3 4 6 7
6 5 7
7 4 5 6 8
8 4 7 9 10 11
9 8 11
10 2 4 8 11
11 8 9 10

* 28.3 (Triển khai DFS bằng cách sử dụng ngăn xếp) Thuật toán tìm kiếm theo độ sâu được mô tả
trong Liệt kê 28.8 sử dụng đệ quy. Thiết kế một thuật toán mới mà không sử dụng đệ quy.

Mô tả nó bằng cách sử dụng mã giả. Triển khai nó bằng cách xác định một lớp
mới có tên là UnweightedGraphWithNonrecursiveDFS mở rộng UnweightedGraph
và ghi đè phương thức dfs .

* 28.4 (Tìm các thành phần được kết nối) Tạo một lớp mới có tên MyGraph làm lớp con của
UnweightedGraph có chứa một phương thức để tìm tất cả các thành phần com được
kết nối trong một biểu đồ với tiêu đề sau:

public List <List <Integer>> getConnectedComponents ();

Phương thức trả về một List <List <Integer>>. Mỗi phần tử trong danh sách là một
danh sách khác chứa tất cả các đỉnh trong một thành phần được kết nối. Đối với đề
thi, đối với đồ thị trong Hình 28.21b, getConnectedComponents () trả về [[0, 1,
2, 3], [4, 5]].
Machine Translated by Google

1056 Chương 28 Đồ thị và Ứng dụng

* 28.5 (Tìm đường dẫn) Thêm một phương thức mới trong AbstractGraph để tìm đường đi giữa hai đỉnh có

tiêu đề sau:

public List <Integer> getPath (int u, int v);

Phương thức trả về một List <Integer> chứa tất cả các đỉnh trong đường dẫn từ u đến v theo

thứ tự này. Sử dụng cách tiếp cận BFS, bạn có thể nhận được đường dẫn ngắn nhất từ u đến v.

Nếu không có đường dẫn từ u đến v, phương thức trả về null.

* 28.6 (Phát hiện chu kỳ) Thêm một phương thức mới trong AbstractGraph để xác định xem có chu trình

trong biểu đồ với tiêu đề sau:

public boolean isCyclic ();

* 28.7 (Tìm chu kỳ) Thêm một phương thức mới trong AbstractGraph để tìm một chu trình trong

biểu đồ với tiêu đề sau:

public List <Integer> getACycle (int u);

Phương thức trả về một Danh sách chứa tất cả các đỉnh trong một chu trình bắt đầu từ u. Nếu

đồ thị không có bất kỳ chu trình nào, phương thức trả về giá trị null.

** 28.8 (Kiểm tra lưỡng tính) Nhớ lại rằng một đồ thị là lưỡng phân nếu các đỉnh của nó có thể được chia

thành hai tập rời rạc sao cho không có cạnh nào tồn tại giữa các đỉnh trong cùng một tập.

Thêm một phương thức mới trong AbstractGraph với tiêu đề sau để phát hiện xem biểu đồ có

phải là lưỡng phân hay không:

public boolean isBipartite ();

** 28.9 (Lấy bộ lưỡng phân) Thêm một phương thức mới trong AbstractGraph với tiêu đề sau để trả về hai

bộ lưỡng phân nếu biểu đồ là hai bên:

public List <List <Integer>> getBipartite ();

Phương thức trả về một Danh sách chứa hai danh sách con, mỗi danh sách chứa một tập các

đỉnh. Nếu đồ thị không phải là lưỡng phân, phương thức trả về null.

28.10 (Tìm đường đi ngắn nhất) Viết chương trình đọc đồ thị liên thông từ một tệp.
Biểu đồ được lưu trữ trong một tệp có cùng định dạng được chỉ định trong
Chương trình ming Bài tập 28.1. Chương trình của bạn sẽ nhắc người dùng
nhập tên của tệp, sau đó là hai đỉnh, và sẽ hiển thị một đường đi ngắn nhất
giữa hai đỉnh. Ví dụ, đối với đồ thị trong Hình 28.21a, một đường đi ngắn nhất giữa 0
và 5 có thể được hiển thị là 0 1 3 5.

Đây là bản chạy mẫu của chương trình:

Nhập tên tệp: c: \ works \ GraphSample1.txt

Nhập hai đỉnh (chỉ số nguyên): 0 5


Số đỉnh là 6

Đỉnh 0: (0, 1) (0, 2)


Đỉnh 1: (1, 0) (1, 3)
Đỉnh 2: (2, 0) (2, 3) (2, 4)
Đỉnh 3: (3, 1) (3, 2) (3, 4) (3, 5)
Đỉnh 4: (4, 2) (4, 3) (4, 5)
Đỉnh 5: (5, 3) (5, 4)
Đường dẫn là 0 1 3 5
Machine Translated by Google

Bài tập lập trình 1057

** 28.11 (Sửa lại Liệt kê 28.14, NineTail.java) Chương trình trong Liệt kê 28.14 cho phép người
dùng nhập đầu vào cho vấn đề chín đuôi từ bảng điều khiển và hiển thị kết quả trên
bảng điều khiển. Viết chương trình cho phép người dùng thiết lập trạng thái ban
đầu của chín đồng tiền (xem Hình 28.22a) và nhấp vào nút Giải để hiển thị giải
pháp, như thể hiện trong Hình 28.22b. Ban đầu, người dùng có thể nhấp vào nút
chuột để lật đồng xu. Đặt màu đỏ trên các ô đã lật.

(Một) (b)

HÌNH 28.22 Chương trình giải quyết vấn đề chín đuôi.

** 28.12 (Biến thể của bài toán chín mặt) Trong bài toán chín mặt, khi bạn lật một đồng xu, các
ô lân cận theo chiều ngang và chiều dọc cũng được lật theo. Viết lại chương
trình, giả sử rằng tất cả các ô lân cận bao gồm cả các ô lân cận đường chéo cũng
bị lật.

** 28.13 (vấn đề 4 * 4 16 đuôi) Liệt kê 28.14, NineTail.java, trình bày một giải pháp cho vấn đề
chín đuôi. Sửa đổi chương trình này cho vấn đề 4 * 4 16 đuôi.

Lưu ý rằng có thể một giải pháp có thể không tồn tại cho mẫu bắt đầu. Nếu vậy, hãy báo cáo

rằng không có giải pháp nào tồn tại.

** 28.14 (phân tích 4 * 4 16 đuôi) Bài toán chín đuôi trong văn bản sử dụng ma trận 3 * 3.

Giả sử rằng bạn có 16 đồng xu được đặt trong ma trận 4 * 4. Viết chương
trình để tìm ra số mẫu bắt đầu không có lời giải.

* 28.15 (4 * 4 16 đuôi GUI) Bài tập lập trình viết lại 28.14 để cho phép người dùng thiết lập
mẫu ban đầu của bài toán 4 * 4 16 đuôi (xem Hình 28.23a). Người dùng có thể nhấp
vào nút Giải để hiển thị giải pháp, như trong Hình 28.23b.

Ban đầu, người dùng có thể nhấp vào nút chuột để lật đồng xu. Nếu một giải pháp không tồn

tại, hãy hiển thị một thông báo để báo cáo nó.

(Một) (b)

HÌNH 28.23 Bài toán giải bài toán 16 đuôi.


Machine Translated by Google

1058 Chương 28 Đồ thị và Ứng dụng

** 28.16 (Đồ thị con quy nạp) Cho một đồ thị vô hướng G = (V, E) và một số nguyên k, hãy
tìm một đồ thị con cảm ứng H của G có kích thước lớn nhất sao cho tất cả các
đỉnh của H đều có bậc 7 = k hoặc kết luận rằng không như vậy tồn tại đồ thị con.
Triển khai phương thức với tiêu đề sau:

public static Graph maxIndncedSubgraph (Graph g, int k)

Phương thức trả về null nếu một đồ thị con như vậy không tồn tại.

(Gợi ý: Một cách tiếp cận trực quan là loại bỏ các đỉnh có bậc nhỏ hơn k.

Khi các đỉnh bị xóa cùng với các cạnh liền kề của chúng, bậc của các đỉnh khác có thể

bị giảm đi. Tiếp tục quá trình cho đến khi không có đỉnh nào có thể bị loại bỏ hoặc tất
cả các đỉnh đã bị loại bỏ.)

*** 28.17 (Chu trình Hamilton) Thuật toán đường đi Hamilton được thực hiện trong Sup plement VI.E.

Thêm phương thức getHamiltonianCycle sau trong giao diện Đồ thị và triển khai nó trong

lớp AbstractGraph :

/ ** Trả về một chu trình Hamilton


* Trả về null nếu đồ thị không chứa chu trình Hamilton * /
danh sách công khai <Integer> getHamiltonianCycle ()

*** 28.18 (Chu kỳ tham quan của hiệp sĩ) Viết lại KnightTourApp.java trong nghiên cứu điển
hình trong Sup plement VI.E để tìm chuyến tham quan của hiệp sĩ ghé thăm từng
ô trong bàn cờ và quay trở lại ô bắt đầu. Rút gọn bài toán về chu trình Hiệp
sĩ thành bài toán tìm chu trình Hamilton.

** 28.19 (Hiển thị cây DFS / BFS trong biểu đồ) Sửa đổi GraphView trong Liệt kê 28.6 để thêm một cây

trường dữ liệu mới với một phương thức đặt. Các cạnh của cây được đánh màu đỏ. Viết

chương trình hiển thị đồ thị trong Hình 28.1 và cây DFS / BFS bắt đầu từ một thành phố

xác định, như trong Hình 28.13 và 28.16. Nếu một thành phố không có trong bản đồ được

nhập, chương trình sẽ hiển thị lỗi mes sage trong nhãn.

* 28.20 (Hiển thị đồ thị) Viết chương trình đọc đồ thị từ một tệp và hiển thị nó.
Dòng đầu tiên trong tệp chứa một số cho biết số đỉnh (n). Các đỉnh có nhãn 0, 1,. . . ,

n-1. Mỗi dòng tiếp theo, với for mat uxy v1 cạnh
v2 mô(u,
tả v1),
vị trí của u tại (x, y) và các

...,
(u, v2), v.v. Hình 28.24a đưa ra một ví dụ về tệp cho đồ thị tương ứng của chúng.

Chương trình của bạn sẽ nhắc người dùng nhập tên của tệp, đọc dữ liệu từ tệp và hiển

thị biểu đồ trên một ngăn bằng GraphView, như thể hiện trong Hình 28.24b.

Tập tin
0 1
7
0 30 30 1 2
1 90 30 0 3 6
2 30 90 0 3 4 2 3 6
3 90 90 1 2 4 5
4 30 150 2 3 5
5 90 150 3 4 6
6 130 90 1 5 4 5

(Một) (b)

HÌNH 28.24 Chương trình đọc thông tin về đồ thị và hiển thị nó một cách trực quan.
Machine Translated by Google

Bài tập lập trình 1059

** 28.21 (Hiển thị tập hợp các vòng kết nối) Sửa đổi Liệt kê 28.10, ConnectedCircles.java, để hiển

thị các tập hợp các vòng kết nối bằng các màu khác nhau.

Nghĩa là, nếu hai vòng tròn được kết nối với nhau, chúng được hiển thị bằng cách sử dụng

cùng một màu; nếu không, chúng không có cùng màu, như trong Hình 28.25. (Gợi ý: Xem Bài tập

lập trình 28.4.)

(Một) (b) (C)

HÌNH 28.25 (a) Các vòng kết nối được hiển thị cùng màu. (b) Các hình chữ nhật không được tô màu nếu chúng không được nối với nhau. (c) Các

hình chữ nhật được tô màu nếu chúng được nối với nhau.

* 28.22 (Di chuyển một vòng tròn) Sửa đổi Liệt kê 28.10, ConnectedCircles.java, để kích hoạt

người dùng để kéo và di chuyển một vòng tròn.

** 28.23 (Hình chữ nhật được kết nối) Liệt kê 28.10, ConnectedCircles.java, cho phép người dùng tạo

các vòng kết nối và xác định xem chúng có được kết nối hay không. Viết lại gam pro cho

các hình chữ nhật. Chương trình cho phép người dùng tạo một hình chữ nhật bằng cách

nhấp chuột vào vùng trống hiện không được bao phủ bởi hình chữ nhật. Khi các rối rec

được thêm vào, các hình chữ nhật được sơn lại như đã tô nếu chúng được nối với nhau
hoặc không được tô bằng cách khác, như trong Hình 28.25b – c.

* 28.24 (Xóa một vòng kết nối) Sửa đổi Liệt kê 28.10, ConnectedCircles.java, để kích hoạt
người dùng xóa vòng kết nối khi con chuột được nhấp vào bên trong vòng kết nối.
Machine Translated by Google

Trang này cố ý để trống


Machine Translated by Google

CHƯƠNG

29
GRAPHS TRỌNG LƯỢNG
VÀ ỨNG DỤNG

Mục tiêu
■ Để biểu diễn các cạnh có trọng số bằng cách sử dụng ma trận kề và cạnh kề
danh sách (§29.2).

■ Để mô hình hóa các đồ thị có trọng số bằng cách sử dụng lớp WeightedGraph
mở rộng lớp AbstractGraph (§29.3).

■ Để thiết kế và triển khai thuật toán tìm cây bao trùm tối thiểu (§29.4).

■ Để định nghĩa lớp MST mở rộng lớp Cây (§29.4).

■ Để thiết kế và triển khai thuật toán tìm nguồn đơn


đường đi ngắn nhất (§29.5).

■ Để xác định lớp ShortestPathTree mở rộng lớp Cây

(§29,5).

■ Để giải bài toán chín đuôi có trọng số bằng thuật toán đường đi ngắn
nhất (§29.6).
Machine Translated by Google

1062 Chương 29 Đồ thị có Trọng số và Ứng dụng

29.1 Giới thiệu


Một đồ thị là một đồ thị có trọng số nếu mỗi cạnh được gán một trọng số. Đồ thị có trọng số
Chìa khóa

Điểm có nhiều ứng dụng trong thực tế.

Hình 28.1 giả định rằng biểu đồ thể hiện số lượng chuyến bay giữa các thành phố. Bạn có thể áp dụng
BFS để tìm ra số chuyến bay ít nhất giữa hai thành phố. Giả sử rằng các cạnh đại diện cho khoảng cách
lái xe giữa các thành phố như trong Hình 29.1. Làm thế nào để bạn tìm thấy tổng khoảng cách tối
thiểu để kết nối tất cả các thành phố? Làm thế nào để bạn tìm thấy con đường ngắn nhất giữa hai
thành phố? Chương này sẽ giải quyết những câu hỏi này. Bài toán trước được gọi là bài toán cây bao
trùm tối thiểu (MST) và bài toán sau là đường đi ngắn nhất

vấn đề vấn đề.

Seattle (0)

2097 Boston (6)


983
1331 Chicago (5)
214
807 1003
787 New York (7)
Denver (3)
1267 533 1260

599
San Francisco (1)
1015 888
Thành phố Kansas (4)
381 1663
864

Los Angeles (2) 496


1435
781 Atlanta (8)

810
Dallas (10)
239
661

Houston (11) 1187

Miami (9)

HÌNH 29.1 Biểu đồ mô hình hóa khoảng cách giữa các thành phố.

Chương trước đã giới thiệu khái niệm về đồ thị. Bạn đã học cách biểu diễn các cạnh bằng cách sử
dụng mảng cạnh, danh sách cạnh, ma trận kề và danh sách kề cũng như cách lập mô hình biểu đồ bằng
giao diện Đồ thị , lớp AbstractGraph và UnweightedGraph
lớp học. Chương trước cũng đã giới thiệu hai kỹ thuật quan trọng để duyệt qua đồ thị: tìm kiếm
theo chiều sâu và tìm kiếm theo chiều rộng, và tính năng duyệt được áp dụng để giải quyết các lỗi
xác suất thực tế. Chương này sẽ giới thiệu về đồ thị có trọng số. Bạn sẽ học thuật toán tìm cây bao
trùm tối thiểu trong Phần 29.4 và thuật toán tìm đường đi ngắn nhất trong Phần 29.5.

Lưu ý sư phạm
công cụ học tập đồ thị có trọng Trước khi chúng tôi giới thiệu các thuật toán và ứng dụng cho đồ thị có trọng số, sẽ rất hữu

số trên Trang web Đồng hành ích nếu bạn làm quen với đồ thị có trọng số bằng công cụ tương tác GUI tại www.cs.armstrong

.edu / liang / animation / WeightedGraphLearningTool.html, như trong Hình 29.2. Công cụ này

cho phép bạn nhập các đỉnh, chỉ định các cạnh và trọng số của chúng, xem biểu đồ và tìm MST

và tất cả các đường đi ngắn nhất từ một nguồn duy nhất, như thể hiện trong Hình 29.2.
Machine Translated by Google

29.2 Biểu diễn đồ thị có trọng số 1063

HÌNH 29.2 Bạn có thể sử dụng công cụ này để tạo biểu đồ có trọng số bằng các thao tác chuột và hiển thị MST và các đường đi ngắn nhất.

29.2 Biểu diễn đồ thị có trọng số


Các cạnh có trọng số có thể được lưu trữ trong danh sách kề.
Chìa khóa

Điểm
Có hai loại đồ thị có trọng số: trọng số đỉnh và trọng số cạnh. Trong đồ thị có trọng số đỉnh, mỗi đỉnh được gán

một trọng số. Trong đồ thị có trọng số cạnh, mỗi cạnh được gán một trọng số. Trong hai loại, đồ thị có trọng số cạnh đồ thị có trọng số đỉnh
có nhiều ứng dụng hơn. Chương này xem xét các đồ thị có trọng số cạnh. đồ thị có trọng số cạnh

Đồ thị có trọng số có thể được biểu diễn giống như đồ thị không có trọng số, ngoại trừ việc bạn phải biểu diễn

trọng số trên các cạnh. Như với đồ thị không có trọng số, các đỉnh trong đồ thị có trọng số có thể được lưu trữ

trong một mảng. Phần này giới thiệu ba cách biểu diễn cho các cạnh trong đồ thị có trọng số.

29.2.1 Biểu diễn các cạnh có trọng số: Mảng cạnh


Các cạnh có trọng số có thể được biểu diễn bằng mảng hai chiều. Ví dụ, bạn có thể lưu trữ tất cả các cạnh trong biểu

đồ trong Hình 29.3a bằng cách sử dụng mảng trong Hình 29.3b.

Ghi chú

Trọng số có thể thuộc bất kỳ loại nào: Số nguyên, Nhân đôi, Số thập phân lớn , v.v.
Bạn có thể sử dụng mảng hai chiều của kiểu Đối tượng để biểu diễn các cạnh có trọng
số như sau:

Đối tượng [] [] edge = {


{new Integer (0), new Integer (1), SomeTypeForWeight (2)} mới,
{new Integer (0), new Integer (3), SomeTypeForWeight (8)} mới,
...

};
Machine Translated by Google

1064 Chương 29 Đồ thị có Trọng số và Ứng dụng

trọng lượng đỉnh

int [] [] edge = {{0, 1, 2}, {0, 3, 8},


1 7 2
{1, 0, 2}, {1, 2, 7}, {1, 3, 3},
3 4 5 {2, 1, 7}, {2, 3, 4}, {2, 4, 5},
2
{3, 0, 8}, {3, 1, 3}, {3, 2, 4}, {3, 4, 6},
số 8
6 {4, 2, 5}, {4, 3, 6}
0 3 4 };
(Một) (b)

HÌNH 29.3 Mỗi cạnh được gán một trọng số trong một đồ thị có trọng số cạnh.

29.2.2 Ma trận gần kề có trọng số


Giả sử rằng đồ thị có n đỉnh. Bạn có thể sử dụng ma trận n * n hai chiều, chẳng hạn như trọng số, để biểu diễn

trọng số trên các cạnh. weights [i] [j] đại diện cho trọng lượng trên cạnh (i, j). Nếu các đỉnh i và j không được

nối với nhau, thì trọng số [i] [j] là rỗng. Ví dụ, trọng số trong đồ thị trong Hình 29.3a có thể được biểu diễn

bằng ma trận kề như sau:

Số nguyên [] [] adjacencyMatrix = { 0 1 2 3 4
{null, 2, null, 8, null}, 0 null 2 vô giá trị số 8 vô giá trị

{2, null, 7, 3, null},


1 2 null 7 3 vô giá trị

{null, 7, null, 4, 5},


2 null 7 vô giá trị 4 5
{8, 3, 4, null, 6},
{null, null, 5, 6, null} 3 số 8 3 4 null 6

}; 4 null null 5 6 vô giá trị

29.2.3 Danh sách gần kề


Một cách khác để biểu diễn các cạnh là xác định các cạnh là các đối tượng. AbstractGraph.Edge _

lớp được định nghĩa để đại diện cho một cạnh không có trọng số trong Liệt kê 28.3. Đối với các cạnh có trọng số,

chúng tôi xác định lớp WeightedEdge như được hiển thị trong Liệt kê 29.1.

LISTING 29.1 WeightedEdge.java


1 lớp công khai WeightedEdge mở rộng AbstractGraph.Edge
2 triển khai <WeightedEdge> {có thể so sánh được
trọng lượng cạnh công kép trọng; // Trọng số trên cạnh (u, v)
3 4

/ ** Tạo một cạnh có trọng số trên (u, v) * /


constructor 5 6 public WeightedEdge (int u, int v, double weight) {
siêu (u, v);
this.weight = trọng lượng;
}
7 8 9 10

11 @Override / ** So sánh hai cạnh trên trọng số * /


so sánh các cạnh 12 public int so sánhTo (WeightedEdge edge) {
13 if (weight> edge.weight) return
14 1;
15 else if (weight == edge.weight) return
16 0;
17 khác
trả về -1;
}
18 19 20}
Machine Translated by Google

29.3 Lớp đồ thị có trọng số 1065

AbstractGraph.Edge là một lớp bên trong được định nghĩa trong lớp AbstractGraph . Nó đại
diện cho một cạnh từ đỉnh u đến v. WeightedEdge mở rộng AbstractGraph.Edge với trọng số thuộc
tính mới.
Để tạo một đối tượng WeightedEdge , hãy sử dụng WeightedEdge mới (i, j, w), trong đó w là trọng số trên

cạnh (i, j). Thường thì bạn cần so sánh trọng lượng của các cạnh. Vì lý do này,

lớp WeightedEdge triển khai giao diện có thể so sánh được .

Đối với đồ thị không có trọng số, chúng tôi sử dụng danh sách kề để biểu diễn các cạnh. Đối với đồ thị

có trọng số, chúng ta vẫn sử dụng danh sách kề, danh sách kề cho các đỉnh trong đồ thị trong hình 29.3a có

thể được biểu diễn như sau:

java.util.List <WeightedEdge> [] list = new java.util.List [5];

danh sách [0] WeightedEdge (0, 1, 2) WeightedEdge (0, 3, 8)

danh sách [1] WeightedEdge (1, 0, 2) WeightedEdge (1, 2, 3) WeightedEdge (1, 2, 7)

danh sách [2] WeightedEdge (2, 3, 4) WeightedEdge (2, 4, 5) WeightedEdge (2, 1, 7)

danh sách [3] WeightedEdge (3, 1, 3) WeightedEdge (3, 2, 4) WeightedEdge (3, 4, 6) WeightedEdge (3, 0, 8)

danh sách [4] WeightedEdge (4, 2, 5) WeightedEdge (4, 3, 6)

list [i] lưu trữ tất cả các cạnh kề với đỉnh i.

Để linh hoạt, chúng tôi sẽ sử dụng danh sách mảng thay vì một mảng có kích thước cố định để biểu diễn danh sách
như sau:

Danh sách <Danh sách <WeightedEdge>> list = new java.util.ArrayList <> ();

29.1 Đối với mã WeightedEdge edge = new WeightedEdge (1, 2, 3.5), edge.u, edge.v, và
edge.weight là gì? Kiểm tra điểm

29.2 Đầu ra của đoạn mã sau là gì?

List <WeightedEdge> list = new ArrayList <> ();


list.add (new WeightedEdge (1, 2, 3.5));
list.add (new WeightedEdge (2, 3, 4.5));
WeightedEdge e = java.util.Collections.max (danh sách);
System.out.println (eu);
System.out.println (ev);
System.out.println (e.weight);

29.3 Lớp WeightedGraph


Lớp WeightedGraph mở rộng AbstractGraph.
Chìa khóa

Điểm
Chương trước đã thiết kế giao diện Graph , lớp AbstractGraph và lớp UnweightedGraph để lập mô hình đồ

thị. Theo mô hình này, chúng tôi thiết kế WeightedGraph như một lớp con của AbstractGraph, như trong Hình

29.4.

WeightedGraph chỉ đơn giản là mở rộng AbstractGraph với năm hàm tạo để tạo các thể hiện
con crete WeightedGraph . WeightedGraph kế thừa tất cả các phương thức từ AbstractGraph,
ghi đè các phương thức clear và addVertex , triển khai một phương thức addEdge mới để thêm
vào một cạnh có trọng số và cũng giới thiệu các phương pháp mới để lấy cây khung tối thiểu
và để tìm tất cả các đường đi ngắn nhất nguồn đơn. Cây bao trùm tối thiểu và đường đi ngắn
nhất sẽ được giới thiệu lần lượt trong Phần 29.4 và 29.5.
Liệt kê 29.2 thực hiện WeightedGraph. Danh sách kề cạnh (dòng 38–63) được sử dụng bên trong để lưu trữ

các cạnh liền kề cho một đỉnh. Khi một WeightedGraph được xây dựng, cạnh của nó
Machine Translated by Google

1066 Chương 29 Đồ thị có Trọng số và Ứng dụng

«Giao diện»
Đồ thị <V>

AbstractGraph <V>

WeightedGraph <V>

+ WeightedGraph () Xây dựng một đồ thị rỗng.

+ WeightedGraph (đỉnh: V [], cạnh: int [] []) Xây dựng một đồ thị có trọng số với các cạnh được chỉ định và
số đỉnh trong mảng.

+ WeightedGraph (đỉnh: List <V>, các cạnh: Xây dựng một đồ thị có trọng số với các cạnh được chỉ định và
Danh sách <WeightedEdge>) số đỉnh.

+ WeightedGraph (các cạnh: int [] [], Xây dựng một đồ thị có trọng số với các cạnh được chỉ định
numberOfVertices: int) trong một mảng và số đỉnh.

+ WeightedGraph (các cạnh: List <WeightedEdge>, Xây dựng một đồ thị có trọng số với các cạnh được chỉ định trong danh sách
numberOfVertices: int) và số lượng đỉnh.

+ printWeightedEdges (): void Hiển thị tất cả các cạnh và trọng lượng.

+ getWeight (int u, int v): gấp đôi Trả về trọng lượng trên cạnh từ u đến v. Ném một
ngoại lệ nếu cạnh không tồn tại.

+ addEdges (u: int, v: int, weight: double): void Thêm một cạnh có trọng số vào biểu đồ và ném một
IllegalArgumentException nếu u, v hoặc w không hợp lệ. Nếu như
(u, v) đã có trong đồ thị, trọng số mới được thiết lập.

+ getMinimumSpanningTree (): MST Trả về cây khung tối thiểu bắt đầu từ đỉnh 0.

+ getMinimumSpanningTree (index: int): MST Trả về một cây bao trùm tối thiểu bắt đầu từ đỉnh v.

+ getShortestPath (index: int): ShortestPathTree Trả về tất cả các đường dẫn ngắn nhất nguồn đơn.

HÌNH 29.4 WeightedGraph mở rộng AbstractGraph.

danh sách kề được tạo (dòng 47 và 57). Các phương thức getMinimumSpanningTree ()
(dòng 99–138) và getShortestPath () (dòng 156–197) sẽ được giới thiệu trong các phần
sắp tới.

LISTING 29,2 WeightedGraph.java


1 nhập java.util. *;
2
3 lớp công khai WeightedGraph <V> mở rộng AbstractGraph <V> {
4 / ** Tạo một rỗng * /
phương thức khởi tạo no-arg 5 public WeightedGraph () {
6}
7
/ ** Xây dựng một WeightedGraph từ các đỉnh và cạnh trong các mảng * /
constructor public WeightedGraph (V [] đỉnh, int [] [] edge) {
8 createWeightedGraph (java.util.Arrays.asList (đỉnh), cạnh);
9 }
10 11 12

13 / ** Xây dựng một WeightedGraph từ các đỉnh và cạnh trong danh sách * /
constructor 14 public WeightedGraph (int [] [] edge, int numberOfVertices) {
15 Liệt kê các đỉnh <V> = new ArrayList <> ();
16 for (int i = 0; i <numberOfVertices; i ++)
17 vertices.add ((V) (new Integer (i)));
18
19 createWeightedGraph (đỉnh, cạnh);
20 }
21
Machine Translated by Google

29.3 Lớp đồ thị có trọng số 1067

22 / ** Xây dựng một WeightedGraph cho các đỉnh 0, 1, 2 và danh sách cạnh * /
23 public WeightedGraph (Danh sách các đỉnh <V>, các cạnh Danh sách <WeightedEdge>) { constructor
24 createWeightedGraph (đỉnh, cạnh);
25 }
26
27 / ** Xây dựng một WeightedGraph từ các đỉnh 0, 1 và mảng cạnh * /
28 public WeightedGraph (Danh sách các cạnh <WeightedEdge>, constructor
29 int numberOfVertices) {
30 Liệt kê các đỉnh <V> = new ArrayList <> ();
31 for (int i = 0; i <numberOfVertices; i ++)
32 vertices.add ((V) (new Integer (i)));
33

34 createWeightedGraph (đỉnh, cạnh);


35 }
36
37 / ** Tạo danh sách kề từ mảng cạnh * /
38 private void createWeightedGraph (Liệt kê các đỉnh <V>, các cạnh int [] []) {
39 this.vertices = đỉnh;
40
41 for (int i = 0; i <vertices.size (); i ++) {
42 Neighbor.add (new ArrayList <Edge> ()); // Tạo danh sách cho các đỉnh tạo danh sách cho các đỉnh

43 }
44
45 for (int i = 0; i <edge.length; i ++) {
46 Neighbor.get (edge [i] [0]). add (
47 new WeightedEdge (các cạnh [i] [0], các cạnh [i] [1], các cạnh [i] [2])); tạo ra một cạnh có trọng số
48 }
49 }
50
51 / ** Tạo danh sách kề từ danh sách cạnh * /
52 private void createWeightedGraph (
53 Liệt kê các đỉnh <V>, Liệt kê các cạnh <WeightedEdge>) {
54 this.vertices = đỉnh;
55
56 for (int i = 0; i <vertices.size (); i ++) {
57 Neighbor.add (new ArrayList <Edge> ()); // Tạo danh sách cho các đỉnh tạo danh sách cho các đỉnh

58 }
59
60 for (WeightedEdge edge: các cạnh) {
61 hàng xóm.get (cạnh.u) .add (cạnh); // Thêm một cạnh vào danh sách
62 }
63 }
64
65 / ** Trả lại trọng lượng trên cạnh (u, v) * /
66 public double getWeight (int u, int v) ném Exception { in cạnh
67 for (Cạnh cạnh: Neighbor.get (u)) {
68 if (edge.v == v) {
69 return ((WeightedEdge) edge). trọng lượng;
70 }
71 }
72
73 ném mới Exception ("Cạnh không thoát");
74 }
75
76 / ** Hiển thị các cạnh có trọng số * /
77 public void printWeightedEdges () {
78 for (int i = 0; i <getSize (); i ++) { có được trọng lượng cạnh

79 System.out.print (getVertex (i) + "(" + i + "): ");


80 for (Cạnh cạnh: Neighbor.get (i)) {
81 System.out.print ("(" + edge.u +
Machine Translated by Google

1068 Chương 29 Đồ thị có Trọng số và Ứng dụng

82 "," + edge.v + "," + ((WeightedEdge) edge) .weight + ") ");


83 }
84 System.out.println ();
85 }
86 }
87
88 / ** Thêm các cạnh vào biểu đồ có trọng số * /
thêm cạnh 89 public boolean addEdge (int u, int v, double weight) {
90 trả về addEdge (new WeightedEdge (u, v, weight));
91 }
92
93 / ** Lấy một cây bao trùm tối thiểu bắt nguồn từ đỉnh 0 * /
nhận MST 94 public MST getMinimumSpanningTree () {
bắt đầu từ đỉnh 0 95 trả về getMinimumSpanningTree (0);
96 }
97
98 / ** Lấy một cây bao trùm tối thiểu bắt nguồn từ một đỉnh được chỉ định * /
MST từ đỉnh bắt đầu 99 public MST getMinimumSpanningTree (int startedVertex ) {
100 // cost [v] lưu trữ chi phí bằng cách thêm v vào cây
101 double [] cost = new double [getSize ()];
102 for (int i = 0; i <cost.length; i ++) {
khởi tạo chi phí 103 chi phí [i] = Double.POSITIVE_INFINITY; // Chi phí ban đầu
104 }
105 chi phí [startedVertex] = 0; // Chi phí nguồn là 0
106
khởi tạo cha mẹ 107 int [] parent = new int [getSize ()]; // Đỉnh cha của một đỉnh
108 cha [startVertex] = -1; // startVertex là thư mục gốc
109 tổng gấp đôiWeight = 0; // Tổng trọng lượng của cây cho đến nay
110
cây bao trùm tối thiểu 111 Danh sách <Integer> T = new ArrayList <> ();
112
113 // Mở rộng T
114 while (T.size () <getSize ()) {
miễn trừ MST
cập nhật tổng chi phí 115 // Tìm chi phí nhỏ nhất v trong V - T int
116 u = -1; // Đỉnh được xác định
117 double currentMinCost = Double.POSITIVE_INFINITY;
118 for (int i = 0; i <getSize (); i ++) {
119 if (! T.contains (i) && cost [i] <currentMinCost) {
120 currentMinCost = chi phí [i];
đỉnh có coust nhỏ nhất 121 u = i;
122 }
123 }
124
thêm vào cây 125 T.add (u); // Thêm đỉnh mới vào T
126 totalWeight + = cost [u]; // Thêm giá [u] vào cây
127
128 // Điều chỉnh chi phí [v] cho v tiếp giáp với u và v trong V - T
điều chỉnh chi phí
129 for (Edge e: Neighbor.get (u)) {
130 if (! T.contains (ev) && cost [ev]> ((WeightedEdge) e) .weight) {
131 cost [ev] = ((WeightedEdge) e) .weight;
132 cha [ev] = u;
133 }
134 }
135 } // Hết thời gian
136
tạo MST 137 trả về MST mới (startVertex, cha, T, totalWeight);
138 }
139
140 / ** MST là một lớp bên trong trong WeightedGraph * /
MST lớp bên trong 141 MST lớp công khai mở rộng Cây {
Machine Translated by Google

29.3 Lớp đồ thị có trọng số 1069

142 tư nhân đôi tổngWeight; // Tổng trọng lượng của tất cả các cạnh trong cây tổng trọng lượng trong cây

143
144 public MST (int root, int [] parent, List <Integer> searchOrder,
145 tổng gấp đôi Trọng lượng) {
146 super (root, cha, searchOrder);
147 this.totalWeight = totalWeight;
148 }
149
150 public double getTotalWeight () {
151 trả về totalWeight;
152 }
153 }
154
155 / ** Tìm đường dẫn ngắn nhất nguồn duy nhất * /
156 public ShortestPathTree getShortestPath (int sourceVertex) { getShortestPath
157 // cost [v] lưu trữ chi phí của đường dẫn từ v đến nguồn
158 double [] cost = newdouble [getSize ()]; khởi tạo chi phí

159 for (int i = 0; i <cost.length; i ++) {


160 chi phí [i] = Double.POSITIVE_INFINITY; // Chi phí ban đầu được đặt thành vô cùng
161 }
162 chi phí [sourceVertex] = 0; // Chi phí nguồn là 0
163
164 // parent [v] lưu trữ đỉnh trước của v trong đường dẫn
165 int [] parent = newint [getSize ()];
166 cha [sourceVertex] = -1; // Nguồn gốc được đặt thành -1
167
168 // T lưu trữ các đỉnh có đường dẫn được tìm thấy cho đến nay
169 Danh sách <Integer> T = new ArrayList <> (); cây con đường ngắn nhất

170
171 // Mở rộng T
172 while (T.size () <getSize ()) { mở rộng cây
173 // Tìm chi phí nhỏ nhất v trong V - T int u
174 = -1; // Đỉnh được xác định
175 double currentMinCost = Double.POSITIVE_INFINITY;
176 for (int i = 0; i <getSize (); i ++) {
177 if (! T.contains (i) && cost [i] <currentMinCost) {
178 currentMinCost = chi phí [i];
179 u = i; đỉnh với chi phí nhỏ nhất

180 }
181 }
182
183 T.add (u); // Thêm đỉnh mới vào T thêm vào T

184
185 // Điều chỉnh chi phí [v] cho v tiếp giáp với u và v trong V - T
186 for (Edge e: Neighbor.get (u)) {
187 if (! T.contains (ev) &&
188 cost [ev]> cost [u] + ((WeightedEdge) e) .weight) {
189 cost [ev] = cost [u] + ((WeightedEdge) e) .weight; điều chỉnh chi

190 cha [ev] = u; phí điều chỉnh cha mẹ

191 }
192 }
193 } // Hết thời gian
194
195 // Tạo một ShortestPathTree
196 trả về ShortestPathTree mới (sourceVertex, cha, T, cost);
tạo một cái cây
197 }
198
199 / ** ShortestPathTree là một lớp bên trong trong WeightedGraph * /
200 public class ShortestPathTree Extended Tree { cây con đường ngắn nhất

201 tư nhân đôi [] chi phí; // cost [v] là chi phí từ v tới nguồn Giá cả
Machine Translated by Google

1070 Chương 29 Đồ thị có Trọng số và Ứng dụng

202
203 / ** Tạo đường dẫn * /
constructor 204 public ShortestPathTree (int source, int [] parent, List
205 <Integer> searchOrder, double [] cost) {
206 super (nguồn, cha, searchOrder);
207 this.cost = chi phí;
208 }
209
210 / ** Trả lại chi phí cho một đường dẫn từ gốc đến đỉnh v * /
nhận chi phí
211 public double getCost (int v) {
212 chi phí trả lại [v];
213 }
214
215 / ** In đường dẫn từ tất cả các đỉnh đến nguồn * /
in tất cả các đường dẫn
216 public void printAllPaths () {
217 System.out.println ("Tất cả các đường đi ngắn nhất từ" +
218 vertices.get (getRoot ()) + "là:");
219 for (int i = 0; i <cost.length; i ++) {
220 printPath (i); // In đường dẫn từ i đến nguồn
221 System.out.println ("(chi phí: " + chi phí [i] + ")"); // Chi phí đường dẫn
222 }
223 }
224 }
225}

Lớp WeightedGraph mở rộng lớp AbstractGraph (dòng 3). Các thuộc tính
các đỉnh và lân cận trong AbstractGraph được kế thừa trong WeightedGraph. hàng xóm là một danh
sách. Mỗi phần tử là danh sách là một danh sách khác có chứa các cạnh. Đối với đồ thị không
trọng số, mỗi cạnh là một thể hiện của AbstractGraph.Edge. Đối với đồ thị có trọng số, mỗi cạnh
là một thể hiện của WeightedEdge. WeightedEdge là một kiểu con của Edge. Vì vậy, bạn có thể thêm
một cạnh có trọng số vào Neighbor.get (i) cho một đồ thị có trọng số (dòng 47).
Liệt kê 29.3 đưa ra một chương trình thử nghiệm tạo một đồ thị cho đồ thị trong Hình 29.1 và một đồ

thị khác cho một đồ thị trong Hình 29.3a.

LISTING 29.3 TestWeightedGraph.java


1 lớp công khai TestWeightedGraph {
2 public static void main (String [] args) {
đỉnh 3 String [] vertices = {"Seattle", "San Francisco", "Los Angeles",
4 "Denver", "Kansas City", "Chicago", "Boston", "New York",
5 "Atlanta", "Miami", "Dallas", "Houston"};
6

các cạnh 7 int [] [] edge = {


8 {0, 1, 807}, {0, 3, 1331}, {0, 5, 2097},
9 {1, 0, 807}, {1, 2, 381}, {1, 3, 1267},
10 {2, 1, 381}, {2, 3, 1015}, {2, 4, 1663}, {2, 10, 1435},
11 {3, 0, 1331}, {3, 1, 1267}, {3, 2, 1015}, {3, 4, 599},
12 {3, 5, 1003},
13 {4, 2, 1663}, {4, 3, 599}, {4, 5, 533}, {4, 7, 1260},
14 {4, 8, 864}, {4, 10, 496},
15 {5, 0, 2097}, {5, 3, 1003}, {5, 4, 533},
16 {5, 6, 983}, {5, 7, 787},
17 {6, 5, 983}, {6, 7, 214},
18 {7, 4, 1260}, {7, 5, 787}, {7, 6, 214}, {7, 8, 888},
19 {8, 4, 864}, {8, 7, 888}, {8, 9, 661},
20 {8, 10, 781}, {8, 11, 810},
21 {9, 8, 661}, {9, 11, 1187},
22 {10, 2, 1435}, {10, 4, 496}, {10, 8, 781}, {10, 11, 239},
23 {11, 8, 810}, {11, 9, 1187}, {11, 10, 239}
Machine Translated by Google

29.3 Lớp Đồ thị Trọng lượng 1071

24 };
25
26 WeightedGraph <String> graph1 =
27 new WeightedGraph <> (đỉnh, cạnh); tạo đồ thị
28 System.out.println (" Số đỉnh trong graph1:"
29 + graph1.getSize ());
30 System.out.println ("Đỉnh có chỉ số 1 là"
31 + graph1.getVertex (1));
32 System.out.println (" Chỉ số cho Miami là" graph1.getIndex+
33 ("Miami"));
34 System.out.println ("Các cạnh của graph1:");
35 graph1.printWeightedEdges (); in cạnh
36

37 edge = new int [] [] { các cạnh

38 {0, 1, 2}, {0, 3, 8},


39 {1, 0, 2}, {1, 2, 7}, {1, 3, 3},
40 {2, 1, 7}, {2, 3, 4}, {2, 4, 5},
41 {3, 0, 8}, {3, 1, 3}, {3, 2, 4}, {3, 4, 6},
42 {4, 2, 5}, {4, 3, 6}
43 };
44 WeightedGraph <Integer> graph2 = new WeightedGraph <> (edge, 5); tạo đồ thị
45 System.out.println ("\ nCác cạnh cho graph2:");
46 graph2.printWeightedEdges (); in cạnh
47 }
48}

Số đỉnh trong đồ thị 1: 12


Đỉnh có chỉ số 1 là San Francisco
Chỉ số cho Miami là 9
Các cạnh cho đồ thị1:
Đỉnh 0: (0, 1, 807) (0, 3, 1331) (0, 5, 2097)
Đỉnh 1: (1, 2, 381) (1, 0, 807) (1, 3, 1267)
Đỉnh 2: (2, 1, 381) (2, 3, 1015) (2, 4, 1663) (2, 10, 1435)
Đỉnh 3: (3, 4, 599) (3, 5, 1003) (3, 1, 1267)
(3, 0, 1331) (3, 2, 1015)
Đỉnh 4: (4, 10, 496) (4, 8, 864) (4, 5, 533) (4, 2, 1663) (4, 7, 1260) (4,
3, 599)
Đỉnh 5: (5, 4, 533) (5, 7, 787) (5, 3, 1003)
(5, 0, 2097) (5, 6, 983)
Đỉnh 6: (6, 7, 214) (6, 5, 983)
Đỉnh 7: (7, 6, 214) (7, 8, 888) (7, 5, 787) (7, 4, 1260)
Đỉnh 8: (8, 9, 661) (8, 10, 781) (8, 4, 864)
(8, 7, 888) (8, 11, 810)
Đỉnh 9: (9, 8, 661) (9, 11, 1187)
Đỉnh 10: (10, 11, 239) (10, 4, 496) (10, 8, 781) (10, 2, 1435)
Đỉnh 11: (11, 10, 239) (11, 9, 1187) (11, 8, 810)

Các cạnh cho đồ thị2:


Đỉnh 0: (0, 1, 2) (0, 3, 8)
Đỉnh 1: (1, 0, 2) (1, 2, 7) (1, 3, 3)
Đỉnh 2: (2, 3, 4) (2, 1, 7) (2, 4, 5)
Đỉnh 3: (3, 1, 3) (3, 4, 6) (3, 2, 4) (3, 0, 8)
Đỉnh 4: (4, 2, 5) (4, 3, 6)

Chương trình tạo graph1 cho đồ thị trong Hình 29.1 trong các dòng 3–27. Các đỉnh của đồ thị
1 được xác định trong các dòng 3–5. Các cạnh của đồ thị 1 được xác định trong các dòng 7–24.
Các cạnh được biểu diễn bằng mảng hai chiều. Đối với mỗi hàng i trong mảng, các cạnh [i] [0]
và các cạnh [i] [1] chỉ ra rằng có một cạnh từ các cạnh đỉnh [i] [0] đến đỉnh
Machine Translated by Google

1072 Chương 29 Đồ thị có Trọng số và Ứng dụng

các cạnh [i] [1] và trọng số của cạnh là các cạnh [i] [2]. Ví dụ: {0, 1, 807}
(dòng 8) đại diện cho cạnh từ đỉnh 0 (các cạnh [0] [0]) đến đỉnh 1 (các cạnh [0] [1])
với trọng số 807 (cạnh [0] [2]). {0, 5, 2097} (dòng 8) biểu thị cạnh từ đỉnh tex 0
(cạnh [2] [0]) đến đỉnh 5 (cạnh [2] [1]) với trọng số 2097 (cạnh [2] [2] ).
Dòng 35 gọi phương thức printWeightedEdges () trên graph1 để hiển thị tất cả các cạnh trong
graph1.
Chương trình tạo các cạnh cho graph2 cho đồ thị trong Hình 29.3a trong các dòng 37–44.
Dòng 46 gọi phương thức printWeightedEdges () trên graph2 để hiển thị tất cả các cạnh trong
graph2.

29.3 Nếu một hàng đợi ưu tiên được sử dụng để lưu trữ các cạnh có trọng số, thì kết quả đầu ra sau đây là gì

Kiểm tra điểm


mã số?

PriorityQueue <WeightedEdge> q = new PriorityQueue <> ();


q.offer (new WeightedEdge (1, 2, 3.5));
q.offer ( WeightedEdge mới (1, 6, 6.5));
q.offer ( WeightedEdge mới (1, 7, 1.5));
System.out.println (q.poll (). Weight);
System.out.println (q.poll (). Weight);
System.out.println (q.poll (). Weight);

29.4 Nếu một hàng đợi ưu tiên được sử dụng để lưu trữ các cạnh có trọng số, điều gì sai trong đoạn mã sau?

Sửa chữa nó và hiển thị đầu ra.

Danh sách <PriorityQueue <WeightedEdge>> queues = new ArrayList <> ();


queues.get (0) .offer (new WeightedEdge (0, 2, 3.5));
queues.get (0) .offer (new WeightedEdge (0, 6, 6.5));
queues.get (0) .offer (new WeightedEdge (0, 7, 1.5));
queues.get (1) .offer (new WeightedEdge (1, 0, 3.5));
queues.get (1) .offer (new WeightedEdge (1, 5, 8.5));
queues.get (1) .offer (new WeightedEdge (1, 8, 19.5));
System.out.println (queues.get (0) .peek ()
.compareTo (queues.get (1) .peek ()));

29.4 Cây có khoảng cách tối thiểu


Cây bao trùm tối thiểu của đồ thị là cây bao trùm có tổng trọng số tối thiểu.
Chìa khóa

Điểm

cây bao trùm tối thiểu Một biểu đồ có thể có nhiều cây bao trùm. Giả sử rằng các cạnh có trọng số. Một cây bao trùm
tối thiểu có tổng trọng số tối thiểu. Ví dụ, các cây trong Hình 29.5b, 29.5c, 29.5d là cây
bao trùm cho đồ thị trong Hình 29.5a. Các cây trong Hình 29.3c và 29.3d là cây bao trùm tối
thiểu.
Bài toán tìm cây bao trùm tối thiểu có rất nhiều ứng dụng. Hãy xem xét một công ty hợp tác
với các chi nhánh ở nhiều thành phố. Công ty muốn thuê đường dây điện thoại để kết nối tất cả
các chi nhánh với nhau. Công ty điện thoại tính các khoản tiền khác nhau để kết nối các cặp
thành phố khác nhau. Có nhiều cách để kết nối tất cả các nhánh với nhau. Cách rẻ nhất là tìm
một cây bao trùm với tổng tỷ lệ tối thiểu.

29.4.1 Các thuật toán cây kéo dài tối thiểu


Làm thế nào để bạn tìm thấy một cây bao trùm tối thiểu? Có một số thuật toán nổi tiếng để làm
Thuật toán của Prim như vậy. Phần này giới thiệu thuật toán của Prim. Thuật toán Prim bắt đầu với một cây bao
trùm T chứa một đỉnh tùy ý. Thuật toán mở rộng cây bằng cách thêm nhiều lần một đỉnh có sự cố
cạnh chi phí thấp nhất vào một đỉnh đã có trong cây. Thuật toán của Prim là một thuật toán
tham lam và nó được mô tả trong Liệt kê 29.4.
Machine Translated by Google

29.4 Cây kéo dài tối thiểu 1073

10 10

6 7 5 số 8
5

số 8 10

5
số 8

7 7 số 8 5 7 7

12

(Một) (b)

6 5 6 5
7

5 7
5 7
số 8

7 số 8

(C) (d)

HÌNH 29.5 Cây trong (c) và (d) là cây khung nhỏ nhất của đồ thị trong (a).

DANH SÁCH 29.4 Thuật toán cây kéo dài tối thiểu của Prim
Đầu vào: Một trọng số vô hướng được kết nối G = (V, E) với các trọng số không âm
Đầu ra: MST (cây bao trùm tối thiểu)

Tối thiểu 1 MSTSpanningTree () {


2 Gọi T là tập các đỉnh trong cây khung;
Ban đầu, thêm đỉnh bắt đầu vào T; thêm đỉnh ban đầu
3 4

while (kích thước của T <n) { nhiều đỉnh hơn?

Tìm u trong T và v theo V - T có trọng lượng nhỏ nhất trên cạnh tìm một đỉnh

(u, v), như hình 29.6;


Thêm v vào T và đặt cha [v] = u; thêm vào cây

}
5 6 7 8 9 10}

Các ngành dọc đã có trong Các ngành dọc hiện không có trong

cây bao trùm V - T cây bao trùm

HÌNH 29.6 Tìm một đỉnh u trong T nối đỉnh v trong V - T với trọng số nhỏ nhất.

Thuật toán bắt đầu bằng cách thêm đỉnh bắt đầu vào T. Sau đó liên tục thêm một đỉnh (ví
dụ v) từ V - T vào T. v là đỉnh kề với đỉnh trong T có trọng số nhỏ nhất trên cạnh. Ví
dụ, có năm cạnh nối các đỉnh trong T và V - T chẳng hạn
Machine Translated by Google

1074 Chương 29 Đồ thị có Trọng số và Ứng dụng

được thể hiện trong Hình 29.6, và (u, v) là vật có trọng lượng nhỏ nhất. Xem xét đồ thị trong hình 29.7. Thuật

toán thêm các đỉnh vào T theo thứ tự sau:

1. Thêm đỉnh 0 vào T.

2. Thêm đỉnh 5 vào T, vì Cạnh (5, 0, 5) có trọng số nhỏ nhất trong số tất cả các cạnh tạo thành đỉnh trong

T, như thể hiện trong Hình 29.7a. Dòng mũi tên từ 0 đến 5 cho biết 0 là con của 5.

3. Thêm đỉnh 1 vào T, vì Cạnh (1, 0, 6) có trọng số nhỏ nhất trong tất cả các cạnh inci

vết lõm đến một đỉnh ở T, như thể hiện trong Hình 29.7b.

4. Thêm đỉnh 6 vào T, vì Cạnh (6, 1, 7) có trọng số nhỏ nhất trong tất cả các cạnh inci

vết lõm đến một đỉnh ở T, như thể hiện trong Hình 29.7c.

1 10 2 1 10 2
T

6 7 5 số 8
6 7 5 số 8

0 0
số 8
6 10 3 số 8
6 10 3

5 5
7 7 số 8 7 7 số 8

T
5 12 4 5 12 4

(Một) (b)

T 1 10 2 1 10 2
T

6 7 5 số 8
6 7 5 số 8

0 0
số 8
6 10 3 số 8
6 10 3

5 5
7 7 số 8 7 7 số 8

5 12 4 5 12 4

(C) (d)

1 10 2 1 10 2
T T

6 7 5 số 8
6 7 5 số 8

0 0
số 8
6 10 3 số 8
6 10 3

5 5
7 7 số 8 7 7 số 8

5 12 4 5 12 4

(e) (f)

HÌNH 29.7 Các đỉnh liền kề có trọng số nhỏ nhất được cộng liên tiếp vào T.
Machine Translated by Google

29,4 Cây kéo dài tối thiểu 1075

5. Thêm đỉnh 2 vào T, vì Cạnh (2, 6, 5) có trọng số nhỏ nhất trong tất cả các cạnh
sự cố đến một đỉnh trong T, như trong Hình 29.7d.

6. Thêm đỉnh 4 vào T, vì Cạnh (4, 6, 7) có trọng số nhỏ nhất trong tất cả các cạnh
sự cố đến một đỉnh trong T, như trong Hình 29.7e.

7. Thêm đỉnh 3 vào T, vì Cạnh (3, 2, 8) có trọng số nhỏ nhất trong tất cả các cạnh
sự cố đến một đỉnh trong T, như trong Hình 29.7f.

Ghi chú

Cây bao trùm tối thiểu không phải là duy nhất. Ví dụ, cả (c) và (d) trong Hình 29.5 đều là cây độc đáo?
cây khung tối thiểu cho đồ thị trong Hình 29.5a. Tuy nhiên, nếu các trọng số là khác nhau,

biểu đồ có một cây bao trùm tối thiểu duy nhất.

Ghi chú

Giả sử rằng đồ thị được kết nối và vô hướng. Nếu một đồ thị không được kết nối hoặc có được kết nối và vô hướng

hướng, thuật toán sẽ không hoạt động. Bạn có thể sửa đổi thuật toán để tìm một khu rừng bao

trùm cho bất kỳ biểu đồ vô hướng nào. Rừng bao trùm là một biểu đồ trong đó mỗi thành phần

được kết nối là một cây.

29.4.2 Tinh chỉnh thuật toán MST của Prim


Để dễ dàng xác định đỉnh tiếp theo để thêm vào cây, chúng ta sử dụng cost [v] để lưu chi phí
của việc thêm đỉnh v vào cây khung T. Ban đầu giá [s] là 0 cho một đỉnh bắt đầu và gán giá
trị vô cùng. giá thành [v] cho tất cả các đỉnh khác. Thuật toán liên tục tìm một đỉnh u trong
V - T với chi phí nhỏ nhất [u] và chuyển u đến T. Phiên bản tinh chỉnh của alogrithm được
đưa ra trong Liệt kê 29.5.

DANH SÁCH 29.5 Phiên bản tinh chỉnh của thuật toán Prim
Đầu vào: Một trọng số vô hướng được kết nối G = (V, E) với các trọng số không âm
Đầu ra: một cây bao trùm tối thiểu với đỉnh bắt đầu s là gốc

1 MST getMinimumSpanngedTree {
2 Gọi T là tập chứa các đỉnh trong cây khung;
3 Ban đầu T rỗng;
4 Đặt chi phí [s] = 0; và cost [v] = infinity cho tất cả các đỉnh khác trong V;
5
while (kích thước của T <n) {
Tìm u không thuộc T với chi phí [u] nhỏ nhất; tìm đỉnh tiếp theo

Thêm u vào T; thêm một đỉnh vào T

for (mỗi v không thuộc T và (u, v) thuộc E)


6 if (cost [v]> w (u, v)) {// Điều chỉnh chi phí [v]
7 8 9 10 11 chi phí [v] = w (u, v); cha [v] = u; điều chỉnh chi phí [v]

12 }
13 }
14}

29.4.3 Thực hiện thuật toán MST


Phương thức getMinimumSpanningTree (int v) được định nghĩa trong lớp WeightedGraph . getMinimumSpanningTree ()
Nó trả về một thể hiện của lớp MST , như trong Hình 29.4. Lớp MST được định nghĩa là một
lớp bên trong trong lớp WeightedGraph , lớp này mở rộng lớp Cây , như thể hiện trong Hình
29.8. Lớp Cây được thể hiện trong Hình 28.11. Lớp MST được triển khai trong các dòng 141–
153 trong Liệt kê 29.2.
Machine Translated by Google

1076 Chương 29 Đồ thị có Trọng số và Ứng dụng

AbstractGraph.Tree

WeightedGraph.MST

-totalWeight: int Tổng trọng lượng của cây.

+ MST (root: int, parent: int [], searchOrder: Xây dựng một MST với gốc được chỉ định, mảng mẹ, đơn
Danh sách <Integer> totalWeight: int) đặt hàng tìm kiếm và tổng trọng lượng cho cây.

+ getTotalWeight (): int Trả về tổng Trọng lượng của cây.

HÌNH 29.8 Lớp MST mở rộng lớp Cây .

Phiên bản tinh chỉnh của thuật ngữ Prim giúp đơn giản hóa việc triển khai một cách đáng kể. Phương

thức getMinimumSpanningTree được triển khai bằng cách sử dụng phiên bản tinh chỉnh của thuật toán Prim

trong các dòng 99–138 trong Liệt kê 29.2. GetMinimumSpanningTree (int


Phương thức startVertex ) đặt cost [startedVertex] thành 0 (dòng 105) và giá [v] đến vô cùng cho tất cả

các đỉnh khác (dòng 102–104). Giá trị gốc của startVertex được đặt thành -1 (dòng 108). T là danh sách

lưu trữ các đỉnh được thêm vào cây khung (dòng 111). Chúng tôi sử dụng một danh sách cho T thay vì một
tập hợp để ghi lại thứ tự của các đỉnh được thêm vào T.

Ban đầu, T là trống. Để mở rộng T, phương thức thực hiện các hoạt động sau:

1. Tìm đỉnh u có chi phí [u] nhỏ nhất (dòng 118–123 và cộng nó vào T (dòng 125).

2. Sau khi thêm u trong T, cập nhật cost [v] và cha [v] cho mỗi v liền kề với u trong VT nếu cost [v]>
w (u, v) (dòng 129–134).

Sau khi một đỉnh mới được thêm vào T, totalWeight được cập nhật (dòng 126). Khi tất cả các đỉnh được
thêm vào T, một thể hiện của MST sẽ được tạo (dòng 137). Lưu ý rằng phương pháp sẽ không hoạt động nếu

đồ thị không được kết nối. Tuy nhiên, bạn có thể sửa đổi nó để lấy MST một phần.

Lớp MST mở rộng lớp Cây (dòng 141). Để tạo một phiên bản của MST, hãy chuyển root,
parent, T và totalWeight (dòng 144-145). Các trường dữ liệu root, cha và searchOrder
được định nghĩa trong lớp Tree , là một lớp bên trong được định nghĩa trong AbstractGraph.

Lưu ý rằng việc kiểm tra xem đỉnh i có nằm trong T hay không bằng cách gọi T.conatins (i) mất O (n)
thời gian phức tạp thời gian, vì T là một danh sách. Do đó, độ phức tạp thời gian tổng thể cho việc triển khai này là O

(n3 ). Độc giả liên tục có thể xem Bài tập Lập trình 29.20 để cải thiện việc triển khai và giảm độ phức

tạp xuống O (n2 ).

Liệt kê 29.6 đưa ra một chương trình thử nghiệm hiển thị các cây bao trùm tối thiểu cho biểu đồ trong

Hình 29.1 và đồ thị trong Hình 29.3a tương ứng.

LISTING 29.6 TestMinimumSpanningTree.java


1 lớp công khai TestMinimumSpanningTree {
2 public static void main (String [] args) {
3 String [] vertices = {"Seattle", "San Francisco", "Los Angeles",
tạo đỉnh
4 "Denver", "Kansas City", "Chicago", "Boston", "New York",
5 "Atlanta", "Miami", "Dallas", "Houston"};
6

7 int [] [] edge = {
tạo các cạnh
8 {0, 1, 807}, {0, 3, 1331}, {0, 5, 2097},
9 {1, 0, 807}, {1, 2, 381}, {1, 3, 1267},
10 {2, 1, 381}, {2, 3, 1015}, {2, 4, 1663}, {2, 10, 1435},
11 {3, 0, 1331}, {3, 1, 1267}, {3, 2, 1015}, {3, 4, 599},
Machine Translated by Google

29,4 Cây kéo dài tối thiểu 1077

12 {3, 5, 1003},
13 {4, 2, 1663}, {4, 3, 599}, {4, 5, 533}, {4, 7, 1260},
14 {4, 8, 864}, {4, 10, 496},
15 {5, 0, 2097}, {5, 3, 1003}, {5, 4, 533},
16 {5, 6, 983}, {5, 7, 787},
17 {6, 5, 983}, {6, 7, 214},
18 {7, 4, 1260}, {7, 5, 787}, {7, 6, 214}, {7, 8, 888},
19 {8, 4, 864}, {8, 7, 888}, {8, 9, 661},
20 {8, 10, 781}, {8, 11, 810},
21 {9, 8, 661}, {9, 11, 1187},
22 {10, 2, 1435}, {10, 4, 496}, {10, 8, 781}, {10, 11, 239},
23 {11, 8, 810}, {11, 9, 1187}, {11, 10, 239}
24 };
25

26 WeightedGraph <String> graph1 = tạo đồ thị1


27 new WeightedGraph <> (đỉnh, cạnh);
28 WeightedGraph <String> .MST tree1 = graph1.getMinimumSpanningTree (); MST cho graph1
29 System.out.println ("Tổng trọng lượng là" + tree1.getTotalWeight ()); Tổng khối lượng
30 tree1.printTree (); cây in
31
32 edge = new int [] [] { tạo các cạnh
33 {0, 1, 2}, {0, 3, 8},
34 {1, 0, 2}, {1, 2, 7}, {1, 3, 3},
35 {2, 1, 7}, {2, 3, 4}, {2, 4, 5},
36 {3, 0, 8}, {3, 1, 3}, {3, 2, 4}, {3, 4, 6},
37 {4, 2, 5}, {4, 3, 6}
38 };
39
40 WeightedGraph <Integer> graph2 = new WeightedGraph <> (edge, 5); tạo đồ thị2
41 WeightedGraph <Integer> .MST tree2 =
42 graph2.getMinimumSpanningTree (1); MST cho graph2
43 System.out.println ("\ nTổng trọng lượng là" + tree2.getTotalWeight ()); Tổng khối lượng
44 tree2.printTree (); cây in
45 }
46}

Tổng trọng lượng là 6513,0


Gốc là: Seattle
Các cạnh: (Seattle, San Francisco) (San Francisco, Los Angeles)
(Los Angeles, Denver) (Denver, Thành phố Kansas) (Thành phố Kansas, Chicago)
(New York, Boston) (Chicago, New York) (Dallas, Atlanta)
(Atlanta, Miami) (Thành phố Kansas, Dallas) (Dallas, Houston)

Tổng trọng lượng là 14,0


Gốc là: 1
Các cạnh: (1, 0) (3, 2) (1, 3) (2, 4)

Chương trình tạo một đồ thị có trọng số cho Hình 29.1 ở dòng 27. Sau đó, nó gọi
getMinimumSpanningTree () (dòng 28) để trả về một MST đại diện cho một cây bao trùm tối
thiểu cho đồ thị. Gọi printTree () (dòng 30) trên đối tượng MST hiển thị các cạnh trong
cây. Lưu ý rằng MST là một lớp con của Cây. Phương thức printTree () được định nghĩa
trong lớp Tree .

Hình minh họa đồ họa của cây bao trùm tối thiểu được thể hiện trong Hình 29.9. Các minh họa đồ họa
sắc độ được thêm vào cây theo thứ tự này: Seattle, San Francisco, Los Angeles, Denver,
Kansas City, Dallas, Houston, Chicago, New York, Boston, Atlanta và Miami.
Machine Translated by Google

1078 Chương 29 Đồ thị có Trọng số và Ứng dụng

Seattle

1 2097 Boston

983
Chicago 9
1331 214
807 số 8

1003
787 Newyork
7
Denver
533
1267 1260
599
3
888
San Francisco 1015 4

Thành phố Kansas


381 2 1663
864

496
1435 10 Atlanta
Los Angeles 5 781

810
Dallas
6 661
239

Houston 1187
11

Miami

HÌNH 29.9 Các cạnh trong cây bao trùm tối thiểu cho các thành phố được tô sáng.

29.5 Tìm một cây khung nhỏ nhất cho đồ thị sau.

1 10 2

5 7 7 số 8

0
2 6 10 3

5
7 7 số 8

5 2 4

29.6 Cây khung tối thiểu có phải là duy nhất không nếu tất cả các cạnh có trọng số khác nhau?

29.7 Nếu bạn sử dụng ma trận kề để biểu diễn các cạnh có trọng số, độ phức tạp về thời gian cho thuật toán

Prim sẽ là bao nhiêu?

29.8 Điều gì xảy ra với phương thức getMinimumSpanningTree () trong WeightedGraph nếu đồ thị không
được kết nối? Xác minh câu trả lời của bạn bằng cách viết một chương trình thử nghiệm tạo
ra một đồ thị không được kết nối và gọi phương thức getMinimumSpanningTree () .
Làm cách nào để bạn khắc phục sự cố bằng cách lấy một phần MST?

29.5 Tìm những con đường ngắn nhất


Đường đi ngắn nhất giữa hai đỉnh là đường đi có tổng trọng số nhỏ nhất.

Đưa ra một biểu đồ có trọng số không âm trên các cạnh, một thuật toán nổi tiếng để tìm đường đi ngắn
nhất giữa hai đỉnh đã được phát hiện bởi Edsger Dijkstra, một nhà khoa học máy tính người Hà Lan. Để
tìm đường đi ngắn nhất từ đỉnh s đến đỉnh v, thuật toán Dijkstra tìm
Machine Translated by Google

29,5 Tìm đường đi ngắn nhất 1079

đường đi ngắn nhất từ s đến tất cả các đỉnh. Vì vậy, thuật toán Dijkstra được gọi là thuật toán con đường ngắn nhất đơn nguồn

đường đi ngắn nhất một nguồn. Thuật toán sử dụng cost [v] để lưu trữ chi phí của một đường đi ngắn
nhất từ đỉnh v đến đỉnh nguồn s. cost [s] là 0. Ban đầu gán giá trị vô cùng cho cost [v] cho tất cả
các đỉnh khác. Thuật toán liên tục tìm một đỉnh u trong V - T với chi phí [u] nhỏ nhất và chuyển u đến T.
Thuật toán được mô tả trong Liệt kê 29.7.

DANH SÁCH 29.7 Thuật toán đường dẫn ngắn nhất nguồn đơn của
Dijkstra
Đầu vào: đồ thị G = (V, E) với trọng số không âm
Đầu ra: một cây đường đi ngắn nhất với đỉnh nguồn là gốc

1 ShortestPathTree getShortestPath {
2 Gọi T là tập hợp chứa các đỉnh có 3 đường đi đến s đã biết;
Ban đầu T rỗng;
4 Đặt chi phí [s] = 0; và cost [v] = infinity cho tất cả các đỉnh khác trong V;
5
while (kích thước của T <n) {
Tìm u không thuộc T với chi phí [u] nhỏ nhất; tìm đỉnh tiếp theo

Thêm u vào T; thêm một đỉnh vào T

for (mỗi v không thuộc T và (u, v) thuộc E) if


6 (cost [v]> cost [u] + w (u, v)) {
7 8 9 10 11 cost [v] = cost [u] + w (u, v); cha [v] = u; điều chỉnh chi phí [v]

12 }
13 }
14}

Thuật toán này rất giống với thuật toán Prim để tìm một cây bao trùm tối thiểu. Cả hai thuật toán đều
chia các đỉnh thành hai tập: T và V - T. Trong trường hợp của thuật toán Prim, tập T chứa các đỉnh
đã được thêm vào cây. Trong trường hợp của Dijkstra, tập T chứa các đỉnh có đường đi ngắn nhất đến
nguồn đã được tìm thấy. Cả hai thuật toán liên tục tìm một đỉnh từ V - T và thêm nó vào T. Trong
trường hợp của thuật toán Prim, đỉnh kề với đỉnh nào đó trong tập hợp có trọng số nhỏ nhất trên
cạnh. Trong thuật toán Dijkstra, đỉnh nằm kề với một số đỉnh trong tập hợp với tổng chi phí tối thiểu
cho nguồn.
Thuật toán bắt đầu bằng cách đặt cost [s] thành 0 (dòng 4), đặt cost [v] thành vô cùng cho tất cả
các đỉnh khác. Sau đó, nó liên tục thêm một đỉnh (giả sử u) từ V - T vào T với chi phí nhỏ nhất [u]
(dòng 7–8), như trong Hình 29.10a. Sau khi thêm u vào T, chi phí cập nhật thuật toán [v]
và cha [v] cho mỗi v không thuộc T nếu (u, v) thuộc T và giá [v]> giá [u] + w (u, v)
(dòng 10–11).

T chứa V - T chứa các đỉnh có đỉnh ngắn nhất T chứa V - T chứa các đỉnh có đỉnh ngắn nhất
đỉnh có đường dẫn đến s vẫn chưa được biết. đỉnh có đường dẫn đến s vẫn chưa được biết.

con đường ngắn nhất đến s con đường ngắn nhất đến s
đươ c biêt đên đươ c biêt đên

V - T V - T

v1 v1
u
T T

v2 v2

S S
u

v3 v3

(a) Trước khi chuyển u đến T (b) Sau khi chuyển u đến T

HÌNH 29.10 (a) Tìm một đỉnh u trong V - T với chi phí [u] nhỏ nhất. (b) Chi phí cập nhật [v] cho v trong V - T và v nằm kề với
u.
Machine Translated by Google

1080 Chương 29 Đồ thị có Trọng số và Ứng dụng

Hãy để chúng tôi minh họa thuật toán Dijkstra bằng cách sử dụng đồ thị trong hình 29.11a. Giả sử

đỉnh nguồn là 1. Do đó, chi phí [1] = 0 và chi phí cho tất cả các đỉnh khác ban đầu là ∞, như thể hiện

trong Hình 29.11b. Chúng tôi sử dụng cha mẹ [i] để biểu thị cha mẹ của i trong đường dẫn. Để thuận

tiện, hãy đặt nút cha của nút nguồn thành -1.

1 10 3

5 9 5 số 8
Giá cả

2 0 1 2 3 4 5 6
số 8
6 số 8
4

cha mẹ
1
5 –1
2 7
0 1 2 3 4 5 6

0 4 5

(Một) (b)

HÌNH 29.11 Thuật toán sẽ tìm tất cả các đường đi ngắn nhất từ đỉnh nguồn 1.

Ban đầu tập T rỗng. Thuật toán chọn đỉnh có chi phí nhỏ nhất. Trong trường hợp này, đỉnh là 1.

Thuật toán thêm 1 vào T, như trong Hình 29.12a. Afterwrads, nó điều chỉnh chi phí cho mỗi đỉnh kề với

1. Chi phí cho các đỉnh 2, 0, 6 và 3 và cha mẹ của chúng bây giờ được cập nhật, như được hiển thị,

như trong Hình 29.12b.

T
1 10 3

5 9 5 số 8
Giá cả

8 0 5 10 9

2 0 1 2 3 4 5 6
số 8
6 số 8
4
cha mẹ
1
5 1 –1 1 1 1
2 7
0 1 2 3 4 5 6

0 4 5

(Một) (b)

HÌNH 29.12 Bây giờ đỉnh 1 nằm trong tập T.

Các đỉnh 2, 0, 6 và 3 liền kề với đỉnh nguồn và đỉnh 2 là đỉnh trong VT

với chi phí nhỏ nhất, vì vậy hãy thêm 2 vào T, như thể hiện trong Hình 29.13 và cập nhật chi phí và

giá trị cha cho các đỉnh trong VT và liền kề với 2. cost [0] hiện được cập nhật thành 6 và cha của nó

được đặt thành 2. Mũi tên dòng từ 1 đến 2 cho biết 1 là cha của 2 sau khi 2 được thêm vào T.
Machine Translated by Google

29,5 Tìm đường đi ngắn nhất 1081

T 1 10 3

5 9 5 số 8
Giá cả

6 0 10 5 9

2 0 1 2 3 4 5 6
số 8
6 số 8
4

cha mẹ
1
5 2 –1 1 1 1
2 7
0 1 2 3 4 5 6

0 4 5

(Một) (b)

HÌNH 29.13 Bây giờ các đỉnh 1 và 2 nằm trong tập T.

Bây giờ T chứa {1, 2}. Đỉnh 0 là đỉnh trong VT có chi phí nhỏ nhất, vì vậy hãy thêm 0 vào T, như

thể hiện trong Hình 29.14 và cập nhật chi phí và đỉnh cha cho các đỉnh trong VT và liền kề với 0
nếu có. cost [5] hiện được cập nhật thành 10 và giá trị chính của nó được đặt thành 0 và giá thành [6] hiện được cập

nhật thành 8 và giá trị gốc của nó được đặt thành 0.

T 1 10 3

5 9 5 số 8
Giá cả

6 0 10 5 10 8

2 0 1 2 3 4 5 6
số 8
6 số 8
4

cha mẹ
1
5 2 –1 1 1 0 0
2 7
0 1 2 3 4 5 6

0 4 5

(Một) (b)

HÌNH 29.14 Bây giờ các đỉnh {1, 2, 0} nằm trong tập T.

Bây giờ T chứa {1, 2, 0}. Đỉnh 6 là đỉnh trong VT có chi phí nhỏ nhất, vì vậy hãy thêm 6 vào T, như

thể hiện trong Hình 29.15 và cập nhật chi phí và đỉnh cha cho các đỉnh trong VT và liền kề với 6

nếu có.

T 1 10 3

5 9 5 số 8
Giá cả

6 0 10 5 10 8

2 0 1 2 3 4 5 6
số 8
6 số 8
4

cha mẹ
1
5 2 1 –1 1 0 0
2 7
0 1 2 3 4 5 6

0 4 5

(Một) (b)

HÌNH 29.15 Bây giờ các đỉnh {1, 2, 0, 6} nằm trong tập T.
Machine Translated by Google

1082 Chương 29 Đồ thị có Trọng số và Ứng dụng

Bây giờ T chứa {1, 2, 0, 6}. Đỉnh 3 hoặc 5 là đỉnh trong VT có chi phí nhỏ nhất. Bạn có thể thêm 3 hoặc 5 vào T. Hãy

để chúng tôi thêm 3 vào T, như thể hiện trong Hình 29.16 và cập nhật chi phí và giá trị gốc cho các đỉnh trong VT và

liền kề với 3 nếu có. cost [4] hiện được cập nhật lên 18

và cha của nó được đặt thành 3.

T 1 10 3

5 9 5 số 8
Giá cả

6 0 5 10 18 10 8

2 0 1 2 3 4 5 6
số 8
6 số 8
4
cha mẹ
1
5 2 1 –1 1 0 3 0
2 7
0 1 2 3 4 5 6

0 4 5

(Một) (b)

HÌNH 29.16 Bây giờ các đỉnh {1, 2, 0, 6, 3} nằm trong tập T.

Bây giờ T chứa {1, 2, 0, 6, 3}. Đỉnh 5 là đỉnh trong VT có chi phí nhỏ nhất, vì vậy hãy thêm 5

thành T, như thể hiện trong Hình 29.17 và cập nhật chi phí và giá trị gốc cho các đỉnh trong VT và liền kề với 5 nếu

có. cost [4] hiện được cập nhật thành 10 và giá trị gốc của nó được đặt thành 5.

T 1 10 3

5 9 5 số 8
Giá cả

6 0 5 10 15 10 8

2 0 1 2 3 4 5 6
số 8
6 số 8
4
cha mẹ
1
2 7 5 2 1 –1 1 0 5 0

0 1 2 3 4 5 6

0 4 5

(Một) (b)

HÌNH 29.17 Bây giờ các đỉnh {1, 2, 0, 6, 3, 5} nằm trong tập T.

Bây giờ T chứa {1, 2, 0, 6, 3, 5}. Đỉnh 4 là đỉnh trong VT có chi phí nhỏ nhất, vì vậy hãy thêm 4 vào T, như trong

Hình 29.18.

Như bạn có thể thấy, thuật toán về cơ bản tìm tất cả các đường đi ngắn nhất từ một ver nguồn, tạo ra một cây bắt

nguồn từ đỉnh nguồn. Chúng tôi gọi cây này là cây đường đi ngắn nhất một nguồn (hoặc đơn giản là cây đường đi ngắn

cây đường ngắn nhất nhất). Để tạo mô hình cây này, hãy xác định một lớp có tên là ShortestPathTree mở rộng lớp Cây , như thể hiện trong

Hình 29.19.

ShortestPathTree được định nghĩa là một lớp bên trong trong WeightedGraph ở các dòng 200–224
trong Liệt kê 29.2.
Phương thức ThegetShortestPath (int sourceVertex) được triển khai trong các dòng 156–197
trong Liệt kê 29.2. Phương pháp đặt giá [sourceVertex] thành 0 (dòng 162) và giá [v] thành
Machine Translated by Google

29,5 Tìm đường đi ngắn nhất 1083

T 1 10 3

5 9 5 số 8
Giá cả

6 0 5 10 15 1 0 8

2 0 1 2 3 4 5 6
số 8
6 số 8
4

cha mẹ
1
5 2 1 –1 1 0 5 0
2 7
0 1 2 3 4 5 6

0 4 5

(Một) (b)

HÌNH 29.18 Bây giờ các đỉnh {1, 2, 6, 0, 3, 5, 4} nằm trong tập T.

AbstractGraph.Tree

WeightedGraph.ShortestPathTree

-cost: int [] cost [v] lưu trữ chi phí cho đường dẫn từ nguồn đến v.

+ ShortestPathTree (nguồn: int, parent: int [], Xây dựng cây đường dẫn ngắn nhất với nguồn được chỉ
searchOrder: List <Integer>, cost: int []) định, mảng mẹ, lệnh tìm kiếm và mảng chi phí.
+ getCost (v: int): int Trả về chi phí cho đường dẫn từ nguồn đến đỉnh v.
+ printAllPaths (): void Hiển thị tất cả các đường dẫn từ nguồn.

HÌNH 29.19 WeightedGraph.ShortestPathTree mở rộng AbstractGraph.Tree.

vô cùng cho tất cả các đỉnh khác (dòng 159–161). Cha của sourceVertex được đặt thành -1

(dòng 166). T là danh sách lưu trữ các đỉnh được thêm vào cây đường đi ngắn nhất (dòng 169). Chúng tôi
sử dụng một danh sách cho T thay vì một tập hợp để ghi lại thứ tự của các đỉnh được thêm vào T.

Ban đầu, T là trống. Để mở rộng T, phương thức thực hiện các hoạt động sau:

1. Tìm đỉnh u có chi phí [u] nhỏ nhất (dòng 175–181) và cộng nó vào T (dòng 183).

2. Sau khi thêm u trong T, hãy cập nhật chi phí [v] và cha [v] cho mỗi v liền kề với u trong VT nếu

cost [v]> cost [u] + w (u, v) (dòng 186–192).

Khi tất cả các đỉnh từ s được thêm vào T, một thể hiện của ShortestPathTree được tạo (dòng 196). Lớp ShortestPathTree

Lớp ShortestPathTree mở rộng lớp Cây (dòng 200). Để tạo một phiên bản của
ShortestPathTree, hãy chuyển sourceVertex, parent, T và cost (dòng 204–205). sourceVertex
trở thành gốc trong cây. Các trường dữ liệu root, cha và searchOrder
được định nghĩa trong lớp Tree , là một lớp bên trong được định nghĩa trong AbstractGraph.

Lưu ý rằng việc kiểm tra xem đỉnh i có nằm trong T hay không bằng cách gọi T.conatins (i) mất O (n) thời gian,).
3
vì T là một danh sách. Do đó, độ phức tạp về thời gian tổng thể cho việc triển khai này là O (n chôn cất Độ phức tạp thời gian của thuật

Bạn đọc mong muốn có thể xem Bài tập Lập trình 29.20 để cải thiện việc triển khai và giảm độ phức tạp xuống toán Dijkstra

2
O (n ).

Thuật toán Dijkstra là sự kết hợp giữa thuật toán tham lam và lập trình động. Nó là một thuật toán tham lập trình tham lam
lam theo nghĩa là nó luôn thêm một đỉnh mới có khoảng cách ngắn nhất đến nguồn. Nó lưu trữ khoảng cách và năng động

ngắn nhất của mỗi đỉnh đã biết đến nguồn và sử dụng nó sau này để tránh tính toán dư thừa, vì vậy thuật

toán Dijkstra cũng sử dụng lập trình động.


Machine Translated by Google

1084 Chương 29 Đồ thị có Trọng số và Ứng dụng

Lưu ý sư phạm
Truy cập www.cs.armstrong.edu/liang/animation/ShortestPathAnimation.html để sử dụng chương trình GUI

hoạt ảnh đường đi ngắn nhất trên liên hoạt động nhằm tìm đường đi ngắn nhất giữa hai thành phố bất kỳ, như trong Hình 29.20.

Trang web đồng hành

HÌNH 29.20 Công cụ hoạt ảnh hiển thị đường đi ngắn nhất giữa hai thành phố.

Liệt kê 29.8 đưa ra một chương trình thử nghiệm hiển thị các đường đi ngắn nhất từ Chicago đến tất

cả các thành phố khác trong Hình 29.1 và các đường đi ngắn nhất từ đỉnh 3 đến tất cả các đỉnh của đồ thị

trong Hình 29.3a, tương ứng.

LISTING 29.8 TestShortestPath.java


1 lớp công khai TestShortestPath {
2 public static void main (String [] args) {
đỉnh 3 String [] vertices = {"Seattle", "San Francisco", "Los Angeles",
4 "Denver", "Kansas City", "Chicago", "Boston", "New York",
5 "Atlanta", "Miami", "Dallas", "Houston"};
6

các cạnh int [] [] edge = {


{0, 1, 807}, {0, 3, 1331}, {0, 5, 2097},
{1, 0, 807}, {1, 2, 381}, {1, 3, 1267},
7 8 9 10 {2, 1, 381}, {2, 3, 1015}, {2, 4, 1663}, {2, 10, 1435},
11 {3, 0, 1331}, {3, 1, 1267}, {3, 2, 1015}, {3, 4, 599},
12 {3, 5, 1003},
13 {4, 2, 1663}, {4, 3, 599}, {4, 5, 533}, {4, 7, 1260},
14 {4, 8, 864}, {4, 10, 496},
15 {5, 0, 2097}, {5, 3, 1003}, {5, 4, 533},
16 {5, 6, 983}, {5, 7, 787},
17 {6, 5, 983}, {6, 7, 214},
18 {7, 4, 1260}, {7, 5, 787}, {7, 6, 214}, {7, 8, 888},
19 {8, 4, 864}, {8, 7, 888}, {8, 9, 661},
20 {8, 10, 781}, {8, 11, 810},
21 {9, 8, 661}, {9, 11, 1187},
Machine Translated by Google

29,5 Tìm đường đi ngắn nhất 1085

22 {10, 2, 1435}, {10, 4, 496}, {10, 8, 781}, {10, 11, 239},


23 {11, 8, 810}, {11, 9, 1187}, {11, 10, 239}
24 };
25
26 WeightedGraph <String> graph1 =
27 new WeightedGraph <> (đỉnh, cạnh); tạo đồ thị1
28 WeightedGraph <String> .ShortestPathTree tree1 =
29 graph1.getShortestPath (graph1.getIndex ("Chicago")); con đường ngắn nhất

30 tree1.printAllPaths ();
31
32 // Hiển thị đường đi ngắn nhất từ Houston đến Chicago
33 System.out.print ("Đường ngắn nhất từ Houston đến Chicago: ");
34 java.util.List <String> path =
35 tree1.getPath (graph1.getIndex ("Houston"));
36 for (String s: path) {
37 System.out.print (s + " ");
38 }
39 tạo các cạnh
40 edge = new int [] [] {
41 {0, 1, 2}, {0, 3, 8},
42 {1, 0, 2}, {1, 2, 7}, {1, 3, 3},
43 {2, 1, 7}, {2, 3, 4}, {2, 4, 5},
44 {3, 0, 8}, {3, 1, 3}, {3, 2, 4}, {3, 4, 6},
45 {4, 2, 5}, {4, 3, 6}
46 };
47 WeightedGraph <Integer> graph2 = new WeightedGraph <> (edge, 5); tạo đồ thị2
48 WeightedGraph <Integer> .ShortestPathTree tree2 =
49 graph2.getShortestPath (3);
50 System.out.println ("\ n");
51 tree2.printAllPaths (); đường dẫn in
52 }
53}

Tất cả các con đường ngắn nhất từ Chicago là:


Một con đường từ Chicago đến Seattle: Chicago Seattle (chi phí: 2097.0)
Một con đường từ Chicago đến San Francisco:
Chicago Denver San Francisco (giá: 2270.0)
Một con đường từ Chicago đến Los Angeles:
Chicago Denver Los Angeles (chi phí: 2018.0)
Một con đường từ Chicago đến Denver: Chicago Denver (chi phí: 1003.0)
Một con đường từ Chicago đến Kansas City: Chicago Kansas City (chi phí: 533.0)
Đường đi từ Chicago đến Chicago: Chicago (chi phí: 0.0)
Một con đường từ Chicago đến Boston: Chicago Boston (chi phí: 983.0)
Một con đường từ Chicago đến New York: Chicago New York (chi phí: 787,0)
Một con đường từ Chicago đến Atlanta:
Chicago Kansas City Atlanta (chi phí: 1397.0)
Một con đường từ Chicago đến Miami:
Chicago Kansas City Atlanta Miami (chi phí: 2058.0)
Một con đường từ Chicago đến Dallas: Chicago Kansas City Dallas (chi phí: 1029.0)
Một con đường từ Chicago đến Houston:
Chicago Kansas City Dallas Houston (chi phí: 1268.0)
Con đường ngắn nhất từ Houston đến Chicago:
Houston Dallas Kansas City Chicago

Tất cả các đường đi ngắn nhất từ 3 là:


Đường dẫn từ 3 đến 0: 3 1 0 (chi phí: 5,0)
Đường dẫn từ 3 đến 1: 3 1 (chi phí: 3.0)
Đường dẫn từ 3 đến 2: 3 2 (chi phí: 4,0)
Đường dẫn từ 3 đến 3: 3 (chi phí: 0,0)
Đường dẫn từ 3 đến 4: 3 4 (chi phí: 6,0)
Machine Translated by Google

1086 Chương 29 Đồ thị có Trọng số và Ứng dụng

Chương trình tạo một đồ thị có trọng số cho Hình 29.1 ở dòng 27. Sau đó, nó gọi phương thức getShortestPath

(graph1.getIndex ("Chicago")) để trả về một đối tượng Path chứa tất cả các đường đi ngắn nhất từ Chicago. Gọi

printAllPaths () trên đối tượng ShortestPathTree hiển thị tất cả các đường dẫn (dòng 30).

Hình minh họa đồ họa của tất cả các đường đi ngắn nhất từ Chicago được thể hiện trong Hình 29.21. Các con

đường ngắn nhất từ Chicago đến các thành phố được tìm thấy theo thứ tự này: Thành phố Kansas, New York, Boston,

Denver, Dallas, Houston, Atlanta, Los Angeles, Miami, Seattle và San Francisco.

Seattle 10

2097 Boston
3
983
Chicago
1331 214
2
807
1003 787 Newyork
Denver
533
4 1260
1267
11
599
888
San Francisco
1
1015
Thành phố Kansas
381 1663 864
số 8

496 7
Los Angeles 1435 5 Atlanta
781

810
Dallas
661
6
239

Houston
1187
9
Miami

HÌNH 29.21 Các con đường ngắn nhất từ Chicago đến tất cả các thành phố khác được tô sáng.

29.9 Theo dõi thuật toán Dijkstra để tìm đường đi ngắn nhất từ Boston đến tất cả các thành phố khác ở

Hình 29.1.

29.10 Có phải một đường đi ngắn nhất giữa hai đỉnh là duy nhất nếu tất cả các cạnh có trọng số khác nhau không?

29.11 Nếu bạn sử dụng ma trận kề để biểu diễn các cạnh có trọng số, thì độ phức tạp về thời gian đối với thuật

toán Dijkstra là bao nhiêu?

29.12 Điều gì xảy ra với phương thức getShortestPath () trong WeightedGraph nếu đỉnh nguồn không thể tiếp cận tất

cả các đỉnh trong biểu đồ? Xác minh câu trả lời của bạn bằng cách viết một chương trình thử nghiệm tạo

một đồ thị không được kết nối và gọi phương thức getShortestPath () . Làm thế nào để bạn khắc phục sự

cố bằng cách lấy một cây đường đi ngắn nhất một phần?

29.13 Nếu không có đường đi từ đỉnh v đến đỉnh nguồn, giá [v] sẽ là bao nhiêu?

29.14 Giả sử rằng đồ thị được liên thông; Liệu phương thức getShortestPath có tìm được đường dẫn ngắn nhất một

cách chính xác nếu các dòng 159–161 trong WeightedGraph bị xóa không?

29.6 Nghiên cứu điển hình: Vấn đề Cửu vĩ hồ


Bài toán chín đuôi có trọng số có thể được rút gọn thành bài toán đường đi ngắn nhất có trọng số.

Phần 28.10 đã trình bày vấn đề chín đuôi và giải nó bằng thuật toán BFS. Phần này trình bày một biến thể của vấn

đề và giải quyết nó bằng cách sử dụng thuật toán đường đi ngắn nhất.
Machine Translated by Google

29.6 Nghiên cứu điển hình: Vấn đề Cửu vĩ hồ 1087

Bài toán chín mặt là tìm số lần di chuyển tối thiểu dẫn đến tất cả các đồng xu đều bị úp xuống. Mỗi

nước đi làm tung một đồng xu đầu và các đồng xu lân cận của nó. Bài toán chín đuôi có trọng số ấn định số

lần lật là trọng lượng trên mỗi lần di chuyển. Ví dụ, bạn có thể thay đổi các đồng xu trong Hình 29.22a

thành các đồng xu trong Hình 29.22b bằng cách lật đồng xu đầu tiên ở hàng đầu tiên và hai đồng tiền lân cận

của nó. Do đó, trọng lượng của nước đi này là 3. Bạn có thể thay đổi các đồng xu trong Hình 29.22c thành

Hình 29.22d bằng cách lật đồng xu ở giữa và bốn đồng xu lân cận của nó. Vậy trọng lượng của lần di chuyển này là 5.

H HH T THỨ TỰ T THỨ TỰ T HH

TT T HTT HHT TTH

HHH HHH HHH HTH

(Một) (b) (C) (d)

HÌNH 29.22 Trọng lượng của mỗi lần di chuyển là số lần lật của chuyển động.

Bài toán chín đuôi có trọng số có thể được rút gọn thành việc tìm đường đi ngắn nhất từ nút bắt đầu

đến nút đích trong một đồ thị có trọng số cạnh. Đồ thị có 512 nút. Tạo một cạnh từ nút v đến u nếu có sự di

chuyển từ nút u sang nút v. Gán số lần lật là trọng lượng của cạnh.

Nhớ lại rằng trong Phần 28.10, chúng ta đã định nghĩa một lớp NineTailModel để lập mô hình bài toán chín

đuôi. Bây giờ chúng ta định nghĩa một lớp mới có tên là WeightedNineTailModel mở rộng NineTailModel, như

trong Hình 29.23.

NineTailModel

#tree: AbstractGraph <Integer> .Tree Một cây bắt nguồn từ nút 511.

+ NineTailModel () Xây dựng một mô hình cho bài toán chín đuôi và thu được
cây.

+ getShortestPath (nodeIndex: int): Trả về một đường dẫn từ nút đã chỉ định đến nút gốc. Con đường
Danh sách <Integer> trả về bao gồm các nhãn nút trong một danh sách.

-getEdges (): Trả về danh sách các đối tượng Edge cho biểu đồ.
Danh sách <AbstractGraph.Edge>
+ getNode (index: int): char [] Trả về một nút bao gồm chín ký tự của H và T.
+ getIndex (node: char []): int Trả về chỉ mục của nút được chỉ định.
+ getFlippedNode (nút: char [], Lật nút ở vị trí được chỉ định và trả về chỉ mục
vị trí: int): int của nút đã lật.
+ flipACell (nút: char [], row: int, Lật nút tại hàng và cột được chỉ định.
cột: int): void
+ printNode (node: char []): void Hiển thị nút trên bảng điều khiển.

WeightedNineTailModel

+ WeightedNineTailModel () Xây dựng mô hình cho bài toán chín đuôi có trọng số
và lấy được một ShortestPathTree được root từ mục tiêu
nút.
+ getNumberOfFlips (u: int): int Trả về số lần lật từ nút u đến đích
nút 511.
-getNumberOfFlips (u: int, v: int): int Trả về số ô khác nhau giữa hai ô
điểm giao.

-getEdges (): Danh sách <WeightedEdge> Lấy các cạnh có trọng số cho chín đuôi có trọng số
vấn đề.

HÌNH 29.23 Lớp WeightedNineTailModel mở rộng NineTailModel.


Machine Translated by Google

1088 Chương 29 Đồ thị có Trọng số và Ứng dụng

Lớp NineTailModel tạo một Đồ thị và lấy một Cây bắt nguồn từ nút đích 511.
WeightedNineTailModel cũng giống như NineTailModel ngoại trừ việc nó tạo một
WeightedGraph và lấy một ShortestPathTree bắt nguồn từ nút đích 511. Weight
edNineTailModel mở rộng NineTailModel . Phương thức getEdges () tìm tất cả các cạnh
trong đồ thị. Phương thức getNumberOfFlips (int u, int v) trả về số lần lật từ nút u
đến nút v. Phương thức getNumberOfFlips (int u) trả về số lần lật từ nút u đến nút đích.

Liệt kê 29.9 triển khai WeightedNineTailModel.

LISTING 29,9 WeightedNineTailModel.java


1 nhập java.util. *;
2
mở rộng NineTailModel 3 lớp công khai WeightedNineTailModel mở rộng NineTailModel {
4 / ** Xây dựng mô hình * /
constructor 5 public WeightedNineTailModel () {
6 // Tạo các cạnh
nhận được các cạnh
7 Liệt kê các cạnh <WeightedEdge> = getEdges ();
số 8

// Tạo đồ thị
tạo một đồ thị 9 WeightedGraph <Integer> graph = new WeightedGraph <> (
10 cạnh, NUMBER_OF_NODES);
11 12

13 // Lấy cây đường dẫn ngắn nhất bắt nguồn từ nút đích
lấy một cái cây
14 tree = graph.getShortestPath (511);
15 }
16
17 / ** Tạo tất cả các cạnh cho biểu đồ * /
có được các cạnh có trọng số 18 danh sách riêng tư <WeightedEdge> getEdges () {
19 // Lưu trữ các cạnh
20 Danh sách các cạnh <WeightedEdge> = new ArrayList <> ();
21
22 for (int u = 0; u <NUMBER_OF_NODES; u ++) {
23 for (int k = 0; k < 9; k ++) {
24 char [] node = getNode (u); // Lấy nút cho đỉnh u
25 if (nút [k] == 'H') {
lấy nút liền kề 26 int v = getFlippedNode (nút, k);
trọng lượng
27 int numberOfFlips = getNumberOfFlips (u, v);
28
29 // Thêm cạnh (v, u) để di chuyển hợp pháp từ nút u sang nút v
thêm một cạnh 30 edge.add (new WeightedEdge (v, u, numberOfFlips));
31 }
32 }
33 }
34
35 trả lại các cạnh;
36 }
37

số lần lật 38 private static int getNumberOfFlips (int u, int v) {


39 char [] node1 = getNode (u);
40 char [] node2 = getNode (v);
41
42 int count = 0; // Đếm số ô khác nhau
43 for (int i = 0; i <node1.length; i ++)
44 if (node1 [i]! = node2 [i]) count ++;
45
46 số lượng trả lại ;
47 }
48
Machine Translated by Google

29.6 Nghiên cứu điển hình: Vấn đề Cửu vĩ hồ 1089

49 public int getNumberOfFlips (int u) { tổng số lần lật


return (int) ((WeightedGraph <Integer> .ShortestPathTree) cây)
50 .getCost (u);
51 }
52 53}

WeightedNineTailModel mở rộng NineTailModel để xây dựng một WeightedGraph nhằm mô hình hóa bài
toán chín đuôi có trọng số (dòng 10–11). Đối với mỗi nút u, phương thức getEdges () tìm một
nút được lật v và gán số lần lật làm trọng số cho cạnh (v, u) (dòng 30).
Phương thức getNumberOfFlips (int u, int v) trả về số lần lật từ nút u
đến nút v (dòng 38–47). Số lần lật là số ô khác nhau giữa hai nút (dòng 44).

WeightedNineTailModel nhận được một ShortestPathTree bắt nguồn từ nút đích 511 (dòng 14).
Lưu ý rằng cây là một trường dữ liệu được bảo vệ được định nghĩa trong NineTailModel và
ShortestPathTree là một lớp con của Tree. Các phương thức được định nghĩa trong NineTailModel

sử dụng thuộc tính cây .


Phương thức getNumberOfFlips (int u) (dòng 49–52) trả về số lần lật từ nút u đến nút đích,
là chi phí của đường dẫn từ nút u đến nút đích. Chi phí này có thể thu được bằng cách gọi
phương thức getCost (u) được xác định trong ShortestPathTree
lớp (dòng 51).
Liệt kê 29.10 đưa ra một chương trình nhắc người dùng nhập một nút ban đầu và hiển thị số
lần lật tối thiểu để đến được nút đích.

LISTING 29.10 WeightedNineTail.java


1 nhập java.util.Scanner;
2
3 hạng công khai WeightedNineTail {
4 public static void main (String [] args) {
5 // Nhắc người dùng nhập chín đồng tiền Hs và Ts
6 System.out.print ("Nhập chín xu ban đầu 'Hs và Ts: ");
Đầu vào máy quét = Máy quét mới (System.in);
7 String s = input.nextLine (); char
8 [] initialNode = s.toCharArray (); nút ban đầu
9 10

11 WeightedNineTailModel model = new WeightedNineTailModel (); tạo mô hình


12 java.util.List <Integer> path =
13 model.getShortestPath (NineTailModel.getIndex (InitialNode)); có được con đường ngắn nhất

14
15 System.out.println ("Các bước để lật đồng xu ");
16 for (int i = 0; i <path.size (); i ++)
17 NineTailModel.printNode (NineTailModel.getNode (path.get (i))); nút in
18
19 System.out.println (" Số lần lật là" model.getNumberOfFlips +
20 (NineTailModel.getIndex (InitialNode))); số lần lật
21 }
22}

Nhập chín xu ban đầu Hs và Ts: HHHTTTHHH

Các bước để lật đồng xu là


HHH
TTT
HHH
Machine Translated by Google

1090 Chương 29 Đồ thị có Trọng số và Ứng dụng

HHH
THT
TTT

TTT
TTT
TTT

Số lần lật là 8

Chương trình nhắc người dùng nhập một nút ban đầu có chín chữ cái với sự kết hợp của Hs và Ts là một

chuỗi ở dòng 8, lấy một mảng ký tự từ chuỗi (dòng 9), tạo một mô hình (dòng 11), lấy đường dẫn ngắn
nhất từ nút ban đầu đến nút đích (dòng 12–13), hiển thị các nút trong đường dẫn (dòng 16–17) và gọi

getNumberOfFlips để nhận số lần lật cần thiết để đến được nút mục tiêu (dòng 20).

29.15 Tại sao trường dữ liệu cây trong NineTailModel trong Liệt kê 28.13 được định nghĩa được bảo vệ?

Kiểm tra điểm 29.16 Các nút được tạo như thế nào cho biểu đồ trong WeightedNineTailModel?

29.17 Các cạnh được tạo như thế nào cho biểu đồ trong WeightedNineTailModel?

ĐIỀU KHOẢN CHÍNH

Thuật toán Dijkstra 1078 con đường ngắn nhất 1078

đồ thị có trọng số cạnh 1063 đường dẫn ngắn nhất nguồn đơn 1079

cây bao trùm tối thiểu 1072 đồ thị có trọng số đỉnh 1063

Thuật toán của Prim 1072

TÓM TẮT CHƯƠNG

1. Bạn có thể sử dụng ma trận kề hoặc danh sách để lưu trữ các cạnh có trọng số trong đồ thị.

2. Cây bao trùm của đồ thị là một đồ thị con là một cây và nối tất cả các đỉnh trong đồ thị.

3. Thuật toán tìm cây bao trùm tối thiểu của Prim hoạt động như sau: thuật toán
bắt đầu với cây bao trùm chứa một đỉnh tùy ý. Thuật toán mở rộng cây bằng
cách thêm một đỉnh có cạnh trọng số nhỏ nhất vào một đỉnh đã có trong cây.

4. Thuật toán Dijkstra bắt đầu tìm kiếm từ đỉnh nguồn và tiếp tục tìm các đỉnh có đường đi ngắn
nhất đến nguồn cho đến khi tất cả các đỉnh được tìm thấy.

ĐỐ

Trả lời câu hỏi cho chương này trực tuyến tại www.cs.armstrong.edu/liang/intro10e/quiz.html.

BÀI TẬP LẬP TRÌNH

* 29.1 (Thuật toán Kruskal) Văn bản giới thiệu thuật toán Prim để tìm cây bao
trùm nhỏ mẹ. Thuật toán Kruskal là một thuật toán nổi tiếng khác để
tìm cây bao trùm tối thiểu. Thuật toán liên tục tìm một cạnh trọng số
tối thiểu và thêm nó vào cây nếu nó không gây ra chu kỳ. Quá trình kết thúc
Machine Translated by Google

Bài tập lập trình 1091

khi tất cả các đỉnh nằm trong cây. Thiết kế và triển khai thuật toán tìm MST bằng
thuật toán Kruskal.

* 29.2 (Triển khai thuật toán Prim bằng ma trận kề) Văn bản thực hiện thuật toán Prim bằng
cách sử dụng danh sách cho các cạnh liền kề. Thực hiện thuật toán bằng cách sử
dụng ma trận kề cho đồ thị có trọng số.

* 29.3 (Triển khai thuật toán Dijkstra sử dụng ma trận kề) Văn bản đưa vào thuật toán
Dijkstra bằng cách sử dụng danh sách cho các cạnh liền kề. Thực hiện phép thuật
toán bằng cách sử dụng ma trận kề cho đồ thị có trọng số.

* 29.4 (Sửa đổi trọng lượng trong bài toán chín đuôi) Trong văn bản, chúng tôi chỉ định
số lần lật là trọng lượng cho mỗi lần di chuyển. Giả sử rằng khối lượng gấp ba
lần số lần lật, hãy xem lại chương trình.

* 29.5 (Chứng minh hoặc bác bỏ) Giả thuyết là cả NineTailModel và WeightedNineTailModel đều
dẫn đến cùng một đường đi ngắn nhất. Viết chương trình để chứng minh hoặc bác bỏ
điều đó. (Gợi ý: Đặt tree1 và tree2 biểu thị các cây bắt nguồn từ nút 511 thu được
từ NineTailModel và WeightedNineTailModel, tương ứng. Nếu độ sâu của nút u giống
nhau trong tree1 và tree2, độ dài của đường dẫn từ u đến đích là như nhau.)

** 29.6 (Mô hình 4 * 4 16 đuôi có trọng số) Bài toán chín đuôi có trọng số trong văn bản sử
dụng ma trận 3 * 3. Giả sử rằng bạn có 16 đồng xu được đặt trong ma trận 4 * 4.
Tạo một lớp mô hình mới có tên là WeightedTailModel16. Tạo một phiên bản của mô
hình và lưu đối tượng vào một tệp có tên là WeightedTailModel16.dat.

** 29.7 (Có trọng số 4 * 4 16 đuôi) Sửa đổi Liệt kê 29.9, WeightedNineTail.java, cho vấn đề
4 * 4 16 đuôi có trọng số. Chương trình của bạn nên đọc đối tượng mô hình
được tạo từ bài tập trước.

** 29.8 (Bài toán nhân viên bán hàng đi du lịch) Bài toán nhân viên bán hàng đi du lịch (TSP)
là tìm một tuyến đường khứ hồi ngắn nhất đến thăm mỗi thành phố đúng một lần và
sau đó quay trở lại thành phố xuất phát. Bài toán tương đương với việc tìm một
chu trình Hamilton ngắn nhất trong Bài tập lập trình 28.17. Thêm phương thức sau
vào lớp WeightedGraph :

// Trả về một chu kỳ ngắn nhất


// Trả về null nếu không có chu trình như vậy tồn tại
danh sách công khai <Integer> getShortestHamiltonianCycle ()

* 29.9 (Tìm cây bao trùm tối thiểu) Viết chương trình đọc biểu đồ liên thông từ một tệp và
hiển thị cây bao trùm tối thiểu của nó. Dòng đầu tiên trong tệp chứa một số cho
biết số đỉnh (n). Các đỉnh được đánh dấu là 0, 1, ..., n-1. Mỗi dòng tiếp theo mô
tả các cạnh ở dạng u1, v1, w1 | u2, v2, w2 | ....

Mỗi bộ ba trong dạng này mô tả một


cạnh và trọng lượng của nó. Hình 29.24 cho thấy một ví dụ về tệp cho biểu đồ phản
hồi cor. Lưu ý rằng chúng tôi giả sử đồ thị là vô hướng. Nếu đồ thị có

100
0 1
Tập tin

3 20 6
40 0, 1, 100 | 0, 2, 3
2 3 1, 3, 20
5 2, 3, 40 | 2, 4, 2
2 5
3, 4, 5 | 3, 5, 5
9 4, 5, 9
4 5
(Một) (b)

HÌNH 29.24 Các đỉnh và cạnh của đồ thị có trọng số có thể được lưu trữ trong một tệp.
Machine Translated by Google

1092 Chương 29 Đồ thị có Trọng số và Ứng dụng

một cạnh (u, v), nó cũng có một cạnh (v, u). Chỉ có một cạnh được thể hiện trong tệp. Khi

bạn xây dựng một đồ thị, cả hai cạnh cần được thêm vào.

Chương trình của bạn sẽ nhắc người dùng nhập tên của tệp, đọc dữ liệu từ tệp, tạo một phiên

bản g của WeightedGraph, gọi g.printWeightedEdges ()

để hiển thị tất cả các cạnh, hãy gọi getMinimumSpanningTree () để lấy một cây thể hiện của

WeightedGraph.MST, gọi tree.getTotalWeight () để hiển thị trọng số của cây bao trùm tối

thiểu và gọi tree.printTree () để hiển thị cây. Đây là bản chạy mẫu của chương trình:

Nhập tên tệp: c: \ works \ WeightedGraphSample.txt


Số đỉnh là 6
Đỉnh 0: (0, 2, 3) (0, 1, 100)
Đỉnh 1: (1, 3, 20) (1, 0, 100)
Đỉnh 2: (2, 4, 2) (2, 3, 40) (2, 0, 3)
Đỉnh 3: (3, 4, 5) (3, 5, 5) (3, 1, 20) (3, 2, 40)
Đỉnh 4: (4, 2, 2) (4, 3, 5) (4, 5, 9)
Đỉnh 5: (5, 3, 5) (5, 4, 9)
Tổng trọng lượng trong MST là 35
Gốc là: 0
Các cạnh: (3, 1) (0, 2) (4, 3) (2, 4) (3, 5)

(Gợi ý: Sử dụng WeightedGraph mới (list, numberOfVertices) để tạo biểu đồ, trong đó danh

sách chứa danh sách các đối tượng WeightedEdge . Sử dụng WeightedEdge mới (u, v, w) để tạo

cạnh. Đọc dòng đầu tiên để biết số đỉnh . Đọc từng dòng tiếp theo thành một chuỗi s và sử

dụng s.split ("[\\ |]")

để chiết xuất các bộ ba. Đối với mỗi bộ ba, sử dụng triplet.split ("[,]") để trích xuất các

đỉnh và trọng số.)

* 29.10 (Tạo tệp cho biểu đồ) Sửa đổi Liệt kê 29.3, TestWeightedGraph.java, để tạo tệp biểu diễn graph1.

Định dạng tệp được mô tả trong Bài tập lập trình 29.9. Tạo tệp từ mảng được xác định trong

các dòng 7–24 trong Liệt kê 29.3. Số đỉnh của biểu đồ là 12, sẽ được lưu trữ trong dòng đầu

tiên của tệp. Một cạnh (u, v) được lưu trữ nếu u < v. Nội dung của tệp sẽ như sau:

12
0, 1, 807 | 0, 3, 1331 | 0, 5, 2097
1, 2, 381 | 1, 3, 1267
2, 3, 1015 | 2, 4, 1663 | 2, 10, 1435
3, 4, 599 | 3, 5, 1003
4, 5, 533 | 4, 7, 1260 | 4, 8, 864 | 4, 10, 496
5, 6, 983 | 5, 7, 787
6, 7, 214
7, 8, 888
8, 9, 661 | 8, 10, 781 | 8, 11, 810
9, 11, 1187
10, 11, 239

* 29.11 (Tìm đường đi ngắn nhất) Viết chương trình đọc biểu đồ liên thông từ một tệp.
Biểu đồ được lưu trữ trong một tệp sử dụng cùng một định dạng được chỉ định trong Lập trình

Bài tập 29.9. Chương trình của bạn sẽ nhắc người dùng nhập tên của tệp sau đó là hai đỉnh

và sẽ hiển thị đường dẫn ngắn nhất giữa hai đỉnh


Machine Translated by Google

Bài tập lập trình 1093

các đỉnh. Ví dụ, đối với đồ thị trong Hình 29.23, đường đi ngắn nhất giữa 0

và 1 có thể được hiển thị dưới dạng 0 2 4 3 1.

Đây là bản chạy mẫu của chương trình:

Nhập tên tệp: WeightedGraphSample2.txt

Nhập hai đỉnh (chỉ số nguyên): 0 1


Số đỉnh là 6
Đỉnh 0: (0, 2, 3) (0, 1, 100)
Đỉnh 1: (1, 3, 20) (1, 0, 100)
Đỉnh 2: (2, 4, 2) (2, 3, 40) (2, 0, 3)
Đỉnh 3: (3, 4, 5) (3, 5, 5) (3, 1, 20) (3, 2, 40)
Đỉnh 4: (4, 2, 2) (4, 3, 5) (4, 5, 9)
Đỉnh 5: (5, 3, 5) (5, 4, 9)
Đường dẫn từ 0 đến 1: 0 2 4 3 1

* 29.12 (Hiển thị đồ thị có trọng số) Sửa đổi GraphView trong Liệt kê 28.6 để hiển thị đồ thị có trọng số.

Viết chương trình hiển thị đồ thị hình 29.1 như hình 29.25. (Giảng viên có thể yêu cầu sinh

viên mở rộng chương trình này bằng cách thêm các thành phố mới với các cạnh thích hợp vào

biểu đồ).

HÌNH 29.25 Bài tập lập trình 29.12 hiển thị một đồ thị có trọng số.

* 29.13 (Hiển thị các đường đi ngắn nhất) Sửa lại GraphView trong Liệt kê 28.6 để hiển thị một biểu đồ có

trọng số và đường đi ngắn nhất giữa hai thành phố được chỉ định, như thể hiện trong Hình

29.19. Bạn cần thêm đường dẫn trường dữ liệu trong GraphView. Nếu một con đường

không rỗng, các cạnh trong đường dẫn được hiển thị bằng màu đỏ. Nếu một thành phố không có trong bản đồ

được nhập, chương trình sẽ hiển thị một văn bản để cảnh báo người dùng.

* 29.14 (Hiển thị cây khung tối thiểu) Sửa lại GraphView trong Liệt kê 28.6 để hiển thị đồ thị có trọng số

và cây khung tối thiểu cho đồ thị trong Hình 29.1, như được hiển thị trong Hình 29.26. Các

cạnh trong MST được hiển thị bằng màu đỏ.

*** 29.15 (Đồ thị động) Viết một chương trình cho phép người dùng tạo một đồ thị có trọng số động. Người dùng

có thể tạo một đỉnh bằng cách nhập tên và vị trí của nó, như trong Hình 29.27. Người dùng

cũng có thể tạo một cạnh để kết nối hai verti ces. Để đơn giản hóa chương trình, giả sử rằng

tên đỉnh giống với đỉnh


Machine Translated by Google

1094 Chương 29 Đồ thị có Trọng số và Ứng dụng

HÌNH 29.26 Bài tập lập trình 29.14 hiển thị một MST.

HÌNH 29.27 Chương trình có thể thêm các đỉnh và các cạnh và hiển thị một đường đi ngắn nhất giữa hai đỉnh

được chỉ định.

các chỉ số. Bạn phải thêm các chỉ số đỉnh 0, 1,. . ., và n, theo thứ tự này. Người dùng có

thể chỉ định hai đỉnh và để chương trình hiển thị đường đi ngắn nhất của chúng bằng màu đỏ.

*** 29.16 (Hiển thị MST động) Viết một chương trình cho phép người dùng tạo một biểu đồ có trọng số động.

Người dùng có thể tạo một đỉnh bằng cách nhập tên và loca tion của nó, như trong Hình 29.28.

Người dùng cũng có thể tạo một cạnh để nối hai đỉnh. Để đơn giản hóa chương trình, giả sử

rằng tên đỉnh giống với tên của chỉ số đỉnh. Bạn phải thêm các chỉ số đỉnh 0, 1,. . ., và n,

theo thứ tự này. Các cạnh trong MST được hiển thị bằng màu đỏ. Khi các cạnh mới được
vào,thêm
MST

sẽ được hiển thị lại.

*** 29.17 (Công cụ trực quan hóa đồ thị có trọng số) Xây dựng chương trình GUI
như hình 29.2, với các yêu cầu sau: (1) Bán kính của mỗi đỉnh là
20 pixel. (2) Người dùng nhấp chuột trái để đặt một đỉnh có tâm
tại điểm chuột, với điều kiện điểm chuột không ở bên trong hoặc quá
gần với đỉnh hiện có. (3) Người dùng nhấp vào nút chuột phải bên
trong một đỉnh ing tồn tại để loại bỏ đỉnh. (4) Người dùng nhấn
nút chuột bên trong một đỉnh và kéo đến một đỉnh khác, sau đó thả nút để tạo
Machine Translated by Google

Bài tập lập trình 1095

HÌNH 29.28 Chương trình có thể thêm đỉnh và cạnh và hiển thị MST động.

cạnh, và khoảng cách giữa hai đỉnh cũng được hiển thị. (5) Người dùng kéo một đỉnh
trong khi nhấn phím CTRL để di chuyển một đỉnh. (6) Các đỉnh là các số bắt đầu từ
0. Khi một đỉnh bị loại bỏ, các đỉnh được đánh số lại. (7) Bạn có thể nhấp vào Hiển
thị MST hoặc Hiển thị Tất cả SP Từ Nguồn
để hiển thị một cây MST hoặc SP từ một đỉnh bắt đầu. (8) Bạn có thể nhấp vào nút
Hiển thị đường đi ngắn nhất để hiển thị đường đi ngắn nhất giữa hai đỉnh được chỉ
định.

*** 29.18 (Phiên bản thay thế của thuật toán Dijkstra) Một phiên bản thay thế của thuật toán Dijk
stra có thể được mô tả như sau:

Đầu vào: đồ thị có trọng số G = (V, E) với các trọng số không âm


Đầu ra: Cây đường đi ngắn nhất từ đỉnh nguồn s

1 ShortestPathTree getShortestPath {
2 Gọi T là tập hợp chứa các đỉnh có 3 đường đi đến s đã biết;

4 Ban đầu T chứa đỉnh nguồn s với cost [s] = 0; thêm đỉnh ban đầu
5 for (mỗi u trong V - T) cost
[u] = infinity;
6 7

while (kích thước của T <n) { đỉnh hơn


Tìm v trong V - T với giá trị [u] + w (u, v) nhỏ nhất trong số mọi u trong tìm đỉnh tiếp theo

8 T;
9 Thêm v vào T và đặt cost [v] = cost [u] + w (u, v); cha [v] = u; thêm một đỉnh
10
11 }
12 13 14}

Thuật toán sử dụng cost [v] để lưu trữ chi phí của một đường đi ngắn nhất từ đỉnh v
đến đỉnh nguồn s. cost [s] là 0. Ban đầu gán giá trị vô cùng cho cost [v] để chỉ ra
rằng không tìm thấy đường đi nào từ v đến s. Gọi V là tất cả các đỉnh trong đồ thị
và T là tập hợp các đỉnh có chi phí đã biết. Ban đầu, đỉnh nguồn s nằm trong T.
Thuật toán liên tục tìm một đỉnh u trong T và một đỉnh v
trong V - T sao cho chi phí [u] + w (u, v) là nhỏ nhất và chuyển v sang T.

Thuật toán đường đi ngắn nhất được đưa ra trong văn bản cập nhật đồng thời chi phí
và giá trị gốc cho một đỉnh trong V - T. Thuật toán này khởi tạo chi phí đến vô cùng
cho mỗi đỉnh và sau đó thay đổi chi phí cho một đỉnh chỉ một lần khi đỉnh được thêm
vào T. Triển khai thuật toán này và sử dụng Liệt kê 29.7, TestShortestPath.
java, để kiểm tra thuật toán mới của bạn.
Machine Translated by Google

1096 Chương 29 Đồ thị có Trọng số và Ứng dụng

*** 29.19 (Tìm u với chi phí nhỏ nhất [u] một cách hiệu quả) Phương pháp getShortestPath tìm một u với chi

phí [u] nhỏ nhất bằng cách sử dụng tìm kiếm tuyến tính, lấy O (V).

Thời gian tìm kiếm có thể được giảm xuống còn O (log V) bằng cách sử dụng cây AVL. Sửa đổi

phương pháp bằng cách sử dụng cây AVL để lưu trữ các đỉnh trong V - T. Sử dụng Liệt kê

29.7, TestShortestPath.java, để kiểm tra việc triển khai mới của bạn.

*** 29.20 (Kiểm tra xem đỉnh u có nằm trong T hiệu quả không) Vì T được triển khai bằng cách sử dụng danh

sách trong phương thức getMinimumSpanningTree và getShortestPath trong Liệt kê 29.2

WeightedGraph.java, kiểm tra xem đỉnh u có nằm trong T hay không bằng cách gọi T.contains

(u ) mất O (n) thời gian. Sửa đổi hai phương thức này bằng cách giới thiệu một mảng có tên

isInT. Đặt isInT [u] thành true khi một đỉnh u được thêm vào T.

Kiểm tra xem đỉnh u có nằm trong T hay không bây giờ có thể được thực hiện trong thời gian O

(1) . Viết chương trình thử nghiệm bằng đoạn mã sau, trong đó graph1 được tạo từ Hình 29.1.

WeightedGraph <String> graph1 = new WeightedGraph <> (cạnh, đỉnh);

WeightedGraph <String> .MST tree1 = graph1.getMinimumSpanningTree ();

System.out.println ("Tổng trọng lượng là" + tree1.getTotalWeight ());

tree1.printTree ();

WeightedGraph <String> .ShortestPathTree tree2 =


graph1.getShortestPath (graph1.getIndex ("Chicago"));

tree2.printAllPaths ();
Machine Translated by Google

CHƯƠNG

30
ĐA NĂNG
VÀ PARALLEL
LẬP TRÌNH

Mục tiêu
■ Để có cái nhìn tổng quan về đa luồng (§30.2).

■ Để phát triển các lớp nhiệm vụ bằng cách triển khai giao diện Runnable
(§30.3).

■ Để tạo luồng để chạy các tác vụ bằng lớp Luồng ( §30.3 ).

■ Để điều khiển luồng bằng các phương thức trong lớp Luồng ( §30.4 ).

■ Để điều khiển hoạt ảnh bằng các chuỗi và sử dụng Platform.runLater

để chạy mã trong chuỗi ứng dụng (§30.5).

■ Để thực thi các tác vụ trong nhóm luồng (§30.6).

■ Sử dụng các phương pháp hoặc khối được đồng bộ hóa để đồng bộ hóa các luồng

nhằm tránh các điều kiện đua (§30.7).

■ Để đồng bộ hóa các luồng bằng khóa (§30.8).

■ Để tạo điều kiện cho truyền thông luồng sử dụng các điều kiện trên khóa

(§§30.9 và 30.10).

■ Để sử dụng hàng đợi chặn (ArrayBlockingQueue,

LinkedBlockingQueue, PriorityBlockingQueue) để đồng bộ


hóa quyền truy cập vào hàng đợi (§30.11).

■ Để hạn chế số lượng tác vụ đồng thời truy cập vào một

tài nguyên sử dụng semaphores (§30.12).

■ Sử dụng kỹ thuật sắp xếp tài nguyên để tránh bế tắc (§30.13).

■ Để mô tả vòng đời của một luồng (§30.14).

■ Để tạo các tập hợp được đồng bộ hóa bằng cách sử dụng các phương thức
tĩnh trong lớp Collections (§30.15).

■ Để phát triển các chương trình song song bằng cách sử dụng Khuôn khổ Fork / Tham gia (§30.16).
Machine Translated by Google

1098 Chương 30 Lập trình đa luồng và song song

30.1 Giới thiệu


Đa luồng cho phép thực hiện đồng thời nhiều tác vụ trong một chương trình.

Một trong những tính năng mạnh mẽ của Java là hỗ trợ tích hợp cho đa luồng — chạy đồng thời nhiều tác vụ trong

một chương trình. Trong nhiều ngôn ngữ lập trình, bạn phải gọi các thủ tục và hàm phụ thuộc vào hệ thống để

thực hiện đa luồng. Chương này giới thiệu các khái niệm về luồng và cách các chương trình đa luồng có thể

được phát triển trong Java.

30.2 Khái niệm chủ đề


Một chương trình có thể gồm nhiều tác vụ có thể chạy đồng thời. Một luồng là luồng thực thi, từ đầu

đến cuối, của một tác vụ.

Một luồng cung cấp cơ chế để chạy một tác vụ. Với Java, bạn có thể khởi chạy đồng thời nhiều luồng từ một

chương trình. Các luồng này có thể được thực thi đồng thời trong các hệ thống đa bộ xử lý, như trong Hình

30.1a.

Chủ đề 1 Chủ đề 1

Chủ đề 2 Chủ đề 2

Chủ đề 3 Chủ đề 3

(Một) (b)

HÌNH 30.1 (a) Ở đây nhiều luồng đang chạy trên nhiều CPU. (b) Ở đây nhiều luồng dùng chung một CPU.

Trong các hệ thống một bộ xử lý, như trong Hình 30.1b, nhiều luồng chia sẻ thời gian CPU, được gọi là chia

sẻ thời gian, và hệ điều hành chịu trách nhiệm lập lịch và phân bổ tài nguyên cho chúng. Sự sắp xếp này là thực

tế bởi vì phần lớn thời gian CPU ở chế độ nhàn rỗi. Chẳng hạn, nó không làm gì trong khi chờ người dùng nhập dữ

liệu.

Đa luồng có thể làm cho chương trình của bạn phản hồi và tương tác tốt hơn, cũng như nâng cao hiệu suất.

Ví dụ, một trình xử lý văn bản tốt cho phép bạn in hoặc lưu một tệp trong khi bạn đang nhập văn bản. Trong một

số trường hợp, các chương trình đa luồng chạy nhanh hơn các chương trình đơn luồng ngay cả trên các hệ

thống xử lý đơn. Java cung cấp sự hỗ trợ đặc biệt tốt cho việc tạo và chạy các luồng ning cũng như để khóa tài

nguyên để tránh xung đột.

Bạn có thể tạo các luồng bổ sung để chạy các tác vụ đồng thời trong chương trình. Trong Java, mỗi tác vụ là

một thể hiện của giao diện Runnable , còn được gọi là đối tượng chạy được. Một luồng về cơ bản là một đối

tượng tạo điều kiện thuận lợi cho việc thực hiện một tác vụ.

30.1 Tại sao cần đa luồng? Làm thế nào để nhiều luồng chạy đồng thời trong một
hệ thống xử lý đơn?

30.2 Đối tượng chạy được là gì? Chủ đề là gì?

30.3 Tạo nhiệm vụ và chủ đề


Một lớp tác vụ phải triển khai giao diện Runnable . Một nhiệm vụ phải được chạy từ một chuỗi.

Nhiệm vụ là các đối tượng. Để tạo các tác vụ, trước tiên bạn phải xác định một lớp cho các tác vụ, lớp này sẽ
Giao diện Runnable chèn vào giao diện Runnable . Giao diện Runnable khá đơn giản. Tất cả những gì nó chứa là phương thức chạy.

phương thức run () Bạn cần triển khai phương pháp này để cho hệ thống biết luồng của bạn sẽ chạy như thế nào. Mẫu để phát triển

một lớp tác vụ được thể hiện trong Hình 30.2a.


Machine Translated by Google

30.3 Tạo nhiệm vụ và chuỗi 1099

java.lang.Runnable TaskClass // Lớp khách hàng


public class Client {
...
// Lớp tác vụ tùy chỉnh public void someMethod () {
lớp công khai TaskClass triển khai Runnable { ...
... // Tạo một thể hiện của TaskClass TaskClass
public TaskClass (...) { task = new TaskClass (...);
...
} // Tạo một chuỗi
Luồng luồng = new Thread (tác vụ);
// Triển khai phương thức run trong Runnable public
void run () { // Bắt đầu một luồng
// Cho hệ thống biết cách chạy chuỗi tùy chỉnh thread.start ();
... ...
} }
... ...
} }

(Một) (b)

HÌNH 30.2 Xác định một lớp tác vụ bằng cách triển khai giao diện Runnable .

Khi bạn đã xác định một TaskClass, bạn có thể tạo một tác vụ bằng cách sử dụng hàm tạo của nó. Ví dụ,

Nhiệm vụ TaskClass = new TaskClass (...);

Một tác vụ phải được thực thi trong một luồng. Lớp Thread chứa các hàm tạo cho cre Lớp chủ đề

ating luồng và nhiều phương pháp hữu ích để kiểm soát luồng. Để tạo một chuỗi cho một nhiệm vụ, hãy sử dụng tạo một nhiệm vụ

Luồng luồng = new Thread (tác vụ);

Sau đó, bạn có thể gọi phương thức start () để cho JVM biết rằng luồng đã sẵn sàng chạy, như sau: tạo một chủ đề

thread.start ();

JVM sẽ thực thi tác vụ bằng cách gọi phương thức run () của tác vụ . Hình 30.2b phác thảo các bước chính

để tạo một tác vụ, một luồng và bắt đầu luồng. bắt đầu một chủ đề

Liệt kê 30.1 đưa ra một chương trình tạo ra ba nhiệm vụ và ba luồng để chạy chúng.

■ Nhiệm vụ đầu tiên in chữ cái 100 lần.

■ Nhiệm vụ thứ hai in chữ b 100 lần.

■ Nhiệm vụ thứ ba in các số nguyên từ 1 đến 100.

Khi bạn chạy chương trình này, ba luồng sẽ chia sẻ CPU và thay phiên nhau in các chữ cái và số trên bảng

điều khiển. Hình 30.3 cho thấy một lần chạy chương trình mẫu.

HÌNH 30.3 Các tác vụ printA, printB và print100 được thực hiện đồng thời để hiển thị chữ a 100 lần,

chữ b 100 lần và các số từ 1 đến 100.


Machine Translated by Google

1100 Chương 30 Lập trình đa luồng và song song

LISTING 30.1 TaskThreadDemo.java


1 lớp công khai TaskThreadDemo {
2 public static void main (String [] args) {
3 // Tạo nhiệm vụ
tạo nhiệm vụ 4 Runnable printA = new PrintChar ('a', 100);
5 Runnable printB = new PrintChar ('b', 100);
6 Runnable print100 = new PrintNum (100);
7
số 8
// Tạo chủ đề
tạo chủ đề Thread thread1 = new Thread (printA);
9 Thread thread2 = new Thread (printB);
10 Thread thread3 = new Thread (print100);
11 12

13 // Bắt đầu chuỗi


bắt đầu chủ đề 14 thread1.start ();
15 thread2.start ();
16 thread3.start ();
17 }
18}
19
20 // Tác vụ in một ký tự một số lần được chỉ định
lớp nhiệm vụ 21 lớp PrintChar triển khai Runnable {
22 ký tự riêng charToPrint; // Ký tự để in
23 lần int riêng tư ; // Số lần lặp lại
24
25 / ** Xây dựng một nhiệm vụ với một ký tự được chỉ định và số lượng
26 * thời gian để in ký tự
27 * /
28 public PrintChar (char c, int t) {
29 charToPrint = c;
30 lần = t;
31 }
32
33 @Override / ** Ghi đè phương thức run () để thông báo cho hệ thống
34 * nhiệm vụ cần thực hiện
35 * /
chạy 36 public void run () {
37 for (int i = 0; i <lần; i ++) {
38 System.out.print (charToPrint);
39 }
40 }
41}
42
43 // Lớp tác vụ để in các số từ 1 đến n cho n cho trước
lớp nhiệm vụ 44 lớp PrintNum triển khai Runnable {
45 private int lastNum;
46
47 / ** Xây dựng tác vụ để in 1, 2, ..., public PrintNum (int n */
48 n) {
49 lastNum = n;
50 }
51
52 @Override / ** Cho chuỗi cách chạy * /
chạy 53 public void run () {
54 for (int i = 1; i <= lastNum; i ++) {
55 System.out.print (" " + i);
56 }
57 }
58}
Machine Translated by Google

30.3 Tạo nhiệm vụ và luồng 1101

Chương trình tạo ra ba nhiệm vụ (dòng 4–6). Để chạy chúng đồng thời, ba luồng được tạo
(dòng 9–11). Phương thức start () (dòng 14–16) được gọi để bắt đầu một luồng khiến phương
thức run () trong tác vụ được thực thi. Khi phương thức run () hoàn tất, luồng kết thúc.

Vì hai tác vụ đầu tiên, printA và printB, có chức năng tương tự nhau, chúng có thể được
định nghĩa trong một lớp tác vụ PrintChar (dòng 21–41). Lớp PrintChar chèn Runnable và ghi
đè phương thức run () (dòng 36–40) bằng hành động ký tự in. Lớp này cung cấp một khuôn khổ
để in bất kỳ ký tự đơn nào trong một số lần nhất định. Các đối tượng có thể chạy được,
printA và printB, là các thể hiện của lớp PrintChar .

Lớp PrintNum (dòng 44–58) triển khai Runnable và ghi đè phương thức run () (dòng 53–57)
bằng hành động print-number. Lớp này cung cấp một khuôn khổ để in các số từ 1 đến n, cho bất
kỳ số nguyên n nào. Đối tượng runnable print100 là một thể hiện của lớp printNum lớp.

Lưu
ý Nếu bạn không thấy hiệu ứng của ba luồng này chạy đồng thời, hãy tăng số
ký tự được in. Ví dụ: thay đổi dòng 4 thành

Lưu ý quan trọng


Phương thức run () trong một tác vụ chỉ định cách thực hiện tác vụ. Phương thức này

được gọi tự động bởi JVM. Bạn không nên gọi nó. Việc gọi run () trực tiếp chỉ đơn thuần
thực thi phương thức này trong cùng một luồng; không có chủ đề mới được bắt đầu.

30.3 Bạn định nghĩa một lớp tác vụ như thế nào? Làm thế nào để bạn tạo một chuỗi cho một nhiệm vụ?

30.4 Điều gì sẽ xảy ra nếu bạn thay thế phương thức start () bằng phương thức run () ở
dòng 14–16 trong Liệt kê 30.1?

print100.start (); Được thay thế bởi print100.run


printA.start (); (); printA.run ();

printB.start (); printB.run ();

30.5 Điều nào sai trong hai chương trình sau đây? Sửa lỗi.

public class Test triển khai Runnable { public class Test triển khai Runnable {
public static void main (String [] args) { new public static void main (String [] args) { new
Test (); } Test ();
}

public Test () public Test ()


{Nhiệm vụ thử nghiệm = new {Thread t = new Thread (this);
Test (); new Thread (task) .start (); t.start (); t.start ();
}
}
public void run ()
{System.out.println ("test"); public void run ()
} {System.out.println ("test");
} }
}

(Một) (b)
Machine Translated by Google

1102 Chương 30 Lập trình đa luồng và song song

30.4 Lớp Chủ đề


Lớp Thread chứa các hàm tạo để tạo luồng cho các tác vụ và các phương thức
Chìa khóa

Điểm để điều khiển luồng.

Hình 30.4 cho thấy sơ đồ lớp cho lớp Thread .

«Giao diện»
java.lang.Runnable

java.lang.Thread

+ Chủ đề () Tạo một chuỗi trống.


+ Chủ đề (tác vụ: Runnable) Tạo một chuỗi cho một nhiệm vụ cụ thể.

+ start (): void + Bắt đầu chuỗi khiến phương thức run () được gọi bởi JVM.
isAlive (): boolean + Kiểm tra xem luồng hiện đang chạy.

setPosystem (p: int): void + join Đặt mức độ ưu tiên p (từ 1 đến 10) cho luồng này.
(): void + sleep (millis: long): Chờ cho chủ đề này kết thúc.

void + output (): void + intern Đặt một chuỗi ở chế độ ngủ trong một thời gian cụ thể tính bằng mili giây.

(): void Khiến một luồng tạm dừng và cho phép các luồng khác thực thi.

Làm gián đoạn chuỗi này.

HÌNH 30.4 Lớp Thread chứa các phương thức để điều khiển các luồng.

Lưu

ý Vì lớp Thread triển khai Runnable, bạn có thể định nghĩa một lớp mở rộng Thread
và triển khai phương thức run , như trong Hình 30.5a, sau đó tạo một đối tượng
từ lớp và gọi phương thức bắt đầu của nó trong một chương trình khách để bắt

tách nhiệm vụ khỏi chuỗi đầu luồng , như trong Hình 30.5b.

java.lang.Thread CustomThread
// Lớp khách hàng
public class Client {
...
// Lớp luồng tùy chỉnh public public void someMethod () {
class CustomThread mở rộng Luồng { ...
... // Tạo một luồng
public CustomThread (...) { CustomThread thread1 = new CustomThread (...);
...
} // Bắt đầu một luồng
thread1.start ();
...
// Ghi đè phương thức run trong Runnable public
void run () { // Tạo một luồng khác
// Cho hệ thống biết cách thực hiện tác vụ này CustomThread thread2 = new CustomThread (...);
...
} // Bắt đầu một luồng
... thread2.start ();
} }
...
}

(Một) (b)

HÌNH 30.5 Định nghĩa một lớp luồng bằng cách mở rộng lớp Luồng .
Machine Translated by Google

30.4 Lớp luồng 1103

Tuy nhiên, cách tiếp cận này không được khuyến khích vì nó kết hợp nhiệm vụ và phương

pháp cơ học của việc chạy tác vụ. Tách nhiệm vụ khỏi luồng là một thiết kế ưa thích.

Ghi chú

Lớp Thread cũng chứa các phương thức stop (), Susan () và Resume () .

Kể từ Java 2, các phương pháp này không được chấp nhận (hoặc lỗi thời) vì chúng được phương pháp không dùng nữa

biết là không an toàn. Thay vì sử dụng phương thức stop () , bạn nên gán giá trị null

cho một biến Thread để chỉ ra rằng nó đã dừng lại.

Bạn có thể sử dụng phương thức output () để tạm thời giải phóng thời gian cho các luồng khác. Đối năng suất ()

với bài kiểm tra, giả sử bạn sửa đổi mã trong phương thức run () ở dòng 53–57 cho PrintNum trong Liệt

kê 30.1 như sau:

public void run () {


for (int i = 1; i <= lastNum; i ++) {
System.out.print (" " + i);
Thread.yield ();
}
}

Mỗi khi in một số, luồng của nhiệm vụ print100 sẽ được nhường cho các luồng khác.

Phương thức sleep (dài mili) đặt luồng ở trạng thái ngủ trong một thời gian cụ thể tính bằng mili ngủ (lâu)
giây để cho phép các luồng khác thực thi. Ví dụ: giả sử bạn sửa đổi mã ở dòng 53–57 trong Liệt kê 30.1,

như sau:

public void run () {


thử {
for (int i = 1; i <= lastNum; i ++) {
System.out.print (" " + i);
if (i> = 50) Thread.sleep (1);
}
}
bắt (Ngoại lệ bị gián đoạn) {
}
}

Mỗi khi một số (> = 50) được in, luồng của nhiệm vụ print100 được chuyển sang trạng thái ngủ trong 1
mili giây.

Phương thức ngủ có thể tạo ra một InterruptException, là một ngoại lệ đã được kiểm tra. Bị gián đoạn
Một ngoại lệ như vậy có thể xảy ra khi phương thức ngắt () của luồng đang ngủ được gọi. Phương thức ngắt ()

rất hiếm khi được gọi trên một luồng, do đó, một ngoại lệ bị gián đoạn

không có khả năng xảy ra. Nhưng vì Java buộc bạn phải bắt các ngoại lệ đã kiểm tra, nên bạn phải đặt nó

vào một khối try-catch . Nếu một phương thức ngủ được gọi trong một vòng lặp, bạn nên bọc vòng lặp đó

trong một khối try-catch , như được hiển thị trong (a) bên dưới. Nếu vòng lặp nằm ngoài khối try-

catch , như được hiển thị trong (b), luồng có thể tiếp tục thực thi ngay cả khi nó đang bị ngắt.

public void run () { public void run () {


thử { trong khi (...)
trong khi (...) { { thử {
... ...
Thread.sleep (1000); Thread.sleep (Thời gian ngủ);
} }
} bắt (Ngoại lệ bị gián đoạn) {
bắt (Ngoại lệ bị gián đoạn) { ex.printStackTrace ();
ex.printStackTrace (); }
} }
} }

(a) đúng (b) Không chính xác


Machine Translated by Google

1104 Chương 30 Lập trình đa luồng và song song

tham gia() Bạn có thể sử dụng phương thức join () để buộc một luồng phải đợi một luồng khác kết thúc. Vì

ví dụ, giả sử bạn sửa đổi mã ở dòng 53–57 trong Liệt kê 30.1 như sau:

public void run () { Chủ đề Chủ đề


Chủ đề thread4 = new Thread ( print100 thread4
new PrintChar ('c', 40));
thread4.start ();
thử {
for (int i = 1; i <= lastNum; i ++) {
System.out.print ("" + i); thread4.join ()
if (i == 50) thread4.join ();
} Chờ luồng4
} kêt thuc
bắt (Ngoại lệ bị gián đoạn) {
} thread4 đã hoàn thành
}

Một luồng 4 mới được tạo và nó in ký tự c 40 lần. Các số từ 50 đến 100


được in sau khi luồng 4 kết thúc.
Java chỉ định mọi luồng một mức độ ưu tiên. Theo mặc định, một luồng kế thừa quyền ưu tiên của
luồng sinh ra nó. Bạn có thể tăng hoặc giảm mức độ ưu tiên của bất kỳ luồng nào bằng cách sử dụng
setPutor (int) phương thức setP priority , và bạn có thể nhận được mức độ ưu tiên của luồng bằng cách sử dụng getPionary
phương pháp. Mức độ ưu tiên là các số từ 1 đến 10. Lớp Thread có các hằng số int MIN_PRIORITY,
NORM_PRIORITY và MAX_PRIORITY, đại diện cho 1, 5 và 10, tương ứng. Ưu tiên của luồng chính là
Thread.NORM_PRIORITY.
JVM luôn chọn luồng có thể chạy được với mức độ ưu tiên cao nhất. Một luồng có mức ưu tiên thấp
hơn chỉ có thể chạy khi không có luồng nào có mức ưu tiên cao hơn đang chạy. Nếu tất cả các luồng
có thể chạy được đều có mức độ ưu tiên như nhau, mỗi luồng sẽ được gán một phần thời gian bằng nhau
lập lịch vòng tròn của CPU trong một hàng đợi tròn. Đây được gọi là lập kế hoạch vòng tròn. Ví dụ: giả sử bạn chèn mã
sau vào dòng 16 trong Liệt kê 30.1:

thread3.setPosystem (Thread.MAX_PRIORITY);

Luồng cho tác vụ print100 sẽ được hoàn thành trước.

Mẹo
Các số ưu tiên có thể được thay đổi trong phiên bản Java trong tương lai. Để giảm thiểu tác động của

bất kỳ thay đổi nào, hãy sử dụng các hằng số trong lớp Thread để chỉ định mức độ ưu tiên của luồng.

Mẹo
Một luồng có thể không bao giờ có cơ hội chạy nếu luôn có một luồng có mức độ ưu tiên cao hơn chạy

tranh chấp hoặc đói khát ning hoặc một luồng có cùng mức độ ưu tiên không bao giờ mang lại. Tình huống này được gọi là tranh chấp

hoặc chết đói. Để tránh tranh chấp, luồng có mức độ ưu tiên cao hơn phải gọi phương thức ngủ hoặc lợi

nhuận theo định kỳ để cho một luồng có mức độ ưu tiên thấp hơn hoặc tương tự có cơ hội chạy.

30.6 Phương thức nào sau đây là phương thức thể hiện trong java.lang.Thread?

Kiểm tra điểm Phương thức nào có thể tạo ra một InterruptException? Cái nào trong số chúng được viết
bằng Java?

chạy, bắt đầu, dừng, tạm ngừng, tiếp tục, ngủ, gián đoạn, nhường, tham gia

30.7 Nếu một vòng lặp chứa một phương thức ném ra một ngoại lệ bị gián đoạn, tại sao phải
vòng lặp được đặt bên trong một khối try-catch ?

30.8 Bạn đặt mức độ ưu tiên cho một luồng như thế nào? Ưu tiên mặc định là gì?
Machine Translated by Google

30.5 Nghiên cứu điển hình: Văn bản nhấp nháy 1105

30.5 Nghiên cứu điển hình: Văn bản nhấp nháy


Bạn có thể sử dụng một chuỗi để điều khiển hoạt ảnh.
Chìa khóa

Điểm
Việc sử dụng đối tượng Dòng thời gian để điều khiển hoạt ảnh đã được giới thiệu trong Phần 15.11, Ani mation.

Ngoài ra, bạn cũng có thể sử dụng một chuỗi để điều khiển hoạt ảnh. Liệt kê 30.2 đưa ra một ví dụ hiển thị văn bản

nhấp nháy trên nhãn, như thể hiện trong Hình 30.6.

HÌNH 30.6 Dòng chữ “Chào mừng” nhấp nháy.

DANH SÁCH 30.2 FlashText.java


1 nhập javafx.application.Application;
2 nhập javafx.application.Platform;
3 nhập javafx.scene.Scene;
4 nhập javafx.scene.control.Label;
5 nhập javafx.scene.layout.StackPane;
6 nhập javafx.stage.Stage;
7
8 lớp công khai FlashText mở rộng Ứng dụng {
9 private String text = "";
10
11 @Override // Ghi đè phương thức bắt đầu trong lớp Ứng dụng
12 public void start (Giai đoạn chínhStage) {
13 Ngăn StackPane = new StackPane ();
14 Label lblText = new Label ("Lập trình thật thú vị"); tạo một nhãn
15 pane.getChildren (). add (lblText); dán nhãn trong ngăn

16
17 Chủ đề mới (new Runnable () { tạo một chủ đề

18 @Ghi đè
19 public void run () { chạy chủ đề

20 thử {
21 trong khi (đúng) {
22 if (lblText.getText (). trim (). length () == 0) thay đổi văn bản

23 text = "Chào mừng";


24 khác
25 text = "";
26
27 Platform.runLater (new Runnable () { // Chạy từ JavaFX GUI Platform.runLater
28 @Override
29 public void run () {
30 lblText.setText (văn bản); cập nhật GUI
31 }
32 });
33
34 Thread.sleep (200); ngủ
35 }
36 }
37 bắt (Ngoại lệ bị gián đoạn) {
38 }
39 }
40 }).khởi đầu();
41
Machine Translated by Google

1106 Chương 30 Lập trình đa luồng và song song

42 // Tạo một cảnh và đặt nó vào vùng hiển thị


Cảnh cảnh = Cảnh mới (ngăn, 200, 50);
43 primaryStage.setTitle ("FlashText"); // Đặt tiêu đề sân khấu
44 primaryStage.setScene (cảnh); // Đặt cảnh vào sân khấu
45 primaryStage.show (); // Hiển thị sân khấu
46 }
47 48}

Chương trình tạo một đối tượng Runnable trong một lớp ẩn danh bên trong (dòng 17–40). Đối tượng này được bắt đầu

ở dòng 40 và chạy liên tục để thay đổi văn bản trong nhãn. Nó đặt văn bản trong nhãn nếu nhãn trống (dòng 23) và đặt

văn bản của nó trống (dòng 25) nếu nhãn có văn bản.

Văn bản được đặt và không được đặt để mô phỏng hiệu ứng nhấp nháy.

Chuỗi ứng dụng JavaFX JavaFX GUI được chạy từ luồng ứng dụng JavaFX. Điều khiển nhấp nháy được chạy từ một luồng riêng biệt. Mã trong

một chuỗi không ứng dụng không thể cập nhật GUI trong chuỗi ứng dụng. Để cập nhật văn bản trong nhãn, một đối tượng

Runnable mới được tạo trong các dòng 27–32.

Platform.runLater Invoking Platform.runLater (Runnable r) yêu cầu hệ thống chạy đối tượng Runnable này trong luồng ứng dụng.

Các lớp bên trong ẩn danh trong chương trình này có thể được đơn giản hóa bằng cách sử dụng các biểu thức
lambda như sau:

new Thread (() -> { // biểu thức lambda


thử {
trong khi (đúng) {
if (lblText.getText (). trim (). length () == 0)
text = "Chào mừng";
khác
text = "";

Platform.runLater (() -> lblText.setText (text)); // lambda exp

Thread.sleep (200);
}
}
bắt (Ngoại lệ bị gián đoạn) {
}
}).khởi đầu();

30.9 Nguyên nhân làm cho văn bản nhấp nháy?

Kiểm tra điểm


30.10 Một thể hiện của FlashText có phải là một đối tượng có thể chạy được không?

30.11 Mục đích của việc sử dụng Platform.runLater là gì?

30.12 Bạn có thể thay thế mã ở dòng 27–32 bằng mã sau không?

Platform.runLater (e -> lblText.setText (text));

30.13 Điều gì xảy ra nếu dòng 34 (Thread.sleep (200)) không được sử dụng?

30.6 Hồ bơi Chủ đề


Một nhóm luồng có thể được sử dụng để thực thi các tác vụ một cách hiệu quả.
Chìa khóa

Điểm
Trong Phần 30.3, Tạo tác vụ và luồng, bạn đã học cách xác định một lớp tác vụ bằng cách nhập vào java.lang.Runnable

và cách tạo một luồng để chạy một tác vụ như sau:

Runnable task = new TaskClass (task);


new Thread (task) .start ();
Machine Translated by Google

30.6 Hồ bơi Chủ đề 1107

Cách tiếp cận này thuận tiện cho việc thực thi một tác vụ đơn lẻ, nhưng không hiệu quả đối với một số lượng

lớn các tác vụ vì bạn phải tạo một luồng cho mỗi tác vụ. Bắt đầu một luồng mới cho mỗi tác vụ có thể hạn chế

thông lượng và gây ra hiệu suất kém. Sử dụng nhóm luồng là một cách lý tưởng để quản lý số lượng tác vụ thực

thi đồng thời. Java cung cấp Trình thực thi

giao diện để thực thi các tác vụ trong một nhóm luồng và giao diện ExecutorService cho các tác vụ kiểm soát

và lão hóa của con người. ExecutorService là một giao diện con của Executor, như trong Hình 30.7.

«Giao diện»

java.util.concurrent.Executor

+ thực thi (đối tượng Runnable): void Thực thi tác vụ có thể chạy được.

«Giao diện»

java.util.concurrent.ExecutorService

+ shutdown (): void Tắt trình thực thi, nhưng cho phép các tác vụ trong trình thực thi
hoàn thành. Sau khi tắt, nó không thể chấp nhận các tác vụ mới.
+ shutdownNow (): Danh sách <Có thể chạy> Tắt người thi hành ngay lập tức mặc dù có
chủ đề chưa hoàn thành trong hồ bơi. Trả về danh sách các nhiệm vụ chưa hoàn thành.

+ isShutdown (): boolean Trả về true nếu trình thực thi đã bị tắt.

+ isTermina (): boolean Trả về true nếu tất cả các tác vụ trong nhóm bị chấm dứt.

HÌNH 30.7 Giao diện Executor thực thi các luồng và giao diện con ExecutorService quản lý các luồng.

Để tạo một đối tượng Executor , hãy sử dụng các phương thức tĩnh trong lớp Executor , như trong Hình

30.8. Phương thức newFixedThreadPool (int) tạo một số luồng cố định trong một nhóm. Nếu một luồng hoàn thành

việc thực thi một tác vụ, nó có thể được sử dụng lại để thực thi một tác vụ khác. Nếu một luồng kết thúc do

lỗi trước khi tắt, một luồng mới sẽ được tạo để thay thế nó nếu tất cả các luồng trong nhóm không ở chế độ

rỗi và có các tác vụ đang chờ thực thi. Phương thức newCachedThreadPool () tạo một luồng mới nếu tất cả các

luồng trong nhóm không rảnh và có các tác vụ đang chờ thực thi. Một chuỗi trong nhóm được lưu trong bộ nhớ

cache sẽ bị kết thúc nếu nó không được sử dụng trong 60 giây. Một nhóm được lưu trong bộ nhớ cache hiệu quả

cho nhiều tác vụ ngắn.

java.util.concurrent.Executor

+ newFixedThreadPool (numberOfThreads: Tạo một nhóm luồng với một số luồng cố định đang thực thi
int): ExecutorService kiêm nhiệm. Một chuỗi có thể được sử dụng lại để thực hiện một tác vụ khác
sau khi nhiệm vụ hiện tại của nó kết thúc.

+ newCachedThreadPool (): Tạo một nhóm luồng để tạo các luồng mới nếu cần, nhưng
ExecutorService sẽ sử dụng lại các chủ đề đã xây dựng trước đó khi chúng
có sẵn.

HÌNH 30.8 Lớp Executor cung cấp các phương thức tĩnh để tạo các đối tượng Executor .

Liệt kê 30.3 chỉ ra cách viết lại Liệt kê 30.1 bằng cách sử dụng một nhóm luồng.

LISTING 30.3 ExecutorDemo.java


1 nhập java.util.concurrent. *;
2
3 lớp công khai ExecutorDemo {
Machine Translated by Google

1108 Chương 30 Lập trình đa luồng và song song

4 public static void main (String [] args) {


// Tạo một nhóm luồng cố định với tối đa ba luồng
tạo người thực thi 5 Người thực thi ExecutorService = Executor.newFixedThreadPool (3);
6 7

// Gửi các tác vụ có thể chạy được cho trình thực thi
nộp nhiệm vụ executive.execute ( new PrintChar ('a', 100));
8 executive.execute ( new PrintChar ('b', 100));
9 10 11 execute.execute (PrintNum mới (100));
12
// Tắt trình thực thi
đóng cửa người thi hành executive.shutdown ();
}
13 14 15 16}

Dòng 6 tạo một trình thực thi nhóm luồng với tổng số tối đa ba luồng. Lớp học PrintChar
và PrintNum được định nghĩa trong Liệt kê 30.1. Dòng 9 tạo một nhiệm vụ, PrintChar mới ('a', 100) và
thêm nó vào nhóm. Tương tự, hai tác vụ có thể chạy khác được tạo và thêm vào cùng một nhóm ở dòng
10 và 11. Trình thực thi tạo ba luồng để thực thi ba tác vụ đồng thời.

Giả sử rằng bạn thay thế dòng 6 bằng

Người thực thi ExecutorService = Executor.newFixedThreadPool (1);

Chuyện gì sẽ xảy ra? Ba tác vụ có thể chạy được sẽ được thực hiện tuần tự vì chỉ có một luồng trong
nhóm.
Giả sử bạn thay thế dòng 6 bằng

Người thực thi ExecutorService = Executor.newCachedThreadPool ();

Chuyện gì sẽ xảy ra? Các luồng mới sẽ được tạo cho mỗi tác vụ đang chờ, vì vậy tất cả các tác vụ sẽ
được thực hiện đồng thời.
Phương thức shutdown () trong dòng 14 yêu cầu trình thực thi tắt. Không thể có nhiệm vụ mới

được chấp nhận, nhưng mọi nhiệm vụ hiện có sẽ tiếp tục hoàn thành.

Mẹo
Nếu bạn cần tạo một luồng chỉ cho một tác vụ, hãy sử dụng lớp Luồng . Nếu bạn cần
tạo luồng cho nhiều tác vụ, tốt hơn nên sử dụng nhóm luồng.

30.14 Lợi ích của việc sử dụng nhóm luồng là gì?

Kiểm tra điểm


30.15 Làm cách nào để bạn tạo một nhóm luồng với ba luồng cố định? Làm thế nào để bạn gửi một nhiệm vụ
vào một nhóm chủ đề? Làm thế nào để bạn biết rằng tất cả các nhiệm vụ đã hoàn thành?

30.7 Đồng bộ hóa luồng


Đồng bộ hóa luồng là điều phối việc thực thi các luồng phụ thuộc.
Chìa khóa

Điểm
Tài nguyên được chia sẻ có thể bị hỏng nếu nó được truy cập đồng thời bởi nhiều luồng.
Ví dụ sau đây chứng minh vấn đề.
Giả sử bạn tạo và khởi chạy 100 chủ đề, mỗi chủ đề sẽ thêm một xu vào tài khoản.
Xác định một lớp có tên Tài khoản để tạo mô hình cho tài khoản, một lớp có tên AddAPennyTask để thêm

một xu vào tài khoản và một lớp chính tạo và khởi chạy các luồng. Mối quan hệ của các lớp này được
thể hiện trong Hình 30.9. Chương trình được đưa ra trong Liệt kê 30.4.
Machine Translated by Google

30.7 Đồng bộ hóa luồng 1109

«Giao diện»
java.lang.Runnable

100 1 1 1
AddAPennyTask AccountWithoutSync Tài khoản

+ run (): void -tài khoản tài khoản -balance: int

+ main (args: String []): void + getBalance (): int


+ tiền gửi (số tiền: int): void

HÌNH 30.9 AccountWithoutSync chứa một phiên bản của Account và 100 luồng của AddAPennyTask.

LISTING 30.4 Tài khoảnWithoutSync.java


1 nhập java.util.concurrent. *;
2
3 tài khoản lớp công khaiWithoutSync {
4 tài khoản private static Account = new Account ();
5

6 public static void main (String [] args) {


7 Người thực thi ExecutorService = Executor.newCachedThreadPool (); tạo người thực thi

số 8

// Tạo và khởi chạy 100 chủ đề


9 for (int i = 0; i < 100; i ++) {
10 executive.execute (AddAPennyTask mới ()); nộp nhiệm vụ
11 }
12 13
14 executive.shutdown (); đóng cửa người thi hành

15
16 // Chờ cho đến khi tất cả các tác vụ hoàn thành
17 while (! execute.isTermina ()) { đợi cho tất cả các nhiệm vụ kết thúc

18 }
19
20 System.out.println ("Số dư là gì?" + Account.getBalance ());
21 }
22
23 // Một chuỗi để thêm một xu vào tài khoản
24 lớp tĩnh riêng AddAPennyTask triển khai Runnable {
25 public void run () {
26 account.deposit (1);
27 }
28 }
29
30 // Một lớp bên trong cho tài khoản
31 Tài khoản lớp tĩnh riêng {
32 private int balance = 0;
33
34 public int getBalance () {
35 trả lại số dư;
36 }
37
38 tiền gửi vô hiệu công khai (số tiền int ) {
39 int newBalance = số dư + số tiền;
40
41 // Độ trễ này được cố tình thêm vào để phóng đại
Machine Translated by Google

1110 Chương 30 Lập trình đa luồng và song song

42 // vấn đề tham nhũng dữ liệu và làm cho nó dễ dàng nhìn thấy.


43 thử {
44 Thread.sleep (5);
45 }
46 bắt (Ngoại lệ bị gián đoạn) {
47 }
48
49 số dư = newBalance;
}
}
50 51 52}

Các lớp AddAPennyTask và Account trong các dòng 24–51 là các lớp bên trong. Dòng 4 tạo Tài khoản có số dư ban

đầu 0. Dòng 11 tạo nhiệm vụ thêm một xu vào tài khoản và gửi nhiệm vụ cho người thực hiện. Dòng 11 được lặp lại

100 lần trong các dòng 10–12. Chương trình liên tục kiểm tra xem tất cả các nhiệm vụ đã được hoàn thành ở dòng

17 và 18. Số dư tài khoản được hiển thị ở dòng 20 sau khi tất cả các nhiệm vụ được hoàn thành.

Chương trình tạo 100 luồng được thực thi trong trình thực thi nhóm luồng ( dòng 10–12). Các

Phương thức isTermina () (dòng 17) được sử dụng để kiểm tra xem luồng có bị kết thúc hay không.

Số dư của tài khoản ban đầu là 0 (dòng 32). Khi tất cả các luồng kết thúc, số dư phải là 100 nhưng kết quả

đầu ra là không thể đoán trước. Như có thể thấy trong Hình 30.10, các câu trả lời là sai trong lần chạy mẫu.

Điều này chứng tỏ vấn đề hỏng dữ liệu xảy ra khi tất cả các luồng có quyền truy cập đồng thời vào cùng một nguồn

dữ liệu.

HÌNH 30.10 Chương trình AccountWithoutSync gây ra sự không nhất quán về dữ liệu.

Các dòng 39–49 có thể được thay thế bằng một câu lệnh:

số dư = số dư + số tiền;

Rất khó xảy ra, mặc dù hợp lý, vấn đề có thể được tái tạo bằng cách sử dụng câu lệnh đơn này. Các câu lệnh trong

dòng 39–49 được thiết kế có chủ ý để phóng đại vấn đề hỏng dữ liệu và giúp bạn dễ dàng nhìn thấy. Nếu bạn chạy

chương trình nhiều lần nhưng vẫn không thấy sự cố, hãy tăng thời gian ngủ ở dòng 44. Điều này sẽ làm tăng khả

năng hiển thị sự cố không nhất quán của dữ liệu.

Vậy điều gì đã gây ra lỗi trong chương trình này? Một kịch bản có thể xảy ra được thể hiện trong Hình 30.11.

Bước Cân bằng Nhiệm vụ 1 Nhiệm vụ 2

1 0 newBalance = số dư + 1;
2 0 newBalance = số dư + 1;
3 1 số dư = newBalance;
4 1 số dư = newBalance;

HÌNH 30.11 Nguyên công 1 và nguyên công 2 đều cộng 1 vào cùng một số dư.
Machine Translated by Google

30.7 Đồng bộ hóa luồng 1111

Ở Bước 1, Nhiệm vụ 1 lấy số dư từ tài khoản. Trong Bước 2, Nhiệm vụ 2 nhận được số dư tương tự từ

tài khoản. Ở Bước 3, Nhiệm vụ 1 ghi số dư mới vào tài khoản. Ở Bước 4, Nhiệm vụ 2 ghi số dư mới vào tài
khoản.

Tác dụng của kịch bản này là Nhiệm vụ 1 không làm gì cả vì trong Bước 4, Nhiệm vụ 2 ghi đè kết quả của

Nhiệm vụ 1. Rõ ràng, vấn đề là Nhiệm vụ 1 và Nhiệm vụ 2 đang truy cập một tài nguyên chung theo cách gây ra
điều kiện của cuộc đua
xung đột. Đây là một vấn đề phổ biến, được gọi là tình trạng chủng tộc, trong các chương trình đa luồng.
chỉ an toàn
Một lớp được cho là an toàn theo luồng nếu một đối tượng của lớp không gây ra tình trạng chạy đua với sự

hiện diện của nhiều luồng. Như đã trình bày trong ví dụ trước, lớp Tài khoản không an toàn theo luồng.

30.7.1 Từ khóa được đồng bộ hóa


Để tránh các điều kiện về chủng tộc, cần phải ngăn chặn nhiều luồng tham gia đồng thời vào một phần nhất

định của chương trình, được gọi là vùng tới hạn. Vùng quan trọng trong Liệt kê 30.4 là toàn bộ phương khu vực quan trọng

thức ký quỹ . Bạn có thể sử dụng từ khóa được đồng bộ hóa

để đồng bộ hóa phương thức sao cho chỉ một luồng có thể truy cập phương thức tại một thời điểm. Có một

số cách để khắc phục sự cố trong Liệt kê 30.4. Một cách tiếp cận là tạo Tài khoản

an toàn luồng bằng cách thêm từ khóa được đồng bộ hóa trong phương thức gửi tiền ở dòng 38, như sau:

tiền gửi vô hiệu được đồng bộ hóa công khai ( số tiền gấp đôi)

Một phương thức được đồng bộ hóa có được một khóa trước khi nó thực thi. Khóa là một cơ chế để sử dụng

tài nguyên một cách rõ ràng. Trong trường hợp của một phương thức thể hiện, khóa nằm trên đối tượng mà
phương thức được gọi. Trong trường hợp của một phương thức tĩnh, khóa nằm trên lớp. Nếu một luồng gọi

một phương thức thể hiện được đồng bộ hóa (tương ứng, phương thức tĩnh) trên một đối tượng, thì khóa

của đối tượng đó (tương ứng, lớp) được nhận trước, sau đó phương thức được thực thi và cuối cùng khóa

được giải phóng. Một luồng khác gọi cùng một phương thức của đối tượng đó (tương ứng, lớp) bị chặn cho
đến khi khóa được giải phóng.

Với phương thức gửi tiền được đồng bộ hóa, kịch bản trước đó không thể xảy ra. Nếu Tác vụ 1 vào

phương thức, thì Tác vụ 2 sẽ bị chặn cho đến khi Tác vụ 1 kết thúc phương thức, như thể hiện trong Hình 30.12.

Nhiệm vụ 1 Nhiệm vụ 2

Yêu cầu khóa tài khoản đối tượng

Thực hiện phương thức gửi tiền

Chờ để có được khóa

Mở khóa

Yêu cầu khóa tài khoản đối tượng

Thực hiện phương thức gửi tiền

Mở khóa

HÌNH 30.12 Nhiệm vụ 1 và Nhiệm vụ 2 được đồng bộ hóa.

30.7.2 Đồng bộ hóa các câu lệnh


Gọi một phương thức thể hiện đồng bộ của một đối tượng nhận được một khóa trên đối tượng và gọi một

phương thức tĩnh đồng bộ của một lớp sẽ nhận được một khóa trên lớp. Một câu lệnh được đồng bộ hóa có

thể được sử dụng để có được một khóa trên bất kỳ đối tượng nào, không chỉ đối tượng này, khi thực thi một
Machine Translated by Google

1112 Chương 30 Lập trình đa luồng và song song

khối đồng bộ khối mã trong một phương thức. Khối này được gọi là khối được đồng bộ hóa. Dạng tổng quát của một câu lệnh được

đồng bộ hóa như sau:

đã đồng bộ hóa (expr) {


các câu lệnh;
}

Biểu thức expr phải đánh giá thành một tham chiếu đối tượng. Nếu đối tượng đã bị khóa bởi một luồng khác, thì luồng

đó sẽ bị chặn cho đến khi khóa được giải phóng. Khi có được một khóa trên đối tượng, các câu lệnh trong khối được

đồng bộ hóa sẽ được thực thi và sau đó khóa được giải phóng.

Các câu lệnh được đồng bộ hóa cho phép bạn đồng bộ hóa một phần mã trong một phương thức thay vì toàn bộ

phương thức. Điều này làm tăng tính đồng thời. Bạn có thể làm cho Liệt kê 30.4 trở nên an toàn bằng cách đặt câu

lệnh ở dòng 26 bên trong một khối được đồng bộ hóa:

đã đồng bộ hóa (tài khoản) {


account.deposit (1);
}

Ghi chú

Bất kỳ phương thức cá thể đồng bộ nào cũng có thể được chuyển đổi thành một câu lệnh được đồng bộ hóa.

Ví dụ: phương thức phiên bản được đồng bộ hóa sau trong (a) tương đương với (b):

công khai đồng bộ hóa void xMethod () { public void xMethod () {


// phần thân của phương thức đã đồng bộ hóa (cái này) {
} // phần thân của phương thức

}
}

(Một) (b)

30.16 Đưa ra một số ví dụ về khả năng tham nhũng tài nguyên khi chạy nhiều luồng.

Kiểm tra điểm Làm cách nào để bạn đồng bộ hóa các luồng xung đột?

30.17 Giả sử bạn đặt câu lệnh ở dòng 26 của Liệt kê 30.4 bên trong một

khối để tránh các điều kiện cuộc đua, như sau:

đã đồng bộ hóa (cái này) {


account.deposit (1);
}

Nó sẽ hoạt động?

30.8 Đồng bộ hóa bằng cách sử dụng khóa


Các khóa và điều kiện có thể được sử dụng một cách rõ ràng để đồng bộ hóa các luồng.
Chìa khóa

Điểm
Nhớ lại rằng trong Liệt kê 30.4, 100 nhiệm vụ gửi đồng thời một xu vào cùng một tài khoản, điều này gây ra xung đột.

Để tránh điều đó, bạn sử dụng từ khóa được đồng bộ hóa trong phương thức gửi tiền , như sau:

tiền gửi vô hiệu được đồng bộ hóa công khai ( số tiền gấp đôi)

Khóa Một phương thức cá thể được đồng bộ hóa hoàn toàn có được một khóa trên cá thể trước khi nó thực thi phương thức.

Java cho phép bạn có được các khóa một cách rõ ràng, giúp bạn kiểm soát nhiều hơn để điều phối các luồng. Khóa

là một thể hiện của giao diện Khóa , giao diện này xác định các phương pháp lấy và giải phóng khóa, như được thể

hiện trong Hình 30.13. Một khóa cũng có thể sử dụng phương thức newCondition () để tạo bất kỳ số lượng đối tượng

Điều kiện nào, có thể được sử dụng cho truyền thông luồng.
Machine Translated by Google

30.8 Đồng bộ hóa bằng cách sử dụng khóa 1113

«Giao diện»

java.util.concurrent.locks.Lock

+ lock (): void Mua lại khóa.


+ unlock (): void Mở khóa.

+ newCondition (): Tình trạng Trả về một thể hiện Điều kiện mới được liên kết với điều này

Khóa phiên bản.

java.util.concurrent.locks.ReentrantLock

+ ReentrantLock () Giống như ReentrantLock (false).


+ ReentrantLock (công bằng: boolean) Tạo một khóa với chính sách công bằng nhất định. Khi mà
sự công bằng là đúng, chủ đề được chờ đợi lâu nhất sẽ nhận được
Khóa. Nếu không, không có lệnh truy cập cụ thể.

HÌNH 30.13 Lớp ReentrantLock triển khai giao diện Khóa để biểu diễn một khóa.

ReentrantLock là một triển khai cụ thể của Lock để tạo các khóa loại trừ lẫn nhau. Bạn có thể
tạo một khóa với chính sách công bằng được chỉ định. Chính sách công bằng thực sự đảm bảo rằng chính sách công bằng

chủ đề chờ đợi lâu nhất sẽ nhận được khóa đầu tiên. Chính sách công bằng sai sẽ tự ý khóa một
chuỗi chờ. Các chương trình sử dụng khóa hợp lý được nhiều luồng truy cập có thể có hiệu suất
tổng thể kém hơn so với các chương trình sử dụng cài đặt mặc định, nhưng chúng có sự khác
biệt nhỏ hơn về thời gian để có được khóa và ngăn chặn nạn đói.
Liệt kê 30.5 sửa đổi chương trình trong Liệt kê 30.7 để đồng bộ hóa việc sửa đổi tài khoản
sử dụng khóa rõ ràng.

LISTING 30.5 Tài khoảnWithSyncUsingLock.java


1 nhập java.util.concurrent. *;
2 nhập java.util.concurrent.locks. *; gói cho ổ khóa
3
4 public class AccountWithSyncUsingLock {
tài khoản private static Account = new Account ();
5 6

7 public static void main (String [] args) {


8 Người thực thi ExecutorService = Executor.newCachedThreadPool ();
9

10 // Tạo và khởi chạy 100 chủ đề


11 for (int i = 0; i < 100; i ++) {
12 executive.execute (AddAPennyTask mới ());
13 }
14
15 executive.shutdown ();
16
17 // Chờ cho đến khi tất cả các tác vụ hoàn thành
18 while (! execute.isTermina ()) {
19 }
20
21 System.out.println ("Số dư là gì?" + Account.getBalance ());
22 }
23
24 // Một chuỗi để thêm một xu vào tài khoản
25 public static class AddAPennyTask triển khai Runnable {
26 public void run () {
27 account.deposit (1);
28 }
29 }
30
Machine Translated by Google

1114 Chương 30 Lập trình đa luồng và song song

31 // Một lớp bên trong cho Tài khoản


32 tài khoản lớp public static {
tạo một ổ khóa 33 khóa private static Lock = new ReentrantLock (); // Tạo khóa
34 private int balance = 0;
35

36 public int getBalance () {


37 trả lại số dư;
38 }
39
40 tiền gửi vô hiệu công khai (số tiền int ) {
lấy khóa 41 lock.lock (); // Lấy khóa
42
43 thử {
44 int newBalance = số dư + số tiền;
45
46 // Độ trễ này được cố tình thêm vào để phóng đại
47 // vấn đề tham nhũng dữ liệu và làm cho nó dễ dàng nhìn thấy.
48 Thread.sleep (5);
49
50 số dư = newBalance;
51 }
52 bắt (Ngoại lệ bị gián đoạn) {
53 }
54 cuối cùng {
mở khóa 55 khóa mở khóa(); // Mở khóa
}
}
}
56 57 58 59}

Dòng 33 tạo khóa, dòng 41 nhận khóa và dòng 55 giải phóng khóa.

Mẹo
Một phương pháp hay là luôn thực hiện ngay theo lệnh gọi khóa () bằng cách thử bắt
chặn và nhả khóa trong mệnh đề cuối cùng , như được hiển thị trong các dòng 41–56, để

đảm bảo rằng khóa luôn được mở ra.

Liệt kê 30.5 có thể được thực hiện bằng cách sử dụng phương pháp đồng bộ hóa để gửi tiền thay vì sử

dụng khóa. Nói chung, sử dụng các phương pháp hoặc câu lệnh được đồng bộ hóa đơn giản hơn so với việc sử

dụng các khóa rõ ràng để loại trừ lẫn nhau. Tuy nhiên, việc sử dụng các khóa rõ ràng sẽ trực quan hơn và có

thể linh hoạt để đồng bộ hóa các luồng với các điều kiện, như bạn sẽ thấy trong phần tiếp theo.

30.18 Làm cách nào để bạn tạo một đối tượng khóa? Làm thế nào để bạn có được một ổ khóa và phát hành một ổ khóa?

Kiểm tra điểm

30.9 Hợp tác giữa các chủ đề


Các điều kiện về khóa có thể được sử dụng để điều phối các tương tác luồng.
Chìa khóa

Điểm
Đồng bộ hóa luồng đủ để tránh các điều kiện chạy đua bằng cách đảm bảo loại trừ lẫn nhau của nhiều luồng

trong vùng quan trọng, nhưng đôi khi bạn cũng cần một cách để các luồng hợp tác. Các điều kiện có thể được
tình trạng sử dụng để tạo điều kiện giao tiếp giữa các luồng. Một luồng có thể chỉ định những gì cần làm trong một điều

kiện nhất định. Điều kiện là các đối tượng được tạo bằng cách gọi phương thức newCondition () trên một đối

tượng Khóa . Sau khi một điều kiện được tạo, bạn có thể sử dụng các phương thức await (), signal () và

signalAll () của nó để truyền thông luồng, như trong Hình 30.14. Phương thức await () làm cho luồng hiện

tại đợi cho đến khi điều kiện được báo hiệu. Phương thức signal () đánh thức một luồng đang chờ và phương

thức signalAll () đánh thức tất cả các luồng đang chờ.


Machine Translated by Google

30,9 Hợp tác giữa các Chủ đề 1115

«Giao diện»
java.util.concurrent.Condition

+ await (): vô hiệu Làm cho luồng hiện tại đợi cho đến khi điều kiện được báo hiệu.
+ signal (): void Đánh thức một chuỗi đang chờ.
+ signalAll (): Điều kiện Đánh thức tất cả các chủ đề đang chờ.

HÌNH 30.14 Giao diện Điều kiện xác định các phương pháp để thực hiện đồng bộ hóa.

Hãy để chúng tôi sử dụng một ví dụ để chứng minh giao tiếp chuỗi. Giả sử rằng bạn tạo và khởi chạy hai tác vụ: ví dụ hợp tác chủ đề
một tác vụ gửi tiền vào tài khoản và một tác vụ rút tiền từ cùng một tài khoản. Nhiệm vụ rút tiền phải đợi nếu số
tiền được rút nhiều hơn số dư tiền thuê. Bất cứ khi nào tiền mới được gửi vào tài khoản, nhiệm vụ gửi tiền sẽ

thông báo cho chuỗi rút tiền được tiếp tục. Nếu số tiền vẫn không đủ để rút, chuỗi rút phải tiếp tục chờ gửi tiền

mới.

Để đồng bộ hóa các hoạt động, hãy sử dụng khóa với điều kiện: newDeposit (nghĩa là thêm khoản tiền gửi mới vào

tài khoản). Nếu số dư ít hơn số tiền cần rút, tác vụ rút tiền sẽ chờ điều kiện Gửi tiền mới . Khi nhiệm vụ gửi

thêm tiền vào tài khoản, nhiệm vụ báo hiệu nhiệm vụ rút tiền đang chờ để thử lại. Sự tương tác giữa hai nguyên

công được thể hiện trong Hình 30.15.

Rút lại tác vụ Nhiệm vụ gửi tiền

lock.lock (); lock.lock ();

trong khi (số dư <rút tiền) số dư + = tiền gửi


newDeposit.await ();

newDeposit.signalAll ();
số dư - = rút lui

khóa mở khóa();
khóa mở khóa();

HÌNH 30.15 Điều kiện newDeposit được sử dụng để liên lạc giữa hai luồng.

Bạn tạo một điều kiện từ một đối tượng Khóa . Để sử dụng một điều kiện, trước tiên bạn phải có được một khóa.

Phương thức await () khiến luồng đợi và tự động giải phóng khóa theo điều kiện. Khi điều kiện phù hợp, luồng sẽ

yêu cầu lại khóa và tiếp tục thực thi.

Giả sử rằng số dư ban đầu là 0 và số tiền để gửi và rút được thực hiện ngay lập tức. Liệt kê 30.6 cung cấp

cho chương trình. Một mẫu chạy chương trình được thể hiện trong Hình 30.16.

HÌNH 30.16 Nhiệm vụ rút tiền sẽ đợi nếu không có đủ tiền để rút.
Machine Translated by Google

1116 Chương 30 Lập trình đa luồng và song song

LISTING 30.6 ThreadCooperation.java


1 nhập java.util.concurrent. *;
2 nhập java.util.concurrent.locks. *;
3
4 lớp công khai ThreadCooperation {
5 tài khoản Tài khoản tĩnh riêng tư = Tài khoản mới ();
6
7 public static void main (String [] args) {
số 8
// Tạo một nhóm luồng với hai luồng
tạo hai chủ đề Người thực thi ExecutorService = Executor.newFixedThreadPool (2);
9 executive.execute ( mới DepositTask ());
10 executive.execute (new WithdrawTask ());
11 executive.shutdown ();
12 13

14 System.out.println (" Luồng 1 \ t \ tThread 2 \ t \ tBalance");


15 }
16
17 public static class DepositTask triển khai Runnable {
18 @Override // Tiếp tục thêm số tiền vào tài khoản
19 public void run () {
20 thử { // Cố ý trì hoãn nó để cho phép phương pháp rút tiền tiếp tục
21 trong khi (đúng) {
22 account.deposit ((int) (Math.random () * 10) + 1);
23 Thread.sleep (1000);
24 }
25 }
26 bắt (Ngoại lệ bị gián đoạn) {
27 ex.printStackTrace ();
28 }
29 }
30 }
31
32 public static class WithdrawTask triển khai Runnable {
33 @Override // Tiếp tục trừ một số tiền trong tài khoản
34 public void run () {
35 trong khi (đúng) {
36 account.withdraw ((int) (Math.random () * 10) + 1);
37 }
38 }
39 }
40

41 // Một lớp bên trong cho tài khoản


42 Tài khoản lớp tĩnh riêng {
43 // Tạo một khóa mới
tạo một ổ khóa 44 khóa private static Lock = new ReentrantLock ();
45
46 // Tạo điều kiện
tạo ra một điều kiện 47 private static Condition newDeposit = lock.newCondition ();
48
49 private int balance = 0;
50
51 public int getBalance () {
52 trả lại số dư;
53 }
54
55 rút tiền vô hiệu công khai (số tiền int ) {
lấy khóa 56 lock.lock (); // Lấy khóa thử {
57
58 trong khi (số dư <số tiền) {
Machine Translated by Google

30,9 Hợp tác giữa các Chủ đề 1117

59 System.out.println ("\ t \ t \ tChờ gửi tiền");


60 newDeposit.await (); chờ đợi với điều kiện

61 }
62
63 số dư - = số tiền;
64 System.out.println ("\ t \ t \ tWithdraw " "\ + số tiền +
65 t \ t" + getBalance ());
66 }
67 bắt (Ngoại lệ bị gián đoạn) {
68 ex.printStackTrace ();
69 }
70 cuối cùng {
71 khóa mở khóa(); // Mở khóa mở khóa
72 }
73 }
74
75 tiền gửi vô hiệu công khai (số tiền int ) {
76 lock.lock (); // Lấy khóa thử { lấy khóa
77
78 số dư + = số tiền;
79 System.out.println ("Gửi tiền " + số tiền +
80 "\ t \ t \ t \ t \ t" + getBalance ());
81
82 // Chuỗi tín hiệu đang chờ với điều kiện
83 newDeposit.signalAll (); chủ đề tín hiệu
84 }
85 cuối cùng {
86 khóa mở khóa(); // Mở khóa mở khóa
87 }
88 }
89 }
90}

Ví dụ này tạo một lớp bên trong mới có tên là Tài khoản để tạo mô hình tài khoản với hai meth
ods, gửi (int) và rút (int), một lớp có tên DepositTask để thêm một số tiền vào số dư, một lớp
có tên WithdrawTask để rút một số tiền từ số dư và một lớp chính tạo và khởi chạy hai luồng.

Chương trình tạo và gửi nhiệm vụ gửi tiền (dòng 10) và nhiệm vụ rút tiền (dòng 11).
Nhiệm vụ gửi tiền được cố ý chuyển sang chế độ ngủ (dòng 23) để cho phép tác vụ rút tiền chạy.
Khi không có đủ tiền để rút, nhiệm vụ rút tiền sẽ đợi (dòng 59) thông báo về sự thay đổi số dư
từ nhiệm vụ gửi tiền (dòng 83).
Một khóa được tạo ở dòng 44. Một điều kiện có tên newDeposit trên khóa được tạo ở dòng 47.
Một điều kiện được ràng buộc với một khóa. Trước khi chờ đợi hoặc báo hiệu điều kiện, trước
tiên một luồng phải có được khóa cho điều kiện. Nhiệm vụ rút tiền nhận được khóa ở dòng 56,
chờ điều kiện Gửi tiền mới (dòng 60) khi không có đủ số tiền để rút và nhả khóa ở dòng 71.
Nhiệm vụ gửi tiền nhận được khóa ở dòng 76 và báo hiệu tất cả đang chờ chủ đề (dòng 83) cho
điều kiện NewDeposit sau khi một khoản tiền gửi mới được thực hiện.

Điều gì sẽ xảy ra nếu bạn thay thế vòng lặp while trong các dòng 58–61 bằng vòng lặp sau nếu
bản tường trình?

if (số dư <số tiền) {


System.out.println ("\ t \ t \ tChờ gửi tiền");
newDeposit.await ();
}

Nhiệm vụ gửi tiền sẽ thông báo nhiệm vụ rút tiền bất cứ khi nào số dư thay đổi. (số dư <số
tiền) có thể vẫn đúng khi tác vụ rút tiền được đánh thức. Sử dụng if
Machine Translated by Google

1118 Chương 30 Lập trình đa luồng và song song

tuyên bố có thể dẫn đến việc rút tiền không chính xác. Sử dụng câu lệnh lặp, nhiệm vụ rút
tiền sẽ có cơ hội kiểm tra lại điều kiện trước khi thực hiện lệnh rút tiền.

Thận trọng

Khi một luồng gọi ra await () theo một điều kiện, thì luồng đó sẽ đợi một tín hiệu để tiếp tục.

chủ đề luôn chờ đợi Nếu bạn quên gọi signal () hoặc signalAll () với điều kiện, luồng sẽ đợi mãi mãi.

Thận trọng

Một điều kiện được tạo từ một đối tượng Khóa . Để gọi phương thức của nó (ví dụ: await (),

IllegalMonitorState signal () và signalAll ()), trước tiên bạn phải sở hữu khóa. Nếu bạn gọi các phương thức này
Ngoại lệ mà không có khóa, IllegalMonitorStateException sẽ được ném ra.

Các khóa và điều kiện đã được giới thiệu trong Java 5. Trước Java 5, truyền thông luồng được lập

trình bằng cách sử dụng các màn hình tích hợp của đối tượng. Các khóa và điều kiện hoạt động mạnh mẽ
và linh hoạt hơn so với màn hình tích hợp, vì vậy bạn sẽ không cần sử dụng màn hình. Tuy nhiên, nếu
Màn hình tích hợp của Java bạn đang làm việc với mã Java kế thừa, bạn có thể gặp phải màn hình tích hợp sẵn của Java.
màn hình Màn hình là một đối tượng có khả năng loại trừ và đồng bộ hóa lẫn nhau. Chỉ một luồng có thể thực
thi một phương thức tại một thời điểm trong màn hình. Một luồng đi vào màn hình bằng cách lấy một
khóa trên đó và thoát ra bằng cách nhả khóa. Bất kỳ đối tượng nào cũng có thể là màn hình. Một đối
tượng trở thành một màn hình sau khi một luồng khóa nó. Khóa được triển khai bằng cách sử dụng từ
khóa được đồng bộ hóa trên một phương thức hoặc một khối. Một luồng phải có được một khóa trước
khi thực thi một phương thức hoặc khối được đồng bộ hóa. Một luồng có thể đợi trong màn hình nếu
điều kiện không phù hợp để nó tiếp tục thực thi trong màn hình. Bạn có thể gọi phương thức wait ()
trên đối tượng màn hình để giải phóng khóa để một số luồng khác có thể vào màn hình và có thể thay
đổi trạng thái của màn hình. Khi điều kiện phù hợp, luồng khác có thể gọi phương thức thông báo ()
hoặc thông báoAll () để báo hiệu một hoặc tất cả các luồng đang chờ lấy lại khóa và tiếp tục thực
thi. Mẫu để gọi các phương thức này được hiển thị trong Hình 30.17.

Nhiệm vụ 1 Nhiệm vụ 2

đã đồng bộ hóa (anObject) {


thử { đã đồng bộ hóa (anObject) {
// Chờ điều kiện trở thành true // Khi điều kiện trở thành true
trong khi (! điều kiện) bản tóm tắt
anObject.notify (); hoặc anObject.notifyAll ();
...
anObject.wait ();
}
// Làm điều gì đó khi điều kiện là đúng
}
bắt (Ngoại lệ bị gián đoạn) {
ex.printStackTrace ();
}
}

HÌNH 30.17 Các phương thức wait (), thông báo () và thông báo cho tất cả ( ) điều phối giao tiếp luồng.

Các phương thức wait (), thông báo () và thông báo Nếu không, IllegalMonitorStateException
sẽ xảy ra.

Khi wait () được gọi, nó tạm dừng luồng và đồng thời giải phóng khóa trên đối tượng. Khi
chuỗi được khởi động lại sau khi được thông báo, khóa sẽ tự động được yêu cầu lại.
Các phương thức wait (), Inform ( ) và InformAll () trên một đối tượng tương tự với
Các phương thức await (), signal () và signalAll () với một điều kiện.
Machine Translated by Google

30.10 Nghiên cứu điển hình: Nhà sản xuất / Người tiêu dùng 1119

30.19 Làm thế nào để bạn tạo một điều kiện cho một khóa? Các phương thức await (), signal ()
và signalAll () để làm gì? Kiểm tra điểm

30.20 Điều gì sẽ xảy ra nếu vòng lặp while ở dòng 58 của Liệt kê 30.6 được thay đổi thành
câu lệnh if ?

Được thay thế bởi


trong khi (số dư <số tiền) if (số dư <số tiền)

30.21 Tại sao lớp sau có lỗi cú pháp?

public class Test triển khai Runnable {


public static void main (String [] args) {
new Test ();
}

public Test () ném InterruptException {


Chủ đề luồng = new Thread (this);
thread.sleep (1000);
}

công khai đồng bộ hóa void run () {


}
}

30.22 Nguyên nhân có thể gây ra IllegalMonitorStateException là gì?

30.23 Có thể gọi hàm wait (), thông báo () và thông báoAll () từ bất kỳ đối tượng nào không? Gì
mục đích của những phương pháp này là gì?

30.24 Điều nào sai trong đoạn mã sau?

đã đồng bộ hóa (object1) {


thử {
while (! điều kiện) object2.wait ();
}
bắt (Ngoại lệ bị gián đoạn) {
}
}

30.10 Nghiên cứu điển hình: Nhà sản xuất / Người tiêu dùng

Phần này đưa ra ví dụ về Người tiêu dùng / Nhà sản xuất cổ điển để thể hiện sự
điều phối luồng.
Chìa khóa

Điểm

Giả sử bạn sử dụng bộ đệm để lưu trữ số nguyên và kích thước bộ đệm bị giới hạn. Bộ đệm
pro cung cấp phương thức write (int) để thêm giá trị int vào vùng đệm và phương thức read ()
để đọc và xóa một giá trị int khỏi bộ đệm. Để đồng bộ hóa các hoạt động, hãy sử dụng một
khóa với hai điều kiện: notEmpty (tức là bộ đệm không trống) và notFull (tức là bộ đệm không
đầy). Khi một tác vụ thêm một int vào bộ đệm, nếu bộ đệm đầy, tác vụ sẽ đợi điều kiện
notFull . Khi một tác vụ đọc một int từ bộ đệm, nếu bộ đệm trống, tác vụ sẽ đợi điều kiện
notEmpty . Sự tương tác giữa hai nguyên công được thể hiện trong Hình 30.18.

Liệt kê 30.7 trình bày chương trình hoàn chỉnh. Chương trình chứa lớp Buffer (dòng 50–
101) và hai nhiệm vụ để thêm và sử dụng nhiều lần các số đến và đi từ bộ đệm (dòng 16–47).
Phương thức write (int) (dòng 62–79) thêm một số nguyên vào bộ đệm. Phương thức read ()
(dòng 81–100) xóa và trả về một số nguyên từ bộ đệm.
Machine Translated by Google

1120 Chương 30 Lập trình đa luồng và song song

Nhiệm vụ thêm một int Tác vụ xóa int

trong khi (đếm == CÔNG SUẤT) trong khi (đếm == 0)


notFull.await (); notEmpty.await ();

Thêm một số nguyên vào bộ đệm Xóa một int khỏi bộ đệm

notEmpty.signal (); notFull.signal ();

HÌNH 30.18 Các điều kiện notFull và notEmpty được sử dụng để điều phối các tương tác
tác vụ.

Bộ đệm thực sự là một hàng đợi vào trước, xuất trước (dòng 52–53). Các điều kiện không
và notFull trên khóa được tạo ở dòng 59–60. Các điều kiện được ràng buộc với một khóa. Một khóa

phải được mua trước khi một điều kiện có thể được áp dụng. Nếu bạn sử dụng hàm wait () và thông báo ()
để viết lại ví dụ này, bạn phải chỉ định hai đối tượng làm màn hình.

LISTING 30.7 ConsumerProductioner.java


1 nhập java.util.concurrent. *;
2 nhập java.util.concurrent.locks. *;
3
4 hạng công chúng Người tiêu dùng {
tạo vùng đệm 5 private static Buffer buffer = new Buffer ();
6
public static void main (String [] args) {
7 8 // Tạo một nhóm luồng với hai luồng
tạo hai chủ đề 9 Người thực thi ExecutorService = Executor.newFixedThreadPool (2);
10 executive.execute (new ProducerTask ());
11 executive.execute (new ConsumerTask ());
12 executive.shutdown ();
13 }
14
15 // Một nhiệm vụ để thêm một int vào bộ đệm
nhiệm vụ nhà sản xuất 16 private static class ProducerTask triển khai Runnable {
17 public void run () {
18 thử {
19 int i = 1;
20 trong khi (đúng) {
21 System.out.println ("Nhà sản xuất ghi" + i);
22 buffer.write (i ++); // Thêm giá trị vào bộ đệm
23 // Đặt luồng vào chế độ ngủ
24 Thread.sleep ((int) (Math.random () * 10000));
25 }
26
27 } catch (InterruptException ex) {
28 ex.printStackTrace ();
29 }
30 }
31 }
32
33 // Một tác vụ để đọc và xóa một int khỏi bộ đệm
nhiệm vụ của người tiêu dùng 34 lớp tĩnh riêng ConsumerTask triển khai Runnable {
35 public void run () {
Machine Translated by Google

30.10 Nghiên cứu điển hình: Nhà sản xuất / Người tiêu dùng 1121

36 thử {
37 trong khi (đúng) {
38 System.out.println ("\ t \ t \ tConsumer đọc" + buffer.read ());
39 // Đặt luồng vào chế độ ngủ
40 Thread.sleep ((int) (Math.random () * 10000));
41 }
42
43 } catch (InterruptException ex) {
44 ex.printStackTrace ();
45 }
46 }
47 }
48

49 // Một lớp bên trong cho bộ đệm


50 bộ đệm lớp tĩnh riêng {
51 private static final int CAPACITY = 1; // kích thước bộ đệm
52 private java.util.LinkedList <Integer> queue =
53 new java.util.LinkedList <> ();
54
55 // Tạo một khóa mới
56 khóa private static Lock = new ReentrantLock (); tạo một ổ khóa

57
58 // Tạo hai điều kiện
59 private static Condition notEmpty = lock.newCondition (); tạo ra một điều kiện

60 private static Condition notFull = lock.newCondition (); tạo ra một điều kiện

61
62 public void write (int value) {
63 lock.lock (); // Lấy khóa lấy khóa
64 thử {
65 while (queue.size () == NĂNG LỰC) {
66 System.out.println ("Chờ điều kiện không đầy đủ");
67 notFull.await (); chờ đợi cho không
68 }
69
70 queue.offer (giá trị);
71 notEmpty.signal (); // Điều kiện báo hiệu notEmpty signal notEmpty
72
73 } catch (InterruptException ex) {
74 ex.printStackTrace ();
75
76 } cuối cùng {
77 khóa mở khóa(); // Mở khóa mở khóa
78 }
79 }
80
81 public int read () {
82 giá trị int = 0;
83 lock.lock (); // Lấy khóa lấy khóa
84 thử {
85 while (queue.isEmpty ()) {
86 System.out.println ("\ t \ t \ tChờ điều kiện notEmpty");
87 notEmpty.await (); đợi cho khôngEmpty
88 }
89
90 value = queue.remove ();
91 notFull.signal (); // Signal notFull condition tín hiệu không đầy đủ

92
93 } catch (InterruptException ex) {
94 ex.printStackTrace ();
95 }
Machine Translated by Google

1122 Chương 30 Lập trình đa luồng và song song

96 cuối cùng {
mở khóa 97 khóa mở khóa(); // Mở khóa
98 giá trị trả về ;
99 }
100 }
101 }
102}

Một mẫu chạy chương trình được thể hiện trong Hình 30.19.

HÌNH 30.19 Các khóa và điều kiện được sử dụng để liên lạc giữa chuỗi Nhà sản xuất và Người tiêu dùng.

30.25 Các phương thức đọc và ghi trong lớp Buffer có thể được thực thi đồng thời không?

Kiểm tra điểm 30.26 Khi gọi phương thức đọc , điều gì xảy ra nếu hàng đợi trống?

30.27 Khi gọi phương thức write , điều gì sẽ xảy ra nếu hàng đợi đầy?

30.11 Chặn hàng đợi


Java Collections Framework cung cấp ArrayBlockingQueue, LinkedBlockingQueue và
Chìa khóa

Điểm PriorityBlockingQueue để hỗ trợ các hàng đợi chặn.

hàng đợi chặn Hàng đợi và hàng đợi ưu tiên được giới thiệu trong Phần 20.9. Hàng đợi chặn khiến luồng bị chặn
khi bạn cố gắng thêm một phần tử vào hàng đợi đầy đủ hoặc xóa một phần tử khỏi hàng đợi trống. Giao
diện BlockingQueue mở rộng java.util.Queue và cung cấp các phương thức put và take được đồng bộ
hóa để thêm một phần tử vào đuôi của hàng đợi và để xóa một phần tử khỏi phần đầu của hàng đợi,
như trong Hình 30.20.

«Giao diện»
java.util.Collection <E>

«Giao diện»
java.util.Queue <E>

«Giao diện»
java.util.concurrent.BlockingQueue <E>

+ put (phần tử: E): void Chèn một phần tử vào đuôi của hàng đợi.
Chờ nếu hàng đợi đã đầy.

+ lấy (): E Truy xuất và loại bỏ phần đầu của cái này

xếp hàng. Chờ nếu hàng đợi trống.

HÌNH 30.20 BlockingQueue là một giao diện con của Queue.


Machine Translated by Google

30.11 Chặn hàng đợi 1123

Ba hàng đợi chặn cụ thể — ArrayBlockingQueue, LinkedBlockingQueue và PriorityBlockingQueue — được


cung cấp trong Java, như trong Hình 30.21. Tất cả đều nằm trong gói java.util.concurrent .
ArrayBlockingQueue triển khai hàng đợi nhập khối bằng cách sử dụng một mảng. Bạn phải chỉ định một
dung lượng hoặc một công bằng tùy chọn để cấu trúc một ArrayBlockingQueue. LinkedBlockingQueue triển
khai hàng đợi chặn bằng danh sách được liên kết. Bạn có thể tạo LinkedBlockingQueue không bị ràng
buộc hoặc bị ràng buộc.
PriorityBlockingQueue là một hàng đợi ưu tiên. Bạn có thể tạo hàng đợi ưu tiên không giới hạn hoặc
không giới hạn.

«Giao diện»
java.util.concurrent.BlockingQueue <E>

ArrayBlockingQueue <E> LinkedBlockingQueue <E> PriorityBlockingQueue <E>

+ ArrayBlockingQueue (dung lượng: int) + LinkedBlockingQueue () + PriorityBlockingQueue ()

+ ArrayBlockingQueue (dung lượng: int, + LinkedBlockingQueue (dung lượng: int) + PriorityBlockingQueue (dung lượng: int)
công bằng: boolean)

HÌNH 30.21 ArrayBlockingQueue, LinkedBlockingQueue và PriorityBlockingQueue là các hàng đợi chặn cụ thể.

Ghi chú

Phương thức put sẽ không bao giờ chặn một LinkedBlockingQueue hoặc hàng đợi không giới hạn

PriorityBlockingQueue không bị ràng buộc.

Liệt kê 30.8 đưa ra một ví dụ về việc sử dụng ArrayBlockingQueue để đơn giản hóa ví dụ
Người tiêu dùng / Nhà sản xuất trong Liệt kê 30.10. Dòng 5 tạo ArrayBlockingQueue để lưu
trữ các số nguyên. Luồng Producer đặt một số nguyên vào hàng đợi (dòng 22) và luồng Người
tiêu dùng lấy một số nguyên từ hàng đợi (dòng 38).

LISTING 30.8 Người tiêu dùngSản xuấtUsingBlockingQueue.java


1 nhập java.util.concurrent. *;
2
3 hạng công khai Người tiêu dùngSản xuấtUsingBlockingQueue {
4 private static ArrayBlockingQueue <Integer> buffer =
mới ArrayBlockingQueue <> (2); tạo vùng đệm
5 6

public static void main (String [] args) {


// Tạo một nhóm luồng với hai luồng
Người thực thi ExecutorService = Executor.newFixedThreadPool (2); tạo hai chủ đề

7 executive.execute (new ProducerTask ());


8 9 10 11 executive.execute (new ConsumerTask ());
12 executive.shutdown ();
13 }
14
15 // Một nhiệm vụ để thêm một int vào bộ đệm
16 Riêng văn phòng phẩm ProducerTask triển khai Runnable { nhiệm vụ nhà sản xuất

17 public void run () {


18 thử {
19 int i = 1;
20 trong khi (đúng) {
21 System.out.println ("Nhà sản xuất viết" + i);
22 buffer.put (i ++); // Thêm bất kỳ giá trị nào vào vùng đệm, giả sử, 1 đặt
23 // Đặt luồng vào chế độ ngủ
Machine Translated by Google

1124 Chương 30 Lập trình đa luồng và song song

24 Thread.sleep ((int) (Math.random () * 10000));


25 }
26
27 } catch (InterruptException ex) {
28 ex.printStackTrace ();
29 }
30 }
31 }
32

33 // Một tác vụ để đọc và xóa một int khỏi bộ đệm


nhiệm vụ của người tiêu dùng 34 lớp tĩnh riêng ConsumerTask triển khai Runnable {
35 public void run () {
36 thử {
37 trong khi (đúng) {
lấy 38 System.out.println ("\ t \ t \ tConsumer đọc" + buffer.take ());
39 // Đặt luồng vào chế độ ngủ
40 Thread.sleep ((int) (Math.random () * 10000));
41 }
42
43 } catch (InterruptException ex) {
44 ex.printStackTrace ();
45 }
46 }
47 }
48}

Trong Liệt kê 30.7, bạn đã sử dụng các khóa và điều kiện để đồng bộ hóa chuỗi Nhà sản xuất và Người tiêu dùng.

Chương trình này không sử dụng khóa và điều kiện, vì đồng bộ hóa đã được triển khai trong ArrayBlockingQueue.

30.28 Hàng đợi chặn là gì? Những hàng đợi chặn nào được hỗ trợ trong Java?
Kiểm tra điểm
30.29 Bạn sử dụng phương pháp nào để thêm phần tử vào ArrayBlockingQueue? Điều gì xảy ra nếu hàng đợi đầy?

30.30 Bạn sử dụng phương pháp nào để lấy một phần tử từ ArrayBlockingQueue?
Điều gì xảy ra nếu hàng đợi trống?

30.12 Semaphores
Chìa khóa
Semaphores có thể được sử dụng để hạn chế số lượng các luồng truy cập vào một tài nguyên được chia sẻ.
Điểm
semaphore Trong khoa học máy tính, semaphore là một đối tượng điều khiển việc truy cập vào một tài nguyên chung. Trước

khi truy cập tài nguyên, một luồng phải có giấy phép từ semaphore. Sau khi kết thúc với tài nguyên, luồng phải

trả lại giấy phép cho semaphore, như trong Hình 30.22.

Một chuỗi truy cập tài nguyên được chia sẻ.

Xin giấy phép từ semaphore. semaphore.acquire ();


Chờ nếu giấy phép không có sẵn.

Truy cập tài nguyên

Giải phóng giấy phép cho semaphore. semaphore.release ();

HÌNH 30.22 Một số luồng giới hạn có thể truy cập tài nguyên dùng chung được kiểm soát bởi semaphore.
Machine Translated by Google

30.12 Semaphores 1125

Để tạo một semaphore, bạn phải chỉ định số lượng giấy phép với một chính sách công bằng tùy chọn, như

thể hiện trong Hình 30.23. Một nhiệm vụ có được giấy phép bằng cách gọi phương thức get () của semaphore

và giải phóng giấy phép bằng cách gọi bản phát hành của semaphore ()

phương pháp. Sau khi có được giấy phép, tổng số giấy phép hiện có trong một semaphore sẽ giảm đi 1. Sau

khi giấy phép được cấp, tổng số giấy phép hiện có trong một semaphore sẽ tăng lên 1.

java.util.concurrent.Semaphore

+ Semaphore (numberOfPermits: int) Tạo một semaphore với số lượng giấy phép được chỉ định. Các
chính sách công bằng là sai lầm.

+ Semaphore (numberOfPermits: int, fair: Tạo một semaphore với số lượng giấy phép được chỉ định và
boolean) chính sách công bằng.

+ get (): void Xin giấy phép từ semaphore này. Nếu không có giấy phép là
có sẵn, chủ đề bị chặn cho đến khi có sẵn.

+ release (): void Phát hành giấy phép trở lại semaphore.

HÌNH 30.23 Lớp Semaphore chứa các phương thức để truy cập một semaphore.

Một semaphore chỉ với một giấy phép có thể được sử dụng để mô phỏng một khóa loại trừ lẫn nhau.

Liệt kê 30.9 sửa đổi lớp bên trong Tài khoản trong Liệt kê 30.9 bằng cách sử dụng semaphore để đảm bảo rằng

chỉ một luồng tại một thời điểm có thể truy cập phương thức ký gửi .

LISTING 30,9 Tài khoản mới Lớp bên trong


1 // Một lớp bên trong cho Tài khoản
2 tài khoản lớp tĩnh riêng tư {
3 // Tạo một semaphore
4 private static Semaphore semaphore = new Semaphore (1); tạo một semaphore
5 private int balance = 0;
6

public int getBalance () {


trả lại số dư;
}
7 8 9 10

11 tiền gửi vô hiệu công khai (số tiền int ) {


12 thử {
13 semaphore.acquire (); // Xin giấy phép xin giấy phép
14 int newBalance = số dư + số tiền;
15
16 // Độ trễ này được cố tình thêm vào để phóng đại
17 // vấn đề tham nhũng dữ liệu và làm cho nó dễ dàng nhìn thấy
18 Thread.sleep (5);
19
20 số dư = newBalance;
21 }
22 bắt (Ngoại lệ bị gián đoạn) {
23 }
24 cuối cùng {
25 semaphore.release (); // Phát hành giấy phép phát hành giấy phép
26 }
27 }
28}

Một semaphore với một giấy phép được tạo ở dòng 4. Một luồng đầu tiên nhận được giấy phép khi thực thi

phương thức ký gửi ở dòng 13. Sau khi số dư được cập nhật, luồng sẽ giải phóng giấy phép ở dòng 25. Một

phương pháp hay là luôn đặt phương thức release () trong mệnh đề cuối cùng để đảm bảo rằng giấy phép cuối

cùng cũng được phát hành ngay cả trong trường hợp ngoại lệ.
Machine Translated by Google

1126 Chương 30 Lập trình đa luồng và song song

30.31 Điểm giống và khác nhau giữa khóa và semaphore là gì?

30.32 Làm cách nào để bạn tạo một semaphore cho phép ba luồng đồng thời? Làm thế nào để bạn có
được một semaphore? Làm thế nào để bạn phát hành một semaphore?

30.13 Tránh bế tắc


Có thể tránh được những bế tắc bằng cách sử dụng một thứ tự tài nguyên thích hợp.

Đôi khi hai hoặc nhiều luồng cần có được các khóa trên một số đối tượng được chia sẻ. Điều này có thể gây ra bế tắc,

trong đó mỗi luồng có khóa trên một trong các đối tượng và đang chờ khóa trên đối tượng kia. Hãy xem xét kịch bản với hai

luồng và hai đối tượng, như trong Hình 30.24. Luồng 1 đã có được một khóa trên object1 và Thread 2 đã có được một khóa

trên object2. Bây giờ Luồng 1 đang đợi khóa trên object2 và Luồng 2 cho khóa trên object1. Mỗi luồng chờ người kia giải

phóng khóa mà nó cần và cho đến khi điều đó xảy ra, cả hai luồng đều không thể tiếp tục chạy.

Bước Chủ đề 1 Chủ đề 2

1 đã đồng bộ hóa (object1) {


2 đã đồng bộ hóa (object2) {
3 // làm gì đó ở đây
4 // làm gì đó ở đây
5 đã đồng bộ hóa (object2) {
6 đã đồng bộ hóa (object1) {
// làm gì đó ở đây // làm gì đó ở đây }
}
} }

Chờ cho Chủ đề 2 đến Chờ Chủ đề 1 đến


nhả khóa trên object2 mở khóa đối tượng1

HÌNH 30.24 Luồng 1 và luồng 2 bị khóa.

Có thể dễ dàng tránh được bế tắc bằng cách sử dụng một kỹ thuật đơn giản được gọi là sắp xếp tài nguyên. Với kỹ thuật

này, bạn chỉ định một thứ tự cho tất cả các đối tượng có khóa phải được lấy và đảm bảo rằng mỗi luồng có được các khóa

theo thứ tự đó. Với ví dụ trong Hình 30.24, giả sử rằng các đối tượng được sắp xếp là object1 và object2. Sử dụng kỹ thuật

sắp xếp tài nguyên, Luồng 2 phải có được một khóa trên object1 trước, sau đó đến object2. Khi Luồng 1 có được một khóa

trên object1, Luồng 2 phải đợi một khóa trên object1. Do đó, Thread 1 sẽ có thể có được một khóa trên object2 và không có

deadlock nào xảy ra.

30,33 Bế tắc là gì? Làm thế nào bạn có thể tránh bế tắc?

30.14 Tiểu bang


Trạng thái luồng cho biết trạng thái của luồng.

Các tác vụ được thực hiện trong các luồng. Luồng có thể ở một trong năm trạng thái: Mới, Sẵn sàng, Đang chạy, Bị chặn hoặc

Hoàn thành (xem Hình 30.25).

Khi một luồng mới được tạo, nó sẽ chuyển sang trạng thái Mới. Sau khi một luồng được bắt đầu bằng cách gọi phương

thức start () của nó, nó sẽ chuyển sang trạng thái Sẵn sàng. Một luồng sẵn sàng có thể chạy được nhưng có thể chưa chạy

được ning. Hệ điều hành phải phân bổ thời gian CPU cho nó.

Khi một luồng sẵn sàng bắt đầu thực thi, nó sẽ chuyển sang trạng thái Đang chạy. Một chuỗi đang chạy có thể

vào trạng thái Sẵn sàng nếu thời gian CPU nhất định của nó hết hạn hoặc phương thức output () của nó được gọi.
Machine Translated by Google

30.15 Bộ sưu tập được đồng bộ hóa 1127

Năng suất (), hoặc Đang chạy


hết giờ
run () đã hoàn thành

Chủ đề được tạo khởi đầu()


chạy()
Mới Sẵn sàng Hoàn thành

tham gia() đợi () ngủ ()

Mục tiêu
hoàn thành

Chờ mục tiêu Đợi thời gian Chờ đợi để được

kêt thuc ngoài thông báo

Hết giờ Đã báo hiệu

Bị chặn

HÌNH 30.25 Một luồng có thể ở một trong năm trạng thái: Mới, Sẵn sàng, Đang chạy, Bị chặn hoặc Hoàn tất.

Một luồng có thể đi vào trạng thái Bị chặn (tức là không hoạt động) vì một số lý do. Nó có thể đã gọi phương

thức join (), sleep () hoặc wait () . Nó có thể đang đợi hoạt động I / O kết thúc. Một chuỗi bị chặn có thể được

kích hoạt lại khi hành động hủy kích hoạt nó được đảo ngược. Ví dụ: nếu một tiểu trình đã được chuyển sang trạng

thái ngủ và thời gian ngủ đã hết hạn, tiểu trình đó sẽ được xác nhận lại và chuyển sang trạng thái Sẵn sàng.

Cuối cùng, một luồng được hoàn thành nếu nó hoàn thành việc thực thi phương thức run () của nó .

Phương thức isAlive () được sử dụng để tìm ra trạng thái của một luồng. Nó trả về true nếu một luồng ở trạng

thái Sẵn sàng, Bị chặn hoặc Đang chạy; nó trả về false nếu một luồng mới và chưa bắt đầu hoặc nếu nó đã kết thúc.

Phương thức ngắt () ngắt một luồng theo cách sau: Nếu một luồng hiện đang ở trạng thái Sẵn sàng hoặc Đang

chạy, cờ ngắt của nó được thiết lập; nếu một luồng hiện đang bị chặn, nó sẽ được đánh thức và chuyển sang trạng

thái Sẵn sàng, và một ngoại lệ java.lang.InterruptException được ném ra.

30.34 Trạng thái luồng là gì? Mô tả các trạng thái cho một chủ đề.

30.15 Bộ sưu tập được đồng bộ hóa


Java Collections Framework cung cấp các bộ sưu tập được đồng bộ hóa cho danh sách, tập hợp và bản đồ.

Các lớp trong Java Collections Framework không an toàn theo luồng; nghĩa là, nội dung của chúng có thể bị hỏng nếu

chúng được nhiều luồng truy cập và cập nhật đồng thời. Bạn có thể bảo vệ dữ liệu trong bộ sưu tập bằng cách khóa

bộ sưu tập hoặc bằng cách sử dụng bộ sưu tập được đồng bộ hóa.

Lớp Collections cung cấp sáu phương thức tĩnh để gói một tập hợp thành một phiên bản đồng bộ hóa, như thể

hiện trong Hình 30.26. Các tập hợp được tạo bằng cách sử dụng các phương pháp này được gọi là trình bao bọc đồng

bộ hóa.

java.util.Collections

+ syncCollection (c: Bộ sưu tập): Bộ sưu tập Trả về một bộ sưu tập được đồng bộ hóa.

+ syncList (list: Danh sách): Danh sách Trả về danh sách được đồng bộ hóa từ danh sách được chỉ định.

+ syncMap (m: Bản đồ): Bản đồ Trả về một bản đồ được đồng bộ hóa từ bản đồ đã chỉ định.

+ syncSet (s: Set): Đặt Trả về một tập hợp được đồng bộ hóa từ tập hợp đã chỉ định.

+ syncSortedMap (s: SortedMap): SortedMap Trả về một bản đồ được sắp xếp đã đồng bộ hóa từ bản đồ đã chỉ định
bản đồ đã sắp xếp.

+ syncSortedSet (s: SortedSet): SortedSet Trả về một tập hợp được sắp xếp đã đồng bộ hóa.

HÌNH 30.26 Bạn có thể lấy các bộ sưu tập được đồng bộ hóa bằng cách sử dụng các phương thức trong lớp Bộ sưu tập .
Machine Translated by Google

1128 Chương 30 Lập trình đa luồng và song song

Gọi Bộ sưu tập được đồng bộ hóa (Bộ sưu tập c) trả về Bộ sưu tập mới

đối tượng, trong đó tất cả các phương thức truy cập và cập nhật bộ sưu tập gốc c được đồng bộ hóa.

Các phương pháp này được thực hiện bằng cách sử dụng từ khóa được đồng bộ hóa . Ví dụ, phương thức

thêm được triển khai như thế này:

public boolean add (E o) {


sync (this) { return
c.add (o);
}
}

Các bộ sưu tập được đồng bộ hóa có thể được nhiều luồng đồng thời truy cập và sửa đổi một cách an toàn.

Ghi chú

Trong java.util.Vector,
phương pháp java.util.Stack và java.util.Hashtable đã được đồng
bộ hóa. Đây là các lớp cũ được giới thiệu trong JDK 1.0. Bắt đầu với JDK
1.5, bạn nên sử dụng java.util.ArrayList để thay thế Vector,
java.util.LinkedList để thay thế Stack và java.util.Map để thay thế Hashtable.
Nếu cần đồng bộ hóa, hãy sử dụng trình bao bọc đồng bộ hóa.

thất bại nhanh chóng


Các lớp của trình bao bọc đồng bộ hóa là an toàn cho luồng, nhưng trình vòng lặp không nhanh. Điều

này có nghĩa là nếu bạn đang sử dụng một trình vòng lặp để duyệt qua một tập hợp trong khi tập hợp bên

dưới đang được sửa đổi bởi một luồng khác, thì trình lặp sẽ ngay lập tức bị lỗi khi ném java.util.

ConcurrentModificationException, là một lớp con của RuntimeException. Để tránh lỗi này,


bạn cần tạo một đối tượng bộ sưu tập đồng bộ và có được một khóa trên đối tượng khi đi
ngang qua nó. Ví dụ, để duyệt qua một tập hợp, bạn phải viết mã như sau:

Đặt hashSet = Collections.synchronizedSet (new HashSet ());

đã đồng bộ hóa (hashSet) { // Phải đồng bộ hóa nó


Trình lặp lặp lại lặp lại = hashSet.iterator ();

while (iterator.hasNext ()) {


System.out.println (iterator.next ());
}
}

Không làm như vậy có thể dẫn đến hành vi không xác định, chẳng hạn như ConcurrentModificationException.

30.35 Tập hợp đồng bộ là gì? ArrayList có được đồng bộ hóa không? Bạn làm thế nào

Kiểm tra điểm nó được đồng bộ hóa?

30.36 Giải thích tại sao một trình lặp không nhanh.

30.16 Lập trình song song


Khung Fork / Join được sử dụng để lập trình song song trong Java.
Chìa khóa

Điểm
Việc sử dụng rộng rãi các hệ thống đa lõi đã tạo ra một cuộc cách mạng về phần mềm. Để được hưởng lợi
Tính năng JDK 7 từ nhiều bộ xử lý, phần mềm cần chạy song song. JDK 7 giới thiệu Khung công tác Fork / Tham gia mới để

lập trình song song, sử dụng các bộ xử lý đa lõi.


Khung Fork / Tham gia Khung Fork / Join được minh họa trong Hình 30.27 (sơ đồ giống như một ngã ba, do đó có tên gọi của

nó). Một bài toán được chia thành các bài toán con không trùng lặp, có thể được giải quyết song song

một cách dễ hiểu. Các giải pháp cho tất cả các bài toán con sau đó được kết hợp để có được một giải

pháp tổng thể cho vấn đề. Đây là cách thực hiện song song của phương pháp chia để trị. Trong Khung công

tác Fork / Join của JDK 7, một fork có thể được xem như một nhiệm vụ độc lập chạy trên một luồng.
Machine Translated by Google

30.16 Lập trình song song 1129

Cái nĩa Vấn đề con Tham gia

Vấn đề con
Vấn đề Giải pháp

Vấn đề con

Vấn đề con

HÌNH 30.27 Các bài toán con không trùng lặp được giải song song.

Khuôn khổ xác định một tác vụ bằng cách sử dụng lớp ForkJoinTask , như thể hiện trong Hình 30.28 ForkJoinTask
và thực thi một tác vụ trong một phiên bản của ForkJoinPool, như trong Hình 30.29. ForkJoinPool

«Giao diện»

java.util.concurrent.Future <V>

+ hủy (ngắt: boolean): boolean + get (): V Cố gắng hủy bỏ nhiệm vụ này.
Chờ nếu cần để tính toán hoàn tất và
trả về kết quả.

+ isDone (): boolean Trả về true nếu nhiệm vụ này được hoàn thành.

java.util.concurrent.ForkJoinTask <V>

+ thích ứng (tác vụ có thể chạy được): ForkJoinTask <V> Trả về ForkJoinTask từ một tác vụ có thể chạy được.

+ fork (): ForkJoinTask <V> + join (): V + invoke (): V Sắp xếp việc thực thi tác vụ không đồng bộ.
Trả về kết quả của các phép tính khi nó được thực hiện.
Thực hiện nhiệm vụ và chờ hoàn thành và trả về
kết quả.

+ invokeAll (task ForkJoinTask <?>…): void Forks các nhiệm vụ đã cho và trả về khi tất cả các nhiệm vụ được hoàn thành.

java.util.concurrent.RecursiveAction <V>

#compute (): void Xác định cách thực hiện nhiệm vụ.

java.util.concurrent.RecursiveTask <V>

#compute (): V Xác định cách thực hiện nhiệm vụ. Trả lại giá
trị sau khi nhiệm vụ hoàn thành.

HÌNH 30.28 Lớp ForkJoinTask định nghĩa một tác vụ để thực thi không đồng bộ.

«Giao diện»
Xem Hình 30.7
java.util.concurrent.ExecutorService

java.util.concurrent.ForkJoinPool

+ ForkJoinPool () Tạo ForkJoinPool với tất cả các bộ xử lý có sẵn.


+ ForkJoinPool (song song: int) + gọi Tạo ForkJoinPool với số lượng bộ xử lý được chỉ định.
(ForkJoinTask <T>): T Thực hiện nhiệm vụ và trả về kết quả của nó khi hoàn thành.

HÌNH 30.29 ForkJoinPool thực thi các tác vụ Fork / Join.


Machine Translated by Google

1130 Chương 30 Lập trình đa luồng và song song

ForkJoinTask là lớp cơ sở trừu tượng cho các tác vụ. ForkJoinTask là một chuỗi giống như

thực thể, nhưng nó nhẹ hơn nhiều so với một luồng bình thường vì một số lượng lớn các nhiệm vụ và tác vụ

con có thể được thực thi bởi một số lượng nhỏ các luồng thực tế trong ForkJoinPool. Các tác vụ chủ yếu được

phối hợp bằng cách sử dụng fork () và join (). Việc gọi fork () trên một tác vụ sắp xếp việc thực thi không

liên tục và việc gọi tham gia () đợi cho đến khi tác vụ hoàn thành. Lời gọi ()

và các phương thức invokeAll (task) ngầm gọi fork () để thực thi nhiệm vụ và join ()

để đợi các tác vụ hoàn thành và trả về kết quả, nếu có. Lưu ý rằng phương thức static invokeAll nhận một

số lượng biến đối số ForkJoinTask bằng cách sử dụng ... cú pháp,


được giới thiệu trong Phần 7.9.

Khung Fork / Join được thiết kế để song song hóa các giải pháp chia để trị, các giải pháp này có tính đệ
RecursiveAction quy một cách tự nhiên. RecursiveAction và RecursiveTask là hai lớp con của ForkJoinTask. Để xác định một lớp
RecursiveTask tác vụ cụ thể, lớp của bạn nên mở rộng RecursiveAction
hoặc RecursiveTask. RecursiveAction dành cho tác vụ không trả về giá trị và RecursiveTask dành cho tác vụ
trả về giá trị. Lớp tác vụ của bạn nên ghi đè phương thức compute () để chỉ định cách một tác vụ được thực

hiện.

Bây giờ chúng tôi sử dụng một loại hợp nhất để chứng minh cách phát triển các chương trình song song bằng cách sử dụng Fork /

Tham gia Framework. Thuật toán sắp xếp hợp nhất (được giới thiệu trong Phần 25.3) chia mảng thành hai nửa

và áp dụng sắp xếp hợp nhất trên mỗi nửa một cách đệ quy. Sau khi hai nửa được sắp xếp, thuật toán sẽ hợp

nhất chúng. Liệt kê 30.10 đưa ra cách triển khai song song của thuật toán sắp xếp hợp nhất và so sánh thời

gian thực thi của nó với một kiểu sắp xếp tuần tự.

DANH SÁCH 30.10 ParallelMergeSort.java


1 nhập java.util.concurrent.RecursiveAction;
2 nhập java.util.concurrent.ForkJoinPool;
3
4 lớp công khai ParallelMergeSort {
public static void main (String [] args) {
5 cuối cùng int SIZE = 7000000;
6 7 int [] list1 = new int [SIZE];
int [] list2 = new int [SIZE];
8 9

10 for (int i = 0; i <list1.length; i ++)


11 list1 [i] = list2 [i] = (int) (Math.random () * 10000000);
12
13 long startTime = System.currentTimeMillis ();
gọi sắp xếp song song 14 song songMergeSort (list1); // Gọi sắp xếp hợp nhất song song
15 long endTime = System.currentTimeMillis ();
16 System.out.println ("\ nThời gian song song với"
17 + Runtime.getRuntime (). AvailableProcessors () +
"
18 bộ xử lý là " + (endTime - startTime) + "mili giây");
19
20 startTime = System.currentTimeMillis ();
gọi sắp xếp tuần tự 21 MergeSort.mergeSort (list2); // MergeSort có trong Liệt kê 23.5
22 endTime = System.currentTimeMillis ();
23 System.out.println ("\ nThời gian tuần tự là" (endTime +
24 - startTime) + "mili giây");
25 }
26
27 public static void songMergeSort (int [] list) {
tạo một ForkJoinTask 28 RecursiveAction mainTask = new SortTask (danh sách);
tạo một ForkJoinPool 29 Hồ bơi ForkJoinPool = new ForkJoinPool ();
thực hiện một nhiệm vụ 30 pool.invoke (mainTask);
31 }
32
xác định cụ thể 33 lớp tĩnh riêng SortTask mở rộng RecursiveAction {
ForkJoinTask 34 private cuối cùng int THRESHOLD = 500;
Machine Translated by Google

30.16 Lập trình song song 1131

35 danh sách private int [] ;


36
37 SortTask (int [] list) {
38 this.list = danh sách;
39 }
40
41 @Ghi đè
42 void compute được bảo vệ () { thực hiện nhiệm vụ
43 if (list.length <THRESHOLD)
44 java.util.Arrays.sort (danh sách); sắp xếp một danh sách nhỏ

45 khác {
46 // Lấy nửa đầu
47 int [] firstHalf = new int [list.length / 2]; chia thành hai phần
48 System.arraycopy (list, 0, firstHalf, 0, list.length / 2);
49
50 // Lấy nửa thứ hai
51 int secondHalfLength = list.length - list.length / 2;
52 int [] secondHalf = new int [secondHalfLength];
53 System.arraycopy (list, list.length / 2,
54 secondHalf, 0, secondHalfLength);
55

56 // Sắp xếp đệ quy hai nửa


57 invokeAll ( SortTask mới (firstHalf), giải quyết từng phần

58 new SortTask (secondHalf));


59
60 // Hợp nhất firstHalf với secondHalf thành danh sách
61 MergeSort.merge (firstHalf, secondHalf, list); hợp nhất hai phần
62 }
63 }
64 }
65}

Thời gian song song với 2 vi xử lý là 2829 mili giây


Thời gian tuần tự là 4751 mili giây

Vì thuật toán sắp xếp không trả về giá trị, chúng tôi xác định một lớp ForkJoinTask cụ thể bằng cách
mở rộng Hành động đệ quy (dòng 33–64). Phương thức tính toán bị ghi đè để áp dụng một loại hợp nhất
đệ quy (dòng 42–63). Nếu danh sách nhỏ, sẽ hiệu quả hơn nếu được giải quyết theo trình tự (dòng
44). Đối với một danh sách lớn, nó được chia thành hai nửa (dòng 47–54). Hai nửa được sắp xếp đồng
thời (dòng 57 và 58) và sau đó được hợp nhất (dòng 61).
Chương trình tạo một ForkJoinTask chính (dòng 28), một ForkJoinPool (dòng 29) và đặt tác vụ
chính để thực thi trong một ForkJoinPool (dòng 30). Phương thức gọi sẽ trở lại sau khi tác vụ chính
hoàn thành.
Khi thực hiện nhiệm vụ chính, nhiệm vụ được chia thành các nhiệm vụ con và các nhiệm vụ con được
gọi bằng phương thức invokeAll (dòng 57 và 58). Phương thức invokeAll sẽ trả về sau khi hoàn thành
tất cả các nhiệm vụ con. Lưu ý rằng mỗi nhiệm vụ con được chia thành các nhiệm vụ nhỏ hơn lặp lại
một cách cẩn thận. Một số lượng lớn các nhiệm vụ con có thể được tạo và thực hiện trong nhóm. Khung
Fork / Join tự động thực thi và điều phối tất cả các tác vụ một cách hiệu quả.
Lớp MergeSort được định nghĩa trong Liệt kê 23.5. Chương trình gọi MergeSort.merge
để hợp nhất hai danh sách con đã sắp xếp (dòng 61). Chương trình cũng gọi MergeSort.mergeSort
(dòng 21) để sắp xếp danh sách bằng cách sử dụng sắp xếp hợp nhất một cách tuần tự. Bạn có thể thấy rằng sắp xếp song song

nhanh hơn nhiều so với sắp xếp tuần tự.

Lưu ý rằng vòng lặp để khởi tạo danh sách cũng có thể được song song hóa. Tuy nhiên, bạn nên
tránh sử dụng Math.random () trong mã vì nó được đồng bộ hóa và không thể thực thi song song (xem
Bài tập lập trình 30.12). Phương thức song songMergeSort chỉ sắp xếp
Machine Translated by Google

1132 Chương 30 Lập trình đa luồng và song song

một mảng các giá trị int , nhưng bạn có thể sửa đổi nó để trở thành một phương thức chung (xem Bài
tập lập trình 30.13).

Nói chung, một vấn đề có thể được giải quyết song song bằng cách sử dụng mẫu sau:

nếu (chương trình nhỏ)


giải quyết nó một cách tuần tự;
khác {
chia vấn đề thành các bài toán con không trùng lặp;
giải các bài toán con đồng thời;
kết hợp các kết quả từ các bài toán con để giải quyết toàn bộ vấn đề;
}

Liệt kê 30.11 phát triển một phương pháp song song để tìm số cực đại trong một danh sách.

DANH SÁCH 30.11 ParallelMax.java


1 nhập java.util.concurrent. *;
2
3 lớp công khai ParallelMax {
public static void main (String [] args) {
4 // Tạo danh sách
5 int cuối cùng N = 9000000;
6 int [] list = new int [N];
7 for (int i = 0; i <list.length; i ++)
8 list [i] = i;
9 10

11 long startTime = System.currentTimeMillis ();


gọi tối đa 12 System.out.println ("\ nSố cực đại là" + max (danh sách));
13 long endTime = System.currentTimeMillis ();
14 System.out.println ("Số lượng bộ xử lý là" +
15 Runtime.getRuntime (). AvailableProcessors ());
16 System.out.println ("Thời gian là" + (endTime
"mili -
giây");
startTime)
17 +
18 }
19
20 public static int max (int [] list) {
tạo một ForkJoinTask 21 RecursiveTask <Integer> task = new MaxTask (list, 0, list.length);
tạo một ForkJoinPool 22 Hồ bơi ForkJoinPool = new ForkJoinPool ();
thực hiện một nhiệm vụ 23 return pool.invoke (tác vụ);
24 }
25
xác định cụ thể 26 lớp tĩnh riêng MaxTask mở rộng RecursiveTask <Integer> {
ForkJoinTask 27 private cuối cùng static int THRESHOLD = 1000;
28 danh sách private int [] ;
29 int thấp;
30 int cao;
31
32 public MaxTask (int [] list, int low, int high) {
33 this.list = danh sách;
34 this.low = thấp;
35 this.high = cao;
36 }
37

38 @Ghi đè
thực hiện nhiệm vụ 39 public Integer compute () {
40 if (high - low <THRESHOLD) {
41 int max = list [0];
giải quyết một vấn đề nhỏ 42 for (int i = low; i <high; i ++) if (list
43 [i]> max)
44 max = list [i];
45 trả về số nguyên mới (max);
Machine Translated by Google

Tóm tắt chương 1133

46
47 } khác {
48 int mid = (thấp + cao) / 2;
49 RecursiveTask <Integer> left = new MaxTask (danh sách, thấp, giữa); chia thành hai phần
50 RecursiveTask <Integer> right = new MaxTask (danh sách, trung bình, cao);
51
52 right.fork (); ngã ba phải
53 left.fork (); ngã ba trái

54 trả về số nguyên mới (Math.max (left.join (). intValue (), tham gia các nhiệm vụ

55 right.join (). intValue ()));


56 }
57 }
58 }
59}

Số cực đại là 8999999

Số lượng bộ xử lý là 2
Thời gian là 44 mili giây

Vì thuật toán trả về một số nguyên, chúng tôi xác định một lớp tác vụ cho phép nối rẽ nhánh bằng cách mở
rộng RecursiveTask <Integer> (dòng 26–58). Phương thức tính toán bị ghi đè để trả về phần tử tối đa

trong danh sách [low..high] (dòng 39–57). Nếu danh sách nhỏ, sẽ hiệu quả hơn nếu được giải theo tuần tự

(dòng 40–46). Đối với một danh sách lớn, nó được chia thành hai nửa (dòng 48–50).

Các nhiệm vụ trái và phải tìm phần tử cực đại ở nửa trái và nửa phải, tương ứng. Gọi fork () trên tác

vụ khiến tác vụ được thực thi (dòng 52 và 53). Phương thức join () chờ tác vụ hoàn thành và sau đó trả

về kết quả (dòng 54 và 55).

30.37 Bạn định nghĩa ForkJoinTask như thế nào? Sự khác biệt giữa
RecursiveAction và RecursiveTask? Kiểm tra điểm

30.38 Bạn yêu cầu hệ thống thực thi một tác vụ như thế nào?

30.39 Bạn có thể sử dụng phương pháp nào để kiểm tra xem một nhiệm vụ đã được hoàn thành chưa?

30.40 Bạn tạo ForkJoinPool như thế nào? Làm cách nào để bạn đặt một nhiệm vụ vào ForkJoinPool?

ĐIỀU KHOẢN CHÍNH

điều kiện 1114 đa luồng 1098


bế tắc 1126 điều kiện chủng tộc 1111

không nhanh 1128 semaphore 1124

chính sách công bằng 1113 trình bao bọc đồng bộ hóa 1127
Khung Fork / Tham gia 1128 đồng bộ khối 1112
khóa 1112 chủ đề 1098

màn hình 1118 an toàn luồng 1111

TÓM TẮT CHƯƠNG

1. Mỗi tác vụ là một thể hiện của giao diện Runnable . Một luồng là một đối tượng tạo điều kiện thuận

lợi cho việc thực hiện một tác vụ. Bạn có thể xác định một lớp tác vụ bằng cách triển khai Runnable

giao diện và tạo một luồng bằng cách gói một tác vụ bằng phương thức khởi tạo Luồng .

2. Sau khi một đối tượng luồng được tạo, hãy sử dụng phương thức start () để bắt đầu một luồng
và phương thức sleep (long) để đặt một luồng ở chế độ ngủ để các luồng khác có cơ hội chạy.
Machine Translated by Google

1134 Chương 30 Lập trình đa luồng và song song

3. Một đối tượng luồng không bao giờ gọi trực tiếp phương thức run . JVM gọi chạy
khi đến lúc thực thi luồng. Lớp của bạn phải ghi đè phương thức run để cho hệ thống biết luồng

sẽ làm gì khi nó chạy.

4. Để ngăn các luồng làm hỏng tài nguyên được chia sẻ, hãy sử dụng các phương pháp hoặc khối được đồng

bộ hóa. Một phương thức được đồng bộ hóa có được một khóa trước khi nó thực thi. Trong trường

hợp của một phương thức thể hiện, khóa nằm trên đối tượng mà phương thức được gọi. Trong trường
hợp của một phương thức tĩnh, khóa nằm trên lớp.

5. Một câu lệnh được đồng bộ hóa có thể được sử dụng để có được một khóa trên bất kỳ đối tượng nào, không chỉ điều này

khi thực thi một khối mã trong một phương thức. Khối này được gọi là khối được đồng bộ hóa.

6. Bạn có thể sử dụng các khóa và điều kiện rõ ràng để tạo điều kiện giao tiếp giữa các luồng,
cũng như sử dụng màn hình tích hợp cho các đối tượng.

7. Các hàng đợi chặn (ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue)


được cung cấp trong Java Collections Framework tự động đồng bộ hóa quyền truy
cập vào một hàng đợi.

8. Bạn có thể sử dụng semaphores để hạn chế số lượng tác vụ đồng thời truy cập vào một
nguồn.

9. Deadlock xảy ra khi hai hoặc nhiều luồng có được các khóa trên nhiều đối tượng và mỗi
luồng có một khóa trên một đối tượng và đang chờ khóa trên đối tượng kia. Nguồn tài nguyên
kỹ thuật đặt hàng có thể được sử dụng để tránh bế tắc.

10. Khung Fork / Join của JDK 7 được thiết kế để phát triển các chương trình song song. Bạn
có thể xác định một lớp tác vụ mở rộng RecursiveAction hoặc RecursiveTask và exe dễ dàng thực

hiện các tác vụ đồng thời trong ForkJoinPool và nhận được giải pháp tổng thể sau khi tất cả các

tác vụ được hoàn thành.

ĐỐ

Trả lời câu hỏi cho chương này trực tuyến tại www.cs.armstrong.edu/liang/intro10e/quiz.html.

BÀI TẬP LẬP TRÌNH

Phần 30.1–30.5
* 30.1 (Sửa lại Liệt kê 30.1) Viết lại Liệt kê 30.1 để hiển thị kết quả đầu ra trong một vùng văn bản,

như thể hiện trong Hình 30.30.

HÌNH 30.30 Đầu ra từ ba luồng được hiển thị trong một vùng văn bản.
Machine Translated by Google

Bài tập lập trình 1135

30.2 (Đua ô tô) Bài tập lập trình viết lại 15.29 sử dụng một luồng để điều khiển đua ô tô. So sánh

chương trình với Bài tập lập trình 15.29 bằng cách đặt thời gian trễ là 10 trong cả hai

chương trình. Cái nào chạy hoạt ảnh nhanh hơn?

30.3 (Giương cờ) Viết lại Liệt kê 15.13 bằng cách sử dụng một chuỗi để tạo hoạt ảnh cho một lá cờ

đang được kéo lên. So sánh chương trình với Liệt kê 15.13 bằng cách đặt thời gian trễ

thành 10 trong cả hai chương trình. Cái nào chạy hoạt ảnh nhanh hơn?

Phần 30.8–30.12
30.4 (Đồng bộ hóa luồng) Viết chương trình khởi chạy 1.000 luồng. Mỗi luồng thêm 1 vào một tổng

biến ban đầu là 0. Xác định một đối tượng trình bao bọc Số nguyên để giữ tổng. Chạy

chương trình có và không có đồng bộ hóa để xem tác dụng của nó.

30.5 (Hiển thị quạt đang chạy) Viết lại bài tập lập trình 15.28 bằng cách sử dụng một sợi để
điều khiển hoạt ảnh của quạt.

30.6 (Bóng nảy) Viết lại Liệt kê 15.17 BallPane.java bằng cách sử dụng một chủ đề để tạo hoạt ảnh

cho các chuyển động của bóng nảy.

30.7 (Điều khiển đồng hồ) Bài tập lập trình viết lại 15.32 bằng cách sử dụng một luồng để điều khiển
hoạt ảnh của đồng hồ.

30.8 (Đồng bộ hóa tài khoản) Viết lại Liệt kê 30.6, ThreadCooperation.java, sử dụng các phương

thức wait () và InformAll () của đối tượng .

30.9 (Chứng minh ConcurrentModificationException) Trình lặp không nhanh. Viết một chương trình

để chứng minh điều đó bằng cách tạo hai luồng truy cập đồng thời và sửa đổi một tập hợp.

Luồng đầu tiên tạo một tập hợp băm chứa đầy các số và thêm một số mới vào tập hợp mỗi

giây. Luồng thứ hai nhận được một trình vòng lặp cho tập hợp và duyệt qua tập hợp đó qua

lại trình vòng lặp mỗi giây. Bạn sẽ nhận được ConcurrentModificationException vì tập hợp
cơ bản đang được sửa đổi trong luồng đầu tiên trong khi tập hợp trong luồng thứ hai đang

được duyệt.

* 30.10 (Sử dụng các bộ được đồng bộ hóa) Sử dụng đồng bộ hóa, khắc phục sự
cố trong bài tập trước để luồng thứ hai không ném ra một
ConcurrentModificationException.

Mục 30.15
* 30.11 (Biểu thị bế tắc) Viết chương trình biểu thị bế tắc.

Mục 30.18
* 30.12 (Bộ khởi tạo mảng song song) Triển khai phương pháp sau bằng Fork /

Tham gia Framework để gán giá trị ngẫu nhiên cho danh sách.

public static voidllelAssignValues (double [] list)

Viết một chương trình thử nghiệm tạo một danh sách với 9.000.000 phần tử và gọi song

songAssignValues để gán các giá trị ngẫu nhiên cho danh sách. Cũng thực hiện một thuật

toán tuần tự và so sánh thời gian thực hiện của cả hai. Lưu ý rằng nếu bạn sử dụng

Math.random (), thời gian thực thi mã song song của bạn sẽ kém hơn thời gian thực thi mã

tuần tự vì Math.random () được đồng bộ hóa và không thể thực thi song song. Để khắc phục

sự cố này, hãy tạo một đối tượng Ngẫu nhiên để gán các giá trị ngẫu nhiên cho một danh

sách nhỏ.
Machine Translated by Google

1136 Chương 30 Lập trình đa luồng và song song

30.13 (Sắp xếp hợp nhất song song chung) Sửa lại Liệt kê 30.10, ParallelMergeSort.java, để
xác định phương thức song song chung chung như sau:

public static <E expand Khoảng trống có thể so sánh được <E>>
song songMergeSort (E [] danh sách)

* 30.14 (Sắp xếp nhanh song song) Thực hiện song song phương pháp sau để sắp xếp danh sách bằng cách

sử dụng sắp xếp nhanh (xem Liệt kê 23.7).

public static voidllelQuickSort (int [] list)

Viết một chương trình kiểm tra nhân thời gian thực hiện cho một danh sách có kích thước 9.000.000

bằng cách sử dụng phương pháp song song và phương pháp tuần tự này.

* 30.15 (Tổng song song) Thực hiện phương pháp sau bằng Fork / Join để tìm
tổng của một danh sách.

công khai tĩnh kép song song (danh sách kép [] )

Viết chương trình kiểm tra tìm tổng trong danh sách 9.000.000 giá trị kép.

* 30.16 (Phép cộng ma trận song song) Bài tập lập trình 8.5 mô tả cách thực hiện phép cộng ma trận.

Giả sử bạn có nhiều bộ xử lý, vì vậy bạn có thể tăng tốc độ cộng ma trận. Thực hiện song

song phương pháp sau.

public static double [] [] llelAddMatrix ( double [] []


a, double [] [] b)

Viết chương trình thử nghiệm đo thời gian thực hiện phép cộng hai ma trận 2.000 *
2.000 tương ứng bằng phương pháp song song và phương pháp tuần tự.

* 30.17 (Phép nhân ma trận song song) Bài tập lập trình 7.6 mô tả cách thực hiện phép nhân ma trận.

Giả sử bạn có nhiều bộ xử lý, vì vậy bạn có thể tăng tốc độ nhân ma trận. Thực hiện song

song phương pháp sau.

public static double [] [] song songMultiplyMatrix (


nhân đôi [] [] a, nhân đôi [] [] b)

Viết chương trình thử nghiệm đo thời gian thực hiện phép nhân hai ma trận 2.000 *
2.000 theo phương pháp song song và phương pháp tuần tự, tương ứng.

* 30.18 (Tám ô vuông song song) Sửa đổi danh sách 22.11, EightQueens.java, để phát triển một thuật toán

song song tìm tất cả các giải pháp cho bài toán Tám ô vuông. (Dấu:

Khởi chạy tám nhiệm vụ phụ, mỗi nhiệm vụ đặt quân hậu vào một cột khác nhau trong
hàng đầu tiên.)

Toàn diện
*** 30.19 (Sắp xếp hoạt ảnh) Viết hoạt ảnh để sắp xếp lựa chọn, sắp xếp chèn và sắp xếp bong bóng,
như thể hiện trong Hình 30.31. Tạo một mảng các số nguyên 1, 2 ,. . .,

50. Xáo trộn nó một cách ngẫu nhiên. Tạo ngăn để hiển thị mảng trong biểu đồ. Bạn
nên gọi từng phương thức sắp xếp trong một chuỗi riêng biệt. Mỗi thuật toán sử
dụng hai vòng lặp lồng nhau. Khi thuật toán hoàn thành một lần lặp trong vòng lặp
ngoài, hãy đặt luồng ở trạng thái ngủ trong 0,5 giây và hiển thị lại mảng trong biểu đồ.
Tô màu cho thanh cuối cùng trong mảng con đã sắp xếp.
Machine Translated by Google

Bài tập lập trình 1137

HÌNH 30.31 Ba thuật toán sắp xếp được minh họa trong hình ảnh động.

*** 30.20 (Hoạt hình tìm kiếm Sudoku) Sửa đổi Bài tập lập trình 22.21 để hiển thị các kết quả
trung gian của tìm kiếm. Hình 30.32 cung cấp một ảnh chụp nhanh của một hoạt
ảnh đang diễn ra với số 2 được đặt trong ô trong Hình 30.32a, số 3 được đặt
trong ô trong Hình 30.32b và số 3 được đặt trong ô trong Hình 30.32c.
Hoạt ảnh hiển thị tất cả các bước tìm kiếm.

(Một) (b) (C)

HÌNH 30.32 Các bước tìm kiếm trung gian được hiển thị trong hình ảnh động cho bài toán Sudoku.
Machine Translated by Google

1138 Chương 30 Lập trình đa luồng và song song

30.21 (Kết hợp các quả bóng nảy va chạm) Viết lại bài tập lập trình 20.5 bằng cách sử dụng một sợi

để tạo hoạt ảnh cho các chuyển động của quả bóng nảy.

*** 30.22 (hoạt ảnh Eight Queens) Sửa đổi Liệt kê 22.11, EightQueens.java, để hiển thị các kết quả trung

gian của tìm kiếm. Như trong Hình 30.33, hàng hiện tại đang được tìm kiếm được tô

sáng. Cứ sau một giây, một trạng thái mới của bàn cờ được hiển thị.

HÌNH 30.33 Các bước tìm kiếm trung gian được hiển thị trong hình ảnh động cho bài toán Eight Queens.
Machine Translated by Google

CHƯƠNG

31
MẠNG

Mục tiêu
■ Giải thích các thuật ngữ: TCP, IP, tên miền, máy chủ tên miền, truyền
thông dựa trên luồng và truyền thông dựa trên gói (§31.2).

■ Để tạo máy chủ bằng cách sử dụng ổ cắm máy chủ (§31.2.1) và máy khách sử dụng máy khách

ổ cắm (§31.2.2).

■ Để triển khai các chương trình mạng Java bằng cách sử dụng các ổ cắm dòng (§31.2.3).

■ Để phát triển một ví dụ về ứng dụng máy khách / máy chủ (§31.2.4).

■ Để lấy địa chỉ Internet bằng lớp InetAddress (§31.3).

■ Để phát triển máy chủ cho nhiều máy khách (§31.4).

■ Gửi và nhận các đối tượng trên mạng (§31.5).

■ Để phát triển trò chơi tic-tac-toe tương tác được chơi trên Internet (§31.6).
Machine Translated by Google

1140, chương 31, kết nối mạng

31.1 Giới thiệu


Mạng máy tính được sử dụng để gửi và nhận tin nhắn giữa các máy tính trên Internet.
Chìa khóa

Điểm
Để duyệt Web hoặc gửi email, máy tính của bạn phải được kết nối với Internet. Internet là mạng toàn

cầu của hàng triệu máy tính. Máy tính của bạn có thể kết nối Internet thông qua Nhà cung cấp dịch vụ

Internet (ISP) bằng cách sử dụng quay số, DSL, hoặc modem cáp hoặc thông qua mạng cục bộ (LAN).

Khi một máy tính cần giao tiếp với một máy tính khác, nó cần biết địa chỉ của máy tính kia. Địa chỉ
địa chỉ IP Giao thức Internet (IP) xác định duy nhất máy tính trên Internet. Địa chỉ IP bao gồm bốn số thập phân
có dấu chấm từ 0 đến 255, chẳng hạn như 130.254.204.31. Vì không dễ nhớ nhiều con số như vậy, chúng

thường được ánh xạ tới các tên có ý nghĩa được gọi là tên miền, chẳng hạn như liang.armstrong.edu.
tên miền Máy chủ đặc biệt được gọi là Máy chủ tên miền (DNS) trên Internet dịch tên máy chủ thành địa chỉ IP.
máy chủ tên miền Khi một máy tính liên hệ với liang.armstrong.edu, trước tiên nó sẽ yêu cầu DNS dịch tên miền này thành

địa chỉ IP dạng số và sau đó gửi yêu cầu bằng địa chỉ IP.

Giao thức Internet là một giao thức cấp thấp để truyền dữ liệu từ máy tính này sang máy tính khác

trên Internet dưới dạng gói. Hai giao thức cấp cao hơn được sử dụng cùng với IP là Giao thức điều
TCP khiển truyền (TCP) và Giao thức sơ đồ người dùng (UDP).
UDP TCP cho phép hai máy chủ thiết lập kết nối và trao đổi luồng dữ liệu. TCP guaran có nhiệm vụ phân phối
dữ liệu và cũng đảm bảo rằng các gói sẽ được phân phối theo đúng thứ tự mà chúng đã được gửi đi. UDP
là một giao thức tiêu chuẩn, chi phí thấp, không kết nối, từ máy chủ đến máy chủ được sử dụng qua IP.

UDP cho phép một chương trình ứng dụng trên một máy tính gửi một gam dữ liệu đến một chương trình

ứng dụng trên một máy tính khác.


giao tiếp dựa trên luồng Java hỗ trợ cả truyền thông dựa trên luồng và dựa trên gói. Thông tin liên lạc dựa trên luồng sử

giao tiếp dựa trên gói dụng TCP để truyền dữ liệu, trong khi truyền thông dựa trên gói sử dụng UDP.
Vì TCP có thể phát hiện các đường truyền bị mất và gửi lại chúng, nên các đường truyền là không mất

dữ liệu và có thể tin cậy. Ngược lại, UDP không thể đảm bảo truyền không mất dữ liệu. Truyền thông dựa

trên luồng được sử dụng trong hầu hết các lĩnh vực lập trình Java và là trọng tâm của chương này.

Truyền thông dựa trên gói được giới thiệu trong Phần bổ sung III.P, Kết nối mạng bằng giao thức Datagram.

31.2 Máy khách / Máy chủ


Java cung cấp lớp ServerSocket để tạo ổ cắm máy chủ và Ổ cắm
Chìa khóa

Điểm lớp để tạo một ổ cắm máy khách. Hai chương trình trên Internet giao tiếp thông qua ổ cắm máy
chủ và ổ cắm máy khách sử dụng các luồng I / O.

Mạng được tích hợp chặt chẽ trong Java. API Java cung cấp các lớp để tạo sock ets nhằm tạo điều kiện
ổ cắm cho chương trình truyền thông qua Internet. Sockets là điểm cuối của kết nối logi cal giữa hai máy
chủ và có thể được sử dụng để gửi và nhận dữ liệu. Java xử lý các giao tiếp socket giống như xử lý

các hoạt động I / O; do đó, các chương trình có thể đọc từ hoặc ghi vào socket dễ dàng như chúng có

thể đọc từ hoặc ghi vào tệp.


Lập trình mạng thường liên quan đến một máy chủ và một hoặc nhiều máy khách. Máy khách gửi yêu cầu
đến máy chủ và máy chủ phản hồi. Máy khách bắt đầu bằng cách cố gắng thiết lập kết nối với máy chủ. Máy
chủ có thể chấp nhận hoặc từ chối kết nối. Sau khi kết nối được thiết lập, máy khách và máy chủ giao

tiếp thông qua các ổ cắm.

Máy chủ phải đang chạy khi máy khách cố gắng kết nối với máy chủ. Máy chủ đợi yêu cầu kết nối từ
máy khách. Các câu lệnh cần thiết để tạo socket trên máy chủ và trên máy khách được thể hiện trong

Hình 31.1.

31.2.1 Ổ cắm máy chủ

ổ cắm máy chủ Để thiết lập một máy chủ, bạn cần tạo một ổ cắm máy chủ và gắn nó vào một cổng, đây là nơi máy chủ

Hải cảng
lắng nghe các kết nối. Cổng xác định dịch vụ TCP trên ổ cắm. Số cổng nằm trong khoảng từ 0 đến 65536,

nhưng số cổng từ 0 đến 1024 được dành riêng cho các dịch vụ đặc quyền.
Machine Translated by Google

31.2 Máy khách / Máy chủ 1141

Máy chủ lưu trữ Máy chủ Khách hàng

Bước 1: Tạo một ổ cắm máy chủ trên một cổng, ví dụ: Bước 3: Một chương trình khách sử dụng những điều sau
câu lệnh để kết nối với máy chủ:
8000, sử dụng câu lệnh sau:
Mạng
ServerSocket serverSocket = new
ServerSocket (8000);
Socket socket = mới
Ổ cắm (serverHost, 8000);
Bước 2: Tạo một ổ cắm để kết nối với máy khách,
sử dụng câu lệnh sau:

Ổ cắm ổ cắm = Luồng I / O

serverSocket.accept ();

HÌNH 31.1 Máy chủ tạo một ổ cắm máy chủ và khi kết nối với máy khách được thiết lập, kết nối với máy khách bằng ổ cắm máy
khách.

Ví dụ: máy chủ email chạy trên cổng 25 và máy chủ Web thường chạy trên cổng 80.
Bạn có thể chọn bất kỳ số cổng nào hiện không được các chương trình khác sử dụng. Câu lệnh sau
tạo một máy chủ ổ cắm máy chủSocket:

ServerSocket serverSocket = new ServerSocket (cổng);

Ghi chú

Cố gắng tạo một ổ cắm máy chủ trên một cổng đã được sử dụng sẽ gây ra java.net.BindException.

BindException

31.2.2 Ổ cắm máy khách

Sau khi tạo ổ cắm máy chủ, máy chủ có thể sử dụng câu lệnh sau để lắng nghe các kết nối:

Socket socket = serverSocket.accept ();

Câu lệnh này đợi cho đến khi một máy khách kết nối với ổ cắm máy chủ. Máy khách đưa ra câu lệnh kết nối với khách hàng

ing sau để yêu cầu kết nối với máy chủ:

Socket socket = new Socket (serverName, port);

Câu lệnh này mở một ổ cắm để chương trình khách có thể giao tiếp với máy chủ. serverName là tên ổ cắm khách hàng

máy chủ Internet hoặc địa chỉ IP của máy chủ. Câu lệnh sau tạo một ổ cắm trên máy khách để kết sử dụng địa chỉ IP

nối với máy chủ 130.254.204.33 tại cổng 8000:

Socket socket = new Socket ("130.254.204.33", 8000)

Ngoài ra, bạn có thể sử dụng tên miền để tạo một ổ cắm, như sau: sử dụng tên miền

Socket socket = new Socket ("liang.armstrong.edu", 8000);

Khi bạn tạo một ổ cắm có tên máy chủ, JVM yêu cầu DNS dịch tên máy chủ thành địa chỉ IP.

Ghi chú

Một chương trình có thể sử dụng tên máy chủ localhost hoặc địa chỉ IP localhost
127.0.0.1 để chỉ máy khách đang chạy.
Machine Translated by Google

1142 Chương 31 Mạng


Ghi chú

UnknownHostException Phương thức khởi tạo Socket ném một java.net.UnknownHostException nếu không tìm
thấy máy chủ.

31.2.3 Truyền dữ liệu qua các ổ cắm


Sau khi máy chủ chấp nhận kết nối, giao tiếp giữa máy chủ và máy khách được tiến hành giống như đối với các

luồng I / O. Các câu lệnh cần thiết để tạo các luồng và trao đổi dữ liệu giữa chúng được thể hiện trong Hình

31.2.

Máy chủ Khách hàng

cổng int = 8000; cổng int = 8000;


DataInputStream trong; String host = "localhost"
DataOutputStream ra; DataInputStream trong;
Máy chủ ServerSocket; DataOutputStream ra;
Ổ cắm ổ cắm; Ổ cắm ổ cắm;
Sự liên quan
Yêu cầu
server = new ServerSocket (cổng);
socket = server.accept (); socket = new Socket (máy chủ, cổng);
in = new DataInputStream in = new DataInputStream
(socket.getInputStream ()); (socket.getInputStream ());
out = new DataOutStream I / O out = new DataOutputStream
(socket.getOutputStream ()); Dòng (socket.getOutputStream ());
System.out.println (in.readDouble ()); out.writeDouble (aNumber);
out.writeDouble (aNumber); System.out.println (in.readDouble ());

HÌNH 31.2 Máy chủ và máy khách trao đổi dữ liệu thông qua các luồng I / O trên đỉnh của ổ cắm.

Để nhận một luồng đầu vào và một luồng đầu ra, hãy sử dụng các phương thức getInputStream () và

getOutputStream () trên một đối tượng socket. Ví dụ: các câu lệnh sau tạo một luồng InputStream được gọi
là đầu vào và một luồng OutputStream được gọi là đầu ra
từ một ổ cắm:

Đầu vào InputStream = socket.getInputStream ();


Đầu ra OutputStream = socket.getOutputStream ();

Các luồng InputStream và OutputStream được sử dụng để đọc hoặc ghi các byte. Bạn có thể sử dụng
DataInputStream, DataOutputStream, BufferedReader và PrintWriter để bọc trên InputStream và OutputStream

để đọc hoặc ghi dữ liệu, chẳng hạn như int, double hoặc String. Ví dụ: các câu lệnh sau tạo đầu vào luồng

DataInputStream và đầu ra luồng DataOutput để đọc và ghi các giá trị dữ liệu nguyên thủy:

DataInputStream input = new DataInputStream


(socket.getInputStream ());
DataOutputStream output = new DataOutputStream
(socket.getOutputStream ());

Máy chủ có thể sử dụng input.readDouble () để nhận giá trị kép từ máy khách và output.writeDouble (d) để gửi

giá trị kép d đến máy khách.

Mẹo
Nhớ lại rằng I / O nhị phân hiệu quả hơn I / O văn bản vì I / O văn bản yêu cầu mã
hóa và giải mã. Do đó, tốt hơn là sử dụng I / O nhị phân để truyền dữ liệu giữa
máy chủ và máy khách để cải thiện hiệu suất.
Machine Translated by Google

31.2 Máy khách / Máy chủ 1143

31.2.4 Ví dụ về Máy khách / Máy chủ


Ví dụ này trình bày một chương trình máy khách và một chương trình máy chủ. Máy khách gửi dữ
liệu đến máy chủ. Máy chủ nhận dữ liệu, sử dụng nó để tạo ra một kết quả, sau đó gửi lại kết
quả cho máy khách. Máy khách hiển thị kết quả trên bảng điều khiển. Trong ví dụ này, dữ liệu
được gửi từ máy khách bao gồm bán kính của một hình tròn và kết quả do máy chủ tạo ra là diện
tích của hình tròn (xem Hình 31.3).

khu vực máy tính bán kính

Máy chủ Khách hàng

diện tích

HÌNH 31.3 Máy khách gửi bán kính đến máy chủ; máy chủ tính toán khu vực và gửi nó cho máy
khách.

Máy khách gửi bán kính thông qua một DataOutputStream trên ổ cắm luồng đầu ra và máy chủ
nhận bán kính thông qua DataInputStream trên ổ cắm luồng đầu vào, như thể hiện trong Hình
31.4a. Máy chủ tính toán khu vực và gửi nó đến máy khách thông qua một DataOutputStream trên ổ
cắm luồng đầu ra và máy khách nhận khu vực thông qua một DataInputStream trên ổ cắm luồng đầu
vào, như thể hiện trong Hình 31.4b. Các chương trình máy chủ và máy khách được đưa ra trong
Liệt kê 31.1 và 31.2. Hình 31.5 chứa một lần chạy mẫu của máy chủ và máy khách.

Máy chủ Khách hàng Máy chủ Khách hàng

bán kính bán kính diện tích diện tích

DataInputStream DataOutputStream DataOutputStream DataInputStream

socket.getInputStream socket.getOutputStream socket.getOutputStream socket.getInputStream

ổ cắm ổ cắm ổ cắm ổ cắm

Mạng Mạng

(Một) (b)

HÌNH 31.4 (a) Máy khách gửi bán kính đến máy chủ. (b) Máy chủ gửi khu vực đến máy khách.

HÌNH 31.5 Máy khách gửi bán kính đến máy chủ. Máy chủ nhận nó, tính toán vùng và gửi vùng đó
cho máy khách.

LISTING 31.1 Server.java


1 nhập java.io. *;
2 nhập java.net. *;
3 nhập java.util.Date;
Machine Translated by Google

1144 chương 31 mạng

4 nhập javafx.application.Application;
5 nhập javafx.application.Platform;
6 nhập javafx.scene.Scene;
7 nhập javafx.scene.control.ScrollPane;
8 nhập javafx.scene.control.TextArea;
9 nhập javafx.stage.Stage;
10
11 Máy chủ lớp công khai mở rộng Ứng dụng {
@Override // Ghi đè phương thức bắt đầu trong lớp Ứng dụng
13
12 public void start (Giai đoạn chínhStage) {
14 // Vùng văn bản để hiển thị nội dung
tạo giao diện người dùng máy chủ 15 TextArea ta = new TextArea ();
16
17 // Tạo một cảnh và đặt nó vào vùng hiển thị
18 Cảnh cảnh = Cảnh mới ( ScrollPane mới (ta), 450, 200);
19 primaryStage.setTitle ("Máy chủ"); // Đặt tiêu đề sân khấu
20 primaryStage.setScene (cảnh); // Đặt cảnh vào sân khấu
21 primaryStage.show (); // Hiển thị sân khấu
22
23 luồng mới (() -> {
24 thử {
25 // Tạo một ổ cắm máy chủ
ổ cắm máy chủ 26 ServerSocket serverSocket = new ServerSocket (8000);
cập nhật giao diện người dùng
27 Platform.runLater (() ->
"
28 ta.appendText ("Máy chủ khởi động lúc + new Date () + '\ n'));
29
30 // Nghe yêu cầu kết nối
kết nối khách hàng 31 Socket socket = serverSocket.accept ();
32
33 // Tạo luồng đầu vào và đầu ra dữ liệu
đầu vào từ khách hàng 34 DataInputStream inputFromClient = new DataInputStream (
35 socket.getInputStream ());
đầu ra cho khách hàng 36 DataOutputStream outputToClient = new DataOutputStream (
37 socket.getOutputStream ());
38

39 trong khi (đúng) {


40 // Nhận bán kính từ máy khách
đọc bán kính 41 bán kính kép = inputFromClient.readDouble ();
42
43 // Khu vực máy tính
44 gấp đôi diện tích = bán kính * bán kính * Math.PI;
45
46 // Gửi khu vực trở lại máy khách
khu vực viết 47 outputToClient.writeDouble (khu vực);
48
cập nhật giao diện người dùng
49 Platform.runLater (() -> {
50 ta.appendText ("Bán kính nhận được từ máy khách:"
51 + bán kính + '\ n');
52 ta.appendText ("Diện tích là:" + area + '\ n');
53 });
54 }
55 }
56 bắt (IOException ex) {
57 ex.printStackTrace ();
58 }
}).khởi đầu();
59 }
60 61}
Machine Translated by Google

31.2 Máy khách / Máy chủ 1145

LISTING 31.2 Client.java


1 nhập java.io. *;
2 nhập java.net. *;
3 nhập javafx.application.Application;
4 nhập javafx.geometry.Insets;
5 nhập javafx.geometry.Pos;
6 nhập javafx.scene.Scene;
7 nhập javafx.scene.control.Label;
8 nhập javafx.scene.control.ScrollPane;
9 nhập javafx.scene.control.TextArea;
10 nhập javafx.scene.control.TextField;
11 nhập javafx.scene.layout.BorderPane;
12 nhập javafx.stage.Stage;
13
14 public class Client mở rộng Ứng dụng {
15 // luồng IO
16 DataOutputStream toServer = null;
17 DataInputStream fromServer = null;
18
19 @Override // Ghi đè phương thức bắt đầu trong lớp Ứng dụng
20 public void start (Giai đoạn chínhStage) {
21 // Bảng p để giữ nhãn và trường văn bản
22 BorderPane paneForTextField = new BorderPane (); tạo giao diện người dùng

23 paneForTextField.setPadding (Insets mới (5, 5, 5, 5));


24 paneForTextField.setStyle ("- fx-border-color: green");
25 paneForTextField.setLeft ( Nhãn mới ("Nhập bán kính: "));
26
27 TextField tf = new TextField ();
28 tf.setAlignment (Pos.BOTTOM_RIGHT);
29 paneForTextField.setCenter (tf);
30
31 BorderPane mainPane = new BorderPane ();
32 // Vùng văn bản để hiển thị nội dung
33 TextArea ta = new TextArea ();
34 mainPane.setCenter ( ScrollPane mới (ta));
35 mainPane.setTop (paneForTextField);
36

37 // Tạo một cảnh và đặt nó vào vùng hiển thị


38 Cảnh cảnh = Cảnh mới (mainPane, 450, 200);
39 primaryStage.setTitle ("Máy khách"); // Đặt tiêu đề sân khấu
40 primaryStage.setScene (cảnh); // Đặt cảnh vào sân khấu
41 primaryStage.show (); // Hiển thị sân khấu
42
43 tf.setOnAction (e -> { xử lý sự kiện hành động

44 thử {
45 // Lấy bán kính từ trường văn bản
46 bán kính kép = Double.parseDouble (tf.getText (). trim ()); đọc bán kính

47
48 // Gửi bán kính đến máy chủ
49 toServer.writeDouble (bán kính); viết bán kính
50 toServer.flush ();
51
52 // Lấy khu vực từ máy chủ
53 vùng kép = fromServer.readDouble (); khu vực đọc

54
55 // Hiển thị vùng văn bản
56 ta.appendText ("Bán kính là" + bán kính + "\ n");
57 ta.appendText ("Khu vực nhận được từ máy chủ là"
58 + khu vực + '\ n');
Machine Translated by Google

1146, chương 31 Kết nối mạng

59 }
60 bắt (IOException ex) {
61 System.err.println (ví dụ);
62 }
63 });
64
65 thử {
66 // Tạo một ổ cắm để kết nối với máy chủ
yêu cầu kết nối 67 Socket socket = new Socket ("localhost", 8000);
68 // Socket socket = new Socket ("130.254.204.36", 8000);
69 // Socket socket = new Socket ("drake.Armstrong.edu", 8000);
70
71 // Tạo luồng đầu vào để nhận dữ liệu từ máy chủ
đầu vào từ máy chủ 72 fromServer = new DataInputStream (socket.getInputStream ());
73
74 // Tạo luồng đầu ra để gửi dữ liệu đến máy chủ
xuất ra máy chủ 75 toServer = new DataOutputStream (socket.getOutputStream ());
}
bắt (IOException ex) {
ta.appendText (ex.toString () + '\ n');
}
76 }
77 78 79 80 81}

Bạn khởi động chương trình máy chủ trước và sau đó khởi động chương trình khách. Trong chương
trình khách, nhập bán kính vào trường văn bản và nhấn Enter để gửi bán kính đến máy chủ. Máy chủ tính
toán khu vực và gửi lại cho máy khách. Quá trình này được lặp lại cho đến khi một trong hai chương
trình kết thúc.

Các lớp mạng nằm trong gói java.net. Bạn nên nhập gói này
khi viết các chương trình mạng Java.
Lớp Server tạo một ServerSocket serverSocket và gắn nó vào cổng 8000
bằng cách sử dụng câu lệnh này (dòng 26 trong Server.java):

ServerSocket serverSocket = new ServerSocket (8000);

Sau đó, máy chủ bắt đầu lắng nghe các yêu cầu kết nối, sử dụng câu lệnh sau (dòng 31 trong Server.java):

Socket socket = serverSocket.accept ();

Máy chủ đợi cho đến khi máy khách yêu cầu kết nối. Sau khi nó được kết nối, máy chủ đọc bán kính từ
máy khách thông qua luồng đầu vào, tính toán khu vực và gửi kết quả đến máy khách thông qua luồng đầu
ra. Phương thức accept () ServerSocket cần thời gian để thực thi. Không thích hợp để chạy phương
pháp này trong chuỗi ứng dụng JavaFX. Vì vậy, chúng tôi đặt nó trong một chuỗi riêng (dòng 23–59).
Các câu lệnh để cập nhật GUI cần phải chạy từ luồng ứng dụng JavaFX bằng phương thức Platform.runLater
(dòng 27–28, 49–53).
Lớp Khách hàng sử dụng câu lệnh sau để tạo một ổ cắm sẽ yêu cầu một con
kết nối với máy chủ trên cùng một máy (localhost) tại cổng 8000 (dòng 67 trong Client.java).

Socket socket = new Socket ("localhost", 8000);

Nếu bạn chạy máy chủ và máy khách trên các máy khác nhau, hãy thay thế localhost bằng tên máy chủ
hoặc địa chỉ IP của máy chủ. Trong ví dụ này, máy chủ và máy khách đang chạy trên cùng một máy.

Nếu máy chủ không chạy, chương trình khách kết thúc bằng java.net.ConnectException. Sau khi nó được
kết nối, máy khách nhận được các luồng đầu vào và đầu ra — được bao bọc bởi luồng đầu vào và đầu ra
dữ liệu — để nhận và gửi dữ liệu đến
máy chủ.
Machine Translated by Google

31.3 Địa chỉ Inet Lớp 1147

Nếu bạn nhận được java.net.BindException khi bạn khởi động máy chủ, thì cổng máy chủ hiện đang được

sử dụng. Bạn cần phải chấm dứt quá trình đang sử dụng cổng máy chủ và sau đó khởi động lại máy chủ.

Ghi chú

Khi bạn tạo một ổ cắm máy chủ, bạn phải chỉ định một cổng (ví dụ: 8000) cho ổ cắm.
Khi một máy khách kết nối với máy chủ (dòng 67 trong Client.java), một ổ cắm được tạo
trên máy khách. Ổ cắm này có cổng cục bộ riêng. Số cổng này (ví dụ: 2047) được JVM tự cổng ổ cắm máy khách

động chọn, như trong Hình 31.6.

số cổng

Máy chủ 0 0 Khách hàng

1 1

. .
. .
. .
2047 ổ cắm

ổ cắm 8000
. .
. .
. .

HÌNH 31.6 JVM tự động chọn một cổng khả dụng để tạo một ổ cắm cho máy khách.

Để xem cổng cục bộ trên máy khách, hãy chèn câu lệnh sau vào dòng 70 trong Client.java.

"
System.out.println (" cổng cục bộ: + socket.getLocalPort ());

31.1 Làm cách nào để bạn tạo một ổ cắm máy chủ? Số cổng nào có thể được sử dụng? Điều gì xảy ra nếu số

cổng được yêu cầu đã được sử dụng? Một cổng có thể kết nối với nhiều máy khách không? Kiểm tra điểm

31.2 Sự khác biệt giữa ổ cắm máy chủ và ổ cắm máy khách là gì?

31.3 Chương trình khách khởi tạo kết nối như thế nào?

31.4 Làm cách nào để máy chủ chấp nhận kết nối?

31.5 Dữ liệu được truyền giữa máy khách và máy chủ như thế nào?

31.3 Lớp InetAddress


Chương trình máy chủ có thể sử dụng lớp InetAddress để lấy thông tin về địa chỉ IP và tên máy
Chìa khóa

chủ cho máy khách. Điểm

Đôi khi, bạn muốn biết ai đang kết nối với máy chủ. Bạn có thể sử dụng lớp InetAddress để tìm tên máy chủ
và địa chỉ IP của máy khách. Lớp InetAddress lập mô hình địa chỉ IP. Bạn có thể sử dụng câu lệnh sau

trong chương trình máy chủ để lấy một phiên bản của InetAddress trên một ổ cắm kết nối với máy khách.

InetAddress inetAddress = socket.getInetAddress ();

Tiếp theo, bạn có thể hiển thị tên máy chủ và địa chỉ IP của khách hàng, như sau:

System.out.println (" Tên máy chủ của khách hàng +


là" inetAddress.getHostName ());
Machine Translated by Google

1148 chương 31 mạng

System.out.println (" Địa chỉ IP của Khách hàng là" +


inetAddress.getHostAddress ());

Bạn cũng có thể tạo một phiên bản InetAddress từ tên máy chủ hoặc địa chỉ IP bằng phương thức
getByName tĩnh . Ví dụ: câu lệnh sau tạo một InetAddress
cho máy chủ liang.armstrong.edu.

Địa chỉ InetAddress = InetAddress.getByName ("liang.armstrong.edu");

Liệt kê 31.3 đưa ra một chương trình xác định tên máy chủ và địa chỉ IP của các đối số mà bạn
truyền vào từ dòng lệnh. Dòng 7 tạo InetAddress bằng getByName
phương pháp. Dòng 8 và 9 sử dụng phương thức getHostName và getHostAddress để lấy tên và địa
chỉ IP của máy chủ. Hình 31.7 cho thấy một lần chạy chương trình mẫu.

HÌNH 31.7 Chương trình xác định tên máy chủ và địa chỉ IP.

DANH SÁCH 31.3 Nhận dạngHostNameIP.java


1 nhập java.net. *;
2
3 công khai lớp xác địnhHostNameIP {
4 public static void main (String [] args) {
5 for (int i = 0; i <args.length; i ++) {
thử {
có được một InetAddress 6 7 Địa chỉ InetAddress = InetAddress.getByName (args [i]);
"
lấy tên máy chủ System.out.print (" Tên máy chủ: + address.getHostName () + " ");
lấy IP máy chủ System.out.println ("Địa chỉ IP :" + address.getHostAddress ());
8

9 } catch (UnknownHostException ex) {


10 System.err.println ("Máy chủ lưu trữ hoặc địa chỉ IP không xác định" + args [i]);
11 12 13 }
14 }
15 }
16}

31.6 Làm cách nào để bạn có được một phiên bản của InetAddress?

Kiểm tra điểm 31.7 Bạn có thể sử dụng những phương pháp nào để lấy địa chỉ IP và tên máy chủ từ InetAddress?

31.4 Phục vụ Nhiều Khách hàng


Một máy chủ có thể phục vụ nhiều máy khách. Kết nối đến mỗi máy khách được xử lý bởi
Chìa khóa
một luồng.
Điểm

Nhiều máy khách thường được kết nối với một máy chủ cùng một lúc. Thông thường, một máy chủ chạy
liên tục trên một máy chủ và các máy khách từ khắp nơi trên Internet có thể kết nối với nó. Bạn
có thể sử dụng các chuỗi để xử lý đồng thời nhiều máy khách của máy chủ — đơn giản là
Machine Translated by Google

31.4 Phục vụ Nhiều Khách hàng 1149

tạo một luồng cho mỗi kết nối. Đây là cách máy chủ xử lý việc thiết lập kết nối:

trong khi (đúng) {


Socket socket = serverSocket.accept (); // Kết nối với khách hàng
Luồng luồng = new ThreadClass (socket);
thread.start ();
}

Ổ cắm máy chủ có thể có nhiều kết nối. Mỗi lần lặp lại của vòng lặp while sẽ tạo ra một kết nối mới. Bất cứ
khi nào kết nối được thiết lập, một luồng mới sẽ được tạo để xử lý giao tiếp giữa máy chủ và máy khách mới

và điều này cho phép nhiều kết nối chạy cùng một lúc.

Liệt kê 31.4 tạo một lớp máy chủ phục vụ nhiều máy khách đồng thời. Đối với mỗi luồng kết nối, máy chủ

bắt đầu một luồng mới. Luồng này liên tục nhận đầu vào (bán kính của hình tròn) từ các máy khách và gửi kết

quả (diện tích của hình tròn) trở lại cho họ (xem Hình 31.8).

Chương trình khách giống như trong Liệt kê 31.2. Một mẫu chạy của máy chủ với hai máy khách được thể hiện

trong Hình 31.9.

Máy chủ

Ổ cắm máy chủ

trên một cảng


Một ổ cắm cho một
Một ổ cắm cho một
khách hàng
khách hàng

Khách hàng 1 . . . Khách hàng n

HÌNH 31.8 Đa luồng cho phép máy chủ xử lý nhiều máy khách độc lập.

HÌNH 31.9 Máy chủ sinh ra một luồng để phục vụ máy khách.

LISTING 31.4 MultiThreadServer.java


1 nhập java.io. *;
2 nhập java.net. *;
3 nhập java.util.Date;
4 nhập javafx.application.Application;
5 nhập javafx.application.Platform;
6 nhập javafx.scene.Scene;
7 nhập javafx.scene.control.ScrollPane;
8 nhập javafx.scene.control.TextArea;
9 nhập javafx.stage.Stage;
10
Machine Translated by Google

1150, chương 31, kết nối mạng

11 lớp công khai MultiThreadServer mở rộng Ứng dụng {


12 // Vùng văn bản để hiển thị nội dung
13 private TextArea ta = new TextArea ();
14
15 // Đánh số cho một khách hàng
16 private int clientNo = 0;
17
18 @Override // Ghi đè phương thức bắt đầu trong lớp Ứng dụng
19 public void start (Giai đoạn chínhStage) {
20 // Tạo một cảnh và đặt nó vào vùng hiển thị
21 Cảnh cảnh = Cảnh mới ( ScrollPane mới (ta), 450, 200);
22 primaryStage.setTitle ("MultiThreadServer"); // Đặt tiêu đề sân khấu
23 primaryStage.setScene (cảnh); // Đặt cảnh vào sân khấu
24 primaryStage.show (); // Hiển thị sân khấu
25
26 luồng mới (() -> {
27 thử {
28 // Tạo một ổ cắm máy chủ
ổ cắm máy chủ 29 ServerSocket serverSocket = new ServerSocket (8000);
"
30 ta.appendText ("MultiThreadServer bắt đầu lúc + new Date
31 () + '\ n');
32
33 trong khi (đúng) {
34 // Nghe yêu cầu kết nối mới
kết nối khách hàng 35 Socket socket = serverSocket.accept ();
36
37 // Máy khách tăng dần Không
38 clientNo ++;
39

cập nhật GUI 40 Platform.runLater (() -> {


41 // Hiển thị số khách hàng
42 ta.appendText ("Bắt đầu chuỗi cho ứng dụng khách" + clientNo +
" "
43 tại + new Date () + '\ n');
44
45 // Tìm tên máy chủ và địa chỉ IP của khách hàng
thông tin mạng 46 InetAddress inetAddress = socket.getInetAddress ();
47 ta.appendText (Tên máy chủ của "Client " + clientNo + "là"
48 + inetAddress.getHostName () + "\ n");
49 ta.appendText ("Máy khách " + + clientNo + "Địa chỉ IP của là"
50 inetAddress.getHostAddress () + "\ n");
51 });
52
53 // Tạo và bắt đầu một chuỗi mới cho kết nối
tạo nhiệm vụ 54 luồng mới (HandleAClient mới (socket)). start ();
55 }
56 }
57 bắt (IOException ex) {
58 System.err.println (ví dụ);
59 }
bắt đầu chủ đề 60 }).khởi đầu();
61 }
62

63 // Xác định lớp luồng để xử lý kết nối mới


64 class HandleAClient triển khai Runnable {
lớp nhiệm vụ 65 ổ cắm Socket riêng ; // Một ổ cắm được kết nối
66
67 / ** Xây dựng một chuỗi * /
68 public HandleAClient (Socket ổ cắm) {
69 this.socket = ổ cắm;
70 }
71
Machine Translated by Google

31.5 Đối tượng Gửi và Nhận 1151

72 / ** Chạy một chuỗi * /


73 public void run () {
74 thử {
75 // Tạo luồng đầu vào và đầu ra dữ liệu
76 DataInputStream inputFromClient = new DataInputStream ( I / O

77 socket.getInputStream ());
78 DataOutputStream outputToClient = new DataOutputStream (
79 socket.getOutputStream ());
80

81 // Liên tục phục vụ khách hàng


82 trong khi (đúng) {
83 // Nhận bán kính từ máy khách
84 bán kính kép = inputFromClient.readDouble ();
85
86 // Khu vực máy tính
87 gấp đôi diện tích = bán kính * bán kính * Math.PI;
88
89 // Gửi khu vực trở lại máy khách
90 outputToClient.writeDouble (khu vực);
91
92 Platform.runLater (() -> {
93 ta.appendText ("bán kính nhận được từ máy khách:" + cập nhật GUI
94 bán kính + '\ n');
95 ta.appendText ("Khu vực tìm thấy:" + area + '\ n');
96 });
97 }
98 }
99 bắt (IOException e) {
100 ex.printStackTrace ();
101 }
102 }
103 }
104}

Máy chủ tạo một ổ cắm máy chủ tại cổng 8000 (dòng 29) và chờ kết nối (dòng 35).
Khi kết nối với máy khách được thiết lập, máy chủ sẽ tạo một luồng mới để xử lý giao tiếp
(dòng 54). Sau đó, nó đợi một kết nối khác trong một vòng lặp while vô hạn ( dòng 33–55).

Các luồng, chạy độc lập với nhau, giao tiếp với các máy khách được chỉ định. Mỗi luồng
tạo ra các luồng đầu vào và đầu ra dữ liệu nhận và gửi dữ liệu đến một máy khách.

31.8 Làm cách nào để bạn làm cho một máy chủ phục vụ nhiều máy khách?

Kiểm tra điểm

31.5 Đối tượng Gửi và Nhận


Một chương trình có thể gửi và nhận các đối tượng từ một chương trình khác.
Chìa khóa

Điểm
Trong các ví dụ trước, bạn đã học cách gửi và nhận dữ liệu kiểu nguyên thủy. Bạn cũng có
thể gửi và nhận các đối tượng bằng ObjectOutputStream và ObjectInputStream
trên các luồng ổ cắm. Để cho phép truyền, các đối tượng phải có thể tuần tự hóa. Ví dụ sau
minh họa cách gửi và nhận các đối tượng.
Ví dụ này bao gồm ba lớp: StudentAddress.java (Liệt kê 31.5), StudentClient.
java (Liệt kê 31.6) và StudentServer.java (Liệt kê 31.7). Chương trình máy khách thu thập
thông tin sinh viên từ máy khách và gửi đến máy chủ, như trong Hình 31.10.
Lớp StudentAddress chứa thông tin sinh viên: tên, đường phố, thành phố, tiểu bang và mã
zip. Lớp StudentAddress triển khai giao diện Serializable . Do đó, một đối tượng
StudentAddress có thể được gửi và nhận bằng cách sử dụng các luồng đầu ra và đầu vào của đối tượng.
Machine Translated by Google

1152 Chương 31 Kết nối mạng

HÌNH 31.10 Máy khách gửi thông tin sinh viên trong một đối tượng đến máy chủ.

LISTING 31.5 StudentAddress.java


đăng nhiều kỳ 1 lớp công khai StudentAddress triển khai java.io.Serializable {
tên chuỗi riêng ;
2
3 tư nhân phố String;

4 thành phố chuỗi tư nhân ;


5 trạng thái chuỗi tư nhân ;
6 chuỗi zip riêng tư ;
7

public StudentAddress (String name, String street, String city,


Trạng thái chuỗi, Chuỗi zip) {
8 9 10 this.name = tên;
11 this.street = đường phố;
12 this.city = thành phố;
13 this.state = trạng thái;
14 this.zip = zip;
15 }
16
17 public String getName () {
18 trả lại tên;
19 }
20
21 public String getStreet () {
22 đường trở về ;
23 }
24
25 public String getCity () {
26 trở lại thành phố;
27 }
28
29 public String getState () {
30 trở lại trạng thái;
31 }
32
33 public String getZip () {
34 trả lại mã zip;
35 }
36}

Máy khách gửi một đối tượng StudentAddress thông qua một ObjectOutputStream trên ổ cắm
luồng đầu ra và máy chủ nhận đối tượng Student thông qua ObjectInputStream trên ổ cắm
luồng đầu vào, như thể hiện trong Hình 31.11. Máy khách sử dụng phương thức writeObject
trong lớp ObjectOutputStream để gửi dữ liệu về một vết lõm đến máy chủ và máy chủ nhận
thông tin của sinh viên bằng cách sử dụng readObject
trong lớp ObjectInputStream . Các chương trình máy chủ và máy khách được đưa ra trong
Liệt kê 31.6 và 31.7.
Machine Translated by Google

31.5 Đối tượng Gửi và Nhận 1153

Máy chủ Khách hàng

đối tượng học sinh đối tượng học sinh

in.readObject () out.writeObject (Đối tượng)

trong: ObjectInputStream out: ObjectOutputStream

socket.getInputStream () socket.getOutputStream ()

ổ cắm ổ cắm

Mạng

HÌNH 31.11 Máy khách gửi một đối tượng StudentAddress đến máy chủ.

LISTING 31.6 StudentClient.java


1 nhập java.io. *;
2 nhập java.net. *;
3 nhập javafx.application.Application;
4 nhập javafx.event.ActionEvent;
5 nhập javafx.event.EventHandler;
6 nhập javafx.geometry.HPos;
7 nhập javafx.geometry.Pos;
8 nhập javafx.scene.Scene;
9 nhập javafx.scene.control.Button;
10 nhập javafx.scene.control.Label;
11 nhập javafx.scene.control.TextField;
12 nhập javafx.scene.layout.GridPane;
13 nhập javafx.scene.layout.HBox;
14 nhập javafx.stage.Stage;
15
16 lớp công khai StudentClient mở rộng Ứng dụng {
private TextField tfName = new TextField ();
18
17 private TextField tfStreet = new TextField ();
19 private TextField tfCity = new TextField ();
20 private TextField tfState = new TextField ();
21 private TextField tfZip = new TextField ();
22
23 // Nút gửi một sinh viên đến máy chủ
24 private Button btRegister = new Button ("Đăng ký vào Máy chủ");
25
26 // Tên máy chủ hoặc ip
27 String host = "localhost";
28
29 @Override // Ghi đè phương thức bắt đầu trong lớp Ứng dụng
30 public void start (Giai đoạn chínhStage) {
31 Ngăn GridPane = new GridPane (); tạo giao diện người dùng

32 pane.add ( Nhãn mới ("Tên"), 0, 0);


33 pane.add (tfName, 1, 0);
34 pane.add ( Nhãn mới ("Phố"), 0, 1);
35 pane.add (tfStreet, 1, 1);
36 pane.add ( Nhãn mới ("Thành phố"), 0, 2);
37
Machine Translated by Google

1154, chương 31, kết nối mạng

38 HBox hBox = mới HBox (2);


39 pane.add (hBox, 1, 2);
40 hBox.getChildren (). addAll (tfCity, new Label ("State"), tfState,
41 Nhãn mới ("Zip"), tfZip);
42 pane.add (btRegister, 1, 3);
43 GridPane.setHalignment (btRegister, HPos.RIGHT);
44
45 pane.setAlignment (Pos.CENTER);
46 tfName.setPrefColumnCount (15);
47 tfStreet.setPrefColumnCount (15);
48 tfCity.setPrefColumnCount (10);
49 tfState.setPrefColumnCount (2);
50 tfZip.setPrefColumnCount (3);
51

đăng ký người nghe 52 btRegister.setOnAction (new ButtonListener ());


53
54 // Tạo một cảnh và đặt nó vào vùng hiển thị
55 Cảnh cảnh = Cảnh mới (ngăn, 450, 200);
56 primaryStage.setTitle ("StudentClient"); // Đặt tiêu đề sân khấu
57 primaryStage.setScene (cảnh); // Đặt cảnh vào sân khấu
58 primaryStage.show (); // Hiển thị sân khấu
59 }
60

61 / ** Thao tác nút xử lý * /


62 lớp riêng ButtonListener triển khai EventHandler <ActionEvent> {
63 @Ghi đè
64 xử lý khoảng trống công cộng (ActionEvent e) {
65 thử {
66 // Thiết lập kết nối với máy chủ
ổ cắm máy chủ 67 Socket socket = new Socket (host, 8000);
68
69 // Tạo luồng đầu ra tới máy chủ
dòng đầu ra 70 ObjectOutputStream toServer =
71 ObjectOutputStream mới (socket.getOutputStream ());
72
73 // Lấy trường văn bản
74 String name = tfName.getText (). Trim ();
75 String street = tfStreet.getText (). Trim ();
76 String city = tfCity.getText (). Trim ();
77 Trạng thái chuỗi = tfState.getText (). Trim ();
78 String zip = tfZip.getText (). Trim ();
79
80 // Tạo một đối tượng Student và gửi đến máy chủ
81 StudentAddress s =
82 StudentAddress mới (tên, đường phố, thành phố, tiểu bang, mã zip);
gửi đến máy chủ 83 toServer.writeObject (các);
84 }
85 bắt (IOException ex) {
86 ex.printStackTrace ();
87 }
88 }
89 }
90}

LISTING 31.7 StudentServer.java


1 nhập java.io. *;
2 nhập java.net. *;
3
4 lớp công khai StudentServer {
Machine Translated by Google

31.5 Đối tượng Gửi và Nhận 1155

private ObjectOutputStream outputToFile;


5 private ObjectInputStream inputFromClient;
6 7

public static void main (String [] args) {


new StudentServer ();
8 }
9 10 11

12 public StudentServer () {
13 thử {
14 // Tạo một ổ cắm máy chủ
15 ServerSocket serverSocket = new ServerSocket (8000); ổ cắm máy chủ

16 System.out.println ("Máy chủ đã khởi động ");


17
18 // Tạo luồng đầu ra đối tượng
19 outputToFile = new ObjectOutputStream ( xuất ra tệp
20 new FileOutputStream ("student.dat", true));
21
22 trong khi (đúng) {
23 // Nghe yêu cầu kết nối mới
24 Socket socket = serverSocket.accept (); kết nối với khách hàng

25
26 // Tạo luồng đầu vào từ socket
27 inputFromClient = luồng đầu vào
28 ObjectInputStream mới (socket.getInputStream ());
29
30 // Đọc từ đầu vào
31 Đối tượng đối tượng = inputFromClient.readObject (); nhận được từ khách hàng

32
33 // Ghi vào tệp
34 outputToFile.writeObject (đối tượng); ghi vào tập tin

35 System.out.println ("Một đối tượng sinh viên mới được lưu trữ");
36 }
37 }
38 catch (ClassNotFoundException ex) {
39 ex.printStackTrace ();
40 }
41 bắt (IOException ex) {
42 ex.printStackTrace ();
43 }
44 cuối cùng {
45 thử
46 {inputFromClient.close ();
47 outputToFile.close ();
48 }
49 catch (Exception ex) {
50 ex.printStackTrace ();
51 }
52 }
53 }
54}

Ở phía máy khách, khi người dùng nhấp vào nút Đăng ký với máy chủ, máy khách sẽ tạo một
ổ cắm để kết nối với máy chủ (dòng 67), tạo một ObjectOutputStream trên luồng đầu ra của
ổ cắm (dòng 70 và 71) và gọi phương thức writeObject để gửi đối tượng StudentAddress
đến máy chủ thông qua luồng đầu ra đối tượng (dòng 83).
Ở phía máy chủ, khi máy khách kết nối với máy chủ, máy chủ tạo một ObjectInputStream
trên luồng đầu vào của socket (dòng 27 và 28), gọi phương thức readObject để nhận đối
tượng StudentAddress thông qua luồng đầu vào đối tượng (dòng 31) , và ghi đối tượng
vào một tệp (dòng 34).
Machine Translated by Google

1156, chương 31, kết nối mạng

31.9 Làm cách nào để máy chủ nhận kết nối từ máy khách? Làm cách nào để khách hàng kết nối với
máy chủ?

31.10 Làm cách nào để bạn tìm thấy tên máy chủ của chương trình khách từ máy chủ?

31.11 Làm thế nào để bạn gửi và nhận một đối tượng?

31.6 Nghiên cứu điển hình: Trò chơi Tic-Tac-Toe được phân phối
Phần này phát triển một chương trình cho phép hai người chơi chơi trò chơi tic-tac-toe trên
Internet.

Trong Phần 16.12, Nghiên cứu điển hình: Phát triển trò chơi Tic-Tac-Toe, bạn đã phát triển một chương

trình cho trò chơi tic-tac-toe cho phép hai người chơi chơi trò chơi trên cùng một máy. Trong phần này,

bạn sẽ học cách phát triển một trò chơi tic-tac-toe phân tán bằng cách sử dụng đa luồng và kết nối mạng

với các luồng socket. Trò chơi tic-tac-toe được phân phối cho phép người dùng chơi trên các máy khác

nhau từ bất kỳ đâu trên Internet.

Bạn cần phát triển một máy chủ cho nhiều máy khách. Máy chủ tạo một ổ cắm máy chủ và chấp nhận các kết

nối từ mỗi hai người chơi để tạo thành một phiên. Mỗi phiên là một chuỗi giao tiếp với hai người chơi

và xác định trạng thái của trò chơi. Máy chủ có thể thiết lập bất kỳ số lượng phiên nào, như thể hiện

trong Hình 31.13.

Đối với mỗi phiên, máy khách đầu tiên kết nối với máy chủ được xác định là người chơi 1 với mã thông

báo X và máy khách thứ hai kết nối được xác định là người chơi 2 với mã thông báo O. Máy chủ thông báo

cho người chơi về mã thông báo tương ứng của họ. Khi hai máy khách được kết nối với nó, máy chủ sẽ bắt

đầu một chuỗi để tạo điều kiện cho trò chơi giữa hai người chơi bằng cách thực hiện lặp lại các bước,

như thể hiện trong Hình 31.13.

Máy chủ

Phiên 1 ... Phiên n

...
Người chơi 1 Người chơi 2 Người chơi 1 Người chơi 2

HÌNH 31.12 Máy chủ có thể tạo nhiều phiên, mỗi phiên tạo điều kiện thuận lợi cho một trò chơi tic-tac-

toe cho hai người chơi.

Máy chủ không nhất thiết phải là một thành phần đồ họa, nhưng việc tạo nó trong GUI, trong đó thông

tin trò chơi có thể được xem là thân thiện với người dùng. Bạn có thể tạo một ngăn cuộn để giữ một vùng

văn bản trong GUI và hiển thị thông tin trò chơi trong vùng văn bản. Máy chủ tạo một luồng để xử lý một

phiên trò chơi khi hai người chơi được kết nối với máy chủ.

Khách hàng có trách nhiệm tương tác với người chơi. Nó tạo ra một giao diện người dùng với chín ô

và hiển thị tên trò chơi và trạng thái cho người chơi trong nhãn. Lớp khách hàng rất giống với lớp

TicTacToe được trình bày trong nghiên cứu điển hình ở Liệt kê 16.13. Tuy nhiên, khách hàng trong ví dụ

này không xác định trạng thái trò chơi (thắng hoặc hòa); nó chỉ đơn giản là chuyển các bước di chuyển

đến máy chủ và nhận trạng thái trò chơi từ máy chủ.

Dựa trên phân tích ở trên, bạn có thể tạo các lớp sau:

■ TicTacToeServer phục vụ tất cả các máy khách trong Liệt kê 31.9.

■ HandleASession tạo điều kiện cho hai người chơi trò chơi. Lớp này được định nghĩa trong
TicTacToeServer.java.
Machine Translated by Google

31.6 Nghiên cứu điển hình: Trò chơi Tic-Tac-Toe được phân phối 1157

Người chơi 1
Máy chủ Người chơi 2

1. Khởi tạo giao diện người dùng. Tạo một ổ cắm máy chủ. 1. Khởi tạo giao diện người dùng.

2. Yêu cầu kết nối với máy chủ và tìm hiểu mã Chấp nhận kết nối từ người chơi đầu tiên và thông báo

thông báo để sử dụng từ người chơi là Người chơi 1 với mã thông báo X.

máy chủ.

Chấp nhận kết nối từ trình phát thứ hai và 2. Yêu cầu kết nối với máy chủ và tìm hiểu sử dụng

thông báo cho người chơi là Người chơi 2 bằng mã thông báo O. mã thông báo nào từ máy chủ.

Bắt đầu một chủ đề cho phiên.

Xử lý một phiên:

1. Yêu cầu Người chơi 1 bắt đầu.


3. Nhận tín hiệu bắt đầu từ máy chủ.

2. Nhận hàng và cột của ô đã chọn từ


4. Chờ người chơi đánh dấu một ô,
Người chơi 1.
gửi chỉ mục hàng và cột của ô tới

máy chủ. 3. Nhận trạng thái từ máy chủ.


3. Xác định trạng thái trò chơi (THẮNG, VẼ,

TIẾP TỤC). Nếu Người chơi 1 thắng hoặc hòa, hãy gửi trạng thái
5. Nhận trạng thái từ máy chủ. 4. Nếu THẮNG, hiển thị người chiến thắng. Nếu Người chơi
(PLAYER1_WON, DRAW) cho cả hai người chơi và gửi nước đi của Người
1 thắng, nhận nước đi cuối cùng của Người chơi 1, và
chơi 1 cho Người chơi 2. Thoát.
6. Nếu THẮNG, hiển thị người chiến thắng; nếu phá vỡ vòng lặp.

Người chơi 2 thắng, nhận nước đi cuối cùng từ


4. Nếu TIẾP TỤC, hãy thông báo cho Người chơi 2 đến lượt, và gửi chỉ
Người chơi 2. Phá vỡ vòng lặp. 5. Nếu DRAW, trò chơi hiển thị kết thúc, và
số hàng và cột mới được chọn của Người chơi 1 cho Người chơi 2.
nhận nước đi cuối cùng của Người chơi 1 và phá

7. Nếu DRAW, trò chơi hiển thị kết thúc; vỡ vòng lặp.

phá vỡ vòng lặp.


5. Nhận hàng và cột của ô đã chọn từ
6. Nếu TIẾP TỤC, nhận của Người chơi 1
Người chơi 2.
hàng và chỉ mục đã chọn và đánh dấu ô

cho Người chơi 1.


6. Nếu Người chơi 2 thắng, hãy gửi trạng thái (PLAYER2_WON)
8. Nếu TIẾP TỤC, nhận chỉ số hàng và cột đã
cho cả hai người chơi và gửi nước đi của Người chơi 2 cho Người chơi 1.
chọn của Người chơi 2 và 7. Chờ người chơi di chuyển và gửi hàng và cột đã
Lối ra.

đánh dấu ô cho Người chơi 2. chọn đến

máy chủ.
7. Nếu TIẾP TỤC, hãy gửi trạng thái và gửi chỉ số hàng và cột mới

được chọn của Người chơi 2 cho Người chơi 1.

HÌNH 31.13 Máy chủ bắt đầu một chuỗi để tạo điều kiện giao tiếp giữa hai người chơi.

■ TicTacToeClient lập mô hình một trình phát trong Liệt kê 31.10.

■ Ô mô hình một ô trong trò chơi. Nó là một lớp bên trong TicTacToeClient.

■ TicTacToeConstants là một giao diện xác định các hằng số được chia sẻ bởi tất cả các lớp trong

ví dụ trong Liệt kê 31.8.

Mối quan hệ của các lớp này được thể hiện trong Hình 31.14.

LISTING 31.8 TicTacToeConstants.java


1 giao diện công khai TicTacToeConstants {
2 public static int PLAYER1 = 1; // Cho biết người chơi 1
3 public static int PLAYER2 = 2; // Cho biết người chơi 2
4 public static int PLAYER1_WON = 1; // Cho biết người chơi 1 đã thắng
5 public static int PLAYER2_WON = 2; // Cho biết người chơi 2 đã thắng
6 public static int DRAW = 3; // Cho biết một trận hòa
7 public static int CONTINUE = 4; // Cho biết để tiếp tục
số 8 }

DANH SÁCH 31,9 TicTacToeServer.java


1 nhập java.io. *;
2 nhập java.net. *;
3 nhập java.util.Date;
4 nhập javafx.application.Application;
Machine Translated by Google

1158, chương 31 kết nối mạng

Đơn xin TicTacToeServer Xử lý

TicTacToeConstants
Tương tự với

Liệt kê 18.10

Đơn xin TicTacToeClient Tủ

Runnable

TicTacToeServer Xử lý TicTacToeClient

start (primaryStage: Stage): -player1: Socket -myTurn: boolean


vô hiệu
-player2: Ổ cắm -myToken: char
-cell: char [] [] -otherToken: char
-continueToPlay: boolean -cell: Ô [] []
«Giao diện»
-continueToPlay: boolean
TicTacToeConstants
-rowSelected: int
+ run (): void -columnSelected: int
+ PLAYER1 = 1: int
+ PLAYER2 = 2: int -isWon (): boolean -fromServer: DataInputStream
-isFull (): boolean -toServer: DataOutputStream
+ PLAYER1_WON = 1: int
-sendMove (hết: -waiting: boolean
+ PLAYER2_WON = 2: int
+ DRAW = 3: int DataOutputStream, hàng: int,
+ CONTINUE = 4: int cột: int): void
+ run (): void
-connectToServer (): void
-receiveMove (): void
-sendMove (): void
-receiveInfoFromServer (): void
-waitForPlayerAction (): void

HÌNH 31.14 TicTacToeServer tạo một thể hiện của HandleASession cho mỗi phiên của hai người chơi.
TicTacToeClient tạo chín ô trong giao diện người dùng.

5 nhập javafx.application.Platform;
6 nhập javafx.scene.Scene;
7 nhập javafx.scene.control.ScrollPane;
8 nhập javafx.scene.control.TextArea;
9 nhập javafx.stage.Stage;
10
11 lớp công khai TicTacToeServer mở rộng Ứng dụng 12 triển khai
TicTacToeConstants {
13 private int sessionNo = 1; // Đánh số phiên
14
15 @Override // Ghi đè phương thức bắt đầu trong lớp Ứng dụng
16 public void start (Giai đoạn chínhStage) {
tạo giao diện người dùng 17 TextArea taLog = new TextArea ();
18
19 // Tạo một cảnh và đặt nó vào vùng hiển thị
20 Cảnh cảnh = Cảnh mới ( ScrollPane mới (taLog), 450, 200);
21 primaryStage.setTitle ("TicTacToeServer"); // Đặt tiêu đề sân khấu
22 primaryStage.setScene (cảnh); // Đặt cảnh vào sân khấu
23 primaryStage.show (); // Hiển thị sân khấu
24
25 luồng mới (() -> {
26 thử {
27 // Tạo một ổ cắm máy chủ
ổ cắm máy chủ 28 ServerSocket serverSocket = new ServerSocket (8000);
Machine Translated by Google

31.6 Nghiên cứu điển hình: Trò chơi Tic-Tac-Toe được phân phối 1159

29 Platform.runLater (() -> taLog.appendText (new Date () +


30 ": Máy chủ khởi động tại socket 8000 \ n"));
31
32 // Sẵn sàng tạo phiên cho mỗi hai người chơi
33 trong khi (đúng) {
34 Platform.runLater (() -> taLog.appendText (new Date () +
35 ": Chờ người chơi tham gia phiên" + sessionNo + '\ n'));
36
37 // Kết nối với người chơi 1
38 Socket player1 = serverSocket.accept (); kết nối với khách hàng

39
40 Platform.runLater (() -> {
41 taLog.appendText (new Date () + ": Người chơi 1 đã tham gia phiên"
42 + sessionNo + '\ n');
43 taLog.appendText (" Địa chỉ IP của Người chơi 1" +
44 player1.getInetAddress (). getHostAddress () + '\ n');
45 });
46
47 // Thông báo rằng người chơi là Người chơi 1 đến người chơi1

48 DataOutputStream mới (
49 player1.getOutputStream ()). writeInt (PLAYER1);
50
51 // Kết nối với người chơi 2
52 Socket player2 = serverSocket.accept (); kết nối với khách hàng

53
54 Platform.runLater (() -> {
55 taLog.appendText (new Date () +
56 ": Người chơi 2 đã tham gia phiên" + sessionNo + '\ n');
57 taLog.appendText (" Địa chỉ IP của Người chơi 2" +
58 player2.getInetAddress (). getHostAddress () + '\ n');
59 });
60

61 // Thông báo rằng người chơi là Người chơi 2


62 DataOutputStream mới ( cho người chơi2

63 player2.getOutputStream ()). writeInt (PLAYER2);


64
65 // Hiển thị phiên này và số phiên tăng dần
66 Platform.runLater (() ->
67 taLog.appendText (new Date () +
68 ": Bắt đầu một chuỗi cho phiên" + sessionNo ++ + '\ n'));
69
70 // Khởi chạy một chuỗi mới cho phiên này của hai người chơi một phiên cho hai người chơi
71 new Thread (new HandleASession (player1, player2)). start ();
72 }
73 }
74 bắt (IOException ex) {
75 ex.printStackTrace ();
76 }
77 }).khởi đầu();
78 }
79
80 // Xác định lớp luồng để xử lý một phiên mới cho hai trình phát
81 class HandleASession triển khai Runnable, TicTacToeConstants {
82 người chơi Socket riêng1 ;
83 người chơi Socket riêng2 ;
84

85 // Tạo và khởi tạo ô


86 char riêng [] [] cell = new char [3] [3];
87
88 DataInputStream riêng tư fromPlayer1;
Machine Translated by Google

1160, chương 31, kết nối mạng

89 DataOutputStream riêng tư toPlayer1;


90 DataInputStream riêng từPlayer2;
91 DataOutputStream riêng tư toPlayer2;
92
93 // Tiếp tục chơi
94 boolean riêng continueToPlay = true;
95
96 / ** Tạo một chuỗi * /
97 public HandleASession (Socket player1, Socket player2) {
98 this.player1 = player1;
99 this.player2 = player2;
100
101 // Khởi tạo ô
102 for (int i = 0; i < 3; i ++)
103 for (int j = 0; j < 3; j ++)
104 ô [i] [j] = ' ';
105 }
106
107 / ** Triển khai phương thức run () cho chuỗi * /
108 public void run () {
109 thử {
110 // Tạo luồng đầu vào và đầu ra dữ liệu
Luồng IO 111 DataInputStream fromPlayer1 = new DataInputStream (
112 player1.getInputStream ());
113 DataOutputStream toPlayer1 = new DataOutputStream (
114 player1.getOutputStream ());
115 DataInputStream fromPlayer2 = new DataInputStream (
116 player2.getInputStream ());
117 DataOutputStream toPlayer2 = new DataOutputStream (
118 player2.getOutputStream ());
119
120 // Viết bất cứ thứ gì để thông báo cho người chơi 1 bắt đầu
121 // Điều này chỉ để cho người chơi 1 biết để bắt đầu
122 toPlayer1.writeInt (1);
123
124 // Liên tục phục vụ người chơi và xác định và báo cáo
125 // trạng thái trò chơi cho người chơi
126 trong khi (đúng) {
127 // Nhận nước đi từ người chơi 1
128 int row = fromPlayer1.readInt ();
129 int column = fromPlayer1.readInt ();
130 ô [row] [column] = 'X';
131
132 // Kiểm tra xem người chơi 1 có thắng không
X đã thắng? 133 if (isWon ('X')) {
134 toPlayer1.writeInt (PLAYER1_WON);
135 toPlayer2.writeInt (PLAYER1_WON);
136 sendMove (toPlayer2, hàng, cột);
137 nghỉ; // Ngắt vòng lặp
138 }
Có đầy đủ không? 139 else if (isFull ()) { // Kiểm tra xem tất cả các ô đã được lấp đầy chưa
140 toPlayer1.writeInt (VẼ);
141 toPlayer2.writeInt (VẼ);
142 sendMove (toPlayer2, hàng, cột);
143 nghỉ;
144 }
145 khác {
146 // Thông báo cho người chơi 2 để đến lượt
147 toPlayer2.writeInt (TIẾP TỤC);
148
Machine Translated by Google

31.6 Nghiên cứu điển hình: Trò chơi Tic-Tac-Toe được phân phối 1161

149 // Gửi hàng và cột đã chọn của người chơi 1 cho người chơi 2
150 sendMove (toPlayer2, hàng, cột);
151 }
152
153 // Nhận nước đi từ Người chơi 2
154 row = fromPlayer2.readInt ();
155 cột = fromPlayer2.readInt ();
156 ô [row] [column] = 'O';
157
158 // Kiểm tra xem người chơi 2 có thắng không
159 if (isWon ('O')) { O đã thắng?

160 toPlayer1.writeInt (PLAYER2_WON);


161 toPlayer2.writeInt (PLAYER2_WON);
162 sendMove (toPlayer1, hàng, cột);
163 nghỉ;
164 }
165 khác {
166 // Thông báo cho người chơi 1 để đến lượt
167 toPlayer1.writeInt (TIẾP TỤC);
168
169 // Gửi hàng và cột đã chọn của người chơi 2 tới người chơi 1
170 sendMove (toPlayer1, hàng, cột);
171 }
172 }
173 }
174 bắt (IOException ex) {
175 ex.printStackTrace ();
176 }
177 }
178
179 / ** Gửi nước đi cho người chơi khác * /
180 private void sendMove (DataOutputStream out, int row, int column) gửi một động thái

181 ném IOException {


182 out.writeInt (hàng); // Gửi chỉ mục hàng
183 out.writeInt (cột); // Gửi chỉ mục cột
184 }
185
186 / ** Xác định xem các ô có bị chiếm hết không * /
187 boolean private isFull () {
188 for (int i = 0; i < 3; i ++)
189 for (int j = 0; j < 3; j ++)
190 if (ô [i] [j] == ' ')
191 trả về sai; // Ít nhất một ô không được điền
192
193 // Tất cả các ô đều được điền
194 trả về true;
195 }
196
197 / ** Xác định xem người chơi có mã thông báo được chỉ định có thắng hay không * /
198 private boolean isWon ( mã thông báo char) {
199 // Kiểm tra tất cả các hàng
200 for (int i = 0; i < 3; i ++)
201 if ((ô [i] [0] == mã thông báo)
202 && (ô [i] [1] == mã thông báo)
203 && (ô [i] [2] == mã thông báo)) {
204 trả về true;
205 }
206
207 / ** Kiểm tra tất cả các cột * /
208 for (int j = 0; j < 3; j ++)
Machine Translated by Google

1162, chương 31 kết nối mạng

209 if ((ô [0] [j] == mã thông báo)


210 && (ô [1] [j] == mã thông báo)
211 && (ô [2] [j] == mã thông báo)) {
212 trả về true;
213 }
214
215 / ** Kiểm tra đường chéo chính * /
216 if ((ô [0] [0] == mã thông báo)
217 && (ô [1] [1] == mã thông báo)
218 && (ô [2] [2] == mã thông báo)) {
219 trả về true;
220 }
221
222 / ** Kiểm tra biểu tượng con * /
223 if ((ô [0] [2] == mã thông báo)
224 && (ô [1] [1] == mã thông báo)
225 && (ô [2] [0] == mã thông báo)) {
226 trả về true;
227 }
228
229 / ** Tất cả đã được chọn, nhưng không có người chiến thắng * /

230 trả về sai;


231 }
232 }
233}

DANH SÁCH 31.10 TicTacToeClient.java


1 nhập java.io. *;
2 nhập java.net. *;
3 nhập java.util.Date;
4 nhập javafx.application.Application;
5 nhập javafx.application.Platform;
6 nhập javafx.scene.Scene;
7 nhập javafx.scene.control.Label;
8 nhập javafx.scene.control.ScrollPane;
9 nhập javafx.scene.control.TextArea;
10 nhập javafx.scene.layout.BorderPane;
11 nhập javafx.scene.layout.GridPane;
12 nhập javafx.scene.layout.Pane;
13 nhập javafx.scene.paint.Color;
14 nhập javafx.scene.shape.Ellipse;
15 nhập javafx.scene.shape.Line;
16 nhập javafx.stage.Stage;
17
18 lớp công khai TicTacToeClient mở rộng Ứng dụng 19 triển khai
TicTacToeConstants {
20 // Cho biết người chơi có lượt đi hay không
21 private boolean myTurn = false;
22
23 // Cho biết mã thông báo cho người chơi
24 char myToken riêng tư = ' ';
25
26 // Chỉ ra mã thông báo cho người chơi khác
27 char riêng tư otherToken = ' ';
28
29 // Tạo và khởi tạo ô
30 private Cell [] [] cell = new Cell [3] [3];
31
Machine Translated by Google

31.6 Nghiên cứu điển hình: Trò chơi Tic-Tac-Toe được phân phối 1163

32 // Tạo và khởi tạo nhãn tiêu đề


33 Nhãn riêng lblTitle = Nhãn mới ();
34

35 // Tạo và khởi tạo một nhãn trạng thái


36 Nhãn riêng lblStatus = new Label ();
37
38 // Cho biết hàng và cột đã chọn bằng cách di chuyển hiện tại
39 private int rowSelected;
40 private int columnSelected;
41
42 // Luồng đầu vào và đầu ra từ / đến máy chủ
43 DataInputStream riêng tư fromServer;
44 DataOutputStream toServer riêng tư ;
45
46 // Tiếp tục chơi?
47 boolean riêng continueToPlay = true;
48
49 // Chờ người chơi đánh dấu một ô
50 boolean riêng tư wait = true;
51

52 // Tên máy chủ hoặc ip


53 private String host = "localhost";
54
55 @Override // Ghi đè phương thức bắt đầu trong lớp Ứng dụng
56 public void start (Giai đoạn chínhStage) {
57 // Ngăn để giữ ô
58 Ngăn GridPane = new GridPane (); tạo giao diện người dùng

59 for (int i = 0; i < 3; i ++)


60 for (int j = 0; j < 3; j ++)
61 pane.add (cell [i] [j] = new Cell (i, j), j, i);
62
63 BorderPane borderPane = new BorderPane ();
64 borderPane.setTop (lblTitle);
65 borderPane.setCenter (ngăn);
66 borderPane.setBottom (lblStatus);
67
68 // Tạo một cảnh và đặt nó vào vùng hiển thị
69 Cảnh cảnh = Cảnh mới (borderPane, 320, 350);
70 primaryStage.setTitle ("TicTacToeClient"); // Đặt tiêu đề sân khấu
71 primaryStage.setScene (cảnh); // Đặt cảnh vào sân khấu
72 primaryStage.show (); // Hiển thị sân khấu
73
74 // Kết nối với máy chủ
75 kết nối với máy chủ(); kết nối với máy chủ

76 }
77
78 private void connectToServer () {
79 thử {
80 // Tạo một ổ cắm để kết nối với máy chủ
81 Socket socket = new Socket (host, 8000);
82
83 // Tạo luồng đầu vào để nhận dữ liệu từ máy chủ
84 fromServer = new DataInputStream (socket.getInputStream ()); đầu vào từ máy chủ
85
86 // Tạo luồng đầu ra để gửi dữ liệu đến máy chủ
87 toServer = new DataOutputStream (socket.getOutputStream ()); xuất ra máy chủ
88 }
89 catch (Exception ex) {
90 ex.printStackTrace ();
91 }
Machine Translated by Google

1164, chương 31, kết nối mạng

92
93 // Kiểm soát trò chơi trên một chuỗi riêng biệt
94 luồng mới (() -> {
95 thử {
96 // Nhận thông báo từ máy chủ
97 int player = fromServer.readInt ();
98
99 // Tôi là người chơi 1 hay 2?
100 if (player == PLAYER1) {
101 myToken = 'X';
102 otherToken = 'O';
103 Platform.runLater (() -> {
104 lblTitle.setText ("Người chơi 1 với mã thông báo 'X'");
105 lblStatus.setText ("Đang đợi người chơi 2 tham gia");
106 });
107
108 // Nhận thông báo khởi động từ máy chủ
109 fromServer.readInt (); // Bất kỳ nội dung nào đã đọc đều bị bỏ qua
110
111 // Người chơi khác đã tham gia
112 Platform.runLater (() ->
113 lblStatus.setText ("Người chơi 2 đã tham gia. Tôi bắt đầu trước"));
114
115 // Tới lượt tôi
116 myTurn = true;
117 }
118 khác nếu (người chơi == PLAYER2) {
119 myToken = 'O';
120 otherToken = 'X';
121 Platform.runLater (() -> {
122 lblTitle.setText ("Người chơi 2 với mã thông báo 'O'");
123 lblStatus.setText ("Đang đợi người chơi 1 di chuyển");
124 });
125 }
126
127 // Tiếp tục chơi
128 trong khi (continueToPlay) {
129 if (player == PLAYER1) {
130 waitForPlayerAction (); // Chờ người chơi 1 di chuyển
131 sendMove (); // Gửi di chuyển đến máy chủ
132 getInfoFromServer (); // Nhận thông tin từ máy chủ
133 }
134 khác nếu (người chơi == PLAYER2) {
135 getInfoFromServer (); // Nhận thông tin từ máy chủ
136 waitForPlayerAction (); // Chờ người chơi 2 di chuyển
137 sendMove (); // Gửi lượt di chuyển của người chơi 2 tới máy chủ
138 }
139 }
140 }
141 catch (Exception ex) {
142 ex.printStackTrace ();
143 }
144 }).khởi đầu();
145 }
146
147 / ** Chờ người chơi đánh dấu một ô * /
148 private void waitForPlayerAction () ném InterruptException {
149 trong khi (chờ đợi) {
150 Thread.sleep (100);
151 }
Machine Translated by Google

31.6 Nghiên cứu điển hình: Trò chơi Tic-Tac-Toe được phân phối 1165

152
153 chờ đợi = true;
154 }
155
156 / ** Gửi di chuyển của người chơi này đến máy chủ * /
157 private void sendMove () ném IOException {
158 toServer.writeInt (rowSelected); // Gửi hàng đã chọn
159 toServer.writeInt (columnSelected); // Gửi cột đã chọn
160 }

162 / ** Nhận thông tin từ máy chủ * /


163 private void getInfoFromServer () ném IOException {
164 // Nhận trạng thái trò chơi
165 int status = fromServer.readInt ();
166
167 nếu (trạng thái == PLAYER1_WON) {
168 // Người chơi 1 đã thắng, dừng chơi
169 continueToPlay = false;
170 if (myToken == 'X') {
171 Platform.runLater (() -> lblStatus.setText ("Tôi đã thắng! (X)"));
172 }
173 else if (myToken == 'O') {
174 Platform.runLater (() ->
174 175 lblStatus.setText ("Người chơi 1 (X) đã thắng!"));
176 getMove ();
177 }
178 }
179 khác nếu (trạng thái == PLAYER2_WON) {
180 // Người chơi 2 đã thắng, dừng chơi
181 continueToPlay = false;
182 if (myToken == 'O') {
183 Platform.runLater (() -> lblStatus.setText ("Tôi đã thắng! (O)"));
184 }
185 else if (myToken == 'X') {
186 Platform.runLater (() ->
187 lblStatus.setText ("Người chơi 2 (O) đã thắng!"));
188 getMove ();
189 }
190 }
191 else if (status == DRAW) {
192 // Không có người chiến thắng, trò chơi kết thúc
193 continueToPlay = false;
194 Platform.runLater (() ->
195 lblStatus.setText ("Trò chơi kết thúc, không có người chiến thắng!"));
196
197 if (myToken == 'O') {
198 getMove ();
199 }
200 }
201 khác {
202 getMove ();
203 Platform.runLater (() -> lblStatus.setText (" Đến lượt tôi"));
204 myTurn = true; // Tới lượt tôi
205 }
206 }
207
208 private void getMove () ném IOException {
209 // Nhận nước đi của người chơi khác
210 int row = fromServer.readInt ();
211 int column = fromServer.readInt ();
Machine Translated by Google

1166, chương 31, kết nối mạng

212 Platform.runLater (() -> ô [hàng] [cột] .setToken (otherToken));


213 }
214
215 // Một lớp bên trong cho một ô
mô hình một ô 216 lớp công khai Ô mở rộng Ngăn {
217 // Cho biết hàng và cột của ô này trong bảng
218 hàng int riêng tư ;
219 cột int riêng tư ;
220
221 // Mã thông báo được sử dụng cho ô này
222 mã thông báo char riêng = ' ';
223
224 ô công cộng (int hàng, int cột) {
225 this.row = row;
226 this.column = column;
227 this.setPrefSize (2000, 2000); // Điều gì xảy ra nếu không có điều này?
228 setStyle ("- fx-border-color: black"); // Đặt đường viền của ô
đăng ký người nghe 229 this.setOnMouseClicked (e -> handleMouseClick ());
230 }
231
232 / ** Mã thông báo trả lại * /
233 public char getToken () {
234 trả lại mã thông báo;
235 }
236
237 / ** Đặt mã thông báo mới * /
238 public void setToken (char c) {
239 mã thông báo = c;
240 Sơn lại();
241 }
242
void repaint () {
if (mã thông báo == 'X') {
vẽ X Dòng line1 = new Dòng (10, 10,
this.getWidth () - 10, this.getHeight () - 10);
line1.endXProperty (). bind (this.widthProperty (). subtract (10));
line1.endYProperty (). bind (this.heightProperty (). subtract (10));
Dòng line2 = new Dòng (10, this.getHeight () - 10,
this.getWidth () - 10, 10);
243244 245 246 247line2.startYProperty
248 249 250 251 (). bind (
252 this.heightProperty (). subtract (10));
253 line2.endXProperty (). bind (this.widthProperty (). subtract (10));
254
255 // Thêm các dòng vào ngăn
256 this.getChildren (). addAll (line1, line2);
257 }
258 khác nếu (mã thông báo == 'O') {
vẽ O 259 Ellipse ellipse = new Ellipse (this.getWidth () / 2,
260 this.getHeight () / 2, this.getWidth () / 2 - 10,
261 this.getHeight () / 2 - 10);
262 ellipse.centerXProperty (). bind (
263 this.widthProperty (). chia (2));
264 ellipse.centerYProperty (). bind (
265 this.heightProperty (). chia (2));
266 ellipse.radiusXProperty (). bind (
267 this.widthProperty (). split (2) .subtract (10));
268 ellipse.radiusYProperty (). bind (
269 this.heightProperty (). split (2) .subtract (10));
270 ellipse.setStroke (Color.BLACK);
271 ellipse.setFill (Màu.WHITE);
Machine Translated by Google

31.6 Nghiên cứu điển hình: Trò chơi Tic-Tac-Toe được phân phối 1167

272
273 getChildren (). add (ellipse); // Thêm hình elip vào ngăn
274 }
275 }
276
277 / * Xử lý sự kiện nhấp chuột * /
278 private void handleMouseClick () { trình xử lý nhấp chuột

279 // Nếu ô không bị chiếm và người chơi có lượt


' '
280 if (mã thông báo == && myTurn) {
281 setToken (myToken); // Đặt mã thông báo của người chơi trong ô
282 myTurn = false;
283 rowSelected = hàng;
284 columnSelected = cột;
285 lblStatus.setText ("Đang đợi người chơi khác di chuyển");
286 chờ đợi = sai; // Vừa hoàn thành một bước di chuyển thành công
287 }
288 }
289 }
290}

Máy chủ có thể phục vụ bất kỳ số lượng phiên nào đồng thời. Mỗi phiên chăm sóc của hai người chơi.
Ứng dụng khách có thể được triển khai để chạy như một ứng dụng Java. Để chạy ứng dụng khách dưới
dạng ứng dụng Java từ trình duyệt Web, máy chủ phải chạy từ máy chủ Web. Hình 31.15 và 31.16 cho
thấy các lần chạy mẫu của máy chủ và máy khách.

HÌNH 31.15 TicTacToeServer chấp nhận các yêu cầu kết nối và tạo các phiên để phục vụ các cặp
người chơi.

HÌNH 31.16 TicTacToeClient có thể chạy dưới dạng applet hoặc độc lập.

Giao diện TicTacToeConstants định nghĩa các hằng số được chia sẻ bởi tất cả các lớp trong dự
án. Mỗi lớp sử dụng các hằng số cần phải triển khai giao diện. Định nghĩa tập trung các hằng số
trong giao diện là một thực tế phổ biến trong Java.
Sau khi một phiên được thiết lập, máy chủ sẽ nhận được các động thái thay thế từ người chơi.
Khi nhận được động thái từ người chơi, máy chủ sẽ xác định trạng thái của trò chơi. Nếu trò chơi
chưa kết thúc, máy chủ sẽ gửi trạng thái (TIẾP TỤC) và chuyển động của người chơi tới
Machine Translated by Google

1168, chương 31, kết nối mạng

người chơi khác. Nếu trò chơi thắng hoặc hòa, máy chủ sẽ gửi trạng thái (PLAYER1_WON,
PLAYER2_WON hoặc DRAW) cho cả hai người chơi.
Việc triển khai các chương trình mạng Java ở cấp socket được đồng bộ hóa chặt chẽ.
Một hoạt động để gửi dữ liệu từ một máy yêu cầu một hoạt động để nhận dữ liệu từ máy kia. Như
thể hiện trong ví dụ này, máy chủ và máy khách được đồng bộ hóa chặt chẽ để gửi hoặc nhận dữ
liệu.

33.11 Điều gì sẽ xảy ra nếu kích thước ưa thích cho một ô không được đặt ở dòng 227 trong Danh sách
Kiểm tra điểm
31,10?

33.12 Nếu một người chơi không có lượt nhưng nhấp vào ô trống, chương trình khách
trong Liệt kê 31.10 sẽ làm gì?

ĐIỀU KHOẢN CHÍNH

ổ cắm máy khách 1141 giao tiếp dựa trên gói 1140
tên miền 1140 ổ cắm máy chủ 1140

máy chủ tên miền 1140 ổ cắm 1140


localhost 1141 giao tiếp dựa trên luồng 1140

Địa chỉ IP 1140 TCP 1140

cổng 1140 UDP 1140

TÓM TẮT CHƯƠNG

1. Java hỗ trợ các ổ cắm dòng và ổ cắm datagram. Các ổ cắm luồng sử dụng TCP (Giao thức điều
khiển truyền tải) để truyền dữ liệu, trong khi các ổ cắm dữ liệu sử dụng UDP (Giao thức dữ
liệu người dùng). Vì TCP có thể phát hiện các đường truyền bị mất và gửi lại chúng, nên các
đường truyền không bị mất dữ liệu và đáng tin cậy. Ngược lại, UDP không thể đảm bảo truyền
không mất dữ liệu.

2. Để tạo một máy chủ, trước tiên bạn phải có được ổ cắm máy chủ, sử dụng ServerSocket mới
(Hải cảng). Sau khi ổ cắm máy chủ được tạo, máy chủ có thể bắt đầu lắng nghe các kết
nối, sử dụng phương thức accept () trên ổ cắm máy chủ. Máy khách yêu cầu kết nối đến
máy chủ bằng cách sử dụng Socket mới (Tên máy chủ, cổng) để tạo ổ cắm máy khách.

3. Giao tiếp cổng luồng rất giống giao tiếp luồng đầu vào / đầu ra
sau khi kết nối giữa máy chủ và máy khách được thiết lập. Bạn có thể lấy luồng đầu vào

bằng phương thức getInputStream () và luồng đầu ra bằng phương thức getOutputStream
() trên socket.

4. Một máy chủ thường xuyên phải làm việc với nhiều máy khách cùng một lúc. Bạn có thể sử
dụng các luồng để xử lý đồng thời nhiều máy khách của máy chủ bằng cách tạo một luồng
cho mỗi kết nối.

ĐỐ

Trả lời câu hỏi cho chương này trực tuyến tại www.cs.armstrong.edu/liang/intro10e/quiz.html.
Machine Translated by Google

Bài tập lập trình 1169

BÀI TẬP LẬP TRÌNH

Mục 31.2
* 31.1 (Máy chủ cho mượn) Viết máy chủ cho máy khách. Khách hàng gửi thông tin về
khoản vay (lãi suất hàng năm, số năm và số tiền vay) đến máy chủ (xem Hình
31.17a). Máy chủ tính toán khoản thanh toán hàng tháng và tổng số tiền thanh
toán, và gửi chúng trở lại máy khách (xem Hình 31.17b). Đặt tên cho máy khách
Bài tập31_01Client và máy chủ Bài tập31_01Server.

(Một) (b)

HÌNH 31.17 Khách hàng ở (a) gửi lãi suất hàng năm, số năm và số tiền vay đến máy chủ và nhận khoản thanh toán
hàng tháng và tổng số tiền thanh toán từ máy chủ ở (b).

* 31.2 (Máy chủ BMI) Viết máy chủ cho máy khách. Máy khách gửi cân nặng và chiều cao
của một người đến máy chủ (xem Hình 31.18a). Máy chủ tính toán BMI (Chỉ số
khối cơ thể) và gửi lại cho máy khách một chuỗi báo cáo chỉ số BMI (xem Hình
31.18b). Xem Phần 3.8 để biết BMI tính toán. Đặt tên cho máy khách Bài
tập31_02Client và máy chủ Bài tập31_02Server.

(Một) (b)

HÌNH 31.18 Máy khách trong (a) gửi cân nặng và chiều cao của một người đến máy chủ và nhận BMI từ máy chủ trong
(b).

Phần 31.3 và 31.4


* 31.3 (Máy chủ cho nhiều máy khách) Sửa lại Bài tập lập trình 31.1 để viết một máy chủ
cho nhiều máy khách.

Mục 31.5
31.4 (Đếm máy khách) Viết một máy chủ theo dõi số lượng máy khách được kết nối với
máy chủ. Khi một kết nối mới được thiết lập, số đếm được tăng lên 1. Số
lượng được lưu trữ bằng tệp truy cập ngẫu nhiên. Viết một chương trình khách hàng
Machine Translated by Google

1170, chương 31, kết nối mạng

nhận số lượng từ máy chủ và hiển thị một thông báo, chẳng hạn như Bạn là
số 11, như trong Hình 31.19. Đặt tên cho khách hàng Bài tập31_04Client
và máy chủ Bài tập31_04Server.

HÌNH 31.19 Máy khách hiển thị số lần máy chủ đã được truy cập. Máy chủ lưu trữ số lượng.

31.5 (Gửi thông tin khoản vay trong một đối tượng) Sửa đổi Bài tập 31.1 cho khách
hàng để gửi một đối tượng cho vay có lãi suất hàng năm, số năm và số tiền cho
vay và để máy chủ gửi khoản thanh toán hàng tháng và tổng số tiền thanh toán.

Mục 31.6
31.6 (Hiển thị và thêm địa chỉ) Phát triển ứng dụng máy khách / máy chủ để xem và
thêm địa chỉ, như trong Hình 31.20.

HÌNH 31.20 Bạn có thể xem và thêm địa chỉ.

■ Sử dụng lớp StudentAddress được định nghĩa trong Liệt kê 31.5 để giữ tên, đường phố, thành

phố, tiểu bang và mã zip trong một đối tượng.

■ Người dùng có thể sử dụng các nút Đầu tiên, Tiếp theo, Trước đó và Cuối cùng để xem địa

chỉ và nút Thêm để thêm địa chỉ mới.


■ Giới hạn kết nối đồng thời cho hai máy khách.

Đặt tên cho máy khách Bài tập31_06Client và máy chủ Bài tập31_6Server.

* 31.7 (Chuyển 100 số cuối cùng trong một mảng) Bài tập lập trình 22.12 lấy 100 số
nguyên tố cuối cùng từ tệp PrimeNumbers.dat. Viết chương trình máy khách
yêu cầu máy chủ gửi 100 số nguyên tố cuối cùng trong một mảng. Đặt tên cho
chương trình máy chủ Bài tập31_07 Máy chủ và chương trình máy khách Bài
tập31_07Client. Giả sử rằng các số thuộc kiểu dài được lưu trữ trong
PrimeNumbers.dat ở định dạng nhị phân.

* 31.8 (Chuyển 100 số cuối cùng trong ArrayList) Bài tập lập trình 24.12 lấy 100 số
nguyên tố cuối cùng từ tệp PrimeNumbers.dat. Viết chương trình khách yêu
cầu máy chủ gửi 100 số nguyên tố cuối cùng trong ArrayList. Đặt tên cho
chương trình máy chủ Bài tập31_08 Máy chủ và chương trình máy khách Bài
tập31_08Client. Giả sử rằng các số thuộc kiểu dài được lưu trữ trong
PrimeNumbers.dat ở định dạng nhị phân.
Machine Translated by Google

Bài tập lập trình 1171

Mục 31.7
** 31.9 (Trò chuyện) Viết chương trình cho phép hai người dùng trò chuyện. Triển khai một người dùng

làm máy chủ (Hình 31.21a) và người kia làm máy khách (Hình 31.21b). Máy chủ có hai vùng

văn bản: một vùng để nhập văn bản và vùng còn lại (không thể chỉnh sửa) để hiển thị văn

bản nhận được từ máy khách. Khi người dùng nhấn phím Enter, dòng hiện tại sẽ được gửi
đến máy khách. Máy khách có hai vùng văn bản: một vùng (không thể sử dụng được) để hiển

thị văn bản từ máy chủ và vùng còn lại để nhập văn bản. Khi người dùng nhấn phím Enter,

dòng hiện tại sẽ được gửi đến máy chủ. Đặt tên cho máy khách Bài tập31_09Client và máy
chủ Bài tập31_09Server.

(Một) (b)

HÌNH 31.21 Máy chủ và máy khách gửi văn bản đến và nhận văn bản từ nhau.

*** 31.10 (Trò chuyện với nhiều khách hàng) Viết chương trình cho phép bất kỳ số lượng khách hàng nào có

thể trò chuyện. Triển khai một máy chủ phục vụ tất cả các máy khách, như trong Hình 31.22.
Đặt tên cho máy khách Bài tập31_10Client và máy chủ Bài tập31_10Server.

(Một) (b) (C)

HÌNH 31.22 Máy chủ bắt đầu ở (a) với ba máy khách ở (b) và (c).
Machine Translated by Google

Trang này cố ý để trống

You might also like