18-1 使用选择排序对数组进行排序
排序的必要性
对数组进行排序,是指将数组中的所有元素按特定顺序排列的过程。在许多不同场景中,数组排序都具有实用价值。例如,电子邮件程序通常按接收时间显示邮件,因为较新的邮件通常被认为更相关。当你查看联系人列表时,姓名通常按字母顺序排列,这样更容易找到目标联系人。这两种呈现方式都涉及数据展示前的排序处理。
排序不仅能提升人类检索效率,对计算机而言同样如此。例如当需要判断某个名字是否出现在名单中时,若未排序则需逐个检查数组元素,对于大型数组而言成本极高。
然而,若假设姓名数组已按字母顺序排序,此时只需搜索至遇到字母排序大于目标姓名的节点。若此时仍未找到目标姓名,即可确定其不存在于数组剩余部分——因为未检查的姓名必然在字母排序上更大!
事实上,针对排序数组存在更高效的搜索算法。通过简单算法,我们仅需20次比较即可完成1,000,000元素排序数组的搜索!当然,排序操作本身成本较高,除非需要频繁搜索,否则通常不值得为提升搜索速度而进行排序。
在某些情况下,排序能彻底消除搜索需求。以寻找最高考试成绩为例:若数组未排序,需遍历所有元素才能找到最高分;若数组已排序,最高分必然位于首尾位置(取决于升序或降序排序),此时根本无需搜索!
排序原理
排序通常通过反复比较数组元素对来实现,当满足预定义条件时交换它们的位置。元素比较的顺序取决于所使用的排序算法,而判断条件则取决于列表的排序方式(例如升序或降序)。
要交换两个元素,可使用C++标准库中的std::swap()函数,该函数定义在实用程序头文件中。
#include <iostream>
#include <utility>
int main()
{
int x{ 2 };
int y{ 4 };
std::cout << "Before swap: x = " << x << ", y = " << y << '\n';
std::swap(x, y); // swap the values of x and y
std::cout << "After swap: x = " << x << ", y = " << y << '\n';
return 0;
}
该程序输出:

请注意,交换后,x 和 y 的值已被互换!
选择排序
排序数组的方法多种多样。选择排序可能是最易理解的排序算法,因此尽管它属于较慢的排序算法之一,仍是教学中的理想选择。
选择排序通过以下步骤将数组从小到大排序:
- 从数组索引0开始,遍历整个数组寻找最小值
- 将找到的最小值与索引0处的值交换
- 从下一个索引开始重复步骤1和2
换言之,我们将找出数组中的最小元素并将其置于首位,接着寻找次小元素置于次位。此过程将持续进行直至数组耗尽。
以下是该算法处理5个元素的示例。首先给出原始数组:
{ 30, 50, 20, 10, 40 }
首先,我们从索引 0 开始找到最小的元素:
{ 30, 50, 20, 10, 40 }
然后我们将其与索引 0 处的元素交换:
{ 10, 50, 20, 30, 40 }
现在第一个元素已排序,我们可以忽略它。现在,我们从索引 1 开始找到最小的元素:
{ 10, 50, 20, 30, 40 }
并将其与索引 1 中的元素交换:
{ 10, 20, 50, 30, 40 }
现在我们可以忽略前两个元素。找到从索引 2 开始的最小元素:
{ 10, 20, 50, 30, 40 }
并将其与索引 2 中的元素交换:
{ 10, 20, 30, 50, 40 }
找到从索引 3 开始的最小元素:
{ 10, 20, 30, 50, 40 }
并将其与索引 3 中的元素交换:
{ 10, 20, 30, 40, 50 }
最后,找到从索引 4 开始的最小元素:
{ 10, 20, 30, 40, 50 }
并将其与索引 4 中的元素交换(不执行任何操作):
{ 10, 20, 30, 40, 50 }
完成!
{ 10, 20, 30, 40, 50 }
请注意,最后一次比较将始终与其自身进行比较(这是多余的),因此我们实际上可以在数组末尾之前停止 1 个元素。
C++中的选择排序
以下是在C++中实现该算法的方法:
#include <iostream>
#include <iterator>
#include <utility>
int main()
{
int array[]{ 30, 50, 20, 10, 40 };
constexpr int length{ static_cast<int>(std::size(array)) };
// Step through each element of the array
// (except the last one, which will already be sorted by the time we get there)
for (int startIndex{ 0 }; startIndex < length - 1; ++startIndex)
{
// smallestIndex is the index of the smallest element we’ve encountered this iteration
// Start by assuming the smallest element is the first element of this iteration
int smallestIndex{ startIndex };
// Then look for a smaller element in the rest of the array
for (int currentIndex{ startIndex + 1 }; currentIndex < length; ++currentIndex)
{
// If we've found an element that is smaller than our previously found smallest
if (array[currentIndex] < array[smallestIndex])
// then keep track of it
smallestIndex = currentIndex;
}
// smallestIndex is now the index of the smallest element in the remaining array
// swap our start element with our smallest element (this sorts it into the correct place)
std::swap(array[startIndex], array[smallestIndex]);
}
// Now that the whole array is sorted, print our sorted array as proof it works
for (int index{ 0 }; index < length; ++index)
std::cout << array[index] << ' ';
std::cout << '\n';
return 0;
}

该算法最令人困惑的部分在于嵌套循环nested loop(即一个循环套在另一个循环内)。外层循环(startIndex)逐个遍历数组元素。每次外层循环迭代时,内层循环(currentIndex)会从剩余数组(从startIndex+1开始)中寻找最小元素。smallestIndex用于记录内层循环找到的最小元素索引。随后将最小索引与起始索引交换。最后外层循环(startIndex)推进一个元素,整个过程循环重复。
提示:若难以理解上述程序逻辑,可尝试在纸上推演示例。将初始(未排序)数组元素横向排列于纸张顶部。绘制箭头标注 startIndex、currentIndex 和 smallestIndex 指向的元素。手动追踪程序执行过程,并随索引变化重新绘制箭头。外层循环每次迭代时,另起一行展示数组的当前状态。
姓名排序采用相同算法。只需将数组类型从 int 改为 std::string,并用相应值初始化即可。
std::sort
由于数组排序操作极为常见,C++标准库提供了名为std::sort的排序函数。该函数位于
#include <algorithm> // for std::sort
#include <iostream>
#include <iterator> // for std::size
int main()
{
int array[]{ 30, 50, 20, 10, 40 };
std::sort(std::begin(array), std::end(array));
for (int i{ 0 }; i < static_cast<int>(std::size(array)); ++i)
std::cout << array[i] << ' ';
std::cout << '\n';
return 0;
}

默认情况下,std::sort 使用 operator< 对元素对进行比较,必要时交换它们的位置,从而实现升序排序(类似于上文选择排序示例的实现方式)。
我们将在后续章节中进一步探讨 std::sort 的相关细节。
测验时间
问题 #1
请手动演示选择排序对以下数组 { 30, 60, 20, 50, 40, 10 } 的操作过程。每次交换后需展示数组状态。
查看解答
30 60 20 50 40 10
**10** 60 20 50 40 **30**
10 **20** **60** 50 40 30
10 20 **30** 50 40 **60**
10 20 30 **40** **50** 60
10 20 30 40 **50** 60 (self-swap)
10 20 30 40 50 **60** (self-swap)
问题 #2
将上述选择排序代码改写为降序排序(最大数优先)。虽然看似复杂,实际操作却出奇简单。

显示解答
只需更改:
if (array[currentIndex] < array[smallestIndex])
为
if (array[currentIndex] > array[smallestIndex])
smallestIndex 可能也应该改名为 largestIndex。
#include <iostream>
#include <iterator> // for std::size
#include <utility>
int main()
{
int array[]{ 30, 50, 20, 10, 40 };
constexpr int length{ static_cast<int>(std::size(array)) }; // C++17
// constexpr int length{ sizeof(array) / sizeof(array[0]) }; // use instead if not C++17 capable
// Step through each element of the array except the last
for (int startIndex{ 0 }; startIndex < length - 1; ++startIndex)
{
// largestIndex is the index of the largest element we've encountered so far.
int largestIndex{ startIndex };
// Search through every element starting at startIndex + 1
for (int currentIndex{ startIndex + 1 }; currentIndex < length; ++currentIndex)
{
// If the current element is larger than our previously found largest
if (array[currentIndex] > array[largestIndex])
// This is the new largest number for this iteration
largestIndex = currentIndex;
}
// Swap our start element with our largest element
std::swap(array[startIndex], array[largestIndex]);
}
// Now print our sorted array as proof it works
for (int index{ 0 }; index < length; ++index)
std::cout << array[index] << ' ';
std::cout << '\n';
return 0;
}
问题 #3
这道题难度较高,请做好准备。
另一种简单排序算法称为“冒泡排序”。其工作原理是比较相邻元素对,若满足交换条件则进行交换,使元素“冒泡”至数组末尾。虽然存在多种优化气泡排序的方法,但本次测验将采用最基础的未优化版本。
未优化的气泡排序通过以下步骤实现数组从小到大排序:
A) 比较数组元素0与元素1。若元素0更大,则与元素1交换位置。
B) 接着对元素1和元素2重复此操作,依次处理后续每对元素直至数组末尾。此时数组末尾元素已完成排序。
C) 重复前两步直至数组完全排序。

请编写代码,根据上述规则对下列数组进行冒泡排序:
#include <iostream>
#include <iterator> // for std::size
#include <utility>
int main()
{
int array[]{ 6, 3, 2, 9, 7, 1, 5, 4, 8 };
constexpr int length{ static_cast<int>(std::size(array)) }; // C++17
// constexpr int length{ sizeof(array) / sizeof(array[0]) }; // use instead if not C++17 capable
// Step through each element of the array (except the last, which will already be sorted by the time we get to it)
for (int iteration{ 0 }; iteration < length-1; ++iteration)
{
// Search through all elements up to the end of the array - 1
// The last element has no pair to compare against
for (int currentIndex{ 0 }; currentIndex < length - 1; ++currentIndex)
{
// If the current element is larger than the element after it, swap them
if (array[currentIndex] > array[currentIndex+1])
std::swap(array[currentIndex], array[currentIndex + 1]);
}
}
// Now print our sorted array as proof it works
for (int index{ 0 }; index < length; ++index)
std::cout << array[index] << ' ';
std::cout << '\n';
return 0;
}
问题 #4
在之前测验题中编写的冒泡排序算法中添加两项优化:
-
注意每次迭代后,剩余的最大数都会被冒泡到数组末尾。首次迭代后,数组末元素已排序;第二次迭代后,倒数第二个元素也排序完毕,以此类推。每次迭代时,我们无需重新检查已知排序的元素。修改循环逻辑,避免重复检查已排序元素。
-
若整个迭代过程中未执行交换操作,则表明数组已处于排序状态。请实现检测机制:判断当前迭代是否发生交换,若未发生则提前终止循环。若循环提前终止,请输出排序提前结束的迭代次数。
输出结果应与以下示例一致:
Early termination on iteration 6
1 2 3 4 5 6 7 8 9
显示解决方案
#include <iostream>
#include <iterator> // for std::size
#include <utility>
int main()
{
int array[]{ 6, 3, 2, 9, 7, 1, 5, 4, 8 };
constexpr int length{ static_cast<int>(std::size(array)) }; // C++17
// constexpr int length{ sizeof(array) / sizeof(array[0]) }; // use instead if not C++17 capable
// Step through each element of the array except the last
for (int iteration{ 0 }; iteration < length-1; ++iteration)
{
// Account for the fact that the last element is already sorted with each subsequent iteration
// so our array "ends" one element sooner
int endOfArrayIndex{ length - iteration };
bool swapped{ false }; // Keep track of whether any elements were swapped this iteration
// Search through all elements up to the end of the array - 1
// The last element has no pair to compare against
for (int currentIndex{ 0 }; currentIndex < endOfArrayIndex - 1; ++currentIndex)
{
// If the current element is larger than the element after it
if (array[currentIndex] > array[currentIndex + 1])
{
// Swap them
std::swap(array[currentIndex], array[currentIndex + 1]);
swapped = true;
}
}
// If we haven't swapped any elements this iteration, we're done early
if (!swapped)
{
// iteration is 0 based, but counting iterations is 1-based. So add 1 here to adjust.
std::cout << "Early termination on iteration: " << iteration+1 << '\n';
break;
}
}
// Now print our sorted array as proof it works
for (int index{ 0 }; index < length; ++index)
std::cout << array[index] << ' ';
std::cout << '\n';
return 0;
}


浙公网安备 33010602011771号