数组排序是计算机科学中的基本算法之一。在实际编程中,我们经常需要对一组数据进行排序,以便更有效地进行搜索、查找或提高算法的性能。本文将深入研究C++中的一些常见数组排序算法,并提供具体的例子来帮助读者理解这些算法的实际应用。
在C++中,有多种排序算法可供选择。我们将介绍冒泡排序、选择排序、插入排序、快速排序和归并排序。以及最后的,如何简单地使用sort函数对数组进行排序。
冒泡排序
冒泡排序是一种基础的排序算法,其核心思想是通过不断地比较和交换相邻元素,使得较大(或较小)的元素逐渐“浮”到数组的顶端。
冒泡排序的算法说明
- 比较相邻元素: 从数组的第一个元素开始,比较相邻的两个元素。
- 交换元素位置: 如果顺序不对(比如,当前元素大于下一个元素),就交换这两个元素的位置。
- 一轮过后的效果: 一轮过后,最大(或最小)的元素就会“冒泡”到数组的末尾。
- 重复进行多轮: 重复以上步骤,每轮都会使数组变得有序,并且确定一个元素的最终位置,直到整个数组有序。
冒泡排序的实现,我们可以使用两个for循环,第一个for循环代表“起泡”的过程,在每一次起泡,都用for循环对相邻的元素进行比较和交换,从而实现排序。
实现冒泡排序的算法如下:
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n-1; i++) {
for (int j = 0; j < n-i-1; j++) {
if (arr[j] > arr[j+1]) {
// 交换元素
int temp = arr[j];//引入第三个变量进行记录
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
冒泡排序算法的简化
在冒泡排序的过程中,由于我们在每轮循环中不断交换最大最小元素的位置,使整个数组逐渐有序,因而,有可能在循环还未结束时,我们的数组就完成了排序,不再进行元素的交换。以数组是否进行元素交换为标志设置bool变量,可以减小计算量。具体实现如下:
void BubbleSort(int arr[], int n) {
bool swapped;
for (int i = 0; i < n-1; i++) {
swapped = false;
for (int j = 0; j < n-i-1; j++) {
if (arr[j] > arr[j+1]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swapped = true;
}
}
// 如果没有发生交换,数组已经有序,提前结束
if (!swapped) {
break;
}
}
}
选择排序
选择排序是一种简单直观的排序算法。它通过在未排序部分选择最小(或最大)元素并将其放置在已排序部分的末尾,直到整个数组有序。
选择排序算法说明
- 初始状态: 将整个数组看作两个部分,已排序部分和未排序部分。初始时,已排序部分为空,未排序部分包含整个数组。
- 选择最小元素: 在未排序部分中找到最小的元素,将其与未排序部分的第一个元素交换位置,这样最小元素就成为已排序部分的最后一个元素。
- 重复过程: 重复上述步骤,在第二次选择,得到未排序数组中最小的元素,相当于当前数组中第二小的元素,将它交换到数组第二的位置。以此类推,选择第i小的元素,将它交换到第i个位置。
同样的,我们可以通过两个for循环实现选择排序,第一个for循环遍历未排序的部分,第二个for循环对未排序部分中的所有元素进行比较,并找到其中的最小值。
具体代码实现如下:
void selectionSort(int arr[], int n) {
for (int i = 0; i < n-1; i++) {
int minIndex = i;//记下当前位置的下标,确定交换的元素
for (int j = i+1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换元素
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
插入排序
插入排序是一种简单直观的排序算法,它通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序算法说明
- 初始状态: 将整个数组看作两个部分,已排序部分和未排序部分。初始时,已排序部分只包含第一个元素,未排序部分包含除第一个元素外的所有元素。
- 逐个插入: 从未排序部分取出一个元素,将其插入到已排序部分的正确位置,使得插入后的序列仍然有序。
- 重复过程: 重复上述步骤,每次从未排序部分取出一个元素并插入到已排序部分,直到整个数组有序。
相比于前面两个基础的排序方式,插入排序的方式难度显然更高。在脑海中模拟插入排序的过程,我们将第n个元素插入前面n-1个元素中,用if条件语句找到了插入的位置,在这之后,我们如何调整数组的位置,使第n个元素变成第i+1个元素?读者不妨先停下来想想。这是实现这个排序方式的难点,也正是这种排序方式的巧妙之处。
在这里,我们先引入变量记下最后一个元素,然后再运用一个while循环,将每个大于插入元素的元素往后放一位。具体代码的实现如下:
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];//记下最后一个元素,使调整元素位置时不被覆盖
int j = i - 1;
// 移动已排序部分的元素,为新元素腾出插入位置
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
// 插入新元素到正确位置
arr[j + 1] = key;
}
}
快速排序
快速排序(Quicksort)是一种高效的分治排序算法,它通过选择一个基准元素将数组分成两部分,分别对两部分进行递归排序。以下是快速排序的详细说明和一个简化实现的示例:
快速排序算法说明
- 选择基准元素: 从数组中选择一个基准元素。通常选择数组的第一个元素,但也可以通过不同的策略选择其他位置的元素。
- 分区过程: 将数组中小于基准元素的元素移到基准元素的左边,将大于基准元素的元素移到右边。基准元素在这个过程中找到了它的最终位置,即数组被分成两个部分。
- 递归排序: 递归地对基准元素左右两侧的子数组进行排序。这样,当所有递归结束时,整个数组就变得有序。
从上面的说明可以看出,快速排序可以分为分区和递归两个过程,我们可以分别为他们编写一个函数。难点显然在于分区过程函数的编写。首先我们需要确定函数的参数,与之前不同的是,通过递归的方法实现排序,我们需要引入low和high两个参数,使分区函数能对每段区域进行排序。接下去是基准函数的选取,以及根据基准元素改变数组顺序。我们这里给出两种方法。
两种分区函数的设计方法
第一种:选取第一个函数为基准元素,然后从末尾开始搜索,找到第一个大于基准元素的数,放入第一个位置,然后从放入的位置后开始搜索,找到第一个大于基准元素的数,放入刚才移出的末尾元素,再继续从末尾搜索,以此类推。当所有元素搜索完毕,最后最中间的位置放入基准元素。这个过程可以用一个嵌套的while循环实现。
int partition(int dp[],int low,int high){
int i=low,j=high;
int k=dp[i];//用第一个元素作为基准元素,空出一个位置,便于交换
do{
while(dp[j]>=k && j>i){
j--;
}
if (dp[j]<k && j>i){
dp[i]=dp[j];
i++;
}
while(dp[i]<=k && i<j){
i++;
}
if (dp[i]>k){
dp[j]=dp[i];
j--;
}
} while(i!=j) ;
dp[i]=k;
return i;
}
第二种: 我们为该数组中找到的比基准元素小的元素设置一个“计数器”,这个计数器最开始的值i=0,然后每找到一个比基准元素小的数,我们就将计数器的值加一,并将第i-1个数换成比基准元素小的数,这样,我们就能保证,前面的i个数都是小于基准元素的,最后,我们在将基准元素放在第i+1个。(因为数组的下标从0开始,编写的时候需要稍微改动)
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j <= high - 1; j++) {
if (arr[j] < pivot) {
i++;
// 交换元素
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 交换基准元素
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
递归函数的编写
注意我们在分区函数中返回值的设置,其实是为了方便递归函数的编写,返回最后的基准元素的下标,让它可以作为递归函数的参数,从而一步步地缩小区间。
void quickSort(int arr[], int low, int high) {
if (low >= high) return;
if (low < high) {
// 找到分区点
int pi = partition(arr, low, high);
// 递归排序分区左右两侧的子数组
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
归并排序
归并排序(Merge Sort)是一种分治算法,它的基本思想是将数组分成两半,分别对每一半进行排序,然后将已排序的两半合并成一个有序数组。归并排序的主要步骤包括拆分(分治)和合并两个已排序的数组。
归并排序算法说明
- 拆分(分治): 将数组拆分为两半,然后对每一半递归地应用归并排序,直到每个子数组都只包含一个元素。
- 合并: 将已排序的两个子数组合并成一个有序数组。合并过程中,比较两个子数组的元素,将较小的元素放入新数组,并移动相应的指针。
- 递归: 递归地对子数组进行拆分和合并,直到整个数组有序。
在归并排序中,与分治排序不同的是,每次归并排序后得到的数组是有序的。我们要做的就是,把两个有序的数组合成一个,而这里,也正是归并排序的数组的巧妙之处。模仿上面的分治排序的第一种方法,你能否想到归并排序算法的设计方法?是的,我们可以用相同的思路,在两个数组之间反复跳跃,找到当前最小的元素,具体实现如下:
void merge(int arr[], int low, int mid, int high) {
int n1 = mid - low + 1;
int n2 = high - mid;
// 创建临时数组
int left[n1];
int right[n2];
// 将数据复制到临时数组 left[] 和 right[]
for (int i = 0; i < n1; i++)
left[i] = arr[low + i];
for (int j = 0; j < n2; j++)
right[j] = arr[mid + 1 + j];
// 合并临时数组 back 到 arr[low..high]
int i = 0; // 初始左半部分的索引
int j = 0; // 初始右半部分的索引
int k = low; // 初始合并数组的索引
while (i < n1 && j < n2) {
if (left[i] <= right[j]) {
arr[k] = left[i];
i++;
} else {
arr[k] = right[j];
j++;
}
k++;
}
// 复制剩余的元素(如果有的话)
while (i < n1) {
arr[k] = left[i];
i++;
k++;
}
while (j < n2) {
arr[k] = right[j];
j++;
k++;
}
}
void mergeSort(int arr[], int low, int high) {
if (low < high) {
// 计算中间位置
int mid = low + (high - low) / 2;
// 递归排序左右两侧
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
// 合并两个已排序的部分
merge(arr, low, mid, high);
}
}
使用sort函数进行排序
在C++中,你可以使用sort
函数进行排序。sort
函数是C++标准库中提供的排序算法,可以用于对数组、向量(vector
)等进行排序。使用时要包含sort 函数的头文件。以下是一个简单的示例:
#include <iostream>
#include <vector>
#include <algorithm> // 包含 sort 函数的头文件
using namespace std;
int main() {
// 示例数组
int arr[] = {5, 2, 9, 1, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
// 使用 sort 对数组进行升序排序
sort(arr, arr + n);
// 打印排序后的数组
cout << "Sorted array: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
// 示例向量
vector<int> vec = {5, 2, 9, 1, 5, 6};
// 使用 sort 对向量进行升序排序
sort(vec.begin(), vec.end());
// 打印排序后的向量
cout << "Sorted vector: ";
for (int x : vec) {
cout << x << " ";
}
cout << std::endl;
return 0;
}
sort函数的参数
template<class RandomIt>
void sort(RandomIt first, RandomIt last);
这里的 RandomIt
是一个模板参数,代表一个迭代器类型,可以是指向数组元素的指针,也可以是 STL 容器(如 std::vector
、std::list
)的迭代器。
first
是排序范围的起始位置(指向第一个元素的迭代器)。last
是排序范围的终止位置(指向排序范围之后的位置的迭代器)。
在对数组的排序中,数组的名称表示的就是数组首元素的位置。因此,可以用sort(arr, arr + n)对数组进行排序。
用sort对数组进行降序排序
sort
还提供了其他形式,允许你传递一个比较函数或 Lambda 表达式,以自定义排序规则。例如:
template<class RandomIt, class Compare>
void sort(RandomIt first, RandomIt last, Compare comp);
其中,Compare comp
是 sort
函数的模板参数,用于指定排序的比较方式。它允许你传递一个自定义的比较函数或者 Lambda 表达式,来定义元素之间的顺序。
具体而言,Compare
是一个模板类型参数,代表比较函数的类型。该比较函数或 Lambda 表达式应该接受两个参数,通常是容器中的元素类型,然后返回一个布尔值表示它们的相对顺序。如果返回值为 true
,则表示第一个参数应该排在第二个参数之前;如果返回值为 false
,则表示它们的相对顺序是相反的。具体的使用方法如下:
// 使用 sort 对数组进行降序排序
sort(arr, arr + n, greater<int>());
//greater<int>() 是一个函数对象,表示按照降序排序
sort(arr, arr + n, [](int a, int b) { return a > b; });
//使用Lambda表达式
最后:关于这些排序的一点总结
性能比较
在实际应用中,选择合适的排序算法是很重要的。下表展示了各个排序算法的时间复杂度:
算法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
快速排序 | O(n log n) | O(n^2) | O(n log n) | O(log n) | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
最佳实践和建议
- 对于小规模数据或部分有序数据,插入排序可能更适合。
- 对于大规模数据,快速排序和归并排序通常是更好的选择。
- 考虑数据的稳定性需求,选择稳定的排序算法。
为什么在有sort函数的情况下,我们还研究数组排序:
研究数组排序算法的重要性有几个方面:
- 理解排序算法的工作原理:学习排序算法有助于理解算法设计和分析的基本原则。排序算法通常涉及到算法的基本思想、逻辑结构、时间复杂度和空间复杂度等概念。通过理解这些原理,你可以更好地理解和设计其他算法。
- 适应不同场景: 虽然标准库提供了排序函数,但了解不同的排序算法可以帮助你选择适合特定场景的算法。有些算法对于小规模数据集或者部分有序的数据表现更好,而另一些算法对于大规模乱序数据可能更为适用。
- 面试和算法竞赛:在计算机科学领域的面试和算法竞赛中,对各种排序算法的了解通常是必备的。掌握不同的排序算法,包括它们的优缺点、时间复杂度和空间复杂度,可以提高你在这些场景中的表现。
总的来说,尽管标准库中提供了方便的排序函数,但深入了解不同的排序算法仍然是编程和算法学习过程中的有益补充。这有助于拓宽对算法设计和优化的理解,并在实际问题中做出更好的选择。
全文终。