C++ 编程学习手册(五)
九、搜索、排序和合并
在本章中,我们将解释以下内容:
- 如何使用顺序搜索来搜索列表
- 如何使用选择排序对列表进行排序
- 如何使用插入排序对列表进行排序
- 如何对字符串列表进行排序
- 如何对并行数组进行排序
- 如何使用二分搜索法搜索排序列表
- 如何合并两个排序列表
9.1 顺序搜索
在许多情况下,数组用于存储信息列表。存储信息后,可能需要在列表中找到给定的项目。例如,一个数组可以用来存储 50 个人的名单。然后可能需要找到给定名字(Indira)在列表中的存储位置。
我们需要开发一种技术来搜索给定特定数组的元素。因为给定的项可能不在数组中,所以我们的技术也必须能够确定这一点。不管数组中元素的类型如何,搜索项的技术都是相同的。然而,对于不同类型的元素,该技术的实现可能是不同的。
我们将使用一个整数数组来说明称为顺序搜索的技术。考虑七个整数的数组num:
我们希望确定数字61是否被存储。在搜索术语中,61被称为搜索关键字,或者简称为关键字。搜索过程如下:
- 将
61与第一个数字num[0]进行比较,第一个数字是35;它们不匹配,所以我们继续下一个数字。 - 将
61与第二个数字num[1]比较,第二个数字是17;它们不匹配,所以我们继续下一个数字。 - 将
61与第三个数字num[2]相比较,第三个数字是48;它们不匹配,所以我们继续下一个数字。 - 将
61与第 4 个数字num[3]进行比较,第 4 个数字是25;它们不匹配,所以我们继续下一个数字。 - 将
61与第 5 个数字num[4]进行比较,第 5 个数字是61;它们匹配,所以搜索停止,我们断定钥匙在位置4。
但是如果我们在找32呢?在这种情况下,我们将比较32和数组中的所有数字,没有一个匹配。我们断定32不在阵列中。
假设数组包含n个数字,我们可以将上述逻辑表达如下:
for h = 0 to n - 1
if (key == num[h]) then key found, exit the loop
endfor
if h < n then key found in position h
else key not found
在这种情况下,我们可能想在查看完数组中的所有元素之前退出循环。另一方面,我们可能必须查看所有元素,然后才能得出结论,关键不在那里。
如果我们找到了密钥,我们就退出循环,h将小于n。如果我们因为h变成n而退出循环,那么这个键不在数组中。
让我们用函数search来表达这种技术,给定一个int数组num,一个整数key,以及两个整数lo和hi,从num[lo]到num[hi]搜索key。如果找到,函数返回数组中的位置。如果没有找到,则返回-1。例如,考虑以下语句:
n = search(num, 61, 0, 6);
这将在num[0]到num[6]中搜索61。它将在位置4找到它并返回4,然后存储在n中。电话
search(num, 32, 0, 6)
将返回-1,因为32没有存储在数组中。search功能如下:
int search(int num[], int key, int lo, int hi) {
//search for key from num[lo] to num[hi]
for (int h = lo; h <= hi; h++)
if (key == num[h]) return h;
return -1;
} //end search
我们首先设置h到lo从那个位置开始搜索。for循环“遍历”数组的元素,直到找到键或者h通过hi。
为了举例说明如何使用搜索,考虑一下上一章的投票问题。计完票数后,我们的数组name和vote如下所示(记住我们没有使用name[0]和vote[0]):
假设我们想知道收到了多少张选票。我们必须在name数组中搜索她的名字。当我们找到它时(在6的位置),我们可以从vote[6]检索她的投票。一般来说,如果一个名字在n的位置,得到的票数将是vote[n]。
我们修改我们的搜索函数,在name数组中查找一个名字:
//search for key from name[lo] to name[hi]
int search(char name[][MaxNameLength+1], char key[], int lo, int hi) {
for (int h = lo; h <= hi; h++)
if (strcmp(key, name[h]) == 0) return h;
return -1;
}
回想一下,我们使用strcmp来比较两个字符串。为了使用任何预定义的字符串函数,我们必须使用指令
#include <string.h>
我们节目的负责人。
我们可以如下使用该函数:
n = search(name, "Carol Khan", 1, 7);
if (n > 0) printf("%s received %d vote(s)\n", name[n], vote[n]);
else printf("Name not found\n");
使用我们的样本数据,search将返回存储在n中的6,。从6 > 0开始,代码将被打印
Carol Khan received 2 vote(s)
9.2 选择排序
考虑 8.15 节的投票程序。在程序 P8.8 中,我们按照给出名字的顺序打印结果。但是,假设我们想按姓名的字母顺序或按收到的票数顺序打印结果,获胜者排在第一位。我们必须按照我们想要的顺序重新排列名字或选票。我们说我们必须将名字按升序排序,或者将选票按降序排序。
排序是将一组值按升序或降序排列的过程。排序的原因有很多。有时我们排序是为了产生更可读的输出(例如,产生一个按字母顺序排列的列表)。教师可能需要按姓名或平均分对学生进行排序。如果我们有一个很大的值集,并且我们想要识别重复项,我们可以通过排序来实现;重复的值将一起出现在排序列表中。排序的方式有很多种。我们将讨论一种叫做选择排序的方法。
考虑以下阵列:
使用选择排序按升序对num进行排序的过程如下:
第一遍
-
找出位置
0到6的最小数字;最小的是15,位于4位置。 -
Interchange the numbers in positions
0and4. We get this:
第二遍
-
找出位置
1到6的最小数字;最小的是33,位于5位置。 -
Interchange the numbers in positions
1and5. We get this:
第三遍
-
找出位置
2到6的最小数字;最小的是48,位于5位置。 -
Interchange the numbers in positions
2and5. We get this:
第四遍
-
找出位置
3到6的最小数字;最小的是52,位于6位置。 -
Interchange the numbers in positions
3and6. We get this:
第五遍
-
找出位置
4到6的最小数字;最小的是57,位于4位置。 -
Interchange the numbers in positions
4and4. We get this:
第六遍
-
找出位置
5到6的最小数字;最小的是65,位于6位置。 -
Interchange the numbers in positions
5and6. We get this:
现在数组已经完全排序了。
如果我们让h从0到5,在每一遍中:
- 我们从位置
h到6找到最小的数字。 - 如果最小的数字在位置
s,我们交换位置h和s的数字。 - 对于大小为
n的数组,我们进行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
在 8.14 节,我们写了一个函数来返回整数数组中最小数的位置。这里是为了便于参考:
//find position of smallest from num[lo] to num[hi]
int getSmallest(int num[], int lo, int hi) {
int small = lo;
for (int h = lo + 1; h <= hi; h++)
if (num[h] < num[small]) small = h;
return small;
} //end getSmallest
我们还编写了一个函数swap,它交换了一个字符数组中的两个元素。我们现在重写swap来交换整数数组中的两个元素:
//swap elements num[i] and num[j]
void swap(int num[], int i, int j) {
int hold = num[i];
num[i] = num[j];
num[j] = hold;
} //end swap
有了getSmallest和swap,我们可以将上面的算法编码成函数selectionSort。为了强调我们可以为参数使用任何名称,我们编写了一个函数来对名为list的整数数组进行排序。为了通用,我们还通过指定下标lo和hi来告诉函数对数组的哪一部分进行排序。与算法中从0到n-2的循环不同,现在是从lo到hi-1——这只是一个微小的变化,以获得更大的灵活性。
//sort list[lo] to list[hi] in ascending order
void selectionSort(int list[], int lo, int hi) {
int getSmallest(int [], int, int);
void swap(int [], int, int);
for (int h = lo; h < hi; h++) {
int s = getSmallest(list, h, hi);
swap(list, h, s);
}
} //end selectionSort
我们现在编写程序 P9.1 来测试selectionSort是否正常工作。程序请求最多 10 个数字(因为数组被声明为大小为 10),将它们存储在数组num中,调用selectionSort,然后打印排序后的列表。
Program P9.1
#include <stdio.h>
int main() {
void selectionSort(int [], int, int);
int v, num[10];
printf("Type up to 10 numbers followed by 0\n");
int n = 0;
scanf("%d", &v);
while (v != 0) {
num[n++] = v;
scanf("%d", &v);
}
//n numbers are stored from num[0] to num[n-1]
selectionSort(num, 0, n-1);
printf("\nThe sorted numbers are\n");
for (int h = 0; h < n; h++) printf("%d ", num[h]);
printf("\n");
} //end main
void selectionSort(int list[], int lo, int hi) {
//sort list[lo] to list[hi] in ascending order
int getSmallest(int [], int, int);
void swap(int [], int, int);
for (int h = lo; h < hi; h++) {
int s = getSmallest(list, h, hi);
swap(list, h, s);
}
} //end selectionSort
int getSmallest(int num[], int lo, int hi) {
//find position of smallest from num[lo] to num[hi]
int small = lo;
for (int h = lo + 1; h <= hi; h++)
if (num[h] < num[small]) small = h;
return small;
} //end getSmallest
void swap(int num[], int i, int j) {
//swap elements num[i] and num[j]
int hold = num[i];
num[i] = num[j];
num[j] = hold;
} //end swap
以下是该程序的运行示例:
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
对程序 P9.1 的评论
这个程序演示了如何在一个数组中读取和存储未知数量的值。该程序最多支持 10 个数字,但如果提供的数字更少,它也必须工作。我们使用n给数组加下标并计数。最初,n是0。下面描述了样本数据的情况:
-
读取第一个数字
57;它不是0,所以我们进入 while 循环。我们将57存储在num[0]中,然后将1添加到n,使其成为1;已读取一个数字,且n为1。 -
读取第二个数字
48;它不是0,所以我们进入the while循环。我们将48存储在num[1]中,然后将1加到n,使其成为2;已读取两个数字,n为2。 -
读取第三个数字
79;它不是0,所以我们进入while循环。我们将79存储在num[2]中,然后将1加到n,使其成为3;已经读取了三个数字,n是3。 -
第 4 个数字,
65,读出;它不是0,所以我们进入while循环。我们将65存储在num[3]中,然后将1加到n,使其成为4;已经读取了四个数字,n是4。 -
第 5 个数字,
15,读出;它不是0,所以我们进入while循环。我们将15存储在num[4]中,然后将1加到n,使其成为5;已读取五个数字,n为5。 -
第 6 个数字,
33,读;它不是0,所以我们进入while循环。我们将33存储在num[5]中,然后将1加到n,使其成为6;已读取六个数字,n为6。 -
第 7 个数字,
52,读;它不是0,所以我们进入while循环。我们将52存储在num[6]中,然后将1加到n,使其成为7;已读取七个数字,n为7。 -
The 8th number,
0, is read; it is0so we exit thewhileloop and the array looks like this:
在任何阶段,n的值表示到那时为止已经存储了多少个数字。最后,n是7,数组中存储了七个数字。程序的其余部分可以假设n给出了数组中实际存储的值的数量;从num[0]到num[n-1]存储数值。
例如,调用
selectionSort(num, 0, n-1);
是对num[0]到num[n-1]进行排序的请求,但是,由于n是7,所以是对num[0]到num[6]进行排序的请求。
如前所述,如果用户在输入0之前输入超过 10 个数字,程序将会崩溃。当读取第 11 个数字时,将试图将其存储在不存在的num[10]中,给出“数组下标”错误。
我们可以通过将while条件改为这样来处理:
while (v != 0 && n < 10)
现在,如果n达到10,则不进入循环(因为10不小于10,并且不会尝试存储第 11 个数字。事实上,第 10 个数字之后的所有数字都将被忽略。
通常,最好在整个程序中使用一个设置为10的符号常量(MaxNum),并使用MaxNum,而不是常量10。
我们已经按升序对数组进行了排序。我们可以用下面的算法对num[0]到num[n-1]进行降序排序:
for h = 0 to n - 2
b = position of biggest number from num[h] to num[n-1]
swap num[h] and num[b]
endfor
我们建议您尝试练习 1 和 2,按姓名升序和收到的票数降序打印投票问题的结果。
9.2.1 选择排序分析
为了找到 k 个项目中最小的一个,我们进行 k-1 次比较。在第一遍中,我们进行 n-1 次比较,找出 n 个项目中最小的一个。在第二遍中,我们进行 n-2 次比较,找出 n-1 项中最小的一项。以此类推,直到最后一遍,我们进行一次比较,找出两个项目中较小的一个。一般来说,在第 I 遍中,我们进行 n-i 次比较,以找到 n-i+1 项中最小的一项。因此:
比较总数= 1 + 2 +...+ n-1 = n(n-1) ≈ n 2
我们说选择排序的顺序是 O(n 2 )(“大 on 的平方”)。常量在“大 O”符号中并不重要,因为当 n 变得很大时,常量变得无关紧要。
每一次,我们用三个任务交换两个项目。我们进行了 n-1 次传递,因此我们总共进行了 3(n-1)次分配。使用“大 O”符号,我们说赋值的个数是 O(n)。常量 3 和 1 并不重要,因为 n 变大了。
如果数据是有序的,选择排序的性能会更好吗?不。一种方法是给它一个排序列表,看看它做什么。如果你完成了这个算法,你会发现这个方法不考虑数据的顺序。不管数据如何,它每次都会进行相同次数的比较。
作为一个练习,修改程序代码,使其计算使用选择排序对列表进行排序时的比较和赋值次数。
9.3 插入排序
考虑与之前相同的阵列:
把数字想象成桌子上的卡片,按照它们在数组中出现的顺序一次拿起一张。因此,我们首先拿起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进行排序的过程如下:
第一遍
-
Process
num[1], that is,48. This involves placing48so that the first two numbers are sorted;num[0]andnum[1]now contain the following:
数组的其余部分保持不变。
第二遍
-
Process
num[2], that is,79. This involves placing79so that the first three numbers are sorted;num[0]tonum[2]now contain the following:
数组的其余部分保持不变。
第三遍
-
Process
num[3], that is,65. This involves placing65so that the first four numbers are sorted;num[0] to num[3]now contain the following:
数组的其余部分保持不变。
第四遍
-
Process
num[4], that is,15. This involves placing15so that the first five numbers are sorted. To simplify the explanation, think of15as 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。
第五遍
-
流程
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]。
第六遍
-
流程
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。
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]中。但是当k为-1时list[k+1]为list[0],所以在这种情况下key插入正确。
该函数按升序排序。要按降序排序,我们所要做的就是改变while条件中的< to >,因此:
while (k >= 0 && key > list[k])
现在,如果一个键变大了,它就会向左移动。
我们编写程序 P9.2 来测试insertionSort是否正确工作。
Program P9.2
#include <stdio.h>
int main() {
void insertionSort(int [], int);
int v, num[10];
printf("Type up to 10 numbers followed by 0\n");
int n = 0;
scanf("%d", &v);
while (v != 0) {
num[n++] = v;
scanf("%d", &v);
}
//n numbers are stored from num[0] to num[n-1]
insertionSort(num, n);
printf("\nThe sorted numbers are\n");
for (int h = 0; h < n; h++) printf("%d ", num[h]);
printf("\n");
} //end main
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
程序请求最多 10 个数字(因为数组被声明为大小为 10),将它们存储在数组num中,调用insertionSort,然后打印排序后的列表。以下是 P9.2 的运行示例:
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
9.3.1 插入排序分析
在处理 j 项时,我们可以进行少至一次的比较(如果num[j]大于num[j-1])或多达 j-1 次的比较(如果num[j]小于前面所有的项)。对于随机数据,平均来说,我们应该进行(j-1)次比较。因此,对 n 个项目进行排序的平均总比较次数如下:
我们说插入排序的阶数为 O(n 2 )(“大 on 的平方”)。随着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 (最差)。赋值的次数总是与比较的次数相同。
作为一个练习,修改程序代码,使其计算使用插入排序对列表进行排序时的比较和赋值次数。
9.3.2 在适当的位置插入一个元素
插入排序使用向已经排序的列表中添加新元素的思想,以便列表保持排序。我们可以把它本身当作一个问题(与插入排序无关)。具体来说,给定一个从list[m]到list[n]的排序列表,我们想要向列表中添加一个新的条目(比如说newItem),以便对list[m]到list[n+1]进行排序。
添加新项目会使列表的大小增加 1。我们假设数组有空间容纳新的项目。我们编写函数insertInPlace来解决这个问题。
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)重写如下:
void insertionSort2(int list[], int lo, int hi) {
//sort list[lo] to list[hi] in ascending order
void insertInPlace(int, int [], int, int);
for (int h = lo + 1; h <= hi; h++)
insertInPlace(list[h], list, lo, h - 1);
} //end insertionSort2
请注意,insertionSort2的原型现在是这样的:
void insertionSort2(int [], int, int);
为了对包含n项的数组num进行排序,我们必须这样调用它:
insertionSort2(num, 0, n-1);
9.4 对字符串数组进行排序
考虑按字母顺序排列姓名列表的问题。我们已经看到,在 C 中,每个名字都存储在一个字符数组中。为了存储几个名字,我们需要一个二维字符数组。例如,考虑下面的名字列表。
| | `0` | `1` | `2` | `3` | `4` | `5` | `6` | `7` | `8` | `9` | `10` | `11` | `12` | `13` | `14` | | `0` | `S` | `a` | `m` | `l` | `a` | `l` | `,` | | `R` | `a` | `w` | `l` | `E` | `\0` | | | `1` | `W` | `i` | `l` | `l` | `i` | `a` | `m` | `s` | `,` | | `M` | `a` | `r` | `k` | `\0` | | `2` | `D` | `e` | `l` | `w` | `i` | `n` | `,` | | `M` | `a` | `c` | `\0` | | | | | `3` | `T` | `a` | `y` | `l` | `o` | `r` | `,` | | `V` | `i` | `c` | `t` | `o` | `r` | `\0` | | `4` | `M` | `o` | `h` | `a` | `m` | `e` | `d` | `,` | | `A` | `b` | `u` | `\0` | | | | `5` | `S` | `i` | `n` | `g` | `h` | `,` | | `K` | `R` | `i` | `s` | `h` | `n` | `a` | `\0` | | `6` | `T` | `a` | `w` | `a` | `r` | `i` | `,` | | `T` | `a` | `u` | `\0` | | | | | `7` | `A` | `b` | `d` | `o` | `o` | `l` | `,` | | `Z` | `a` | `i` | `d` | `\0` | | |为了存储这个列表,我们需要一个如下所示的声明:
char list[8][15];
为了迎合更长的名字,我们可以增加 15 个,为了迎合更多的名字,我们可以增加 8 个。
排序list的过程本质上与排序整数数组相同。主要区别在于,我们使用<来比较两个数字,而我们必须使用strcmp来比较两个名字。在前面显示的函数insertionSort中,while条件由此改变:
while (k >= lo && key < list[k])
到下面,其中key现在被声明为char key[15]:
while (k >= lo && strcmp(key, list[k]) < 0)
此外,我们现在必须使用strcpy(因为我们不能对字符串使用=)来为另一个位置分配名称。我们将在下一节看到完整的功能。
可变长度数组
我们将用这个例子来展示可变长度数组(vla)在 C 语言中的用法,这个特性只在 C99 及更高版本的 C 语言中可用。其思想是数组的大小可以在运行时指定,而不是在编译时指定。
在下面的函数中,注意参数列表中list ( char list[][max])的声明。与一维数组一样,第一维的大小没有指定。使用参数max指定第二维的尺寸;调用函数时会指定max的值。这给了我们更多的灵活性,因为我们可以在运行时指定第二维的大小。
void insertionSort3(int lo, int hi, int max, char list[][max]) {
//Sort the strings in list[lo] to list[hi] in alphabetical order.
//The maximum string size is max - 1 (one char taken up by \0).
char key[max];
for (int h = lo + 1; h <= hi; h++) {
strcpy(key, list[h]);
int k = h - 1; //start comparing with previous item
while (k >= lo && strcmp(key, list[k]) < 0) {
strcpy(list[k + 1], list[k]);
--k;
}
strcpy(list[k + 1], key);
} //end for
} // end insertionSort3
我们编写一个简单的main例程来测试insertionSort3,如程序 P9.3 所示。
Program P9.3
#include <stdio.h>
#include <string.h>
#define MaxNameSize 14
#define MaxNameBuffer MaxNameSize+1
#define MaxNames 8
int main() {
void insertionSort3(int, int, int max, char [][max]);
char name[MaxNames][MaxNameBuffer] =
{"Samlal, Rawle", "Williams, Mark","Delwin, Mac",
"Taylor, Victor", "Mohamed, Abu","Singh, Krishna",
"Tawari, Tau", "Abdool, Zaid" };
insertionSort3(0, MaxNames-1, MaxNameBuffer, name);
printf("\nThe sorted names are\n\n");
for (int h = 0; h < MaxNames; h++) printf("%s\n", name[h]);
} //end main
void insertionSort3(int lo, int hi, int max, char list[][max]) {
//Sort the strings in list[lo] to list[hi] in alphabetical order.
//The maximum string size is max - 1 (one char taken up by \0).
char key[max];
for (int h = lo + 1; h <= hi; h++) {
strcpy(key, list[h]);
int k = h - 1; //start comparing with previous item
while (k >= lo && strcmp(key, list[k]) < 0) {
strcpy(list[k + 1], list[k]);
--k;
}
strcpy(list[k + 1], key);
} //end for
} // end insertionSort3
name的声明用前面显示的八个名字初始化它。运行时,该程序产生以下输出:
The sorted names are
Abdool, Zaid
Delwin, Mac
Mohamed, Abu
Samlal, Rawle
Singh, Krishna
Tawari, Tau
Taylor, Victor
Williams, Mark
9.5 排序并行数组
在不同的数组中有相关的信息是很常见的。例如,假设除了name,我们还有一个整数数组id,使得id[h]是与name[h]相关联的标识号,如下所示。
在不同的数组中有相关的信息是很常见的。例如,假设除了name,我们还有一个整数数组id,使得id[h]是与name[h]相关联的标识号,如下所示。
考虑按字母顺序排列名字的问题。最后,我们希望每个名字都有正确的 ID 号。所以,比如排序完成后,name[0]应该包含Abdool, Zaid,id[0]应该包含7746。
为此,在排序过程中,每次移动一个姓名时,相应的 ID 号也必须移动。因为姓名和 ID 号必须“并行”移动,所以我们说我们正在进行并行排序,或者我们正在对并行数组进行排序。
我们重写insertionSort3来说明如何对并行数组进行排序。我们只需添加代码,以便在移动名称时移动 ID。我们称之为parallelSort。
void parallelSort(int lo, int hi, int max, char list[][max], int id[]) {
//Sort the names in list[lo] to list[hi] in alphabetical order, ensuring
//that each name remains with its original id number.
//The maximum string size is max - 1 (one char taken up by \0).
char key[max];
for (int h = lo + 1; h <= hi; h++) {
strcpy(key, list[h]);
int m = id[h]; // extract the id number
int k = h - 1; //start comparing with previous item
while (k >= lo && strcmp(key, list[k]) < 0) {
strcpy(list[k + 1], list[k]);
id[k+ 1] = id[k]; // move up id when we move a name
--k;
}
strcpy(list[k + 1], key);
id[k + 1] = m; // store id in the same position as the name
} //end for
} //end parallelSort
我们通过编写下面的main例程来测试parallelSort:
#include <stdio.h>
#include <string.h>
#define MaxNameSize 14
#define MaxNameBuffer MaxNameSize+1
#define MaxNames 8
int main() {
void parallelSort(int, int, int max, char [][max], int[]);
char name[MaxNames][MaxNameBuffer] =
{"Samlal, Rawle", "Williams, Mark","Delwin, Mac",
"Taylor, Victor", "Mohamed, Abu","Singh, Krishna",
"Tawari, Tau", "Abdool, Zaid" };
int id[MaxNames] = {8742,5418,4833,4230,8583,2458,5768,3313};
parallelSort(0, MaxNames-1, MaxNameBuffer, name, id);
printf("\nThe sorted names and IDs are\n\n");
for (int h = 0; h < MaxNames; h++)
printf("%-18s %d\n", name[h], id[h]);
} //end main
运行时,它会产生以下输出:
The sorted names and IDs are
Abdool, Zaid 3313
Delwin, Mac 4833
Mohamed, Abu 8583
Samlal, Rawle 8742
Singh, Krishna 2458
Tawari, Tau 5768
Taylor, Victor 4230
Williams, Mark 5418
我们顺便注意到,使用 C 结构可以更方便地存储“并行数组”。在我们学习了一些结构之后,我们将在 10.9 节讨论一个例子。
9.6 二分搜索法
如果列表是有序的(升序或降序),二分搜索法是搜索给定项目列表的一种非常快速的方法。如果列表不有序,可以使用前面描述的任何方法进行排序。
为了说明该方法,考虑一个由 11 个数字组成的列表,按升序排列。
假设我们希望搜索56。搜索过程如下:
- 首先,我们找到列表中的中间项。这是在位置
5的49。我们将56与49相比较。由于56更大,我们知道如果56在列表中,它一定在位置5之后,因为数字是升序排列的。下一步,我们将搜索范围限制在位置6到10。 - 接下来,我们从位置
6到10找到中间的项目。这是8位置的物品,即72。 - 我们比较
56和72。由于56较小,我们知道如果56在列表中,它一定在位置8之前,因为数字是按升序排列的。下一步,我们将搜索范围限制在位置6到7。 - 接下来,我们从位置
6到7找到中间的项目。在这种情况下,我们可以选择项目6或项目7。我们要写的算法会选择6项,也就是56。 - 我们比较
56和56。由于它们是相同的,我们的搜索成功结束,在位置6找到了所需的项目。
假设我们正在搜索60。搜索将如上进行,直到我们将60与56(在位置6)进行比较。
- 由于
60更大,我们知道如果60在列表中,它一定在位置6之后,因为数字是按升序排列的。下一步,我们将搜索范围限制在7到7的地点。这只是一个地点。 - 我们将
60与7项进行比较,即66。由于60较小,我们知道如果60在列表中,它一定在位置7之前。由于它不可能在位置6之后,位置7之前,所以我们断定它不在列表中。
在搜索的每个阶段,我们将搜索限制在列表的某个部分。让我们使用变量lo和hi作为定义这一部分的下标。换句话说,我们的搜索将被限制在从num[lo]到num[hi]的数字范围内。
最初,我们想要搜索整个列表,因此在本例中,我们将把lo设置为0,把hi设置为10。
我们如何找到中项的下标?我们将使用计算
mid = (lo + hi) / 2;
因为将执行整数除法,所以分数(如果有的话)将被丢弃。例如当lo为0,hi为10,mid变为5;当lo为6且hi为10时,mid变为8;当lo为6且hi为7时,mid变为6。
只要lo小于或等于hi,它们就定义了要搜索的列表的非空部分。当lo等于hi时,它们定义了要搜索的单个项目。如果lo变得比hi大,这意味着我们已经搜索了整个列表,但没有找到该项目。
基于这些想法,我们现在可以编写一个函数binarySearch。更一般地说,我们将编写它,以便调用例程可以指定它希望搜索数组的哪个部分来查找该项。
因此,必须给该函数指定要搜索的项(key)、数组(list)、搜索的开始位置(lo)和搜索的结束位置(hi)。例如,为了在上面的数组num中搜索数字56,我们可以发出下面的调用:
binarySearch(56, num, 0, 10)
这个函数必须告诉我们搜索的结果。如果找到了该项,该函数将返回它的位置。如果没有找到,将返回-1。
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
int mid;
while (lo <=``h
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
} //end binarySearch
如果item包含一个要搜索的数字,我们可以编写以下代码来调用binarySearch并检查搜索的结果:
int ans = binarySearch(item, num, 0, 12);
if (ans == -1) printf(“%d not found\n”, item);
else printf(“%d found in location %d\n”, item, ans);
如果我们希望从位置i到j搜索item,我们可以这样写:
int ans = binarySearch(item, num, i, j);
9.7 词频计数
让我们写一个程序来阅读一篇英语文章,并统计每个单词出现的次数。输出由单词及其频率的字母列表组成。
我们可以使用以下大纲来开发我们的程序:
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。
这里的一个主要设计决策是如何搜索表,这反过来又取决于新单词在表中的插入位置和插入方式。以下是两种可能性:
A new word is inserted in the next free position in the table. This implies that a sequential search must be used to look for an incoming word since the words would not be in any particular order. This method has the advantages of simplicity and easy insertion, but searching takes longer as more words are put in the table. A new word is inserted in the table in such a way that the words are always in alphabetical order. This may entail moving words that have already been stored so that the new word may be slotted in the right place. However, since the table is in order, a binary search can be used to search for an incoming word.
对于这种方法,搜索速度更快,但是插入速度比(1)慢。因为一般来说,搜索比插入更频繁,(2)可能更好。
(2)的另一个优点是,在最后,单词已经按字母顺序排列,不需要排序。如果使用(1),则需要对单词进行排序,以获得字母顺序。
我们将使用(2)中的方法编写程序。完整的程序如程序 P9.4 所示。
Program P9.4
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#define MaxWords 50
#define MaxLength 10
#define MaxWordBuffer MaxLength+1
int main() {
int getWord(FILE *, char[]);
int binarySearch(int, int, char [], int max, char [][max]);
void addToList(char[], int max, char [][max], int[], int, int);
void printResults(FILE *, int max, char [][max], int[], int);
char wordList[MaxWords][MaxWordBuffer], word[MaxWordBuffer];
int frequency[MaxWords], numWords = 0;
FILE * in = fopen("passage.txt", "r");
if (in == NULL){
printf("Cannot find file\n");
exit(1);
}
FILE * out = fopen("output.txt", "w");
if (out == NULL){
printf("Cannot create output file\n");
exit(2);
}
for (int h = 1; h <= MaxWords ; h++) frequency[h] = 0;
while (getWord(in, word) != 0) {
int loc = binarySearch (0, numWords-1, word, MaxWordBuffer,
wordList);
if (strcmp(word, wordList[loc]) == 0)
++frequency[loc]; //word found
else //this is a new word
if (numWords < MaxWords) { //if table is not full
addToList(word, MaxWordBuffer, wordList, frequency, loc,
numWords-1);
++numWords;
}
else fprintf(out, "'%s' not added to table\n", word);
}
printResults(out, MaxWordBuffer, wordList, frequency, numWords);
} // end main
int getWord(FILE * in, char str[]) {
// store the next word, if any, in str; convert word to lowercase
// return 1 if a word is found; 0, otherwise
char ch;
int n = 0;
// read over white space
while (!isalpha(ch = getc(in)) && ch != EOF) ; //empty while body
if (ch == EOF) return 0;
str[n++] = tolower(ch);
while (isalpha(ch = getc(in)) && ch != EOF)
if (n < MaxLength) str[n++] = tolower(ch);
str[n] = '\0';
return 1;
} // end getWord
int binarySearch(int lo, int hi, char key[], int max, char list[][max]) {
//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 = strcmp(key, list[mid]);
if (cmp == 0) return mid; // found
if (cmp < 0) hi = mid - 1;
else lo = mid + 1;
}
return lo; //not found; should be inserted in location lo
} //end binarySearch
void addToList(char item[], int max, char list[][max],
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--) {
strcpy(list[h+1], list[h]);
freq[h+1] = freq[h];
}
strcpy(list[p], item);
freq[p] = 1;
} //end addToList
void printResults(FILE *out, int max, char list[][max],
int freq[], int n) {
fprintf(out, "\nWords Frequency\n\n");
for (int h = 0; h < n; h++)
fprintf(out, "%-15s %2d\n", list[h], freq[h]);
} //end printResults
假设文件passage.txt包含以下数据(来自 Rudyard Kipling 的 If):
If you can dream—and not make dreams your master;
If you can think—and not make thoughts your aim;
If you can meet with Triumph and Disaster
And treat those two impostors just the same...
If you can fill the unforgiving minute
With sixty seconds’ worth of distance run,
Yours is the Earth...
使用这些数据运行程序 P9.4 时,它会产生以下输出:
Words Frequency
aim 1
and 4
can 4
disaster 1
distance 1
dream 1
dreams 1
earth 1
fill 1
if 4
impostors 1
is 1
just 1
make 2
master 1
meet 1
minute 1
not 2
of 1
run 1
same 1
seconds 1
sixty 1
the 3
think 1
those 1
thoughts 1
treat 1
triumph 1
two 1
unforgivin 1
with 2
worth 1
you 4
your 2
yours 1
对 P9.4 计划的评论
- 出于我们的目的,我们假设一个单词以字母开头,并且只由字母组成。如果您想包含其他字符(如连字符或撇号),您只需更改
getWord函数。 MaxWords表示满足的不同单词的最大数量。为了测试程序,我们使用了50作为这个值。如果文章中不同单词的数量超过了MaxWords(50),那么50th 之后的所有单词都将被读取,但不会被存储,并且会打印一条大意如此的消息。然而,如果再次遇到,已经存储的单词的计数将增加。MaxLength(我们用10来测试)表示一个单词的最大长度。字符串使用MaxLength+1(定义为MaxWordBuffer)来声明,以迎合\0,它必须添加在每个字符串的末尾。main检查输入文件是否存在,输出文件是否可以创建。接下来,它将频率计数初始化为0。然后,它根据本节开始时显示的大纲处理文章中的单词。getWord读取输入文件并存储在其字符串参数中找到的下一个单词。如果找到一个单词,它返回1,否则返回0。如果一个单词比MaxLength长,则只存储第一个MaxLength字母;其余的被读取并丢弃。例如,使用字长10将unforgiving截断为unforgivin。- 所有单词都被转换成小写,例如,
The和the被视为同一个单词。 - 我们编写了
binarySearch,这样如果找到这个单词,就会返回它的位置(loc)。如果没有找到,则返回单词应该插入的位置。测试 if (strcmp(word, wordList[loc]) == 0)- 确定是否找到了它。
addToList被赋予插入新单词的位置。该位置右侧的单词(包括该位置)将被移动,以便为新单词腾出空间。 - 在声明函数原型时,一些编译器允许像在
char [][]中那样声明一个二维数组参数,没有为任何一个维度指定大小。其他要求必须指定第二维的大小。指定第二维的大小应该适用于所有编译器。在我们的程序中,我们使用参数max指定第二维度,调用函数时将提供该参数的值。
9.8 合并排序列表
合并是将两个或多个有序列表合并成一个有序列表的过程。例如,给定两个数字列表,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
28 47
35 54
40
61
75
现在最上面的两张卡是21和25。较小的21被移除并添加到C,?? 现在包含了16 21。这就暴露了数字28。我们有这个:
28 25
35 47
40 54
61
75
现在最上面的两张卡是28和25。较小的25被移除并添加到C,?? 现在包含了16 21 25。这就暴露了数字47。我们有这个:
28 47
35 54
40
61
75
现在最上面的两张卡是28和47。较小的28被移除并添加到C,?? 现在包含了16 21 25 28。这就暴露了数字35。我们有这个:
35 47
40 54
61
75
现在最上面的两张卡是35和47。较小的35被移除并添加到C,?? 现在包含了16 21 25 28 35。这就暴露了数字40。我们有这个:
40 47
61 54
75
现在最上面的两张卡是40和47。较小的40被移除并添加到C,?? 现在包含了16 21 25 28 35 40。这就暴露了数字61。我们有这个:
61 47
75 54
现在最上面的两张卡是61和47。较小的47被移除并添加到C,?? 现在包含了16 21 25 28 35 40 47。这就暴露了数字54。我们有这个:
61 54
75
现在最上面的两张卡是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
实施合并
假设数组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 来实现。我们可以用下面的代码实现合并:
i = 0; //i points to the first (smallest) number in A
j = 0; //j points to the first (smallest) number in B
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];
程序 P9.5 显示了一个简单的主函数,它测试了我们方法的逻辑。我们把合并写成一个函数,给定参数A、m、B、n和C,执行合并并返回C中的元素数量m+n。运行时,程序打印出C的内容,如下所示:
16 21 25 28 35 40 47 54 61 75
Program P9.5
#include <stdio.h>
int main () {
int merge(int[], int, int[], int, int[]);
int A[] = {21, 28, 35, 40, 61, 75};
int B[] = {16, 25, 47, 54};
int C[20];
int n = merge(A, 6, B, 4, C);
for (int h = 0; h < n; h++) printf("%d ", C[h]);
printf("\n\n");
} //end main
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
有趣的是,我们也可以如下实现 merge:
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;
} //end merge
while循环表达了以下逻辑:只要在A或B中至少有一个元素要处理,我们就进入循环。如果我们完成了A ( i == m,从B到C复制一个元素。如果我们完成了B ( j == n),将一个元素从A复制到C。否则,将A[i]和B[j]中较小的一个复制到C。每当我们从一个数组中复制一个元素,我们就给这个数组的下标加 1。
虽然以前的版本以一种简单的方式实现了合并,但是说这个版本更简洁似乎是合理的。
EXERCISES 9In the voting problem of Section 8.15, print the results in alphabetical order by candidate name. Hint: in sorting the name array, when you move a name, make sure and move the corresponding item in the vote array. In the voting problem of Section 8.15, print the results in descending order by candidate score. Write a function to sort a double array in ascending order using selection sort. Do the sort by finding the largest number on each pass. Write a program to find out, for a class of students, the number of families with 1, 2, 3, ... up to 8 or more children. The data consists of the number of children in each pupil’s family, terminated by 0. Print the results in decreasing order by family-size popularity. That is, print the most popular family-size first and the least popular family-size last. 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). Write a program to read the names of the artists, followed by the votes, and find out which artist is the most popular. Choose a suitable end-of-data marker. Print a table of the results with the most popular artist first and the least popular last. The median of a set of n numbers (not necessarily distinct) is obtained by arranging the numbers in order and taking the number in the middle. If n is odd, there is a unique middle number. If n is even, then the average of the two middle values is the median. Write a program to read a set of n positive integers (assume n < 100) and print their median; n is not given but 0 indicates the end of the data. The mode of a set of n numbers is the number that appears most frequently. For example, the mode of 7 3 8 5 7 3 1 3 4 8 9 is 3. Write a program to read a set of n arbitrary positive integers (assume n < 100) and print their mode; n is not given but 0 indicates the end of the data. Write an efficient program to find the mode if it is known that the numbers all lie between 1 and 999, inclusive, with no restriction on the amount of numbers supplied; 0 ends the data. An array num contains k numbers in num[0] to num[k-1], sorted in descending order. Write a function insertInPlace which, given num, k and another number x, inserts x in its proper position such that num[0] to num[k] are sorted in descending order. Assume the array has room for x. A multiple-choice examination consists of 20 questions. Each question has 5 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 Each subsequent line contains the answers for a candidate. Data on a line consists of a candidate number (an integer), followed by 1 or more spaces, followed by the 20 answers given by the candidate in the next 20 consecutive character positions. An X is used if a candidate did not answer a particular question. You may assume all data are valid and stored in a file exam.dat. A sample line is: 4325 BECDCBAXDEBACCAEDXBE There are at most 100 candidates. A line containing a “candidate number” 0 only indicates the end of the data. Points for a question are awarded as follows:– correct answer: 4 points; wrong answer: -1 point; no answer: 0 points. Write a program to process the data and print a report consisting of candidate number and the total points obtained by the candidate, in ascending order by candidate number. At the end, print the average number of points gained by the candidates. An array A contains integers that first increase in value and then decrease in value, for example:
It is unknown at which point the numbers start to decrease. Write efficient code to copy the numbers from A to another array B so that B is sorted in ascending order. Your code must take advantage of the way the numbers are arranged in A. (Hint: perform a merge starting at both ends.) You are given two integer arrays A and B each of maximum size 500. If A[0] contains m, say, then m numbers are stored in arbitrary order from A[1] to A[m]. If B[0] contains n, say, then n numbers are stored in arbitrary order from B[1] to B[n]. Write code to merge the elements of A and B into another array C such that C[0] contains m+n and C[1] to C[m+n] contain the numbers in ascending order. An anagram is a word or phrase formed by rearranging the letters of another word or phrase. Examples of one-word anagrams are: sister/resist and senator/treason. We can get more interesting anagrams if we ignore letter case and punctuation marks. Examples are: Time-table/Bet I’m Late, Clint Eastwood/Old West Action, and Astronomers/No More Stars. Write a function that, given two strings, returns 1 if the strings are anagrams of each other and 0 if they are not. An input file contains one word or phrase per line. Write a program to read the file and output all words/phrases (from the file) that are anagrams of each other. Print a blank line between each group of anagrams.
十、结构
在本章中,我们将解释以下内容:
- 什么是结构
- 如何声明一个结构
- 如何使用 typedef 更方便地处理结构
- 如何使用结构数组
- 如何搜索结构数组
- 如何对结构数组进行排序
- 如何声明嵌套结构
- 如何使用结构处理分数
- 如何使用结构存储并行数组
- 如何将结构传递给函数
10.1 对结构的需求
在 C 语言中,结构是一个或多个变量的集合,这些变量可能是不同类型的,为了方便处理,它们被组合在一个名字下。
在许多情况下,我们希望处理关于某个实体或对象的数据,但是这些数据由各种类型的项目组成。例如,学生的数据(学生记录)可能由几个字段组成,如姓名、地址和电话号码(都是字符串类型);参加的课程数量(整数);应付费用(浮点);课程名称(字符串);获得的成绩(性格);等等。
汽车的数据可能包括制造商、型号和注册号码(字符串);座位容量和燃料容量(整数);以及里程和价格(浮点)。对于一本书,我们可能想存储作者和书名(字符串);价格(浮点);页数(整数);装订类型:精装、平装、螺旋(线装);和库存份数(整数)。
假设我们想在一个程序中存储 100 名学生的数据。一种方法是每个字段有一个单独的数组,并使用下标将字段链接在一起。因此,name[i]、address[i]、fees[i]等等,引用第i个学生的数据。
这种方法的问题是,如果有许多字段,那么处理几个并行数组就变得笨拙而不实用。例如,假设我们想通过参数列表将学生的数据传递给一个函数。这将涉及几个数组的传递。此外,如果我们按姓名对学生进行排序,比方说,每次两个姓名互换时,我们都必须编写语句来交换其他数组中的数据。在这种情况下,使用 C 结构很方便。
10.2 如何声明一个结构
考虑在程序中存储日期的问题。日期由三部分组成:日、月和年。这些部分中的每一个都可以用一个整数来表示。例如,日期“2006 年 9 月 14 日”可以用日 14 来表示;月份,9;2006 年。我们说日期由三个字段组成,每个字段都是整数。
如果我们愿意,我们也可以用月份的名称来表示日期,而不是月份的数字。在这种情况下,日期由三个字段组成,其中一个是字符串,另外两个是整数。
在 C 中,我们可以使用关键字struct将日期类型声明为一个结构。请考虑以下声明:
struct date {int day, month, year;};
它由关键字struct组成,后跟我们选择的结构名称(在本例中为date);接下来是用左右括号括起来的字段声明。注意右括号前声明末尾的分号——这是分号结束声明的常见情况。右括号后面是分号,结束了struct声明。
我们也可以编写如下声明,其中每个字段都是单独声明的:
struct date {
int day;
int month;
int year;
};
这可以按如下方式编写,但上面的样式更具可读性:
struct date {int day; int month; int year;};
给定struct声明,我们可以声明类型struct date的变量,如下所示:
struct date dob; //to hold a "date of birth"
这将dob声明为date类型的“结构变量”。它有三个字段,分别叫做day、month和year。这可以如下图所示:
我们将日字段称为dob.day,月字段称为dob.month,年字段称为dob.year。在 C 语言中,这里使用的句点(.)被称为结构成员运算符。
一般来说,字段是由结构变量名指定的,后面跟一个句点,再后面跟字段名。
我们可以一次声明多个变量,如下所示:
struct date borrowed, returned; //for a book in a library, say
这些变量中的每一个都有三个字段:day、month和year。borrowed的字段由borrowed.day、borrowed.month和borrowed.year引用。返回的字段由returned.day、returned.month和returned.year引用。
在这个例子中,每个字段都是一个int,并且可以在任何可以使用int变量的上下文中使用。例如,要将日期“2015 年 11 月 14 日”指定给dob,我们可以这样使用:
dob.day = 14;
dob.month = 11;
dob.year = 2015;
这可以如下图所示:
我们也可以用下面的代码读取day、month和year的值:
scanf("%d %d %d", &dob.day, &dob.month, &dob.year);
假设today被声明如下:
struct date today;
假设我们已经在today中存储了一个值,那么我们可以将today的所有字段分配给dob,如下所示:
dob = today;
这条语句相当于以下语句:
dob.day = today.day;
dob.month = today.month;
dob.year = today.year;
我们可以这样打印dob的“值”:
printf("The party is on %d/%d/%d\n", dob.day, dob.month, dob.year);
对于此示例,将打印以下内容:
The party is on 14/11/2015
请注意,每个字段都必须单独打印。我们也可以编写一个函数printDate,比如说,它打印一个作为参数给出的日期。下面的程序展示了如何编写和使用printDate。
#include <stdio.h>
struct date {
int day;
int month;
int year;
};
int main() {
struct date dob;
void printDate(struct date);
dob.day = 14 ;
dob.month = 11;
dob.year = 2015;
printDate(dob);
}
void printDate(struct date d) {
printf("%d/%d/%d \n", d.day, d.month, d.year);
}
运行时,该程序打印
14/11/2015
我们顺便注意到,C 在标准库中提供了一个日期和时间结构tm。除了日期之外,它还提供了精确到秒的时间。要使用它,您的程序前面必须有以下内容:
#include <time.h>
与单个单词类型int或double相比,struct date这个结构使用起来有点麻烦。幸运的是,C 为我们提供了typedef来使处理结构变得更加方便。
10.2.1 typedef
我们可以使用typedef给某个现有的类型命名,然后这个名称可以用来声明该类型的变量。我们还可以使用typedef为预定义的 C 类型或用户声明的类型(如结构)构造更短或更有意义的名称。例如,下面的语句声明了一个新的类型名Whole,它与预定义类型int同义:
typedef int Whole;
请注意,Whole出现在与变量相同的位置,而不是紧接在单词typedef之后。然后我们可以声明Whole类型的变量,如下所示:
Whole amount, numCopies;
这完全等同于
int amount, numCopies;
对于那些习惯了 Pascal 或 FORTRAN 等语言的术语real的人来说,下面的语句允许他们声明类型为Real的变量:
typedef float Real;
在本书中,我们至少使用一个大写字母来区分使用typedef声明的类型名。
我们可以用下面的声明给前面显示的日期结构取一个简短而有意义的名字Date:
typedef struct date {
int day;
int month;
int year;
} Date;
回想一下,C 区分大写字母和小写字母,使得date不同于Date。如果我们愿意,我们可以使用任何其他标识符,比如DateType,而不是Date。
我们现在可以声明Date类型的“结构变量”,如下所示:
Date dob, borrowed, returned;
请注意,与下面的代码相比,这段代码要简洁得多:
struct date dob, borrowed, returned;
由于几乎没有任何理由使用第二种形式,我们可以从上面的声明中省略date,写成:
typedef struct {
int day;
int month;
int year;
} Date;
此后,只要需要struct,我们就可以使用Date。例如,我们可以将printDate改写如下:
void printDate(Date d) {
printf("%d/%d/%d \n", d.day, d.month, d.year);
}
为了实现日期示例,假设我们想要存储“短”名称——月份的前三个字母,例如Aug。我们将需要使用这样的声明:
typedef struct {
int day;
char month[4]; //one position for \0 to end string
int year;
} Date;
我们可以在变量dob的Date中表示日期“2015 年 11 月 14 日”,如下所示:
dob.day = 14;
strcpy(dob.month, "Nov");//remember to #include <string.h> to use strcpy
dob.year = 2015;
我们可以这样写printDate:
void printDate(Date d) {
printf("%s %d, %d \n", d.month, d.day, d.year);
}
电话
printDate(dob);
将打印以下内容:
Nov 14, 2015
假设我们想要存储关于学生的信息。对于每个学生,我们希望存储他们的姓名、年龄和性别(男性或女性)。假设一个名称不超过 30 个字符,我们可以使用下面的声明:
typedef struct {
char name[31];
int age;
char gender;
} Student;
我们现在可以声明类型为Student的变量,如下所示:
Student stud1, stud2;
每个stud1和stud2都有自己的字段——name、age和gender。我们可以参考以下这些字段:
stud1.name stud1.age stud1.gender
stud2.name stud2.age stud2.gender
像往常一样,我们可以给这些字段赋值,或者将值读入这些字段。而且,如果我们愿意,我们可以用一条语句将stud1的所有字段分配给stud2:
stud2 = stud1;
结构的排列
假设我们想要存储 100 名学生的数据。我们将需要一个大小为 100 的数组,数组的每个元素将保存一个学生的数据。因此,每个元素都必须是一个结构——我们需要一个“结构阵列”
我们可以用下面的语句来声明数组,类似于我们说“int pupil[100]”来声明一个大小为 100 的整数数组:
Student pupil[100];
这为pupil[0]、pupil[1]、pupil[2、…,直到pupil[99]分配存储。每个元素pupil[i]由三个字段组成,可参考如下:
pupil[i].name pupil[i].age pupil[i].gender
首先,我们需要在数组中存储一些数据。假设我们有以下格式的数据(姓名、年龄、性别):
"Jones, John" 24 M
"Mohammed, Lisa" 33 F
"Singh, Sandy" 29 F
"Layne, Dennis" 49 M
"END"
假设数据存储在文件input.txt中,并且in声明如下:
FILE * in = fopen("input.txt", "r");
如果str是一个字符数组,假设我们可以调用这个函数
getString(in, str)
将下一个带引号的数据字符串存储在str中,不带引号。还假设readChar(in)将读取数据并返回下一个非空白字符。
练习:编写函数getString和readChar。
我们可以用下面的代码将数据读入数组pupil:
int n = 0;
char temp[31];
getString(in, temp);
while (strcmp(temp, "END") != 0) {
strcpy(pupil[n].name, temp);
fscanf(in, "%d", &pupil[n].age);
pupil[n].gender = readChar(in);
n++;
getString(in, temp);
}
最后,n包含存储的学生人数,pupil[0]到pupil[n-1]包含这些学生的数据。
为了确保我们不会试图存储超过我们在数组中的空间的数据,我们应该检查n是否在数组的边界内。假设MaxItems的值为100,这可以通过将while条件改为如下来实现:
while (n < MaxItems && strcmp(temp, "END") != 0)
或者在语句n++后插入以下内容;在循环内部:
if (n == MaxItems) break;
10.4 搜索结构数组
有了存储在数组中的数据,我们可以用各种方式操纵它。例如,我们可以编写一个函数来搜索一个给定的名字。假设数据没有按特定顺序存储,我们可以使用如下顺序搜索:
int search(char key[], Student list[], int n) {
//search for key in list[0] to list[n-1]
//if found, return the location; if not found, return -1
for (int h = 0; h < n; h++)
if (strcmp(key, list[h].name) == 0) return h;
return -1;
} //end search
给定前面的数据,调用
search("Singh, Sandy", pupil, 4)
将返回2,下面的调用将返回-1:
search("Layne, Sandy", pupil, 4)
10.5 对结构数组进行排序
假设我们需要按姓名字母顺序排列的学生名单。需要对数组pupil进行排序。下面的函数使用插入排序来完成这项工作。这个过程与对一个int数组进行排序是一样的,除了name字段用于控制排序。
void sort(Student list[], int n) {
//sort list[0] to list[n-1] by name using an insertion sort
Student temp;
int k;
for (int h = 1; h < n; h++) {
Student temp = list[h];
k = h - 1;
while (k >= 0 && strcmp(temp.name, list[k].name) < 0) {
list[k + 1] = list[k];
k = k - 1;
}
}
list[k + 1] = temp;
} //end sort
请遵守以下声明:
list[k + 1] = list[k];
这将把list[k]的所有字段分配给list[k+1]。
如果我们想按年龄对学生进行排序,我们需要改变的只是while条件。为了按升序排序,我们这样写:
while (k >= 0 && temp.age < list[k].age)
//move smaller numbers to the left
要按降序排序,我们这样写:
while (k >= 0 && temp.age > list[k].age)
//move bigger numbers to the left
我们甚至可以根据性别字段将列表分为男生和女生。因为按字母顺序 F 在 M 之前,我们可以把女性放在第一位,写为:
while (k >= 0 && temp.gender < list[k].gender)
//move Fs to the left
我们可以把男性放在第一位:
while (k >= 0 && temp.gender > list[k].gender)
//move Ms to the left
10.6 读取、搜索和排序结构
我们通过编写程序 P10.1 来说明前面讨论的思想。该程序执行以下操作:
- 从文件
input.txt中读取学生的数据,并将它们存储在一个结构数组中。 - 按照数组中存储的顺序打印数据。
- 测试通过读取几个名字并在数组中查找它们来进行搜索。
- 按照
name的字母顺序对数据进行排序。 - 打印排序后的数据。
该程序还说明了如何编写函数getString和readChar。getString让我们读取包含在任何“分隔符”字符中的字符串。例如,我们可以指定一个字符串为$John Smith$或"John Smith.",这是一种非常灵活的指定字符串的方式。每个字符串都可以指定自己的分隔符,对于下一个字符串,分隔符可能会有所不同。它对于指定可能包含特殊字符(如双引号)的字符串特别有用,而不必使用转义序列(如\")。
Program P10.1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MaxStudents 100
#define MaxNameLength 30
#define MaxNameBuffer MaxNameLength+1
typedef struct {
char name[MaxNameBuffer];
int age;
char gender;
} Student;
int main() {
Student pupil[MaxStudents];
char aName[MaxNameBuffer];
void getString(FILE *, char[]);
int getData(FILE *, Student[]);
int search(char[], Student[], int);
void sort(Student[], int);
void printStudent(Student);
void getString(FILE *, char[]);
FILE * in = fopen("input.txt", "r");
if (in == NULL) {
printf("Error opening input file.\n");
exit(1);
}
int numStudents = getData(in, pupil);
if (numStudents == 0) {
printf("No data supplied for students");
exit(1);
}
printf("\n");
for (int h = 0; h < numStudents; h++) printStudent(pupil[h]);
printf("\n");
getString(in, aName);
while (strcmp(aName, "END") != 0) {
int ans = search(aName, pupil, numStudents);
if (ans == -1) printf("%s not found\n", aName);
else printf("%s found at location %d\n", aName, ans);
getString(in, aName);
}
sort(pupil, numStudents);
printf("\n");
for (int h = 0; h < numStudents; h++) printStudent(pupil[h]);
} //end main
void printStudent(Student t) {
printf("Name: %s Age: %d Gender: %c\n", t.name, t.age, t.gender);
} //end printStudent
int getData(FILE *in, Student list[]) {
char temp[MaxNameBuffer];
void getString(FILE *, char[]);
char readChar(FILE *);
int n = 0;
getString(in, temp);
while (n < MaxStudents && strcmp(temp, "END") != 0) {
strcpy(list[n].name, temp);
fscanf(in, "%d", &list[n].age);
list[n].gender = readChar(in);
n++;
getString(in, temp);
}
return n;
} //end getData
int search(char key[], Student list[], int n) {
//search for key in list[0] to list[n-1]
//if found, return the location; if not found, return -1
for (int h = 0; h < n; h++)
if (strcmp(key, list[h].name) == 0) return h;
return -1;
} //end search
void sort(Student list[], int n) {
//sort list[0] to list[n-1] by name using an insertion sort
Student temp;
int k;
for (int h = 1; h < n; h++) {
temp = list[h];
k = h - 1;
while (k >= 0 && strcmp(temp.name, list[k].name) < 0) {
list[k + 1] = list[k];
k = k - 1;
}
list[k + 1] = temp;
} //end for
} //end sort
void getString(FILE * in, char str[]) {
// stores, in str, the next string within delimiters
// the first non-whitespace character is the delimiter
// the string is read from the file 'in'
char ch, delim;
int n = 0;
str[0] = '\0';
// read over white space
while (isspace(ch = getc(in))) ; //empty while body
if (ch == EOF) return;
delim = ch;
while (((ch = getc(in)) != delim) && (ch != EOF))
str[n++] = ch;
str[n] = '\0';
} // end getString
char readChar(FILE * in) {
char ch;
while (isspace(ch = getc(in))) ; //empty while body
return ch;
} //end readChar
假设文件input.txt包含以下数据:
"Jones, John" 24 M
"Mohammed, Lisa" 33 F
"Singh, Sandy" 29 F
"Layne, Dennis" 49 M
"Singh, Cindy" 16 F
"Ali, Imran" 39 M
"Kelly, Trudy" 30 F
"Cox, Kerry" 25 M
"END"
"Kelly, Trudy"
"Layne, Dennis"
"Layne, Cindy"
"END"
该程序打印以下内容:
Name: Jones, John Age: 24 Gender: M
Name: Mohammed, Lisa Age: 33 Gender: F
Name: Singh, Sandy Age: 29 Gender: F
Name: Layne, Dennis Age: 49 Gender: M
Name: Singh, Cindy Age: 16 Gender: F
Name: Ali, Imran Age: 39 Gender: M
Name: Kelly, Trudy Age: 30 Gender: F
Name: Cox, Kerry Age: 25 Gender: M
Kelly, Trudy found at location 6
Layne, Dennis found at location 3
Layne, Cindy not found
Name: Ali, Imran Age: 39 Gender: M
Name: Cox, Kerry Age: 25 Gender: M
Name: Jones, John Age: 24 Gender: M
Name: Kelly, Trudy Age: 30 Gender: F
Name: Layne, Dennis Age: 49 Gender: M
Name: Mohammed, Lisa Age: 33 Gender: F
Name: Singh, Cindy Age: 16 Gender: F
Name: Singh, Sandy Age: 29 Gender: F
10.7 嵌套结构
c 允许我们使用一个结构作为另一个结构定义的一部分——结构中的结构,称为嵌套结构。考虑一下Student结构。假设我们想存储学生的出生日期,而不是年龄。这可能是一个更好的选择,因为学生的出生日期是固定的,而他的年龄是变化的,并且该字段必须每年更新。
我们可以使用下面的声明:
typedef struct {
char name[31];
Date dob;
char gender;
} Student;
如果mary是一个Student类型的变量,那么mary.dob指的是她的出生日期。但是mary.dob本身就是一个Date结构。如果需要,我们可以用mary.dob.day、mary.dob.month、mary.dob.year来指代它的字段。
如果我们想以更灵活的方式存储姓名,例如,名、中间名和姓,我们可以使用这样的结构:
typedef struct {
char first[21];
char middle;
char last[21];
} Name;
现在,Student结构变成如下,它包含两个结构,Name和Date:
typedef struct {
Name name; //assumes Name has already been declared
Date dob; //assumes Date has already been declared
char gender;
} Student;
如果st是类型Student的变量,
st.name是指Name类型的结构;
st.name.first指学生的名字;和
st.name.last[0]指她姓氏的第一个字母。
现在,如果我们想按姓氏对数组pupil进行排序,函数sort中的while条件就变成这样:
while (k >= 0 && strcmp(temp.name.last, pupil[k].name.last) < 0)
一个结构可以嵌套到你想要的深度。点(.)运算符从左到右关联。如果a、b和c是结构,则构造
a.b.c. d
被解释为
((a.b).c).d
10.8 使用分数
考虑处理分数的问题,其中分数由两个整数值表示:一个表示分子,另一个表示分母。例如,5/9由两个数字5和9表示。
我们将使用以下结构来表示分数:
typedef struct {
int num;
int den;
} Fraction;
如果f是Fraction类型的变量,我们可以用这个在f中存储 5/9:
f.num = 5;
f.den = 9;
这可以如下图所示:
我们还可以读取代表分数的两个值,并使用如下语句将它们存储在f中:
scanf("%d %d", &f.num, &f.den);
我们可以写一个函数printFraction,来打印一个分数。它显示在下面的程序中。
#include <stdio.h>
typedef struct {
int num;
int den;
} Fraction;
int main() {
void printFraction(Fraction);
Fraction f;
f.num = 5;
f.den = 9;
printFraction(f);
}
void printFraction(Fraction f) {
printf("%d/%d", f.num, f.den);
}
运行时,该程序将打印
5/9
操纵分数
我们可以编写函数对分数进行各种运算。例如,由于
我们可以编写一个函数将两个分数相加如下:
Fraction addFraction(Fraction a, Fraction b) {
Fraction c;
c.num = a.num * b.den + a.den * b.num;
c.den = a.den * b.den;
return c;
} //end addFraction
类似地,我们可以编写函数来加减乘除分数。
Fraction subFraction(Fraction a, Fraction b) {
Fraction c;
c.num = a.num * b.den - a.den * b.num;
c.den = a.den * b.den;
return c;
} //end subFraction
Fraction mulFraction(Fraction a, Fraction b) {
Fraction c;
c.num = a.num * b.num;
c.den = a.den * b.den;
return c;
} //end mulFraction
Fraction divFraction(Fraction a, Fraction b) {
Fraction c;
c.num = a.num * b.den;
c.den = a.den * b.num;
return c;
} //end divFraction
为了说明它们的用途,假设我们想找到
{ 的2537+58
我们可以使用以下语句来实现这一点:
Fraction a, b, c, sum, ans;
a.num = 2; a.den = 5;
b.num = 3; b.den = 7;
c.num = 5; c.den = 8;
sum = addFraction(b, c);
ans = mulFraction(a, sum);
printFraction(ans);
严格地说,变量sum和ans不是必需的,但是我们用它们来简化解释。因为函数的参数可以是一个表达式,所以我们可以得到相同的结果:
printFraction(mulFraction(a, addFraction(b, c)));
运行时,此代码将打印以下内容,正确答案是:
118/280
然而,如果你愿意,你可以写一个函数把一个分数化为它的最低项。这可以通过找到分子和分母的最大公因数(HCF)来实现。然后将分子和分母除以它们的 HCF。例如,118 和 280 的 HCF 是 2,因此118/280减少为59/140。编写这个函数作为一个练习。
10.9 投票问题
这个例子将用来说明关于函数参数传递的几个要点。它进一步强调了数组参数和简单变量参数之间的区别。我们将展示一个函数如何通过使用一个结构向一个调用函数返回多个值。为此,我们将编写一个程序来解决我们在 8.15 节中遇到的投票问题。又来了:
- 问题:在一次选举中,有七名候选人。每个选民都被允许为他们选择的候选人投一票。投票记录为从 1 到 7 的数字。投票人数事先未知,但以
0票终止投票。任何不是从 1 到 7 的数字的投票都是无效的。 - 文件
votes.txt包含候选人的姓名。第一个名字被视为候选人 1,第二个被视为候选人 2,依此类推。名字后面是投票。写一个程序来读取数据并评估选举的结果。打印所有输出到文件,results.txt。 - 您的输出应该指定总投票数、有效投票数和无效投票数。接下来是每位候选人和选举获胜者获得的票数。
假设文件votes.txt包含以下数据:
Victor Taylor
Denise Duncan
Kamal Ramdhan
Michael Ali
Anisa Sawh
Carol Khan
Gary Olliverie
3 1 2 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
Victor Taylor 4
Denise Duncan 3
Kamal Ramdhan 6
Michael Ali 4
Anisa Sawh 6
Carol Khan 2
Gary Olliverie 3
The winner(s):
Kamal Ramdhan
Anisa Sawh
我们现在解释如何使用 C 结构解决这个问题。考虑这些声明:
typedef struct {
char name[31];
int numVotes;
} PersonData;
PersonData candidate[8];
这里,candidate是一个结构体数组。我们将使用candidate[1]到candidate[7]来表示七个候选人;我们不会用candidate[0]。这将使我们更自然地处理选票。投个票(v,说吧),candidate[v]会更新。如果我们使用candidate[0],我们会有一个尴尬的情况,投票v,candidate[v-1]必须被更新。
元素candidate[h]不仅仅是单个数据项,而是由两个字段组成的结构。这些字段可参考如下:
candidate[h].name and candidate[h].numVotes
为了使程序灵活,我们将定义以下符号常量:
#define MaxCandidates 7
#define MaxNameLength 30
#define MaxNameBuffer MaxNameLength+1
我们还将前面的声明更改为以下内容:
typedef struct {
char name[MaxNameBuffer];
int numVotes;
} PersonData;
PersonData candidate[MaxCandidates+1];
该解决方案基于以下大纲:
initialize
process the votes
print the results
函数initialize将从文件in中读取名字,并将投票计数设置为0。该文件作为参数传递给函数。我们将分两部分(名和姓)读取候选人的姓名,然后将它们连接在一起,创建一个单一的姓名,并存储在person[h].name中。将为max人员读取数据。下面是函数:
void initialize(PersonData person[], int max, FILE *in) {
char lastName[MaxNameBuffer];
for (int h = 1; h <= max; h++) {
fscanf(in, "%s %s", person[h].name, lastName);
strcat(person[h].name, " ");
strcat(person[h].name, lastName);
person[h].numVotes = 0;
}
} //end initialize
处理投票将基于以下大纲:
get a vote
while the vote is not 0
if the vote is valid
add 1 to validVotes
add 1 to the score of the appropriate candidate
else
print invalid vote
add 1 to spoiltVotes
endif
get a vote
endwhile
在处理完所有的投票后,这个函数需要返回有效投票和无效投票的数量。但是一个函数怎么能返回多个值呢?如果值存储在结构中,并且该结构作为函数的“值”返回,则可以。
我们将使用以下声明:
typedef struct {
int valid, spoilt;
} VoteCount;
我们将把processVotes写成:
VoteCount processVotes(PersonData person[], int max, FILE *in, FILE *out) {
VoteCount temp;
temp.valid = temp.spoilt = 0;
int v;
fscanf(in, "%d", &v);
while (v != 0) {
if (v < 1 || v > max) {
fprintf(out, "Invalid vote: %d\n", v);
++temp.spoilt;
}
else {
++person[v].numVotes;
++temp.valid;
}
fscanf(in, "%d", &v);
} //end while
return temp;
} //end processVotes
接下来,我们编写main,前面是编译器指令和结构声明。
#include <stdio.h>
#include <string.h>
#define MaxCandidates 7
#define MaxNameLength 30
#define MaxNameBuffer MaxNameLength+1
typedef struct {
char name[MaxNameBuffer];
int numVotes;
} PersonData;
PersonData candidate[MaxCandidates];
typedef struct {
int valid, spoilt;
} VoteCount;
int main() {
void initialize(PersonData[], int, FILE *);
VoteCount processVotes(PersonData[], int, FILE *, FILE *);
void printResults(PersonData[], int, VoteCount, FILE *);
PersonData candidate[MaxCandidates+1];
VoteCount count;
FILE *in = fopen("votes.txt", "r");
FILE *out = fopen("results.txt", "w");
initialize(candidate, MaxCandidates, in);
count = processVotes(candidate, MaxCandidates, in, out);
printResults(candidate, MaxCandidates, count, out);
fclose(in);
fclose(out);
} //end main
PersonData和VoteCount的声明在main之前。这样做是为了让其他函数可以引用它们,而不必重复整个声明。如果它们是在main中声明的,那么PersonData和VoteCount的名字只会在main中被知道,其他函数将无法访问它们。
现在我们知道了如何读取和处理投票,剩下的只是确定获胜者并打印结果。我们将把这个任务委托给函数printResults。
使用样本数据,数组candidate将在所有投票被统计后包含如下所示的值(记住,我们没有使用candidate[0])。
要找到获胜者,我们必须首先找到数组中的最大值。为此,我们将如下调用函数getLargest:
int win = getLargest(candidate, 1, MaxCandidates);
这将把win设置为从candidate[1]到candidate[7]的numVotes字段中最大值的下标(因为MaxCandidates是7):
在我们的例子中,win将被设置为3,因为最大值6位于位置3。(6也在5位置,但是我们只需要最大值,我们可以从任一位置得到。)
这里是getLargest:
int getLargest(PersonData person[], int lo, int hi) {
//returns the index of the highest vote from person[lo] to person[hi]
int big = lo;
for (int h = lo + 1; h <= hi; h++)
if (person[h].numVotes > person[big].numVotes) big = h;
return big;
} //end getLargest
现在我们知道最大值在candidate[win].numVotes中,我们可以“遍历”数组,寻找具有该值的候选值。这样,我们将找到所有得票最高的候选人(如果不止一个),并宣布他们为获胜者。
printResults的概要如下:
printResults
print the number of voters, valid votes and spoilt votes
print the score of each candidate
determine and print the winner(s)
详细信息在函数printResults中给出:
void printResults(PersonData person[], int max, VoteCount c, FILE*out) {
int getLargest(PersonData[], int, int);
fprintf(out, "\nNumber of voters: %d\n", c.valid + c.spoilt);
fprintf(out, "Number of valid votes: %d\n", c.valid);
fprintf(out, "Number of spoilt votes: %d\n", c.spoilt);
fprintf(out, "\nCandidate Score\n\n");
for (int h = 1; h <= max; h++)
fprintf(out, "%-15s %3d\n", person[h].name,
person[h].numVotes);
fprintf(out, "\nThe winner(s)\n");
int win = getLargest(person, 1, max);
int winningVote = person[win].numVotes;
for (int h = 1; h <= max; h++)
if (person[h].numVotes == winningVote) fprintf(out, "%s\n",
person[h].name);
} //end printResults
把所有的片段放在一起,我们得到了程序 P10.2,解决投票问题的程序。
Program P10.2
#include <stdio.h>
#include <string.h>
#define MaxCandidates 7
#define MaxNameLength 30
#define MaxNameBuffer MaxNameLength+1
typedef struct {
char name[MaxNameBuffer];
int numVotes;
} PersonData;
PersonData candidate[MaxCandidates];
typedef struct {
int valid, spoilt;
} VoteCount;
int main() {
void initialize(PersonData[], int, FILE *);
VoteCount processVotes(PersonData[], int, FILE *, FILE *);
void printResults(PersonData[], int, VoteCount, FILE *);
PersonData candidate[MaxCandidates+1];
VoteCount count;
FILE *in = fopen("votes.txt", "r");
FILE *out = fopen("results.txt", "w");
initialize(candidate, MaxCandidates, in);
count = processVotes(candidate, MaxCandidates, in, out);
printResults(candidate, MaxCandidates, count, out);
fclose(in);
fclose(out);
} //end main
void initialize(PersonData person[], int max, FILE *in) {
char lastName[MaxNameBuffer];
for (int h = 1; h <= max; h++) {
fscanf(in, "%s %s", person[h].name, lastName);
strcat(person[h].name, " ");
strcat(person[h].name, lastName);
person[h].numVotes = 0;
}
} //end initialize
VoteCount processVotes(PersonData person[], int max, FILE *in, FILE *out) {
VoteCount temp;
temp.valid = temp.spoilt = 0;
int v;
fscanf(in, "%d", &v);
while (v != 0) {
if (v < 1 || v > max) {
fprintf(out, "Invalid vote: %d\n", v);
++temp.spoilt;
}
else {
++person[v].numVotes;
++temp.valid;
}
fscanf(in, "%d", &v);
} //end while
return temp;
} //end processVotes
int getLargest(PersonData person[], int lo, int hi) {
//returns the index of the highest vote from person[lo] to person[hi]
int big = lo;
for (int h = lo + 1; h <= hi; h++)
if (person[h].numVotes > person[big].numVotes) big = h;
return big;
} //end getLargest
void printResults(PersonData person[], int max, VoteCount c, FILE *out) {
int getLargest(PersonData[], int, int);
fprintf(out, "\nNumber of voters: %d\n", c.valid + c.spoilt);
fprintf(out, "Number of valid votes: %d\n", c.valid);
fprintf(out, "Number of spoilt votes: %d\n", c.spoilt);
fprintf(out, "\nCandidate Score\n\n");
for (int h = 1; h <= max; h++)
fprintf(out, "%-15s %3d\n", person[h].name, person[h].numVotes);
fprintf(out, "\nThe winner(s)\n");
int win = getLargest(person, 1, max);
int winningVote = person[win].numVotes;
for (int h = 1; h <= max; h++)
if (person[h].numVotes == winningVote)
fprintf(out, "%s\n", person[h].name);
} //end printResults
假设需要通过numVotes按降序打印候选人的姓名。为此,必须使用numVotes字段控制排序,以降序对结构数组candidate进行排序。这可以通过以下函数调用来完成:
sortByVote(candidate, 1, MaxCandidates);
sortByVote使用插入排序,并使用形参person(任何名字都可以)编写,如下所示:
void sortByVote(PersonData person[], int lo, int hi) {
//sort person[lo..hi] in descending order by numVotes
PersonData insertItem;
// process person[lo+1] to person[hi]
for (int h = lo + 1; h <= hi; h++) {
// insert person h in its proper position
insertItem = person[h];
int k = h -1;
while (k >= lo && insertItem.numVotes > person[k].numVotes) {
person[k + 1] = person[k];
--k;
}
person[k + 1] = insertItem;
}
} //end sortByVote
请注意,函数的结构与我们对一个简单的整数数组进行排序时非常相似。主要的区别是在while条件中,我们必须指定哪个字段用于确定排序顺序。(在这个例子中,我们也使用了>,而不是person[h],我们将其复制到临时结构insertItem。这释放了person[h],以便在必要时person[h-1]可以移动到位置h。要将数组元素向右移动,我们使用下面的简单赋值:
person[k + 1] = person[k];
这将移动整个结构(在本例中是两个字段)。
如果我们需要按字母顺序对候选人进行排序,我们可以使用函数sortByName:
void sortByName(PersonData person[], int lo, int hi) {
//sort person[lo..hi] in alphabetical order by name
PersonData insertItem;
// process person[lo+1] to person[hi]
for (int h = lo + 1; h <= hi; h++) {
// insert person j in its proper position
insertItem = person[h];
int k = h -1;
while (k > 0 && strcmp(insertItem.name, person[k].name) < 0) {
person[k + 1] = person[k];
--k;
}
person[k + 1] = insertItem;
}
} //end sortByName
函数sortByName与sortByVote相同,除了while条件,该条件指定在比较中使用哪个字段,并使用<按升序排序。注意使用标准字符串函数strcmp来比较两个名字。如果strcmp(s1, s2)为负,则意味着按照字母顺序,字符串s1在字符串s2之前。
作为一个练习,重写解决投票问题的程序,使它按照投票的降序和字母顺序打印结果。
10.10 将结构传递给函数
在投票问题中,我们看到了将结构数组candidate传递给各种函数的例子。我们现在讨论将结构传递给函数时出现的一些其他问题。
考虑具有以下字段的“书籍类型”的结构:
typedef struct {
char author[31];
char title[51];
char binding; //paperback, hardcover, spiral, etc.
double price;
int quantity; //quantity in stock
} Book;
Book text;
这声明了一个名为Book的新类型,而text被声明为Book类型的变量。
我们可以以通常的方式将单个字段传递给函数;对于简单变量,传递它的值,但是对于数组变量,传递它的地址。因此:
fun1(text.quantity); // value of text.quantity is passed
fun2(text.binding); // value of text.binding is passed
fun3(text.price); // value of text.price is passed
但是,
fun4(text.title); // address of array text.title is passed
我们甚至可以通过标题的第一个字母,如下:
fun5(text.title[0]); // value of first letter of title is passed
为了传递整个结构,我们使用这个:
fun6(text);
当然,这些函数的头必须用适当的参数类型编写。
在最后一个例子中,text的字段被复制到一个临时位置(称为运行时堆),副本被传递给fun6;也就是说,该结构是“按值”传递的如果一个结构很复杂或者包含数组,复制操作可能会很耗时。此外,当函数返回时,必须从堆中移除结构元素的值;这增加了开销——执行函数调用所需的额外处理。
为了避免这种开销,可以传递该结构的地址。这可以通过下面的语句来实现:
fun6(&text);
然而,进一步的讨论涉及到指针的更深层次的知识,这超出了本书的范围。
EXERCISES 10Write a program to read names and phone numbers into a structure array. Request a name and print the person’s phone number. Use binary search to look up the name. Write a function that, given two date structures, d1 and d2, returns -1 if d1 comes before d2, 0 if d1 is the same as d2, and 1 if d1 comes after d2. Write a function that, given two date structures, d1 and d2, returns the number of days that d2 is ahead of d1. If d2 comes before d1, return a negative value. A time in 24-hour clock format is represented by two numbers; for example, 16 45 means the time 16:45: that is, 4:45 p.m. Using a structure to represent a time, write a function that, given two time structures, t1 and t2, returns the number of minutes from t1 to t2. For example, if the two given times are 16 45 and 23 25, your function should return 400. Modify the function so that it works as follows: if t2 is less than t1, take it to mean a time for the next day. For example, given the times 20:30 and 6:15, take this to mean 8.30 p.m. to 6.15 a.m. of the next day. Your function should return 585. A length, specified in meters and centimeters, is represented by two integers. For example, the length 3m 75cm is represented by 3 75. Using a structure to represent a length, write functions to compare, add, and subtract two lengths. A file contains the names and distances jumped by athletes in a long-jump competition. Using a structure to hold a name and distance (which is itself a structure as in Exercise 5), write a program to read the data and print a list of names and distance jumped in order of merit (best jumper first). A data file contains registration information for six courses – CS20A, CS21A, CS29A, CS30A, CS35A, and CS36A. Each line of data consists of a seven-digit student registration number followed by six (ordered) values, each of which is 0 or 1. A value of 1 indicates that the student is registered for the corresponding course; 0 means the student is not. Thus, 1 0 1 0 1 1 means that the student is registered for CS20A, CS29A, CS35A, and CS36A, but not for CS21A and CS30A. You may assume that there are no more than 100 students and a registration number 0 ends the data. Write a program to read the data and produce a class list for each course. Each list consists of the registration numbers of those students taking the course. At a school’s bazaar, activities were divided into stalls. At the close of the bazaar, the manager of each stall submitted information to the principal consisting of the name of the stall, the income earned, and its expenses. Here are some sample data: Games 2300.00 1000.00 Sweets 900.00 1000.00 Create a structure to hold a stall’s data Write a program to read the data and print a report consisting of the stall name and net income (income – expenses), in order of decreasing net income (that is, with the most profitable stall first and the least profitable stall last). In addition, print the number of stalls, the total profit or loss of the bazaar, and the stall(s) that made the most profit. Assume that a line containing xxxxxx only ends the data.