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;
}

该程序输出:

image

请注意,交换后,x 和 y 的值已被互换!


选择排序

排序数组的方法多种多样。选择排序可能是最易理解的排序算法,因此尽管它属于较慢的排序算法之一,仍是教学中的理想选择。

选择排序通过以下步骤将数组从小到大排序:

  1. 从数组索引0开始,遍历整个数组寻找最小值
  2. 将找到的最小值与索引0处的值交换
  3. 从下一个索引开始重复步骤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;
}

image

该算法最令人困惑的部分在于嵌套循环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;
}

image

默认情况下,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

将上述选择排序代码改写为降序排序(最大数优先)。虽然看似复杂,实际操作却出奇简单。

image

显示解答

只需更改:
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) 重复前两步直至数组完全排序。

image

请编写代码,根据上述规则对下列数组进行冒泡排序:

#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;
}

image

posted @ 2026-01-14 10:25  游翔  阅读(8)  评论(0)    收藏  举报