《信息学奥林匹克辞典》是由机械工业出版社出版、中国计算机学会组编的一本介绍全国青少年信息学奥林匹克竞赛大纲的书籍。"辞典立足于 NOI 大纲的知识体系,从准确性、学术性和实用性等原则出发,对有关的知识和概念给出了严谨的解析,并在此基础上对所涉及的思想、方法和技巧做了精要的述评,全面涵盖了全国青少年信息学奥林匹克竞赛所考查的计算机科学基础知识、程序设计语言及其环境、数据结构与算法,以及数学和其他内容。"(以上内容引用自此书的前勒口)
这本书有370页,内容覆盖了 CSP-J/S 和 NOI 竞赛大纲。笔者在撰写《CCF GESP 直通车》时,就引用了书中的部分内容。应该说,书中的绝大部分内容都是正确的,但百密一疏,笔者发现了2.4.4.1归并排序中的一个小错误。笔者觉得这个错误不是一般的错误,读者未必能看得出来,所以有必要指出来。但是出版社没有留下任何的联系方式(虽然有几个客服电话,但是笔者觉得这样的错误客服未必能理解并准确地传递给相关人员),于是撰文指出,目的不是为了批评出版社和作者,而是为了指出问题,让其他读者收益,并希望在后续重印时能够修正。
这个错误是第175页,归并排序代码的最后一行(第175页的最后一行)。为了让没有这本书的读者也能理解这个问题,下面贴出代码(第175页的最后一行对应与这里的第17行):
cpp
//函数 mergeSort可完成数组中指定范围内的归并排序
//a:为待排序元素的数组
//b:存放中间结果的临时数组
//l:数组a中待排序元素的最小下标
//r:数组a中待排序元素的最大下标
void mergeSort(int *a, int l, int r) {
if(r == l) //当前元素数量为1,无需处理
return;
int mid = (l + r) >> 1;
mergeSort(a, l, mid); //递归调用,对左半部分元素进行排序
mergeSort(a, mid + 1, r); //递归调用,对右半部分元素进行排序
int i = l, j = mid + 1, cnt = l - 1;
//合并排好序的左右两部分
while (i <= mid && j <= r) {
if (a[i] < a[j])
b[++cnt] = a[i++];
else
b[++cnt] = a[j++];
}
while (i <= mid)
b[++cnt] = a[i++];
while (j <= r)
b[++cnt] = a[j++];
//将排好序的元素依次复制到原数组中
for (int i = l; i <= r; ++i)
a[i] = b[i];
}
这里的第17行为"if (a[i] < a[j])",正确的写法应该是"if (a[i] <= a[j])"。
虽然只差了一个字符,但是前者会导致排序不稳定,后者才是稳定的。
我们知道稳定性是衡量排序好坏的一个重要的指标。归并排序一般认为是稳定的(书中未提及到,这也是再印时需要补充的),但是前提是,代码必须要写对,才能保证排序稳定。书中的代码,当a[i] = a[j] 时,会先选择a[j],而 a[j] 是序列中右半部分的元素,这就会导致相等的元素中,原来在右侧的现在到了左侧,破坏了稳定性。
当然,更好的写法是:
cpp
if (a[j] < a[i])
b[++cnt] = a[j++];
else
b[++cnt] = a[i++];
即仍然使用"<",但把 a[j] 放在左边。这样做的好处是,跟 compare 函数的逻辑一致。我们知道,在 C++ 中,许多排序函数都可以带一个自定义的比较函数。在这个比较函数中,第一个参数位于原序列中的右边,第二个参数位于原序列中的左边。compar 函数的写法一般是这样(假设比较的两个数为 int 类型,排序的要求是升序):
cpp
bool compare(int &a, int &b) {
return (a < b);
}
这里用的就是"<"。
如果在合并时需要使用 compare 函数,则一般是这样:
cpp
if (compare(a[j], a[i]))
b[++cnt] = a[j++];
else
b[++cnt] = a[i++];
对此,大家有什么看法呢?欢迎留言讨论。
本文为学漄乐码堂主撰写。要了解学漄乐码学堂更多的信息,可查看堂主个人简介。