Java 高级主题(一)
原文:Advanced Topics in Java: Core Concepts in Data Structures
零、前言
这本书假设你有基本编程概念的工作知识,如变量、常数、赋值、选择(如果...else),以及循环(while,for)。它还假设您熟悉编写函数和使用数组。如果你不是,建议你先学习 Java 编程:初学者课程 ( www.amazon.com/Java-Programming-Beginners-Noel-Kalicharan/dp/1438265182/)或任何其他介绍 Java 编程的书籍,然后再阅读本书中的内容。
重点不是教授 Java 语言的高级概念,而是使用 Java 来教授任何有抱负的程序员都应该知道的高级编程概念。涵盖的主要主题包括基本排序方法(选择、插入)、搜索(顺序、二进制)、合并、指针(在 Java 中称为引用)、链表、栈、队列、递归、随机数、文件(文本、二进制、随机访问、索引)、二叉树、高级排序方法(堆排序、快速排序、合并排序、外壳排序)和哈希(一种非常快速的搜索方法)。
第一章复习一些你应该知道的基本概念。它处理使用选择和插入排序对列表进行排序,使用顺序和二分搜索法搜索列表,以及合并两个有序列表。
Java 被认为是一种面向对象的编程语言。其存在的核心是类和对象的概念。第二章详细介绍了这些概念。
第三章处理链表——它本身是一种重要的数据结构,但也是树和图等更高级结构的基础。我们将解释如何创建一个链表,如何添加和删除一个链表,如何建立一个排序链表,以及如何合并两个排序链表。
第四章专门讨论栈和队列,也许是最有用的线性列表。它们在计算机科学中有重要的应用。我们将向您展示如何使用数组和链表实现栈和队列,以及如何通过将算术表达式转换为后缀形式来对其求值。
第五章介绍了一种强大的编程方法——递归。毫无疑问,递归需要一点时间来适应。但是,一旦掌握了,你将能够解决一个全新的世界的问题,这些问题用传统的非递归技术很难解决。在其他有趣的问题中,我们将向您展示如何解决汉诺塔问题以及如何逃离迷宫。
我们都喜欢玩游戏。但是在这些玩游戏的程序中潜伏着什么呢?随机数。第六章向你展示如何使用随机数来玩一些简单的游戏,模拟现实生活中的情况。我们将解释如何编写一个算术训练程序,如何玩 Nim 的完美游戏,以及如何模拟在超市收银台或银行排队,等等。我们还将讨论随机数的一种新用途——估计数值,如π (pi)。
几乎所有我们需要存储在计算机上的东西都必须存储在一个文件中。我们使用文本文件来存储我们用文本编辑器或文字处理器创建的各种文档。我们使用二进制文件来存储照片图像文件、声音文件、视频文件和记录文件。第七章展示了如何创建和操作文本和二进制文件。它还解释了如何处理两种最有用的文件——随机存取和索引文件。
第八章介绍了最通用的数据结构——二叉树。二叉树结合了数组和链表的优点而没有缺点。例如,二叉查找树允许我们在“二分搜索法时间”内进行搜索(就像一个排序数组一样),并利用链表的功能进行插入/删除。
第一章中讨论的排序方法(选择、插入)很简单,因为它们完成了工作,但是如果给定大量的项目(比如一百万个)要排序,就会很慢。例如,他们将需要大约六天的时间(!)在每秒能处理一百万次比较的计算机上对一百万个项目进行排序。第九章讨论了一些更快的排序方法——堆排序、快速排序和 Shell 排序。Heapsort 和 quicksort 可以在不到一分钟的时间内对同一台计算机上的百万个项目进行排序,而 Shell sort 则需要一分多钟。
第十章介绍哈希,这是最快的搜索方法之一。它涵盖了哈希基础知识,并讨论了解决冲突的几种方法,这是任何哈希算法性能的关键。
我们的目标是提供对高级编程技术的良好掌握,以及对重要数据结构(链表、栈、队列、二叉树)的基本理解,以及如何在 Java 中实现它们。我们希望这将激起你对计算机科学这一令人兴奋的重要领域进行更深入研究的兴趣。
许多练习要求你写一个程序。不回答这些问题是故意的。我的经验是,在我们的快餐文化中,当有答案时,学生不会花必要的时间去找出解决方案。无论如何,编程练习的目的是让你编写程序。
编程是一个迭代的过程。当您编译并运行您的解决方案时,您将知道它们是否正确。如果不是这样,你必须找出程序不工作的原因,进行改进,然后再试一次。学好编程的唯一方法就是写程序解决新问题。提供答案会缩短这个过程,没有任何好处。
Java 语言的高级主题中的程序可以用 5.0 及更高版本的 Java 开发工具包(JDK)编译。这些程序是独立的。例如,它们不需要有人提供一个类来完成基本的输入/输出。它们将“开箱即用”
本书中所示示例的代码可在 Apress 网站www.apress.com上获得。在该书的信息页面上的源代码/下载选项卡上可以找到一个链接。该选项卡位于页面的相关标题部分。
感谢您花时间阅读和研究这本书。我们相信您会喜欢在这里的经历,并且能够以一种轻松、愉快、愉快和有益的方式继续您的编程之旅。
—诺埃尔·卡利查兰
一、排序、搜索和合并
在本章中,我们将解释以下内容:
- 如何使用选择排序对项目列表进行排序
- 如何使用插入排序对项目列表进行排序
- 如何向排序列表中添加一个新项目,使列表保持排序
- 如何对字符串数组进行排序
- 如何对相关(并行)数组进行排序
- 如何使用二分搜索法搜索排序列表
- 如何搜索字符串数组
- 如何写一个程序来计算一篇文章中单词的频率
- 如何合并两个排序列表以创建一个排序列表
1.1 排序一个数组:选择排序
S orting 是将一组数值按升序或降序排列的过程。排序的原因有很多。有时我们排序是为了产生更可读的输出(例如,产生一个按字母顺序排列的列表)。教师可能需要按姓名或平均分对学生进行排序。如果我们有一个很大的值集,并且我们想要识别重复项,我们可以通过排序来实现;重复的值将一起出现在排序列表中。
排序的另一个优点是,对于排序后的数据,一些操作可以更快更有效地执行。例如,如果数据已经排序,可以使用二分搜索法搜索,这比使用顺序搜索要快得多。此外,合并两个单独的项目列表可以比列表未排序时快得多。
排序的方式有很多种。在这一章中,我们将讨论两种“简单”的方法:选择和插入排序。在第九章中,我们将会看到更复杂的排序方式。我们从选择排序开始。
考虑以下存储在 Java 数组中的数字列表,num :
使用选择排序按升序对num进行排序的过程如下:
1STT5pass
-
从位置
0到6找到整个列表中最小的数字;最小的是15,位于4位置。 -
Interchange the numbers in positions
0and4. This gives us the following:
22通过
-
找出位置
1到6的最小数字;最小的是33,位于5位置。 -
Interchange the numbers in positions
1and5. This gives us the following:
3rdpass
-
找出位置
2到6的最小数字;最小的是48,位于5位置。 -
Interchange the numbers in positions
2and5. This gives us the following:
4 次3 次 5 次过关
-
找出位置
3到6的最小数字;最小的是52,位于6位置。 -
Interchange the numbers in positions
3and6. This gives us the following:
5 次3 次 过关
-
找出位置
4到6的最小数字;最小的是57,位于4位置。 -
Interchange the numbers in positions
4and4. This gives us the following:
6 次3 次 过关
-
找出位置
5到6的最小数字;最小的是65,位于6位置。 -
Interchange the numbers in positions
5and6. This gives us the following:
数组现在已经完全排序了。注意,一旦第 6 个最大的被放置在其最终位置(5),最大的(79)将自动处于最后位置(6)。
在本例中,我们进行了六次传递。我们将通过让变量h从0到5来计算这些通道。在每一遍中,我们找到从位置h到6的最小数字。如果最小的数字在位置s,我们交换位置h和s的数字。
一般来说,对于大小为n的数组,我们进行n-1遍。在我们的例子中,我们对6通道中的7号进行了排序。下面是排序num[0..n-1] 的算法的伪代码概要:
for h = 0 to n - 2
s = position of smallest number from num[h] to num[n-1]
swap num[h] and num[s]
endfor
我们可以使用通用参数list如下实现该算法:
public static void selectionSort(int[] list, int lo, int hi) {
//sort list[lo] to list[hi] in ascending order
for (int h = lo; h < hi; h++) {
int s = getSmallest(list, h, hi);
swap(list, h, s);
}
}
for循环中的两个语句可以替换为以下内容:
swap(list, h, getSmallest(list, h, hi));
我们可以把getSmallest和swap写成:
public static int getSmallest(int list[], int lo, int hi) {
//return location of smallest from list[lo..hi]
int small = lo;
for (int h = lo + 1; h <= hi; h++)
if (list[h] < list[small]) small = h;
return small;
}
public static void swap(int list[], int i, int j) {
//swap elements list[i] and list[j]
int hold = list[i];
list[i] = list[j];
list[j] = hold;
}
为了测试selectionSort是否正常工作,我们编写程序 P1.1 。仅显示了main。要完成程序,只需添加selectionSort、getSmallest和swap。
程序 p 1.1
import java.util.*;
public class SelectSortTest {
final static int MaxNumbers = 10;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int[] num = new int[MaxNumbers];
System.out.printf("Type up to %d numbers followed by 0\n", MaxNumbers);
int n = 0;
int v = in.nextInt();
while (v != 0 && n < MaxNumbers) {
num[n++] = v;
v = in.nextInt();
}
if (v != 0) {
System.out.printf("\nMore than %d numbers entered\n", MaxNumbers);
System.out.printf("First %d used\n", MaxNumbers);
}
if (n == 0) {
System.out.printf("\nNo numbers supplied\n");
System.exit(1);
}
//n numbers are stored from num[0] to num[n-1]
selectionSort(num, 0, n-1);
System.out.printf("\nThe sorted numbers are\n");
for (v = 0; v < n; v++) System.out.printf("%d ", num[v]);
System.out.printf("\n");
} //end main
// selectionSort, getSmallest and swap go here
} //end class SelectSortTest
程序请求最多 10 个数字(由MaxNumbers定义),将它们存储在数组num中,调用selectionSort,然后打印排序后的列表。
以下是该程序的运行示例:
Type up to 10 numbers followed by 0
57 48 79 65 15 33 52 0
The sorted numbers are
15 33 48 52 57 65 79
请注意,如果用户输入了十个以上的数字,程序会识别出来,并且只对前十个进行排序。
1.1.1 选择排序分析
为了找到最小的 k 项,我们进行 k -1 比较。在第一遍中,我们进行 n -1 次比较,以找到 n 项中最小的一项。在第二遍中,我们进行第 n -2 次比较,以找到第 n -1 项中最小的一项。以此类推,直到最后一遍,我们进行一次比较,找出两个项目中较小的一个。一般来说,在第 j 遍时,我们进行 n-j 比较,以找到 n-j +1 项中最小的一项。因此,我们有了这个:
总比较次数= 1+2+…+n-1 =n(n-1)≈n2
我们说选择排序的顺序是 O( n 2 )(“大 on 的平方”)。常数在“大 O”符号中并不重要,因为随着 n 变得非常大,常数变得无关紧要。
每一次,我们用三个任务交换两个项目。因为我们做了 n -1 遍,所以我们总共做了 3( n -1)次分配。使用“大 O”符号,我们说赋值的个数是 O( n )。常数3和1并不重要,因为 n 变大了。
如果数据是有序的,选择排序的性能会更好吗?不。一种方法是给它一个排序列表,看看它做什么。如果你完成了这个算法,你会发现这个方法不考虑数据的顺序。不管数据如何,它每次都会进行相同次数的比较。
正如我们将看到的,一些排序方法(mergesort 和 quicksort 参见第五章和第九章需要额外的数组存储来实现它们。请注意,选择排序是在给定数组中“就地”执行的,不需要额外的存储。
作为一个练习,修改程序代码,使其计算使用选择排序对列表进行排序时的比较和赋值次数。
1.2 数组排序:插入排序
考虑与之前相同的阵列:
现在,把数字想象成桌子上的卡片,按照它们在数组中出现的顺序一次拿一张。因此,我们首先拿起57,然后是48,然后是79,以此类推,直到我们拿起52。然而,当我们拿起每一个新的数字时,我们把它加到我们手上,这样我们手上的数字都被排序了。
当我们拿起57时,我们手中只有一个数字。我们认为有一个数字需要排序。
当我们拿起48时,我们将它添加到57前面,因此我们的手包含以下内容:
48 57
当我们拿起79时,我们把它放在57之后,所以我们的手包含这个:
48 57 79
当我们拿起65时,我们把它放在57之后,所以我们的手包含这个:
48 57 65 79
在这个阶段,四个数字已经被捡起来了,我们的手按排序顺序包含它们。
当我们拿起15时,我们把它放在48之前,所以我们的手包含这个:
15 48 57 65 79
当我们拿起33时,我们把它放在15之后,所以我们的手包含这个:
15 33 48 57 65 79
最后,当我们拿起52时,我们把它放在48之后,所以我们的手包含这个:
15 33 48 52 57 65 79
这些数字已经按升序排序。
所描述的方法说明了插入排序 背后的思想。从左到右,一次处理一个数组中的数字。这相当于从表中一次选取一个数字。由于第一个数字本身是已排序的,我们将从第二个数字开始处理数组中的数字。
当我们开始处理num[h]时,我们可以假设num[0]到num[h-1]被排序。我们在num[0]到num[h-1]中插入num[h],这样num[0]到num[h]就被排序了。然后我们继续处理num[h+1]。当我们这样做时,我们的假设num[0]到num[h]被排序将为真。
使用插入排序按升序对num排序的过程如下:
1STT5pass
-
Process
num[1], that is,48. This involves placing48so that the first two numbers are sorted;num[0]andnum[1]now contain the following:
数组的其余部分保持不变。
22通过
-
Process
num[2], that is,79. This involves placing79so that the first three numbers are sorted;num[0]tonum[2]now contain the following:
数组的其余部分保持不变。
3rdpass
-
Process
num[3], that is,65. This involves placing65so that the first four numbers are sorted;num[0]tonum[3]now contain the following:
数组的其余部分保持不变。
4 次3 次 5 次过关
-
Process
num[4], that is,15. This involves placing15so that the first five numbers are sorted. To simplify the explanation, think of 15 as being taken out and stored in a simple variable (key, say) leaving a “hole” innum[4]. We can picture this as follows:
将15插入其正确位置的过程如下:
-
Compare
15with79; it is smaller, so move79to location4, leaving location3free. This gives the following: -
Compare
15with65; it is smaller, so move65to location3, leaving location2free. This gives the following: -
Compare
15with57; it is smaller, so move57to location2, leaving location1free. This gives the following: -
Compare
15with48; it is smaller, so move48to location1, leaving location0free. This gives the following: -
There are no more numbers to compare with
15, so it is inserted in location0, giving the following: -
我们可以把
15(key)的摆放逻辑用它和它左边的数字比较来表达,从最近的一个开始。只要key小于num[k],对于某些k,我们就把num[k]移到num[k + 1]位置,继续考虑num[k-1],前提是它存在。当k实际上是0的时候就不会存在了。在这种情况下,过程停止,并且key插入位置0。
5 次3 次 过关
-
流程
num[5],即33。这包括放置33,以便对前六个数字进行排序。这是按如下方式完成的: -
将
33存储在key中,留下位置5空闲。 -
比较
33和79;它变小了,所以把79移到位置5,留下位置4空闲。 -
比较
33和65;它变小了,所以把65移到位置4,留下位置3空闲。 -
比较
33和57;它变小了,所以把57移到位置3,留下位置2空闲。 -
比较
33和48;它变小了,所以把48移到位置2,留下位置1空闲。 -
Compare
33with15; it is bigger, so insert33in location1. This gives the following: -
我们可以通过与它左边的数字比较来表达放置
33的逻辑,从最近的一个开始。只要key小于num[k],对于某些k,我们就把num[k]移到位置num[k + 1],继续考虑num[k-1],前提是它存在。如果某些k的key大于或等于num[k],则key插入k+1位置。这里,33大于num[0],所以插入num[1]。
6 次3 次 过关
-
流程
num[6],即52。这包括放置52,以便对前七个(所有)数字进行排序。这是按如下方式完成的: -
将
52存储在key中,留下位置6空闲。 -
比较
52和79;它变小了,所以把79移到位置6,留下位置5空闲。 -
比较
52和65;它变小了,所以把65移到位置5,留下位置4空闲。 -
比较
52和57;它变小了,所以把57移到位置4,留下位置3空闲。 -
Compare
52with48; it is bigger; so insert52in location3. This gives the following:
数组现在已经完全排序了。
以下是如何使用插入排序对数组num的前n个元素进行排序的概述:
for h = 1 to n - 1 do
insert num[h] among num[0] to num[h-1] so that num[0] to num[h] are sorted
endfor
使用这个大纲,我们使用参数list 编写函数insertionSort。
public static void insertionSort(int list[], int n) {
//sort list[0] to list[n-1] in ascending order
for (int h = 1; h < n; h++) {
int key = list[h];
int k = h - 1; //start comparing with previous item
while (k >= 0 && key < list[k]) {
list[k + 1] = list[k];
--k;
}
list[k + 1] = key;
} //end for
} //end insertionSort
while语句是该排序的核心。它声明,只要我们在数组(k >= 0)中,并且当前数字(key)小于数组(key < list[k])中的数字,我们就将list[k]向右移动(list[k+1] = list[k]),并继续移动到左边的下一个数字(--k)。
对于某些k,如果k等于-1或者如果key大于或等于list[k],我们退出while循环。无论哪种情况,key都被插入到list[k+1]中。
如果k为-1,则表示当前数字小于列表中所有之前的数字,必须插入到list[0]中。但是list[k + 1] 是 list[0]当k是-1时,那么key在这种情况下是正确插入的。
该函数按升序排序。要按降序排序,我们所要做的就是将while条件中的<改为>,就像这样:
while (k >= 0 && key > list[k])
现在,如果比大,一个键会向左移动。
我们编写程序 P1.2 来测试insertionSort是否正常工作。仅显示了main。添加功能insertionSort完成程序。
程序 P1.2
import java.util.*;
public class InsertSortTest {
final static int MaxNumbers = 10;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int[] num = new int[MaxNumbers];
System.out.printf("Type up to %d numbers followed by 0\n", MaxNumbers);
int n = 0;
int v = in.nextInt();
while (v != 0 && n < MaxNumbers) {
num[n++] = v;
v = in.nextInt();
}
if (v != 0) {
System.out.printf("\nMore than %d numbers entered\n", MaxNumbers);
System.out.printf("First %d used\n", MaxNumbers);
}
if (n == 0) {
System.out.printf("\nNo numbers supplied\n");
System.exit(1);
}
//n numbers are stored from num[0] to num[n-1]
insertionSort(num, n);
System.out.printf("\nThe sorted numbers are\n");
for (v = 0; v < n; v++) System.out.printf("%d ", num[v]);
System.out.printf("\n");
} //end main
public static void insertionSort(int list[], int n) {
//sort list[0] to list[n-1] in ascending order
for (int h = 1; h < n; h++) {
int key = list[h];
int k = h - 1; //start comparing with previous item
while (k >= 0 && key < list[k]) {
list[k + 1] = list[k];
--k;
}
list[k + 1] = key;
} //end for
} //end insertionSort
} //end class InsertSortTest
程序请求最多十个数字(由MaxNumbers定义),将它们存储在数组num中,调用insertionSort,然后打印排序后的列表。
以下是该程序的运行示例:
Type up to 10 numbers followed by 0
57 48 79 65 15 33 52 0
The sorted numbers are
15 33 48 52 57 65 79
请注意,如果用户输入了十个以上的数字,程序会识别出来,并且只对前十个进行排序。
我们可以很容易地推广insertionSort来排序列表的部分。为了举例说明,我们重写了insertionSort(称之为insertionSort1)来将list[lo]排序为list[hi],其中lo和hi作为参数传递给函数。
由于元素lo是第一个,我们从lo+1开始处理元素,直到元素hi。这在for语句中有所体现。还有现在,下标最低的是lo,而不是0。这反映在while状态k >= lo中。其他一切都和以前一样。
public static void insertionSort1(int list[], int lo, int hi) {
//sort list[lo] to list[hi] in ascending order
for (int h = lo + 1; h <= hi; h++) {
int key = list[h];
int k = h - 1; //start comparing with previous item
while (k >= lo && key < list[k]) {
list[k + 1] = list[k];
--k;
}
list[k + 1] = key;
} //end for
} //end insertionSort1
我们可以用程序 P1.2a 来测试insertionSort1。
程序 p 1.2a
import java.util.*;
public class InsertSort1Test {
final static int MaxNumbers = 10;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int[] num = new int[MaxNumbers];
System.out.printf("Type up to %d numbers followed by 0\n", MaxNumbers);
int n = 0;
int v = in.nextInt();
while (v != 0 && n < MaxNumbers) {
num[n++] = v;
v = in.nextInt();
}
if (v != 0) {
System.out.printf("\nMore than %d numbers entered\n", MaxNumbers);
System.out.printf("First %d used\n", MaxNumbers);
}
if (n == 0) {
System.out.printf("\nNo numbers supplied\n");
System.exit(1);
}
//n numbers are stored from num[0] to num[n-1]
insertionSort1(num, 0, n-1);
System.out.printf("\nThe sorted numbers are\n");
for (v = 0; v < n; v++) System.out.printf("%d ", num[v]);
System.out.printf("\n");
} //end main
// insertionSort1 goes here
} //end class InsertSort1Test
1.2.1 插入排序分析
在处理项目 j 时,我们可以少做 1 次比较(如果num[j]大于num[j-1])或多做 j -1 次比较(如果num[j]小于前面所有项目)。对于随机数据,我们期望平均进行( j -1)次比较。因此,对 n 项进行排序的平均总比较次数为:
我们说插入排序的顺序是 O(n 2 )(“大 O ^ n 的平方”)。当 n 变大时,常数并不重要。
每次我们做一个比较,我们也做一个分配。因此,分配的总数也是 n(n-1) ≈ n 2 。
我们强调这是随机数据的平均值。与选择排序不同,插入排序的实际性能取决于所提供的数据。如果给定的数组已经排序,插入排序将通过进行 n-1 次比较来快速确定这一点。在这种情况下,它以 O(n)时间运行。人们会认为,数据中的顺序越多,插入排序的性能就越好。
如果给定的数据是降序排列的,插入排序的性能最差,因为每个新数字都必须一直移动到列表的开头。在这种情况下,比较的次数是 n(n-1) ≈ n 2 。分配数也是 n(n-1) ≈ n 2 。
因此,通过插入排序进行比较的次数从 n-1(最佳)到 n 2 (平均)到 n 2 (最差)。赋值的次数总是与比较的次数相同。
与选择排序一样,插入排序的实现不需要额外的数组存储。
作为一个练习,修改程序代码,使其计算使用插入排序对列表进行排序时的比较和赋值次数。
1.3 在 处插入一个元素
插入排序使用向已经排序的列表中添加新元素的思想,以便列表保持排序。我们可以把它本身当作一个问题(与插入排序无关)。具体来说,给定一个从list[m]到list[n]的排序列表,我们想要向列表中添加一个新的条目(比如说newItem),以便对list[m]到list[n+1]进行排序。
添加新项目会使列表的大小增加 1。我们假设数组有空间容纳新的项目。我们编写函数insertInPlace来解决这个问题。
public static void insertInPlace(int newItem, int list[], int m, int n) {
//list[m] to list[n] are sorted
//insert newItem so that list[m] to list[n+1] are sorted
int k = n;
while (k >= m && newItem < list[k]) {
list[k + 1] = list[k];
--k;
}
list[k + 1] = newItem;
} //end insertInPlace
使用insertInPlace,我们可以将insertionSort(称之为insertionSort2)重写如下:
public static void insertionSort2(int list[], int lo, int hi) {
//sort list[lo] to list[hi] in ascending order
for (int h = lo + 1; h <= hi; h++)
insertInPlace(list[h], list, lo, h - 1);
} //end insertionSort2
我们可以用程序 P1.2b 来测试insertionSort2。
程序 p 1.2b
import java.util.*;
public class InsertSort2Test {
final static int MaxNumbers = 10;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int[] num = new int[MaxNumbers];
System.out.printf("Type up to %d numbers followed by 0\n", MaxNumbers);
int n = 0;
int v = in.nextInt();
while (v != 0 && n < MaxNumbers) {
num[n++] = v;
v = in.nextInt();
}
if (v != 0) {
System.out.printf("\nMore than %d numbers entered\n", MaxNumbers);
System.out.printf("First %d used\n", MaxNumbers);
}
if (n == 0) {
System.out.printf("\nNo numbers supplied\n");
System.exit(1);
}
//n numbers are stored from num[0] to num[n-1]
insertionSort2(num, 0, n-1);
System.out.printf("\nThe sorted numbers are\n");
for (v = 0; v < n; v++) System.out.printf("%d ", num[v]);
System.out.printf("\n");
} //end main
public static void insertionSort2(int list[], int lo, int hi) {
//sort list[lo] to list[hi] in ascending order
for (int h = lo + 1; h <= hi; h++)
insertInPlace(list[h], list, lo, h - 1);
} //end insertionSort2
public static void insertInPlace(int newItem, int list[], int m, int n) {
//list[m] to list[n] are sorted
//insert newItem so that list[m] to list[n+1] are sorted
int k = n;
while (k >= m && newItem < list[k]) {
list[k + 1] = list[k];
--k;
}
list[k + 1] = newItem;
} //end insertInPlace
} //end class InsertSort2Test
1.4 对字符串数组进行排序
考虑按字母顺序排列姓名列表的问题。在 Java 中,一个名字存储在一个String变量中,我们需要一个String数组来存储这个列表。在大多数情况下,我们可以像处理一个原始类型一样处理String,但是有时候记住严格来说,它是一个类是很有用的。在必要的地方,我们会指出区别。
这里我们关心的一个区别是,我们不能使用关系运算符(==、<、>等等)来比较字符串。我们必须使用String类中的函数(或者自己编写)。常用功能有equals、equalsIgnoreCase、compareTo、compareToIgnoreCase。我们编写一个函数,使用插入排序对字符串数组进行排序。我们称之为insertionSort3。
public static void insertionSort3(String[] list, int lo, int hi) {
//sort list[lo] to list[hi] in ascending order
for (int h = lo + 1; h <= hi; h++) {
String key = list[h];
int k = h - 1; //start comparing with previous item
while (k >= lo && key.compareToIgnoreCase(list[k]) < 0) {
list[k + 1] = list[k];
--k;
}
list[k + 1] = key;
} //end for
} //end insertionSort3
除了声明list和使用compareToIgnoreCase来比较两个字符串之外,该函数与前面的函数非常相似。如果情况紧急,你可以使用compareTo。
我们用程序 P1.3 测试insertionSort3。
程序 P1.3
import java.util.*;
public class SortStrings {
final static int MaxNames = 8;
public static void main(String[] args) {
String name[] = {"Graham, Ariel", "Perrott, Chloe",
"Charles, Kandice", "Seecharan, Anella", "Reyes, Aaliyah",
"Graham, Ashleigh", "Reyes, Ayanna", "Greaves, Sherrelle" };
insertionSort3(name, 0, MaxNames - 1);
System.out.printf("\nThe sorted names are\n\n");
for (int h = 0; h < MaxNames; h++)
System.out.printf("%s\n", name[h]);
} //end main
// insertionSort3 goes here
} //end class SortStrings
运行时,程序 P1.3 产生如下输出:
The sorted names are
Charles, Kandice
Graham, Ariel
Graham, Ashleigh
Greaves, Sherrelle
Perrott, Chloe
Reyes, Aaliyah
Reyes, Ayanna
Seecharan, Anella
1.5 排序并行数组
在不同的数组中有相关的信息是很常见的。例如,假设除了name,我们还有一个整数数组id,使得id[h]是与name[h]关联的标识号,如图图 1-1 所示。
图 1-1 。包含相关信息的两个数组
考虑按字母顺序排列名字的问题。最后,我们希望每个名字都有正确的 ID 号。因此,例如,在排序完成后,name[0]应该包含“查尔斯,坎蒂斯”,id[0]应该包含4455。
为此,在排序过程中,每次移动一个姓名时,相应的 ID 号也必须移动。因为姓名和 ID 号必须“并行”移动,所以我们说我们正在进行“并行排序”或者我们正在排序“并行数组”。
我们重写insertionSort3来说明如何对并行数组进行排序。我们只需添加代码,以便在移动名称时移动 ID。我们称之为parallelSort。
public static void parallelSort(String[] list, int id[], int lo, int hi) {
//Sort the names in list[lo] to list[hi] in alphabetical order,
//ensuring that each name remains with its original id number.
for (int h = lo + 1; h <= hi; h++) {
String key = list[h];
int m = id[h]; // extract the id number
int k = h - 1; //start comparing with previous item
while (k >= lo && key.compareToIgnoreCase(list[k]) < 0) {
list[k + 1] = list[k];
id[k+ 1] = id[k]; //move up id number when we move a name
--k;
}
list[k + 1] = key;
id[k + 1] = m; //store the id number in the same position as the name
} //end for
} //end parallelSort
我们通过编写程序 P1.4 来测试parallelSort。
程序 P1.4
import java.util.*;
public class ParallelSort {
final static int MaxNames = 8;
public static void main(String[] args) {
String name[] = {"Graham, Ariel", "Perrott, Chloe",
"Charles, Kandice", "Seecharan, Anella", "Reyes, Aaliyah",
"Graham, Ashleigh", "Reyes, Ayanna", "Greaves, Sherrelle" };
int id[] = {3050,2795,4455,7824,6669,5000,5464,6050};
parallelSort(name, id, 0, MaxNames - 1);
System.out.printf("\nThe sorted names and IDs are\n\n");
for (int h = 0; h < MaxNames; h++)
System.out.printf("%-20s %d\n", name[h], id[h]);
} //end main
// parallelSort goes here
} //end class ParallelSort
当程序 P1.4 运行时,它产生以下输出:
The sorted names and IDs are
Charles, Kandice 4455
Graham, Ariel 3050
Graham, Ashleigh 5000
Greaves, Sherrelle 6050
Perrott, Chloe 2795
Reyes, Aaliyah 6669
Reyes, Ayanna 5464
Seecharan, Anella 7824
我们顺便注意到,如果我们有几组相关的项目要处理,那么将每一组存储在一个单独的数组中并不是最好的处理方式。最好将项目分组到一个类中,并像处理单个项目一样与该组一起工作。我们将在 2.14 节向您展示如何做到这一点。
1.6 二分搜索法
是一种快速搜索给定项目列表的方法,提供列表排序(升序或降序)。为了说明该方法,考虑一个由 13 个数字组成的列表,按升序排序并存储在数组num[0..12]中。
*
假设我们要搜索66。搜索过程如下:
- 我们找到了列表中的中间项。这是在位置
6的56。我们将66与56相比较。由于66更大,我们知道如果66在列表中,那么一定是在位置6之后的*,因为数字是按升序排列的。下一步,我们将搜索范围限制在位置7到12。* - 我们从位置
7到12找到中间的项目。在这种情况下,我们可以选择项目9或项目10。我们要写的算法会选择9项,也就是78。 - 我们比较
66和78。由于66较小,我们知道如果66在列表中,那么必须在位置9之前,因为数字是按升序排列的。下一步,我们将搜索范围限制在位置7到8。 - 我们从位置
7到8找到中间的项目。在这种情况下,我们可以选择项目7或项目8。我们要写的算法会选择7项,也就是66。 - 我们比较
66和66。由于它们是相同的,我们的搜索成功结束,在位置7找到了所需的项目。
假设我们正在搜索70。搜索将如上所述继续进行,直到我们将70与66(在位置7)进行比较。
- 由于
70更大,我们知道如果70在列表中,那么必须在位置7之后*,因为数字是升序排列的。下一步,我们将搜索范围限制在8到8的地点。这只是一个地点。* - 我们将
70与8项进行比较,即72。由于70较小,我们知道如果70在列表中,那么必须在位置8之前在位置。由于它不可能在位置7之后和位置8之前*,所以我们断定它不在列表中。*
在搜索的每个阶段,我们将搜索限制在列表的某个部分。让我们使用变量lo和hi作为定义这一部分的下标。换句话说,我们的搜索将局限于num[lo]到num[hi]。
最初,我们想要搜索整个列表,因此在本例中,我们将把lo设置为0,把hi设置为12。
我们如何找到中项的下标?我们将使用以下计算方法:
mid = (lo + hi) / 2;
因为将执行整数除法,所以分数(如果有的话)将被丢弃。例如,当lo为0、hi为12时,mid变为6;当lo为7且hi为12时,mid变为9;当lo为7且hi为8时,mid变为7。
只要lo小于或等于hi,它们就定义了要搜索的列表的非空部分。当lo等于hi时,它们定义了要搜索的单个项目。如果lo变得比hi大,这意味着我们已经搜索了整个列表,但没有找到该项目。
基于这些想法,我们现在可以编写一个函数binarySearch。更一般地说,我们将编写它,以便调用例程可以指定它希望搜索数组的哪个部分来查找该项。
因此,必须给该函数指定要搜索的项(key)、数组(list)、搜索的开始位置(lo)和搜索的结束位置(hi)。例如,为了在上面的数组num中搜索数字66,我们可以发出调用binarySearch(66, num, 0, 12)。
这个函数必须告诉我们搜索的结果。如果找到了该项,该函数将返回它的位置。如果没有找到,将返回-1。
public static int binarySearch(int key, int[] list, int lo, int hi) {
//search for key from list[lo] to list[hi]
//if found, return its location; otherwise, return -1
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (key == list[mid]) return mid; // found
if (key < list[mid]) hi = mid - 1;
else lo = mid + 1;
}
return -1; //lo and hi have crossed; key not found
}
如果item包含一个要搜索的数字,我们可以编写如下代码:
int ans = binarySearch(item, num, 0, 12);
if (ans == -1) System.out.printf("%d not found\n", item);
else System.out.printf("%d found in location %d\n", item, ans);
如果我们想从位置i到j搜索item,我们可以写如下:
int ans = binarySearch(item, num, i, j);
我们可以用程序 P1.5 来测试binarySearch。
程序 p 1.5
public class BinarySearchTest {
public static void main(String[] args) {
int[] num = {17, 24, 31, 39, 44, 49, 56, 66, 72, 78, 83, 89, 96};
int n = binarySearch(66, num, 0, 12);
System.out.printf("%d\n", n); //will print 7; 66 in pos. 7
n = binarySearch(66, num, 0, 6);
System.out.printf("%d\n", n); //will print -1; 66 not in 0 to 6
n = binarySearch(70, num, 0, 12);
System.out.printf("%d\n", n); //will print -1; 70 not in list
n = binarySearch(89, num, 5, 12);
System.out.printf("%d\n", n); //will print 11; 89 in pos. 11
} //end main
// binarySearch goes here
} //end class BinarySearchTest
运行时,该程序将打印以下内容:
7
-1
-1
11
1.7 搜索字符串数组
我们可以使用与搜索整数数组相同的技术来搜索字符串的排序数组(比如按字母顺序排列的名称)。主要区别在于数组的声明和使用String函数compareTo,而不是==或<,来比较两个字符串。以下是binarySearch的字符串版本:
public static int binarySearch(String key, String[] list, int lo, int hi) {
//search for key from list[lo] to list[hi]
//if found, return its location; otherwise, return -1
while (lo <= hi) {
int mid = (lo + hi) / 2;
int cmp = key.compareTo(list[mid]);
if (cmp == 0) return mid; // search succeeds
if (cmp < 0) hi = mid -1; // key is ‘less than’ list[mid]
else lo = mid + 1; // key is ‘greater than’ list[mid]
}
return -1; //lo and hi have crossed; key not found
} //end binarySearch
因为我们需要知道一个字符串是等于还是小于另一个,所以最好使用compareTo方法。
注意,我们只调用了一次compareTo。返回值(cmp)告诉了我们需要知道的一切。如果我们在比较单词或名字,并且希望在比较中忽略字母的大小写,我们可以使用compareToIgnoreCase。
可以用 P r程序 P1.6 测试该功能。
程序 p 1.6
import java.util.*;
public class BinarySearchString {
final static int MaxNames = 8;
public static void main(String[] args) {
String name[] = {"Charles, Kandice", "Graham, Ariel",
"Graham, Ashleigh", "Greaves, Sherrelle", "Perrott, Chloe",
"Reyes, Aaliyah", "Reyes, Ayanna", "Seecharan, Anella"};
int n = binarySearch("Charles, Kandice", name, 0, MaxNames - 1);
System.out.printf("%d\n", n);
//will print 0, location of Charles, Kandice
n = binarySearch("Reyes, Ayanna", name, 0, MaxNames - 1);
System.out.printf("%d\n", n);
//will print 6, location of Reyes, Ayanna
n = binarySearch("Perrott, Chloe", name, 0, MaxNames - 1);
System.out.printf("%d\n", n);
//will print 4, location of Perrott, Chloe
n = binarySearch("Graham, Ariel", name, 4, MaxNames - 1);
System.out.printf("%d\n", n);
//will print -1, since Graham, Ariel is not in locations 4 to 7
n = binarySearch("Cato, Brittney", name, 0, MaxNames - 1);
System.out.printf("%d\n", n);
//will print -1 since Cato, Brittney is not in the list
} //end main
// binarySearch goes here
} //end class BinarySearchString
这将按照字母顺序设置名称数组name。然后它用不同的名字调用binarySearch并打印每个搜索的结果。
人们可能想知道这样的呼叫会发生什么:
n = binarySearch("Perrott, Chloe", name, 5, 10);
这里,我们告诉binarySearch在给定数组的位置5到10中寻找"Perrott, Chloe"。然而,位置8到10在数组中不存在。搜索的结果将是不可预测的。程序可能会崩溃或返回不正确的结果。确保用有效的参数调用binarySearch(或任何其他函数)是调用程序的责任。
1.8 示例:词频统计
让我们写一个程序来阅读一篇英语文章,并统计每个单词出现的次数。输出由单词及其频率的字母列表组成。
我们可以使用以下大纲来开发我们的程序:
while there is input
get a word
search for word
if word is in the table
add 1 to its count
else
add word to the table
set its count to 1
endif
endwhile
print table
这是典型的“搜索并插入”情况。我们在目前存储的单词中搜索下一个单词。如果搜索成功,唯一要做的就是增加它的计数。如果搜索失败,该单词将被放入表中,并且其计数设置为 1。
这里的一个主要设计决策是如何搜索表,这反过来又取决于新单词在表中的插入位置和插入方式。以下是两种可能性:
- 在表格的下一个空闲位置插入一个新单词。这意味着必须使用顺序搜索来查找输入的单词,因为这些单词没有任何特定的顺序。这种方法具有简单和易于插入的优点,但是随着更多的单词被放入表中,搜索需要更长的时间。
- 在表格中插入一个新单词时,单词总是按字母顺序排列。这可能需要移动已经存储的单词,以便可以将新单词插入正确的位置。但是,由于表是有序的,所以可以使用二分搜索法来搜索输入的单词。
对于(2),搜索速度更快,但插入速度比(1)慢。因为一般来说,搜索比插入更频繁,(2)可能更好。
(2)的另一个优点是,在最后,单词已经按字母顺序排列,不需要排序。如果使用(1),则需要对单词进行排序,以获得字母顺序。
我们将使用(2)中的方法编写程序。完整的程序如程序 P1.7 所示。
程序 P1.7
import java.io.*;
import java.util.*;
public class WordFrequency {
final static int MaxWords = 50;
public static void main(String[] args) throws IOException {
String[] wordList = new String[MaxWords];
int[] frequency = new int[MaxWords];
FileReader in = new FileReader("passage.txt");
PrintWriter out = new PrintWriter(new FileWriter("output.txt"));
for (int h = 0; h < MaxWords; h++) {
frequency[h] = 0;
wordList[h] = "";
}
int numWords = 0;
String word = getWord(in).toLowerCase();
while (!word.equals("")) {
int loc = binarySearch(word, wordList, 0, numWords-1);
if (word.compareTo(wordList[loc]) == 0) ++frequency[loc]; //word found
else //this is a new word
if (numWords < MaxWords) { //if table is not full
addToList(word, wordList, frequency, loc, numWords-1);
++numWords;
}
else out.printf("'%s' not added to table\n", word);
word = getWord(in).toLowerCase();
}
printResults(out, wordList, frequency, numWords);
in.close();
out.close();
} // end main
public static int binarySearch(String key, String[] list, int lo, int hi){
//search for key from list[lo] to list[hi]
//if found, return its location;
//if not found, return the location in which it should be inserted
//the calling program will check the location to determine if found
while (lo <= hi) {
int mid = (lo + hi) / 2;
int cmp = key.compareTo(list[mid]);
if (cmp == 0) return mid; // search succeeds
if (cmp < 0) hi = mid -1; // key is 'less than' list[mid]
else lo = mid + 1; // key is 'greater than' list[mid]
}
return lo; //key must be inserted in location lo
} //end binarySearch
public static void addToList(String item, String[] list, int[] freq, int p, int n) {
//adds item in position list[p]; sets freq[p] to 1
//shifts list[n] down to list[p] to the right
for (int h = n; h >= p; h--) {
list[h + 1] = list[h];
freq[h + 1] = freq[h];
}
list[p] = item;
freq[p] = 1;
} //end addToList
public static void printResults(PrintWriter out, String[] list, int freq[], int n) {
out.printf("\nWords Frequency\n\n");
for (int h = 0; h < n; h++)
out.printf("%-20s %2d\n", list[h], freq[h]);
} //end printResults
public static String getWord(FileReader in) throws IOException {
//returns the next word found
final int MaxLen = 255;
int c, n = 0;
char[] word = new char[MaxLen];
// read over non-letters
while (!Character.isLetter((char) (c = in.read())) && (c != -1)) ;
//empty while body
if (c == -1) return ""; //no letter found
word[n++] = (char) c;
while (Character.isLetter(c = in.read()))
if (n < MaxLen) word[n++] = (char) c;
return new String(word, 0, n);
} // end getWord
} //end class WordFrequency
假设以下数据存储在passage.txt中:
Be more concerned with your character than your reputation,
because your character is what you really are,
while your reputation is merely what others think you are.
Our character is what we do when we think no one is looking.
当程序 P1.7 运行时,它将其输出存储在output.txt中。以下是输出:
Words Frequency
are 2
be 1
because 1
character 3
concerned 1
do 1
is 4
looking 1
merely 1
more 1
no 1
one 1
others 1
our 1
really 1
reputation 2
than 1
think 2
we 2
what 3
when 1
while 1
with 1
you 2
your 4
以下是对程序 P1.7 的一些评论:
- 出于我们的目的,我们假设一个单词以字母开头,并且只由字母组成。如果您想包含其他字符(如连字符或撇号),您只需更改
getWord功能。 MaxWords表示满足的不同单词的最大数量。为了测试程序,我们使用了50作为这个值。如果文章中不同单词的数量超过了MaxWords(比如说 50),那么第 50 个之后的所有单词都将被读取,但不会被存储,并且会打印一条大意如此的消息。然而,如果再次遇到,已经存储的单词的计数将增加。main将频率计数初始化为0并将String数组中的项目初始化为空字符串。然后,它根据 1.8 节开头所示的大纲处理文章中的单词。getWord读取输入文件并返回找到的下一个单词。- 所有单词都被转换成小写,例如,
The和the被视为同一个单词。 binarySearch是这样写的,如果找到这个单词,就返回它的位置。如果没有找到该单词,则返回其应该插入的位置。函数addToList被赋予插入新单词的位置。该位置右侧的单词(包括该位置)将移动一个位置,以便为新单词腾出空间。
1.9 合并有序列表
合并是将两个或多个有序列表合并成一个有序列表的过程。例如,给定两个数字列表,A和B,如下所示:
A: 21 28 35 40 61 75
B: 16 25 47 54
它们可以组合成一个有序列表,C:
C: 16 21 25 28 35 40 47 54 61 75
列表C包含列表A和B中的所有数字。如何执行合并?
一种思考方式是想象给定列表中的数字存储在卡片上,每张卡片一个,卡片面朝上放在桌子上,最小的放在顶部。我们可以想象列表A和B如下:
21 16
28 25
35 47
40 54
61
75
我们看最上面的两张卡,21和16。较小的16被移除并放置在C中。这就暴露了25这个数字。
现在最上面的两张卡是21和25。较小的21被移除并添加到C,?? 现在包含了16 21。这就暴露了数字28。
现在最上面的两张卡是28和25。较小的25被移除并添加到C,?? 现在包含了16 21 25。这就暴露了数字47。
现在最上面的两张卡是28和47。较小的28被移除并添加到C,?? 现在包含了16 21 25 28。这就暴露了数字35。
现在最上面的两张卡是35和47。较小的35被移除并添加到C,?? 现在包含了16 21 25 28 35。这就暴露了数字40。
现在最上面的两张卡是40和47。较小的40被移除并添加到C,?? 现在包含了16 21 25 28 35 40。这就暴露了数字61。
现在最上面的两张卡是61和47。较小的47被移除并添加到C,?? 现在包含了16 21 25 28 35 40 47。这就暴露了数字54。
现在最上面的两张卡是61和54。较小的54被移除并添加到C,?? 现在包含了16 21 25 28 35 40 47 54。列表B没有更多数字。
我们将A的剩余元素(61 75)复制到C,现在包含以下内容:
16 21 25 28 35 40 47 54 61 75
合并已完成。
在合并的每一步,我们将最小剩余数A与最小剩余数B进行比较。其中较小的被添加到C。如果较小的数字来自于A,我们继续前进到A的下一个数字;如果较小的数字来自B,我们将继续处理B中的下一个数字。
重复这一过程,直到使用完A或B中的所有号码。如果A中的所有号码都已被使用,我们将从B到C的剩余号码相加。如果B中的所有数字都已被使用,我们将从A到C的剩余数字相加。
我们可以将合并的逻辑表达如下:
while (at least one number remains in both A and B) {
if (smallest in A < smallest in B)
add smallest in A to C
move on to next number in A
else
add smallest in B to C
move on to next number in B
endif
}
if (A has ended) add remaining numbers in B to C
else add remaining numbers in A to C
1.9.1 实现合并
假设数组A包含存储在A[0]到A[m-1]中的 m 个数,数组B包含存储在B[0]到B[n-1]中的 n 个数。假设数字按升序存储。我们希望将A和B中的数字合并到另一个数组C中,这样C[0]到C[m+n-1]就包含了A和B中按升序排序的所有数字。
我们将使用整数变量i、j和k来分别下标数组A、B和C。在数组中“移动到下一个位置”可以通过给下标变量加上1来完成。我们可以用这个函数实现合并:
public static int merge(int[] A, int m, int[] B, int n, int[] C) {
int i = 0; //i points to the first (smallest) number in A
int j = 0; //j points to the first (smallest) number in B
int k = -1; //k will be incremented before storing a number in C[k]
while (i < m && j < n) {
if (A[i] < B[j]) C[++k] = A[i++];
else C[++k] = B[j++];
}
if (i == m) ///copy B[j] to B[n-1] to C
for ( ; j < n; j++) C[++k] = B[j];
else // j == n, copy A[i] to A[m-1] to C
for ( ; i < m; i++) C[++k] = A[i];
return m + n;
} //end merge
该函数接受参数A、m、B、n和C,执行合并,并返回C中的元素数量m + n。
程序 P1.8 显示了一个简单的main函数,用于测试merge的逻辑。它设置数组A和B,调用merge,打印C。运行时,该程序打印以下内容:
16 21 25 28 35 40 47 54 61 75
程序 P1.8
public class MergeTest {
public static void main(String[] args) {
int[] A = {21, 28, 35, 40, 61, 75}; //size 6
int[] B = {16, 25, 47, 54}; //size 4
int[] C = new int[20]; //enough to hold all the elements
int n = merge(A, 6, B, 4, C);
for (int j = 0; j < n; j++) System.out.printf("%d ", C[j]);
System.out.printf("\n");
} //end main
// merge goes here
} //end class MergeTest
有趣的是,我们也可以如下实现merge:
public static int merge(int[] A, int m, int[] B, int n, int[] C) {
int i = 0; //i points to the first (smallest) number in A
int j = 0; //j points to the first (smallest) number in B
int k = -1; //k will be incremented before storing a number in C[k]
while (i < m || j < n) {
if (i == m) C[++k] = B[j++];
else if (j == n) C[++k] = A[i++];
else if (A[i] < B[j]) C[++k] = A[i++];
else C[++k] = B[j++];
}
return m + n;
}
while循环表达了以下逻辑:只要在A 或 B中至少有一个元素要处理,我们就进入循环。如果我们完成了A ( i == m,从B到C复制一个元素。如果我们完成了B ( j == n),将一个元素从A复制到C。否则,将A[i]和B[j]中较小的一个复制到C。每当我们从一个数组中复制一个元素,我们就给这个数组的下标加 1。
虽然以前的版本以一种简单的方式实现了合并,但是说这个版本更简洁似乎是合理的。
练习 1
-
A survey of 10 pop artists is made. Each person votes for an artist by specifying the number of the artist (a value from 1 to 10). Each voter is allowed one vote for the artist of their choice. The vote is recorded as a number from
1to10. The number of voters is unknown beforehand, but the votes are terminated by a vote of0. Any vote that is not a number from1to10is a spoiled vote. A file,votes.txt, contains the names of the candidates. The first name is considered as candidate 1, the second as candidate 2, and so on. The names are followed by the votes. Write a program to read the data and evaluate the results of the survey.按艺术家姓名的字母顺序和收到的票数顺序打印结果(票数最多者优先)。打印所有输出到文件,
results.txt。 -
写一个程序将名字和电话号码读入两个数组。询问对方的姓名,并打印出他的电话号码。使用二分搜索法查找该名称。
-
写一个程序,把英语单词和它们对应的西班牙语单词读入两个数组。要求用户键入几个英语单词。对于每一项,打印相应的西班牙语单词。选择合适的数据结束标记。使用二分搜索法搜索键入的单词。修改程序,让用户键入西班牙语单词。
-
一组 n 个数(不一定是截然不同的)的中位数是将数按顺序排列,取中间的数得到的。如果 n 是奇数,则有一个唯一的中间数。如果 n 是偶数,那么两个中间值的平均值就是中位数。写程序读取一组 n 正整数(假设n100)并打印它们的中位数; n 未给出,但
0表示数据结束。 -
一组 n 数字的模式是出现频率最高的数字。比如
7 3 8 5 7 3 1 3 4 8 9的模式是3。写程序读取一组 n 正整数(假设n100)并打印出它们的模式; n 未给出,但0表示数据结束。 -
数组
chosen包含n个不按特定顺序排列的不同整数。另一个名为winners的数组包含按照升序排列的m个不同的整数。编写代码来确定chosen中有多少个数字出现在winners中。 -
A multiple-choice examination consists of 20 questions. Each question has five choices, labeled A, B, C, D, and E. The first line of data contains the correct answers to the 20 questions in the first 20 consecutive character positions, for example:
BECDCBAADEBACBAEDDBE
随后的每一行都包含候选人的答案。一行中的数据由一个候选数字(一个整数)组成,后面是一个或多个空格,再后面是候选人在接下来的 20 个连续字符位置给出的 20 个答案。如果候选人没有回答特定的问题,则使用 X。您可以假设所有数据都是有效的,并存储在文件
exam.dat中。示例行如下:4325 BECDCBAXDEBACCAEDXBE
最多有 100 名候选人。包含“候选编号”
0的行仅表示数据的结束。一个问题的分值授予如下—正确答案:4 分;回答错误:-1 分;没有回答:0 分。
编写一个程序来处理数据,并打印一份报告,该报告由候选人编号和候选人获得的总分数组成,按候选人编号升序排列。最后,打印候选人获得的平均分数。
-
A是按降序排序的数组。B是一个降序排列的数组。将A和B合并成C,使C按照递减的顺序。 -
A是按降序排序的数组。B是一个降序排列的数组。将A和B合并为C,使C按照升序排列。 -
A是按升序排序的数组。B是一个降序排列的数组。将A和B合并为C,使C按照升序排列。 -
An array A contains integers that first increase in value and then decrease in value. Here’s an example:

不知道从哪一点开始数量开始减少。编写高效的代码将 A 中的数字复制到另一个数组 B 中,使 B 按升序排序。您的代码必须利用数字在。
12. Two words are anagrams if one word can be formed by rearranging all the letters of the other word, for example: section, notices. Write a program to read two words and determine whether they are anagrams.
编写另一个程序来读取单词列表并找到所有单词集合,使得一个集合中的单词是彼此的变位词。*
二、对象简介
在本章中,我们将解释以下内容:
- 什么是类、对象、字段和方法
- 对象变量不保存对象,而是保存一个指向对象实际位置的指针(或引用)
- 类变量(也称为静态变量)和实例变量(也称为非静态变量)之间的区别
- 类方法(也称为静态方法)和实例方法(也称为非静态方法)之间的区别
- 访问修饰符 public、private 和 protected 是什么意思
- 什么是信息隐藏
- 如何引用类和实例变量
- 如何初始化类和实例变量
- 什么是构造函数以及如何编写构造函数
- 超载是什么意思
- 什么是数据封装
- 如何编写访问器和赋值器方法
- 如何以各种方式打印对象的数据
- 为什么
tostring()方法在 Java 中是特殊的 - 当我们将一个对象变量赋给另一个对象变量时会发生什么
- 将一个对象变量与另一个对象变量进行比较意味着什么
- 如何比较两个对象的内容
- 函数如何使用对象返回多个值
2.1 对象
Java 被认为是一种面向对象的编程语言。设计者创造它是为了让物体成为关注的焦点。Java 程序创建和操作对象,试图模拟现实世界是如何运作的。出于我们的目的,对象是一个拥有状态和方法来操作该状态的实体。对象的状态由其属性决定。
比如,我们可以把一个人想象成一个物体。一个人有姓名、年龄、性别、身高、头发颜色、眼睛颜色等属性。在一个程序中,每个属性都由一个适当的变量来表示;例如,String变量可以代表的名字,一个int变量可以代表的年龄,一个char变量可以代表的性别,一个double变量可以代表的身高,等等。
我们通常使用术语字段名(或者简单地说,字段)来指代这些变量。因此,对象的状态由其字段中的值定义。此外,我们将需要方法来设置和/或更改字段的值,以及检索它们的值。例如,如果我们对一个人的身高感兴趣,我们将需要一个方法来“观察”该对象并返回高度字段的值。
汽车是物体的另一个常见例子。它具有制造商、型号、座位容量、燃料容量、油箱中的实际燃料、里程、音乐设备的类型和速度等属性。book 对象具有诸如作者、标题、价格、页数、装订类型(精装、平装、螺旋)以及是否有货等属性。一个人、一辆车和一本书都是具体物体的例子。但是,请注意,一个对象也可以表示一个抽象的概念,例如公司的一个部门或大学的一个系。
在前面的例子中,我们没有提到某个特定的人。相反,我们谈到了一个总的范畴“人”,这个范畴中的每个人都具有所提到的属性。(类似的言论也适用于汽车和书籍。)在 Java 术语中,“人”是一个类。我们认为类是一个通用的类别(模板),从中我们可以创建特定的对象。
那么,一个对象就是一个类的实例;在这个例子中,Person对象指的是一个特定的人。为了处理两个Person对象,我们需要从Person的类定义中创建两个对象。每个对象都有自己的字段变量副本(也称为实例变量);一个对象中的变量值可能与另一个对象中的变量值不同。
2.2 定义类和创建对象
最简单的 Java 程序只包含一个类。在类中,我们编写一个或多个方法/函数来执行一些任务。程序 P2.1 显示了一个例子。
程序 P2.1
//prompt for two numbers and find their sum
import java.util.*;
public class Sum {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.printf("Enter first number: ");
int a = in.nextInt();
System.out.printf("Enter second number: ");
int b = in.nextInt();
System.out.printf("%d + %d = %d\n", a, b, a + b);
}
} //end class Sum
程序由一个类(ProgramP1_1)和该类中的一个方法(main)组成。这个类只是用来作为编写程序逻辑的框架。我们现在将展示如何定义和使用一个类来创建(我们说实例化)对象。
在 Java 中,每个对象都属于某个类,并且只能从类定义中创建。考虑类Book的以下(部分)定义:
public class Book {
private static double Discount = 0.25; //class variable
private static int MinBooks = 5; //class variable
private String author; // instance variable
private String title; // instance variable
private double price; // instance variable
private int pages; // instance variable
private char binding; // instance variable
private boolean inStock; // instance variable
// methods to manipulate book data go here
} //end class Book
类标题(第一行)由以下内容组成:
- 可选的访问修饰符;
public在示例中使用,并且将用于我们的大多数类。本质上,这意味着该类可供任何其他类使用;也可以扩展创建子类。其他访问修饰符是abstract和final;我们不会在这本书里讨论这些。 - 关键词
class。 - 类名的用户标识符;例中使用了
Book。
大括号将类的主体括起来。一般而言,该机构将包括以下声明:
- 静态变量(类变量);整个类将有一个副本,所有对象将共享这个副本。使用单词
static声明一个类变量。如果我们省略static这个词,变量就是实例。 - 非静态变量(实例变量);创建的每个对象都有自己的副本。组成对象数据的是实例变量。
- 静态方法(类方法);这些在类加载时加载一次,无需创建任何对象即可使用。静态方法访问非静态变量(属于对象)是没有意义的,所以 Java 禁止。
- 非静态方法(实例方法);这些只能通过从类中创建的对象使用。操纵对象中数据(非静态字段)的是非静态方法。
String类是在 Java 中预定义的。如果word是String(准确地说是一个String对象)并且我们编写了word.toLowerCase(),我们要求将String类的实例方法toLowerCase应用到String对象word。该方法将用于调用它的(String)对象中的大写字母转换成小写字母。- 同样,如果
in是一个Scanner对象(我们说new Scanner...时创建的),表达式in.nextInt()将实例方法nextInt应用于对象in;这里,它从与in相关的输入流中读取下一个整数。
在Book类中,我们声明了两个类变量(Discount和MinBooks,用static声明)和六个实例变量;默认情况下它们是实例(省略了static这个词)。
2.2.1 访问类和实例变量
除了static,还可以使用可选的访问修饰符private、public或protected来声明一个字段。在Book类中,我们使用private声明了所有的实例变量。关键字private表示变量只在类中“已知”,并且只能由类中的方法直接操作*。换句话说,类外的任何方法都不能直接访问private变量。然而,我们很快就会看到,我们可以提供其他类可以用来设置和访问private变量的值的public方法。这样,我们确保了类数据只能由类中的方法来更改。*
声明一个变量public意味着可以从类外部直接访问它。因此,其他类可以用一个public变量“为所欲为”。例如,如果Discount被声明为public,那么任何其他类都可以使用Book.Discount来访问它,并以它喜欢的任何方式更改它。通常不鼓励这样做,因为这样类就会失去对其数据的控制。
在大多数情况下,我们将使用private声明一个类的字段。这样做是实现信息隐藏概念的第一步,这是面向对象编程哲学的一部分。其思想是对象的用户不能直接处理对象的数据;它们应该通过对象的方法来实现。
声明一个变量protected意味着它可以直接从类和它的任何子类中访问,也可以从同一个包中的其他类中访问。在这个介绍中,我们将不使用protected变量。
如果没有指定访问修饰符,那么这个变量只能被同一个包中的其他类直接访问。
一个类中的方法可以简单地通过名字引用类中的任何变量(static或非static、public或private)。(一个例外是静态方法不能访问非静态变量。)如果一个静态变量在类之外是已知的(也就是说,不是private,那么通过用类名限定变量来引用它,就像在Book.Discount和Book.MinBooks中一样。
在类的外部,一个非私有的实例变量只能通过它所属的对象来引用;下一节将对此进行说明。然而,如上所述,良好的编程实践表明,大多数情况下,我们的变量将被声明为private,因此不会出现从类外部直接访问的概念。
2.2.2 初始化类和实例变量
当Book类被加载时,存储立即被分配给类变量Discount和MinBooks;然后,它们分别被赋予初始值0.25和 5。这些变量背后的含义是,如果一本书售出五本或更多,那么就给予 25%的折扣。因为这些值适用于所有的书,把它们和每本书的数据存储在一起会浪费存储空间,因此把它们声明为static变量。所有 book 对象都可以访问这些变量的单个副本。(但是,请注意,如果我们想在不同的书中改变这些值,那么它们将成为特定书的属性,并且必须声明为非静态的。)
当第一次加载类时,没有存储空间分配给实例(非静态)变量。此时,我们只有实例变量的规范,但是实际上还没有。当从类中创建一个对象时,它们就存在了。对象的数据由实例变量决定。当一个对象被“创建”时,存储空间被分配给类中定义的所有实例变量;创建的每个对象都有自己的实例变量副本。要创建一个对象,我们使用关键字new,如下所示:
Book b;
b = new Book();
第一条语句将b声明为Book类型的变量。由此,我们看到一个类名被认为是一种类型(类似于int或char),可以用来声明变量。我们说b是一个Book类型的对象变量。
b的声明不创建对象;它只是创建一个变量,该变量的值最终将成为一个对象的指针。如图所示声明时,其值是未定义的。
第二条语句找到一些可以存储Book对象的可用内存,创建该对象,并将该对象的地址存储在b中。(把地址想象成对象占用的第一个内存位置。如果对象占据位置2575到2599,那么它的地址就是2575。)我们说b包含一个指向该对象的引用或指针。因此,对象变量的值是一个内存地址,而不是一个对象。如图 2-1 中的所示。
图 2-1 。书籍对象的实例
作为一种快捷方式,我们可以在一条语句中声明b并创建一个 book 对象,如下所示:
Book b = new Book();
认为Book变量b可以容纳Book对象是一个常见的错误。它不能;它只能保存对一个Book对象的引用。(以类似的方式,我们应该熟悉这样的想法:一个String变量并不保存一个字符串,而是保存字符串存储位置的地址。)然而,在区别(对象和对对象的引用之间的区别)无关紧要的地方,我们将认为b持有一个Book对象。
一旦对象b被创建,我们可以像这样引用它的实例字段:
b.author b.title b.price
b.pages b.binding b.inStock
然而,只有当字段被声明为public时,我们才能从类外的中这样做。我们将在后面看到当字段被声明为private时如何间接访问它们。
当创建一个对象时,除非我们另有说明,否则它的实例字段初始化如下:
- 数值字段被设置为
0。 - 字符字段被设置为
'\0'(准确地说是 Unicode'\u0000')。 - 布尔字段被设置为
false。 - 对象字段被设置为
null。(值为null的变量意味着它不引用或指向任何东西。)
在我们的示例中,会发生以下情况:
b.author(类型String)设置为null;记住String是一个对象类型。b.title(类型String)被设置为null。b.price(类型double)被设置为0.0。b.pages(类型int)被设置为0。b.binding(类型char)被设置为'\0'。b.inStock(类型boolean)被设置为false。
我们可以在声明一个实例变量时指定一个初始值。考虑以下代码:
public class Book {
private static double Discount = 0.25;
private static int MinBooks = 5;
private String author = "No Author";
private String title;
private double price;
private int pages;
private char binding = 'P'; // for paperback
private boolean inStock = true;
}
现在,当创建一个对象时,author、binding和inStock将被设置为指定值,而title、price和pages将采用默认值。只有在没有明确赋值的情况下,变量才会被赋予默认值。假设我们用这个创建一个对象b:
Book b = new Book();
这些字段将被初始化如下:
author设置为"No Author"。//在声明中指定title设置为null。//默认为(String)对象类型price设置为0.0。//数值类型的默认值pages设置为0。//数值类型的默认值binding设置为'P'。//在声明中指定inStock设置为true。//在声明中指定
2.3 构造函数
构造函数提供了更灵活的方法来初始化创建时的对象状态。在下面的陈述中,Book()被称为构造器:
Book b = new Book();
它类似于方法调用。但是,你可能会说,我们没有在类定义中编写任何这样的方法。没错,但在这种情况下,Java 提供了一个默认构造函数——一个没有参数的构造函数(也称为无参数构造函数)。默认的构造函数非常简单;它只是将实例变量的值设置为它们默认的初始值。稍后,我们可以为对象的字段分配更有意义的值,如下所示:
b.author = "Noel Kalicharan";
b.title = "DigitalMath";
b.price = 29.95;
b.pages = 200;
b.binding = 'P'; //for paperback
b.inStock = true; //stock is available
现在假设当我们创建一个 book 对象时,我们希望 Java 自动分配作者和标题。我们希望能够使用如下语句来创建新的 book 对象:
Book b = new Book("Noel Kalicharan", "DigitalMath");
我们可以这样做,但是我们必须首先编写一个适当的构造函数,一个用两个参数定义的构造函数。下面展示了如何实现这一点:
public Book(String a, String t) {
author = a;
title = t;
}
以下是一些需要注意的要点:
- 一个类的构造函数和这个类有相同的名字。我们班叫
Book;因此,必须调用构造函数Book。因为构造函数是要被其他类使用的,所以它被声明为public。 - 构造函数可以有零个或多个参数。调用时,必须为构造函数提供适当数量和类型的参数。在我们的例子中,构造函数是用两个参数
String、a和t声明的。调用构造函数时,必须提供两个String参数。 - 构造函数的主体包含调用构造函数时要执行的代码。我们的示例将实例变量
author设置为第一个参数,将title设置为第二个参数。一般来说,除了设置实例变量的值之外,我们还可以使用其他语句。例如,我们可以在将提供的值赋给字段之前验证它。我们将在下一节看到一个这样的例子。 - 构造函数没有返回类型,甚至没有
void。 - 如果在声明中为实例变量提供了初始值,那么这些值将在调用构造函数之前存储。
例如,假设类Book 现在声明如下:
public class Book {
private static double Discount = 0.25;
private static int MinBooks = 5;
private String author = "No Author";
private String title;
private double price;
private int pages;
private char binding = 'P'; // for paperback
private boolean inStock = true;
public Book(String a, String t) {
author = a;
title = t;
}
} //end class Book
该声明
Book b = new Book("Noel Kalicharan", "DigitalMath");
将按如下方式执行:
-
为一个
Book对象找到存储器,存储器的地址存储在 b 中。 -
这些字段设置如下:
author is set to "No Author"; // specified in the declaration title is set to null; // default for (String) object type price is set to 0.0; // default for numeric type pages is set to 0; // default for numeric type binding is set to 'P'; // specified in the declaration inStock is set to true. // specified in the declaration -
用参数
"Noel Kalicharan"和"DigitalMath"调用构造函数;这将author设置为"Noel Kalicharan",将title设置为"DigitalMath",其他字段保持不变。当构造函数完成时,这些字段将具有以下值:author "Noel Kalicharan" title "DigitalMath" price 0.0 pages 0 binding 'P' inStock true
2.3.1 重载构造函数
Java 允许我们有不止一个构造函数,假设每个构造函数都有不同的签名。当几个构造函数可以有相同的名字时,这被称为*重载构造函数。*假设我们希望能够使用无参数的构造函数以及带有作者和标题参数的构造函数。我们可以像这样在类声明中包含这两者:
public class Book {
private static double Discount = 0.25;
private static int MinBooks = 5;
private String author = "No Author";
private String title;
private double price;
private int pages;
private char binding = 'P'; // for paperback
private boolean inStock = true;
public Book() { }
public Book(String a, String t) {
author = a;
title = t;
}
} //end class Book
请注意,无参数构造函数的主体由一个空块组成。当执行下面的语句时,实例变量被设置为它们的初始值(指定值或默认值),并且执行构造函数。在这种情况下,不会发生进一步的事情。
Book b = new Book();
请注意,当我们提供构造函数时,默认的无参数构造函数不再可用。如果我们也想使用无参数构造函数,我们必须显式地编写它,就像前面的例子一样。当然,我们可以自由地在正文中写任何我们想写的东西,包括什么都不写。
作为最后一个例子,我们提供了一个构造函数,让我们在创建对象时显式地设置所有字段。这是:
public Book(String a, String t, double p, int g, char b, boolean s) {
author = a;
title = t;
price = p;
pages = g;
binding = b;
inStock = s;
}
如果b是类型为Book的变量,调用示例如下:
b = new Book("Noel Kalicharan", "DigitalMath", 29.95, 200, 'P', true);
这些字段将被赋予以下值:
author "Noel Kalicharan"
title "DigitalMath"
price 29.95
pages 200
binding 'P'
inStock true
2.4 数据封装、访问器和赋值器方法
我们将使用术语用户类来表示其方法需要访问另一个类的字段和方法的类。
当一个类的字段被声明为public时,任何其他的类都可以通过名字直接访问该字段。考虑以下类别:
public class Part {
public static int NumParts = 0; // class variable
public String name; // instance variable
public double price; // instance variable
}
这里,我们将一个静态(或类)变量和两个实例变量定义为public。任何用户类都可以使用Part.NumParts 访问静态变量,并且可以包含如下语句:
Part.NumParts = 25;
这可能是不可取的。假设NumParts是为了计算从Part创建的对象的数量。任何外部类都可以将它设置为它喜欢的任何值,所以类Part的作者不能保证它总是反映所创建对象的数量。
和往常一样,实例变量只能通过对象来访问。当一个用户类创建一个类型为Part的对象p时,它可以使用p.price(或p.name)直接引用实例变量,如果需要的话,可以用一个简单的赋值语句来改变它。没有什么可以阻止用户类将变量设置为不合理的值。例如,假设所有价格都在 0.00 到 99.99 之间。用户类可以包含以下语句,这会损害价格数据的完整性:
p.price = 199.99;
要解决这些问题,我们必须使数据字段private;我们说我们必须隐藏数据。然后我们为其他人提供public方法来设置和检索字段中的值。私有数据和公共方法是数据封装的精髓。设置或更改字段值的方法称为 mutator 方法。检索字段中的值的方法称为访问器方法。
让我们展示一下如何解决提到的两个问题。首先,我们将字段重新定义为private:
public class Part {
private static int NumParts = 0; // class variable
private String name; // instance variable
private double price; // instance variable
}
现在它们是private,没有其他的职业可以访问它们。如果我们想让NumParts反映从类中创建的对象的数量,我们需要在每次调用构造函数时递增它。例如,我们可以编写一个无参数构造函数,如下所示:
public Part() {
name = "NO PART";
price = -1.0; // we use –1 since 0 might be a valid price
NumParts++;
}
每当用户类执行如下语句时,就会创建一个新的Part对象并将 1添加到NumParts:
Part p = new Part();
因此,NumParts的值将总是所创建的Part个对象的数量。进一步说,这是改变其值的唯一方式;类Part的作者可以保证NumParts的值总是被创建的对象的数量。
当然,用户类可能需要在任何给定时间知道NumParts的值。既然它没有对NumParts的访问,我们必须提供一个公共访问器方法 ( GetNumParts,比方说;我们使用大写的G作为静态访问器,因为它提供了一种快速区分静态和非静态的方法,后者返回值。方法如下:
public static int GetNumParts() {
return NumParts;
}
该方法被声明为static,因为它只对一个static变量进行操作,不需要调用一个对象。它可以用Part.GetNumParts()来称呼。如果p是一个Part对象,Java 允许你用p.GetNumParts()调用它。然而,这意味着GetNumParts是一个实例方法(通过对象调用并对实例变量进行操作的方法),所以这可能会引起误解。我们建议通过类名调用类(静态)方法,而不是通过类中的对象。
作为一个练习,向Book类添加一个字段来计算创建的 book 对象的数量,并更新构造函数来增加这个字段。
2.4.1 改进的构造器
我们可以不使用无参数构造函数,而是采用更实际的方法,编写一个构造函数,让用户在创建对象时分配名称和价格,如下所示:
Part af = new Part("Air Filter", 8.75);
我们可以将构造函数写成:
public Part(String n, double p) {
name = n;
price = p;
NumParts++;
}
除了用户仍然可以为某个零件设置无效价格之外,这种方法是可行的。没有任何东西可以阻止用户编写这个语句:
Part af = new Part("Air Filter", 199.99);
构造函数会忠实地将price设置为无效值199.99。然而,我们可以在构造函数中做更多的事情,而不仅仅是给变量赋值。如果有必要,我们可以测试一个值并拒绝它。我们认为,如果提供了一个无效的价格,对象仍将被创建,但会打印一条消息,价格将被设置为–1.0。下面是构造函数的新版本:
public Part(String n, double p) {
name = n;
if (p < 0.0 || p > 99.99) {
System.out.printf("Part: %s\n", name);
System.out.printf("Invalid price: %3.2f. Set to -1.0.\n", p);
price = -1.0;
}
else price = p;
NumParts++;
} //end constructor Part
作为一种良好的编程风格,我们应该将价格限制(0.00和99.99)和“空”价格(-1.0)声明为类常量。我们可以使用以下内容:
private static final double MinPrice = 0.0;
private static final double MaxPrice = 99.99;
private static final double NullPrice = -1.0;
这些标识符现在可以在构造函数中使用。
2.4.2 访问器方法
由于用户类可能需要知道一个商品的名称或价格,我们必须为name和price提供公共访问器方法。访问器方法只是返回特定字段中的值。按照惯例,我们在这些方法的名称前面加上get这个词。这些方法如下:
public String getName() { // accessor
return name;
}
public double getPrice() { // accessor
return price;
}
请注意,访问器的返回类型与字段的类型相同。例如,getName的返回类型是String,因为name的类型是String。
因为访问器方法返回实例字段中的值,所以只在与特定对象相关时调用它是有意义的(因为每个对象都有自己的实例字段)。如果p是类型为Part的对象,那么p.getName()返回p的name字段中的值,而p.getPrice()返回p的price字段中的值。
作为练习,为Book类的所有字段编写访问器方法。
这些访问器是非静态或实例方法的例子(在它们的声明中没有使用单词static)。我们可以认为每个对象在一个类中都有自己的实例方法副本。然而实际上,这些方法仅仅是对一个对象可用的 ??。将有一个方法的副本,并且当在对象上调用该方法时,该方法将被绑定到一个特定的对象。
假设一个Part对象p存储在位置725,我们可以把这个对象描绘成如图 2-2 中的所示。
图 2-2 。带有字段和访问器的部件对象
想象一下字段name和price被锁在一个盒子里,外界看到它们的唯一方式是通过方法getName和getPrice。
2.4.3 赋值函数方法
作为该类的作者,我们必须决定是否允许用户在对象创建后更改其名称或价格。有理由假设用户可能不想更改名称。然而,价格是变化的,所以我们应该提供一种(或多种)改变价格的方法。例如,我们编写了一个用户类可以调用的公共赋值函数方法 ( setPrice),如下所示:
p.setPrice(24.95);
这将Part对象p的价格设置为24.95。和以前一样,该方法不允许设置无效的价格。它将验证所提供的价格,并在必要时打印适当的消息。使用第 2.4.1 节中声明的常数,这里是setPrice:
public void setPrice(double p) {
if (p < MinPrice || p > MaxPrice) {
System.out.printf("Part: %s\n", name);
System.out.printf("Invalid price: %3.2f; Set to %3.2f\n", p, NullPrice);
price = NullPrice;
}
else price = p;
} //end setPrice
有了这个加法,我们可以认为Part p如图图 2-3 。
图 2-3 。添加了 setPrice()的零件对象
观察setPrice的箭头方向;一个值正从外部世界发送到对象的私有字段。
同样,我们强调声明字段private并为其提供赋值函数/访问函数方法的优越性,这与声明字段public并让用户类直接访问它相反。
我们还可以提供一些方法,以给定的数量或给定的百分比来提高或降低价格。这些都是作为练习留下的。
作为另一个练习,为Book类的price和inStock字段编写赋值函数方法。
2.5 打印对象的数据
为了验证我们的部件被赋予了正确的值,我们需要某种方法来打印对象字段中的值。
2.5.1 使用实例方法 (首选方式)
一种方法是编写一个实例方法(printPart),当调用一个对象时,它将打印那个对象的数据。为了打印Part p的数据,我们将这样写:
p.printPart();
方法如下:
public void printPart() {
System.out.printf("\nName of part: %s\n", name);
System.out.printf("Price: $%3.2f\n", price);
} //end printPart
假设我们用这个创建一个零件:
Part af = new Part("Air Filter", 8.75);
表达式af.printPart()将显示以下内容:
Name of part: Air Filter
Price: $8.75
当通过af调用printPart时,printPart中对字段name和price的引用变成对af字段的引用。这在图 2-4 中进行了说明。
图 2-4 。名称和价格指的是 af 字段
2.5.2 使用静态方法
如果我们愿意,我们可以将printPart写成一个static方法,这个方法将与p一起被调用,作为一个参数,以便打印它的字段。在这种情况下,我们将编写以下内容:
public static void printPart(Part p) {
System.out.printf("\nName of part: %s\n", p.name);
System.out.printf("Price: $%3.2f\n", p.price);
}
字段名必须用对象变量p限定。如果没有p,我们会遇到静态方法引用非静态字段的情况,这是 Java 所禁止的。
如果c是在用户类中创建的一个Part对象,我们将不得不使用下面的代码来打印它的字段:
Part.printPart(c);
这比前面显示的使用实例方法稍微麻烦一些。相比之下,您可以使用Character.isDigit(ch)来访问标准 Java 类Character中的静态方法isDigit。
2.5.3 使用 toString()方法
toString方法返回一个String并且在 Java 中是特殊的。如果我们在需要字符串的上下文中使用一个对象变量,那么 Java 将试图从该对象所属的类中调用toString。例如,假设我们写下如下,其中p是一个Part变量:
System.out.printf("%s", p);
由于不清楚打印任意对象意味着什么,Java 将在类本身中寻找指导。据推测,这个类将知道如何打印它的对象。如果它提供了一个toString方法,Java 就会使用它。(如果没有,Java 将打印一些通用的东西,比如类名和十六进制的对象地址,例如:Part@72e15c32。)在我们的示例中,我们可以将以下内容添加到类Part :
public String toString() {
return "\nName of part: " + name + "\nPrice: $" + price + "\n";
}
如果af是空气过滤器部件,那么下面的语句将调用调用af.toString():
System.out.printf("%s", af);
实际上,printf变成了这样:
System.out.printf("%s", af.toString());
af.toString()将返回此:
"\nName of part: Air Filter \nPrice: $8.75\n"
结果是printf将打印如下内容:
Name of part: Air Filter
Price: $8.75
2.6 类部分
将所有的更改放在一起,类Part现在看起来像这样:
public class Part {
// class constants
private static final double MinPrice = 0.0;
private static final double MaxPrice = 99.99;
private static final double NullPrice = -1.0;
private static int NumParts = 0; // class variable
private String name; // instance variable
private double price; // instance variable
public Part(String n, double p) { // constructor
name = n;
if (p < MinPrice || p > MaxPrice) {
System.out.printf("Part: %s\n", name);
System.out.printf("Invalid price: %3.2f; Set to %3.2f\n", p, NullPrice);
price = NullPrice;
}
else price = p;
NumParts++;
} //end constructor Part
public static int GetNumParts() { // accessor
return NumParts;
}
public String getName() { // accessor
return name;
}
public double getPrice() { // accessor
return price;
}
public void setPrice(double p) { // mutator
if (p < MinPrice || p > MaxPrice) {
System.out.printf("Part: %s\n", name);
System.out.printf("Invalid price: %3.2f; Set to %3.2f\n", p, NullPrice);
price = NullPrice;
}
else price = p;
} //end setPrice
public void printPart() {
System.out.printf("\nName of part: %s\n", name);
System.out.printf("Price: $%3.2f\n", price);
}
public String toString() {
return "\nName of part: " + name + "\nPrice: $" + price + "\n";
}
} // end class Part
2.6.1 测试类别部分
当我们编写一个类时,我们必须测试它以确保它正常工作。对于类Part,我们必须检查构造函数是否正常工作,换句话说,访问器方法是否返回正确的值,以及赋值器方法是否正确设置了(新的)价格。
我们还必须检查该类是否正确处理了无效价格。在程序 P2.2 中,我们创建三个零件对象(其中一个具有无效价格)并打印它们的名称/价格信息。然后,我们打印通过调用GetNumParts创建的零件数量。在我们运行测试程序之前,我们应该计算出预期的输出,这样我们就可以预测一个正确的程序应该输出什么。如果输出符合我们的预测,很好;如果不是,就有一个必须解决的问题。
程序 P2.2
public class PartTest{
// a program for testing the class Part
public static void main(String[] args) {
Part a, b, c; // declare 3 Part variables
// create 3 Part objects
a = new Part("Air Filter", 8.75);
b = new Part("Ball Joint", 29.95);
c = new Part("Headlamp", 199.99); // invalid price
a.printPart(); // should print Air Filter, $8.75
b.printPart(); // should print Ball Joint, $29.95
c.printPart(); // should print Headlamp, $-1.0
c.setPrice(36.99);
c.printPart(); // should print Headlamp, $36.99
// print the number of parts; should print 3
System.out.printf("\nNumber of parts: %d\n", Part.GetNumParts());
} //end main
} // end class PartTest
当程序 P2.2 运行时,产生如下输出:
Part: Headlamp
Invalid price: 199.99; Set to -1.0
Name of part: Air Filter
Price: $8.75
Name of part: Ball Joint
Price: $29.95
Name of part: Headlamp
Price: $-1.0
Name of part: Headlamp
Price: $36.99
Number of parts: 3
这是预期的输出,所以我们确信这个类会正常工作。
这里是关于Part类的最后一句话。如果出于某种奇怪的原因,类Part没有提供printPart或toString方法,用户类可以编写自己的方法来打印部件的字段。然而,它必须使用Part的访问器方法来获取对象的数据,因为它不能直接引用private字段。下面显示了如何做到这一点:
public static void printPart(Part p) {
// a method in a user class
System.out.printf("\nName of part: %s\n", p.getName());
System.out.printf("Price: $%3.2f\n", p.getPrice());
}
从用户类,我们可以这样写:
Part af = new Part("Air Filter", 8.75);
printPart(af);
将打印以下内容:
Name of part: Air Filter
Price: $8.75
2.7 如何命名 Java 文件
如果我们的程序由一个单独的public类组成,Java 要求我们将这样一个类存储在一个名为的文件中。所以如果类是Palindrome,我们必须调用文件Palindrome.java。
在Part示例中,我们必须将Part类存储在名为Part.java的文件中,并将PartTest类存储在名为PartTest.java的文件中。我们可以用以下命令编译这些类:
javac Part.java
javac PartTest.java
然后,我们可以使用以下命令运行测试:
java PartTest
回想一下,这将从类PartTest中执行main。请注意,这样的尝试是没有意义的:
java Part
如果我们这样做,Java 会简单地抱怨在类Part中没有main方法。
如果我们愿意,我们可以将两个类放在一个文件中。但是,只能将其中一个类指定为public。因此,例如,我们可以让类PartTest保持原样,并简单地从public class Part中删除单词public。我们现在可以将这两个类放在一个文件中,其中的必须被命名为PartTest.java,因为PartTest是public类。
当我们编译PartTest.java时,Java 会产生两个文件——PartTest.class和Part.class。然后我们可以用这个来运行测试:
java PartTest
2.8 使用对象
到目前为止,我们已经看到了如何使用构造函数定义一个类并从该类创建对象。我们还看到了如何使用访问器方法从对象中检索数据,以及如何使用 mutator 方法更改对象中的数据。我们现在来看看在使用对象时出现的一些问题。
2.8.1 将一个对象变量分配给另一个
一个对象变量(p,比方说)使用一个类名(Part,比方说)来声明,如下所示:
Part p;
我们再次强调,p不能保存一个对象,而是一个指向对象的指针(或引用)。p的值是一个内存地址——存储一个Part对象的位置。请考虑以下几点:
Part a = new Part("Air Filter", 8.75);
Part b = new Part("Ball Joint", 29.95);
假设空气过滤器对象存储在位置3472,球窝接头对象存储在位置5768。那么a的值将是3472,而b的值将是5768。创建两个对象后,我们会看到图 2-5 所示的情况。
图 2-5 。创建两个零件对象后
假设我们将a赋值给c,就像这样:
Part c = a; // assign 3472 to c
这将值3472分配给c;实际上,c(以及a)现在指向空气过滤器对象。我们可以使用任何一个变量来访问对象。例如,下面将空气过滤器对象的价格设置为9.50:
c.setPrice(9.50);
我们有图 2-6 所示的情况。
图 2-6 。将 a 赋值给 c 后
如果我们现在用以下内容检索对象a的价格,将返回空气过滤器的(新)价格:
a.getPrice(); // returns the price 9.50
假设我们写下这个语句:
c = b; // assign 5768 to c
c被赋值5768现在指向球关节对象。它不再指向空气过滤器。我们可以使用b或c来访问球节数据。如果我们有了一个对象的地址,我们就有了操作这个对象所需的所有信息。
2.8.2 失去对对象的访问
请考虑以下几点:
Part a = new Part("Air Filter", 8.75);
Part b = new Part("Ball Joint", 29.95);
假设这些陈述产生了图 2-7 中所示的情况。
图 2-7 。创建两个零件对象后
假设我们执行以下语句:
a = b;
情况变为图 2-8 中的所示。
图 2-8 。将 b 分配给 a 后
现在,a和b具有相同的值5768。它们都指向球关节对象。实际上,当我们改变a的值时,我们失去了对空气过滤器对象的访问。当没有变量指向某个对象时,该对象不可访问,也不能使用。对象占用的存储将被系统垃圾收集,并返回到可用存储池。这是自动发生的,不需要程序的任何动作。
然而,假设我们这样写:
c = a; // c holds 3472, address of "Air Filter"
a = b; // a, b hold 5768, address of "Ball Joint"
现在,我们仍然可以通过c访问空气过滤器。
比较对象变量
考虑以下创建两个相同但独立的对象并将它们的地址存储在a和b中的情况:
Part a = new Part("Air Filter", 8.75);
Part b = new Part("Air Filter", 8.75);
假设这些陈述产生了图 2-9 中所示的情况。
图 2-9 。创建两个相同的对象后
由于对象是相同的,所以下面的条件是false可能会令人惊讶:
a == b
但是,如果你记得a和b包含地址而不是对象,那么我们是在比较a ( 2000)中的地址和b ( 4000)中的地址。既然这些不一样,那就比较false。
只有当两个对象变量包含相同的地址时,它们才会比较相等(在这种情况下,它们指向同一个对象)。例如,当我们将一个对象变量赋给另一个对象变量时,就会发生这种情况。
当然,我们需要知道两个物体是否相同。也就是说,如果a和b指向两个对象,那么这些对象的内容是否相同?为此,我们必须编写自己的方法来逐个比较字段。
以类Part为例,我们编写了一个方法equals,如果一个对象与另一个对象相同,则返回true,否则返回false。比较a和b指向的Part对象的方法如下:
if (a.equals(b)) ...
该方法简单地检查两个对象的name字段和price字段是否相同。由于name字段是String对象,我们调用String类的equals方法来比较它们。 1
public boolean equals(Part p) {
return name.equals(p.name) && (price == p.price);
}
在该方法中,变量name和price(未被限定)指的是通过其调用该方法的对象的字段。假设我们使用了下面的表达式:
a.equals(b)
变量指的是字段a.name和a.price。当然,p.name和p.price指的是equals(例子中的b)的参数字段。实际上,return语句变成了这样:
return a.name.equals(b.name) && (a.price == b.price);
现在,假设我们有这些陈述:
Part a = new Part("Air Filter", 8.75);
Part b = new Part("Air Filter", 8.75);
(a == b)是false(由于a和b持有不同的地址),而a.equals(b)是true(由于它们指向的对象内容相同)。
2.9 空指针
对象变量的声明如下例所示:
Part p;
最初,它是未定义的(就像原始类型的变量一样)。给p赋值最常见的方法是创建一个Part对象,并使用new操作符将其地址存储在p中,如下所示:
p = new Part("Air Filter", 8.75);
Java 还提供了一个特殊的指针值,用null表示,可以赋给任何对象变量。我们可以编写下面的代码将null赋给Part变量p:
Part p = null;
实际上,这意味着p有一个定义好的值,但是它没有指向任何东西。如果p的值为null,则试图引用由p指向的对象是错误的。换句话说,如果p是null,谈论p.name或p.price是没有意义的,因为p没有指向任何东西。
如果两个对象变量p和q都是null,我们可以拿它们和==比较,结果会是true。另一方面,如果p指向某个对象,而q是null,那么,不出所料,比较的是false。
当我们需要初始化一列对象变量时,空指针是很有用的。当我们创建链表或二叉树等数据结构时,我们也会用到它们,我们需要一个特殊的值来表示一个列表的结束。我们将在下一章看到如何使用空指针。
2.10 将对象作为参数传递
对象变量保存一个地址——一个实际对象的地址。当我们使用对象变量作为方法的参数时,传递给方法的是一个地址。因为 Java 中的参数是“按值”传递的,所以实际传递的是包含变量值的临时位置。在第 2.6.1 节中,我们遇到了用于打印零件的类Part中的静态方法printPart。这是:
public static void printPart(Part p) {
System.out.printf("\nName of part: %s\n", p.name);
System.out.printf("Price: $%3.2f\n", p.price);
}
此外,假设用户类包含以下语句:
Part af = new Part("Air Filter", 8.75);
printPart(af);
假设第一条语句将地址4000分配给af。当调用printPart时,4000被复制到一个临时位置,这个位置被传递给printPart,在那里它被称为p,即形参的名字。由于p的值是4000,实际上它可以访问原始对象。在本例中,该方法只是打印实例变量的值。但如果它愿意,也可以改变它们。
考虑类Part中的以下方法,该方法将amount添加到零件的价格中:
public static void changePrice(Part p, double amount) {
p.price += amount;
}
用户类可以通过以下调用将1.50添加到空气过滤器的价格中:
Part.changePrice(af, 1.50);
如前所述,参数p可以访问原始对象。对由p指向的对象的任何改变实际上都是对原始对象的改变。
我们强调方法不能改变实参af的值(因为它没有访问它的权限),但是它可以改变af指向的对象。
顺便说一下,注意我们使用这个例子主要是为了说明的目的。实际上,编写一个实例方法来改变一个Part对象的价格可能会更好。
2.11 对象的数组
在 Java 中,String是一个对象。因此,String的数组实际上是对象的数组。然而,String在 Java 中是一种特殊的对象,在某些方面与其他对象有所不同。首先,一个String是不可变的;我们不能改变它的价值。另一方面,我们认为String只有一个字段——字符串中的字符——而一个典型的对象会有几个字段。出于这些原因,我们来看看除了String之外的对象数组。
考虑之前定义的类Part 。该类包含两个实例变量,定义如下:
public class Part {
private String name; // instance variable
private double price; // instance variable
// methods and static variables
} //end class Part
回忆一下当我们如下声明一个Part变量p时会发生什么是很有帮助的:
Part p;
首先,记住p可以保存一个Part对象的地址,而不是一个对象本身。该声明只是为 p 分配了存储空间*,但是没有定义。我们可以将null值赋给p,如下所示:*
p = null;
我们还可以创建一个Part对象,并使用以下语句将其地址分配给p:
p = new Part("Air Filter", 8.75);
现在考虑下面的声明:
Part[] part = new Part[5];
这声明了一个名为part的数组,该数组包含5个元素。因为它们是对象变量,所以 Java 保证这些元素被设置为null。到目前为止,还没有创建任何Part对象。我们可以创建对象并将它们分别分配给part的每个元素,如下所示:
part[0] = new Part("Air Filter", 8.75);
part[1] = new Part("Ball Joint", 29.95);
part[2] = new Part("Headlamp", 36.99);
part[3] = new Part("Spark Plug", 5.00);
part[4] = new Part("Disc Pads", 24.95);
数组part现在可以如图图 2-10 所示。
图 2-10 。零件对象的数组
part的每个元素都包含对应对象的地址。
请记住,一般来说,数组的每个元素都可以像数组类型的简单变量一样处理。例如,part[2]可以用与上述p相同的方式处理。而且就像我们可以写p.setPrice(40.00)一样,我们可以写part[2].setPrice(40.00)把头灯的价格改成40.00。
我们如何引用一个Part对象的字段?通常,这取决于代码是写在类Part内部还是外部。如果在内部,代码可以直接访问实例变量name和price,例如part[2].name。如果在外部,它必须使用 accessor 和 mutator 方法来获取和设置字段中的值,例如part[2].getName()。
如果我们必须处理数百个零件,最好将零件的数据存储在一个文件中(parts.dat),并使用一个for或while循环将它们读入数组。假设上面的数据像这样存储在文件中(我们将部件名写成一个单词,这样就可以用Scanner类中的next来读取它):
AirFilter 8.75
BallJoint 29.95
Headlamp 36.99
Spark Plug 5.00
DiscPads 24.95
我们可以用下面的代码设置part数组:
Scanner in = new Scanner(new FileReader("parts.dat"));
Part[] part = new Part[5];
for (int h = 0; h < part.length; h++)
part[h] = new Part(in.next(), in.nextDouble());
这段代码更好,也更灵活。要读取 1000 个零件,我们只需要改变part的声明,并提供文件中的数据。上面的代码保持不变。像往常一样,我们没有有用零件数据填充整个数组。我们可以读取数据,直到到达某个数据结束标记(End)为止。
如果我们需要打印零件的数据,我们可以使用以下内容:
for (int h = 0; h < part.length; h++) part[h].printPart();
假设我们想要交换数组中的两个部分,例如,part[2]和part[4]。我们用同样的方法交换任何两个相同类型的变量的值,就像这样:
Part p = part[2];
part[2] = part[4];
part[4] = p;
值得注意的是,实际对象保留在它们最初存储的地方。我们在这里所做的只是交换存储在part[2]和part[4]中的地址。在图 2-10 中,认为箭头被互换了。
2.11.1 找到价格最低的零件
假设我们想找到价格最低的部分(从某种意义上说,我们想找到“最小”的对象)。假设我们在类Part之外编写这个代码,我们可以编写getLowestPrice来返回价格最低的部分的位置,如下所示:
public static int getLowestPrice(Part[] part, int lo, int hi) {
// return the position of the part with the lowest price
// from part[lo] to part[hi], inclusive
int small = lo;
for (int h = lo + 1; h <= hi; h++)
if (part[h].getPrice() < part[small].getPrice()) small = h;
return small;
} //end getLowestPrice
如果我们在类Part内部编写,我们可以让方法保持原样。但是由于我们现在可以直接访问实例变量,我们可以用下面的语句替换if语句:
if (part[h].price < part[small].price) small = h;
要打印价格最低的零件的名称,我们可以这样写:
System.out.printf("\nPart with lowest price: %s\n",
part[getLowestPrice(part, 0, part.length-1)].getName());
作为练习,编写一个函数来返回价格最高的商品。
2.12 搜索一组对象
我们假设您知道如何在基元类型数组或字符串数组中搜索一个项目。这里,我们考虑如何搜索具有多个字段的对象数组(更准确地说,是对对象的引用)。例如,假设我们有一个由以下内容(部分)定义的Person类:
public class Person {
String name;
int age;
char gender;
// constructors, static fields and other methods
} //end class Person
我们希望在包含类型为Person的对象的数组person中搜索一个具有给定名称key的对象。在搜索基本类型或字符串的情况下,搜索关键字的类型与数组中元素的类型相同。在搜索具有多个字段的对象数组的情况下,搜索关键字的类型与对象的字段中的相同。
我们的搜索方法必须将key与正确的字段进行比较。在这个例子中,我们比较了key和person[h].name。下面的方法在数组Person中搜索给定的名称。我们使用equalsIgnoreCase,这样键和数组的大小写差异就无关紧要了;Mary将与mary相同。
// search for key in the first n elements of the array person;
// if found, return the position, else return -1
public static int sequentialSearch(String key, Person[] person, int n) {
for (int h = 0; h < n; h++)
if (key.equalsIgnoreCase(person[h].name)) return h;
return -1;
}
如果我们想搜索给定了age的人,我们只需要将key声明为int,并将if语句改为:
if (key == person[h].age) return h;
注意,这将返回它找到的第一个具有给定年龄的人。我们编写程序 P2.3 来测试这个函数。
程序 P2.3
import java.util.*;
public class SearchTest {
public static void main(String[] args) {
// set up an array with 7 persons
Person[] person = new Person[7];
person[0] = new Person("Gary", 25, 'M');
person[1] = new Person("Inga", 21, 'F');
person[2] = new Person("Abel", 30, 'M');
person[3] = new Person("Olga", 36, 'F');
person[4] = new Person("Nora", 19, 'F');
person[5] = new Person("Mary", 27, 'F');
person[6] = new Person("Bert", 32, 'M');
Scanner in = new Scanner(System.in);
String s;
System.out.printf("Enter names, one at a time, and I'll tell you\n");
System.out.printf("their age and gender. To end, press Enter\n\n");
while (!(s = in.nextLine()).equals("")) {
int n = sequentialSearch(s, person, person.length);
if (n >= 0)
System.out.printf("%d %c\n\n", person[n].age, person[n].gender);
else System.out.printf("Not found\n\n");
}
} // end main
// search for key in the first n elements of the array person ;
// if found, return the position, else return -1
public static int sequentialSearch(String key, Person[] person, int n) {
for (int h = 0; h < n; h++)
if (key.equalsIgnoreCase(person[h].name)) return h;
return -1;
} // end sequentialSearch
} // end class SearchTest
class Person {
String name;
int age;
char gender;
Person(String n, int a, char g) {
name = n;
age = a;
gender = g;
}
} //end class Person
main方法建立了一个名为person的数组,包含七个人的数据。然后,它要求用户输入姓名。对于每一个名字,sequentialSearch被称为;比方说,它返回一个值n。如果找到(n >= 0,则打印出该人的年龄和性别。如果不是,则打印消息Not found。以下是运行示例:
Enter names, one at a time, and I'll tell you
their age and gender. To end, press Enter
Olga
36 F
Bart
Not found
bert
32 M
INGA
21 F
注意我们是如何定义类Person的。我们省略了单词public,这样我们可以把它和SearchTest放在同一个文件中。为了多样化,我们在字段名— name、age、gender上不使用访问修饰符(public或private)。当我们这样做时,同一个文件中的其他类可以直接引用字段名;比如在main中,我们指的是person[n].age和person[n].gender。
我们还可以对一组对象使用二分搜索法,前提是这些对象是根据我们想要搜索的字段进行排序的。例如,如果对象按名称顺序排列,我们可以在person数组中查找名称。下面是函数:
// search for a person with name key in the first n elements of the
// array person ; if found, return the position, else return -1
public static int binarySearch(String key, Person[] person, int n) {
int lo = 0, hi = n - 1;
while (lo <= hi) { // as long as more elements remain to consider
int mid = (lo + hi) / 2;
int cmp = key.compareToIgnoreCase(person[mid].name);
if (cmp == 0) return mid; // search succeeds
if (cmp < 0) hi = mid - 1; // key is ‘less than’ person[mid].name
else lo = mid + 1; // key is ‘greater than’ person[mid].name
}
return -1; // key is not in the array
} // end binarySearch
作为练习,编写一个类似于程序 P2.3 的程序来测试binarySearch。
2.13 排序一个对象数组
我们假设您知道如何使用选择和插入排序对字符串或基本类型的数组进行排序。下面显示了如何使用选择排序按升序通过name对对象数组Person进行排序:
public static void selectionSort(Person[] list, int lo, int hi) {
// sort list[lo] to list[hi] using selection sort
for (int h = lo; h <= hi; h++)
swap(list, h, getSmallest(list, h, hi));
} //end selectionSort
public static int getSmallest(Person[] list, int lo, int hi) {
// return the position of the ‘smallest’ name from list[lo] to list[hi]
int small = lo;
for (int h = lo + 1; h <= hi; h++)
if (list[h].name.compareToIgnoreCase(list[small].name) < 0) small = h;
return small;
} //end getSmallest
public static void swap(Person[] list, int h, int k) {
// swaps list[h] with list[k]
Person hold = list[h];
list[h] = list[k];
list[k] = hold;
} //end swap
在getSmallest中,我们将一个数组元素的name字段与另一个数组元素的name字段进行比较。
我们可以通过下面的调用对来自程序 P2.1 的数组person进行排序:
selectionSort(person, 0, person.length - 1);
然后我们可以打印数组person,如下所示:
for (int h = 0; h < person.length; h++) person[h].printPerson();
其中printPerson在类别Person中定义如下:
void printPerson() {
System.out.printf("%s %d %c\n", name, age, gender);
}
对于程序 P2.3 中的数组,这将打印以下输出:
Abel 30 M
Bert 32 M
Gary 25 M
Inga 21 F
Mary 27 F
Nora 19 F
Olga 36 F
我们还可以使用插入排序对一组Person对象进行排序,如下所示:
public static void insertionSort(Person[] list, int lo, int hi) {
//sort list[lo] to list[hi] in ascending order by name
for (int h = lo + 1; h <= hi; h++) {
Person hold = list[h];
int k = h - 1; //start comparing with previous item
while (k >= 0 && hold.name.compareToIgnoreCase(list[k].name) < 0) {
list[k + 1] = list[k];
--k;
}
list[k + 1] = hold;
} //end for
} //end insertionSort
我们可以用下面的调用按名称对来自程序 P2.3 的数组person进行排序:
insertionSort(person, 0, person.length - 1);
在while条件下,我们将被处理的人的name字段(在h位置的人)与数组元素的name字段进行比较。
2.14 使用类对数据进行分组:词频计数
在 1.8 节中,我们编写了一个程序(程序 P1.7 )来统计一篇文章中单词的频率。在这里,我们使用一个String数组(wordlist)来保存单词,使用一个int数组(frequency)来保存频率。代码是这样写的,frequency[i]保存了wordlist[i]中单词的计数。我们现在展示如何通过使用类以稍微不同的方式解决相同的问题。
我们可以把文章中的每个单词想象成一个具有两种属性的物体——单词中的字母和它出现的次数。我们将定义一个类WordInfo,从中我们将创建“word 对象”
class WordInfo {
String word;
int freq = 0;
WordInfo(String w, int f) {
word = w;
freq = f;
}
void incrFreq() {
freq++;
}
} //end class WordInfo
该类有两个字段:word和freq。它有一个构造函数将一个WordInfo对象初始化为给定的单词和频率。它还有一个方法,就是给一个词的频率加 1。假设wo是用这条语句创建的WordInfo对象:
WordInfo wo = new WordInfo(aWord, 1); //String aWord
wo.word指词,wo.freq是其频率。我们可以用wo.incrFreq()给它的频率加 1。
接下来,我们将定义一个WordInfo数组;每个元素将保存关于一个单词的信息。
WordInfo[] wordTable = new WordInfo[MaxWords + 1];
MaxWords表示满足的不同单词的最大数量。为了测试程序,我们使用了50作为这个值。如果文章中不同单词的数量超过了MaxWords(比如说 50),那么第 50 个之后的所有单词都将被读取,但不会被存储,并且会打印一条大意如此的消息。然而,如果再次遇到,已经存储的单词的计数将增加。
这些想法的实现如程序 P2.4 所示。
程序 P2.4
import java.io.*;
import java.util.*;
public class P2_4WordFrequency {
final static int MaxWords = 50;
public static void main(String[] args) throws IOException {
WordInfo[] wordTable = new WordInfo[MaxWords];
FileReader in = new FileReader("passage.txt");
PrintWriter out = new PrintWriter(new FileWriter("output.txt"));
for (int h = 0; h < MaxWords; h++) wordTable[h] = new WordInfo("", 0);
int numWords = 0;
String word = getWord(in).toLowerCase();
while (!word.equals("")) {
int loc = binarySearch(word, wordTable, 0, numWords-1);
if (word.compareTo(wordTable[loc].word) == 0) wordTable[loc].incrFreq();
else //this is a new word
if (numWords < MaxWords) { //if table is not full
addToList(word, wordTable, loc, numWords-1);
++numWords;
}
else out.printf("'%s' not added to table\n", word);
word = getWord(in).toLowerCase();
}
printResults(out, wordTable, numWords);
in.close();
out.close();
} //end main
public static int binarySearch(String key, WordInfo[] list, int lo, int hi) {
//search for key from list[lo] to list[hi]
//if found, return its location;
//if not found, return the location in which it should be inserted
//the calling program will check the location to determine if found
while (lo <= hi) {
int mid = (lo + hi) / 2;
int cmp = key.compareTo(list[mid].word);
if (cmp == 0) return mid; // search succeeds
if (cmp < 0) hi = mid -1; // key is 'less than' list[mid].word
else lo = mid + 1; // key is 'greater than' list[mid].word
}
return lo; //key must be inserted in location lo
} //end binarySearch
public static void addToList(String item, WordInfo[] list, int p, int n) {
//sets list[p].word to item; sets list[p].freq to 1
//shifts list[n] down to list[p] to the right
for (int h = n; h >= p; h--) list[h + 1] = list[h];
list[p] = new WordInfo(item, 1);
} //end addToList
public static void printResults(PrintWriter out, WordInfo[] list, int n) {
out.printf("\nWords Frequency\n\n");
for (int h = 0; h < n; h++)
out.printf("%-20s %2d\n", list[h].word, list[h].freq);
} //end printResults
public static String getWord(FileReader in) throws IOException {
//returns the next word found
final int MaxLen = 255;
int c, n = 0;
char[] word = new char[MaxLen];
// read over non-letters
while (!Character.isLetter((char) (c = in.read())) && (c != -1)) ;
//empty while body
if (c == -1) return ""; //no letter found
word[n++] = (char) c;
while (Character.isLetter(c = in.read()))
if (n < MaxLen) word[n++] = (char) c;
return new String(word, 0, n);
} //end getWord
} //end class P2_4WordFrequency
class WordInfo {
String word;
int freq = 0;
WordInfo(String w, int f) {
word = w;
freq = f;
}
void incrFreq() {
freq++;
}
} //end class WordInfo
假设文件passage.txt包含以下数据:
Strive not to be a success, but rather to be of value.
Whatever the mind can conceive and believe, it can achieve.
There is only one way to avoid criticism: do nothing, say nothing,
and be nothing.
运行时,程序 P2.4 将其输出存储在文件output.txt中。以下是输出:
Words Frequency
a 1
achieve 1
and 2
avoid 1
be 3
believe 1
but 1
can 2
conceive 1
criticism 1
do 1
is 1
it 1
mind 1
not 1
nothing 3
of 1
one 1
only 1
rather 1
say 1
strive 1
success 1
the 1
there 1
to 3
value 1
way 1
whatever 1
2.15 如何返回多个值:投票
这个例子将用来说明关于类和对象的使用的几个问题。我们将再次使用一个类来分组数据,我们将展示一个函数如何使用一个对象返回多个值。
- 问题:一次选举,有七个候选人。每个选民都被允许为他们选择的候选人投一票。投票记录为从
1到7的数字。投票人的数量事先不得而知,但投票会因0的投票而终止。任何不是从1到7的数字的投票都是无效(作废)票。 - 一个文件,
votes.txt,包含了候选人的名字。第一个名字被视为候选人 1,第二个被视为候选人 2,依此类推。名字后面是投票。写一个程序来读取数据并评估选举的结果。将所有输出打印到文件results.txt。 - 您的输出应该指定总投票数、有效投票数和无效投票数。接下来是每位候选人和选举获胜者获得的票数。
给定votes.txt中的数据:
Nirvan Singh
Denise Duncan
Avasa Tawari
Torrique Granger
Saskia Kalicharan
Dawren Greenidge
Jordon Cato
3 1 6 5 4 3 5 3 5 3 2 8 1 6 7 7 3 5
6 9 3 4 7 1 2 4 5 5 1 4 0
您的程序应该向results.txt发送以下输出:
Invalid vote: 8
Invalid vote: 9
Number of voters: 30
Number of valid votes: 28
Number of spoilt votes: 2
Candidate Score
Nirvan Singh 4
Denise Duncan 2
Avasa Tawari 6
Torrique Granger 4
Saskia Kalicharan 6
Dawren Greenidge 3
Jordon Cato 3
The winner(s)
Avasa Tawari
Saskia Kalicharan
我们将使用以下大纲来解决这个问题:
get the names and set the scores to 0
process the votes
print the results
我们需要存储七个候选人的名字和每个人得到的分数。我们可以使用一个String数组来表示名字,使用一个int数组来表示分数。但是如果我们需要存储候选人更多的属性呢?对于每个属性,我们需要添加另一个适当类型的数组。为了适应这种可能性并使我们的程序更加灵活,我们将创建一个类Person并使用一个数组Person。
这个Person类会是什么样子?对于我们的问题,它将有两个实例字段,name和numVotes。我们将其定义如下:
class Person {
String name;
int numVotes;
Person(String s, int n) {
name = s;
numVotes = n;
}
} //end class Person
为了迎合七个候选对象,我们将符号常数MaxCandidates设置为7,并将Person数组candidate声明如下:
Person[] candidate = new Person[MaxCandidates+1];
我们将使用candidate[h]来存储候选人h、h = 1、7的信息;我们不会用candidate[0]。这将使我们能够比使用candidate[0]更自然地处理投票。例如,如果有候选人 4 的投票,我们希望增加candidate[4]的投票数。如果我们使用candidate[0]存储第一个候选人的信息,我们将不得不增加candidate[3]的计数,假设投票数为 4。这可能会误导和令人不安。
假设in声明如下:
Scanner in = new Scanner(new FileReader("votes.txt"));
我们将读取姓名,并用以下代码将分数设置为 0:
for (int h = 1; h <= MaxCandidates; h++)
candidate[h] = new Person(in.nextLine(), 0);
当执行这段代码时,我们可以描绘出如图图 2-11 所示的candidate。记住,我们没有使用candidate[0]。
图 2-11 。读取名称并将分数设置为 0 后的候选数组
接下来,我们必须处理投票。我们将把它委托给函数processVotes。这将读取每张选票,并为合适的候选人增加 1 分。因此,如果投票数为 5,则候选人 5 的得分必须加 1。
该函数的另一个任务是统计有效和无效投票的数量,并将这些值返回给main。但是一个函数如何返回多个值呢?嗯,它可以返回一个值——一个对象——这个对象可以包含许多字段。
在这个例子中,我们可以声明一个带有两个字段和一个构造函数的类(VoteCount),如下所示:
class VoteCount {
int valid, spoilt;
VoteCount(int v, int s) {
valid = v;
spoilt = s;
}
}
下面的语句将创建一个名为votes的对象,并将votes.valid和votes.spoilt设置为0。
VoteCount votes = new VoteCount(0, 0);
我们也可以省去构造函数,用下面的语句创建对象:
VoteCount votes = new VoteCount();
这个将将字段初始化为0,但是最好显式地这样做,就像这样:
votes.valid = votes.spoilt = 0;
当我们读到一个有效的投票时,++votes.valid给votes.valid加 1,当我们读到一个无效的投票时,++votes.spoilt给votes.spoilt加 1。最后,该函数将返回votes——一个包含两个计数的对象。
最后,我们必须编写printResults,它以前面指定的格式打印结果。首先,我们打印有效和无效投票的总票数。然后,使用一个for循环,我们打印个人得分。
接下来,它通过调用getLargest找到其numVotes字段最大的候选项来确定获胜分数。这是通过以下语句实现的:
int win = getLargest(list, 1, MaxCandidates);
int winningVote = list[win].numVotes;
这里,list是Person数组。使用winningVote,它再次遍历数组,寻找具有这个分数的候选者。这确保了如果获胜者有平局,所有的都将被打印。程序 P2.5 是解决这个投票问题的完整解决方案。
程序 P2.5
import java.util.*;
import java.io.*;
public class Voting {
final static int MaxCandidates = 7;
public static void main(String[] args) throws IOException {
Scanner in = new Scanner(new FileReader("votes.txt"));
PrintWriter out = new PrintWriter(new FileWriter("results.txt"));
Person[] candidate = new Person[MaxCandidates+1];
//get the names and set the scores to 0
for (int h = 1; h <= MaxCandidates; h++)
candidate[h] = new Person(in.nextLine(), 0);
VoteCount count = processVotes(candidate, MaxCandidates, in, out);
printResults(out, candidate, MaxCandidates, count);
in.close();
out.close();
} //end main
public static VoteCount processVotes(Person[] list, int max, Scanner in, PrintWriter out) {
VoteCount votes = new VoteCount(0, 0); //set valid, spoilt counts to 0
int v = in.nextInt();
while (v != 0) {
if (v < 1 || v > max) {
out.printf("Invalid vote: %d\n", v);
++votes.spoilt;
}
else {
++list[v].numVotes;
++votes.valid;
}
v = in.nextInt();
} //end while
return votes;
} //end processVotes
public static void printResults(PrintWriter out, Person[] list, int max, VoteCount votes) {
out.printf("\nNumber of voters: %d\n", votes.valid + votes.spoilt);
out.printf("Number of valid votes: %d\n", votes.valid);
out.printf("Number of spoilt votes: %d\n", votes.spoilt);
out.printf("\nCandidate Score\n\n");
for (int h = 1; h <= MaxCandidates; h++)
out.printf("%-18s %3d\n", list[h].name, list[h].numVotes);
out.printf("\nThe winner(s)\n");
int win = getLargest(list, 1, MaxCandidates);
int winningVote = list[win].numVotes;
for (int h = 1; h <= MaxCandidates; h++)
if (list[h].numVotes == winningVote) out.printf("%s\n", list[h].name);
} //end printResults
public static int getLargest(Person[] list, int lo, int hi) {
int big = lo;
for (int h = lo + 1; h <= hi; h++)
if (list[h].numVotes > list[big].numVotes) big = h;
return big;
} //end getLargest
} //end class Voting
class Person {
String name;
int numVotes;
Person(String s, int n) {
name = s;
numVotes = n;
}
} //end class Person
class VoteCount {
int valid, spoilt;
VoteCount(int v, int s) {
valid = v;
spoilt = s;
}
} //end class VoteCount
如果我们想按字母顺序打印结果,我们可以用下面的语句调用selectionSort(第 2.13 节):
selectionSort(candidate, 1, MaxCandidates);
我们可以通过像这样调用insertionSort(第 2.13 节)来获得相同的结果:
insertionSort(candidate, 1, MaxCandidates);
但是假设我们想按收到的票数从到的顺序打印候选人的名字,也就是说,首先打印获胜的候选人。为此,对象数组candidate必须使用numVotes字段控制排序,以降序排序。这可以通过下面的调用来完成,其中sortByVote使用插入排序(任何排序都可以),并使用形参list来编写:
sortByVote(candidate, 1, MaxCandidates);
我们可以这样写sortByVote:
public static void sortByVote(Person[] list, int lo, int hi) {
//sort list[lo] to list[hi] in descending order by numVotes
for (int h = lo + 1; h <= hi; h++) {
Person hold = list[h];
int k = h - 1; //start comparing with previous item
while (k >= lo && hold.numVotes > list[k].numVotes) {
list[k + 1] = list[k];
--k;
}
list[k + 1] = hold;
} //end for
} //end sortByVote
假设我们将sortByVote添加到程序 P2.5 中,我们插入以下语句:
sortByVote(candidate, 1, MaxCandidates);
就在这个之前:
printResults(out, candidate, MaxCandidates, count);
如果我们使用与之前相同的数据运行程序,它将产生以下输出。首先打印候选人的最高分,最后打印最低分。
Invalid vote: 8
Invalid vote: 9
Number of voters: 30
Number of valid votes: 28
Number of spoilt votes: 2
Candidate Score
Avasa Tawari 6
Saskia Kalicharan 6
Nirvan Singh 4
Torrique Granger 4
Dawren Greenidge 3
Jordon Cato 3
Denise Duncan 2
The winner(s)
Avasa Tawari
Saskia Kalicharan
练习 2
- 对象的状态是什么意思?什么决定了一个物体的状态?
- 区分类和对象。
- 区分类变量和实例变量。
- 区分类方法和实例方法。
- 区分公共变量和私有变量。
- 解释执行语句
String S = new String("Hi")时会发生什么。 - 创建对象时,实例字段初始化为什么值?
- 什么是无参数构造函数?它是如何提供给一个班级的?
- 你已经为一个类写了一个构造函数。使用无参数构造函数需要做什么?
- 术语数据封装是什么意思?
- "对象变量不包含对象."解释一下。
- 解释 Java 中
toString()方法的作用。 - 写一个程序将名字和电话号码读入一个对象数组。询问对方的姓名,并打印出他的电话号码。
- 写一个程序,把英语单词和它们对应的西班牙语单词读入一个对象数组。要求用户键入几个英语单词。对于每一项,打印相应的西班牙语单词。选择合适的数据结束标记。使用二分搜索法搜索键入的单词。
- 日期由日、月和年组成。编写一个类来创建日期对象和操作日期。例如,编写一个函数,给定两个日期
d1和d2,如果d1在d2之前,则返回-1,如果d1与d2相同,则返回0,如果d1在d2之后,则返回1。此外,编写一个返回d2比d1早多少天的函数。如果d2在d1之前,返回一个负值。并编写一个方法以您选择的格式打印日期。 - 24 小时制格式的时间由两个数字表示;例如,16 45 表示时间 16:45,即下午 4:45。使用一个对象表示时间,编写一个函数,给定两个时间对象
t1和t2,返回从t1到t2的分钟数。例如,如果两个给定时间是16 45和23 25,你的函数应该返回400。 - 考虑使用分数的问题,其中分数由两个整数值表示,一个表示分子,另一个表示分母。例如,5/9 由两个数字 5 和 9 表示。写一个处理分数的类。例如,编写加减乘除分数的方法。此外,写一个方法,以减少一个分数,以其最低的条款;你需要找到两个整数的 HCF。
- 书商需要存储关于书籍的信息。对于每本书,他都希望存储作者、书名、价格和库存数量。他还需要随时知道已经创建了多少图书对象。根据以下内容为类
Book编写 Java 代码: * 编写一个无参数构造函数,将作者设置为无作者,将标题设置为无标题,将价格和库存数量设置为 0。 * 编写一个构造函数,给定四个参数——作者、标题、价格和数量——用给定的值创建一个Book对象。价格必须至少为 5 美元,数量不能为负。如果违反这些条件中的任何一个,价格和数量都被设置为 0。 * 为 author 和 price 字段编写访问器方法。 * 写一个方法,将一本书的价格设置为一个给定值。如果给定的价格不是至少 5 美元,价格应该保持不变。 * 编写一个方法,将库存数量减少给定的数量。如果这样做会使数量为负,则应打印一条消息,数量保持不变。 * 编写一个实例方法,打印书籍的数据,每行一个字段。 * 编写一个toString()方法,返回一个字符串,如果打印,将打印一本书的数据,每行一个字段。 * 编写一个equals方法,如果两个Book对象的内容相同,则返回 true,否则返回 false。 * 编写一个Test类,创建您选择的三个Book对象,打印它们的数据,并打印所创建的Book对象的数量。 - A multiple-choice examination consists of 20 questions. Each question has five choices, labeled
A,B,C,D, andE. The first line of data contains the correct answers to the 20 questions in the first 20 consecutive character positions. Here’s an example:
BECDCBAADEBACBAEDDBE
随后的每一行都包含候选人的答案。一行中的数据由一个候选数字(整数)组成,后面是一个或多个空格,再后面是候选人在接下来的 20 个*连续*字符位置给出的 20 个答案。如果候选人没有回答某个特定的问题,则使用`X`。您可以假设所有数据都是有效的,并存储在一个名为`exam.dat`的文件中。示例行如下:
`4325 BECDCBAXDEBACCAEDXBE`
最多有 100 名候选人。包含“候选编号”`0`的行仅表示数据的结束。
一个问题的积分奖励如下:正确答案:4 分;回答错误:-1 分;没有回答:0 分。
编写一个程序来处理数据,并打印一份报告,该报告由候选人编号和候选人获得的总分数组成,按候选人编号升序排列。(这个问题也在第一章中,但是这次你将使用对象来解决它)。
20. 一个数据文件包含六个课程的注册信息—CS20A、CS21A、CS29A、CS30A、CS35A 和 CS36A。每一行数据都由一个七位数的学生注册号后跟六个(有序)值组成,每个值都是0或1。值1表示学生注册了相应的课程;0表示学生不是。因此,1 0 1 0 1 1意味着学生注册了 CS20A、CS29A、CS35A 和 CS36A,但没有注册 CS21A 和 CS30A。你可以假设学生不超过 100 人,一个注册号0结束数据。编写一个程序来读取数据,并为每门课程生成一个课程表。每个列表都从一个新的页面开始,由参加该课程的学生的注册号组成。
1 使用相同的名称equals来比较Part s 和String s 没有冲突,如果equals是通过Part对象调用的,那么就使用Part类中的equals方法。如果通过一个String对象调用equals,那么就使用来自String类的equals方法。