Professional Documents
Culture Documents
Cây Tìm Kiếm Binary: Chương
Cây Tìm Kiếm Binary: Chương
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).
■ Để xóa các phần tử khỏi cây tìm kiếm nhị phân (§25.3).
■ Để tạo các trình vòng lặp để duyệt cây nhị phân (§25.5).
cây (§25.6).
Machine Translated by Google
Đ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.
Đ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/
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ử.
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:
public TreeNode (E e)
{element = e;
}
}
Machine Translated by Google
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.
■ 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 hiện tại là null, cây con trống và phần tử không có trong cây (dòng 14).
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
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.
55 100 55 100
cha mẹ cha mẹ
45 57 67 107 45 57 67 107
101 59 101
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
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
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à
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.
«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.
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.
AbstractTree <E>
m 0
TreeNode <E> BST <E mở rộng có thể so sánh được <E>>
+ đườ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.
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
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
158 TreeNode <E> current = root; while xác định vị trí hiện tại
170 }
171
172 if (hiện tại == null) không tìm thấy
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
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?
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
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
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
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
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
nguồn gốc 2
1 4
nguồn gốc
George
3 số 8
Adam Michael 5
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
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.
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
cha mẹ cha mẹ
(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.
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
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
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
cha mẹ cha mẹ
. .
. .
. .
parentOfRightMost parentOfRightMost
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.
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
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
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
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ó.
Peter Peter
Daniel Daniel
Xóa nút này
Peter Peter
Daniel Daniel
Peter Peter
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
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 ()
parentOfRightMost.right = rightMost.left;
Machine Translated by Google
Đ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
HÌNH 25.17 Cây nhị phân được hiển thị bằng đồ thị.
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
// 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}
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
Bộ điều khiển
BSTAnimation
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?
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
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
«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
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ươ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
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.
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
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:
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).
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ì?
Đ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
0 1
0 1 tôi
M 000 1
P 001 2
S 01 4
S 1 4
0 1 tôi
M p
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
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
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
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
Đâ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.
'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ột) (b)
trọng lượng: 11
0 1
'tôi' 'tôi'
0 1
'S' 'S'
0 1
(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ố
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
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ự.
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
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.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ứ?
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
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 ()
* 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.)
** 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:
** 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:
*** 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
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
** 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à
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
* 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
* 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
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:
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
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
(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:
*** 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:
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
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:
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 để
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:
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 để
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:
Độ 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).
■ Để 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
Đ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/
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ử.
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
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
A 2 C 0
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ó
(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
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
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.
m 0
AVLTreeNode <E> AVLTree <E mở rộng có thể so sánh được <E>>
+ 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
-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.
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.
#element: E
#height: int
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
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.
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.
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
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
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
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.
nguồn gốc
parentOfA
MỘT
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?
Đ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.
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
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
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
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
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?
Đ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 .
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
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
46 java.util.ArrayList <TreeNode <E>> path = path (e); for (int i có được con đường
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
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
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
Đầ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
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
Đ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.
47 System.out.println ();
}
48 49}
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
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
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Ì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
HÌNH 26.11 Cây phát triển khi các phần tử bị xóa khỏi cây.
Độ 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
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
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
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,
ĐỐ
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.
* 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
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
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.
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:
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)
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;
}
}
** 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
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).
■ Để 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).
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).
Đ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
.
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
đã 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
đó 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.
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
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,
Đố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
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
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.
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ư
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à:
^ 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à:
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?
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.
độ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
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
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.
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)
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
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
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
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
để 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ố
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 (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
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
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.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,
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,
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;
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ư
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
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
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.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.
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»
+ 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.
Trả về một tập hợp bao gồm các giá trị trong bản đồ này.
+ 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.
-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.
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.
/ ** 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
@Ghi đè
53 public String toString () {
54 return "[" + phím + "," + giá trị + "]";
55 }
56 }
57 58}
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
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
50 kích thước = 0;
51 removeEntries ();
52 }
Machine Translated by Google
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
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
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 đượ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
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
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
isEmpty () O (1)
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
Liệt kê 27.3 đưa ra một chương trình thử nghiệm sử dụng MyHashMap.
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?
Đ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ạ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>
«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.
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)
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.
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}
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
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
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
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
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ử
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 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 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
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
(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.
isEmpty () O (1)
Liệt kê 27.6 đưa ra một chương trình thử nghiệm sử dụng MyHashSet.
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):
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.
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
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.
** 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
** 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:
** 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.
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
■ Để mô tả các thuật ngữ đồ thị: đỉnh, cạnh, đồ thị đơn giản, đồ thị có trọng số / không
■ Để 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,
■ Để biểu diễn đường đi ngang của đồ thị bằng cách sử dụng lớp AbstractGraph.Tree (§28.6).
■ Để 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
Đ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)
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
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
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
Đ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
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:
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.
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)
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
đồ 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
.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.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?
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ị
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 đồ
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:
public City (String cityName, int dân, String thị trưởng) { this.cityName
= cityName; this.population = dân số; this.mayor = thị trưởng;
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.
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
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
int [] [] edge = {{0, 2}, {1, 2}, {2, 4}, {3, 4}};
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:
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ề.
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
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
Ma trận kề cho đồ thị có hướng trong Hình 28.3a có thể được biểu diễn như sau:
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à,
Để 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:
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à,
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ề
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
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)
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)
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
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
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
1 4
0 3
2 5
Đ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
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
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
Đồ thị <V>
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.
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>
#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
#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
#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
#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à
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 (đỉ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
+ 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
+ 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à
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
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.
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
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
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.
/ ** 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
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
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
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
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
constructor 19 public UnweightedGraph (Danh sách các cạnh <Edge>, int numberOfVertices) {
20 siêu (cạnh, numberOfVertices);
21 }
22
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.
Đ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
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ư
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
Để 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.
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.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 ();
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
Đ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
+ Tree (root: int, parent: int [], Tạo cây với gốc, gốc, và
searchOrder: List <Integer>) + searchOrder.
+ 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.
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
28.12 Bạn sử dụng phương pháp nào để tìm đỉnh cha của đỉnh trong cây?
Đ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
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 đồ.
}
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
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
0 1 0 1 0 1
2 2 2
3 4 3 4 3 4
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ó.
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
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.
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
■ 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
đồ 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.15 Vẽ cây DFS cho đồ thị trong Hình 28.1 bắt đầu từ đỉnh Atlanta.
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.
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
24 if (! isInsideACircle (new Point2D (e.getX (), e.getY ()))) { nó nằm bên trong một vòng tròn khác?
28 }
29 });
30 }
Machine Translated by Google
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
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
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
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?
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
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?
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
0 1 0 1 0 1
2 2 2
3 4 3 4 3 4
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à
(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
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.
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
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
■ 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
■ 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.
■ 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
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
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
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
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,
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
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
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
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.
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
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
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
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
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
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
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
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.
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.
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
(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:
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
* 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:
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.
* 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
* 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
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ó
** 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
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.
** 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)
** 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
** 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)
** 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:
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 :
*** 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
** 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
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
** 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
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).
(§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
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
Seattle (0)
599
San Francisco (1)
1015 888
Thành phố Kansas (4)
381 1663
864
810
Dallas (10)
239
661
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
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.
Đ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ố.
đồ 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:
};
Machine Translated by Google
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.
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
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ị
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.
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,
Đố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ó
danh sách [3] WeightedEdge (3, 1, 3) WeightedEdge (3, 2, 4) WeightedEdge (3, 4, 6) WeightedEdge (3, 0, 8)
Để 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
Đ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
«Giao diện»
Đồ thị <V>
AbstractGraph <V>
WeightedGraph <V>
+ 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.
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.
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
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
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
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í
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
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
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 đồ
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
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
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ì
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?
Đ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.
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ì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
}
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
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
đượ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
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
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,
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
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
12 }
13 }
14}
AbstractGraph.Tree
WeightedGraph.MST
+ 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.
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
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
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
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
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
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
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
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
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?
Đư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
đườ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
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
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ị,
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)
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
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)
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
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
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
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
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.
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
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
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.
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ị
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}
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
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?
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
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
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ư
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 đề.
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.
// 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
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
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}
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?
đồ 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
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.
* 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
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 :
* 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 | ....
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
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
để 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:
(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ử
để 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
* 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
các đỉnh. Ví dụ, đối với đồ thị trong Hình 29.23, đường đi ngắn nhất giữa 0
* 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
*** 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
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á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
*** 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
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:
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
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
*** 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
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.
tree1.printTree ();
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).
■ Để điều khiển luồng bằng các phương thức trong lớp Luồng ( §30.4 ).
■ 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
■ Để 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).
■ Để hạn chế số lượng tác vụ đồng thời truy cập vào một
■ Để 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
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ể
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
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?
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) (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ụ,
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ụ
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.
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ươ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
đượ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?
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 ();
}
(Một) (b)
Machine Translated by Google
«Giao diện»
java.lang.Runnable
java.lang.Thread
+ 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.
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
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
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
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:
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.
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:
thread3.setPosystem (Thread.MAX_PRIORITY);
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
Đ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.
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
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
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:
Thread.sleep (200);
}
}
bắt (Ngoại lệ bị gián đoạn) {
}
}).khởi đầu();
30.12 Bạn có thể thay thế mã ở dòng 27–32 bằng mã sau không?
30.13 Điều gì xảy ra nếu dòng 34 (Thread.sleep (200)) không được sử dụng?
Đ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
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
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ả
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.
// 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.
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
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.
Đ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
«Giao diện»
java.lang.Runnable
100 1 1 1
AddAPennyTask AccountWithoutSync Tài khoản
HÌNH 30.9 AccountWithoutSync chứa một phiên bản của Account và 100 luồng của AddAPennyTask.
số 8
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
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ả
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.
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
Ở 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.
đị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
để đồ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
Mở khóa
Mở khóa
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
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
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
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):
}
}
(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
Nó sẽ hoạt động?
Đ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
«Giao diện»
java.util.concurrent.locks.Lock
+ 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
java.util.concurrent.locks.ReentrantLock
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.
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, để
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?
Đ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
«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
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ẽ
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
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?
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
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
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 ?
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.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
Thêm một số nguyên vào bộ đệm Xóa một int khỏi bộ đệm
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.
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
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
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?
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
«Giao diện»
java.util.concurrent.BlockingQueue <E>
+ 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
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).
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.
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
Để 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 .
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
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?
Đô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.
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ó
30,33 Bế tắc là gì? Làm thế nào bạn có thể tránh bế tắc?
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
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
Mục tiêu
hoàn thành
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
30.34 Trạng thái luồng là gì? Mô tả các trạng thái cho một chủ đề.
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
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
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.
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.
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
30.36 Giải thích tại sao một trình lặp không nhanh.
Đ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 để
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
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
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
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ự.
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
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
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
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:
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.
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
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ụ
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ả
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?
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
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
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
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.
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
ĐỐ
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.
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,
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
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
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
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
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.
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
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
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.
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
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
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
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.
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
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).
■ Để 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
Đ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
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.
Đ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ó
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.
ổ 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
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:
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:
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
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:
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
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
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
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
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ủ.
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.
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:
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:
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
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
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ạ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.
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
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
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):
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):
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).
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
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
số cổ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?
Đô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.
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:
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.
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.
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?
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
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:
Ổ 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
Máy chủ
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.
77 socket.getInputStream ());
78 DataOutputStream outputToClient = new DataOutputStream (
79 socket.getOutputStream ());
80
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?
Đ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
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ủ.
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
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ủ.
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ủ
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
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
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
Đố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,
Máy chủ
...
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-
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:
■ 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ủ.
Xử lý một phiên:
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.
7. Nếu DRAW, trò chơi hiển thị kết thúc; vỡ vòng lặp.
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
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.
■ Ô 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
Mối quan hệ của các lớp này được thể hiện trong Hình 31.14.
TicTacToeConstants
Tương tự với
Liệt kê 18.10
Runnable
TicTacToeServer Xử lý TicTacToeClient
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
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
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?
31.6 Nghiên cứu điển hình: Trò chơi Tic-Tac-Toe được phân phối 1163
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
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 }
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
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
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ì?
ổ 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
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
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).
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
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.
■ Sử dụng lớp StudentAddress được định nghĩa trong Liệt kê 31.5 để giữ tên, đường phố, thành
■ 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
Đặ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
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.
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