title: 数据结构:常见排序算法解析
author: shuo-liu
date: 2024-6-15
categories: 算法
tags:
- 数据结构
- 排序算法
- C++
因为我经常忘记各种排序算法的思路,所以我写了这篇文章。文章中使用了结构体数组来实现排序,已经完成了快排、希尔等多种排序方法,值得一提的是,我采用了 arr[0] = {}
这样的方式来置空数组,并将其作为标兵来使用。
排序算法
(1)直接插入排序;
(2)折半插入排序;
(3)冒泡排序;
(4)简单选择排序。
(5)希尔排序;
(6)快速排序。
首先我先定义一下排序需要用到的…ababab
struct Student {
string name;
int score;
};
//打印排序后的数组
void printStudents(const Student students[], int size) {
for(int i = 1; i < size; i++) {
cout << "姓名:" << students[i].name << " 成绩:" << students[i].score << endl;
}
cout << endl;
}
int main() {
Student students[] = {
{},
{"aaa", 87},
{"bbb", 76},
{"ccc", 92},
{"ddd", 64},
{"eee", 55},
{"fff", 78},
{"ggg", 100},
{"hhh", 43},
};
int size = sizeof(students) / sizeof(students[1]);
printStudents(students, size);
// insert(students, size);
// binary(students, size);
// bubble(students, size);
// selection(students, size);
// quickSort(students, 1, size);
// cout << "快速排序:" << endl;
// printStudents(students, size);
shell(students, size, size);
return 0;
}
直接插入排序
算法步骤
首先取第一个元素假设这个范围内有序(升序),
接着扩大这个范围,由1到n,
始终保持范围内有序,最终完成排序。
void insert(Student students[], int size) {
//这里假设前i-1个元素有序
for(int i = 2; i < size; i++) {
//中转变量,存储即将进入有序区域的数据
Student tmp = students[i];
int j = i-1;
//这里 j = 0 时,students[j].score > tmp.score 必定为false
while(j >= 0 && students[j].score > tmp.score) {
students[j + 1] = students[j];
j--;
}
students[j + 1] = tmp;
}
cout << "直接插入排序:" << endl;
printStudents(students, size);
}
算法分析
a.算法性能
时间复杂度:平均(O(n^2))
空间复杂度:额外空间复杂度是 ( O(1) )
b.稳定性(稳定性是指排序过程中相同元素的相对位置不变)
直接插入排序是稳定的排序算法,因此如果原始数组中存在相同值的元素,在排序后它们的相对顺序不会改变。
c.适用性
相比较于其他 ( O(n^2) ) 的排序算法(如冒泡排序和选择排序),直接插入排序在一般情况下效率更高,特别是在数据部分有序的情况下。
然而,对于大规模数据或需要快速排序的情况,更高效的排序算法(如快速排序、归并排序等 ( O(n \log n) ) 的算法)更为合适。
d.总结
直接插入排序虽然简单,但其性能上不如快速排序等 ( O(n \log n) ) 级别的排序算法。然而,它易于实现,在小规模数据或者数据接近有序时,可以是一个不错的选择。
折半插入排序算法
算法步骤
折半插入排序与直接插入排序类似,但在寻找插入位置时采用了二分查找的方式,以提高查找插入位置的效率。
void binary(Student students[], int size) {
for(int i = 2; i < size; i++) {
Student tmp = students[i];
int left = 1;
int right = i-1;
while(left <= right) {
//这样可以防止溢出,虽然这里用不到吧
int mid = left + (right - left) / 2;
if(students[mid].score > tmp.score) {
right = mid - 1;
} else {
left = mid + 1;
}
}
//虽然这样查找的比较快,但是还是要遍历
for(int j = i; j > left; j--) {
students[j] = students[j-1];
}
students[left] = tmp;
}
cout << "折半插入排序:" << endl;
printStudents(students, size);
}
算法分析
a. 算法性能
时间复杂度:平均情况下 ( O(n^2) ),虽然查找插入位置的过程利用了二分查找,但插入操作仍然是 ( O(n) )。
空间复杂度:额外空间复杂度为 ( O(1) )
b. 稳定性
折半插入排序同样是稳定的排序算法,相同元素的相对位置不会改变。
c. 适用性
折半插入排序适用于需要稳定排序且对空间复杂度有要求的场景,特别是当直接插入排序在查找插入位置上效率较低时,折半插入排序可以提供更好的性能。
d. 总结
折半插入排序相比直接插入排序,在查找插入位置时能够更快地定位,但整体的时间复杂度仍然是 ( O(n^2) ),因此对于大规模数据或需要更高效排序算法的情况,应考虑使用 ( O(n \log n) ) 级别的排序算法,如快速排序或归并排序。
冒泡排序算法
算法步骤
从第一个元素开始,依次比较相邻的两个元素,如果顺序不对则交换它们。
继续对每一对相邻元素进行同样的操作,直到最后一个元素。
重复以上步骤,每次都将未排序部分中最大的元素"浮"到最后,直到整个序列有序。
void bubble(Student students[], int size) {
for(int i = 1; i < size; i++) {
for(int j = 1; j < size - i; j++) {
if(students[j].score > students[j+1].score) {
swap(students[j], students[j+1]);
}
}
// printStudents(students, size);
}
cout << "冒泡排序:" << endl;
printStudents(students, size);
}
算法分析
a. 算法性能
时间复杂度:O(n^2)
空间复杂度:额外空间复杂度为 ( O(1) )
b. 稳定性
冒泡排序是稳定的排序算法。在比较相邻元素时,如果他们的值相等,则不进行交换,因此相等的元素在排序后的相对位置保持不变。
c. 总结
由于其实现简单且易于理解,在一些教学场景中仍然被广泛使用。实际并不常用。
简单选择排序
算法步骤
首先在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置
然后,再从剩余未排序元素中继续寻找最小(或最大)元素,放到已排序序列的末尾
以此类推,直到所有元素均排序完毕
void selection(Student students[], int size) {
for(int i = 1; i < size - 1; i++) {
int min_score = i;
for(int j = i+1; j < size; j++) {
if(students[min_score].score > students[j].score) {
min_score = j;
}
}
swap(students[i], students[min_score]);
}
cout << "简单选择排序:" << endl;
printStudents(students, size);
}
算法分析
a. 算法性能
时间复杂度:O(n^2)
空间复杂度:额外空间复杂度为 ( O(1) )
b. 稳定性
简单选择排序是不稳定的排序算法。在寻找最小(或最大)元素的过程中,如果有多个相等的元素,那么可能会破坏它们之间的相对顺序。
c. 总结
由于其实现简单且易于理解,在一些教学场景中仍然被广泛使用。实际并不常用。
希尔排序
算法步骤
选择增量序列:希尔排序的关键在于选择合适的增量序列(也称为间隔序列),初始时,选择一个增量h(通常选择数组长度的一半,即 h = n / 2,n为数组长度),然后逐步减小增量。
分组排序:对于当前的增量h,将待排序的数组分成h个长度为n/h的子数组(如果n不能被h整除,则最后一个子数组的长度会小于n/h)。
对每个子数组进行直接插入排序。此时,由于每个子数组中的元素间隔为h,因此只需要考虑每隔h个元素的比较和移动。
逐步减小增量:在完成一轮分组排序后,减小增量h的值(如 h = h / 2),并重复步骤2,直到h变为1。
最后排序:当h变为1时,整个数组被分为n个长度为1的子数组(即每个元素自成一组)。此时,再对整个数组进行一次直接插入排序,即可得到完全有序的数组。
void shell(Student students[], int size, int n) {
// 逐步缩小间隔直到间隔为1
for (int gap = n / 2; gap > 0; gap /= 2) {
// 使用间隔进行插入排序
for (int i = gap; i < n; i++) {
Student tmp = students[i];
int j;
// 在当前间隔内进行插入排序·
for (j = i; j >= gap && students[j - gap].score > tmp.score; j -= gap) {
students[j] = students[j - gap];
}
students[j] = tmp;
}
}
cout << "希尔排序:" << endl;
printStudents(students, size);
}
算法分析
a. 算法性能
时间复杂度:希尔排序的时间复杂度很难准确计算,因为它与数据序列的初始状态以及增量序列的选择都有关。
空间复杂度:额外空间复杂度为 ( O(1) )
b. 稳定性
希尔排序是不稳定的排序算法。在增量大于1的排序过程中,相同元素的相对位置可能会发生变化
c. 总结
希尔排序在实际应用中并不常见,因为它通常不如快速排序、归并排序等算法高效。希尔排序的优点是代码实现简单,且在某些特定情况下(如数据基本有序时)效率较高。
快速排序
算法步骤
选择基准(Pivot):从待排序的序列中选取一个元素作为基准(pivot),通常选择序列的第一个或最后一个元素,或者随机选择。
分割过程(Partition):将待排序的序列重新排列,所有比基准值小的元素放在基准的前面,所有比基准值大的元素放在基准的后面。在这个分割过程之后,基准元素处于序列的中间位置(左侧的所有元素都比它小,右侧的所有元素都比它大)。这个过程称为一次划分(Partition)。
递归处理:递归地对基准元素左右两侧的子序列进行快速排序,直到每个子序列的长度为1(即已经有序),递归结束。
int quick(Student students[], int left, int right) {
Student tmp = students[left];
while(left < right) {
while(left < right && students[right].score >= tmp.score)
right--;
students[left] = students[right];
while(left < right && students[left].score <= tmp.score)
left++;
students[right] = students[left];
}
students[left] = tmp;
return left;
}
void quickSort(Student students[], int left, int right) {
if(left < right) {
int pi = quick(students, left, right);
quickSort(students, left, pi - 1);
quickSort(students, pi + 1, right);
}
}
算法分析
a. 算法性能
时间复杂度:O(n log n)
空间复杂度:额外空间复杂度为 O(log n)。
b. 稳定性
快速排序是不稳定的排序算法,即相等的元素在排序后可能会改变它们的相对顺序。
c. 总结
快速排序算法由于其高效性和广泛的适用性,在多个领域都有着广泛的应用。
代码全貌
#include <bits/stdc++.h>
using namespace std;
struct Student {
string name;
int score;
};
void printStudents(const Student students[], int size) {
for(int i = 1; i < size; i++) {
cout << "姓名:" << students[i].name << " 成绩:" << students[i].score << endl;
}
cout << endl;
}
void insert(Student students[], int size) {
for(int i = 2; i < size; i++) {
Student tmp = students[i];
int j = i-1;
while(j >= 0 && students[j].score > tmp.score) {
students[j + 1] = students[j];
j--;
}
students[j + 1] = tmp;
}
cout << "直接插入排序:" << endl;
printStudents(students, size);
}
void binary(Student students[], int size) {
for(int i = 2; i < size; i++) {
Student tmp = students[i];
int left = 1;
int right = i-1;
while(left <= right) {
int mid = left + (right - left) / 2;
if(students[mid].score > tmp.score) {
right = mid - 1;
} else {
left = mid + 1;
}
}
for(int j = i; j > left; j--) {
students[j] = students[j-1];
}
students[left] = tmp;
}
cout << "折半插入排序:" << endl;
printStudents(students, size);
}
void bubble(Student students[], int size) {
for(int i = 1; i < size; i++) {
for(int j = 1; j < size - i; j++) {
if(students[j].score > students[j+1].score) {
swap(students[j], students[j+1]);
}
}
// printStudents(students, size);
}
cout << "冒泡排序:" << endl;
printStudents(students, size);
}
void selection(Student students[], int size) {
for(int i = 1; i < size - 1; i++) {
int min_score = i;
for(int j = i+1; j < size; j++) {
if(students[min_score].score > students[j].score) {
min_score = j;
}
}
swap(students[i], students[min_score]);
}
cout << "简单选择排序:" << endl;
printStudents(students, size);
}
void shell(Student students[], int size, int n) {
// 逐步缩小间隔直到间隔为1
for (int gap = n / 2; gap > 0; gap /= 2) {
// 使用间隔进行插入排序
for (int i = gap; i < n; i++) {
Student tmp = students[i];
int j;
// 在当前间隔内进行插入排序
for (j = i; j >= gap && students[j - gap].score > tmp.score; j -= gap) {
students[j] = students[j - gap];
}
students[j] = tmp;
}
}
cout << "希尔排序:" << endl;
printStudents(students, size);
}
int quick(Student students[], int left, int right) {
Student tmp = students[left];
while(left < right) {
while(left < right && students[right].score >= tmp.score)
right--;
students[left] = students[right];
while(left < right && students[left].score <= tmp.score)
left++;
students[right] = students[left];
}
students[left] = tmp;
return left;
}
void quickSort(Student students[], int left, int right) {
if(left < right) {
int pi = quick(students, left, right);
quickSort(students, left, pi - 1);
quickSort(students, pi + 1, right);
}
}
int main() {
Student students[] = {
{},
{"aaa", 87},
{"bbb", 76},
{"ccc", 92},
{"ddd", 64},
{"eee", 55},
{"fff", 78},
{"ggg", 100},
{"hhh", 43},
};
int size = sizeof(students) / sizeof(students[1]);
printStudents(students, size);
// insert(students, size);
// binary(students, size);
// bubble(students, size);
// selection(students, size);
// quickSort(students, 1, size);
// cout << "快速排序:" << endl;
// printStudents(students, size);
shell(students, size, size);
return 0;
}